@massu/core 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/commands/README.md +23 -11
  2. package/commands/massu-deploy.python-docker.md +170 -0
  3. package/commands/massu-deploy.python-fly.md +189 -0
  4. package/commands/massu-deploy.python-launchd.md +144 -0
  5. package/commands/massu-deploy.python-systemd.md +163 -0
  6. package/commands/massu-scaffold-page.swift.md +10 -10
  7. package/commands/massu-scaffold-router.python-django.md +153 -0
  8. package/commands/massu-scaffold-router.python-fastapi.md +145 -0
  9. package/dist/cli.js +9914 -4133
  10. package/dist/hooks/auto-learning-pipeline.js +45 -2
  11. package/dist/hooks/classify-failure.js +45 -2
  12. package/dist/hooks/cost-tracker.js +45 -2
  13. package/dist/hooks/fix-detector.js +45 -2
  14. package/dist/hooks/incident-pipeline.js +45 -2
  15. package/dist/hooks/post-edit-context.js +45 -2
  16. package/dist/hooks/post-tool-use.js +45 -2
  17. package/dist/hooks/pre-compact.js +45 -2
  18. package/dist/hooks/pre-delete-check.js +45 -2
  19. package/dist/hooks/quality-event.js +45 -2
  20. package/dist/hooks/rule-enforcement-pipeline.js +45 -2
  21. package/dist/hooks/session-end.js +45 -2
  22. package/dist/hooks/session-start.js +4790 -406
  23. package/dist/hooks/user-prompt.js +45 -2
  24. package/package.json +13 -4
  25. package/src/cli.ts +22 -2
  26. package/src/commands/config-refresh.ts +91 -23
  27. package/src/commands/init.ts +131 -24
  28. package/src/commands/install-commands.ts +142 -26
  29. package/src/commands/refresh-log.ts +37 -0
  30. package/src/commands/template-engine.ts +260 -0
  31. package/src/commands/watch.ts +430 -0
  32. package/src/config.ts +71 -0
  33. package/src/detect/adapters/nextjs-trpc.ts +166 -0
  34. package/src/detect/adapters/parse-guard.ts +133 -0
  35. package/src/detect/adapters/python-django.ts +208 -0
  36. package/src/detect/adapters/python-fastapi.ts +223 -0
  37. package/src/detect/adapters/query-helpers.ts +170 -0
  38. package/src/detect/adapters/runner.ts +252 -0
  39. package/src/detect/adapters/swift-swiftui.ts +171 -0
  40. package/src/detect/adapters/tree-sitter-loader.ts +467 -0
  41. package/src/detect/adapters/types.ts +173 -0
  42. package/src/detect/codebase-introspector.ts +190 -0
  43. package/src/detect/index.ts +28 -2
  44. package/src/detect/migrate.ts +4 -4
  45. package/src/detect/regex-fallback.ts +449 -0
  46. package/src/hooks/session-start.ts +94 -3
  47. package/src/lib/gitToplevel.ts +22 -0
  48. package/src/lib/installLock.ts +179 -0
  49. package/src/lib/pidLiveness.ts +67 -0
  50. package/src/lsp/auto-detect.ts +98 -0
  51. package/src/lsp/client.ts +776 -0
  52. package/src/lsp/enrich.ts +127 -0
  53. package/src/lsp/types.ts +221 -0
  54. package/src/watch/daemon.ts +385 -0
  55. package/src/watch/lockfile-detector.ts +65 -0
  56. package/src/watch/paths.ts +279 -0
  57. package/src/watch/state.ts +178 -0
@@ -14,13 +14,17 @@ This README covers:
14
14
  ## 1. Variant filename convention
15
15
 
16
16
  ```
17
- <base>.md # default — used when no variant matches
18
- <base>.python.md # FastAPI / Django / generic Python
19
- <base>.swift.md # SwiftUI / iOS / visionOS
20
- <base>.rust.md # axum / actix / generic Rust
21
- <base>.typescript.md # reserved for future use; currently no variants ship
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
22
24
  ```
23
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
+
24
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.
25
29
 
26
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.
@@ -100,11 +104,22 @@ Prints the resolved template content (post-variant-resolution) to stdout. Used i
100
104
 
101
105
  | Base | Variants |
102
106
  |------|----------|
103
- | `massu-scaffold-router` | `.python.md` (FastAPI) |
107
+ | `massu-scaffold-router` | `.python-fastapi.md` (FastAPI — two-axis), `.python-django.md` (Django — two-axis) |
104
108
  | `massu-scaffold-page` | `.swift.md` (SwiftUI), regenerated default with embedded Next.js / FastAPI / SwiftUI / Rust examples |
105
- | `massu-deploy` | `.python.md` (launchd / systemd / pm2 / docker) |
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.
106
121
 
107
- All other 57 top-level templates ship as variant-agnostic defaults (one `<base>.md`).
122
+ Pass `--skip-commands` to `massu init` or `massu refresh` to suppress command installation entirely.
108
123
 
109
124
  ## 6. Adding a new variant
110
125
 
@@ -112,11 +127,8 @@ All other 57 top-level templates ship as variant-agnostic defaults (one `<base>.
112
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).
113
128
  3. Add a row to the table in section 5.
114
129
  4. Add a test case to `packages/core/src/__tests__/install-commands.test.ts` that exercises the new variant against a fixture config.
115
- 5. Update `docs/internal/2026-04-26-template-variant-audit.md` (the audit doc) with the new label.
116
130
 
117
131
  ## 7. Reference
118
132
 
119
- - Plan: `/Users/ekoultra/hedge/docs/plans/2026-04-26-massu-stack-aware-command-templates.md`
120
- - Audit: `docs/internal/2026-04-26-template-variant-audit.md`
121
133
  - Implementation: `packages/core/src/commands/install-commands.ts`
122
134
  - 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.