@massu/core 1.2.1 → 1.3.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 +122 -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.md +143 -0
- package/dist/cli.js +482 -223
- package/dist/hooks/auto-learning-pipeline.js +7 -4
- package/dist/hooks/classify-failure.js +7 -4
- package/dist/hooks/cost-tracker.js +7 -4
- package/dist/hooks/fix-detector.js +7 -4
- package/dist/hooks/incident-pipeline.js +7 -4
- package/dist/hooks/post-edit-context.js +7 -4
- package/dist/hooks/post-tool-use.js +7 -4
- package/dist/hooks/pre-compact.js +7 -4
- package/dist/hooks/pre-delete-check.js +7 -4
- package/dist/hooks/quality-event.js +7 -4
- package/dist/hooks/rule-enforcement-pipeline.js +7 -4
- package/dist/hooks/session-end.js +7 -4
- package/dist/hooks/session-start.js +7 -4
- package/dist/hooks/user-prompt.js +7 -4
- package/package.json +1 -1
- package/src/cli.ts +6 -0
- package/src/commands/install-commands.ts +366 -42
- package/src/commands/show-template.ts +65 -0
- package/src/config.ts +6 -3
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,122 @@
|
|
|
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
|
|
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
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
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
|
+
|
|
26
|
+
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.
|
|
27
|
+
|
|
28
|
+
## 2. Variant-resolution algorithm
|
|
29
|
+
|
|
30
|
+
Given a base template name `B` and the consumer's `framework` config from `massu.config.yaml`:
|
|
31
|
+
|
|
32
|
+
1. **Build the candidate list `V`** in priority order:
|
|
33
|
+
1. `framework.primary` (or `framework.type` if `primary` is undefined). Skipped if the value is `"multi"`.
|
|
34
|
+
2. Each declared `framework.languages.<lang>` entry whose `framework` field is a non-empty string, in **YAML declaration order** (first-declared wins on ties).
|
|
35
|
+
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.
|
|
36
|
+
4. The unsuffixed default (`""`) as the last fallback.
|
|
37
|
+
2. **Probe disk** in order: for each candidate `c`, check whether `<base>.<c>.md` exists in the bundled commands directory.
|
|
38
|
+
3. **Return**:
|
|
39
|
+
- First hit → copy that file.
|
|
40
|
+
- No hit AND `framework.type === "multi"` AND `framework.primary` is undefined → write a one-line warning to stderr and copy the unsuffixed default.
|
|
41
|
+
- 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).
|
|
42
|
+
|
|
43
|
+
The target filename in the consumer dir is always the BASE name (`<base>.md`) — variant suffixes are an internal package detail.
|
|
44
|
+
|
|
45
|
+
### Tie-break — which variant wins?
|
|
46
|
+
|
|
47
|
+
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.
|
|
48
|
+
|
|
49
|
+
There is no per-template override mechanism. If you need one, file an issue.
|
|
50
|
+
|
|
51
|
+
## 3. Local-edit protection — the manifest
|
|
52
|
+
|
|
53
|
+
`@massu/core@1.3.0` introduces a manifest at:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
<consumer-root>/<claudeDirName>/.massu/install-manifest.json
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
(where `claudeDirName` is the value of `conventions.claudeDirName` in `massu.config.yaml`, defaulting to `.claude`).
|
|
60
|
+
|
|
61
|
+
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.
|
|
62
|
+
|
|
63
|
+
### What the installer does on each run
|
|
64
|
+
|
|
65
|
+
For each file `<asset>` to be installed, three hashes are computed:
|
|
66
|
+
|
|
67
|
+
| Hash | What it represents |
|
|
68
|
+
|------|--------------------|
|
|
69
|
+
| `sourceHash` | The bundled upstream content NOW. |
|
|
70
|
+
| `existingHash` | The current consumer file content. `undefined` if missing. |
|
|
71
|
+
| `lastInstalledHash` | The hash recorded in the manifest. `undefined` on first install. |
|
|
72
|
+
|
|
73
|
+
Decision matrix:
|
|
74
|
+
|
|
75
|
+
| Condition | Action | Counter |
|
|
76
|
+
|-----------|--------|---------|
|
|
77
|
+
| target missing | write upstream, record `sourceHash` in manifest | `installed` |
|
|
78
|
+
| `existingHash === sourceHash` | already in sync; record `sourceHash` (covers manifest healing) | `skipped` |
|
|
79
|
+
| `lastInstalledHash` undefined AND `existingHash !== sourceHash` | first-install heuristic: keep the consumer file, record `existingHash`, print a one-line notice | `kept` |
|
|
80
|
+
| `existingHash !== lastInstalledHash` (consumer edited it) | preserve, print `kept your version` notice + diff hint | `kept` |
|
|
81
|
+
| `existingHash === lastInstalledHash` AND `sourceHash !== lastInstalledHash` (clean upstream upgrade) | write upstream, record `sourceHash` | `updated` |
|
|
82
|
+
|
|
83
|
+
The manifest is written **atomically** (`tempfile + renameSync`) so a crash mid-install never leaves a partially-written manifest.
|
|
84
|
+
|
|
85
|
+
### What this means for you
|
|
86
|
+
|
|
87
|
+
- Edit your `<claudeDirName>/commands/*.md` freely. The installer will not stomp your edits.
|
|
88
|
+
- To accept the upstream version of a file you've edited: delete it and rerun `install-commands`.
|
|
89
|
+
- To diff your version against upstream:
|
|
90
|
+
```bash
|
|
91
|
+
diff .claude/commands/massu-scaffold-router.md \
|
|
92
|
+
<(npx @massu/core show-template massu-scaffold-router)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## 4. `npx @massu/core show-template <name>`
|
|
96
|
+
|
|
97
|
+
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.
|
|
98
|
+
|
|
99
|
+
## 5. Currently shipped variants
|
|
100
|
+
|
|
101
|
+
| Base | Variants |
|
|
102
|
+
|------|----------|
|
|
103
|
+
| `massu-scaffold-router` | `.python.md` (FastAPI) |
|
|
104
|
+
| `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) |
|
|
106
|
+
|
|
107
|
+
All other 57 top-level templates ship as variant-agnostic defaults (one `<base>.md`).
|
|
108
|
+
|
|
109
|
+
## 6. Adding a new variant
|
|
110
|
+
|
|
111
|
+
1. Create `<base>.<lang>.md` in this directory using the same frontmatter shape as the default.
|
|
112
|
+
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
|
+
3. Add a row to the table in section 5.
|
|
114
|
+
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
|
+
|
|
117
|
+
## 7. Reference
|
|
118
|
+
|
|
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
|
+
- Implementation: `packages/core/src/commands/install-commands.ts`
|
|
122
|
+
- Tests: `packages/core/src/__tests__/install-commands.test.ts` and `show-template.test.ts`
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: massu-deploy
|
|
3
|
+
description: "Deploy a Python service (FastAPI / asgi) to production. Default target is a long-running process supervised by launchd, systemd, pm2, or docker — NOT Vercel. Asks which surface before acting."
|
|
4
|
+
allowed-tools: Bash(*), Read(*), Grep(*), Glob(*)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
> **Shared rules apply.** Read `${paths.commands}/_shared-preamble.md` before proceeding.
|
|
8
|
+
|
|
9
|
+
# Massu Deploy: Python Service Production Deploy
|
|
10
|
+
|
|
11
|
+
## Workflow Position
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
/massu-create-plan -> /massu-plan -> /massu-loop -> /massu-commit -> /massu-push -> /massu-deploy
|
|
15
|
+
(CREATE) (AUDIT) (IMPLEMENT) (COMMIT) (PUSH) (DEPLOY)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
This command deploys to production. **If the service is doing anything financial, transactional, or otherwise consequential, real money / data integrity is at risk** — pre-flight checks are mandatory.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## CRITICAL — Deployment Surfaces
|
|
23
|
+
|
|
24
|
+
A non-Vercel Python project usually has multiple deploy surfaces. Ask the user which one(s) before proceeding:
|
|
25
|
+
|
|
26
|
+
| Surface | What "deploy" means | Authority |
|
|
27
|
+
|---------|---------------------|-----------|
|
|
28
|
+
| **Service** (default) | Restart the supervised process — `launchctl kickstart`, `systemctl restart`, `pm2 restart`, or `docker compose up -d` | The actual production brain |
|
|
29
|
+
| **Static / docs** | rsync / S3 sync (if applicable) | Static site only |
|
|
30
|
+
| **Worker(s)** | Restart any background workers (celery, rq, custom asyncio task runners) | Background processing |
|
|
31
|
+
|
|
32
|
+
**There is NO assumption of Vercel.** Do NOT run `vercel --prod` from this template. If the user wants a Vercel deploy of a separate sub-project, route them to a different surface or scaffold a dedicated script.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## NON-NEGOTIABLE RULES
|
|
37
|
+
|
|
38
|
+
- **Never deploy with uncommitted changes** — push first via `/massu-push`
|
|
39
|
+
- **Never deploy with failing tests** — `pytest` (or your project's test runner) must be green
|
|
40
|
+
- **Always restart-and-probe** — file-saved ≠ process-running-the-fix. After restart, hit a health endpoint and diff before/after
|
|
41
|
+
- **Never kill processes without identifying them first** — get the PID, log line, or supervisor label before sending any signal
|
|
42
|
+
- **Live-trading or live-billing flag flips are a separate concern** — this command does not flip `auto_*_mode`, `*_live`, `production_*` flags. Use the dedicated approval flow
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## START NOW
|
|
47
|
+
|
|
48
|
+
### Step 0: Ask the user
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
===============================================================================
|
|
52
|
+
PYTHON DEPLOY — Which surface?
|
|
53
|
+
===============================================================================
|
|
54
|
+
|
|
55
|
+
service Restart the supervised Python service (default)
|
|
56
|
+
workers Restart background workers (celery / rq / custom)
|
|
57
|
+
all service + workers
|
|
58
|
+
static rsync / S3 sync (only if applicable)
|
|
59
|
+
|
|
60
|
+
Which? [service / workers / all / static]
|
|
61
|
+
===============================================================================
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Path A: Service (default)
|
|
67
|
+
|
|
68
|
+
Substitute `${supervisor}` with whatever your project actually uses (`launchd`, `systemd`, `pm2`, `docker`). Substitute `${service_label}` with the actual label / unit name from `massu.config.yaml` (`deploy.python.service_label`) or from your project's process manager.
|
|
69
|
+
|
|
70
|
+
### Pre-flight
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# 1. Branch + working tree clean
|
|
74
|
+
test -z "$(git status --porcelain)" || { echo "DIRTY — commit/stash first"; exit 1; }
|
|
75
|
+
|
|
76
|
+
# 2. Tests green (impacted area only — full suite is for /massu-push)
|
|
77
|
+
${paths.python_test_command:-pytest} -x 2>&1 | tail -10
|
|
78
|
+
|
|
79
|
+
# 3. Identify the running process (NEVER blind-kill)
|
|
80
|
+
# launchd: launchctl list | grep ${service_label}
|
|
81
|
+
# systemd: systemctl status ${service_label}
|
|
82
|
+
# pm2: pm2 jlist | jq '.[] | select(.name=="${service_label}")'
|
|
83
|
+
# docker: docker compose ps ${service_label}
|
|
84
|
+
|
|
85
|
+
# 4. Capture current health for diff
|
|
86
|
+
curl -sS ${health_url:-http://localhost:8000/health} | python3 -m json.tool > /tmp/${service_label}-health-before.json
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Approval gate
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
===============================================================================
|
|
93
|
+
APPROVAL REQUIRED — RESTART ${service_label}
|
|
94
|
+
===============================================================================
|
|
95
|
+
|
|
96
|
+
Pre-flight: PASS
|
|
97
|
+
Branch: <current>
|
|
98
|
+
HEAD: <sha>
|
|
99
|
+
Last service uptime: <etime>
|
|
100
|
+
|
|
101
|
+
This will:
|
|
102
|
+
1. Restart ${service_label} via ${supervisor}
|
|
103
|
+
2. Wait 5s, poll /health until 200 (max 60s)
|
|
104
|
+
3. Smoke: /health, /api/feature-flags (if applicable), one critical endpoint
|
|
105
|
+
4. Diff before/after health responses
|
|
106
|
+
5. If any check fails: print rollback (`git revert HEAD` + restart)
|
|
107
|
+
|
|
108
|
+
Reply "approve" or "abort".
|
|
109
|
+
===============================================================================
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Restart + verify
|
|
113
|
+
|
|
114
|
+
Pick the line that matches your supervisor:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# launchd
|
|
118
|
+
launchctl kickstart -k gui/$(id -u)/${service_label}
|
|
119
|
+
|
|
120
|
+
# systemd (user)
|
|
121
|
+
systemctl --user restart ${service_label}
|
|
122
|
+
|
|
123
|
+
# pm2
|
|
124
|
+
pm2 restart ${service_label}
|
|
125
|
+
|
|
126
|
+
# docker compose
|
|
127
|
+
docker compose up -d --force-recreate ${service_label}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Then probe:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# Poll until ready (60s budget)
|
|
134
|
+
for i in $(seq 1 30); do
|
|
135
|
+
sleep 2
|
|
136
|
+
status=$(curl -sS -o /dev/null -w "%{http_code}" ${health_url:-http://localhost:8000/health} || echo "000")
|
|
137
|
+
[ "$status" = "200" ] && { echo "READY after ${i} polls"; break; }
|
|
138
|
+
done
|
|
139
|
+
|
|
140
|
+
# Smoke + diff
|
|
141
|
+
curl -sS ${health_url:-http://localhost:8000/health} | python3 -m json.tool > /tmp/${service_label}-health-after.json
|
|
142
|
+
diff /tmp/${service_label}-health-before.json /tmp/${service_label}-health-after.json | head -50
|
|
143
|
+
|
|
144
|
+
# Tail the supervisor log for startup errors
|
|
145
|
+
# launchd:
|
|
146
|
+
log show --predicate 'subsystem == "${service_label}"' --last 2m --info 2>/dev/null | grep -iE "error|warn" | head -20
|
|
147
|
+
# systemd:
|
|
148
|
+
# journalctl --user -u ${service_label} --since "2 minutes ago" | grep -iE "error|warn" | head -20
|
|
149
|
+
# pm2:
|
|
150
|
+
# pm2 logs ${service_label} --lines 100 --nostream | grep -iE "error|warn" | head -20
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Rollback
|
|
154
|
+
|
|
155
|
+
If `/health` does not return 200 within 60s, or smoke fails:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
git revert HEAD --no-edit && <restart-command-from-above>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Then page yourself or your on-call: an unhealthy production service is an incident.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Path B: Workers
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
# Restart background workers (celery / rq / custom asyncio runners)
|
|
169
|
+
# Substitute the supervisor label list for your project's worker names.
|
|
170
|
+
for label in ${worker_labels}; do
|
|
171
|
+
echo "restarting $label..."
|
|
172
|
+
# launchd: launchctl kickstart -k gui/$(id -u)/$label
|
|
173
|
+
# systemd: systemctl --user restart $label
|
|
174
|
+
# pm2: pm2 restart $label
|
|
175
|
+
done
|
|
176
|
+
|
|
177
|
+
# Verify each worker is processing again — push a no-op job if you have one,
|
|
178
|
+
# or read the queue depth before/after.
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Path C: Static (only if applicable)
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
# Build static site, rsync, or aws s3 sync — fill in your project's pipeline.
|
|
187
|
+
# This is intentionally NOT a default; only run if your project has a static deploy target.
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## After ANY surface deploys
|
|
193
|
+
|
|
194
|
+
Save the deployment sha and timestamp to your project's audit log so the boundary is visible:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
echo "$(date -u +%FT%TZ) deploy surface=<surface> sha=$(git rev-parse HEAD) actor=$(whoami)" >> ${paths.audit_log:-data/audit/deploys.log}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Done. Report: surface, sha, time, smoke results, any warnings.
|
|
@@ -1,24 +1,40 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: massu-scaffold-page
|
|
3
|
-
description: "When user wants to create a new page,
|
|
3
|
+
description: "When user wants to create a new page, screen, or view — asks which framework target (Next.js, SwiftUI, FastAPI templates, Rust web), then scaffolds component / view / handler with project conventions"
|
|
4
4
|
allowed-tools: Bash(*), Read(*), Write(*), Edit(*), Grep(*), Glob(*)
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
# Scaffold New Page
|
|
7
|
+
# Scaffold New Page / View
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
This default template is **framework-agnostic**. It asks the user (or, when invoked by an agent, infers from `massu.config.yaml`) which target to scaffold, then dispatches to one of the embedded patterns below.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
> **If your project ships a stack-specific variant** of this template (e.g., `massu-scaffold-page.swift.md`), the variant is preferred and this default is not installed. See `${paths.commands}/README.md` for the variant-resolution rules.
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|------|---------|
|
|
15
|
-
| `page.tsx` | Main page component with Suspense boundary |
|
|
16
|
-
| `loading.tsx` | Skeleton loading state |
|
|
17
|
-
| `error.tsx` | Error boundary with retry |
|
|
13
|
+
## Step 1 — Pick a target
|
|
18
14
|
|
|
19
|
-
|
|
15
|
+
Ask the user (or read `framework.primary` / `framework.languages` from the consumer's `massu.config.yaml`):
|
|
16
|
+
|
|
17
|
+
| Stack | Path A: web | Path B: native / mobile | Path C: backend rendered |
|
|
18
|
+
|-------|-------------|-------------------------|--------------------------|
|
|
19
|
+
| TypeScript / Next.js | `app/<route>/page.tsx` + loading + error | — | — |
|
|
20
|
+
| Swift / SwiftUI | — | `Features/<feature>/Views/<Name>View.swift` (+ ViewModel + Response) | — |
|
|
21
|
+
| Python / FastAPI | — | — | Jinja template + handler in `routers/<name>.py` |
|
|
22
|
+
| Rust / Axum | — | — | `src/handlers/<name>.rs` returning `Html` or JSON |
|
|
23
|
+
|
|
24
|
+
If the user is unsure, default to whatever `framework.primary` points at.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Pattern 1 — Next.js App Router page
|
|
29
|
+
|
|
30
|
+
### What gets created
|
|
31
|
+
|
|
32
|
+
- `${paths.web_source}/app/<route>/page.tsx`
|
|
33
|
+
- `${paths.web_source}/app/<route>/loading.tsx`
|
|
34
|
+
- `${paths.web_source}/app/<route>/error.tsx`
|
|
35
|
+
|
|
36
|
+
### `page.tsx`
|
|
20
37
|
|
|
21
|
-
### page.tsx
|
|
22
38
|
```tsx
|
|
23
39
|
import { Suspense } from 'react';
|
|
24
40
|
import { PageContent } from './page-content';
|
|
@@ -26,82 +42,179 @@ import Loading from './loading';
|
|
|
26
42
|
|
|
27
43
|
export default function Page() {
|
|
28
44
|
return (
|
|
29
|
-
<
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
</Suspense>
|
|
33
|
-
</div>
|
|
45
|
+
<Suspense fallback={<Loading />}>
|
|
46
|
+
<PageContent />
|
|
47
|
+
</Suspense>
|
|
34
48
|
);
|
|
35
49
|
}
|
|
36
50
|
```
|
|
37
51
|
|
|
38
|
-
### page-content.tsx
|
|
52
|
+
### `page-content.tsx`
|
|
53
|
+
|
|
39
54
|
```tsx
|
|
40
55
|
'use client';
|
|
41
56
|
|
|
42
|
-
import {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
import { useEffect, useState } from 'react';
|
|
58
|
+
|
|
59
|
+
export function PageContent() {
|
|
60
|
+
const [data, setData] = useState<unknown>(null);
|
|
61
|
+
const [error, setError] = useState<string | null>(null);
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
// Substitute the URL with your project's API base — tRPC client, REST URL, etc.
|
|
65
|
+
fetch(`${process.env.NEXT_PUBLIC_API_BASE ?? '/api'}/<endpoint>`, {
|
|
66
|
+
credentials: 'include',
|
|
67
|
+
})
|
|
68
|
+
.then(async (r) => {
|
|
69
|
+
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
70
|
+
setData(await r.json());
|
|
71
|
+
})
|
|
72
|
+
.catch((e: Error) => setError(e.message));
|
|
73
|
+
}, []);
|
|
74
|
+
|
|
75
|
+
if (error) throw new Error(error);
|
|
76
|
+
if (!data) return null;
|
|
77
|
+
return <pre>{JSON.stringify(data, null, 2)}</pre>;
|
|
57
78
|
}
|
|
58
79
|
```
|
|
59
80
|
|
|
60
|
-
### loading.tsx
|
|
61
|
-
```tsx
|
|
62
|
-
import { Skeleton } from '@/components/ui/skeleton';
|
|
81
|
+
### `loading.tsx` / `error.tsx`
|
|
63
82
|
|
|
83
|
+
```tsx
|
|
84
|
+
// loading.tsx — show a skeleton, NOT a flash of empty state.
|
|
64
85
|
export default function Loading() {
|
|
65
|
-
return
|
|
66
|
-
<div className="page-container">
|
|
67
|
-
<Skeleton className="h-8 w-48 mb-6" />
|
|
68
|
-
<Skeleton className="h-64 w-full" />
|
|
69
|
-
</div>
|
|
70
|
-
);
|
|
86
|
+
return <div className="page-skeleton" />;
|
|
71
87
|
}
|
|
72
88
|
```
|
|
73
89
|
|
|
74
|
-
### error.tsx
|
|
75
90
|
```tsx
|
|
91
|
+
// error.tsx — must be a client component. Provides a retry handle to the user.
|
|
76
92
|
'use client';
|
|
77
|
-
|
|
78
93
|
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
|
79
94
|
return (
|
|
80
|
-
<div
|
|
81
|
-
<
|
|
82
|
-
<p className="text-muted-foreground">{error.message}</p>
|
|
95
|
+
<div role="alert">
|
|
96
|
+
<p>Something went wrong: {error.message}</p>
|
|
83
97
|
<button onClick={reset}>Try again</button>
|
|
84
98
|
</div>
|
|
85
99
|
);
|
|
86
100
|
}
|
|
87
101
|
```
|
|
88
102
|
|
|
89
|
-
|
|
103
|
+
### Verification
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npm run build 2>&1 | tail -20 # or pnpm/yarn equivalent
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Pattern 2 — SwiftUI iOS / visionOS view
|
|
112
|
+
|
|
113
|
+
(Full version available as the `.swift.md` variant of this template.)
|
|
114
|
+
|
|
115
|
+
### What gets created
|
|
116
|
+
|
|
117
|
+
- `${paths.swift_source}/Features/<feature>/Views/<Name>View.swift`
|
|
118
|
+
- `${paths.swift_source}/Features/<feature>/ViewModels/<Name>ViewModel.swift`
|
|
119
|
+
- `${paths.swift_source}/Features/<feature>/Models/<Name>Response.swift`
|
|
120
|
+
|
|
121
|
+
### Sketch
|
|
122
|
+
|
|
123
|
+
```swift
|
|
124
|
+
struct <Name>View: View {
|
|
125
|
+
@StateObject private var viewModel = <Name>ViewModel()
|
|
126
|
+
var body: some View {
|
|
127
|
+
Group {
|
|
128
|
+
if viewModel.isLoading { ProgressView() }
|
|
129
|
+
else if let err = viewModel.error { ErrorState(message: err) { Task { await viewModel.load() } } }
|
|
130
|
+
else { content }
|
|
131
|
+
}
|
|
132
|
+
.task { await viewModel.load() }
|
|
133
|
+
}
|
|
134
|
+
private var content: some View { /* real content */ EmptyView() }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
@MainActor
|
|
138
|
+
final class <Name>ViewModel: ObservableObject {
|
|
139
|
+
@Published var data: <Name>Response?
|
|
140
|
+
@Published var isLoading = false
|
|
141
|
+
@Published var error: String?
|
|
142
|
+
func load() async { /* fetch and decode */ }
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**Critical**: With `JSONDecoder.keyDecodingStrategy = .convertFromSnakeCase`, mismatched property names decode to nil silently. Hand-verify every Decodable property against a real API response.
|
|
147
|
+
|
|
148
|
+
### Verification
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
xcodebuild -scheme <Target>_iOS -destination 'generic/platform=iOS Simulator' build | tail -20
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Pattern 3 — FastAPI Jinja-rendered page
|
|
90
157
|
|
|
91
|
-
|
|
92
|
-
- **Suspense boundaries** are REQUIRED for pages using `use(params)` or `useSearchParams()`
|
|
93
|
-
- **protectedProcedure** for ALL tRPC queries that need auth
|
|
94
|
-
- **No `sm:page-container`** — only exception is mobile chat layouts
|
|
95
|
-
- **Breadcrumbs**: Add to PageHeader if page is nested (e.g., `/crm/contacts/[id]`)
|
|
158
|
+
### What gets created
|
|
96
159
|
|
|
97
|
-
|
|
160
|
+
- `${paths.python_source}/routers/<name>.py` (Jinja-rendering handler)
|
|
161
|
+
- `${paths.python_templates}/<name>.html`
|
|
98
162
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
163
|
+
### Sketch
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
from fastapi import APIRouter, Depends, Request
|
|
167
|
+
from fastapi.responses import HTMLResponse
|
|
168
|
+
from fastapi.templating import Jinja2Templates
|
|
169
|
+
|
|
170
|
+
from ._shared import get_current_user
|
|
171
|
+
|
|
172
|
+
router = APIRouter()
|
|
173
|
+
templates = Jinja2Templates(directory="${paths.python_templates}")
|
|
174
|
+
|
|
175
|
+
@router.get("/<route>", response_class=HTMLResponse)
|
|
176
|
+
async def render(request: Request, user: dict = Depends(get_current_user)):
|
|
177
|
+
return templates.TemplateResponse("<name>.html", {"request": request, "user": user})
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Critical**: this pattern serves HTML from FastAPI directly; for pure-API endpoints, use `/massu-scaffold-router` instead.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Pattern 4 — Axum (Rust) HTML handler
|
|
185
|
+
|
|
186
|
+
### What gets created
|
|
187
|
+
|
|
188
|
+
- `${paths.rust_source}/handlers/<name>.rs`
|
|
189
|
+
- Wire-up in `src/main.rs` or `src/router.rs` via `.route("/<path>", get(<name>::handler))`.
|
|
190
|
+
|
|
191
|
+
### Sketch
|
|
192
|
+
|
|
193
|
+
```rust
|
|
194
|
+
use axum::{response::Html, routing::get, Router};
|
|
195
|
+
|
|
196
|
+
pub fn router() -> Router {
|
|
197
|
+
Router::new().route("/<path>", get(handler))
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async fn handler() -> Html<&'static str> {
|
|
201
|
+
Html("<!doctype html><meta charset='utf-8'><title>Page</title><h1>Hello</h1>")
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Verification
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
cargo build 2>&1 | tail -10 && cargo test --quiet
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
104
212
|
|
|
105
213
|
## START NOW
|
|
106
214
|
|
|
107
|
-
Ask the user
|
|
215
|
+
Ask the user (in this order):
|
|
216
|
+
|
|
217
|
+
1. **Which target?** TS / Next.js · Swift / SwiftUI · Python / FastAPI · Rust / Axum. Default to `framework.primary` from `massu.config.yaml` if the user is unsure.
|
|
218
|
+
2. What's the URL path / feature name?
|
|
219
|
+
3. What does the page render, and which API endpoint feeds it?
|
|
220
|
+
4. Any auth requirements? (logged-in only · role-gated · biometric-gated for sensitive actions)
|