@topy-ai/maggie 0.7.4 → 0.7.6

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 (29) hide show
  1. package/README.md +34 -6
  2. package/README.zh-TW.md +5 -1
  3. package/bin/maggie.js +18 -2
  4. package/bundled-contracts/maggie-deployment/release-runner-v1.json +14 -0
  5. package/bundled-contracts/maggiedash/README.md +6 -0
  6. package/bundled-contracts/maggiedash/agent-content-v1.json +17 -0
  7. package/bundled-contracts/maggiedash/section-identity-v1.json +14 -0
  8. package/bundled-contracts/maggiedash/translation-cache-policy-v1.json +15 -0
  9. package/bundled-references/localization-extraction-render-prd.md +55 -0
  10. package/bundled-skills/maggie-content-localization/SKILL.md +1 -1
  11. package/bundled-skills/maggie-dash/SKILL.md +53 -1
  12. package/bundled-skills/maggie-deployment/SKILL.md +20 -2
  13. package/bundled-skills/maggie-feedback/SKILL.md +4 -1
  14. package/bundled-skills/maggie-ops/SKILL.md +17 -0
  15. package/bundled-skills/maggie-seo-geo/SKILL.md +20 -0
  16. package/bundled-templates/maggiedash/section-registry.json +12 -0
  17. package/bundled-tools/clis/maggie_agent_content.py +135 -0
  18. package/bundled-tools/clis/maggie_dash.py +44 -0
  19. package/bundled-tools/clis/maggie_deployment.py +51 -0
  20. package/bundled-tools/clis/maggie_feedback.py +35 -7
  21. package/bundled-tools/clis/maggie_ops.py +23 -0
  22. package/bundled-tools/clis/maggie_verification.py +33 -0
  23. package/bundled-tools/runtime/agent_runtime.py +38 -0
  24. package/bundled-tools/runtime/maggie_dash_store.py +51 -0
  25. package/bundled-tools/runtime/maggie_sections.py +143 -0
  26. package/bundled-tools/runtime/restart_gate.py +22 -0
  27. package/bundled-tools/runtime/surface_coverage.py +122 -0
  28. package/package.json +1 -1
  29. package/references/localization-extraction-render-prd.md +55 -0
package/README.md CHANGED
@@ -92,6 +92,9 @@ maggie bootstrap interview | phase ...
92
92
  maggie dash install | init | status | migrate | cms ...
93
93
  maggie dash transition ... # explicit content approval transition
94
94
  maggie dash variant ... # service variant create/review/preview/publish
95
+ maggie dash sections ... # validate/catalogue/identity keys
96
+ maggie agent-content write ... # host-authorized, origin-bound content bridge
97
+ maggie verification coverage ... # changed surface/locale evidence gate
95
98
  maggie clone ... # authorized homepage capture
96
99
  maggie design ... # authorized interior-page design
97
100
  maggie clone-to-template ... # URL → validated marketplace template
@@ -109,6 +112,18 @@ maggie design icon-inventory --source-dir src --runtime assets/icons.css
109
112
  maggie api lifecycle | site-audit | ops audit
110
113
  ```
111
114
 
115
+ Agent content writes must declare the exact host origin that minted the
116
+ short-lived token; the CLI rejects nested credential keys and cross-origin
117
+ redirects:
118
+
119
+ ```bash
120
+ maggie agent-content write \
121
+ --url https://example.test/api/maggie/agent-content.json \
122
+ --allowed-origin https://example.test \
123
+ --token-env MAGGIE_CONTENT_TOKEN \
124
+ --payload .maggie/content-operation.json
125
+ ```
126
+
112
127
  Run `maggie --help` for the complete syntax. `maggie-emdash` is migration
113
128
  history only and is not an installable package skill.
114
129
 
@@ -191,8 +206,8 @@ artifact schemas.
191
206
  Recommended upgrade sequence for the current release:
192
207
 
193
208
  ```bash
194
- npx @topy-ai/maggie@0.7.4 update --project . --force
195
- npx @topy-ai/maggie@0.7.4 cleanup --project .
209
+ npx @topy-ai/maggie@0.7.6 update --project . --force
210
+ npx @topy-ai/maggie@0.7.6 cleanup --project .
196
211
  ```
197
212
 
198
213
  Maintainers should pass npm credentials through the repository helper, never
@@ -202,15 +217,25 @@ as a command-line argument:
202
217
  node scripts/publish-npm.mjs --maggie-env-file ../.env
203
218
  ```
204
219
 
205
- The 0.7.4 workflow adds the installable MaggieDash admin distribution and
220
+ The 0.7.6 workflow adds the installable MaggieDash admin distribution and
206
221
  audited CMS operations (`cms revisions`,
207
222
  `trash`, `restore`, `schedule`, `publish-due`, `duplicate`, `redirect`, and
208
223
  signed `preview`). It also adds a read-only MaggieDash `--diff`/`--dry-run`
209
224
  migration inventory, body-only dashboard prop validation, dialog accessibility
210
225
  contract checks, 404-only redirect policy, trash/scheduler/preview invariants,
211
- and a shared first-party traffic filter for every measurement channel. The
212
- release retains the existing service matching, localization, seed-manifest,
213
- lockfile/analytics, sitemap, deployment and rollback workflows.
226
+ and a shared first-party traffic filter for every measurement channel. It also
227
+ adds stable section identities and reusable section presets, a machine-readable
228
+ MaggieDash section registry, a short-lived agent content bridge, restart gates
229
+ for out-of-band translation writes, deployment release-runner generation,
230
+ disk-versus-manifest skill inventory checks, and changed-surface/locale
231
+ verification. Surface evidence is route-level and rejects duplicate
232
+ observations; every route must include status, canonical, indexability, and
233
+ required metadata checks. Agent content writes require an explicit approved
234
+ origin, same-origin redirects, and recursive credential validation. Feedback
235
+ submissions persist and print only an allowlisted acknowledgement. The release
236
+ retains the existing service matching,
237
+ localization, seed-manifest, lockfile/analytics, sitemap, deployment and
238
+ rollback workflows.
214
239
 
215
240
  ## MaggieDash lifecycle
216
241
 
@@ -300,6 +325,9 @@ maggie feedback submit .maggie/feedback/<feedback-id>.json \
300
325
  Submission is never implicit. Project paths, source files, logs, and secrets
301
326
  are excluded by default. The hosted endpoint stores the normalized feedback in
302
327
  NoBlox for maintainer review; it does not automatically create a GitHub Issue.
328
+ The local submission record and command output keep only `status`, `feedbackId`,
329
+ and `requestId` from the provider acknowledgement; arbitrary response bodies
330
+ are not persisted or printed.
303
331
  For public reports, use the repository's
304
332
  [GitHub Issue Forms](https://github.com/TOPY-AI-LTD/ai-cmo-skills/issues/new/choose).
305
333
 
package/README.zh-TW.md CHANGED
@@ -8,7 +8,7 @@ Codex、Claude Code 與相容的 coding agents。
8
8
  ## 安裝
9
9
 
10
10
  ```bash
11
- npx @topy-ai/maggie@0.6.9 init --agent all
11
+ npx @topy-ai/maggie@0.7.6 init --agent all
12
12
  npx @topy-ai/maggie doctor --project .
13
13
  ```
14
14
 
@@ -25,6 +25,10 @@ maggie doctor --project . --require-bootstrap --strict
25
25
  deployment、memory、feedback 和 MaggieDash。內容先 draft/review,外部寫入、
26
26
  publish 與 production deployment 需要明確確認。
27
27
 
28
+ MaggieDash 也提供穩定 section identity、可重用 section arrangement、機器可讀
29
+ section registry、短期 agent content bridge,以及 translation out-of-band write
30
+ 後的 restart gate。`maggie doctor` 會比較 install manifest 與磁碟上的實際 skills。
31
+
28
32
  新版 release gates:
29
33
 
30
34
  ```bash
package/bin/maggie.js CHANGED
@@ -66,9 +66,11 @@ Usage:
66
66
  maggie doctor [--project PATH]
67
67
  maggie bootstrap interview [project]
68
68
  maggie dash init|install|status|migrate|transition|variant|cms --project PATH [options]
69
+ maggie dash sections <validate|prompt|keys> [options]
69
70
  maggie dash status --project PATH
70
71
  maggie dash migrate --project PATH --confirm
71
72
  maggie content FILE --source PROVIDER --project PATH --confirm
73
+ maggie agent-content write --url URL --token-env ENV --payload FILE
72
74
  maggie bootstrap phase <start|pass|fail|status> <phase> [project]
73
75
  maggie remove [SKILL ...] [--project PATH] [--agent codex|claude|all]
74
76
  maggie cleanup --project PATH [--confirm]
@@ -117,6 +119,7 @@ Usage:
117
119
  maggie site-audit URL --crawl --save-baseline FILE --reviewer NAME
118
120
  maggie site-audit URL --crawl --baseline FILE
119
121
  maggie browser-audit URL --browse PATH --output DIR --required SELECTOR [--sticky SELECTOR]
122
+ maggie verification coverage --contract FILE --evidence FILE
120
123
  maggie ops audit|preflight|verify|lockfiles|seed-manifest --project PATH
121
124
  maggie ops preflight --project PATH --write
122
125
 
@@ -320,11 +323,22 @@ function doctor(args) {
320
323
  ["claude-skills", existsSync(join(root, ".claude", "skills"))],
321
324
  ];
322
325
  for (const [name, passed] of checks) console.log(`${passed ? "PASS" : "INFO"} ${name}`);
326
+ const manifestPath = join(root, STATE_DIR, "install.json");
327
+ let manifest = null;
328
+ try { if (existsSync(manifestPath)) manifest = JSON.parse(readFileSync(manifestPath, "utf8")); } catch { manifest = null; }
329
+ const expected = Array.isArray(manifest?.skills) ? [...new Set(manifest.skills)] : null;
323
330
  const installed = [];
331
+ const installedNames = new Set();
324
332
  for (const agentRoot of [join(root, ".agents"), join(root, ".claude")]) {
325
- for (const skill of SKILL_NAMES) if (existsSync(join(agentRoot, "skills", skill, "SKILL.md"))) installed.push(`${agentRoot}/${skill}`);
333
+ for (const skill of SKILL_NAMES) if (existsSync(join(agentRoot, "skills", skill, "SKILL.md"))) {
334
+ installed.push(`${agentRoot}/${skill}`);
335
+ installedNames.add(skill);
336
+ }
326
337
  }
327
- console.log(`INFO installed-skills=${installed.length}`);
338
+ const missing = expected ? expected.filter((skill) => !installedNames.has(skill)) : [];
339
+ const inventoryState = expected && missing.length ? "INCOMPLETE" : "PASS";
340
+ console.log(`${inventoryState} skill-inventory manifest=${expected?.length ?? "unknown"} on-disk=${installedNames.size}${missing.length ? ` missing=${missing.join(",")}` : ""}`);
341
+ console.log(`INFO installed-skills=${installedNames.size}`);
328
342
  console.log("INFO doctor is diagnostic; bootstrap completion remains a project decision gate.");
329
343
  }
330
344
 
@@ -394,7 +408,9 @@ try {
394
408
  else if (command === "feedback") workflowCli("maggie_feedback.py", args);
395
409
  else if (command === "site-audit") workflowCli("site_audit.py", args);
396
410
  else if (command === "browser-audit") workflowCli("maggie_browser_audit.py", args);
411
+ else if (command === "verification") workflowCli("maggie_verification.py", args);
397
412
  else if (command === "dash") workflowCli("maggie_dash.py", args);
413
+ else if (command === "agent-content") workflowCli("maggie_agent_content.py", args);
398
414
  else if (command === "content") workflowCli("maggie_content.py", args);
399
415
  else if (command === "init" || command === "install") install(args);
400
416
  else if (command === "update") update(args);
@@ -0,0 +1,14 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://maggiedash.noblox.app/contracts/release-runner-v1.schema.json",
4
+ "title": "Maggie deployment release runner v1",
5
+ "type": "object",
6
+ "required": ["schemaVersion", "orderedSteps", "retention"],
7
+ "properties": {
8
+ "schemaVersion": {"const": "maggie-release-runner.v1"},
9
+ "orderedSteps": {"const": ["install", "typecheck", "migrate", "build", "carry-agent-state", "switch", "restart", "verify", "prune"]},
10
+ "retention": {"const": "exactly-current-plus-one-rollback"},
11
+ "mutation": {"enum": ["reviewable-script", "executed-after-confirmation"]}
12
+ },
13
+ "additionalProperties": false
14
+ }
@@ -18,6 +18,12 @@ frontend framework.
18
18
  self/chain safety for canonical route redirects;
19
19
  - [`telemetry-policy-v1.json`](telemetry-policy-v1.json): first-party/toolchain
20
20
  traffic exclusion for every measurement channel.
21
+ - [`agent-content-v1.json`](agent-content-v1.json): short-lived host bridge for
22
+ validated agent content writes without database credentials;
23
+ - [`section-identity-v1.json`](section-identity-v1.json): stable section IDs
24
+ for translation keys and ordered migration;
25
+ - [`translation-cache-policy-v1.json`](translation-cache-policy-v1.json):
26
+ restart-after-out-of-band-write evidence.
21
27
 
22
28
  MaggieDash owns these contracts. Provider adapters may add namespaced metadata,
23
29
  but they may not change the required identity, status, provenance, or approval
@@ -0,0 +1,17 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://maggiedash.noblox.app/contracts/agent-content-v1.schema.json",
4
+ "title": "MaggieDash agent content bridge v1",
5
+ "type": "object",
6
+ "required": ["method", "authorization", "tokenLifetime", "databaseCredentials", "originPolicy", "payloadSecurity"],
7
+ "properties": {
8
+ "method": {"const": "POST"},
9
+ "authorization": {"const": "Bearer short-lived session token"},
10
+ "tokenLifetime": {"const": "terminal session"},
11
+ "databaseCredentials": {"const": "never exposed to agent"},
12
+ "originPolicy": {"const": "explicit approved origin allowlist; redirects remain same-origin"},
13
+ "payloadSecurity": {"const": "recursive sensitive-key rejection and response redaction"},
14
+ "writes": {"type": "array", "items": {"enum": ["validated content operation", "revision", "audit event"]}}
15
+ },
16
+ "additionalProperties": false
17
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://maggiedash.noblox.app/contracts/section-identity-v1.schema.json",
4
+ "title": "MaggieDash stable section identity v1",
5
+ "type": "object",
6
+ "required": ["schemaVersion", "keyPattern", "fallback", "migrationOrder"],
7
+ "properties": {
8
+ "schemaVersion": {"const": "maggiedash-section-identity.v1"},
9
+ "keyPattern": {"const": "page.<id>.sections.<section-id>.<field>"},
10
+ "fallback": {"const": "array-index-until-migrated"},
11
+ "migrationOrder": {"const": "longest-old-prefix-first"}
12
+ },
13
+ "additionalProperties": false
14
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://maggiedash.noblox.app/contracts/translation-cache-policy-v1.schema.json",
4
+ "title": "MaggieDash translation cache policy v1",
5
+ "type": "object",
6
+ "required": ["schemaVersion", "outOfBandWrite", "restartRequired", "restartCommand", "verificationAfterRestart"],
7
+ "properties": {
8
+ "schemaVersion": {"const": "maggie-restart-gate.v1"},
9
+ "outOfBandWrite": {"const": true},
10
+ "restartRequired": {"const": true},
11
+ "restartCommand": {"type": "string", "minLength": 1},
12
+ "verificationAfterRestart": {"const": true}
13
+ },
14
+ "additionalProperties": false
15
+ }
@@ -0,0 +1,55 @@
1
+ # Localization source extraction and rendered validation
2
+
3
+ ## Problem
4
+
5
+ `maggie-content-localization` currently accepts a prepared `content.json` and
6
+ validates the resulting job. It does not extract translatable strings from a
7
+ real project, so malformed JSX fragments, multiline text nodes, untranslated
8
+ Latin terms, and script mismatches can pass the record validator while still
9
+ breaking the rendered page.
10
+
11
+ ## Proposed stable workflow
12
+
13
+ Add a read-only extractor and a separate rendered validation gate:
14
+
15
+ ```bash
16
+ maggie localization extract --project . --source-dir src \
17
+ --routes-file docs/routes.tsv --output .maggie/localization/source.json
18
+ maggie localization plan --content .maggie/localization/source.json ...
19
+ maggie localization validate .maggie/localization/<job>.json \
20
+ --source .maggie/localization/source.json \
21
+ --render-report .maggie/localization/rendered.json
22
+ ```
23
+
24
+ The extractor must use an AST-aware adapter for JSX/TSX/Astro/Vue/Svelte
25
+ where available, with a conservative HTML fallback. It must never rewrite
26
+ source files. Each extracted string records file, line, route, source hash,
27
+ attribute/text-node kind, and an explicit translatable/non-translatable
28
+ decision. Fragments containing braces or JavaScript operators are rejected or
29
+ reported for review, not interpreted as text.
30
+
31
+ The validation report must cover multiline nodes, required metadata, protected
32
+ facts, script expectations, glossary/allowlist exceptions, and browser output.
33
+ The rendered report must identify route, screenshot, console errors, failed
34
+ asset requests, placeholder count, and untranslated required strings. Any
35
+ failure keeps the locale non-indexable and blocks publish.
36
+
37
+ ## Acceptance criteria
38
+
39
+ - JSX conditionals and expressions are never emitted as translation strings.
40
+ - Text nodes spanning multiple lines are extracted and tested.
41
+ - Script/Latin-word checks support all registered languages and configurable
42
+ proper-noun/glossary allowlists.
43
+ - Source and translated strings are linked by stable IDs and source revision.
44
+ - A clean extraction plus clean rendered report is required before approval;
45
+ record-only validation is not sufficient.
46
+ - Unit fixtures cover Astro/JSX/Vue/Svelte/HTML, false positives, protected
47
+ facts, multiline nodes, script checks, and route-level browser errors.
48
+ - The feature remains local-first, dependency-adaptable, redacted, and
49
+ backward-compatible with existing `content.json` input.
50
+
51
+ ## Out of scope
52
+
53
+ Automatic translation quality scoring, silent source rewriting, and automatic
54
+ publish. Translation generation and owner/Admin approval remain separate
55
+ operations.
@@ -114,7 +114,7 @@ line numbers, routes, and attribute/text-node kinds while skipping scripts,
114
114
  styles, SVG, and dynamic expressions. Supplying source and render reports to
115
115
  `validate` makes the checks fail closed; the prepared `content.json` flow
116
116
  remains backward-compatible without those artifacts. See
117
- [`docs/localization-extraction-render-prd.md`](../../docs/localization-extraction-render-prd.md)
117
+ [`docs/localization-extraction-render-prd.md`](../../references/localization-extraction-render-prd.md)
118
118
  for the artifact contract and limitations.
119
119
 
120
120
  Use the shared translation index and route predicates from
@@ -46,6 +46,25 @@ owns the framework route, email/password session middleware, database, media
46
46
  storage, provider credentials, and `/api/maggie/*` adapter endpoints. Read the
47
47
  MaggieDash host adapter contract before adding a new framework adapter.
48
48
 
49
+ For a built/deployed host, an agent must use the host's validated content
50
+ bridge rather than editing release files or receiving database credentials:
51
+
52
+ ```bash
53
+ maggie agent-content write \
54
+ --url https://example.test/api/maggie/agent-content.json \
55
+ --allowed-origin https://example.test \
56
+ --token-env MAGGIE_CONTENT_TOKEN --payload .maggie/content-operation.json
57
+ ```
58
+
59
+ The host mints a short-lived session token. The bridge uses `POST` with
60
+ `Authorization: Bearer`, requires the exact approved host origin, rejects
61
+ cross-origin redirects, delegates to the ordinary save/revision/audit path,
62
+ and returns 401/422 for invalid authority or content. Nested credential-like
63
+ payload keys are rejected and response values are recursively redacted. Never
64
+ put database credentials or the short-lived token in the payload or generated
65
+ reports. See
66
+ the [agent content contract](../../bundled-contracts/maggiedash/agent-content-v1.json).
67
+
49
68
  ## Workflow
50
69
 
51
70
  1. Inspect the project with `maggie dash status --project PATH`.
@@ -125,6 +144,11 @@ maggie dash cms publish-due --project . --project-id local-project \
125
144
  --reason "scheduled publish tick" --confirm
126
145
  maggie dash cms duplicate --project . --project-id local-project --document-id <id> \
127
146
  --new-id <new-id> --new-slug <new-slug> --reason "create draft" --confirm
147
+ maggie dash cms preset-save --project . --project-id local-project \
148
+ --preset-id treatment-page --name "Treatment page" \
149
+ --sections-file .maggie/sections.json --confirm
150
+ maggie dash cms preset-insert --project . --project-id local-project \
151
+ --preset-id treatment-page --existing-id hero-1 --reason "add arrangement" --confirm
128
152
  maggie dash cms redirect --project . --project-id local-project --from-path /old --to-path /new \
129
153
  --reason "canonical slug change" --confirm
130
154
  maggie dash cms preview --project . --project-id local-project --document-id <id> \
@@ -141,4 +165,32 @@ the route returns 404 and self/chain redirects are rejected. Preview tokens
141
165
  are short-lived HMAC-signed tokens; preview responses must use a real preview
142
166
  URL with `noindex`, `X-Robots-Tag: noindex`, and `Cache-Control: no-store`.
143
167
  Never place the secret in source control or generated reports. See the
144
- provider-neutral [MaggieDash contract set](../../contracts/maggiedash/README.md).
168
+ provider-neutral [MaggieDash contract set](../../bundled-contracts/maggiedash/README.md).
169
+
170
+ Reusable arrangements are stored without page-specific section IDs and expand
171
+ into ordinary editable sections with fresh IDs on every insert. Use the
172
+ machine-readable section catalogue before asking a model to plan copy:
173
+
174
+ ```bash
175
+ maggie dash sections validate --registry templates/maggiedash/section-registry.json \
176
+ --sections-file .maggie/sections.json
177
+ maggie dash sections prompt --registry templates/maggiedash/section-registry.json
178
+ maggie dash sections keys --registry templates/maggiedash/section-registry.json \
179
+ --page-id <page-id> --sections-file .maggie/sections.json
180
+ ```
181
+
182
+ The catalogue declares purpose, usage, placement, repeatability and layout
183
+ limits. Over-limit copy is an editor note; unknown types fail validation.
184
+ Translation keys use stable section IDs, with an array-index fallback only
185
+ until the ordered migration is complete.
186
+
187
+ After any script or direct adapter write to translation data, invalidate the
188
+ running process before verification. Record the restart and run the rendering
189
+ check afterward:
190
+
191
+ ```bash
192
+ maggie ops restart-gate --manifest .maggie/translation-restart.json
193
+ ```
194
+
195
+ This prevents an in-process cache window from turning correct database state
196
+ into temporarily stale pages and false verification failures.
@@ -63,11 +63,15 @@ Generate a reviewable VPS plan and configuration artifacts locally:
63
63
  python3 tools/clis/maggie_deployment.py /path/to/project \
64
64
  --vps-plan --domain example.co.uk --service example \
65
65
  --release-root /var/www/example \
66
- --plan-dir .maggie/deployment/vps-staging
66
+ --plan-dir .maggie/deployment/vps-staging \
67
+ --runner-output .maggie/deployment/vps-staging/release-runner.sh
67
68
  ```
68
69
 
69
70
  The output contains only the immutable release/current layout, systemd unit,
70
- Nginx reverse-proxy config, health commands and rollback command. It never
71
+ Nginx reverse-proxy config, health commands, rollback command, and an ordered
72
+ reviewable release runner. The runner keeps migrations before the symlink flip,
73
+ carries `.agents`/`.claude` state, restarts before verification, and prunes to
74
+ the current release plus one rollback candidate. It never
71
75
  contains credentials and never SSHs, changes DNS, restarts systemd or deploys.
72
76
 
73
77
  VPS plans include a bounded retention policy: keep two immutable releases,
@@ -136,6 +140,20 @@ MaggieDash schema compatibility, and live runtime security-header checks. It wri
136
140
  `.maggie/release-preflight.json`; a failed gate blocks release. It never
137
141
  deploys, migrates, publishes, or sends provider requests.
138
142
 
143
+ Generate a repeatable VPS runner as part of the plan. A deploy step that exists
144
+ only in an operator's shell history is not a release contract:
145
+
146
+ ```bash
147
+ python3 tools/clis/maggie_deployment.py /path/to/project \
148
+ --vps-plan --domain example.co.uk --service example \
149
+ --release-root /var/www/example \
150
+ --runner-output .maggie/deployment/release-runner.sh
151
+ ```
152
+
153
+ Review the generated runner before execution. Its order is install → typecheck
154
+ → migrate → build → carry agent state → switch → restart → verify → prune;
155
+ retention keeps the current release and one rollback candidate.
156
+
139
157
  Production additionally requires `docs/deployment-rollback-smoke.json` with
140
158
  `passed: true`, `environment: production`, a non-empty `previousRelease`, and
141
159
  `testedAt`. A rollback command in a plan is not proof that the rollback was
@@ -44,7 +44,10 @@ maggie feedback submit .maggie/feedback/<feedback-id>.json \
44
44
 
45
45
  Only HTTPS endpoints are accepted. Project paths, file contents, secrets, and
46
46
  logs are excluded by default. `--allow-project-context` is an explicit opt-in;
47
- even then, send only a short public-safe context note.
47
+ even then, send only a short public-safe context note. After submission, the
48
+ local record and stdout retain only the provider acknowledgement fields
49
+ `status`, `feedbackId`, and `requestId`; arbitrary response bodies are never
50
+ stored or printed.
48
51
 
49
52
  ## Lifecycle
50
53
 
@@ -226,3 +226,20 @@ shared contract is in
226
226
  [references/maggiedash-telemetry-contract.md](../../references/maggiedash-telemetry-contract.md);
227
227
  it excludes only explicit tool/test markers and never infers identity from IP
228
228
  or private fields.
229
+
230
+ ## Agent runtime preflight
231
+
232
+ Before starting an agent from a dashboard, run the runtime preflight and show
233
+ its warnings to the operator. Terminal output must be stripped of ANSI escape
234
+ sequences before URL/code parsing. Headless hosts should use device auth and
235
+ state the account prerequisite before starting. Release installs and agent
236
+ conversation state must be carried outside the immutable release directory.
237
+ When continuing a conversation, pass the agent's resume subcommand before
238
+ other arguments; otherwise the CLI may interpret it as a new prompt.
239
+
240
+ ```bash
241
+ maggie ops agent-preflight --output .maggie/agent-runtime-preflight.json
242
+ ```
243
+
244
+ The preflight is advisory about the host environment but its warnings are
245
+ load-bearing operational evidence, not a claim that an agent login succeeded.
@@ -19,6 +19,26 @@ Static audit success
19
19
  does not establish Google indexing, scroll behavior, responsive visibility,
20
20
  or translation quality. Record those as unverified without separate evidence.
21
21
 
22
+ Choose verification from the changed surface. An admin-only change still needs
23
+ the admin routes; an API change needs the API responses; a localized change
24
+ needs every affected locale. Declare the matrix and validate explicit evidence
25
+ before release:
26
+
27
+ ```bash
28
+ maggie verification coverage \
29
+ --contract contracts/maggie-verification/surface-coverage-v1.json \
30
+ --evidence .maggie/surface-coverage.json
31
+ ```
32
+
33
+ The evidence must contain one observation for every declared
34
+ surface/locale/route key with a unique `route`, HTTP `status`, `passed: true`,
35
+ `canonical: true`, a boolean `indexable`, and a `metadata` object containing
36
+ every field named by that surface's `requiredMetadata`. Duplicate keys fail;
37
+ the verifier preserves the observation count and reports the duplicate and
38
+ the original failure instead of allowing a later passing observation to hide
39
+ it. Public-page checks alone cannot close an admin or API change, and one
40
+ source-language rendering cannot close a localized change.
41
+
22
42
  Crawl reports include `summary.byLocaleTemplate` with total, passed, failed
23
43
  and failed URLs per declared locale and template. Regions/scripts remain
24
44
  distinct (for example en-GB versus en-US). A template is identified only when
@@ -0,0 +1,12 @@
1
+ {
2
+ "schemaVersion": "maggiedash-section-registry.v1",
3
+ "description": "The machine-readable section vocabulary used by page planners and editors.",
4
+ "sections": [
5
+ {"type": "hero", "purpose": "Says what the page is about and offers the one important action.", "usage": "First on the page; once.", "position": "opening", "repeatable": false, "fields": [{"name": "title", "usage": "Plain words a visitor would search for.", "limit": 70, "required": true}, {"name": "intro", "usage": "Who it is for and what it does.", "limit": 220}, {"name": "ctaLabel", "usage": "The action as a verb.", "limit": 30}]},
6
+ {"type": "prose", "purpose": "Explains a topic at length, optionally beside an image.", "usage": "Use in the body for explanation.", "position": "body", "repeatable": true, "fields": [{"name": "heading", "usage": "The argument in a sentence.", "limit": 90}, {"name": "paragraphs", "usage": "Two or three standalone paragraphs.", "limit": 400, "repeats": {"min": 1, "max": 4}}]},
7
+ {"type": "cards", "purpose": "Presents parallel points side by side.", "usage": "Use for steps or genuinely parallel points.", "position": "body", "repeatable": true, "fields": [{"name": "heading", "usage": "What the cards share.", "limit": 90}, {"name": "items", "usage": "The parallel points.", "limit": 220, "repeats": {"min": 3, "max": 3}}]},
8
+ {"type": "links", "purpose": "Sends the reader to related pages.", "usage": "Near the end after the page has done its job.", "position": "closing", "repeatable": true, "fields": [{"name": "heading", "usage": "What the links have in common.", "limit": 90}, {"name": "items", "usage": "Real related pages on this site.", "limit": 160, "repeats": {"min": 2, "max": 6}}]},
9
+ {"type": "faq", "purpose": "Answers questions a visitor would otherwise ask.", "usage": "Once near the end; use real visitor questions.", "position": "closing", "repeatable": false, "fields": [{"name": "heading", "usage": "The question topic.", "limit": 90}, {"name": "items", "usage": "Direct questions and answers.", "limit": 600, "repeats": {"min": 2, "max": 8}}]},
10
+ {"type": "cta", "purpose": "Makes the closing ask.", "usage": "Last on the page; once.", "position": "closing", "repeatable": false, "fields": [{"name": "heading", "usage": "The invitation in a sentence.", "limit": 90, "required": true}, {"name": "body", "usage": "What happens next.", "limit": 220}, {"name": "label", "usage": "The action as a verb.", "limit": 30, "required": true}]}
11
+ ]
12
+ }
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env python3
2
+ """Use a short-lived MaggieDash host token for validated content writes."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import os
9
+ import sys
10
+ from pathlib import Path
11
+ from urllib.error import HTTPError, URLError
12
+ from urllib.parse import urlparse
13
+ from urllib.request import HTTPRedirectHandler, Request, build_opener
14
+
15
+
16
+ SENSITIVE_KEYS = {"databaseurl", "password", "secret", "token", "apikey", "authorization", "privatekey", "connectionstring", "clientsecret"}
17
+ SENSITIVE_KEY_PARTS = ("databaseurl", "password", "secret", "token", "apikey", "authorization", "privatekey", "connectionstring")
18
+
19
+
20
+ def normalized_key(value: object) -> str:
21
+ return "".join(character for character in str(value).lower() if character.isalnum())
22
+
23
+
24
+ def is_sensitive_key(value: object) -> bool:
25
+ key = normalized_key(value)
26
+ return key in SENSITIVE_KEYS or any(part in key for part in SENSITIVE_KEY_PARTS)
27
+
28
+
29
+ def validate_no_credentials(value: object) -> None:
30
+ if isinstance(value, dict):
31
+ for key, child in value.items():
32
+ if is_sensitive_key(key):
33
+ raise ValueError("payload must not contain credentials; use the short-lived token environment variable")
34
+ validate_no_credentials(child)
35
+ elif isinstance(value, list):
36
+ for child in value:
37
+ validate_no_credentials(child)
38
+
39
+
40
+ def validate_payload(value: object) -> dict:
41
+ if not isinstance(value, dict):
42
+ raise ValueError("payload must be a JSON object")
43
+ validate_no_credentials(value)
44
+ if not value.get("op"):
45
+ raise ValueError("payload.op is required")
46
+ return value
47
+
48
+
49
+ def origin(url: str) -> str:
50
+ parsed = urlparse(url)
51
+ if parsed.scheme not in {"http", "https"} or not parsed.hostname or parsed.username or parsed.password or parsed.query or parsed.fragment:
52
+ raise ValueError("content host must be an absolute HTTP(S) URL without credentials or query parameters")
53
+ try:
54
+ port = parsed.port
55
+ except ValueError as error:
56
+ raise ValueError("content host origin has an invalid port") from error
57
+ default = (parsed.scheme == "http" and port == 80) or (parsed.scheme == "https" and port == 443)
58
+ return f"{parsed.scheme.lower()}://{parsed.hostname.lower()}" + (f":{port}" if port and not default else "")
59
+
60
+
61
+ def approved_origin(value: str) -> str:
62
+ parsed = urlparse(value)
63
+ if parsed.path not in {"", "/"}:
64
+ raise ValueError("approved origin must not contain a path")
65
+ return origin(value)
66
+
67
+
68
+ class SameOriginRedirectHandler(HTTPRedirectHandler):
69
+ def __init__(self, approved_origin: str):
70
+ super().__init__()
71
+ self.approved_origin = approved_origin
72
+
73
+ def redirect_request(self, req, fp, code, msg, headers, newurl): # noqa: N802
74
+ if origin(newurl) != self.approved_origin:
75
+ raise ValueError("content host redirected to a different origin")
76
+ return super().redirect_request(req, fp, code, msg, headers, newurl)
77
+
78
+
79
+ def redact_response(value: object) -> object:
80
+ if isinstance(value, dict):
81
+ return {key: "[REDACTED]" if is_sensitive_key(key) else redact_response(child) for key, child in value.items()}
82
+ if isinstance(value, list):
83
+ return [redact_response(child) for child in value]
84
+ return value
85
+
86
+
87
+ def write(url: str, token_env: str, payload: dict, allowed_origin: str, timeout: int = 30) -> dict:
88
+ token = os.environ.get(token_env, "")
89
+ if not token:
90
+ raise ValueError(f"{token_env} is not set")
91
+ approved = approved_origin(allowed_origin)
92
+ if origin(url) != approved:
93
+ raise ValueError("content host URL must match the approved origin")
94
+ request = Request(url, data=json.dumps(payload, ensure_ascii=False).encode("utf-8"), method="POST", headers={
95
+ "Accept": "application/json", "Content-Type": "application/json", "Authorization": f"Bearer {token}",
96
+ })
97
+ try:
98
+ opener = build_opener(SameOriginRedirectHandler(approved))
99
+ with opener.open(request, timeout=timeout) as response:
100
+ raw = response.read().decode("utf-8", errors="replace")
101
+ status = response.status
102
+ except HTTPError as error:
103
+ raw = error.read().decode("utf-8", errors="replace")
104
+ status = error.code
105
+ except URLError as error:
106
+ raise RuntimeError("content host could not be reached") from error
107
+ try:
108
+ body = json.loads(raw) if raw else {}
109
+ except json.JSONDecodeError:
110
+ body = {"error": "host returned non-JSON"}
111
+ return {"status": status, "passed": 200 <= status < 300, "response": redact_response(body)}
112
+
113
+
114
+ def main() -> int:
115
+ parser = argparse.ArgumentParser(description=__doc__)
116
+ sub = parser.add_subparsers(dest="command", required=True)
117
+ command = sub.add_parser("write", help="POST a validated content operation")
118
+ command.add_argument("--url", required=True)
119
+ command.add_argument("--allowed-origin", required=True, help="exact HTTP(S) origin approved by the host")
120
+ command.add_argument("--token-env", default="MAGGIE_CONTENT_TOKEN")
121
+ command.add_argument("--payload", type=Path, required=True)
122
+ command.add_argument("--timeout", type=int, default=30)
123
+ args = parser.parse_args()
124
+ try:
125
+ payload = validate_payload(json.loads(args.payload.read_text(encoding="utf-8")))
126
+ result = write(args.url, args.token_env, payload, args.allowed_origin, args.timeout)
127
+ print(json.dumps(result, ensure_ascii=False, indent=2))
128
+ return 0 if result["passed"] else 1
129
+ except (OSError, ValueError, RuntimeError) as error:
130
+ print(f"agent-content: {error}", file=sys.stderr)
131
+ return 2
132
+
133
+
134
+ if __name__ == "__main__":
135
+ raise SystemExit(main())