@topy-ai/maggie 0.7.24 → 0.7.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -60,6 +60,11 @@ Maggie keeps the existing project foundation and asks for decisions before
60
60
  shared routes, analytics, or publishing boundaries change. The current
61
61
  package ships 19 installable skills and a local-first MaggieDash foundation.
62
62
 
63
+ The current release is `0.7.26`. It corrects the top-level feedback help and
64
+ includes the host-owned mobile app surface
65
+ contract (`maggie design app-init`/`app-validate`), direct Astro route
66
+ resolution, correlated feedback batches, and runtime package-version capture.
67
+
63
68
  For repeatable browser QA, create a project-owned scenario manifest and record
64
69
  the test, fix, and retest lifecycle:
65
70
 
@@ -213,7 +218,7 @@ maggie design ... # authorized interior-page design
213
218
  maggie clone-to-template ... # URL → validated marketplace template
214
219
  maggie marketplace ... # catalog and on-demand template workflow
215
220
  maggie memory ... # confirmed preferences and lessons
216
- maggie feedback ... # redact, preview, submit, list
221
+ maggie feedback ... # redact, preview, batch, submit, list
217
222
  maggie qa ... # scenario browser QA, fix/retest, release gate
218
223
  maggie localization ... # plan, validate, review, publish, stale
219
224
  maggie service ... # import, sync/report, catalogue, lifecycle, validate
@@ -393,8 +398,8 @@ artifact schemas.
393
398
  Recommended upgrade sequence for the current release:
394
399
 
395
400
  ```bash
396
- npx @topy-ai/maggie@0.7.24 update --project . --force
397
- npx @topy-ai/maggie@0.7.24 cleanup --project .
401
+ npx @topy-ai/maggie@0.7.26 update --project . --force
402
+ npx @topy-ai/maggie@0.7.26 cleanup --project .
398
403
  ```
399
404
 
400
405
  Maintainers should pass npm credentials through the repository helper, never
@@ -404,7 +409,10 @@ as a command-line argument:
404
409
  node scripts/publish-npm.mjs --maggie-env-file ../.env
405
410
  ```
406
411
 
407
- The 0.7.24 workflow adds browser-runtime `icon-rendered` assertions, sanitized
412
+ The 0.7.25 workflow adds host-owned mobile app surface plans with signed-in
413
+ camera-state QA, direct `.astro` route resolution, correlated feedback batch
414
+ drafts, and runtime package-version precedence. The 0.7.24 workflow adds
415
+ browser-runtime `icon-rendered` assertions, sanitized
408
416
  worker/browser parity preflight evidence, and the provider-neutral asynchronous
409
417
  service-onboarding contract. The 0.7.23 workflow adds the single-scroll-owner MaggieDash modal primitive,
410
418
  server-runtime-only auth/provider configuration guidance, best-effort optional
@@ -553,6 +561,15 @@ maggie feedback submit .maggie/feedback/<feedback-id>.json \
553
561
  --endpoint https://feedback.noblox.app/api/feedback --confirm
554
562
  ```
555
563
 
564
+ For related observations, create 1–50 local drafts with shared correlation
565
+ metadata and review them before submitting:
566
+
567
+ ```bash
568
+ maggie feedback batch --project . --batch-file ./feedback-batch.json
569
+ ```
570
+
571
+ The batch command never submits automatically or writes active memory.
572
+
556
573
  Submission is never implicit. Project paths, source files, logs, and secrets
557
574
  are excluded by default. The hosted endpoint stores the normalized feedback in
558
575
  NoBlox for maintainer review; it does not automatically create a GitHub Issue.
package/README.zh-TW.md CHANGED
@@ -8,7 +8,7 @@ Codex、Claude Code 與相容的 coding agents。
8
8
  ## 安裝
9
9
 
10
10
  ```bash
11
- npx @topy-ai/maggie@0.7.24 init --agent all
11
+ npx @topy-ai/maggie@0.7.26 init --agent all
12
12
  npx @topy-ai/maggie doctor --project .
13
13
  ```
14
14
 
@@ -25,6 +25,10 @@ maggie doctor --project . --require-bootstrap --strict
25
25
  deployment、memory、feedback 和 MaggieDash。內容先 draft/review,外部寫入、
26
26
  publish 與 production deployment 需要明確確認。
27
27
 
28
+ 目前 release 是 `0.7.26`,修正 top-level feedback help 並包含 host-owned mobile app surface contract、signed-in
29
+ camera-state QA、直接 Astro route resolution、correlated feedback batch,以及
30
+ runtime package version capture。
31
+
28
32
  0.7.24 加入 `icon-rendered` browser QA assertion、worker/browser deployment parity
29
33
  preflight,以及 import/search/research 的非同步 service onboarding contract。
30
34
  0.7.23 加入 dashboard dialog 單一 scroll owner、server runtime-only auth/provider
package/bin/maggie.js CHANGED
@@ -88,6 +88,8 @@ Usage:
88
88
  maggie design reference-ui --project PATH --reference PATH --surface blog,service --confirm
89
89
  maggie design init --project PATH --surface blog|service --reference-run PATH --confirm
90
90
  maggie design validate-ui --project PATH --plan PATH --rendered-dir PATH --confirm
91
+ maggie design app-init --project PATH --route /app/valuation --auth-mode email-password --confirm
92
+ maggie design app-validate --project PATH --plan PATH --runtime-evidence FILE --rendered-dir PATH --confirm
91
93
  maggie design icon-inventory --project PATH --source-dir src --runtime assets/icons.css --output docs/icon-inventory.json
92
94
  maggie design author --project PATH --route /about --purpose TEXT --audience TEXT --confirm
93
95
  maggie design in-place --project PATH --route /pricing [--content-source PATH]
@@ -124,7 +126,7 @@ Usage:
124
126
  maggie memory <init|list|search|context|add|record-error|transition|export> --project PATH
125
127
  maggie localization <extract|plan|generate|preview|validate|review|publish|stale|glossary> [options]
126
128
  maggie seo performance|images|sitemap|indexnow|social-cards|head-tags [options] (sitemap supports strict validate and agent-files)
127
- maggie feedback <collect|preview|submit|list> [options]
129
+ maggie feedback <collect|preview|batch|submit|list> [options]
128
130
  maggie qa <start|record|summary|export|assertion-audit> [options]
129
131
  maggie site-audit URL [--crawl] [--access-log FILE] [--require-sitemap-request] [--languages en-GB,es-MX,ja-JP] [--check-hreflang]
130
132
  maggie site-audit URL --crawl --save-baseline FILE --reviewer NAME
@@ -327,7 +329,8 @@ function workflowCli(name, args) {
327
329
  const root = projectRoot(args);
328
330
  const script = join(root, "tools", "clis", name);
329
331
  if (!existsSync(script)) throw new Error(`${name} is missing: ${script}`);
330
- const result = spawnSync("python3", [script, ...args], { stdio: "inherit", cwd: root });
332
+ const env = name === "maggie_feedback.py" ? { ...process.env, MAGGIE_VERSION: PACKAGE_VERSION } : process.env;
333
+ const result = spawnSync("python3", [script, ...args], { stdio: "inherit", cwd: root, env });
331
334
  if (result.error) throw result.error;
332
335
  process.exitCode = result.status ?? 1;
333
336
  }
@@ -0,0 +1,99 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/TOPY-AI-LTD/ai-cmo-skills/blob/main/contracts/maggie-design/mobile-app-surface-v1.schema.json",
4
+ "title": "Maggie mobile app surface contract",
5
+ "type": "object",
6
+ "required": [
7
+ "schemaVersion",
8
+ "workflow",
9
+ "mode",
10
+ "route",
11
+ "auth",
12
+ "shell",
13
+ "camera",
14
+ "viewports",
15
+ "requiredSteps",
16
+ "publicWrite"
17
+ ],
18
+ "properties": {
19
+ "schemaVersion": {
20
+ "const": "maggie-mobile-app-surface.v1"
21
+ },
22
+ "workflow": {
23
+ "const": "maggie-design"
24
+ },
25
+ "mode": {
26
+ "const": "app-view-init"
27
+ },
28
+ "route": {
29
+ "type": "string",
30
+ "pattern": "^/"
31
+ },
32
+ "auth": {
33
+ "type": "object",
34
+ "required": ["mode", "required", "credentialPolicy"],
35
+ "properties": {
36
+ "mode": {
37
+ "enum": ["email-password", "existing-session", "none"]
38
+ },
39
+ "required": {
40
+ "type": "boolean"
41
+ },
42
+ "credentialPolicy": {
43
+ "const": "use-host-auth-without-recording-credentials"
44
+ }
45
+ },
46
+ "additionalProperties": true
47
+ },
48
+ "shell": {
49
+ "type": "object",
50
+ "required": ["fixedHeader", "fixedFooter", "oneScrollOwner"],
51
+ "properties": {
52
+ "fixedHeader": {"const": true},
53
+ "fixedFooter": {"const": true},
54
+ "oneScrollOwner": {"const": true}
55
+ },
56
+ "additionalProperties": true
57
+ },
58
+ "camera": {
59
+ "type": "object",
60
+ "required": ["requiredStates", "controls", "fallback"],
61
+ "properties": {
62
+ "requiredStates": {
63
+ "type": "array",
64
+ "contains": {"const": "permission"}
65
+ },
66
+ "controls": {
67
+ "type": "array",
68
+ "minItems": 4
69
+ },
70
+ "fallback": {
71
+ "type": "string",
72
+ "minLength": 1
73
+ }
74
+ },
75
+ "additionalProperties": true
76
+ },
77
+ "viewports": {
78
+ "type": "object",
79
+ "required": ["desktop", "tablet", "mobile"],
80
+ "additionalProperties": {
81
+ "type": "array",
82
+ "prefixItems": [
83
+ {"type": "integer", "minimum": 1},
84
+ {"type": "integer", "minimum": 1}
85
+ ],
86
+ "minItems": 2,
87
+ "maxItems": 2
88
+ }
89
+ },
90
+ "requiredSteps": {
91
+ "type": "array",
92
+ "contains": {"const": "signed-in-browser-qa"}
93
+ },
94
+ "publicWrite": {
95
+ "const": false
96
+ }
97
+ },
98
+ "additionalProperties": true
99
+ }
@@ -2,7 +2,7 @@
2
2
  name: maggie-design
3
3
  description: Design authorized interior pages, review rendered responsive layouts, or explicitly rebrand a packaged homepage/template. Use rebrand only with a named source brand and target brand.
4
4
  metadata:
5
- version: 1.5.0
5
+ version: 1.6.0
6
6
  ---
7
7
 
8
8
  # Maggie Design
@@ -10,7 +10,7 @@ metadata:
10
10
  ## In-place route evidence
11
11
 
12
12
  `maggie design in-place --project . --route /example` resolves local source
13
- files and falls back to a discovered `[...slug]`/`[[...slug]]` file. A catch-all
13
+ files, including direct `src/pages/<route>.astro` files, and falls back to a discovered `[...slug]`/`[[...slug]]` file. A catch-all
14
14
  is recorded as `resolution: unproven` unless `--content-source` names the
15
15
  actual DB/content resolver. A source candidate is not proof that the concrete
16
16
  URL is served: verify the HTTP response, content identity, and framework route
@@ -202,6 +202,33 @@ semantics from `maggie-blog`. For service UI, preserve provider identity,
202
202
  variants, booking URLs, duplicate canonical/noindex decisions, and service
203
203
  sitemap semantics from `maggie-service-booking`.
204
204
 
205
+ ## Mobile app surfaces
206
+
207
+ Use the app surface contract for camera, scan, inventory, valuation, or other
208
+ authenticated app views. It is a general host contract, not a replacement for
209
+ the project's auth or camera implementation:
210
+
211
+ maggie design app-init --project . --route /app/valuation \
212
+ --auth-mode email-password --confirm
213
+
214
+ The initializer records the fixed header/footer, one-scroll-owner rule,
215
+ camera permission/ready/gallery/scan/unavailable states, decision-critical UI,
216
+ and desktop/tablet/mobile evidence requirements. It never collects credentials
217
+ or copies private project data. Use `existing-session` only when the host
218
+ already supplies a safe signed-in session; use `none` for public app surfaces.
219
+
220
+ After implementation, validate sanitized runtime evidence and screenshots:
221
+
222
+ maggie design app-validate --project . \
223
+ --plan .maggie/design/app-view/app-<id>/plan.json \
224
+ --runtime-evidence ./qa/mobile-app-evidence.json \
225
+ --rendered-dir ./qa/screenshots --confirm
226
+
227
+ The evidence must prove the auth state, fixed shell, exactly one scroll owner,
228
+ all required camera states, and valid PNGs at 390px, 768px, and desktop
229
+ widths. The machine-readable contract is
230
+ [mobile-app-surface-v1.schema.json](../../bundled-contracts/maggie-design/mobile-app-surface-v1.schema.json).
231
+
205
232
  ## Explicit homepage rebrand mode
206
233
 
207
234
  The homepage `review` mode only compares screenshots. It does not rebrand a
@@ -2,7 +2,7 @@
2
2
  name: maggie-feedback
3
3
  description: Collect, review, and explicitly submit privacy-safe feedback about a Maggie skill run, workflow result, bug, or feature request.
4
4
  metadata:
5
- version: 1.0.0
5
+ version: 1.1.0
6
6
  ---
7
7
 
8
8
  # Maggie Feedback
@@ -39,6 +39,41 @@ If a draft is marked fixed with `--fixed` (or the supplied run report says
39
39
  also scrubs common POSIX/home/temp and Windows absolute paths by default while
40
40
  preserving public URLs and route paths.
41
41
 
42
+ The CLI records the executing Maggie package version when it is available from
43
+ the runtime environment or `node_modules/@topy-ai/maggie/package.json`. A
44
+ stale `.maggie/install.json` is only a fallback; conflicting runtime package
45
+ versions fail closed as `unknown`.
46
+
47
+ ## Correlated feedback batches
48
+
49
+ When several observations belong to one workflow run, create bounded drafts
50
+ with one shared correlation ID instead of losing their relationship:
51
+
52
+ maggie feedback batch --project . --batch-file ./feedback-batch.json
53
+
54
+ The batch file contains 1–50 items and may provide shared `skill`, `runId`,
55
+ `phase`, `priority`, and `affectedCli` values. An item can override those
56
+ fields and supplies the normal `type`, `summary`, `expected`, `actual`,
57
+ `errorFingerprint`, reproduction, resolution, validation, and screenshot
58
+ metadata fields. Each generated draft stores only the safe batch ID, zero-based
59
+ index, and bounded size. The batch command still creates local drafts only;
60
+ review each draft and submit explicitly.
61
+
62
+ Example:
63
+
64
+ {
65
+ "batchId": "run-review-001",
66
+ "skill": "maggie-design",
67
+ "runId": "design-001",
68
+ "phase": "post-release-review",
69
+ "priority": "normal",
70
+ "affectedCli": "design",
71
+ "items": [
72
+ {"type": "bug", "summary": "Route evidence is incomplete", "expected": "Direct route resolves", "actual": "Fallback was used"},
73
+ {"type": "feature", "summary": "Need an app-surface QA contract", "expected": "Reusable evidence plan", "actual": "Only public-page rules exist"}
74
+ ]
75
+ }
76
+
42
77
  ## Submit explicitly
43
78
 
44
79
  The CLI never submits automatically. After reviewing the draft:
@@ -157,6 +157,114 @@ def validate_content_ui(project: Path, plan_path: Path, rendered_dir: Path | Non
157
157
  output = plan_path.parent / "validation.json"; output.write_text(json.dumps(result, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"); print(json.dumps(result, indent=2, ensure_ascii=False)); return 0 if not errors else 1
158
158
 
159
159
 
160
+ def app_surface_init(project: Path, route: str, auth_mode: str, force: bool, confirm: bool) -> int:
161
+ """Create a host-owned mobile app surface contract and QA manifest."""
162
+ project = project.resolve()
163
+ if not route.startswith("/") or "?" in route or "#" in route:
164
+ raise ValueError("route must be a path beginning with / and must not contain a query or hash")
165
+ if auth_mode not in {"email-password", "existing-session", "none"}:
166
+ raise ValueError("auth mode must be email-password, existing-session, or none")
167
+ contract = require_design_contract(project)
168
+ relative = route.strip("/")
169
+ candidates = [
170
+ project / relative,
171
+ project / "src" / "pages" / relative,
172
+ project / "src" / "pages" / f"{relative}.astro",
173
+ project / "src" / "pages" / f"{relative}.html",
174
+ project / "src" / "app" / relative,
175
+ project / "src" / "app" / relative / "page.tsx",
176
+ ]
177
+ source = next((path for path in candidates if path.is_file()), None)
178
+ digest = hashlib.sha256((str(project) + "\n" + route + "\n" + auth_mode).encode()).hexdigest()[:12]
179
+ output_dir = project / ".maggie" / "design" / "app-view" / f"app-{digest}"
180
+ if output_dir.exists() and not force:
181
+ print((output_dir / "plan.json").read_text(encoding="utf-8"), end="")
182
+ return 0
183
+ output_dir.mkdir(parents=True, exist_ok=True)
184
+ viewports = {"desktop": REVIEW_VIEWPORTS["desktop"], "tablet": REVIEW_VIEWPORTS["tablet"], "mobile": REVIEW_VIEWPORTS["mobile"]}
185
+ plan = {
186
+ "schemaVersion": "maggie-mobile-app-surface.v1",
187
+ "id": f"app-{digest}",
188
+ "workflow": "maggie-design",
189
+ "mode": "app-view-init",
190
+ "phase": "ready",
191
+ "route": route,
192
+ "routeSource": str(source) if source else None,
193
+ "designContract": contract,
194
+ "auth": {"mode": auth_mode, "required": auth_mode != "none", "credentialPolicy": "use-host-auth-without-recording-credentials"},
195
+ "shell": {"fixedHeader": True, "fixedFooter": True, "oneScrollOwner": True, "cameraMayUseFullScreenViewport": True, "hostShellSourceOfTruth": "existing homepage shell"},
196
+ "camera": {"requiredStates": ["permission", "ready", "gallery", "scan", "unavailable"], "controls": ["close", "gallery", "shutter", "mode"], "fallback": "show an actionable unavailable state; never leave a black screen"},
197
+ "decisionCriticalUi": ["card normalization", "graded-price fallback", "grade-9-to-10 economics", "scan-to-inventory or valuation transition"],
198
+ "viewports": viewports,
199
+ "requiredSteps": ["inspect-host-shell", "define-auth-gate", "define-camera-states", "capture-desktop-tablet-mobile", "signed-in-browser-qa", "accessibility-check", "route-validation", "explicit-approval"],
200
+ "provenance": {"hostOwnsAuthAndCamera": True, "designContractReused": True, "privateDataCopied": False},
201
+ "publicWrite": False,
202
+ "createdAt": datetime.now(timezone.utc).isoformat(),
203
+ }
204
+ evidence = {
205
+ "schemaVersion": "maggie-mobile-app-evidence.v1",
206
+ "route": route,
207
+ "auth": plan["auth"],
208
+ "shell": plan["shell"],
209
+ "camera": plan["camera"],
210
+ "viewports": {name: {"screenshot": f"{name}.png", "captured": False} for name in viewports},
211
+ "status": "pending",
212
+ }
213
+ qa = {"schemaVersion": "maggie-mobile-app-qa.v1", "route": route, "auth": plan["auth"], "scenarios": [
214
+ {"id": "shell-scroll", "viewport": "mobile", "assertions": ["fixed header and footer remain usable", "exactly one scroll owner"]},
215
+ {"id": "camera-permission", "viewport": "mobile", "assertions": ["permission state is explained", "denied permission shows fallback"]},
216
+ {"id": "camera-scan", "viewport": "mobile", "assertions": ["gallery/shutter/mode/close controls are reachable", "scan result can enter inventory or valuation"]},
217
+ {"id": "valuation-auth", "viewport": "desktop", "assertions": ["signed-in route does not redirect unexpectedly", "grade economics and fallback price remain legible"]},
218
+ ]}
219
+ (output_dir / "plan.json").write_text(json.dumps(plan, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
220
+ (output_dir / "qa-scenarios.json").write_text(json.dumps(qa, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
221
+ (output_dir / "validation.json").write_text(json.dumps(evidence, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
222
+ (output_dir / "screenshots").mkdir(exist_ok=True)
223
+ print(json.dumps({"plan": str(output_dir / "plan.json"), "qaScenarios": str(output_dir / "qa-scenarios.json"), "surface": "app", "phase": "ready", "publicWrite": False}, indent=2, ensure_ascii=False))
224
+ return 0
225
+
226
+
227
+ def validate_app_surface(project: Path, plan_path: Path, rendered_dir: Path | None, runtime_evidence: Path | None, confirm: bool) -> int:
228
+ """Validate signed-in app evidence and responsive screenshots."""
229
+ project = project.resolve(); plan_path = plan_path.resolve()
230
+ if not plan_path.exists(): raise ValueError(f"app surface plan not found: {plan_path}")
231
+ plan = json.loads(plan_path.read_text(encoding="utf-8")); errors = []
232
+ if plan.get("workflow") != "maggie-design" or plan.get("mode") != "app-view-init": errors.append("invalid mobile app surface plan")
233
+ if plan.get("publicWrite") is not False: errors.append("publicWrite must remain false before approval")
234
+ if not plan.get("shell", {}).get("oneScrollOwner"): errors.append("app surface must define one scroll owner")
235
+ if not plan.get("auth", {}).get("credentialPolicy"): errors.append("app surface must define a credential policy")
236
+ try: require_design_contract(project)
237
+ except ValueError as error: errors.append(str(error))
238
+ evidence = {}
239
+ evidence_path = runtime_evidence.resolve() if runtime_evidence else plan_path.parent / "runtime-evidence.json"
240
+ if evidence_path.is_file():
241
+ try: evidence = json.loads(evidence_path.read_text(encoding="utf-8"))
242
+ except json.JSONDecodeError: errors.append("runtime evidence is not valid JSON")
243
+ else:
244
+ errors.append("runtime evidence file is required")
245
+ if evidence:
246
+ if evidence.get("schemaVersion") != "maggie-mobile-app-evidence.v1": errors.append("runtime evidence schemaVersion is invalid")
247
+ if evidence.get("route") != plan.get("route"): errors.append("runtime evidence route does not match plan")
248
+ auth = evidence.get("auth", {})
249
+ if plan.get("auth", {}).get("required") and auth.get("signedIn") is not True: errors.append("signed-in evidence is required for this app route")
250
+ shell = evidence.get("shell", {})
251
+ if shell.get("fixedHeader") is not True or shell.get("fixedFooter") is not True or shell.get("scrollOwnerCount") != 1: errors.append("runtime evidence must prove fixed shell and one scroll owner")
252
+ camera = evidence.get("camera", {})
253
+ camera_states = set(camera.get("states", [])) if isinstance(camera, dict) else set()
254
+ if not set(plan.get("camera", {}).get("requiredStates", [])).issubset(camera_states): errors.append("runtime evidence is missing a required camera state")
255
+ screenshot_status = {}
256
+ for name in ("desktop", "tablet", "mobile"):
257
+ path = (rendered_dir / f"{name}.png") if rendered_dir else plan_path.parent / "screenshots" / f"{name}.png"
258
+ valid = path.is_file() and path.read_bytes()[:8] == b"\x89PNG\r\n\x1a\n"
259
+ screenshot_status[name] = {"path": str(path), "validPng": valid}
260
+ if not valid: errors.append(f"missing valid {name} screenshot")
261
+ result = {"schemaVersion": "maggie-mobile-app-validation.v1", "plan": str(plan_path), "route": plan.get("route"), "screenshots": screenshot_status, "runtimeEvidence": str(evidence_path), "checks": {"fixedShell": not any("fixed shell" in error for error in errors), "signedInRoute": not any("signed-in" in error for error in errors), "cameraStates": not any("camera state" in error for error in errors), "publicWriteBlocked": plan.get("publicWrite") is False}, "status": "passed" if not errors else "failed", "errors": errors, "validatedAt": datetime.now(timezone.utc).isoformat()}
262
+ if confirm:
263
+ plan_path.parent.joinpath("validation.json").write_text(json.dumps(result, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
264
+ print(json.dumps(result, indent=2, ensure_ascii=False))
265
+ return 0 if not errors else 1
266
+
267
+
160
268
  def rebrand_template(args: argparse.Namespace) -> int:
161
269
  """Apply an explicit brand identity to a packaged homepage template."""
162
270
  template = args.template.resolve()
@@ -547,6 +655,7 @@ def in_place_job(project: Path, routes: list[str], force: bool = False, content_
547
655
  project / relative,
548
656
  project / "src" / "pages" / relative,
549
657
  project / "src" / "app" / relative,
658
+ project / "src" / "pages" / f"{relative}.astro",
550
659
  project / "src" / "pages" / f"{relative}.html",
551
660
  project / "src" / "app" / relative / "page.tsx",
552
661
  ]
@@ -680,7 +789,7 @@ commands:
680
789
  status show a design job and its per-step progress
681
790
  resume restart a failed design workflow
682
791
  author plan an original first-party page
683
- reference-ui, init, validate-ui, rebrand, review
792
+ reference-ui, init, validate-ui, app-init, app-validate, rebrand, review
684
793
  """)
685
794
  return 0
686
795
  if len(sys.argv) > 1 and sys.argv[1] == "reference-ui":
@@ -719,6 +828,30 @@ commands:
719
828
  return validate_content_ui(args.project, args.plan, args.rendered_dir, args.confirm)
720
829
  except (OSError, ValueError, json.JSONDecodeError) as error:
721
830
  print(f"BLOCKED: maggie-design validate-ui: {error}", file=sys.stderr); return 1
831
+ if len(sys.argv) > 1 and sys.argv[1] == "app-init":
832
+ command = argparse.ArgumentParser(description="Create a mobile app surface contract with signed-in QA scenarios.")
833
+ command.add_argument("--project", type=Path, default=Path.cwd())
834
+ command.add_argument("--route", required=True)
835
+ command.add_argument("--auth-mode", choices=["email-password", "existing-session", "none"], default="email-password")
836
+ command.add_argument("--force", action="store_true")
837
+ command.add_argument("--confirm", action="store_true")
838
+ args = command.parse_args(sys.argv[2:])
839
+ try:
840
+ return app_surface_init(args.project, args.route, args.auth_mode, args.force, args.confirm)
841
+ except (OSError, ValueError, json.JSONDecodeError) as error:
842
+ print(f"BLOCKED: maggie-design app-init: {error}", file=sys.stderr); return 1
843
+ if len(sys.argv) > 1 and sys.argv[1] == "app-validate":
844
+ command = argparse.ArgumentParser(description="Validate mobile app runtime evidence and responsive screenshots.")
845
+ command.add_argument("--project", type=Path, default=Path.cwd())
846
+ command.add_argument("--plan", type=Path, required=True)
847
+ command.add_argument("--rendered-dir", type=Path)
848
+ command.add_argument("--runtime-evidence", type=Path)
849
+ command.add_argument("--confirm", action="store_true")
850
+ args = command.parse_args(sys.argv[2:])
851
+ try:
852
+ return validate_app_surface(args.project, args.plan, args.rendered_dir, args.runtime_evidence, args.confirm)
853
+ except (OSError, ValueError, json.JSONDecodeError) as error:
854
+ print(f"BLOCKED: maggie-design app-validate: {error}", file=sys.stderr); return 1
722
855
  if len(sys.argv) > 1 and sys.argv[1] == "rebrand":
723
856
  rebrand = argparse.ArgumentParser(description="Apply an explicit brand identity to a packaged marketplace template.")
724
857
  rebrand.add_argument("--template", type=Path, required=True)
@@ -27,6 +27,7 @@ LOCAL_PATH_RE = re.compile(
27
27
  r"\\\\[^\\/\s,;:)\]}]+(?:[\\/]+[^\\/\s,;:)\]}]+)+)"
28
28
  )
29
29
  VERSION_RE = re.compile(r"\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?")
30
+ SAFE_BATCH_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$")
30
31
  ACKNOWLEDGEMENT_KEYS = ("status", "feedbackId", "requestId")
31
32
  REPO_ROOT = Path(__file__).resolve().parents[2]
32
33
 
@@ -45,10 +46,25 @@ def project_fingerprint(project: Path) -> str:
45
46
 
46
47
 
47
48
  def installed_version(project: Path) -> str:
48
- """Resolve the exact runtime version and fail closed on source drift."""
49
+ """Resolve the exact runtime version, preferring the running package."""
49
50
  if os.environ.get("MAGGIE_VERSION"):
50
51
  value = safe_text(os.environ["MAGGIE_VERSION"])
51
52
  return value if VERSION_RE.fullmatch(value) else "unknown"
53
+
54
+ # A consumer project can retain an old .maggie/install.json after npm
55
+ # upgrade. The package actually being executed is stronger evidence.
56
+ package_versions = []
57
+ for parent in (project.resolve(), *project.resolve().parents):
58
+ package_path = parent / "node_modules" / "@topy-ai" / "maggie" / "package.json"
59
+ try:
60
+ value = json.loads(package_path.read_text(encoding="utf-8")).get("version", "")
61
+ if VERSION_RE.fullmatch(str(value)):
62
+ package_versions.append(str(value))
63
+ except (OSError, ValueError, json.JSONDecodeError, AttributeError):
64
+ continue
65
+ if package_versions:
66
+ return package_versions[0] if len(set(package_versions)) == 1 else "unknown"
67
+
52
68
  install_path = project / ".maggie" / "install.json"
53
69
  try:
54
70
  value = json.loads(install_path.read_text(encoding="utf-8")).get("version", "")
@@ -102,6 +118,20 @@ def collect(args: argparse.Namespace) -> int:
102
118
  validation = safe_text(args.validation or report.get("validation"))
103
119
  if fixed and (not resolution or not validation):
104
120
  raise ValueError("fixed feedback requires non-empty --resolution and --validation")
121
+ batch_id = safe_text(getattr(args, "batch_id", ""))
122
+ batch_index = getattr(args, "batch_index", None)
123
+ batch_size = getattr(args, "batch_size", None)
124
+ if batch_id:
125
+ if not SAFE_BATCH_ID_RE.fullmatch(batch_id):
126
+ raise ValueError("batch ID is invalid")
127
+ if not isinstance(batch_index, int) or isinstance(batch_index, bool) or batch_index < 0:
128
+ raise ValueError("batch index is required when batch ID is provided")
129
+ if not isinstance(batch_size, int) or isinstance(batch_size, bool) or batch_size < 1 or batch_index >= batch_size:
130
+ raise ValueError("batch size must be positive and greater than batch index")
131
+ priority = safe_text(getattr(args, "priority", "") or "normal").lower()
132
+ if priority not in {"low", "normal", "high", "critical"}:
133
+ raise ValueError("priority must be low, normal, high, or critical")
134
+ affected_cli = safe_text(getattr(args, "affected_cli", ""))
105
135
  context = {"projectFingerprint": project_fingerprint(project)}
106
136
  if args.allow_project_context and args.context_note:
107
137
  context["note"] = safe_text(args.context_note)
@@ -124,12 +154,16 @@ def collect(args: argparse.Namespace) -> int:
124
154
  "fixed": fixed,
125
155
  "resolution": resolution,
126
156
  "validation": validation,
157
+ "priority": priority,
158
+ "affectedCli": affected_cli,
127
159
  "attachments": [attachment(value) for value in args.screenshot],
128
160
  "environment": {"os": platform.system().lower(), "python": platform.python_version()},
129
161
  "privacy": {"secretsRedacted": True, "projectContextAllowed": bool(args.allow_project_context)},
130
162
  "projectContext": context if args.allow_project_context else None,
131
163
  "submission": {"status": "draft"},
132
164
  }
165
+ if batch_id:
166
+ data["batch"] = {"batchId": batch_id, "index": batch_index, "size": batch_size}
133
167
  directory = project / ".maggie" / "feedback"
134
168
  directory.mkdir(parents=True, exist_ok=True)
135
169
  path = directory / f"{data['feedbackId']}.json"
@@ -144,7 +178,11 @@ def collect(args: argparse.Namespace) -> int:
144
178
 
145
179
 
146
180
  def to_markdown(data: dict) -> str:
147
- lines = [f"# Maggie feedback: {data.get('summary') or data.get('type')}", "", "<!-- Generated by maggie feedback. Secrets and local paths are omitted. -->", "", "## Run", "", f"- Skill: `{data.get('skill')}`", f"- Maggie version: `{data.get('maggieVersion')}`", f"- Run ID: `{data.get('runId') or 'not provided'}`", f"- Phase: `{data.get('phase') or 'not provided'}`", f"- Status: `{data.get('status')}`", f"- Error fingerprint: `{data.get('errorFingerprint') or 'not provided'}`", "", "## Report", "", f"**Summary:** {data.get('summary') or 'Not provided'}", "", f"**Expected:** {data.get('expected') or 'Not provided'}", "", f"**Actual:** {data.get('actual') or 'Not provided'}", ""]
181
+ batch = data.get("batch") or {}
182
+ lines = [f"# Maggie feedback: {data.get('summary') or data.get('type')}", "", "<!-- Generated by maggie feedback. Secrets and local paths are omitted. -->", "", "## Run", "", f"- Skill: `{data.get('skill')}`", f"- Maggie version: `{data.get('maggieVersion')}`", f"- Run ID: `{data.get('runId') or 'not provided'}`", f"- Phase: `{data.get('phase') or 'not provided'}`", f"- Status: `{data.get('status')}`", f"- Priority: `{data.get('priority') or 'normal'}`", f"- Affected CLI: `{data.get('affectedCli') or 'not provided'}`", f"- Error fingerprint: `{data.get('errorFingerprint') or 'not provided'}`"]
183
+ if batch:
184
+ lines.append(f"- Batch: `{batch.get('batchId')}` ({batch.get('index', 0) + 1}/{batch.get('size')})")
185
+ lines += ["", "## Report", "", f"**Summary:** {data.get('summary') or 'Not provided'}", "", f"**Expected:** {data.get('expected') or 'Not provided'}", "", f"**Actual:** {data.get('actual') or 'Not provided'}", ""]
148
186
  if data.get("stepsToReproduce"):
149
187
  lines += ["**Steps to reproduce:**", ""] + [f"{index}. {step}" for index, step in enumerate(data["stepsToReproduce"], 1)] + [""]
150
188
  lines += [f"**Fixed:** {'yes' if data.get('fixed') else 'no'}", f"**Resolution:** {data.get('resolution') or 'Not provided'}", f"**Validation:** {data.get('validation') or 'Not provided'}", "", "## Privacy", "", f"- Project context allowed: `{bool(data.get('privacy', {}).get('projectContextAllowed'))}`", f"- Attachments: `{len(data.get('attachments', []))}` metadata-only reference(s)", ""]
@@ -211,6 +249,57 @@ def list_feedback(args: argparse.Namespace) -> int:
211
249
  return 0
212
250
 
213
251
 
252
+ def collect_batch(args: argparse.Namespace) -> int:
253
+ """Create one privacy-safe draft per item while retaining shared batch context."""
254
+ manifest = load_json(Path(args.batch_file).expanduser().resolve())
255
+ batch_id = safe_text(manifest.get("batchId"))
256
+ if not SAFE_BATCH_ID_RE.fullmatch(batch_id):
257
+ raise ValueError("batch file requires a safe batchId")
258
+ items = manifest.get("items")
259
+ if not isinstance(items, list) or not items or len(items) > 50:
260
+ raise ValueError("batch file requires between 1 and 50 items")
261
+ shared = {
262
+ "skill": safe_text(manifest.get("skill")),
263
+ "run_id": safe_text(manifest.get("runId")),
264
+ "phase": safe_text(manifest.get("phase")),
265
+ "priority": safe_text(manifest.get("priority") or "normal"),
266
+ "affected_cli": safe_text(manifest.get("affectedCli")),
267
+ }
268
+ paths = []
269
+ for index, item in enumerate(items):
270
+ if not isinstance(item, dict):
271
+ raise ValueError(f"items[{index}] must be an object")
272
+ values = {
273
+ "project": args.project,
274
+ "run_report": None,
275
+ "skill": safe_text(item.get("skill") or shared["skill"]),
276
+ "run_id": safe_text(item.get("runId") or shared["run_id"]),
277
+ "type": item.get("type", "bug"),
278
+ "phase": safe_text(item.get("phase") or shared["phase"]),
279
+ "summary": item.get("summary", ""),
280
+ "expected": item.get("expected", ""),
281
+ "actual": item.get("actual", ""),
282
+ "error_fingerprint": item.get("errorFingerprint", ""),
283
+ "reproduce": item.get("stepsToReproduce", []),
284
+ "fixed": item.get("fixed") is True,
285
+ "resolution": item.get("resolution", ""),
286
+ "validation": item.get("validation", ""),
287
+ "screenshot": item.get("screenshots", []),
288
+ "allow_project_context": False,
289
+ "context_note": "",
290
+ "batch_id": batch_id,
291
+ "batch_index": index,
292
+ "batch_size": len(items),
293
+ "priority": item.get("priority") or shared["priority"],
294
+ "affected_cli": item.get("affectedCli") or shared["affected_cli"],
295
+ }
296
+ if not isinstance(values["reproduce"], list): values["reproduce"] = []
297
+ if not isinstance(values["screenshot"], list): values["screenshot"] = []
298
+ paths.append(collect(argparse.Namespace(**values)))
299
+ print(json.dumps({"batchId": batch_id, "count": len(paths), "status": "drafts-created"}, ensure_ascii=False))
300
+ return 0
301
+
302
+
214
303
  def main() -> int:
215
304
  parser = argparse.ArgumentParser(description=__doc__)
216
305
  parser.add_argument("--project", default=".")
@@ -235,6 +324,14 @@ def main() -> int:
235
324
  collect_parser.add_argument("--screenshot", action="append", default=[])
236
325
  collect_parser.add_argument("--allow-project-context", action="store_true")
237
326
  collect_parser.add_argument("--context-note", default="")
327
+ collect_parser.add_argument("--batch-id", default="")
328
+ collect_parser.add_argument("--batch-index", type=int)
329
+ collect_parser.add_argument("--batch-size", type=int)
330
+ collect_parser.add_argument("--priority", choices=("low", "normal", "high", "critical"), default="normal")
331
+ collect_parser.add_argument("--affected-cli", default="")
332
+ batch_parser = sub.add_parser("batch")
333
+ batch_parser.add_argument("--project", default=argparse.SUPPRESS)
334
+ batch_parser.add_argument("--batch-file", required=True)
238
335
  preview_parser = sub.add_parser("preview")
239
336
  preview_parser.add_argument("feedback")
240
337
  preview_parser.add_argument("--format", choices=("json", "markdown"), default="json")
@@ -246,6 +343,7 @@ def main() -> int:
246
343
  args = parser.parse_args()
247
344
  try:
248
345
  if args.command == "collect": return collect(args)
346
+ if args.command == "batch": return collect_batch(args)
249
347
  if args.command == "preview": return preview(args)
250
348
  if args.command == "submit": return submit(args)
251
349
  return list_feedback(args)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topy-ai/maggie",
3
- "version": "0.7.24",
3
+ "version": "0.7.26",
4
4
  "description": "Install and manage Maggie Skills for AI coding agents",
5
5
  "license": "MIT",
6
6
  "type": "module",