@topy-ai/maggie 0.1.0 → 0.1.2

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/bin/maggie.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
+ import { createHash } from "node:crypto";
4
5
  import { cp } from "node:fs/promises";
5
6
  import { dirname, join, resolve } from "node:path";
6
7
  import { fileURLToPath } from "node:url";
@@ -9,9 +10,12 @@ const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
9
10
  const SKILLS_ROOT = join(PACKAGE_ROOT, "bundled-skills");
10
11
  const REFERENCES_ROOT = join(PACKAGE_ROOT, "bundled-references");
11
12
  const TOOLS_ROOT = join(PACKAGE_ROOT, "bundled-tools");
13
+ const STATE_DIR = ".maggie";
12
14
  const SKILL_NAMES = [
13
15
  "maggie-blog-bootstrap",
14
16
  "maggie-clone",
17
+ "maggie-design",
18
+ "maggie-ops",
15
19
  "maggie-deployment",
16
20
  "maggie-project-context",
17
21
  "maggie-seo-geo",
@@ -24,6 +28,7 @@ function usage() {
24
28
  Usage:
25
29
  maggie init [--project PATH] [--agent auto|codex|claude|all] [--skills LIST]
26
30
  maggie install [SKILL ...] [--project PATH] [--agent auto|codex|claude|all]
31
+ maggie update [SKILL ...] [--project PATH] [--agent auto|codex|claude|all] [--force]
27
32
  maggie list
28
33
  maggie doctor [--project PATH]
29
34
  maggie remove [SKILL ...] [--project PATH] [--agent codex|claude|all]
@@ -77,6 +82,35 @@ function copyIfMissing(source, target, force = false) {
77
82
  return "installed";
78
83
  }
79
84
 
85
+ function digest(path) {
86
+ return createHash("sha256").update(readFileSync(path)).digest("hex");
87
+ }
88
+
89
+ function syncTree(source, target, force) {
90
+ let changed = 0;
91
+ for (const entry of readdirSync(source, { withFileTypes: true })) {
92
+ const sourcePath = join(source, entry.name);
93
+ const targetPath = join(target, entry.name);
94
+ if (entry.isDirectory()) {
95
+ changed += syncTree(sourcePath, targetPath, force);
96
+ continue;
97
+ }
98
+ if (!existsSync(targetPath)) {
99
+ mkdirSync(dirname(targetPath), { recursive: true });
100
+ writeFileSync(targetPath, readFileSync(sourcePath));
101
+ console.log(`installed ${targetPath}`);
102
+ changed++;
103
+ } else if (force || digest(sourcePath) === digest(targetPath)) {
104
+ writeFileSync(targetPath, readFileSync(sourcePath));
105
+ console.log(`updated ${targetPath}`);
106
+ changed++;
107
+ } else {
108
+ console.log(`preserved ${targetPath} (local changes; use --force to replace)`);
109
+ }
110
+ }
111
+ return changed;
112
+ }
113
+
80
114
  function install(args) {
81
115
  const root = projectRoot(args);
82
116
  const skills = selectedSkills(args);
@@ -103,6 +137,34 @@ function install(args) {
103
137
  console.log("Run `maggie doctor --project .` before using mutating workflows.");
104
138
  }
105
139
 
140
+ function update(args) {
141
+ const root = projectRoot(args);
142
+ const skills = selectedSkills(args);
143
+ const force = args.includes("--force");
144
+ const roots = agentRoots(args, root);
145
+ if (!existsSync(SKILLS_ROOT)) throw new Error("bundled skills are missing; run npm pack from the package source");
146
+ let updated = 0;
147
+ for (const agentRoot of roots) {
148
+ for (const skill of skills) {
149
+ const source = join(SKILLS_ROOT, skill);
150
+ const target = join(agentRoot, "skills", skill);
151
+ if (!existsSync(target)) {
152
+ console.log(`absent ${target} (run install to add it)`);
153
+ continue;
154
+ }
155
+ updated += syncTree(source, target, force);
156
+ }
157
+ if (existsSync(REFERENCES_ROOT) && existsSync(join(agentRoot, "references"))) updated += syncTree(REFERENCES_ROOT, join(agentRoot, "references"), force);
158
+ }
159
+ const tools = join(root, "tools");
160
+ if (existsSync(TOOLS_ROOT)) for (const group of ["clis", "integrations"]) if (existsSync(join(tools, group))) updated += syncTree(join(TOOLS_ROOT, group), join(tools, group), force);
161
+ const stateDir = join(root, STATE_DIR);
162
+ mkdirSync(stateDir, { recursive: true });
163
+ writeFileSync(join(stateDir, "install.json"), JSON.stringify({ version: "0.1.0", agents: roots.map((item) => item.slice(root.length + 1)), skills, updated_at: new Date().toISOString() }, null, 2) + "\n");
164
+ console.log(`Maggie update complete: ${updated} files changed`);
165
+ if (!force) console.log("Local files with changes were preserved. Review the output and rerun with --force only when replacement is intended.");
166
+ }
167
+
106
168
  function list() {
107
169
  for (const skill of SKILL_NAMES) console.log(skill);
108
170
  }
@@ -143,6 +205,7 @@ try {
143
205
  if (["help", "--help", "-h"].includes(command)) usage();
144
206
  else if (command === "list") list();
145
207
  else if (command === "init" || command === "install") install(args);
208
+ else if (command === "update") update(args);
146
209
  else if (command === "doctor") doctor(args);
147
210
  else if (command === "remove") remove(args);
148
211
  else throw new Error(`unknown command: ${command}`);
@@ -56,6 +56,14 @@ with the canonical post source enum while remaining unique and rerunnable.
56
56
  This is the default for a new, small single-instance project. Use the same
57
57
  logical fields with Postgres or another engine when the deployment requires it.
58
58
 
59
+ ## Default listing configuration
60
+
61
+ The default blog index is a 3-column grid with 9 posts per page (3×3). Store
62
+ this as typed configuration (`postsPerPage: 9`, `gridColumns: 3`) rather than
63
+ scattering literals through templates. Users may change it later through site
64
+ settings; pagination, canonical URLs, sitemap eligibility, and audits must use
65
+ the same configured value.
66
+
59
67
  ```sql
60
68
  CREATE TABLE posts (
61
69
  id TEXT PRIMARY KEY,
@@ -0,0 +1,25 @@
1
+ # Maggie Decision Loop
2
+
3
+ Every state-changing Maggie skill follows this protocol:
4
+
5
+ 1. Inspect first and label evidence `Detected`, `Likely`, `Missing`, or
6
+ `Unknown`.
7
+ 2. Propose one recommended choice and up to two alternatives.
8
+ 3. Ask one focused question at a time, showing the selection, evidence,
9
+ affected files, and risks.
10
+ 4. Record each answer in `.maggie/decisions.json` or a skill checkpoint.
11
+ 5. Summarize all selections and require explicit final confirmation before
12
+ mutation or external writes.
13
+ 6. If an earlier answer changes, recalculate dependent choices and confirm
14
+ again; never silently overwrite a confirmed decision.
15
+
16
+ Headless runs use an answers JSON file plus a separate `--confirm` flag. They
17
+ must follow the same sequence and may not bypass final confirmation.
18
+
19
+ ```bash
20
+ python3 tools/clis/maggie.py bootstrap interview .
21
+ python3 tools/clis/maggie.py bootstrap interview . --answers-file .maggie/answers.json --non-interactive --confirm
22
+ ```
23
+
24
+ Use this loop for framework changes, clone routes, rewrite policy, publishing,
25
+ integrations, migrations, and deployment.
@@ -0,0 +1,41 @@
1
+ # Maggie Ops Dashboard Contract
2
+
3
+ Bootstrap must implement a private, authenticated Ops application before the
4
+ blog is complete. Visual design is flexible; the operational surface and
5
+ server-side boundaries are fixed.
6
+
7
+ ## Required screens
8
+
9
+ ```text
10
+ /ops health summary and pending actions
11
+ /ops/posts inventory, filters, bulk preview/apply
12
+ /ops/posts/new create draft
13
+ /ops/posts/[id]/edit validated editor and metadata preview
14
+ /ops/posts/[id]/preview noindex preview
15
+ /ops/topics topics and FAQs
16
+ /ops/sitemap sources, matching runs, unmatched and eligible counts
17
+ /ops/reports SEO, content quality, visibility, analytics
18
+ /ops/settings/site origin, locale, timezone and defaults
19
+ /ops/settings/integrations Maggie API, GSC, GA4 and provider health
20
+ /ops/operations calendar, tasks, media, redirects and agency work
21
+ /ops/wordpress migration preview, apply, validate and resume
22
+ ```
23
+
24
+ Required server routes include `/api/ops/summary`, `/api/ops/posts`,
25
+ `/api/ops/topics`, `/api/ops/sitemap/{runs,match,auto-detect}`,
26
+ `/api/ops/reports`, `/api/ops/pull/{project-context,sync,sync-updates}`,
27
+ `/api/ops/rewrite/{queue,history,policy}`, and content-tracking report-state.
28
+
29
+ Every request enforces session authentication, role authorization, input
30
+ validation, and state-transition validation. Keys never reach browser code.
31
+ Mutations show an impact preview where applicable and create an audit event
32
+ with actor, timestamp, previous state, next state, reason, and an
33
+ idempotency/correlation key. Ops pages are `noindex`, excluded from public
34
+ sitemaps, and blocked in robots rules.
35
+
36
+ ## Bootstrap acceptance
37
+
38
+ The bootstrap manifest contains the complete Ops route set, an enabled
39
+ dashboard decision, and a verification command. `maggie doctor --strict
40
+ --require-bootstrap` fails when the host project has no Ops UI, Ops API, or
41
+ authentication boundary.
@@ -7,6 +7,8 @@ Skills are the agent-facing workflows. They compose with the tools in
7
7
  |---|---|---|
8
8
  | `maggie-blog-bootstrap` | Build a complete blog in an existing project | site audit, analytics, API Pull |
9
9
  | `maggie-clone` | Reverse-engineer authorized URLs into namespaced blog-project pages | browser MCP, clone planner CLI, bootstrap state |
10
+ | `maggie-design` | Fully clone authorized interior pages, then reconcile them to the homepage header/footer shell | browser MCP, clone planner CLI, completed Maggie homepage |
11
+ | `maggie-ops` | Build, connect, operate, upgrade, and verify the private blog operations dashboard | Ops dashboard contract, authenticated API bridge, host tests |
10
12
  | `maggie-deployment` | Deploy and verify a dynamic Maggie blog, Cloudflare-first | Cloudflare Workers, D1, R2, KV, Wrangler |
11
13
  | `maggie-project-context` | Sync Project, voice, site and CTA context | project-context CLI/API |
12
14
  | `maggie-seo-geo` | Plan, audit, create/rewrite and measure SEO/GEO | visibility, SEO audit, GSC/GA4, content quality |
@@ -2,7 +2,7 @@
2
2
  name: maggie-blog-bootstrap
3
3
  description: Build or complete a small SEO/GEO-ready blog in an existing vibe-coded project, including posts, metadata, sitemap, analytics hooks, and optional AI CMO API Pull integration. Use when adding a blog to a custom site, WordPress, Wix, Shopify, or headless stack.
4
4
  metadata:
5
- version: 1.3.0
5
+ version: 1.4.0
6
6
  ---
7
7
 
8
8
  # Maggie Blog Bootstrap
@@ -31,6 +31,9 @@ GA4/GSC, AI CMO API Pull, rewrite delivery, or a basic content system.
31
31
  blog” is not approval to select an unconfirmed database, visual identity,
32
32
  language, or publishing policy.
33
33
  6. Prefer deterministic adapters and idempotent upserts over one-off scripts.
34
+ 7. Use the shared [Maggie decision loop](../../references/decision-loop.md):
35
+ inspect, propose, ask one question, record the answer, summarize, and then
36
+ require final confirmation. The CLI equivalent is `bootstrap interview`.
34
37
 
35
38
  ## Required phases
36
39
 
@@ -140,7 +143,15 @@ database, language, or visual system is unknown, say so instead of guessing.
140
143
 
141
144
  ### Phase 3: Decision gates
142
145
 
143
- Ask the user to confirm the recommendation in small groups before writing code:
146
+ Ask the user to confirm the recommendation in small groups before writing code.
147
+ Run the interactive loop when possible:
148
+
149
+ ```bash
150
+ python3 tools/clis/maggie.py bootstrap interview .
151
+ ```
152
+
153
+ It checkpoints every answer, shows the complete proposal, and requires a
154
+ separate final confirmation. Use the following groups for the conversation:
144
155
 
145
156
  1. **Foundation:** framework, programming language, content language, runtime,
146
157
  and deployment target.
@@ -150,6 +161,11 @@ Ask the user to confirm the recommendation in small groups before writing code:
150
161
  4. **Content and publishing:** initial routes, API Pull, sitemap matching,
151
162
  rewrite approval, analytics, GSC, and whether the first run is dry-run.
152
163
 
164
+ 5. **Operations handoff:** decide whether the project needs the private Maggie
165
+ Ops dashboard now. If yes, hand off to `maggie-ops`; do not silently build a
166
+ partial admin surface inside bootstrap. If no, record Ops as pending while
167
+ completing the public blog foundation.
168
+
153
169
  Show the proposed choices, detected evidence, files likely to change, and
154
170
  unknowns for each gate. If the user confirms only part of a gate, implement
155
171
  only that part and leave the rest pending. Do not ask a single broad “is this
@@ -169,6 +185,11 @@ English only when existing project language is absent
169
185
  These are proposals, not automatic permission. SQLite, English, a new font,
170
186
  or a new design system must never silently replace detected project choices.
171
187
 
188
+ The default public blog index is 9 posts per page in a 3-column (3×3) grid.
189
+ Store `postsPerPage: 9` and `gridColumns: 3` in typed configuration so the
190
+ operator can change them later from site settings without changing the post
191
+ model or pagination logic.
192
+
172
193
  ### Phase 4: Contract
173
194
 
174
195
  Implement or map these post fields:
@@ -180,7 +201,8 @@ canonicalUrl, coverImage, author, tags, status
180
201
 
181
202
  The public contract must support:
182
203
 
183
- - `/posts` with stable pagination or a bounded list;
204
+ - `/posts` with stable pagination, defaulting to 9 posts per page and a 3-column
205
+ grid;
184
206
  - `/posts/:slug` with a real 404 for missing/unpublished posts;
185
207
  - canonical, Open Graph, Twitter, and Article metadata;
186
208
  - post links that are crawlable plain anchors;
@@ -199,6 +221,9 @@ Implement the host project's equivalent of:
199
221
  - JSON-LD `Article` or `BlogPosting` with valid dates and image URLs;
200
222
  - GA4 page view/event hooks that do nothing when analytics is disabled;
201
223
  - GSC verification via a public token or DNS instruction, never a private key.
224
+ - an explicit Ops decision and handoff to `maggie-ops` when private content
225
+ operations are requested; the Ops dashboard is not satisfied by a mock or
226
+ partial bootstrap screen.
202
227
 
203
228
  ### Phase 6: Optional AI CMO integration
204
229
 
@@ -1,27 +1,30 @@
1
1
  ---
2
2
  name: maggie-clone
3
- description: Reverse-engineer one or more authorized websites and create matching pages inside an existing Maggie blog project. Use with /maggie-clone <target-url1> [<target-url2> ...] when a user wants to replicate a page's structure, content, assets, responsive behavior, and interactions.
4
- argument-hint: "<target-url1> [<target-url2> ...]"
3
+ description: Reverse-engineer an authorized website homepage and create the Maggie homepage foundation inside an existing blog project. Use with /maggie-clone plus a homepage URL when the user wants to replicate the homepage structure, header, footer, assets, responsive behavior, and interactions.
5
4
  metadata:
6
5
  version: 1.0.0
7
6
  ---
8
7
 
9
8
  # Maggie Clone
10
9
 
11
- Create one or more website-inspired pages inside an existing Maggie blog
12
- project. This workflow is for authorized sites and pages. It preserves the
13
- host project's bootstrap foundation, public blog routes, content contracts,
14
- SEO safeguards, and existing user work while adding namespaced page output.
10
+ Before any route or asset mutation, follow the shared [Maggie decision loop](../../references/decision-loop.md): inspect, propose, ask focused questions, checkpoint answers, summarize, and confirm.
11
+
12
+ Create the authorized website homepage inside an existing Maggie blog project.
13
+ This is the foundation workflow: it establishes the shared header, footer,
14
+ navigation, brand assets, design tokens, and responsive shell that
15
+ `maggie-design` will reuse for interior pages. It preserves the host project's
16
+ bootstrap foundation, public blog routes, content contracts, SEO safeguards,
17
+ and existing user work while adding namespaced page output.
15
18
 
16
19
  Invoke it as:
17
20
 
18
21
  ```text
19
- /maggie-clone <target-url1> [<target-url2> ...]
22
+ /maggie-clone <homepage-url>
20
23
  ```
21
24
 
22
- The URL path becomes the default destination path. Multiple URLs from one
23
- origin remain separate routes; query strings and fragments are inspected for
24
- state but do not silently create duplicate routes.
25
+ The target must be the origin homepage or an explicitly approved homepage
26
+ variant. If the user supplies an interior page, stop and route the request to
27
+ `maggie-design`; do not use homepage clone to create an interior page.
25
28
 
26
29
  ## Boundaries
27
30
 
@@ -36,9 +39,15 @@ state but do not silently create duplicate routes.
36
39
  - Use real public assets only when the user is authorized to reproduce them.
37
40
  Preserve attribution or licensing requirements and report assets that could
38
41
  not be safely reused.
39
- - The clone is an additive page operation. Do not overwrite an existing route,
40
- component namespace, asset namespace, or research artifact without explicit
41
- approval.
42
+ - A supplied homepage URL is always a create or update request. If the
43
+ homepage route, component namespace, asset namespace, or research artifact
44
+ already exists, inspect it and reconcile/regenerate the requested target;
45
+ never silently skip it because output exists. Preserve unrelated local
46
+ changes and report changed, preserved, and conflicted files. Explicit
47
+ approval is still required for unrelated shared-foundation changes.
48
+ - The homepage header and footer become the shared shell source of truth. Record
49
+ their component paths, tokens, breakpoints, states, and asset dependencies so
50
+ later design pages never fork them.
42
51
  - Keep AI CMO writes, publishing, and external deployment separate from local
43
52
  page construction. A clone does not automatically create, publish, or queue
44
53
  content in Maggie.
@@ -61,7 +70,7 @@ editing and report the missing capability.
61
70
  ```bash
62
71
  python3 tools/clis/maggie.py status <project-root>
63
72
  python3 tools/clis/maggie.py doctor <project-root> --require-bootstrap --strict
64
- python3 tools/clis/maggie_clone.py plan <url1> [<url2> ...] --project <project-root>
73
+ python3 tools/clis/maggie_clone.py plan <homepage-url> --project <project-root>
65
74
  ```
66
75
 
67
76
  The clone planner emits collision-resistant site/page keys and destination
@@ -76,7 +85,7 @@ editing and report the missing capability.
76
85
  approval for a combined route-scoped app. Do not mix global fonts or CSS
77
86
  foundations silently.
78
87
 
79
- Write an output plan before implementation containing, for every URL:
88
+ Write an output plan before implementation containing:
80
89
 
81
90
  ```text
82
91
  source URL -> destination route
@@ -185,7 +194,7 @@ The cloned page remains a Maggie blog project page:
185
194
 
186
195
  ## Phase 4: Verification
187
196
 
188
- For every target:
197
+ For the homepage target:
189
198
 
190
199
  1. Run the host typecheck/lint/build commands and `doctor --strict`.
191
200
  2. Verify the exact destination URL, neighboring blog routes, 404 behavior,
@@ -207,7 +216,8 @@ differences and whether they are known, measured, or unverified.
207
216
 
208
217
  ## Completion report
209
218
 
210
- Report source-to-route mappings, preserved routes, sections/components/specs,
219
+ Report source-to-route mapping, the shared header/footer source of truth,
220
+ preserved routes, sections/components/specs,
211
221
  assets downloaded and failed, files changed, commands run, build/audit status,
212
222
  visual QA result, and known limitations. Deployment, API Pull, rewrite, and
213
223
  publication require a separate explicit request.
@@ -7,6 +7,8 @@ metadata:
7
7
 
8
8
  # Maggie Deployment
9
9
 
10
+ Before changing deployment or production state, follow the shared [Maggie decision loop](../../references/decision-loop.md) and obtain explicit confirmation of target, environment, migrations, and rollback plan.
11
+
10
12
  Deploy a confirmed Maggie blog to a real hosting target. Cloudflare Workers is
11
13
  the default target for dynamic Astro sites; use the Cloudflare reference for
12
14
  Worker, D1, R2, KV, secrets, environments, and verification details.
@@ -0,0 +1,214 @@
1
+ ---
2
+ name: maggie-design
3
+ description: Fully clone authorized interior pages, then reconcile their header and footer to the Maggie homepage shell. Use with /maggie-design plus one or more target URLs for pricing, about, feature, landing, or other non-homepage pages.
4
+ metadata:
5
+ version: 1.1.0
6
+ ---
7
+
8
+ # Maggie Design
9
+
10
+ Create authorized interior pages inside an existing Maggie project. This is
11
+ the interior-page companion to `maggie-clone`, but it is not a shortened
12
+ content-only clone. For every supplied URL, first execute the complete clone
13
+ workflow and create a full first-pass page, including the target's header and
14
+ footer. Then reconcile the page to the already-cloned homepage shell so that
15
+ only the page-specific content remains different.
16
+
17
+ Invoke it as:
18
+
19
+ ```text
20
+ /maggie-design <target-url1> [<target-url2> ...]
21
+ ```
22
+
23
+ Create the execution contract before browser research:
24
+
25
+ ```bash
26
+ python3 tools/clis/maggie_design.py <target-url1> [<target-url2> ...] \\
27
+ --project <project-root> --save
28
+ ```
29
+
30
+ The contract is non-skippable. For every supplied URL, execute every phase in
31
+ `required_phases` in order. `operation` is `create` or `update-regenerate`;
32
+ existing routes, artifacts, or screenshots never change a URL into a skip.
33
+
34
+ Every URL is a required work item. A URL that already has a route, component,
35
+ or research directory means update/regenerate that target; it must never be
36
+ silently skipped because output already exists.
37
+
38
+ ## Hard boundaries
39
+
40
+ - Require a completed `.maggie/bootstrap-state.json` and a completed homepage
41
+ foundation from `maggie-clone`. If either is missing, stop and request it.
42
+ - Reject the origin homepage as a target. Use `maggie-clone` for that job.
43
+ - The target is initially cloned as a complete page. After that first pass,
44
+ replace its header and footer with the homepage's canonical shared shell.
45
+ Reuse the homepage component, tokens, navigation, logo, fonts, breakpoints,
46
+ responsive behavior, accessibility behavior, analytics hooks, and footer
47
+ links. Do not leave a target-specific shell in the final route.
48
+ - The final page-specific scope is the content between the shared shell:
49
+ hero, sections, cards, forms, pricing/features, testimonials, FAQs,
50
+ conversion blocks, and page-specific interactions. Do not copy credentials,
51
+ private data, tracking IDs, authentication, checkout logic, or proprietary
52
+ backend behavior.
53
+ - Keep each page in its own route and component namespace. Existing blog,
54
+ homepage, metadata, sitemap, robots, and Ops routes remain unchanged except
55
+ for the explicitly requested target route update. Preserve unrelated local
56
+ changes, but do not convert an existing target route into a no-op.
57
+
58
+ ## Phase 0: Preflight and decision loop
59
+
60
+ Follow the shared [Maggie decision loop](../../references/decision-loop.md).
61
+ Inspect before editing:
62
+
63
+ ```bash
64
+ python3 tools/clis/maggie.py status <project-root>
65
+ python3 tools/clis/maggie.py doctor <project-root> --require-bootstrap --strict
66
+ python3 tools/clis/maggie_clone.py plan <target-url1> [<target-url2> ...] --project <project-root>
67
+ ```
68
+
69
+ Read and follow the complete `maggie-clone` workflow before proceeding. Verify
70
+ the homepage foundation exists and locate its actual shared shell. The plan
71
+ must state, for every target:
72
+
73
+ ```text
74
+ source URL -> destination route
75
+ homepage shell component/layout -> reused unchanged
76
+ content component namespace -> new isolated namespace
77
+ research and screenshots -> page-scoped directories
78
+ assets -> page-scoped directory; shared assets remain shared
79
+ operation -> create or update/regenerate (never skip)
80
+ files preserved -> all unrelated public/blog/Ops routes
81
+ ```
82
+
83
+ Ask for confirmation of destination routes, page scope, asset reuse, and any
84
+ requested CTA/form behavior before mutation. Interior pages do not silently
85
+ become published posts or sitemap entries. Confirmation authorizes processing
86
+ each listed URL, including an existing route; it does not authorize unrelated
87
+ route changes.
88
+
89
+ ## Phase 1: Complete clone reconnaissance
90
+
91
+ This phase is the full `maggie-clone` reconnaissance contract applied to the
92
+ interior URL. Do not replace it with a content-only scrape. The target header
93
+ and footer are deliberately captured and implemented in the first pass even
94
+ though they will be replaced later.
95
+
96
+ Use the available browser/Chrome/Playwright capability. Capture desktop
97
+ 1440px, tablet 768px, and mobile 390px. Run the same full reconnaissance
98
+ contract as `maggie-clone`, including the target header and footer. They must
99
+ be captured because the first-pass clone needs complete evidence before the
100
+ shell is reconciled. Record:
101
+
102
+ - full page topology, including header, footer, sticky layers, and shell
103
+ transitions;
104
+ - content topology and section order;
105
+ - exact visible copy, links, labels, images, forms, and public states;
106
+ - content-area typography, spacing, colors, borders, radii, shadows, and
107
+ responsive changes, comparing every value with the homepage tokens;
108
+ - hover, focus, open, loading, empty, error, scroll, tab, and reduced-motion
109
+ behavior;
110
+ - required assets and their reuse/licensing notes;
111
+ - page metadata and whether the page is a marketing route, conversion route,
112
+ or actual blog content.
113
+
114
+ Persist page-scoped evidence before building:
115
+
116
+ ```text
117
+ docs/research/<site-key>/<page-key>/
118
+ OUTPUT_PLAN.md
119
+ PAGE_TOPOLOGY.md
120
+ BEHAVIORS.md
121
+ DESIGN_TOKENS.md
122
+ COMPONENT_INVENTORY.md
123
+ ASSET_MANIFEST.md
124
+ docs/design-references/<site-key>/<page-key>/
125
+ desktop.png tablet.png mobile.png
126
+ ```
127
+
128
+ Every new component spec must initially document the full target component,
129
+ then include a reconciliation note naming the homepage shell source file and
130
+ the final content-only target file and states.
131
+
132
+ ## Phase 2: Build the complete first-pass page
133
+
134
+ The first pass is a real full clone, not a mock, analysis-only artifact, or
135
+ shell-free approximation. It must contain the target header, target footer,
136
+ page content, responsive behavior, assets, and observed interactions.
137
+
138
+ Build the target page from the full clone evidence, including its observed
139
+ header, footer, responsive shell, assets, and interactions. This first pass is
140
+ required even when the final result will replace the shell. For an existing
141
+ target route, inspect the current implementation, calculate the update plan,
142
+ and regenerate/reconcile that route; report changed and preserved files.
143
+
144
+ Do not report `already exists` as a completion state and do not skip browser
145
+ research, asset checks, or verification for an existing URL.
146
+
147
+ ## Phase 3: Reconcile to the homepage shell
148
+
149
+ After the complete first pass, make the homepage shell the sole source of
150
+ truth. Put page-specific output under an isolated namespace, for example:
151
+
152
+ ```text
153
+ src/components/sites/<site-key>/<page-key>/
154
+ src/pages/<approved-route>.*
155
+ public/sites/<site-key>/<page-key>/
156
+ ```
157
+
158
+ Perform this reconciliation explicitly:
159
+
160
+ 1. Identify the generated target header/footer and the homepage header/footer
161
+ components, layout wrappers, tokens, assets, and behavior contracts.
162
+ 2. Replace the generated target shell with imports/composition from the
163
+ homepage source of truth. Do not copy its markup into the page namespace.
164
+ 3. Remove target-only shell styles, duplicate logo/font/icon assets, and
165
+ duplicate navigation/footer links from the final page bundle.
166
+ 4. Keep the target's route-specific content and interactions inside the
167
+ content region. A global token or shared-shell change requires explicit
168
+ approval.
169
+ 5. Verify the resulting DOM has one header and one footer, with the target
170
+ content between them and no hidden duplicate shell.
171
+
172
+ This is a replacement step after the first pass, never a substitute for it.
173
+ The final implementation must not retain the target header/footer as an
174
+ alternative path or silently fall back to them when the homepage shell exists.
175
+
176
+ The final page must:
177
+
178
+ - render inside the exact homepage shell and use its skip link, landmarks,
179
+ navigation, footer, fonts, and responsive breakpoints;
180
+ - use the existing metadata helper and canonical route;
181
+ - use normal anchors and real form validation;
182
+ - keep marketing pages out of the post sitemap and Article JSON-LD unless they
183
+ are explicitly mapped to the canonical post contract;
184
+ - preserve analytics consent and server-only API keys;
185
+ - implement observed interactions rather than guessed click-only substitutes.
186
+
187
+ If a page needs a new global navigation item, footer link, font, token, or
188
+ shared component change, stop, show the impact, and obtain explicit approval.
189
+
190
+ ## Phase 4: Verify
191
+
192
+ For every target:
193
+
194
+ 1. Run the host build, typecheck/lint, and `maggie doctor --strict` gates.
195
+ 2. Verify the exact destination route, homepage route, blog routes, 404,
196
+ robots, sitemap exclusion/inclusion, canonical metadata, and JSON-LD.
197
+ 3. Compare the complete first-pass clone to the target at 1440px, 768px, and
198
+ 390px. Then compare the final page shell to the homepage and compare only
199
+ the final content region to the target for page fidelity.
200
+ 4. Sweep keyboard focus, links, forms, hover, tabs/dialogs, scroll behavior,
201
+ mobile menu, and reduced-motion behavior.
202
+ 5. Run `python3 tools/clis/site_audit.py <local-or-production-url> --json`.
203
+
204
+ Do not claim exact fidelity when a dynamic state, asset, or authenticated
205
+ target could not be inspected. Report measured differences and limitations.
206
+
207
+ ## Completion report
208
+
209
+ Report every input URL and its create/update result, target-to-route mappings,
210
+ the full first-pass artifacts, reused homepage shell files, removed duplicate
211
+ shell artifacts, final content-only components, research/screenshots, assets,
212
+ preserved unrelated routes, commands and test results, visual QA status, and
213
+ any requested global changes pending approval. Deployment, publishing, API
214
+ Pull, and rewrite operations are separate actions.
@@ -0,0 +1,163 @@
1
+ ---
2
+ name: maggie-ops
3
+ description: Build, connect, operate, upgrade, and verify a private Maggie Ops dashboard for blog content, API Pull, sitemap matching, rewrite approvals, reports, and integrations. Use when the user asks for Ops/admin functionality or lifecycle operations rather than public blog pages.
4
+ metadata:
5
+ version: 1.0.0
6
+ ---
7
+
8
+ # Maggie Ops
9
+
10
+ Manage the private operational surface for a Maggie blog. This skill is
11
+ separate from `maggie-blog-bootstrap`: bootstrap establishes the public blog
12
+ contract, while Maggie Ops establishes the authenticated editor and operations
13
+ contract around it.
14
+
15
+ Invoke it as:
16
+
17
+ ```text
18
+ /maggie-ops <audit|install|connect|operate|upgrade|verify>
19
+ ```
20
+
21
+ If the user does not name a mode, inspect the project and propose the smallest
22
+ mode that satisfies the request. Do not rebuild the public blog or change its
23
+ framework just to add Ops.
24
+
25
+ ## Required preflight
26
+
27
+ 1. Inspect `.maggie/analysis.json` and `.maggie/bootstrap-state.json`.
28
+ 2. Read [`ops-dashboard-contract.md`](../../references/ops-dashboard-contract.md).
29
+ 3. Inventory existing private routes, session/auth middleware, role checks,
30
+ API proxy routes, content tables, audit events, robots, and deployment
31
+ configuration.
32
+ 4. Run the host's existing typecheck, test, and build commands before editing.
33
+ 5. State which mode is being run, which routes/API resources it will touch,
34
+ and whether any operation can consume quota or change production state.
35
+
36
+ Do not proceed with a production mutation when authentication, ownership,
37
+ role authorization, secret configuration, or rollback information is unknown.
38
+
39
+ ## Modes
40
+
41
+ ### Audit
42
+
43
+ Produce a gap report against the Ops contract. Check the actual rendered
44
+ screens and server routes, not just documentation. Classify every item as
45
+ implemented, partial, missing, or unverifiable. Include:
46
+
47
+ - dashboard/list/editor/detail screens and responsive states;
48
+ - session authentication, role authorization, CSRF/input validation, and
49
+ server-only secret handling;
50
+ - content inventory and identity editing;
51
+ - API Pull status, project context, delivery state, and idempotency;
52
+ - sitemap sources, matching runs, unmatched URLs, matched assets, and history;
53
+ - rewrite queue, preview quota reservation, approval/rejection transitions;
54
+ - reports, GSC/GA4/provider health, audit events, noindex, and robots rules.
55
+
56
+ ### Install
57
+
58
+ Add or complete the private Ops surface in the existing project. Follow the
59
+ host's UI system and routing conventions, but use the EmDash-style information
60
+ architecture as the baseline:
61
+
62
+ ```text
63
+ /ops dashboard and pending actions
64
+ /ops/posts paginated content list
65
+ /ops/posts/[id]/edit validated editor
66
+ /ops/posts/[id]/preview noindex preview
67
+ /ops/sitemap sources, match runs, history, eligibility
68
+ /ops/reports SEO, GEO, visibility, analytics
69
+ /ops/settings/integrations Maggie API and provider health
70
+ /ops/operations calendar, tasks, media, redirects, agency work
71
+ ```
72
+
73
+ Use a clean list → detail/editor journey, outlined cards, clear status
74
+ chips, predictable spacing, explicit loading/empty/error states, and a
75
+ desktop/mobile layout consistent with the reference admin product. Do not
76
+ ship a mock table whose buttons do not reach a server route.
77
+
78
+ ### Connect
79
+
80
+ Wire the dashboard to real backend contracts. Prefer the first-party Maggie
81
+ API or a narrowly scoped server-side Ops bridge. The browser may call only
82
+ the project's authenticated proxy; Maggie/API secrets must remain server-side.
83
+
84
+ At minimum, verify real readbacks for:
85
+
86
+ - content assets and versions/identity;
87
+ - pull runs and delivery status;
88
+ - sitemap sources and matching runs;
89
+ - rewrite jobs and action state;
90
+ - project context and CTA/settings data.
91
+
92
+ Every mutation must validate the session, role, resource ownership/project
93
+ scope, input schema, legal state transition, and idempotency/correlation key.
94
+ Record actor, timestamp, previous state, next state, reason, and result.
95
+
96
+ ### Operate
97
+
98
+ Use the dashboard or API to perform an explicitly requested operation. Show an
99
+ impact preview before bulk or quota-consuming work. Keep these actions
100
+ separate:
101
+
102
+ ```text
103
+ read status → inspect impact → run/mutate → read back result → record audit
104
+ ```
105
+
106
+ Sitemap matching must report total sitemap documents, total URLs, matched
107
+ posts, already matched/queued posts, not-tracked URLs, newly eligible posts,
108
+ and matching time. Rewrite preview reserves quota; approval is a separate
109
+ human action. Never silently publish, overwrite content, or enqueue a whole
110
+ site because a list endpoint returned data.
111
+
112
+ ### Upgrade
113
+
114
+ Upgrade an existing Ops installation without losing local work:
115
+
116
+ 1. read the installed version and local modifications;
117
+ 2. compare the target contract, frontend routes, backend routes, and schema;
118
+ 3. apply additive changes first and write migrations only when required;
119
+ 4. preserve local UI customizations unless the user explicitly requests a
120
+ replacement;
121
+ 5. run read-only verification before enabling new mutations;
122
+ 6. report changed, preserved, conflicting, and deprecated files.
123
+
124
+ For an installed Maggie package, use:
125
+
126
+ ```bash
127
+ npx @topy-ai/maggie update --project .
128
+ ```
129
+
130
+ Use `--force` only after the user explicitly approves replacing locally
131
+ modified managed files. An npm skill update does not automatically deploy the
132
+ dashboard or migrate a production database.
133
+
134
+ ### Verify
135
+
136
+ Run the host build/test/typecheck gates and verify the actual private routes
137
+ with an authenticated session. Test at least:
138
+
139
+ - unauthenticated and non-admin denial;
140
+ - list, detail, editor save, and validation errors;
141
+ - API Pull readback and stale/error states;
142
+ - sitemap matching with zero, partial, and complete matches;
143
+ - rewrite preview, approval, rejection, retry, and quota failure;
144
+ - refresh/idempotency behavior and audit records;
145
+ - `noindex`, robots blocking, and exclusion from public sitemap;
146
+ - desktop and mobile layout against the EmDash-style design baseline.
147
+
148
+ Do not claim an API is connected because a frontend button rendered. Show the
149
+ endpoint, response status, persisted state, and verification command.
150
+
151
+ ## Stop conditions
152
+
153
+ Stop before a write when the user has not authorized it, the project scope is
154
+ ambiguous, the endpoint is only proposed/documented rather than reachable,
155
+ the admin identity cannot be verified, the operation would expose a secret,
156
+ or a migration/rollback plan is missing.
157
+
158
+ ## Completion report
159
+
160
+ Report the mode, screens and endpoints implemented, authentication/ownership
161
+ boundary, quota impact, database/migration status, audit behavior, preserved
162
+ local changes, commands and live/readback checks, remaining gaps, and whether
163
+ deployment was actually performed.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Maggie Ops"
3
+ short_description: "Build and operate Maggie blog admin workflows"
4
+ default_prompt: "Use $maggie-ops to audit and connect this project's private blog operations dashboard."
@@ -7,6 +7,8 @@ metadata:
7
7
 
8
8
  # Maggie Project Context
9
9
 
10
+ For changes, follow the shared [Maggie decision loop](../../references/decision-loop.md): inspect the project, propose mappings, checkpoint answers, and confirm before writing context or calling external APIs.
11
+
10
12
  Use the authenticated AI CMO Project as the source of truth for brand
11
13
  positioning, audience, voice, site URL, and conversion CTAs.
12
14
 
@@ -7,6 +7,8 @@ metadata:
7
7
 
8
8
  # Maggie SEO and GEO
9
9
 
10
+ For state-changing audits, rewrites, or publishing, follow the shared [Maggie decision loop](../../references/decision-loop.md) and confirm policy and scope before external writes.
11
+
10
12
  Treat SEO and GEO as one measurable content system: people-first content,
11
13
  technical accessibility, extractable structure, evidence, authority, brand
12
14
  voice, CTA alignment, and observed search/AI outcomes.
@@ -7,6 +7,8 @@ metadata:
7
7
 
8
8
  # Maggie Social Share
9
9
 
10
+ For scheduling or publishing actions, follow the shared [Maggie decision loop](../../references/decision-loop.md): inspect, propose channels and copy, checkpoint answers, then confirm.
11
+
10
12
  Social Share is a distribution layer downstream of the same AI CMO Project,
11
13
  brand voice, CTA, canonical URL, content asset, and approval state used by SEO
12
14
  and GEO. It is not a second content source of truth.
@@ -397,6 +397,83 @@ def command_status(args: argparse.Namespace) -> int:
397
397
  return 0 if state.get("status") == "completed" else 1
398
398
 
399
399
 
400
+ BOOTSTRAP_QUESTIONS = [
401
+ ("framework", "Which framework should Maggie preserve?", ["detected", "astro", "nextjs", "eleventy", "custom"]),
402
+ ("language", "Which implementation language should the blog use?", ["detected", "typescript", "javascript"]),
403
+ ("content_language", "What is the default content language?", ["detected", "en", "zh-TW", "zh-CN", "multilingual"]),
404
+ ("ui_system", "Which UI system should the blog use?", ["preserve", "tailwind", "css-modules", "plain-css"]),
405
+ ("icon_set", "Which icon set should the blog use?", ["preserve", "heroicons", "lucide", "none"]),
406
+ ("font", "Which typography choice should the blog use?", ["preserve", "system", "custom"]),
407
+ ("database", "Which database boundary should the blog use?", ["preserve", "sqlite", "postgres", "mysql"]),
408
+ ("content_source", "Where should posts be stored?", ["preserve", "local-content", "database", "api-pull"]),
409
+ ("posts_per_page", "How many posts should appear on each blog page?", ["9", "12", "6", "custom"]),
410
+ ("grid_columns", "How many columns should the default post grid use?", ["3", "2", "4", "custom"]),
411
+ ("ops_dashboard", "Should the project add the private Maggie Ops dashboard after bootstrap?", ["disabled", "enabled"]),
412
+ ]
413
+
414
+
415
+ def question_default(key: str, analysis: dict) -> str:
416
+ detected = analysis.get(key, {}).get("value") if isinstance(analysis.get(key), dict) else None
417
+ if key == "posts_per_page": return "9"
418
+ if key == "grid_columns": return "3"
419
+ if key == "ops_dashboard": return "disabled"
420
+ return detected or {"ui_system": "preserve-or-tailwind", "icon_set": "preserve-or-heroicons", "font": "preserve-or-system", "database": "preserve-or-sqlite"}.get(key, "detected")
421
+
422
+
423
+ def interview_choices(key: str, choices: list[str], current: str) -> str:
424
+ print(f"\n{key}: {current}")
425
+ for index, choice in enumerate(choices, 1):
426
+ marker = " (recommended)" if choice == current else ""
427
+ print(f" {index}. {choice}{marker}")
428
+ while True:
429
+ answer = input("Select a number, or type a value: ").strip()
430
+ if answer.isdigit() and 1 <= int(answer) <= len(choices):
431
+ return choices[int(answer) - 1]
432
+ if answer in choices or (key in {"posts_per_page", "grid_columns"} and answer.isdigit()):
433
+ return answer
434
+ print("Please choose one of the listed options.")
435
+
436
+
437
+ def command_bootstrap_interview(args: argparse.Namespace) -> int:
438
+ root = Path(args.project).resolve()
439
+ analysis = detect(root)
440
+ answers: dict[str, str] = {}
441
+ if args.answers_file:
442
+ try:
443
+ loaded = json.loads(Path(args.answers_file).read_text(encoding="utf-8"))
444
+ except (OSError, json.JSONDecodeError) as error:
445
+ raise RuntimeError(f"invalid answers file: {args.answers_file}") from error
446
+ if not isinstance(loaded, dict): raise RuntimeError("answers file must contain a JSON object")
447
+ answers.update({str(key): str(value) for key, value in loaded.items()})
448
+ for key, question, choices in BOOTSTRAP_QUESTIONS:
449
+ value = answers.get(key, question_default(key, analysis))
450
+ if not args.non_interactive and key not in answers:
451
+ print(f"\n{question}")
452
+ value = interview_choices(key, choices, value)
453
+ elif value not in choices and key not in {"posts_per_page", "grid_columns"}:
454
+ raise RuntimeError(f"invalid value for {key}: {value}")
455
+ answers[key] = value
456
+ checkpoint = root / STATE_DIR / "bootstrap-interview.json"
457
+ checkpoint.parent.mkdir(parents=True, exist_ok=True)
458
+ checkpoint.write_text(json.dumps({"status": "in_progress", "answers": answers, "updated_at": utc_now()}, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
459
+ print("\nBootstrap proposal")
460
+ print(json.dumps(answers, indent=2, ensure_ascii=False))
461
+ if not args.confirm:
462
+ if args.non_interactive:
463
+ print("CONFIRMATION_REQUIRED: rerun with --confirm after reviewing the proposal.", file=sys.stderr)
464
+ return 2
465
+ if input("Apply these selections and generate the bootstrap contract? [y/N] ").strip().lower() not in {"y", "yes"}:
466
+ print("Bootstrap remains pending; the checkpoint was saved.")
467
+ return 0
468
+ complete_args = argparse.Namespace(
469
+ project=str(root), confirm=["foundation", "experience", "data", "publishing"],
470
+ framework=answers["framework"], language=answers["language"], content_language=answers["content_language"],
471
+ ui_system=answers["ui_system"], icon_set=answers["icon_set"], font=answers["font"], database=answers["database"], content_source=answers["content_source"],
472
+ posts_per_page=int(answers["posts_per_page"]), grid_columns=int(answers["grid_columns"]), ops_dashboard=answers["ops_dashboard"] == "enabled",
473
+ )
474
+ return command_complete(complete_args)
475
+
476
+
400
477
  def command_complete(args: argparse.Namespace) -> int:
401
478
  root = Path(args.project).resolve()
402
479
  required = {"foundation", "experience", "data", "publishing"}
@@ -406,7 +483,7 @@ def command_complete(args: argparse.Namespace) -> int:
406
483
  print("Confirmation required for: " + ", ".join(sorted(missing)), file=sys.stderr)
407
484
  return 2
408
485
  state = {
409
- "schema_version": "1.0",
486
+ "schema_version": "1.1",
410
487
  "status": "completed",
411
488
  "completed_at": utc_now(),
412
489
  "confirmed": {name: True for name in sorted(required)},
@@ -419,6 +496,9 @@ def command_complete(args: argparse.Namespace) -> int:
419
496
  "font": args.font,
420
497
  "database": args.database,
421
498
  "content_source": args.content_source,
499
+ "posts_per_page": args.posts_per_page,
500
+ "grid_columns": args.grid_columns,
501
+ "ops_dashboard": args.ops_dashboard,
422
502
  },
423
503
  }
424
504
  path = state_path(root)
@@ -427,8 +507,8 @@ def command_complete(args: argparse.Namespace) -> int:
427
507
  contracts = {
428
508
  "project.json": {"schema_version": "1.0", "framework": args.framework, "language": args.language, "database": args.database, "content_source": args.content_source},
429
509
  "decisions.json": state["decisions"],
430
- "schema.json": {"post_required": ["id", "slug", "title", "content", "status", "canonicalUrl", "publishedAt", "updatedAt", "author"], "public_status": "published"},
431
- "routes.json": {"public": ["/about", "/blog", "/blog/[slug]", "/topics", "/authors/[slug]", "/sitemap.xml", "/robots.txt"], "ops": ["/ops", "/ops/posts", "/ops/operations", "/ops/wordpress", "/api/ops/bulk", "/api/ops/agency", "/api/ops/entities", "/api/ops/migrations/[id]/resume", "/api/ops/wordpress/migration-plan", "/api/ops/pull/project-context", "/api/ops/pull/sync-updates", "/api/ops/sitemap/matching-history", "/api/ops/content-tracking/report-state", "/api/ops/content-tracking/report-state/batch", "/api/ops/playbooks/[id]/run"]},
510
+ "schema.json": {"post_required": ["id", "slug", "title", "content", "status", "canonicalUrl", "publishedAt", "updatedAt", "author"], "public_status": "published", "posts_per_page": args.posts_per_page, "grid_columns": args.grid_columns, "grid_default": f"{args.grid_columns}x{args.posts_per_page // args.grid_columns}"},
511
+ "routes.json": {"public": ["/about", "/blog", "/blog/page/[page]", "/blog/[slug]", "/topics", "/topics/[slug]", "/authors/[slug]", "/sitemap.xml", "/robots.txt"], "ops": ["/ops", "/ops/posts", "/ops/posts/new", "/ops/posts/[id]/edit", "/ops/posts/[id]/preview", "/ops/topics", "/ops/sitemap", "/ops/reports", "/ops/settings/site", "/ops/settings/integrations", "/ops/operations", "/ops/wordpress", "/api/ops/summary", "/api/ops/posts", "/api/ops/topics", "/api/ops/bulk", "/api/ops/agency", "/api/ops/entities", "/api/ops/migrations/import", "/api/ops/migrations/[id]/resume", "/api/ops/wordpress/migration-plan", "/api/ops/pull/project-context", "/api/ops/pull/sync", "/api/ops/pull/sync-updates", "/api/ops/sitemap/matching-history", "/api/ops/sitemap/match", "/api/ops/sitemap/auto-detect", "/api/ops/rewrite/queue", "/api/ops/rewrite/history", "/api/ops/rewrite/policy", "/api/ops/content-tracking/report-state", "/api/ops/content-tracking/report-state/batch", "/api/ops/reports", "/api/ops/playbooks/[id]/run"]},
432
512
  "integrations.json": {"ai_cmo": "server-only", "gsc": "server-only", "ga4": "consent-aware"},
433
513
  "migration.json": {"supported": ["wordpress-rest", "wxr", "csv", "json", "sitemap", "media-archive"], "idempotency": "source-id-and-checksum", "default_conflict": "manual", "sync_modes": ["all", "new", "modified"], "deleted_policy": "archive"},
434
514
  }
@@ -511,10 +591,12 @@ def command_doctor(args: argparse.Namespace) -> int:
511
591
  "faq_surface": "faq" in text.lower(),
512
592
  "cta_surface": "cta" in text.lower(),
513
593
  "pagination_surface": "pagination" in text.lower() or "page_size" in text.lower(),
594
+ "nine_posts_three_columns": bool(re.search(r"(?:postsPerPage|posts_per_page|PUBLIC_POSTS_PER_PAGE)[^\n]{0,80}(?:9|default.?9)", text, re.IGNORECASE)) and ("grid" in text.lower() or "grid_columns" in text.lower()),
514
595
  "eeat_authorship": "author" in text.lower() and ("credential" in text.lower() or "organisation" in text.lower() or "organization" in text.lower()),
515
596
  "modified_date": "dateModified" in text or "updatedAt" in text,
516
597
  "ops_surface": any("ops" in path for path in paths),
517
598
  "ops_authentication": "auth" in text.lower() or "middleware" in text.lower(),
599
+ "ops_api_surface": all(token in text for token in ("/api/ops/summary", "/api/ops/posts", "/api/ops/sitemap", "/api/ops/reports")),
518
600
  "integration_config": "AI_CMO_API_KEY" in text or "measurement_id" in text.lower() or "verification" in text.lower(),
519
601
  "bootstrap_contract": all((root / STATE_DIR / filename).exists() for filename in ("project.json", "decisions.json", "schema.json", "routes.json", "integrations.json", "migration.json")),
520
602
  "scheduled_publication_state": "scheduled" in text.lower() and "published" in text.lower(),
@@ -610,9 +692,18 @@ def parser() -> argparse.ArgumentParser:
610
692
  bootstrap_sub = bootstrap.add_subparsers(dest="bootstrap_command", required=True)
611
693
  complete = bootstrap_sub.add_parser("complete", help="write state after explicit user confirmations")
612
694
  complete.add_argument("project", nargs="?", default=".")
695
+ interview = bootstrap_sub.add_parser("interview", help="walk through one decision at a time and confirm the proposal")
696
+ interview.add_argument("project", nargs="?", default=".")
697
+ interview.add_argument("--answers-file", help="JSON answers for headless/CI use")
698
+ interview.add_argument("--non-interactive", action="store_true", help="do not prompt; requires --answers-file and --confirm")
699
+ interview.add_argument("--confirm", action="store_true", help="apply the reviewed proposal")
700
+ interview.set_defaults(func=command_bootstrap_interview)
613
701
  complete.add_argument("--confirm", action="append", choices=["foundation", "experience", "data", "publishing"], required=True)
614
702
  for name, default in (("framework", "detected"), ("language", "detected"), ("content-language", "confirmed"), ("ui-system", "preserve-or-tailwind"), ("icon-set", "preserve-or-heroicons"), ("font", "preserve-or-system"), ("database", "preserve-or-sqlite"), ("content-source", "confirmed")):
615
703
  complete.add_argument("--" + name, dest=name.replace("-", "_"), default=default)
704
+ complete.add_argument("--posts-per-page", type=int, default=9)
705
+ complete.add_argument("--grid-columns", type=int, default=3)
706
+ complete.add_argument("--ops-dashboard", action=argparse.BooleanOptionalAction, default=False)
616
707
  complete.set_defaults(func=command_complete)
617
708
  generate = sub.add_parser("generate", help="generate stable contract or fixture files")
618
709
  generate.add_argument("target", choices=["contract", "fixture", "post", "topic", "author", "migration", "seo", "ops-page"])
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env python3
2
+ """Create a non-skippable execution contract for Maggie interior-page design.
3
+
4
+ This command does not fetch a site or edit the host project. It gives an
5
+ agent a machine-readable checklist so an interior URL cannot accidentally be
6
+ treated as a content-only clone or skipped because output already exists.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ from pathlib import Path
14
+ from urllib.parse import urlsplit
15
+
16
+ from maggie_clone import normalized, plan
17
+
18
+
19
+ PHASES = [
20
+ {
21
+ "id": "preflight",
22
+ "required": [
23
+ "bootstrap-state",
24
+ "homepage-shell",
25
+ "host-inventory",
26
+ "baseline-validation",
27
+ ],
28
+ },
29
+ {
30
+ "id": "full-research",
31
+ "required": [
32
+ "desktop-1440",
33
+ "tablet-768",
34
+ "mobile-390",
35
+ "target-header",
36
+ "target-footer",
37
+ "assets",
38
+ "interactions",
39
+ "component-specs",
40
+ ],
41
+ },
42
+ {
43
+ "id": "full-first-pass",
44
+ "required": [
45
+ "target-header",
46
+ "target-footer",
47
+ "page-content",
48
+ "responsive-behavior",
49
+ "target-interactions",
50
+ ],
51
+ },
52
+ {
53
+ "id": "shell-reconciliation",
54
+ "required": [
55
+ "replace-target-header-with-homepage-shell",
56
+ "replace-target-footer-with-homepage-shell",
57
+ "remove-duplicate-shell-assets-and-styles",
58
+ "one-header-and-one-footer",
59
+ ],
60
+ },
61
+ {
62
+ "id": "verification",
63
+ "required": [
64
+ "build-typecheck-lint",
65
+ "route-and-404",
66
+ "metadata-robots-sitemap-jsonld",
67
+ "shell-regression",
68
+ "desktop-tablet-mobile-comparison",
69
+ "site-audit",
70
+ ],
71
+ },
72
+ ]
73
+
74
+
75
+ def existing_evidence(project: Path, target: dict[str, str]) -> list[str]:
76
+ """Return evidence paths without deciding that any work can be skipped."""
77
+
78
+ candidates = [
79
+ Path(target["artifact_root"]),
80
+ Path(target["screenshot_root"]),
81
+ Path(target["component_root"]),
82
+ Path(target["asset_root"]),
83
+ ]
84
+ route = target["destination_route"].strip("/") or "root"
85
+ for base in (project / "src" / "app", project / "src" / "pages"):
86
+ if base.exists():
87
+ candidates.extend(
88
+ path
89
+ for path in base.rglob("*")
90
+ if path.is_file() and route.lower() in str(path).lower()
91
+ )
92
+ return sorted({str(path) for path in candidates if path.exists()})
93
+
94
+
95
+ def design_plan(urls: list[str], project: Path) -> dict[str, object]:
96
+ targets = []
97
+ for target in plan(urls, project):
98
+ parsed = urlsplit(target["source_url"])
99
+ if parsed.path in {"", "/"}:
100
+ raise ValueError(
101
+ f"homepage URL is not valid for maggie-design: {target['source_url']} "
102
+ "(use maggie-clone)"
103
+ )
104
+ evidence = existing_evidence(project, target)
105
+ targets.append(
106
+ {
107
+ **target,
108
+ "operation": "update-regenerate" if evidence else "create",
109
+ "input_url_is_required_work_item": True,
110
+ "skip_allowed": False,
111
+ "existing_evidence": evidence,
112
+ "required_phases": PHASES,
113
+ "shell_source_of_truth": {
114
+ "must_resolve_before_build": True,
115
+ "must_be_reused_in_final_page": True,
116
+ "target_shell_allowed_in_final_page": False,
117
+ },
118
+ }
119
+ )
120
+ return {
121
+ "workflow": "maggie-design",
122
+ "contract_version": "1.0",
123
+ "project_root": str(project),
124
+ "non_skippable": True,
125
+ "rule": "Every supplied URL is processed through all required phases; existing output changes the operation to update-regenerate, never skip.",
126
+ "targets": targets,
127
+ }
128
+
129
+
130
+ def main() -> int:
131
+ parser = argparse.ArgumentParser(description=__doc__)
132
+ parser.add_argument("urls", nargs="+", help="authorized interior-page URLs")
133
+ parser.add_argument("--project", type=Path, default=Path.cwd())
134
+ parser.add_argument("--save", action="store_true", help="save .maggie/design-plan.json")
135
+ args = parser.parse_args()
136
+ try:
137
+ project = args.project.resolve()
138
+ result = design_plan(args.urls, project)
139
+ except ValueError as error:
140
+ parser.error(str(error))
141
+ encoded = json.dumps(result, indent=2, ensure_ascii=False) + "\n"
142
+ if args.save:
143
+ destination = project / ".maggie" / "design-plan.json"
144
+ destination.parent.mkdir(parents=True, exist_ok=True)
145
+ destination.write_text(encoded, encoding="utf-8")
146
+ result["saved_to"] = str(destination)
147
+ encoded = json.dumps(result, indent=2, ensure_ascii=False) + "\n"
148
+ print(encoded, end="")
149
+ return 0
150
+
151
+
152
+ if __name__ == "__main__":
153
+ raise SystemExit(main())
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topy-ai/maggie",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Install and manage Maggie Skills for AI coding agents",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -56,6 +56,14 @@ with the canonical post source enum while remaining unique and rerunnable.
56
56
  This is the default for a new, small single-instance project. Use the same
57
57
  logical fields with Postgres or another engine when the deployment requires it.
58
58
 
59
+ ## Default listing configuration
60
+
61
+ The default blog index is a 3-column grid with 9 posts per page (3×3). Store
62
+ this as typed configuration (`postsPerPage: 9`, `gridColumns: 3`) rather than
63
+ scattering literals through templates. Users may change it later through site
64
+ settings; pagination, canonical URLs, sitemap eligibility, and audits must use
65
+ the same configured value.
66
+
59
67
  ```sql
60
68
  CREATE TABLE posts (
61
69
  id TEXT PRIMARY KEY,
@@ -0,0 +1,25 @@
1
+ # Maggie Decision Loop
2
+
3
+ Every state-changing Maggie skill follows this protocol:
4
+
5
+ 1. Inspect first and label evidence `Detected`, `Likely`, `Missing`, or
6
+ `Unknown`.
7
+ 2. Propose one recommended choice and up to two alternatives.
8
+ 3. Ask one focused question at a time, showing the selection, evidence,
9
+ affected files, and risks.
10
+ 4. Record each answer in `.maggie/decisions.json` or a skill checkpoint.
11
+ 5. Summarize all selections and require explicit final confirmation before
12
+ mutation or external writes.
13
+ 6. If an earlier answer changes, recalculate dependent choices and confirm
14
+ again; never silently overwrite a confirmed decision.
15
+
16
+ Headless runs use an answers JSON file plus a separate `--confirm` flag. They
17
+ must follow the same sequence and may not bypass final confirmation.
18
+
19
+ ```bash
20
+ python3 tools/clis/maggie.py bootstrap interview .
21
+ python3 tools/clis/maggie.py bootstrap interview . --answers-file .maggie/answers.json --non-interactive --confirm
22
+ ```
23
+
24
+ Use this loop for framework changes, clone routes, rewrite policy, publishing,
25
+ integrations, migrations, and deployment.
@@ -0,0 +1,41 @@
1
+ # Maggie Ops Dashboard Contract
2
+
3
+ Bootstrap must implement a private, authenticated Ops application before the
4
+ blog is complete. Visual design is flexible; the operational surface and
5
+ server-side boundaries are fixed.
6
+
7
+ ## Required screens
8
+
9
+ ```text
10
+ /ops health summary and pending actions
11
+ /ops/posts inventory, filters, bulk preview/apply
12
+ /ops/posts/new create draft
13
+ /ops/posts/[id]/edit validated editor and metadata preview
14
+ /ops/posts/[id]/preview noindex preview
15
+ /ops/topics topics and FAQs
16
+ /ops/sitemap sources, matching runs, unmatched and eligible counts
17
+ /ops/reports SEO, content quality, visibility, analytics
18
+ /ops/settings/site origin, locale, timezone and defaults
19
+ /ops/settings/integrations Maggie API, GSC, GA4 and provider health
20
+ /ops/operations calendar, tasks, media, redirects and agency work
21
+ /ops/wordpress migration preview, apply, validate and resume
22
+ ```
23
+
24
+ Required server routes include `/api/ops/summary`, `/api/ops/posts`,
25
+ `/api/ops/topics`, `/api/ops/sitemap/{runs,match,auto-detect}`,
26
+ `/api/ops/reports`, `/api/ops/pull/{project-context,sync,sync-updates}`,
27
+ `/api/ops/rewrite/{queue,history,policy}`, and content-tracking report-state.
28
+
29
+ Every request enforces session authentication, role authorization, input
30
+ validation, and state-transition validation. Keys never reach browser code.
31
+ Mutations show an impact preview where applicable and create an audit event
32
+ with actor, timestamp, previous state, next state, reason, and an
33
+ idempotency/correlation key. Ops pages are `noindex`, excluded from public
34
+ sitemaps, and blocked in robots rules.
35
+
36
+ ## Bootstrap acceptance
37
+
38
+ The bootstrap manifest contains the complete Ops route set, an enabled
39
+ dashboard decision, and a verification command. `maggie doctor --strict
40
+ --require-bootstrap` fails when the host project has no Ops UI, Ops API, or
41
+ authentication boundary.