@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.
- package/README.md +34 -6
- package/README.zh-TW.md +5 -1
- package/bin/maggie.js +18 -2
- package/bundled-contracts/maggie-deployment/release-runner-v1.json +14 -0
- package/bundled-contracts/maggiedash/README.md +6 -0
- package/bundled-contracts/maggiedash/agent-content-v1.json +17 -0
- package/bundled-contracts/maggiedash/section-identity-v1.json +14 -0
- package/bundled-contracts/maggiedash/translation-cache-policy-v1.json +15 -0
- package/bundled-references/localization-extraction-render-prd.md +55 -0
- package/bundled-skills/maggie-content-localization/SKILL.md +1 -1
- package/bundled-skills/maggie-dash/SKILL.md +53 -1
- package/bundled-skills/maggie-deployment/SKILL.md +20 -2
- package/bundled-skills/maggie-feedback/SKILL.md +4 -1
- package/bundled-skills/maggie-ops/SKILL.md +17 -0
- package/bundled-skills/maggie-seo-geo/SKILL.md +20 -0
- package/bundled-templates/maggiedash/section-registry.json +12 -0
- package/bundled-tools/clis/maggie_agent_content.py +135 -0
- package/bundled-tools/clis/maggie_dash.py +44 -0
- package/bundled-tools/clis/maggie_deployment.py +51 -0
- package/bundled-tools/clis/maggie_feedback.py +35 -7
- package/bundled-tools/clis/maggie_ops.py +23 -0
- package/bundled-tools/clis/maggie_verification.py +33 -0
- package/bundled-tools/runtime/agent_runtime.py +38 -0
- package/bundled-tools/runtime/maggie_dash_store.py +51 -0
- package/bundled-tools/runtime/maggie_sections.py +143 -0
- package/bundled-tools/runtime/restart_gate.py +22 -0
- package/bundled-tools/runtime/surface_coverage.py +122 -0
- package/package.json +1 -1
- 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.
|
|
195
|
-
npx @topy-ai/maggie@0.7.
|
|
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.
|
|
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.
|
|
212
|
-
|
|
213
|
-
|
|
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
|
|
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")))
|
|
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
|
-
|
|
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`](../../
|
|
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
|
|
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())
|