@massu/core 1.2.1 → 1.4.0-soak.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/README.md +40 -0
- package/commands/README.md +137 -0
- package/commands/massu-deploy.python-docker.md +170 -0
- package/commands/massu-deploy.python-fly.md +189 -0
- package/commands/massu-deploy.python-launchd.md +144 -0
- package/commands/massu-deploy.python-systemd.md +163 -0
- package/commands/massu-deploy.python.md +200 -0
- package/commands/massu-scaffold-page.md +172 -59
- package/commands/massu-scaffold-page.swift.md +121 -0
- package/commands/massu-scaffold-router.python-django.md +153 -0
- package/commands/massu-scaffold-router.python-fastapi.md +145 -0
- package/commands/massu-scaffold-router.python.md +143 -0
- package/dist/cli.js +10170 -4138
- package/dist/hooks/auto-learning-pipeline.js +44 -6
- package/dist/hooks/classify-failure.js +44 -6
- package/dist/hooks/cost-tracker.js +44 -6
- package/dist/hooks/fix-detector.js +44 -6
- package/dist/hooks/incident-pipeline.js +44 -6
- package/dist/hooks/post-edit-context.js +44 -6
- package/dist/hooks/post-tool-use.js +44 -6
- package/dist/hooks/pre-compact.js +44 -6
- package/dist/hooks/pre-delete-check.js +44 -6
- package/dist/hooks/quality-event.js +44 -6
- package/dist/hooks/rule-enforcement-pipeline.js +44 -6
- package/dist/hooks/session-end.js +44 -6
- package/dist/hooks/session-start.js +4789 -410
- package/dist/hooks/user-prompt.js +44 -6
- package/package.json +10 -4
- package/src/cli.ts +28 -2
- package/src/commands/config-refresh.ts +88 -20
- package/src/commands/init.ts +130 -23
- package/src/commands/install-commands.ts +482 -42
- package/src/commands/refresh-log.ts +37 -0
- package/src/commands/show-template.ts +65 -0
- package/src/commands/template-engine.ts +262 -0
- package/src/commands/watch.ts +430 -0
- package/src/config.ts +69 -3
- package/src/detect/adapters/nextjs-trpc.ts +166 -0
- package/src/detect/adapters/parse-guard.ts +133 -0
- package/src/detect/adapters/python-django.ts +208 -0
- package/src/detect/adapters/python-fastapi.ts +223 -0
- package/src/detect/adapters/query-helpers.ts +170 -0
- package/src/detect/adapters/runner.ts +252 -0
- package/src/detect/adapters/swift-swiftui.ts +171 -0
- package/src/detect/adapters/tree-sitter-loader.ts +348 -0
- package/src/detect/adapters/types.ts +174 -0
- package/src/detect/codebase-introspector.ts +190 -0
- package/src/detect/index.ts +28 -2
- package/src/detect/regex-fallback.ts +449 -0
- package/src/hooks/session-start.ts +94 -3
- package/src/lib/gitToplevel.ts +22 -0
- package/src/lib/installLock.ts +179 -0
- package/src/lib/pidLiveness.ts +67 -0
- package/src/lsp/auto-detect.ts +89 -0
- package/src/lsp/client.ts +590 -0
- package/src/lsp/enrich.ts +127 -0
- package/src/lsp/types.ts +221 -0
- package/src/watch/daemon.ts +385 -0
- package/src/watch/lockfile-detector.ts +65 -0
- package/src/watch/paths.ts +279 -0
- package/src/watch/state.ts +178 -0
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# @massu/core
|
|
2
|
+
|
|
3
|
+
AI Engineering Governance MCP Server — session memory, feature registry, code intelligence, and rule enforcement for AI coding assistants.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx massu init
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
This sets up the MCP server, configuration, and lifecycle hooks in one command.
|
|
12
|
+
|
|
13
|
+
## What is Massu?
|
|
14
|
+
|
|
15
|
+
Massu is a source-available [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that adds governance capabilities to AI coding assistants like Claude Code. It provides:
|
|
16
|
+
|
|
17
|
+
- **73 MCP Tools** — quality analytics, cost tracking, security scoring, dependency analysis, and more
|
|
18
|
+
- **15 Lifecycle Hooks** — pre-commit gates, security scanning, intent suggestion, session management, and auto-learning pipeline
|
|
19
|
+
- **3-Database Architecture** — code graph (read-only), data (imports/mappings), and memory (sessions/analytics)
|
|
20
|
+
- **Config-Driven** — all project-specific data lives in `massu.config.yaml`
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
After `npx massu init`, your AI assistant gains access to all governance tools automatically via the MCP protocol.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# Health check
|
|
28
|
+
npx massu doctor
|
|
29
|
+
|
|
30
|
+
# Validate configuration
|
|
31
|
+
npx massu validate-config
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Documentation
|
|
35
|
+
|
|
36
|
+
Full documentation at [massu.ai](https://massu.ai).
|
|
37
|
+
|
|
38
|
+
## License
|
|
39
|
+
|
|
40
|
+
[BSL 1.1](https://github.com/massu-ai/massu/blob/main/LICENSE) — source-available. Free to use, modify, and distribute. See LICENSE for full terms.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Massu Slash-Command Templates
|
|
2
|
+
|
|
3
|
+
This directory ships the slash commands installed into a consumer project's `<claudeDirName>/commands/` (default `.claude/commands/`) when the consumer runs `npx @massu/core install-commands` (or `npx @massu/core init`, which calls install-commands as part of its flow).
|
|
4
|
+
|
|
5
|
+
As of `@massu/core@1.3.0`, command files are **stack-aware**: a template can ship one or more language-specific variants alongside the default, and the installer picks the variant that matches the consumer's `massu.config.yaml`. Local edits are preserved across reinstalls via a manifest written to `<claudeDirName>/.massu/install-manifest.json`.
|
|
6
|
+
|
|
7
|
+
This README covers:
|
|
8
|
+
|
|
9
|
+
1. The variant filename convention.
|
|
10
|
+
2. The variant-resolution algorithm (priority order + tie-break).
|
|
11
|
+
3. The local-edit-protection manifest.
|
|
12
|
+
4. The `npx @massu/core show-template <name>` debugging command.
|
|
13
|
+
|
|
14
|
+
## 1. Variant filename convention
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
<base>.md # default — used when no variant matches
|
|
18
|
+
<base>.python.md # FastAPI / Django / generic Python (one-axis)
|
|
19
|
+
<base>.python-fastapi.md # Python + FastAPI (two-axis: lang + sub-framework)
|
|
20
|
+
<base>.python-django.md # Python + Django (two-axis: lang + sub-framework)
|
|
21
|
+
<base>.swift.md # SwiftUI / iOS / visionOS
|
|
22
|
+
<base>.rust.md # axum / actix / generic Rust
|
|
23
|
+
<base>.typescript.md # reserved for future use; currently no variants ship
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The two-axis form (`<base>.<lang>-<framework>.md`) is tried BEFORE the one-axis (`<base>.<lang>.md`) form. It is selected when the consumer's config declares `framework.languages.<lang>.framework = "<sub-framework>"` (e.g. `fastapi` or `django`).
|
|
27
|
+
|
|
28
|
+
The variant convention applies **only at the top level** of `packages/core/commands/`. Files inside subdirectories (e.g., `_shared-references/`, `massu-loop/references/`) are copied recursively as-is — no variant resolution, no dot-skip filtering. Future authors can use dotted filenames in nested dirs without losing them to the variant filter.
|
|
29
|
+
|
|
30
|
+
The variant convention is **opt-in**: a template that does NOT ship any `<base>.<variant>.md` siblings is variant-agnostic and the unsuffixed `<base>.md` is used everywhere.
|
|
31
|
+
|
|
32
|
+
## 2. Variant-resolution algorithm
|
|
33
|
+
|
|
34
|
+
Given a base template name `B` and the consumer's `framework` config from `massu.config.yaml`:
|
|
35
|
+
|
|
36
|
+
1. **Build the candidate list `V`** in priority order:
|
|
37
|
+
1. `framework.primary` (or `framework.type` if `primary` is undefined). Skipped if the value is `"multi"`.
|
|
38
|
+
2. Each declared `framework.languages.<lang>` entry whose `framework` field is a non-empty string, in **YAML declaration order** (first-declared wins on ties).
|
|
39
|
+
3. **Passthrough fallback**: well-known top-level `framework.<lang>` blocks (typescript / javascript / python / swift / rust / go) with a non-empty `framework` field, in fixed order, excluding any language already covered. This is what lets a project that declares `framework.swift` at the top level (alongside `framework.languages.python`) still pick up the `.swift.md` variant when the languages block doesn't contain swift.
|
|
40
|
+
4. The unsuffixed default (`""`) as the last fallback.
|
|
41
|
+
2. **Probe disk** in order: for each candidate `c`, check whether `<base>.<c>.md` exists in the bundled commands directory.
|
|
42
|
+
3. **Return**:
|
|
43
|
+
- First hit → copy that file.
|
|
44
|
+
- No hit AND `framework.type === "multi"` AND `framework.primary` is undefined → write a one-line warning to stderr and copy the unsuffixed default.
|
|
45
|
+
- No hit otherwise → skip the file (this only happens if the base template was deleted and only orphan variants remain, which the Phase 0 audit prevents).
|
|
46
|
+
|
|
47
|
+
The target filename in the consumer dir is always the BASE name (`<base>.md`) — variant suffixes are an internal package detail.
|
|
48
|
+
|
|
49
|
+
### Tie-break — which variant wins?
|
|
50
|
+
|
|
51
|
+
If a consumer declares both `python` and `swift` in `framework.languages` AND a template ships both `.python.md` and `.swift.md`, the variant declared FIRST in `framework.languages` wins. Consumers control this by reordering keys in `massu.config.yaml`. Passthrough-fallback entries (rule 1.3) are appended AFTER all `framework.languages` entries.
|
|
52
|
+
|
|
53
|
+
There is no per-template override mechanism. If you need one, file an issue.
|
|
54
|
+
|
|
55
|
+
## 3. Local-edit protection — the manifest
|
|
56
|
+
|
|
57
|
+
`@massu/core@1.3.0` introduces a manifest at:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
<consumer-root>/<claudeDirName>/.massu/install-manifest.json
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
(where `claudeDirName` is the value of `conventions.claudeDirName` in `massu.config.yaml`, defaulting to `.claude`).
|
|
64
|
+
|
|
65
|
+
Each entry maps an asset-relative path (e.g., `commands/massu-scaffold-router.md`) to the SHA-256 of its content at the last successful install.
|
|
66
|
+
|
|
67
|
+
### What the installer does on each run
|
|
68
|
+
|
|
69
|
+
For each file `<asset>` to be installed, three hashes are computed:
|
|
70
|
+
|
|
71
|
+
| Hash | What it represents |
|
|
72
|
+
|------|--------------------|
|
|
73
|
+
| `sourceHash` | The bundled upstream content NOW. |
|
|
74
|
+
| `existingHash` | The current consumer file content. `undefined` if missing. |
|
|
75
|
+
| `lastInstalledHash` | The hash recorded in the manifest. `undefined` on first install. |
|
|
76
|
+
|
|
77
|
+
Decision matrix:
|
|
78
|
+
|
|
79
|
+
| Condition | Action | Counter |
|
|
80
|
+
|-----------|--------|---------|
|
|
81
|
+
| target missing | write upstream, record `sourceHash` in manifest | `installed` |
|
|
82
|
+
| `existingHash === sourceHash` | already in sync; record `sourceHash` (covers manifest healing) | `skipped` |
|
|
83
|
+
| `lastInstalledHash` undefined AND `existingHash !== sourceHash` | first-install heuristic: keep the consumer file, record `existingHash`, print a one-line notice | `kept` |
|
|
84
|
+
| `existingHash !== lastInstalledHash` (consumer edited it) | preserve, print `kept your version` notice + diff hint | `kept` |
|
|
85
|
+
| `existingHash === lastInstalledHash` AND `sourceHash !== lastInstalledHash` (clean upstream upgrade) | write upstream, record `sourceHash` | `updated` |
|
|
86
|
+
|
|
87
|
+
The manifest is written **atomically** (`tempfile + renameSync`) so a crash mid-install never leaves a partially-written manifest.
|
|
88
|
+
|
|
89
|
+
### What this means for you
|
|
90
|
+
|
|
91
|
+
- Edit your `<claudeDirName>/commands/*.md` freely. The installer will not stomp your edits.
|
|
92
|
+
- To accept the upstream version of a file you've edited: delete it and rerun `install-commands`.
|
|
93
|
+
- To diff your version against upstream:
|
|
94
|
+
```bash
|
|
95
|
+
diff .claude/commands/massu-scaffold-router.md \
|
|
96
|
+
<(npx @massu/core show-template massu-scaffold-router)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## 4. `npx @massu/core show-template <name>`
|
|
100
|
+
|
|
101
|
+
Prints the resolved template content (post-variant-resolution) to stdout. Used in the diff one-liner above. Accepts both `massu-scaffold-router` and `massu-scaffold-router.md`. Exits 1 on unknown names.
|
|
102
|
+
|
|
103
|
+
## 5. Currently shipped variants
|
|
104
|
+
|
|
105
|
+
| Base | Variants |
|
|
106
|
+
|------|----------|
|
|
107
|
+
| `massu-scaffold-router` | `.python-fastapi.md` (FastAPI — two-axis), `.python-django.md` (Django — two-axis) |
|
|
108
|
+
| `massu-scaffold-page` | `.swift.md` (SwiftUI), regenerated default with embedded Next.js / FastAPI / SwiftUI / Rust examples |
|
|
109
|
+
| `massu-deploy` | `.python-launchd.md`, `.python-systemd.md`, `.python-docker.md`, `.python-fly.md` (all two-axis) |
|
|
110
|
+
|
|
111
|
+
All other top-level templates ship as variant-agnostic defaults (one `<base>.md`).
|
|
112
|
+
|
|
113
|
+
### Template variable substitution
|
|
114
|
+
|
|
115
|
+
As of `@massu/core@1.3.0`, template files may contain `{{variable.path}}` and `{{variable.path | default("fallback")}}` placeholders. These are rendered against the consumer's `massu.config.yaml` (all `framework.*`, `paths.*`, and `config.*` fields) plus the `detected.*` block written by the codebase introspector (e.g. `detected.python.auth_dep`, `detected.swift.api_client_class`).
|
|
116
|
+
|
|
117
|
+
Rules:
|
|
118
|
+
- Every `{{detected.*}}` reference MUST include a `| default("...")` fallback — introspection may return nothing.
|
|
119
|
+
- Use `\{{` to emit a literal `{{` in the rendered output (e.g. Go/Docker format strings).
|
|
120
|
+
- A render error on a single file causes that file to be skipped (stderr message only); the rest of the install continues.
|
|
121
|
+
|
|
122
|
+
Pass `--skip-commands` to `massu init` or `massu refresh` to suppress command installation entirely.
|
|
123
|
+
|
|
124
|
+
## 6. Adding a new variant
|
|
125
|
+
|
|
126
|
+
1. Create `<base>.<lang>.md` in this directory using the same frontmatter shape as the default.
|
|
127
|
+
2. The default `<base>.md` should remain generic (or be regenerated if it was previously stack-specific — see `massu-scaffold-page.md` for the pattern of an embedded multi-stack default).
|
|
128
|
+
3. Add a row to the table in section 5.
|
|
129
|
+
4. Add a test case to `packages/core/src/__tests__/install-commands.test.ts` that exercises the new variant against a fixture config.
|
|
130
|
+
5. Update `docs/internal/2026-04-26-template-variant-audit.md` (the audit doc) with the new label.
|
|
131
|
+
|
|
132
|
+
## 7. Reference
|
|
133
|
+
|
|
134
|
+
- Plan: `/Users/ekoultra/hedge/docs/plans/2026-04-26-massu-stack-aware-command-templates.md`
|
|
135
|
+
- Audit: `docs/internal/2026-04-26-template-variant-audit.md`
|
|
136
|
+
- Implementation: `packages/core/src/commands/install-commands.ts`
|
|
137
|
+
- Tests: `packages/core/src/__tests__/install-commands.test.ts` and `show-template.test.ts`
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: massu-deploy
|
|
3
|
+
description: "Deploy a containerized Python service via docker compose — force-recreate the service container, health-check poll, diff before/after, rollback if unhealthy"
|
|
4
|
+
allowed-tools: Bash(*), Read(*), Grep(*), Glob(*)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Massu Deploy: Python Service — Docker Compose
|
|
8
|
+
|
|
9
|
+
Force-recreates the Docker container for a Python service using `docker compose`. Use this variant when your service is containerized and `massu.config.yaml` declares `config.python.service_label` matching the compose service name.
|
|
10
|
+
|
|
11
|
+
## Workflow Position
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
/massu-push -> /massu-deploy (docker variant)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
This command redeploys a running container. **If the service handles financial, transactional, or consequential state, real data is at risk** — pre-flight checks are mandatory.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## NON-NEGOTIABLE RULES
|
|
22
|
+
|
|
23
|
+
- **Never deploy with uncommitted changes** — push first via `/massu-push`
|
|
24
|
+
- **Never deploy with failing tests** — test suite must be green before this runs
|
|
25
|
+
- **Always restart-and-probe** — `docker compose up` ≠ the new image is healthy
|
|
26
|
+
- **Never stop containers without identifying them first** — confirm the compose service name before running any destructive command
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Pre-flight
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# 1. Branch + working tree clean
|
|
34
|
+
test -z "$(git status --porcelain)" || { echo "DIRTY — commit/stash first"; exit 1; }
|
|
35
|
+
|
|
36
|
+
# 2. Tests green
|
|
37
|
+
pytest -x 2>&1 | tail -10
|
|
38
|
+
|
|
39
|
+
# 3. Confirm the service is running
|
|
40
|
+
docker compose ps {{config.python.service_label | default("<service-label>")}}
|
|
41
|
+
|
|
42
|
+
# 4. Capture current health for diff
|
|
43
|
+
curl -sS http://localhost:8000/health | python3 -m json.tool \
|
|
44
|
+
> /tmp/{{config.python.service_label | default("service")}}-health-before.json
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Build (if image needs rebuilding)
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Rebuild the image for this service only (skip if using a pre-built registry image)
|
|
53
|
+
docker compose build {{config.python.service_label | default("<service-label>")}}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Approval Gate
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
===============================================================================
|
|
62
|
+
APPROVAL REQUIRED — DOCKER COMPOSE REDEPLOY
|
|
63
|
+
===============================================================================
|
|
64
|
+
|
|
65
|
+
Compose service : {{config.python.service_label | default("<service-label>")}}
|
|
66
|
+
Supervisor : docker compose
|
|
67
|
+
Pre-flight : PASS
|
|
68
|
+
|
|
69
|
+
This will:
|
|
70
|
+
1. docker compose up -d --force-recreate {{config.python.service_label | default("<service-label>")}}
|
|
71
|
+
2. Poll /health every 2s against the container (max 60s) until 200
|
|
72
|
+
3. Smoke /health + any critical endpoint
|
|
73
|
+
4. Diff before/after health JSON
|
|
74
|
+
5. On failure: print rollback command
|
|
75
|
+
|
|
76
|
+
Reply "approve" or "abort".
|
|
77
|
+
===============================================================================
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Redeploy
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
docker compose up -d --force-recreate {{config.python.service_label | default("<service-label>")}}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`--force-recreate` ensures the container is replaced even if the image tag didn't change. Omit if you only want `up` to no-op on unchanged images.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Poll Container Health
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
for i in $(seq 1 30); do
|
|
96
|
+
sleep 2
|
|
97
|
+
status=$(curl -sS -o /dev/null -w "%{http_code}" http://localhost:8000/health || echo "000")
|
|
98
|
+
[ "$status" = "200" ] && { echo "READY after ${i} polls"; break; }
|
|
99
|
+
echo "poll ${i}: ${status}"
|
|
100
|
+
done
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
If the compose file declares a `HEALTHCHECK` directive, you can also poll the Docker health state:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
for i in $(seq 1 30); do
|
|
107
|
+
sleep 2
|
|
108
|
+
state=$(docker inspect --format='\{{.State.Health.Status}}' \
|
|
109
|
+
"$(docker compose ps -q {{config.python.service_label | default("<service-label>")}})") 2>/dev/null || state="unknown"
|
|
110
|
+
[ "$state" = "healthy" ] && { echo "Container healthy after ${i} polls"; break; }
|
|
111
|
+
echo "poll ${i}: $state"
|
|
112
|
+
done
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Smoke + Diff
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
curl -sS http://localhost:8000/health | python3 -m json.tool \
|
|
121
|
+
> /tmp/{{config.python.service_label | default("service")}}-health-after.json
|
|
122
|
+
|
|
123
|
+
diff \
|
|
124
|
+
/tmp/{{config.python.service_label | default("service")}}-health-before.json \
|
|
125
|
+
/tmp/{{config.python.service_label | default("service")}}-health-after.json \
|
|
126
|
+
| head -50
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Tail Container Logs
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
# Last 100 lines, filtered for errors
|
|
135
|
+
docker compose logs {{config.python.service_label | default("<service-label>")}} \
|
|
136
|
+
--tail=100 --no-log-prefix | grep -iE "error|warn" | head -20
|
|
137
|
+
|
|
138
|
+
# Follow live (Ctrl-C to stop)
|
|
139
|
+
docker compose logs -f {{config.python.service_label | default("<service-label>")}} --no-log-prefix
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Rollback
|
|
145
|
+
|
|
146
|
+
If `/health` does not return 200 within 60s, or any smoke check fails:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
# Option A: revert code + redeploy
|
|
150
|
+
git revert HEAD --no-edit
|
|
151
|
+
docker compose build {{config.python.service_label | default("<service-label>")}}
|
|
152
|
+
docker compose up -d --force-recreate {{config.python.service_label | default("<service-label>")}}
|
|
153
|
+
|
|
154
|
+
# Option B: roll back to the previous image tag (if using a registry)
|
|
155
|
+
# docker compose pull {{config.python.service_label | default("<service-label>")}}:<prev-tag>
|
|
156
|
+
# docker compose up -d --force-recreate {{config.python.service_label | default("<service-label>")}}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Then page yourself or your on-call — an unhealthy production container is an incident.
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Audit Log
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
echo "$(date -u +%FT%TZ) deploy surface=docker sha=$(git rev-parse HEAD) service={{config.python.service_label | default("<service-label>")}} actor=$(whoami)" \
|
|
167
|
+
>> data/audit/deploys.log
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Done. Report: compose service name, image digest or sha, restart time, health status, any warnings.
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: massu-deploy
|
|
3
|
+
description: "Deploy a Python service to Fly.io — flyctl deploy, status check, log tail, rollback if unhealthy"
|
|
4
|
+
allowed-tools: Bash(*), Read(*), Grep(*), Glob(*)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Massu Deploy: Python Service — Fly.io
|
|
8
|
+
|
|
9
|
+
Deploys a Python service to Fly.io using `flyctl deploy`. Use this variant when your project targets Fly.io and has a `fly.toml` in the repository root. The app name comes from `fly.toml` (or `config.python.service_label` as a fallback).
|
|
10
|
+
|
|
11
|
+
## Workflow Position
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
/massu-push -> /massu-deploy (fly variant)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
This command deploys to a live Fly.io app. **If the app handles financial, transactional, or otherwise consequential state, real data is at risk** — pre-flight checks are mandatory.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## NON-NEGOTIABLE RULES
|
|
22
|
+
|
|
23
|
+
- **Never deploy with uncommitted changes** — push first via `/massu-push`
|
|
24
|
+
- **Never deploy with failing tests** — test suite must be green before this runs
|
|
25
|
+
- **Always check status after deploy** — `flyctl deploy` success ≠ the new release is healthy
|
|
26
|
+
- **Never force a deploy to fix a broken deploy** — diagnose first, then decide
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Pre-flight
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# 1. Branch + working tree clean
|
|
34
|
+
test -z "$(git status --porcelain)" || { echo "DIRTY — commit/stash first"; exit 1; }
|
|
35
|
+
|
|
36
|
+
# 2. Tests green
|
|
37
|
+
pytest -x 2>&1 | tail -10
|
|
38
|
+
|
|
39
|
+
# 3. Confirm flyctl is authenticated and the app exists
|
|
40
|
+
flyctl status --app {{config.python.service_label | default("<app-name>")}} 2>&1 | head -20
|
|
41
|
+
|
|
42
|
+
# 4. Note the current release number for rollback reference
|
|
43
|
+
flyctl releases --app {{config.python.service_label | default("<app-name>")}} --limit 3
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## `fly.toml` — Key Fields to Verify
|
|
49
|
+
|
|
50
|
+
Before deploying, confirm these sections exist in `fly.toml`:
|
|
51
|
+
|
|
52
|
+
```toml
|
|
53
|
+
app = "{{config.python.service_label | default("<app-name>")}}"
|
|
54
|
+
primary_region = "<region>" # e.g. "ord", "lax", "iad"
|
|
55
|
+
|
|
56
|
+
[build]
|
|
57
|
+
# Dockerfile or builder block
|
|
58
|
+
|
|
59
|
+
[http_service]
|
|
60
|
+
internal_port = 8000
|
|
61
|
+
force_https = true
|
|
62
|
+
auto_stop_machines = true
|
|
63
|
+
auto_start_machines = true
|
|
64
|
+
|
|
65
|
+
[[vm]]
|
|
66
|
+
memory = "512mb"
|
|
67
|
+
cpu_kind = "shared"
|
|
68
|
+
cpus = 1
|
|
69
|
+
|
|
70
|
+
[checks]
|
|
71
|
+
[checks.health]
|
|
72
|
+
grace_period = "10s"
|
|
73
|
+
interval = "30s"
|
|
74
|
+
method = "GET"
|
|
75
|
+
path = "/health"
|
|
76
|
+
protocol = "https"
|
|
77
|
+
timeout = "10s"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Missing a `[checks.health]` block means Fly.io won't automatically detect an unhealthy release.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Approval Gate
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
===============================================================================
|
|
88
|
+
APPROVAL REQUIRED — FLY.IO DEPLOY
|
|
89
|
+
===============================================================================
|
|
90
|
+
|
|
91
|
+
App name : {{config.python.service_label | default("<app-name>")}}
|
|
92
|
+
Platform : Fly.io
|
|
93
|
+
Pre-flight : PASS
|
|
94
|
+
|
|
95
|
+
This will:
|
|
96
|
+
1. flyctl deploy --app {{config.python.service_label | default("<app-name>")}}
|
|
97
|
+
2. Wait for Fly.io health checks to pass (built-in to flyctl)
|
|
98
|
+
3. flyctl status to confirm all instances are running
|
|
99
|
+
4. Smoke /health endpoint
|
|
100
|
+
5. On failure: print rollback command
|
|
101
|
+
|
|
102
|
+
Reply "approve" or "abort".
|
|
103
|
+
===============================================================================
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Deploy
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
flyctl deploy --app {{config.python.service_label | default("<app-name>")}}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`flyctl deploy` builds the image (or reuses a cached layer), pushes to the Fly.io registry, creates a new release, and waits for health checks. Pass `--strategy rolling` or `--strategy bluegreen` if your app supports it.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Post-Deploy Status
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# Confirm all machines are healthy
|
|
122
|
+
flyctl status --app {{config.python.service_label | default("<app-name>")}}
|
|
123
|
+
|
|
124
|
+
# Show the new release
|
|
125
|
+
flyctl releases --app {{config.python.service_label | default("<app-name>")}} --limit 1
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Smoke
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# Hit the public health endpoint (replace with your actual hostname)
|
|
134
|
+
APP_HOST=$(flyctl info --app {{config.python.service_label | default("<app-name>")}} --hostname 2>/dev/null | head -1 || echo "{{config.python.service_label | default("<app-name>")}}.fly.dev")
|
|
135
|
+
curl -sS "https://${APP_HOST}/health" | python3 -m json.tool
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Tail Logs
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
# Live log stream (Ctrl-C to stop)
|
|
144
|
+
flyctl logs --app {{config.python.service_label | default("<app-name>")}}
|
|
145
|
+
|
|
146
|
+
# Filter for errors in recent output
|
|
147
|
+
flyctl logs --app {{config.python.service_label | default("<app-name>")}} 2>&1 | grep -iE "error|warn|exception" | head -30
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Rollback
|
|
153
|
+
|
|
154
|
+
If health checks fail or the smoke check returns non-200:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
# Roll back to the previous release
|
|
158
|
+
flyctl releases --app {{config.python.service_label | default("<app-name>")}} --limit 5
|
|
159
|
+
flyctl deploy --image <previous-image-ref> --app {{config.python.service_label | default("<app-name>")}}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Or use the Fly dashboard's "Rollback" button if the release number is known.
|
|
163
|
+
|
|
164
|
+
Then page yourself or your on-call — an unhealthy Fly.io release is an incident.
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Secrets Management
|
|
169
|
+
|
|
170
|
+
Do NOT hard-code secrets in `fly.toml`. Use Fly secrets:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
# Set a secret (prompts for value if omitted)
|
|
174
|
+
flyctl secrets set MY_SECRET_KEY=<value> --app {{config.python.service_label | default("<app-name>")}}
|
|
175
|
+
|
|
176
|
+
# List current secret names (values are redacted)
|
|
177
|
+
flyctl secrets list --app {{config.python.service_label | default("<app-name>")}}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Audit Log
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
echo "$(date -u +%FT%TZ) deploy surface=fly sha=$(git rev-parse HEAD) app={{config.python.service_label | default("<app-name>")}} actor=$(whoami)" \
|
|
186
|
+
>> data/audit/deploys.log
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Done. Report: app name, release number, deploy time, health status, any warnings.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: massu-deploy
|
|
3
|
+
description: "Deploy a Python service supervised by launchd (macOS) — restart the launchd agent, poll health, diff before/after, rollback if unhealthy"
|
|
4
|
+
allowed-tools: Bash(*), Read(*), Grep(*), Glob(*)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Massu Deploy: Python Service — launchd (macOS)
|
|
8
|
+
|
|
9
|
+
Restarts a Python service running under `launchd` on macOS. Use this variant when your `massu.config.yaml` declares `config.python.service_label` and your host is macOS.
|
|
10
|
+
|
|
11
|
+
## Workflow Position
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
/massu-push -> /massu-deploy (launchd variant)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
This command restarts a production service. **If the service handles financial, transactional, or otherwise consequential state, real data is at risk** — pre-flight checks are mandatory.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## NON-NEGOTIABLE RULES
|
|
22
|
+
|
|
23
|
+
- **Never deploy with uncommitted changes** — push first via `/massu-push`
|
|
24
|
+
- **Never deploy with failing tests** — test suite must be green before this runs
|
|
25
|
+
- **Always restart-and-probe** — file-saved ≠ process-running-the-fix
|
|
26
|
+
- **Never kill processes without identifying them first** — confirm the label before sending any signal
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Pre-flight
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# 1. Branch + working tree clean
|
|
34
|
+
test -z "$(git status --porcelain)" || { echo "DIRTY — commit/stash first"; exit 1; }
|
|
35
|
+
|
|
36
|
+
# 2. Tests green
|
|
37
|
+
pytest -x 2>&1 | tail -10
|
|
38
|
+
|
|
39
|
+
# 3. Confirm the launchd agent is registered
|
|
40
|
+
launchctl list | grep {{config.python.service_label | default("<service-label>")}}
|
|
41
|
+
|
|
42
|
+
# 4. Capture current health for diff
|
|
43
|
+
curl -sS http://localhost:8000/health | python3 -m json.tool \
|
|
44
|
+
> /tmp/{{config.python.service_label | default("service")}}-health-before.json
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Approval Gate
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
===============================================================================
|
|
53
|
+
APPROVAL REQUIRED — LAUNCHD RESTART
|
|
54
|
+
===============================================================================
|
|
55
|
+
|
|
56
|
+
Service label : {{config.python.service_label | default("<service-label>")}}
|
|
57
|
+
Supervisor : launchd (macOS)
|
|
58
|
+
Pre-flight : PASS
|
|
59
|
+
|
|
60
|
+
This will:
|
|
61
|
+
1. launchctl kickstart -k gui/$(id -u)/{{config.python.service_label | default("<service-label>")}}
|
|
62
|
+
2. Poll /health every 2s (max 60s) until 200
|
|
63
|
+
3. Smoke /health + any critical endpoint
|
|
64
|
+
4. Diff before/after health JSON
|
|
65
|
+
5. On failure: print rollback command
|
|
66
|
+
|
|
67
|
+
Reply "approve" or "abort".
|
|
68
|
+
===============================================================================
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Restart
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
launchctl kickstart -k gui/$(id -u)/{{config.python.service_label | default("<service-label>")}}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The `-k` flag kills the existing process before relaunching — equivalent to a clean restart. If you need to stop without restart, use `launchctl kill SIGTERM gui/$(id -u)/{{config.python.service_label | default("<service-label>")}}` instead.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Poll Health
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
for i in $(seq 1 30); do
|
|
87
|
+
sleep 2
|
|
88
|
+
status=$(curl -sS -o /dev/null -w "%{http_code}" http://localhost:8000/health || echo "000")
|
|
89
|
+
[ "$status" = "200" ] && { echo "READY after ${i} polls"; break; }
|
|
90
|
+
echo "poll ${i}: ${status}"
|
|
91
|
+
done
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Smoke + Diff
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
curl -sS http://localhost:8000/health | python3 -m json.tool \
|
|
100
|
+
> /tmp/{{config.python.service_label | default("service")}}-health-after.json
|
|
101
|
+
|
|
102
|
+
diff \
|
|
103
|
+
/tmp/{{config.python.service_label | default("service")}}-health-before.json \
|
|
104
|
+
/tmp/{{config.python.service_label | default("service")}}-health-after.json \
|
|
105
|
+
| head -50
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Tail Startup Logs
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
# Unified log (most reliable for launchd-managed services)
|
|
114
|
+
log show \
|
|
115
|
+
--predicate 'subsystem == "{{config.python.service_label | default("<service-label>")}}"' \
|
|
116
|
+
--last 2m --info 2>/dev/null | grep -iE "error|warn" | head -20
|
|
117
|
+
|
|
118
|
+
# Alternative: stderr from the plist's StandardErrorPath
|
|
119
|
+
# cat ~/Library/Logs/{{config.python.service_label | default("<service-label>")}}/stderr.log | tail -40
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Rollback
|
|
125
|
+
|
|
126
|
+
If `/health` does not return 200 within 60s, or any smoke check fails:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
git revert HEAD --no-edit
|
|
130
|
+
launchctl kickstart -k gui/$(id -u)/{{config.python.service_label | default("<service-label>")}}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Then page yourself or your on-call — an unhealthy production service is an incident.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Audit Log
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
echo "$(date -u +%FT%TZ) deploy surface=launchd sha=$(git rev-parse HEAD) label={{config.python.service_label | default("<service-label>")}} actor=$(whoami)" \
|
|
141
|
+
>> data/audit/deploys.log
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Done. Report: label, sha, restart time, health status, any warnings.
|