@topy-ai/maggie 0.1.0 → 0.1.1

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,11 @@ 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",
15
18
  "maggie-deployment",
16
19
  "maggie-project-context",
17
20
  "maggie-seo-geo",
@@ -24,6 +27,7 @@ function usage() {
24
27
  Usage:
25
28
  maggie init [--project PATH] [--agent auto|codex|claude|all] [--skills LIST]
26
29
  maggie install [SKILL ...] [--project PATH] [--agent auto|codex|claude|all]
30
+ maggie update [SKILL ...] [--project PATH] [--agent auto|codex|claude|all] [--force]
27
31
  maggie list
28
32
  maggie doctor [--project PATH]
29
33
  maggie remove [SKILL ...] [--project PATH] [--agent codex|claude|all]
@@ -77,6 +81,35 @@ function copyIfMissing(source, target, force = false) {
77
81
  return "installed";
78
82
  }
79
83
 
84
+ function digest(path) {
85
+ return createHash("sha256").update(readFileSync(path)).digest("hex");
86
+ }
87
+
88
+ function syncTree(source, target, force) {
89
+ let changed = 0;
90
+ for (const entry of readdirSync(source, { withFileTypes: true })) {
91
+ const sourcePath = join(source, entry.name);
92
+ const targetPath = join(target, entry.name);
93
+ if (entry.isDirectory()) {
94
+ changed += syncTree(sourcePath, targetPath, force);
95
+ continue;
96
+ }
97
+ if (!existsSync(targetPath)) {
98
+ mkdirSync(dirname(targetPath), { recursive: true });
99
+ writeFileSync(targetPath, readFileSync(sourcePath));
100
+ console.log(`installed ${targetPath}`);
101
+ changed++;
102
+ } else if (force || digest(sourcePath) === digest(targetPath)) {
103
+ writeFileSync(targetPath, readFileSync(sourcePath));
104
+ console.log(`updated ${targetPath}`);
105
+ changed++;
106
+ } else {
107
+ console.log(`preserved ${targetPath} (local changes; use --force to replace)`);
108
+ }
109
+ }
110
+ return changed;
111
+ }
112
+
80
113
  function install(args) {
81
114
  const root = projectRoot(args);
82
115
  const skills = selectedSkills(args);
@@ -103,6 +136,34 @@ function install(args) {
103
136
  console.log("Run `maggie doctor --project .` before using mutating workflows.");
104
137
  }
105
138
 
139
+ function update(args) {
140
+ const root = projectRoot(args);
141
+ const skills = selectedSkills(args);
142
+ const force = args.includes("--force");
143
+ const roots = agentRoots(args, root);
144
+ if (!existsSync(SKILLS_ROOT)) throw new Error("bundled skills are missing; run npm pack from the package source");
145
+ let updated = 0;
146
+ for (const agentRoot of roots) {
147
+ for (const skill of skills) {
148
+ const source = join(SKILLS_ROOT, skill);
149
+ const target = join(agentRoot, "skills", skill);
150
+ if (!existsSync(target)) {
151
+ console.log(`absent ${target} (run install to add it)`);
152
+ continue;
153
+ }
154
+ updated += syncTree(source, target, force);
155
+ }
156
+ if (existsSync(REFERENCES_ROOT) && existsSync(join(agentRoot, "references"))) updated += syncTree(REFERENCES_ROOT, join(agentRoot, "references"), force);
157
+ }
158
+ const tools = join(root, "tools");
159
+ 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);
160
+ const stateDir = join(root, STATE_DIR);
161
+ mkdirSync(stateDir, { recursive: true });
162
+ 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");
163
+ console.log(`Maggie update complete: ${updated} files changed`);
164
+ if (!force) console.log("Local files with changes were preserved. Review the output and rerun with --force only when replacement is intended.");
165
+ }
166
+
106
167
  function list() {
107
168
  for (const skill of SKILL_NAMES) console.log(skill);
108
169
  }
@@ -143,6 +204,7 @@ try {
143
204
  if (["help", "--help", "-h"].includes(command)) usage();
144
205
  else if (command === "list") list();
145
206
  else if (command === "init" || command === "install") install(args);
207
+ else if (command === "update") update(args);
146
208
  else if (command === "doctor") doctor(args);
147
209
  else if (command === "remove") remove(args);
148
210
  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,7 @@ 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` | Build authorized interior pages using the homepage header/footer as the shared shell | browser MCP, clone planner CLI, completed Maggie homepage |
10
11
  | `maggie-deployment` | Deploy and verify a dynamic Maggie blog, Cloudflare-first | Cloudflare Workers, D1, R2, KV, Wrangler |
11
12
  | `maggie-project-context` | Sync Project, voice, site and CTA context | project-context CLI/API |
12
13
  | `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,14 @@ 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:** enable the complete private Maggie Ops dashboard, including
165
+ content inventory/editor/preview, topics and FAQs, sitemap matching and
166
+ history, rewrite/API Pull controls, reports, settings/integrations,
167
+ migrations, calendar, media, redirects, tasks, agency scope, audit events,
168
+ authentication, role checks, and noindex/robots protection. This is enabled
169
+ by default and is required for a completed bootstrap unless the user
170
+ explicitly chooses a different operating model.
171
+
153
172
  Show the proposed choices, detected evidence, files likely to change, and
154
173
  unknowns for each gate. If the user confirms only part of a gate, implement
155
174
  only that part and leave the rest pending. Do not ask a single broad “is this
@@ -169,6 +188,11 @@ English only when existing project language is absent
169
188
  These are proposals, not automatic permission. SQLite, English, a new font,
170
189
  or a new design system must never silently replace detected project choices.
171
190
 
191
+ The default public blog index is 9 posts per page in a 3-column (3×3) grid.
192
+ Store `postsPerPage: 9` and `gridColumns: 3` in typed configuration so the
193
+ operator can change them later from site settings without changing the post
194
+ model or pagination logic.
195
+
172
196
  ### Phase 4: Contract
173
197
 
174
198
  Implement or map these post fields:
@@ -180,7 +204,8 @@ canonicalUrl, coverImage, author, tags, status
180
204
 
181
205
  The public contract must support:
182
206
 
183
- - `/posts` with stable pagination or a bounded list;
207
+ - `/posts` with stable pagination, defaulting to 9 posts per page and a 3-column
208
+ grid;
184
209
  - `/posts/:slug` with a real 404 for missing/unpublished posts;
185
210
  - canonical, Open Graph, Twitter, and Article metadata;
186
211
  - post links that are crawlable plain anchors;
@@ -199,6 +224,9 @@ Implement the host project's equivalent of:
199
224
  - JSON-LD `Article` or `BlogPosting` with valid dates and image URLs;
200
225
  - GA4 page view/event hooks that do nothing when analytics is disabled;
201
226
  - GSC verification via a public token or DNS instruction, never a private key.
227
+ - a complete private Ops dashboard following
228
+ [`ops-dashboard-contract.md`](../../references/ops-dashboard-contract.md),
229
+ backed by the same repository as public reads; do not ship a mock-only Ops UI.
202
230
 
203
231
  ### Phase 6: Optional AI CMO integration
204
232
 
@@ -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,12 @@ 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,
42
+ - The clone is an additive homepage operation. Do not overwrite an existing route,
40
43
  component namespace, asset namespace, or research artifact without explicit
41
44
  approval.
45
+ - The homepage header and footer become the shared shell source of truth. Record
46
+ their component paths, tokens, breakpoints, states, and asset dependencies so
47
+ later design pages never fork them.
42
48
  - Keep AI CMO writes, publishing, and external deployment separate from local
43
49
  page construction. A clone does not automatically create, publish, or queue
44
50
  content in Maggie.
@@ -61,7 +67,7 @@ editing and report the missing capability.
61
67
  ```bash
62
68
  python3 tools/clis/maggie.py status <project-root>
63
69
  python3 tools/clis/maggie.py doctor <project-root> --require-bootstrap --strict
64
- python3 tools/clis/maggie_clone.py plan <url1> [<url2> ...] --project <project-root>
70
+ python3 tools/clis/maggie_clone.py plan <homepage-url> --project <project-root>
65
71
  ```
66
72
 
67
73
  The clone planner emits collision-resistant site/page keys and destination
@@ -76,7 +82,7 @@ editing and report the missing capability.
76
82
  approval for a combined route-scoped app. Do not mix global fonts or CSS
77
83
  foundations silently.
78
84
 
79
- Write an output plan before implementation containing, for every URL:
85
+ Write an output plan before implementation containing:
80
86
 
81
87
  ```text
82
88
  source URL -> destination route
@@ -185,7 +191,7 @@ The cloned page remains a Maggie blog project page:
185
191
 
186
192
  ## Phase 4: Verification
187
193
 
188
- For every target:
194
+ For the homepage target:
189
195
 
190
196
  1. Run the host typecheck/lint/build commands and `doctor --strict`.
191
197
  2. Verify the exact destination URL, neighboring blog routes, 404 behavior,
@@ -207,7 +213,8 @@ differences and whether they are known, measured, or unverified.
207
213
 
208
214
  ## Completion report
209
215
 
210
- Report source-to-route mappings, preserved routes, sections/components/specs,
216
+ Report source-to-route mapping, the shared header/footer source of truth,
217
+ preserved routes, sections/components/specs,
211
218
  assets downloaded and failed, files changed, commands run, build/audit status,
212
219
  visual QA result, and known limitations. Deployment, API Pull, rewrite, and
213
220
  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,146 @@
1
+ ---
2
+ name: maggie-design
3
+ description: Design or recreate authorized interior pages inside a Maggie blog while reusing the cloned homepage header and footer exactly. 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.0.0
6
+ ---
7
+
8
+ # Maggie Design
9
+
10
+ Create authorized interior pages inside an existing Maggie project. This is
11
+ the page-design companion to `maggie-clone`: the homepage establishes the
12
+ global shell, and this skill designs only the content area between that shell.
13
+
14
+ Invoke it as:
15
+
16
+ ```text
17
+ /maggie-design <target-url1> [<target-url2> ...]
18
+ ```
19
+
20
+ ## Hard boundaries
21
+
22
+ - Require a completed `.maggie/bootstrap-state.json` and a completed homepage
23
+ foundation from `maggie-clone`. If either is missing, stop and request it.
24
+ - Reject the origin homepage as a target. Use `maggie-clone` for that job.
25
+ - Treat the homepage header and footer as immutable shared layout boundaries.
26
+ Reuse their component, tokens, navigation, logo, fonts, breakpoints,
27
+ responsive behavior, accessibility behavior, analytics hooks, and footer
28
+ links. Do not restyle, duplicate, or fork them for an interior page.
29
+ - Clone only the target's content region: hero, sections, cards, forms,
30
+ pricing/features, testimonials, FAQs, conversion blocks, and page-specific
31
+ interactions. Do not copy credentials, private data, tracking IDs,
32
+ authentication, checkout logic, or proprietary backend behavior.
33
+ - Keep each page in its own route and component namespace. Existing blog,
34
+ homepage, metadata, sitemap, robots, and Ops routes remain unchanged unless
35
+ the user explicitly approves a shared-shell change.
36
+
37
+ ## Phase 0: Preflight and decision loop
38
+
39
+ Follow the shared [Maggie decision loop](../../references/decision-loop.md).
40
+ Inspect before editing:
41
+
42
+ ```bash
43
+ python3 tools/clis/maggie.py status <project-root>
44
+ python3 tools/clis/maggie.py doctor <project-root> --require-bootstrap --strict
45
+ python3 tools/clis/maggie_clone.py plan <target-url1> [<target-url2> ...] --project <project-root>
46
+ ```
47
+
48
+ Verify the homepage foundation exists and locate its actual shared shell. The
49
+ plan must state, for every target:
50
+
51
+ ```text
52
+ source URL -> destination route
53
+ homepage shell component/layout -> reused unchanged
54
+ content component namespace -> new isolated namespace
55
+ research and screenshots -> page-scoped directories
56
+ assets -> page-scoped directory; shared assets remain shared
57
+ files preserved -> all existing public/blog/Ops routes
58
+ ```
59
+
60
+ Ask for confirmation of destination routes, page scope, asset reuse, and any
61
+ requested CTA/form behavior before mutation. Interior pages do not silently
62
+ become published posts or sitemap entries.
63
+
64
+ ## Phase 1: Content-area reconnaissance
65
+
66
+ Use the available browser/Chrome/Playwright capability. Capture desktop
67
+ 1440px, tablet 768px, and mobile 390px. Inspect the target with the global
68
+ header and footer excluded from the page scope after verifying their local
69
+ equivalent. Record:
70
+
71
+ - content topology and section order;
72
+ - exact visible copy, links, labels, images, forms, and public states;
73
+ - content-area typography, spacing, colors, borders, radii, shadows, and
74
+ responsive changes, comparing every value with the homepage tokens;
75
+ - hover, focus, open, loading, empty, error, scroll, tab, and reduced-motion
76
+ behavior;
77
+ - required assets and their reuse/licensing notes;
78
+ - page metadata and whether the page is a marketing route, conversion route,
79
+ or actual blog content.
80
+
81
+ Persist page-scoped evidence before building:
82
+
83
+ ```text
84
+ docs/research/<site-key>/<page-key>/
85
+ OUTPUT_PLAN.md
86
+ PAGE_TOPOLOGY.md
87
+ BEHAVIORS.md
88
+ DESIGN_TOKENS.md
89
+ COMPONENT_INVENTORY.md
90
+ ASSET_MANIFEST.md
91
+ docs/design-references/<site-key>/<page-key>/
92
+ desktop.png tablet.png mobile.png
93
+ ```
94
+
95
+ Every new component spec must explicitly say: `header: reuse homepage`,
96
+ `footer: reuse homepage`, and list the content-only target file and states.
97
+
98
+ ## Phase 2: Build the content area
99
+
100
+ Build the page using the host framework and the homepage's shared layout
101
+ component. Put page-specific output under an isolated namespace, for example:
102
+
103
+ ```text
104
+ src/components/sites/<site-key>/<page-key>/
105
+ src/pages/<approved-route>.*
106
+ public/sites/<site-key>/<page-key>/
107
+ ```
108
+
109
+ The page must:
110
+
111
+ - render inside the exact homepage shell and use its skip link, landmarks,
112
+ navigation, footer, fonts, and responsive breakpoints;
113
+ - use the existing metadata helper and canonical route;
114
+ - use normal anchors and real form validation;
115
+ - keep marketing pages out of the post sitemap and Article JSON-LD unless they
116
+ are explicitly mapped to the canonical post contract;
117
+ - preserve analytics consent and server-only API keys;
118
+ - implement observed interactions rather than guessed click-only substitutes.
119
+
120
+ If a page needs a new global navigation item, footer link, font, token, or
121
+ shared component change, stop, show the impact, and obtain explicit approval.
122
+
123
+ ## Phase 3: Verify
124
+
125
+ For every target:
126
+
127
+ 1. Run the host build, typecheck/lint, and `maggie doctor --strict` gates.
128
+ 2. Verify the exact destination route, homepage route, blog routes, 404,
129
+ robots, sitemap exclusion/inclusion, canonical metadata, and JSON-LD.
130
+ 3. Compare local and target screenshots at 1440px, 768px, and 390px. Compare
131
+ the header/footer separately to prove they are the same local components,
132
+ then compare only the content region for page fidelity.
133
+ 4. Sweep keyboard focus, links, forms, hover, tabs/dialogs, scroll behavior,
134
+ mobile menu, and reduced-motion behavior.
135
+ 5. Run `python3 tools/clis/site_audit.py <local-or-production-url> --json`.
136
+
137
+ Do not claim exact fidelity when a dynamic state, asset, or authenticated
138
+ target could not be inspected. Report measured differences and limitations.
139
+
140
+ ## Completion report
141
+
142
+ Report target-to-route mappings, the reused homepage shell files, new
143
+ content-only components, research/screenshots, assets, preserved routes,
144
+ commands and test results, visual QA status, and any requested global changes
145
+ that remain pending approval. Deployment, publishing, API Pull, and rewrite
146
+ operations are separate actions.
@@ -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 bootstrap implement the complete private Maggie Ops dashboard?", ["enabled", "disabled"]),
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 "enabled"
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=True)
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"])
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.1",
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.