@design-ai/cli 4.55.0 → 4.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +39 -0
  3. package/README.ko.md +7 -7
  4. package/README.md +9 -9
  5. package/cli/lib/mcp-server.mjs +208 -10
  6. package/cli/lib/site-analysis.mjs +297 -0
  7. package/cli/lib/site-args.mjs +433 -0
  8. package/cli/lib/site-bundle-build.mjs +127 -0
  9. package/cli/lib/site-bundle-check.mjs +454 -0
  10. package/cli/lib/site-bundle-commands.mjs +95 -0
  11. package/cli/lib/site-bundle-compare.mjs +157 -0
  12. package/cli/lib/site-bundle-contract.mjs +79 -0
  13. package/cli/lib/site-bundle-files.mjs +87 -0
  14. package/cli/lib/site-bundle-handoff-runbook-actions.mjs +113 -0
  15. package/cli/lib/site-bundle-handoff-runbook-evidence-fields.mjs +164 -0
  16. package/cli/lib/site-bundle-handoff-runbook-evidence.mjs +334 -0
  17. package/cli/lib/site-bundle-handoff-runbook-format.mjs +31 -0
  18. package/cli/lib/site-bundle-handoff-runbook-stage-metadata.mjs +84 -0
  19. package/cli/lib/site-bundle-handoff-runbook.mjs +1331 -0
  20. package/cli/lib/site-bundle-handoff-summary.mjs +183 -0
  21. package/cli/lib/site-bundle-handoff.mjs +271 -0
  22. package/cli/lib/site-bundle-readme.mjs +98 -0
  23. package/cli/lib/site-bundle-repair-report.mjs +143 -0
  24. package/cli/lib/site-bundle-repair.mjs +68 -0
  25. package/cli/lib/site-content.mjs +399 -0
  26. package/cli/lib/site-evidence.mjs +35 -0
  27. package/cli/lib/site-mcp-commands.mjs +28 -0
  28. package/cli/lib/site-mcp-probes.mjs +159 -0
  29. package/cli/lib/site-mcp-readiness.mjs +157 -0
  30. package/cli/lib/site-mcp-report.mjs +324 -0
  31. package/cli/lib/site-next-actions.mjs +333 -0
  32. package/cli/lib/site-options.mjs +104 -0
  33. package/cli/lib/site-prompts.mjs +332 -0
  34. package/cli/lib/site-starter.mjs +153 -0
  35. package/cli/lib/site-strings.mjs +23 -0
  36. package/cli/lib/site-tasks.mjs +93 -0
  37. package/cli/lib/site-workflow-graph.mjs +309 -0
  38. package/cli/lib/site-workspace.mjs +492 -0
  39. package/cli/lib/site.mjs +108 -6617
  40. package/docs/DISTRIBUTION.ko.md +35 -6
  41. package/docs/DISTRIBUTION.md +35 -8
  42. package/docs/RELEASE-CHECKLIST.md +20 -3
  43. package/docs/ROADMAP.md +2179 -0
  44. package/docs/external-status.md +22 -7
  45. package/docs/integrations/design-ai-mcp-server.md +32 -0
  46. package/docs/integrations/vscode-walkthrough.ko.md +3 -3
  47. package/docs/integrations/vscode-walkthrough.md +3 -3
  48. package/docs/site-overrides/main.html +1 -1
  49. package/package.json +1 -1
  50. package/tools/audit/package-smoke.py +106 -0
  51. package/tools/audit/registry-smoke.py +378 -10
  52. package/tools/audit/smoke_assertions.py +83 -22
@@ -55,6 +55,7 @@ from smoke_assertions import (
55
55
  assert_check_examples_json_component_spec,
56
56
  assert_check_stdin_json_component_spec,
57
57
  assert_command_alias_output,
58
+ assert_design_ai_mcp_protocol_responses,
58
59
  assert_examples_human_output,
59
60
  assert_examples_json_route_hit,
60
61
  assert_force_overwrite_replaced,
@@ -127,6 +128,7 @@ from smoke_assertions import (
127
128
  format_cmd,
128
129
  help_alias_script,
129
130
  help_topic_script,
131
+ mcp_smoke_input,
130
132
  parse_help_topics,
131
133
  passing_doctor_report_json,
132
134
  passing_check_artifact_content,
@@ -154,6 +156,104 @@ from smoke_assertions import (
154
156
  ROOT = Path(__file__).resolve().parents[2]
155
157
  PACKAGE = json.loads((ROOT / "package.json").read_text(encoding="utf-8"))
156
158
  DEFAULT_PACKAGE_SPEC = f"{PACKAGE['name']}@{PACKAGE['version']}"
159
+ EXPECTED_SITE_MCP_PROBE_COUNTS = {"count": 4, "pass": 4, "warn": 0, "fail": 0}
160
+ EXPECTED_SITE_BUNDLE_MCP_PROBES_KEYS = [
161
+ "enabled",
162
+ "mode",
163
+ "externalCalls",
164
+ "status",
165
+ "count",
166
+ "pass",
167
+ "warn",
168
+ "fail",
169
+ "items",
170
+ ]
171
+ EXPECTED_SITE_BUNDLE_MCP_PROBE_ITEM_KEYS = [
172
+ "id",
173
+ "key",
174
+ "label",
175
+ "requestedStatus",
176
+ "level",
177
+ "passed",
178
+ "message",
179
+ "evidence",
180
+ "actions",
181
+ ]
182
+ EXPECTED_SITE_BUNDLE_MCP_PROBE_IDS = [
183
+ "github-repo-reference",
184
+ "figma-url-reference",
185
+ "browser-smoke-target",
186
+ "deploy-provider-reference",
187
+ ]
188
+
189
+
190
+ def assert_site_mcp_probe_counts(
191
+ actual: object,
192
+ *,
193
+ context: str,
194
+ label: str,
195
+ ) -> None:
196
+ if actual != EXPECTED_SITE_MCP_PROBE_COUNTS:
197
+ raise SystemExit(f"{label} after {context} MCP probe counts changed: {actual!r}")
198
+
199
+
200
+ def assert_smoke_json_keys(
201
+ payload: object,
202
+ expected_keys: list[str],
203
+ *,
204
+ label: str,
205
+ context: str,
206
+ command_label: str,
207
+ ) -> dict:
208
+ if not isinstance(payload, dict):
209
+ raise SystemExit(f"{command_label} after {context} {label} must be an object")
210
+ keys = list(payload.keys())
211
+ if keys != expected_keys:
212
+ raise SystemExit(f"{command_label} after {context} {label} keys changed: {keys!r}")
213
+ return payload
214
+
215
+
216
+ def assert_site_bundle_mcp_probes_payload(payload: object, *, context: str) -> None:
217
+ checked = assert_smoke_json_keys(
218
+ payload,
219
+ EXPECTED_SITE_BUNDLE_MCP_PROBES_KEYS,
220
+ label="mcp-probes.json",
221
+ context=context,
222
+ command_label="site bundle",
223
+ )
224
+ if checked.get("enabled") is not True or checked.get("mode") != "read-only-local":
225
+ raise SystemExit(f"site bundle after {context} mcp-probes.json mode changed")
226
+ if checked.get("externalCalls") is not False:
227
+ raise SystemExit(f"site bundle after {context} mcp-probes.json must remain read-only")
228
+ if checked.get("status") != "pass":
229
+ raise SystemExit(f"site bundle after {context} mcp-probes.json status changed")
230
+ assert_site_mcp_probe_counts(
231
+ {key: checked.get(key) for key in ("count", "pass", "warn", "fail")},
232
+ context=context,
233
+ label="site bundle mcp-probes.json",
234
+ )
235
+
236
+ items = checked.get("items")
237
+ if not isinstance(items, list) or len(items) != len(EXPECTED_SITE_BUNDLE_MCP_PROBE_IDS):
238
+ raise SystemExit(f"site bundle after {context} mcp-probes.json item count changed")
239
+ checked_ids = []
240
+ for item in items:
241
+ checked_item = assert_smoke_json_keys(
242
+ item,
243
+ EXPECTED_SITE_BUNDLE_MCP_PROBE_ITEM_KEYS,
244
+ label="mcp-probes.json item",
245
+ context=context,
246
+ command_label="site bundle",
247
+ )
248
+ checked_ids.append(checked_item.get("id"))
249
+ if checked_item.get("level") != "pass" or checked_item.get("passed") is not True:
250
+ raise SystemExit(f"site bundle after {context} mcp-probes.json item should pass: {checked_item.get('id')}")
251
+ if not isinstance(checked_item.get("evidence"), list) or not checked_item.get("evidence"):
252
+ raise SystemExit(f"site bundle after {context} mcp-probes.json item evidence missing")
253
+ if not isinstance(checked_item.get("actions"), list):
254
+ raise SystemExit(f"site bundle after {context} mcp-probes.json item actions must be an array")
255
+ if checked_ids != EXPECTED_SITE_BUNDLE_MCP_PROBE_IDS:
256
+ raise SystemExit(f"site bundle after {context} mcp-probes.json item order changed")
157
257
 
158
258
 
159
259
  def npm_exec_cmd(package_spec: str, *args: str) -> list[str]:
@@ -169,6 +269,18 @@ def npm_exec_cmd(package_spec: str, *args: str) -> list[str]:
169
269
  ]
170
270
 
171
271
 
272
+ def npm_exec_mcp_cmd(package_spec: str) -> list[str]:
273
+ return [
274
+ "npm",
275
+ "exec",
276
+ "--yes",
277
+ "--package",
278
+ package_spec,
279
+ "--",
280
+ "design-ai-mcp",
281
+ ]
282
+
283
+
172
284
  def npm_exec_shell_cmd(package_spec: str, script: str) -> list[str]:
173
285
  return [
174
286
  "npm",
@@ -2621,6 +2733,45 @@ def assert_version_json_smoke(cmd: list[str], *, env: dict[str, str], cwd: Path
2621
2733
  assert_version_json(result.stdout, context=context, cmd=cmd)
2622
2734
 
2623
2735
 
2736
+ def assert_design_ai_mcp_protocol_smoke(
2737
+ cmd: list[str],
2738
+ *,
2739
+ env: dict[str, str],
2740
+ cwd: Path | None,
2741
+ context: str,
2742
+ ) -> None:
2743
+ print(f"$ {format_cmd(cmd)} < MCP smoke", flush=True)
2744
+ result = subprocess.run(
2745
+ cmd,
2746
+ cwd=cwd,
2747
+ env=env,
2748
+ input=mcp_smoke_input(),
2749
+ text=True,
2750
+ capture_output=True,
2751
+ timeout=15,
2752
+ )
2753
+ if result.stdout:
2754
+ print(result.stdout, end="")
2755
+ if result.stderr:
2756
+ print(result.stderr, end="", file=sys.stderr)
2757
+
2758
+ if result.returncode != 0:
2759
+ raise SystemExit(f"command failed with exit code {result.returncode}: {format_cmd(cmd)}")
2760
+
2761
+ output = f"{result.stdout}\n{result.stderr}"
2762
+ assert_no_ansi(output, cmd)
2763
+
2764
+ responses = []
2765
+ for line in result.stdout.splitlines():
2766
+ if not line.strip():
2767
+ continue
2768
+ try:
2769
+ responses.append(json.loads(line))
2770
+ except json.JSONDecodeError as error:
2771
+ raise SystemExit(f"{context}: MCP stdout line is not JSON: {error}") from error
2772
+ assert_design_ai_mcp_protocol_responses(responses, context=context, cmd=cmd)
2773
+
2774
+
2624
2775
  def assert_workspace_json_smoke(cmd: list[str], *, env: dict[str, str], cwd: Path | None = None, context: str) -> None:
2625
2776
  result = run_plain(cmd, cwd=cwd, env=env)
2626
2777
  assert_workspace_json(result.stdout, context=context, cmd=cmd)
@@ -3031,6 +3182,7 @@ def assert_site_bundle_smoke(
3031
3182
  "summary.json",
3032
3183
  "website-workspace.tasks.json",
3033
3184
  "mcp-check.json",
3185
+ "mcp-probes.json",
3034
3186
  "mcp-action-plan.md",
3035
3187
  "website-handoff.md",
3036
3188
  "website-prompts.md",
@@ -3046,8 +3198,26 @@ def assert_site_bundle_smoke(
3046
3198
  raise SystemExit(f"site bundle after {context} summary status changed: {summary.get('status')!r}")
3047
3199
  if summary.get("taskGeneration", {}).get("totalTasks") != 3:
3048
3200
  raise SystemExit(f"site bundle after {context} expected 3 generated/retained tasks")
3201
+ evidence_counts = summary.get("implementationEvidence")
3202
+ if (
3203
+ not isinstance(evidence_counts, dict)
3204
+ or evidence_counts.get("executedWork") != 0
3205
+ or evidence_counts.get("verificationResults") != 0
3206
+ or evidence_counts.get("remainingRisks") != 3
3207
+ or evidence_counts.get("nextActions") != 0
3208
+ ):
3209
+ raise SystemExit(f"site bundle after {context} implementation evidence counts changed")
3049
3210
  if summary.get("files") != expected_files:
3050
3211
  raise SystemExit(f"site bundle after {context} file manifest changed")
3212
+ execution_checklist = summary.get("handoff", {}).get("executionChecklist")
3213
+ if not isinstance(execution_checklist, list) or [item.get("id") for item in execution_checklist] != [
3214
+ "confirm-target-repo",
3215
+ "inspect-architecture",
3216
+ "apply-focused-task",
3217
+ "verify-quality-gates",
3218
+ "record-handoff-evidence",
3219
+ ]:
3220
+ raise SystemExit(f"site bundle after {context} handoff execution checklist changed")
3051
3221
  checksums = summary.get("checksums", {})
3052
3222
  checksum_files = checksums.get("files", {})
3053
3223
  if checksums.get("algorithm") != "sha256":
@@ -3066,9 +3236,14 @@ def assert_site_bundle_smoke(
3066
3236
  task_ids = [task.get("id") for task in tasks.get("refactorTasks", [])]
3067
3237
  if task_ids != ["task-homepage-cta", "task-accessibility", "task-content-quality"]:
3068
3238
  raise SystemExit(f"site bundle after {context} task ids changed: {task_ids!r}")
3239
+ evidence = tasks.get("implementationEvidence")
3240
+ if not isinstance(evidence, dict) or len(evidence.get("remainingRisks", [])) != 3:
3241
+ raise SystemExit(f"site bundle after {context} did not preserve implementationEvidence in workspace JSON")
3069
3242
 
3070
3243
  mcp_check = json.loads((out_dir / "mcp-check.json").read_text(encoding="utf-8"))
3071
3244
  assert_site_mcp_check_json(json.dumps(mcp_check), context=context, cmd=cmd)
3245
+ mcp_probes = json.loads((out_dir / "mcp-probes.json").read_text(encoding="utf-8"))
3246
+ assert_site_bundle_mcp_probes_payload(mcp_probes, context=context)
3072
3247
  assert_site_mcp_plan_markdown((out_dir / "mcp-action-plan.md").read_text(encoding="utf-8"), context=context, cmd=cmd)
3073
3248
  implementation_prompt = (out_dir / "codex-implementation.md").read_text(encoding="utf-8")
3074
3249
  assert_no_ansi(implementation_prompt, cmd)
@@ -3083,6 +3258,8 @@ def assert_site_bundle_smoke(
3083
3258
  readme = (out_dir / "README.md").read_text(encoding="utf-8")
3084
3259
  if "Website improvement handoff bundle" not in readme or "does not call external MCPs" not in readme:
3085
3260
  raise SystemExit(f"site bundle after {context} README missing bundle boundary guidance")
3261
+ if "Target Repo Execution Checklist" not in readme or "Run target repo quality gates" not in readme:
3262
+ raise SystemExit(f"site bundle after {context} README missing target repo execution checklist")
3086
3263
 
3087
3264
 
3088
3265
  def assert_site_warning_bundle_smoke(
@@ -3141,22 +3318,82 @@ def assert_site_bundle_check_json_smoke(
3141
3318
  env: dict[str, str],
3142
3319
  cwd: Path | None = None,
3143
3320
  context: str,
3321
+ expected_evidence_counts: dict[str, int] | None = None,
3144
3322
  ) -> None:
3323
+ if expected_evidence_counts is None:
3324
+ expected_evidence_counts = {
3325
+ "executedWork": 0,
3326
+ "verificationResults": 0,
3327
+ "remainingRisks": 3,
3328
+ "nextActions": 0,
3329
+ }
3145
3330
  result = run_plain(cmd, cwd=cwd, env=env)
3146
3331
  assert_no_ansi(result.stdout, cmd)
3147
3332
  payload = json.loads(result.stdout)
3148
3333
  if payload.get("status") != "pass" or payload.get("valid") is not True:
3149
3334
  raise SystemExit(f"site bundle check after {context} expected pass/valid output")
3150
- if payload.get("counts", {}).get("presentFiles") != 8:
3151
- raise SystemExit(f"site bundle check after {context} expected 8 present files")
3152
- if payload.get("counts", {}).get("verifiedChecksumFiles") != 7:
3153
- raise SystemExit(f"site bundle check after {context} expected 7 verified checksum files")
3335
+ if payload.get("externalCalls") is not False or payload.get("targetRepoMutation") is not False:
3336
+ raise SystemExit(f"site bundle check after {context} boundary flags changed")
3337
+ boundaries = payload.get("boundaries")
3338
+ if (
3339
+ not isinstance(boundaries, list)
3340
+ or "deterministic-local" not in boundaries
3341
+ or "no-external-mcp-calls" not in boundaries
3342
+ or "no-target-repo-mutation" not in boundaries
3343
+ ):
3344
+ raise SystemExit(f"site bundle check after {context} boundary list changed: {boundaries!r}")
3345
+ if payload.get("counts", {}).get("presentFiles") != 9:
3346
+ raise SystemExit(f"site bundle check after {context} expected 9 present files")
3347
+ if payload.get("counts", {}).get("verifiedChecksumFiles") != 8:
3348
+ raise SystemExit(f"site bundle check after {context} expected 8 verified checksum files")
3154
3349
  if payload.get("counts", {}).get("checksumFailures") != 0:
3155
3350
  raise SystemExit(f"site bundle check after {context} expected no checksum failures")
3351
+ if payload.get("counts", {}).get("verifiedGeneratedFiles") != 8:
3352
+ raise SystemExit(f"site bundle check after {context} expected 8 current-contract generated files")
3353
+ if payload.get("counts", {}).get("generatedFailures") != 0:
3354
+ raise SystemExit(f"site bundle check after {context} expected no generated bundle contract failures")
3355
+ generated_contract = payload.get("generatedContract")
3356
+ if not isinstance(generated_contract, dict) or generated_contract.get("available") is not True:
3357
+ raise SystemExit(f"site bundle check after {context} generated contract diagnostics missing")
3358
+ if generated_contract.get("expectedFiles") != 8 or generated_contract.get("verifiedFiles") != 8:
3359
+ raise SystemExit(f"site bundle check after {context} generated contract file counts changed")
3360
+ if generated_contract.get("driftFiles") != []:
3361
+ raise SystemExit(f"site bundle check after {context} expected no generated contract drift files")
3362
+ generated_files = generated_contract.get("files")
3363
+ if not isinstance(generated_files, list) or len(generated_files) != 8:
3364
+ raise SystemExit(f"site bundle check after {context} expected 8 generated contract file diagnostics")
3365
+ for item in generated_files:
3366
+ if item.get("present") is not True or item.get("matches") is not True:
3367
+ raise SystemExit(f"site bundle check after {context} generated contract file did not match: {item!r}")
3368
+ for key in ("expectedDigest", "actualDigest"):
3369
+ digest = item.get(key)
3370
+ if not isinstance(digest, str) or len(digest) != 64 or any(char not in "0123456789abcdef" for char in digest):
3371
+ raise SystemExit(f"site bundle check after {context} generated contract {key} is not a SHA-256 hex digest")
3372
+ repair_guidance = payload.get("repairGuidance")
3373
+ if not isinstance(repair_guidance, dict) or repair_guidance.get("available") is not True:
3374
+ raise SystemExit(f"site bundle check after {context} repair guidance missing")
3375
+ if repair_guidance.get("targetRepoMutation") is not False or repair_guidance.get("externalCalls") is not False:
3376
+ raise SystemExit(f"site bundle check after {context} repair guidance boundary flags changed")
3377
+ repair_command = repair_guidance.get("command")
3378
+ verify_command = repair_guidance.get("verifyCommand")
3379
+ if (
3380
+ not isinstance(repair_command, str)
3381
+ or "website-workspace.tasks.json --bundle --out " not in repair_command
3382
+ or " --force" not in repair_command
3383
+ ):
3384
+ raise SystemExit(f"site bundle check after {context} repair command changed: {repair_command!r}")
3385
+ if not isinstance(verify_command, str) or "--bundle-check --strict --json" not in verify_command:
3386
+ raise SystemExit(f"site bundle check after {context} repair verify command changed: {verify_command!r}")
3156
3387
  if payload.get("summary", {}).get("totalTasks") != 3:
3157
3388
  raise SystemExit(f"site bundle check after {context} expected 3 tasks")
3158
3389
  if payload.get("summary", {}).get("siteName") != "Korean SaaS marketing site":
3159
3390
  raise SystemExit(f"site bundle check after {context} site name changed")
3391
+ evidence_counts = payload.get("summary", {}).get("implementationEvidence")
3392
+ if not isinstance(evidence_counts, dict):
3393
+ raise SystemExit(f"site bundle check after {context} implementationEvidence counts missing")
3394
+ for key, expected in expected_evidence_counts.items():
3395
+ if evidence_counts.get(key) != expected:
3396
+ raise SystemExit(f"site bundle check after {context} evidence count {key} changed: {evidence_counts.get(key)!r}")
3160
3397
  if payload.get("summary", {}).get("checksumAlgorithm") != "sha256":
3161
3398
  raise SystemExit(f"site bundle check after {context} checksum algorithm changed")
3162
3399
  bundle_digest = payload.get("summary", {}).get("checksumBundleDigest")
@@ -3164,6 +3401,18 @@ def assert_site_bundle_check_json_smoke(
3164
3401
  raise SystemExit(f"site bundle check after {context} bundle digest changed")
3165
3402
  if payload.get("mcpStatus") != "pass":
3166
3403
  raise SystemExit(f"site bundle check after {context} MCP status changed")
3404
+ if payload.get("mcpProbeStatus") != "pass":
3405
+ raise SystemExit(f"site bundle check after {context} MCP probe status changed")
3406
+ assert_site_mcp_probe_counts(
3407
+ payload.get("mcpProbeCounts"),
3408
+ context=context,
3409
+ label="site bundle check",
3410
+ )
3411
+ assert_site_mcp_probe_counts(
3412
+ payload.get("summary", {}).get("mcpProbeCounts"),
3413
+ context=context,
3414
+ label="site bundle check summary",
3415
+ )
3167
3416
  issue_ids = [issue.get("id") for issue in payload.get("issues", [])]
3168
3417
  if issue_ids != ["bundle-ready"]:
3169
3418
  raise SystemExit(f"site bundle check after {context} expected bundle-ready only, got {issue_ids!r}")
@@ -3175,7 +3424,15 @@ def assert_site_bundle_compare_json_smoke(
3175
3424
  env: dict[str, str],
3176
3425
  cwd: Path | None = None,
3177
3426
  context: str,
3427
+ expected_evidence_counts: dict[str, int] | None = None,
3178
3428
  ) -> None:
3429
+ if expected_evidence_counts is None:
3430
+ expected_evidence_counts = {
3431
+ "executedWork": 0,
3432
+ "verificationResults": 0,
3433
+ "remainingRisks": 3,
3434
+ "nextActions": 0,
3435
+ }
3179
3436
  result = run_plain(cmd, cwd=cwd, env=env)
3180
3437
  assert_no_ansi(result.stdout, cmd)
3181
3438
  payload = json.loads(result.stdout)
@@ -3191,6 +3448,21 @@ def assert_site_bundle_compare_json_smoke(
3191
3448
  raise SystemExit(f"site bundle compare after {context} {side} bundle digest changed")
3192
3449
  if payload.get(side, {}).get("siteName") != "Korean SaaS marketing site":
3193
3450
  raise SystemExit(f"site bundle compare after {context} {side} site name changed")
3451
+ assert_site_mcp_probe_counts(
3452
+ payload.get(side, {}).get("mcpProbeCounts"),
3453
+ context=context,
3454
+ label=f"site bundle compare {side}",
3455
+ )
3456
+ if payload.get(side, {}).get("verifiedGeneratedFiles") != 8 or payload.get(side, {}).get("generatedFailures") != 0:
3457
+ raise SystemExit(f"site bundle compare after {context} {side} generated bundle contract verification changed")
3458
+ if payload.get(side, {}).get("generatedDriftFiles") != []:
3459
+ raise SystemExit(f"site bundle compare after {context} {side} generated bundle contract drift changed")
3460
+ evidence_counts = payload.get(side, {}).get("implementationEvidence")
3461
+ if not isinstance(evidence_counts, dict):
3462
+ raise SystemExit(f"site bundle compare after {context} {side} implementationEvidence counts missing")
3463
+ for key, expected in expected_evidence_counts.items():
3464
+ if evidence_counts.get(key) != expected:
3465
+ raise SystemExit(f"site bundle compare after {context} {side} evidence count {key} changed: {evidence_counts.get(key)!r}")
3194
3466
  issue_ids = [issue.get("id") for issue in payload.get("issues", [])]
3195
3467
  if issue_ids != ["bundle-compare-identical"]:
3196
3468
  raise SystemExit(f"site bundle compare after {context} expected bundle-compare-identical only, got {issue_ids!r}")
@@ -3202,17 +3474,72 @@ def assert_site_bundle_handoff_json_smoke(
3202
3474
  env: dict[str, str],
3203
3475
  cwd: Path | None = None,
3204
3476
  context: str,
3477
+ expected_evidence_counts: dict[str, int] | None = None,
3205
3478
  ) -> None:
3479
+ if expected_evidence_counts is None:
3480
+ expected_evidence_counts = {
3481
+ "executedWork": 0,
3482
+ "verificationResults": 0,
3483
+ "remainingRisks": 3,
3484
+ "nextActions": 0,
3485
+ }
3206
3486
  result = run_plain(cmd, cwd=cwd, env=env)
3207
3487
  assert_no_ansi(result.stdout, cmd)
3208
3488
  payload = json.loads(result.stdout)
3209
3489
  if payload.get("status") != "pass" or payload.get("valid") is not True:
3210
3490
  raise SystemExit(f"site bundle handoff after {context} expected pass/valid output")
3491
+ expected_boundaries = [
3492
+ "deterministic-local",
3493
+ "no-external-mcp-calls",
3494
+ "no-target-repo-mutation",
3495
+ "no-lighthouse-axe-visual-diff",
3496
+ "target-repo-work-after-handoff",
3497
+ ]
3498
+ if payload.get("boundaries") != expected_boundaries:
3499
+ raise SystemExit(f"site bundle handoff after {context} boundary list changed: {payload.get('boundaries')!r}")
3500
+ if payload.get("externalCalls") is not False or payload.get("targetRepoMutation") is not False:
3501
+ raise SystemExit(f"site bundle handoff after {context} boundary flags changed")
3211
3502
  bundle = payload.get("bundle", {})
3503
+ source_bundle = payload.get("sourceBundle")
3504
+ if not isinstance(source_bundle, dict):
3505
+ raise SystemExit(f"site bundle handoff after {context} source bundle provenance missing")
3506
+ if bundle.get("sourceBundle") != source_bundle:
3507
+ raise SystemExit(f"site bundle handoff after {context} source bundle provenance is not mirrored under bundle")
3508
+ if (
3509
+ source_bundle.get("status") != "pass"
3510
+ or source_bundle.get("valid") is not True
3511
+ or source_bundle.get("sourceWorkspace") != "stdin"
3512
+ or source_bundle.get("siteName") != "Korean SaaS marketing site"
3513
+ ):
3514
+ raise SystemExit(f"site bundle handoff after {context} source bundle provenance summary changed: {source_bundle!r}")
3515
+ if (
3516
+ source_bundle.get("verifiedGeneratedFiles") != 8
3517
+ or source_bundle.get("expectedGeneratedFiles") != 8
3518
+ or source_bundle.get("verifiedChecksumFiles") != 8
3519
+ or source_bundle.get("expectedChecksumFiles") != 8
3520
+ ):
3521
+ raise SystemExit(f"site bundle handoff after {context} source bundle provenance verification counts changed: {source_bundle!r}")
3212
3522
  if bundle.get("siteName") != "Korean SaaS marketing site":
3213
3523
  raise SystemExit(f"site bundle handoff after {context} site name changed")
3214
- if bundle.get("verifiedChecksumFiles") != 7 or bundle.get("checksumFailures") != 0:
3524
+ if bundle.get("boundaries") != expected_boundaries:
3525
+ raise SystemExit(f"site bundle handoff after {context} bundle boundary list changed: {bundle.get('boundaries')!r}")
3526
+ if bundle.get("externalCalls") is not False or bundle.get("targetRepoMutation") is not False:
3527
+ raise SystemExit(f"site bundle handoff after {context} bundle boundary flags changed")
3528
+ if bundle.get("verifiedChecksumFiles") != 8 or bundle.get("checksumFailures") != 0:
3215
3529
  raise SystemExit(f"site bundle handoff after {context} checksum verification changed")
3530
+ if bundle.get("verifiedGeneratedFiles") != 8 or bundle.get("generatedFailures") != 0:
3531
+ raise SystemExit(f"site bundle handoff after {context} generated bundle contract verification changed")
3532
+ assert_site_mcp_probe_counts(
3533
+ bundle.get("mcpProbeCounts"),
3534
+ context=context,
3535
+ label="site bundle handoff",
3536
+ )
3537
+ evidence_counts = bundle.get("implementationEvidence")
3538
+ if not isinstance(evidence_counts, dict):
3539
+ raise SystemExit(f"site bundle handoff after {context} implementationEvidence counts missing")
3540
+ for key, expected in expected_evidence_counts.items():
3541
+ if evidence_counts.get(key) != expected:
3542
+ raise SystemExit(f"site bundle handoff after {context} evidence count {key} changed: {evidence_counts.get(key)!r}")
3216
3543
  digest = bundle.get("checksumBundleDigest")
3217
3544
  if not isinstance(digest, str) or len(digest) != 64:
3218
3545
  raise SystemExit(f"site bundle handoff after {context} bundle digest changed")
@@ -3221,9 +3548,9 @@ def assert_site_bundle_handoff_json_smoke(
3221
3548
  raise SystemExit(f"site bundle handoff after {context} prompt missing")
3222
3549
  for fragment in (
3223
3550
  "Website improvement target-repo handoff prompt",
3224
- "Bundle digest:",
3225
- "Run `design-ai site",
3226
- "Work inside the target website repository, not inside the design-ai repository.",
3551
+ "SHA-256 bundle digest:",
3552
+ "Source bundle strict check command: `design-ai site",
3553
+ "You are Codex working in the target website repository, not in the design-ai repository.",
3227
3554
  "# Codex implementation prompt",
3228
3555
  ):
3229
3556
  if fragment not in prompt:
@@ -4502,7 +4829,7 @@ def assert_learning_signals_report_smoke(
4502
4829
  cwd: Path | None = None,
4503
4830
  context: str,
4504
4831
  ) -> None:
4505
- seed_force_overwrite_target(output_path, context=context, cmd=["design-ai", "learn", "--signals", "--report"])
4832
+ seed_force_overwrite_target(output_path, context=context, cmd=["design-ai", "learn", "--signals", "--report", "--force"])
4506
4833
  cmd = command_factory(
4507
4834
  "learn",
4508
4835
  "--signals",
@@ -4536,7 +4863,7 @@ def assert_learning_agent_backlog_report_smoke(
4536
4863
  cwd: Path | None = None,
4537
4864
  context: str,
4538
4865
  ) -> None:
4539
- seed_force_overwrite_target(output_path, context=context, cmd=["design-ai", "learn", "--agent-backlog", "--report"])
4866
+ seed_force_overwrite_target(output_path, context=context, cmd=["design-ai", "learn", "--agent-backlog", "--report", "--force"])
4540
4867
  cmd = command_factory(
4541
4868
  "learn",
4542
4869
  "--agent-backlog",
@@ -4800,6 +5127,12 @@ def smoke_registry_package(package_spec: str, *, retries: int, delay: float) ->
4800
5127
  env=env,
4801
5128
  context="registry smoke npm exec version JSON",
4802
5129
  )
5130
+ assert_design_ai_mcp_protocol_smoke(
5131
+ npm_exec_mcp_cmd(package_spec),
5132
+ cwd=npx_root,
5133
+ env=env,
5134
+ context="registry smoke npm exec design-ai-mcp protocol",
5135
+ )
4803
5136
  assert_workspace_json_smoke(
4804
5137
  npm_exec_cmd(package_spec, "workspace", "--json"),
4805
5138
  cwd=npx_root,
@@ -6192,6 +6525,41 @@ def run_self_test() -> None:
6192
6525
  expected="without external calls",
6193
6526
  scope="registry smoke",
6194
6527
  )
6528
+ mcp_protocol_cmd = ["design-ai-mcp"]
6529
+ assert_design_ai_mcp_protocol_responses(
6530
+ [
6531
+ {"jsonrpc": "2.0", "id": 1, "result": {"serverInfo": {"name": "design-ai"}}},
6532
+ {"jsonrpc": "2.0", "id": 2, "result": {"tools": [{"name": "design_ai_route"}, {"name": "design_ai_search"}]}},
6533
+ {
6534
+ "jsonrpc": "2.0",
6535
+ "id": 3,
6536
+ "error": {"code": -32602, "message": "design_ai_search.limit must be an integer"},
6537
+ },
6538
+ ],
6539
+ context="registry smoke self-test design-ai MCP protocol",
6540
+ cmd=mcp_protocol_cmd,
6541
+ )
6542
+ expect_self_test_failure(
6543
+ lambda: assert_design_ai_mcp_protocol_responses(
6544
+ [
6545
+ {"jsonrpc": "2.0", "id": 1, "result": {"serverInfo": {"name": "design-ai"}}},
6546
+ {
6547
+ "jsonrpc": "2.0",
6548
+ "id": 2,
6549
+ "result": {"tools": [{"name": "design_ai_route"}, {"name": "design_ai_search"}]},
6550
+ },
6551
+ {
6552
+ "jsonrpc": "2.0",
6553
+ "id": 3,
6554
+ "error": {"code": -32602, "message": "wrong validation message"},
6555
+ },
6556
+ ],
6557
+ context="registry smoke self-test design-ai MCP protocol",
6558
+ cmd=mcp_protocol_cmd,
6559
+ ),
6560
+ expected="MCP invalid argument response did not preserve invalid params validation",
6561
+ scope="registry smoke",
6562
+ )
6195
6563
  assert_site_mcp_plan_markdown(
6196
6564
  passing_site_mcp_plan_markdown(),
6197
6565
  context="registry smoke self-test site mcp-plan",