@amityco/social-plus-vise 1.2.0 → 1.4.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 (47) hide show
  1. package/CHANGELOG.md +75 -2
  2. package/README.md +23 -10
  3. package/dist/capabilities.js +35 -4
  4. package/dist/entryState.js +71 -0
  5. package/dist/experience.js +70 -0
  6. package/dist/explore.js +51 -0
  7. package/dist/flow.js +382 -0
  8. package/dist/humanFormat.js +251 -0
  9. package/dist/intake.js +117 -0
  10. package/dist/intelligence/placement.js +2 -1
  11. package/dist/outcomes.js +141 -42
  12. package/dist/productExpectations.js +15 -0
  13. package/dist/requestReadiness.js +99 -0
  14. package/dist/server.js +675 -56
  15. package/dist/sidecar.js +5 -0
  16. package/dist/solutionPath.js +2 -2
  17. package/dist/tools/compliance.js +1014 -133
  18. package/dist/tools/creative.js +14 -12
  19. package/dist/tools/debug.js +83 -26
  20. package/dist/tools/design.js +344 -14
  21. package/dist/tools/experienceCompiler.js +1 -1
  22. package/dist/tools/experienceSensors.js +1 -1
  23. package/dist/tools/harness.js +13 -0
  24. package/dist/tools/integration.js +263 -90
  25. package/dist/tools/learning.js +1 -3
  26. package/dist/tools/project.js +987 -72
  27. package/dist/tools/sdkFacts.js +8 -1
  28. package/dist/tools/smoke.js +134 -0
  29. package/dist/tools/uxHarness.js +54 -6
  30. package/dist/uikitCustomization.js +22 -6
  31. package/dist/version.js +7 -3
  32. package/package.json +1 -1
  33. package/packages/intelligence/catalog/catalog.schema.json +1 -1
  34. package/packages/intelligence/catalog/experience-objects.json +2 -2
  35. package/packages/intelligence/catalog/variants.json +24 -0
  36. package/rules/event.yaml +3 -0
  37. package/rules/feed.yaml +251 -0
  38. package/rules/invitation.yaml +4 -0
  39. package/rules/live-data.yaml +110 -0
  40. package/rules/notification-tray.yaml +4 -0
  41. package/rules/poll.yaml +5 -0
  42. package/rules/sdk-lifecycle.yaml +559 -0
  43. package/rules/search.yaml +5 -0
  44. package/rules/security.yaml +12 -0
  45. package/rules/story.yaml +5 -0
  46. package/rules/user-blocking.yaml +5 -0
  47. package/skills/social-plus-vise/SKILL.md +163 -15
package/dist/server.js CHANGED
@@ -3,11 +3,14 @@ import { copyFile, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { sidecarDir as sidecarPath } from "./sidecar.js";
7
+ import { blueprintDigest, readFlowState, readOmittedSurfaces, renderFlowBlueprint, runtimeReadinessForStages, writeFlowBlueprintHtml, writeFlowState, } from "./flow.js";
8
+ import { normalizeSolutionPathAnswer } from "./solutionPath.js";
6
9
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
7
10
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
11
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
9
- import { attestRule, attestRuleTool, checkCompliance, checkComplianceTool, explainRule, explainRuleTool, experienceReportTool, initCompliance, initComplianceTool, initEngagement, initEngagementTool, showEngagement, showEngagementTool, statusCompliance, syncCompliance, syncComplianceTool, } from "./tools/compliance.js";
10
- import { designCheckTool, designExtractTool, designInitTokensTool, designPreviewTool, designReferenceTool } from "./tools/design.js";
12
+ import { attestRule, attestRuleTool, checkCompliance, checkComplianceTool, explainRule, explainRuleTool, experienceReportTool, initCompliance, initComplianceTool, recordBaseline, initEngagement, initEngagementTool, listRules, showEngagement, showEngagementTool, statusCompliance, syncCompliance, syncComplianceTool, } from "./tools/compliance.js";
13
+ import { designCheckTool, designContrastTool, designExtractTool, designInitTokensTool, designPreviewTool, designReferenceTool } from "./tools/design.js";
11
14
  import { getDocPageTool, searchDocsTool } from "./tools/docs.js";
12
15
  import { compileExperienceTool } from "./tools/experienceCompiler.js";
13
16
  import { experienceSensorsTool } from "./tools/experienceSensors.js";
@@ -17,12 +20,16 @@ import { recordLearningTool, showLearningTool } from "./tools/learning.js";
17
20
  import { inspectProjectTool, validateSetupTool } from "./tools/project.js";
18
21
  import { resolveRequestTool, suggestPatchTool } from "./tools/resolve.js";
19
22
  import { runSensorsTool } from "./tools/sensors.js";
23
+ import { smokeTool } from "./tools/smoke.js";
20
24
  import { getSdkFactsTool } from "./tools/sdkFacts.js";
21
25
  import { addBlockInstall, listRegistryBlocks, planBlockInstall, validateBlockInstall } from "./tools/blocks.js";
22
26
  import { debugIssueTool, debugIssue } from "./tools/debug.js";
23
27
  import { creativeAcceptTool, creativeBriefTool } from "./tools/creative.js";
24
28
  import { uxHarnessTool } from "./tools/uxHarness.js";
25
- import { packageName, packageVersion } from "./version.js";
29
+ import { exploreRequest } from "./explore.js";
30
+ import { renderHuman, wantsHumanFormat } from "./humanFormat.js";
31
+ import { buildSource, packageName, packageRoot, packageVersion } from "./version.js";
32
+ const SUPPORT_URL = "mailto:support@social.plus";
26
33
  const tools = new Map([
27
34
  searchDocsTool,
28
35
  getDocPageTool,
@@ -46,11 +53,13 @@ const tools = new Map([
46
53
  showEngagementTool,
47
54
  resolveRequestTool,
48
55
  runSensorsTool,
56
+ smokeTool,
49
57
  validateSetupTool,
50
58
  suggestPatchTool,
51
59
  debugIssueTool,
52
60
  designExtractTool,
53
61
  designCheckTool,
62
+ designContrastTool,
54
63
  designPreviewTool,
55
64
  designReferenceTool,
56
65
  designInitTokensTool,
@@ -107,14 +116,14 @@ async function handleCli(args) {
107
116
  console.log(helpText(args[1]));
108
117
  return "exit";
109
118
  }
110
- if (command === "doctor" || command === "--doctor") {
111
- console.log(JSON.stringify(doctorResult(), null, 2));
112
- return "exit";
113
- }
114
119
  if (isHelpRequest(args)) {
115
120
  console.log(helpText(command));
116
121
  return "exit";
117
122
  }
123
+ if (command === "doctor" || command === "--doctor") {
124
+ emitResult("doctor", doctorResult(), args);
125
+ return "exit";
126
+ }
118
127
  try {
119
128
  if (command === "install-skill" || command === "install_skill") {
120
129
  console.log(JSON.stringify(await installSkill(args.slice(1)), null, 2));
@@ -286,15 +295,10 @@ async function handleCli(args) {
286
295
  }
287
296
  if (command === "plan" || command === "plan-integration") {
288
297
  const input = await planCliInput(args);
289
- if (hasFlag(args, "summary")) {
290
- const result = await planIntegrationTool.call(input);
291
- const text = result.content.map((item) => item.text).join("\n");
292
- const payload = JSON.parse(text);
293
- console.log(JSON.stringify(planSummary(payload), null, 2));
294
- }
295
- else {
296
- await printToolResult(planIntegrationTool, input);
297
- }
298
+ const result = await planIntegrationTool.call(input);
299
+ const text = result.content.map((item) => item.text).join("\n");
300
+ const payload = JSON.parse(text);
301
+ emitResult("plan", hasFlag(args, "summary") ? planSummary(payload) : payload, args);
298
302
  return "exit";
299
303
  }
300
304
  if (command === "plan-harness") {
@@ -318,7 +322,14 @@ async function handleCli(args) {
318
322
  console.log(JSON.stringify(await completeWorkplanSurface(subArgs), null, 2));
319
323
  return "exit";
320
324
  }
321
- console.error(`Unknown workplan subcommand: ${sub ?? "(none)"}. Expected "next", "status", or "complete".`);
325
+ if (sub === "trim") {
326
+ assertOnlyKnownFlags(subArgs, ["request", "surface", "surface-path", "answer", "reason"], "workplan trim");
327
+ const trim = await trimWorkplanSurface(subArgs);
328
+ console.log(JSON.stringify(trim.payload, null, 2));
329
+ process.exitCode = trim.exitCode;
330
+ return "exit";
331
+ }
332
+ console.error(`Unknown workplan subcommand: ${sub ?? "(none)"}. Expected "next", "status", "complete", or "trim".`);
322
333
  process.exitCode = 1;
323
334
  return "exit";
324
335
  }
@@ -345,6 +356,21 @@ async function handleCli(args) {
345
356
  }
346
357
  return "exit";
347
358
  }
359
+ if (command === "smoke") {
360
+ const printed = await printToolResult(smokeTool, {
361
+ repoPath: positionalRepoPath(args.slice(1)),
362
+ logPath: flagValue(args, "log") ?? flagValue(args, "log-path"),
363
+ surface: flagValue(args, "surface"),
364
+ });
365
+ const status = toolResultStatus(printed);
366
+ if (status === "failed") {
367
+ process.exitCode = 1;
368
+ }
369
+ else if (status === "no-config" || status === "no-log" || status === "no-surfaces") {
370
+ process.exitCode = 2;
371
+ }
372
+ return "exit";
373
+ }
348
374
  if (command === "resolve") {
349
375
  await printToolResult(resolveRequestTool, {
350
376
  repoPath: positionalRepoPath(args.slice(1)),
@@ -418,21 +444,47 @@ async function handleCli(args) {
418
444
  return "exit";
419
445
  }
420
446
  if (command === "init") {
421
- assertOnlyKnownFlags(args, ["request", "surface", "surface-path", "answer", "allow-unresolved-intake"], "init");
422
- const result = await initCompliance(positionalRepoPath(args.slice(1)), requiredFlagValue(args, "request", "init requires --request."), flagValue(args, "surface") ?? flagValue(args, "surface-path"), keyValueFlag(args, "answer"), { allowUnresolvedIntake: hasFlag(args, "allow-unresolved-intake") });
447
+ assertOnlyKnownFlags(args, ["request", "surface", "surface-path", "answer", "allow-unresolved-intake", "baseline"], "init");
448
+ const initRepo = positionalRepoPath(args.slice(1));
449
+ const initRequest = requiredFlagValue(args, "request", "init requires --request.");
450
+ const initWorkplanPlan = await workplanPlan(initRepo, initRequest, args);
451
+ const { sequence: initSequence, omitted: initOmitted } = await activeBlueprint(initRepo, initRequest, initWorkplanPlan);
452
+ if (initSequence.length >= 2 && initWorkplanPlan.platform !== "unknown") {
453
+ const blueprintGate = await requireBlueprintConfirmation(initRepo, initRequest, initWorkplanPlan, initSequence, args, new Date().toISOString(), initOmitted);
454
+ if (blueprintGate) {
455
+ console.log(JSON.stringify(blueprintGate, null, 2));
456
+ process.exitCode = 7;
457
+ return "exit";
458
+ }
459
+ const workplanInitGate = await requireNextWorkplanSurfaceInit(initRepo, initRequest, initSequence, keyValueFlag(args, "answer"));
460
+ if (workplanInitGate) {
461
+ console.log(JSON.stringify(workplanInitGate, null, 2));
462
+ process.exitCode = 7;
463
+ return "exit";
464
+ }
465
+ }
466
+ const result = await initCompliance(initRepo, initRequest, flagValue(args, "surface") ?? flagValue(args, "surface-path"), keyValueFlag(args, "answer"), { allowUnresolvedIntake: hasFlag(args, "allow-unresolved-intake") });
423
467
  if (result.status === "needs-clarification" && typeof result.exitCode === "number") {
424
468
  process.exitCode = result.exitCode;
425
469
  }
470
+ if (hasFlag(args, "baseline") && result.status === "initialized") {
471
+ result.baseline_recorded = await recordBaseline(initRepo);
472
+ }
426
473
  console.log(JSON.stringify(result, null, 2));
427
474
  return "exit";
428
475
  }
429
476
  if (command === "check") {
430
- assertOnlyKnownFlags(args, ["ci"], "check");
431
- const result = await checkCompliance(positionalRepoPath(args.slice(1)));
432
- console.log(JSON.stringify(hasFlag(args, "ci") ? ciCheckResult(result) : result, null, 2));
477
+ assertOnlyKnownFlags(args, ["ci", "format", "new-only"], "check");
478
+ const result = await checkCompliance(positionalRepoPath(args.slice(1)), { newOnly: hasFlag(args, "new-only") });
479
+ emitResult("check", hasFlag(args, "ci") ? ciCheckResult(result) : result, args);
433
480
  process.exitCode = result.exitCode;
434
481
  return "exit";
435
482
  }
483
+ if (command === "baseline") {
484
+ assertOnlyKnownFlags(args, [], "baseline");
485
+ console.log(JSON.stringify(await recordBaseline(positionalRepoPath(args.slice(1))), null, 2));
486
+ return "exit";
487
+ }
436
488
  if (command === "sync") {
437
489
  assertOnlyKnownFlags(args, [], "sync");
438
490
  console.log(JSON.stringify(await syncCompliance(positionalRepoPath(args.slice(1))), null, 2));
@@ -452,13 +504,26 @@ async function handleCli(args) {
452
504
  return "exit";
453
505
  }
454
506
  if (command === "explain") {
455
- assertOnlyKnownFlags(args, [], "explain");
456
- console.log(JSON.stringify(await explainRule(requiredPositionalText(args.slice(1), "explain requires a rule id.")), null, 2));
507
+ assertOnlyKnownFlags(args, ["format"], "explain");
508
+ const explainRuleId = positionalValues(args.slice(1))[0];
509
+ const payload = explainRuleId ? await explainRule(explainRuleId) : await listRules();
510
+ emitResult("explain", payload, args);
511
+ return "exit";
512
+ }
513
+ if (command === "explore") {
514
+ assertOnlyKnownFlags(args, ["request", "platform", "format"], "explore");
515
+ const request = flagValue(args, "request") ??
516
+ requiredPositionalText(args.slice(1), 'explore requires a request, e.g. `vise explore "add a social feed"`.');
517
+ emitResult("explore", exploreRequest(request, flagValue(args, "platform")), args);
457
518
  return "exit";
458
519
  }
459
520
  if (command === "status") {
460
- assertOnlyKnownFlags(args, [], "status");
461
- console.log(JSON.stringify(await statusCompliance(positionalRepoPath(args.slice(1))), null, 2));
521
+ assertOnlyKnownFlags(args, ["format", "new-only"], "status");
522
+ const result = await statusCompliance(positionalRepoPath(args.slice(1)), { newOnly: hasFlag(args, "new-only") });
523
+ emitResult("status", result, args);
524
+ if (typeof result.exitCode === "number") {
525
+ process.exitCode = result.exitCode;
526
+ }
462
527
  return "exit";
463
528
  }
464
529
  if (command === "engagement") {
@@ -512,6 +577,17 @@ async function handleCli(args) {
512
577
  await printToolResult(designCheckTool, { repoPath: positionalRepoPath(subArgs) });
513
578
  return "exit";
514
579
  }
580
+ if (sub === "contrast") {
581
+ assertOnlyKnownFlags(subArgs, [], "design contrast");
582
+ const colors = subArgs.filter((arg) => !arg.startsWith("-"));
583
+ if (colors.length < 2) {
584
+ console.error("design contrast requires two colours: vise design contrast <foreground> <background> (e.g. vise design contrast '#5a6477' '#0e1726').");
585
+ process.exitCode = 1;
586
+ return "exit";
587
+ }
588
+ await printToolResult(designContrastTool, { foreground: colors[0], background: colors[1] });
589
+ return "exit";
590
+ }
515
591
  if (sub === "preview") {
516
592
  assertOnlyKnownFlags(subArgs, ["reference", "no-write"], "design preview");
517
593
  await printToolResult(designPreviewTool, {
@@ -538,7 +614,7 @@ async function handleCli(args) {
538
614
  });
539
615
  return "exit";
540
616
  }
541
- console.error(`Unknown design subcommand: ${sub ?? "(none)"}. Expected "extract", "check", "preview", "reference", or "init-tokens".`);
617
+ console.error(`Unknown design subcommand: ${sub ?? "(none)"}. Expected "extract", "check", "contrast", "preview", "reference", or "init-tokens".`);
542
618
  process.exitCode = 1;
543
619
  return "exit";
544
620
  }
@@ -595,7 +671,9 @@ Re-plan with collected answers (repeat --answer for each intake question):
595
671
  vise plan . --request "Add a social feed" \\
596
672
  --answer feed_scope=community \\
597
673
  --answer feed_target=existing\\ communityId \\
598
- --answer target_screen_or_route=app/feed/page.tsx`;
674
+ --answer target_screen_or_route=app/feed/page.tsx
675
+
676
+ Output is JSON by default (for agents/CI). Add --format human for a readable plan summary.`;
599
677
  }
600
678
  if (command === "creative") {
601
679
  return `${packageName} creative
@@ -610,8 +688,8 @@ Usage:
610
688
  vise creative [repoPath] --request "Add engagement" --prototype ./prototype.html
611
689
  vise creative [repoPath] --request "Add engagement" --ranking-preview
612
690
  vise creative [repoPath] --request "Add engagement" --no-write
613
- vise creative accept [repoPath] --variant community-first
614
- vise creative accept [repoPath] --variant none --rationale "No variant fits X; closest is Y" --closest discovery-first
691
+ vise creative accept [repoPath] --variant community-first --rationale "Always-on community participation is the primary destination." --confidence high
692
+ vise creative accept [repoPath] --variant none --rationale "No variant fits X; closest is Y" --confidence high --closest discovery-first
615
693
 
616
694
  Output:
617
695
  Writes sp-vise/creative-brief.json and sp-vise/creative-brief.md unless --no-write is set.
@@ -695,12 +773,17 @@ Usage:
695
773
  vise workplan next [repoPath] --request "Add feed, comments, chat, and profile"
696
774
  vise workplan status [repoPath] --request "Add feed, comments, chat, and profile"
697
775
  vise workplan complete [repoPath] --request "Add feed, comments, chat, and profile" --surface feed
776
+ vise workplan trim [repoPath] --request "..." --surface chat --reason "goal never asked for messaging"
698
777
 
699
778
  Notes:
700
779
  next/status re-run the broad plan, ignore any feature_surface answer, and combine it
701
780
  with sp-vise/workplan.json progress. complete records a host-agent progress marker;
702
781
  run vise check, vise sync, vise validate, and vise run-sensors before marking a
703
- surface complete.`;
782
+ surface complete.
783
+ trim deliberately omits a companion surface (surface-level scope-omit): --reason is
784
+ required, the surface must not already be built, and at least two surfaces must remain.
785
+ A trim changes the blueprint, so the sign-off gate re-fires — the human re-confirms the
786
+ trimmed blueprint (which names what was dropped) before any surface proceeds.`;
704
787
  }
705
788
  if (command === "search-docs" || command === "search_docs") {
706
789
  return `${packageName} search-docs
@@ -789,6 +872,23 @@ Run detected project command sensors such as typecheck, test, build, Flutter, or
789
872
 
790
873
  Usage:
791
874
  vise run-sensors [repoPath] [--dry-run] [--include "npm test"] [--timeout-ms 120000]`;
875
+ }
876
+ if (command === "smoke") {
877
+ return `${packageName} smoke
878
+
879
+ Assess a captured runtime mount-smoke log into a pass/fail verdict and record runtime-smoke
880
+ evidence. Catches the session-establish race (a live-data query fired before the SDK session is
881
+ established) that static \`vise check\` cannot see. Declare collection surfaces in sp-vise/smoke.json,
882
+ emit VISE_SMOKE markers from the mounted SDK query lifecycle, cold-launch into the social route,
883
+ wait for the collection to resolve, capture the runtime log, then:
884
+
885
+ Usage:
886
+ vise smoke [repoPath] --log <captured-log> [--surface <id>]
887
+
888
+ Android: adb logcat -c before launch, then adb logcat -d -v time | grep VISE_SMOKE > sp-vise/evidence/runtime-smoke.log
889
+ iOS: xcrun simctl spawn <device> log show --style compact --last 3m --predicate 'eventMessage CONTAINS "VISE_SMOKE"' > sp-vise/evidence/runtime-smoke.log
890
+
891
+ See docs/RUNTIME_SMOKE.md.`;
792
892
  }
793
893
  if (command === "resolve") {
794
894
  return `${packageName} resolve
@@ -844,7 +944,9 @@ Usage:
844
944
  Check the current source and recorded attestations against the compliance contract. Read-only.
845
945
 
846
946
  Usage:
847
- vise check [repoPath] [--ci]`;
947
+ vise check [repoPath] [--ci] [--format human]
948
+
949
+ Output is JSON by default (for agents/CI). Add --format human for a readable verdict summary.`;
848
950
  }
849
951
  if (command === "sync") {
850
952
  return `${packageName} sync
@@ -860,23 +962,45 @@ Usage:
860
962
  Record a host-agent or local-human attestation for one compliance rule.
861
963
 
862
964
  Usage:
863
- vise attest [repoPath] --rule sdk.init.at-startup --confidence high --signer host-agent --evidence-file evidence.json --rationale "Why this rule is satisfied."`;
965
+ vise attest [repoPath] --rule typescript.client.region --confidence high --signer host-agent --evidence-file evidence.json --rationale "Why this rule is satisfied."`;
864
966
  }
865
967
  if (command === "explain") {
866
968
  return `${packageName} explain
867
969
 
868
- Explain one compliance rule.
970
+ Explain one compliance rule, or list every valid rule id when run with no id.
971
+
972
+ Usage:
973
+ vise explain [--format human] List all rule ids (public + contract) with where each applies
974
+ vise explain typescript.client.region [--format human]`;
975
+ }
976
+ if (command === "explore") {
977
+ return `${packageName} explore
978
+
979
+ Discover what social.plus offers for a request — before any project or credentials exist. Read-only:
980
+ maps the request to a candidate outcome (or lists the full menu when it can't), and for each shows the
981
+ capabilities involved, the canonical docs, and the command to start. No project, no API key, no writes.
869
982
 
870
983
  Usage:
871
- vise explain sdk.init.at-startup`;
984
+ vise explore "add a social feed" [--platform typescript] [--format human]`;
985
+ }
986
+ if (command === "doctor") {
987
+ return `${packageName} doctor
988
+
989
+ Print install diagnostics (Node version, transport, docs source, registered tools).
990
+ Read-only. JSON by default; --format human for a readable summary.
991
+
992
+ Usage:
993
+ vise doctor [--format human]
994
+
995
+ If diagnostics report a problem, see ${SUPPORT_URL}`;
872
996
  }
873
997
  if (command === "status") {
874
998
  return `${packageName} status
875
999
 
876
- Print a compact compliance summary.
1000
+ Print a compact compliance summary. JSON by default; --format human for a readable verdict.
877
1001
 
878
1002
  Usage:
879
- vise status [repoPath]`;
1003
+ vise status [repoPath] [--format human]`;
880
1004
  }
881
1005
  if (command === "design") {
882
1006
  return `${packageName} design
@@ -897,7 +1021,8 @@ extract Build a graded design contract and write it to sp-vise/design-contrac
897
1021
  With --from-project (no external prototype), derive the contract from the host
898
1022
  project's OWN design system: CSS custom properties (incl. shadcn :root and
899
1023
  Tailwind v4 @theme), TS/JS token modules, inline tailwind configs, Android
900
- colors.xml/dimens.xml, Flutter Color(0x..), and iOS .xcassets/.colorset +
1024
+ colors.xml/dimens.xml, Jetpack Compose Kotlin themes (Color(0xFF..) in
1025
+ Theme.kt/Color.kt), Flutter Color(0x..), and iOS .xcassets/.colorset +
901
1026
  Swift colors. Reference values (var()/theme()/calc()) are skipped, so a
902
1027
  var-mapped config contributes nothing rather than wrong tokens.
903
1028
  check Advisory, non-blocking report on how closely the project's UI code
@@ -925,14 +1050,17 @@ contractual record of which broad-social outcomes are in scope for this project.
925
1050
 
926
1051
  Usage:
927
1052
  vise engagement init [repoPath] --tier <free|pro|partner> --customer-id <id> --scope <outcome,...>
928
- vise engagement init [repoPath] --scope feed,communities --target-completion 2026-12-31 \\
1053
+ vise engagement init [repoPath] --scope add-feed,add-community --target-completion 2026-12-31 \\
929
1054
  --reviewer-name "Jane Doe" --reviewer-email jane@example.com --evidence-upload-consent
930
1055
  vise engagement show [repoPath]
931
1056
 
932
1057
  Flags (init):
933
1058
  --tier <free|pro|partner> Engagement tier (validated).
934
1059
  --customer-id <id> Customer identifier recorded in the artifact.
935
- --scope <outcome,...> In-scope broad-social outcomes (repeatable; comma-separated).
1060
+ --scope <outcome,...> In-scope outcomes, comma-separated. One or more of: setup-sdk,
1061
+ setup-push, setup-live-data, add-feed, add-comments, add-chat,
1062
+ add-community, add-follow, add-moderation, add-notifications,
1063
+ troubleshoot, validate-setup.
936
1064
  --target-completion <date> Target completion date, YYYY-MM-DD.
937
1065
  --reviewer-name <name> Human reviewer recorded on the engagement.
938
1066
  --reviewer-email <email> Human reviewer contact.
@@ -950,7 +1078,7 @@ Usage:
950
1078
  vise print-skill Print bundled skill markdown
951
1079
  vise inspect [repoPath] Inspect platform and design signals
952
1080
  vise creative [repoPath] --request "..." Create an Engagement Intelligence brief
953
- vise creative accept [repoPath] --variant <id|none> Accept a creative variant, or record a no-fit catalog gap with --variant none
1081
+ vise creative accept [repoPath] --variant <id|none> --rationale "..." --confidence high
954
1082
  vise ux-harness [repoPath] Generate UX Harness expectations from creative selection
955
1083
  vise experience-report [repoPath] Write the advisory dimensioned Experience Report
956
1084
  vise experience compile [repoPath] Compile selected variant into implementation artifacts
@@ -958,10 +1086,12 @@ Usage:
958
1086
  vise learning record [repoPath] Record a local-only learning event
959
1087
  vise learning show [repoPath] Show local learning summary
960
1088
  vise debug [repoPath] --error ... Debug an SDK-specific runtime error and emit a repair brief
1089
+ vise explore "<request>" Discover what social.plus offers for a request (no project/credentials needed)
961
1090
  vise plan [repoPath] --request "..." Create an implementation plan
962
1091
  vise workplan next [repoPath] --request "..." Get the next broad-social surface to implement
963
- vise init [repoPath] --request "..." Initialize compliance sidecar
964
- vise check [repoPath] Check compliance contract
1092
+ vise init [repoPath] --request "..." Initialize compliance sidecar (add --baseline on a brownfield app)
1093
+ vise check [repoPath] Check compliance contract (add --new-only to gate on findings since the baseline)
1094
+ vise baseline [repoPath] Snapshot pre-existing findings so check --new-only gates only new ones
965
1095
  vise sync [repoPath] Persist deterministic-pass evidence
966
1096
  vise attest [repoPath] --rule ... Record a compliance attestation
967
1097
  vise explain <ruleId> Explain one compliance rule
@@ -999,6 +1129,16 @@ async function printToolResult(tool, input) {
999
1129
  console.log(text);
1000
1130
  return { result, text };
1001
1131
  }
1132
+ function emitResult(command, payload, args) {
1133
+ if (wantsHumanFormat(flagValue(args, "format"))) {
1134
+ const human = renderHuman(command, payload);
1135
+ if (human !== null) {
1136
+ console.log(human);
1137
+ return;
1138
+ }
1139
+ }
1140
+ console.log(JSON.stringify(payload, null, 2));
1141
+ }
1002
1142
  async function planCliInput(args) {
1003
1143
  const subArgs = args.slice(1);
1004
1144
  const requestFromFlag = flagValue(args, "request");
@@ -1365,11 +1505,263 @@ function assertOnlyKnownFlags(args, allowed, commandName) {
1365
1505
  const allowedList = allowed.length > 0 ? `Allowed flags: ${allowed.map((flag) => `--${flag}`).join(", ")}.` : "This command takes no flags.";
1366
1506
  throw new Error(`${commandName} does not accept ${unknown.join(", ")}. ${allowedList} Run \`vise ${commandName} --help\` for usage.`);
1367
1507
  }
1508
+ function blueprintStagesForSequence(sequence) {
1509
+ return sequence.map((surface) => ({ id: surface.id, outcome: surface.outcome, label: surface.label }));
1510
+ }
1511
+ async function activeBlueprint(repoRoot, request, plan) {
1512
+ const planSequence = socialWorkplanSequence(plan);
1513
+ const full = planSequence.length
1514
+ ? planSequence
1515
+ : await socialWorkplanSequenceFromSignedFlow(repoRoot, request);
1516
+ const omitted = await readOmittedSurfaces(repoRoot, request);
1517
+ const omittedIds = new Set(omitted.map((item) => item.id));
1518
+ return { sequence: full.filter((surface) => !omittedIds.has(surface.id)), omitted };
1519
+ }
1520
+ async function socialWorkplanSequenceFromSignedFlow(repoRoot, request) {
1521
+ const state = await readFlowState(repoRoot);
1522
+ if (state?.request !== request) {
1523
+ return [];
1524
+ }
1525
+ const stages = state.blueprint?.stages ?? [];
1526
+ if (stages.length <= 1) {
1527
+ return [];
1528
+ }
1529
+ return stages.map((stage, index) => {
1530
+ const id = stage.id || stage.outcome;
1531
+ const commonArgs = `. --request ${shellQuoteForCommand(request)} --answer feature_surface=${id}`;
1532
+ return {
1533
+ id,
1534
+ order: index + 1,
1535
+ outcome: stage.outcome,
1536
+ label: stage.label ?? id,
1537
+ planCommand: `vise plan ${commonArgs}`,
1538
+ initCommand: `vise init ${commonArgs}`,
1539
+ intake: {
1540
+ status: "from-signed-blueprint",
1541
+ questions: [],
1542
+ remainingBlocking: 0,
1543
+ },
1544
+ validation: ["validate_setup", "run_sensors"],
1545
+ };
1546
+ });
1547
+ }
1548
+ async function requireBlueprintConfirmation(repoRoot, request, plan, sequence, args, now, omitted = []) {
1549
+ if (sequence.length === 0) {
1550
+ return null;
1551
+ }
1552
+ const stages = blueprintStagesForSequence(sequence);
1553
+ const runtimeReadiness = runtimeReadinessForStages(stages);
1554
+ const existing = await readFlowState(repoRoot);
1555
+ const answers = keyValueFlag(args, "answer");
1556
+ const existingBlueprint = existing?.request === request ? existing.blueprint : undefined;
1557
+ const existingPath = existingBlueprint?.path ?? null;
1558
+ const existingPathDecisionConfirmed = Boolean(existingBlueprint?.path_decision_confirmed === true || existingBlueprint?.confirmed_digest);
1559
+ const persistedPath = existingPathDecisionConfirmed ? existingPath : null;
1560
+ const explicitPathAnswer = normalizeSolutionPathAnswer(answers.solution_path);
1561
+ const pathDecidedness = plan.entryState?.pathDecidedness ?? null;
1562
+ const pathNeedsDecision = (pathDecidedness === "undecided" || pathDecidedness === "conflict") && !persistedPath && !explicitPathAnswer;
1563
+ const pathForDigest = explicitPathAnswer ?? persistedPath ?? plan.solutionPath?.recommendation ?? null;
1564
+ const design = plan.designReview?.digest ?? plan.designContract?.digest ?? null;
1565
+ const digest = blueprintDigest({ path: pathForDigest, stages, design });
1566
+ const confirmedByAnswer = answers.blueprint_confirmation === digest;
1567
+ const alreadyConfirmed = existing?.request === request && existing?.blueprint?.confirmed_digest === digest;
1568
+ const persist = (confirmed) => {
1569
+ const pathDecisionConfirmed = confirmed || Boolean(existingPathDecisionConfirmed && existingPath && existingPath === pathForDigest);
1570
+ return {
1571
+ schema_version: 1,
1572
+ vise_version: packageVersion,
1573
+ generated_at: existing?.generated_at ?? now,
1574
+ updated_at: now,
1575
+ request,
1576
+ blueprint: {
1577
+ digest,
1578
+ confirmed_digest: confirmed ? digest : null,
1579
+ ...(confirmed ? { confirmed_at: alreadyConfirmed ? existing?.blueprint?.confirmed_at ?? now : now } : {}),
1580
+ stages,
1581
+ ...(pathForDigest ? { path: pathForDigest, path_decision_confirmed: pathDecisionConfirmed } : {}),
1582
+ ...(omitted.length ? { omitted } : {}),
1583
+ runtime_readiness: runtimeReadiness,
1584
+ },
1585
+ };
1586
+ };
1587
+ if (alreadyConfirmed) {
1588
+ await writeFlowState(repoRoot, persist(true));
1589
+ return null;
1590
+ }
1591
+ const experience = await blueprintExperience(repoRoot);
1592
+ const blueprintRel = (fsPath) => repoRelativePath(repoRoot, fsPath);
1593
+ const approvalChecklist = (pathDecision) => [
1594
+ {
1595
+ id: "solution_path",
1596
+ label: "Solution path",
1597
+ status: pathDecision ? "choose-one-before-signoff" : "confirm",
1598
+ value: pathForDigest ?? null,
1599
+ ...(pathDecision ? { question: pathDecision.question, options: pathDecision.options } : {}),
1600
+ },
1601
+ {
1602
+ id: "experience",
1603
+ label: "EI experience",
1604
+ status: experience?.selected ? "confirm-or-switch" : "not-provided",
1605
+ value: experience?.selected ?? null,
1606
+ alternatives: experience?.alternatives ?? [],
1607
+ },
1608
+ {
1609
+ id: "surfaces",
1610
+ label: "Journey surfaces",
1611
+ status: "confirm-in-scope",
1612
+ value: stages,
1613
+ },
1614
+ {
1615
+ id: "design",
1616
+ label: "Design source",
1617
+ status: design ? "confirm-contract" : "no-accepted-design-contract",
1618
+ value: design,
1619
+ },
1620
+ {
1621
+ id: "runtime_readiness",
1622
+ label: "Runtime readiness",
1623
+ status: "review-and-ask-runtime-proof-consent",
1624
+ value: runtimeReadiness,
1625
+ },
1626
+ ...(omitted.length
1627
+ ? [{
1628
+ id: "omitted_surfaces",
1629
+ label: "Trimmed surfaces",
1630
+ status: "confirm-out-of-scope",
1631
+ value: omitted.map((item) => ({ id: item.id, outcome: item.outcome, reason: item.reason })),
1632
+ }]
1633
+ : []),
1634
+ ];
1635
+ const unattendedBlueprintStop = (pathDecision) => ({
1636
+ status: "blocked",
1637
+ mode: "handoff-required",
1638
+ reason: "Blueprint sign-off is an alignment decision about what to build.",
1639
+ instruction: "If the user is not available, stop and return the blueprint path, approvalChecklist, and required answers. Do not choose the solution path, sign the digest, initialize a surface, or record completion on the user's behalf.",
1640
+ allowedActions: [
1641
+ "open or summarize sp-vise/flow-blueprint.html",
1642
+ "collect an explicit solution_path choice when required",
1643
+ "collect explicit approval or requested trims/switches",
1644
+ ],
1645
+ resumeWhen: [
1646
+ ...(pathDecision ? ["the user chooses solution_path=sdk|uikit|hybrid"] : []),
1647
+ "the user approves the experience and journey, or asks for a trim/switch",
1648
+ "the command is re-run with the required --answer values",
1649
+ ],
1650
+ });
1651
+ if (pathNeedsDecision) {
1652
+ const decision = plan.solutionPath?.decision;
1653
+ const pathDecision = {
1654
+ question: decision?.question ?? "Should this build use social.plus UIKit components, a direct SDK implementation, or a hybrid path?",
1655
+ options: decision?.options ?? [
1656
+ "uikit: use social.plus UIKit for standard social surfaces and theme/customize it",
1657
+ "sdk: build the experience directly with the social.plus SDK",
1658
+ "hybrid: use UIKit for standard surfaces and SDK/custom code for differentiated app-layer behavior",
1659
+ ],
1660
+ };
1661
+ const fsPath = await writeFlowBlueprintHtml(repoRoot, renderFlowBlueprint({
1662
+ request,
1663
+ path: null,
1664
+ stages,
1665
+ design,
1666
+ digest,
1667
+ omitted,
1668
+ experience: experience ?? undefined,
1669
+ pathDecision,
1670
+ runtimeReadiness,
1671
+ }));
1672
+ await writeFlowState(repoRoot, persist(false));
1673
+ return {
1674
+ status: "needs-blueprint-confirmation",
1675
+ request,
1676
+ blueprintDigest: digest,
1677
+ blueprintPath: blueprintRel(fsPath),
1678
+ blueprint: { path: null, stages },
1679
+ pathDecision: { ...pathDecision, required: true },
1680
+ approvalChecklist: approvalChecklist(pathDecision),
1681
+ unattended: unattendedBlueprintStop(pathDecision),
1682
+ runtimeReadiness,
1683
+ ...(experience ? { experience } : {}),
1684
+ ...(omitted.length ? { omitted: omitted.map((item) => ({ id: item.id, outcome: item.outcome, reason: item.reason })) } : {}),
1685
+ nextStep: `A multi-surface build blueprint was produced, but the solution path is undecided. Open ${blueprintRel(fsPath)} and review the journey, scope, design, and runtime readiness with the customer, and present the path options (${pathDecision.options.map((o) => o.split(":")[0]).join(" / ")}). ` +
1686
+ `First re-run with --answer solution_path=<sdk|uikit|hybrid>, then sign off the resulting blueprint. ` +
1687
+ (experience?.selected ? `The recommended experience is "${experience.selected.title}"${experience.alternatives.length ? ` (alternatives: ${experience.alternatives.map((a) => a.title).join(", ")} — switch with \`vise creative accept . --variant <id> --rationale "<why>" --confidence high\`)` : ""}. ` : "") +
1688
+ "Surfaces cannot be started or recorded until the blueprint is signed off; prepare identity, natural target discovery, tenant data, and Console prerequisites before feeding the workplan to a dogfood agent. Ask the user before running runtime proof because browser/simulator/emulator proof can consume local resources and agent tokens.",
1689
+ };
1690
+ }
1691
+ if (confirmedByAnswer) {
1692
+ await writeFlowState(repoRoot, persist(true));
1693
+ return null;
1694
+ }
1695
+ const fsPath = await writeFlowBlueprintHtml(repoRoot, renderFlowBlueprint({
1696
+ request,
1697
+ path: pathForDigest,
1698
+ stages,
1699
+ design,
1700
+ digest,
1701
+ omitted,
1702
+ experience: experience ?? undefined,
1703
+ runtimeReadiness,
1704
+ }));
1705
+ await writeFlowState(repoRoot, persist(false));
1706
+ return {
1707
+ status: "needs-blueprint-confirmation",
1708
+ request,
1709
+ blueprintDigest: digest,
1710
+ blueprintPath: blueprintRel(fsPath),
1711
+ blueprint: { path: pathForDigest, stages },
1712
+ approvalChecklist: approvalChecklist(),
1713
+ unattended: unattendedBlueprintStop(),
1714
+ runtimeReadiness,
1715
+ ...(experience ? { experience } : {}),
1716
+ ...(omitted.length
1717
+ ? { omitted: omitted.map((item) => ({ id: item.id, outcome: item.outcome, reason: item.reason })) }
1718
+ : {}),
1719
+ nextStep: `A multi-surface build blueprint was produced. Open ${blueprintRel(fsPath)} and review the path, journey, scope, design, and runtime readiness with the customer. ` +
1720
+ (experience?.selected && experience.alternatives.length
1721
+ ? `The recommended experience is "${experience.selected.title}" (alternatives: ${experience.alternatives.map((a) => a.title).join(", ")} — to build a different one, re-run \`vise creative accept . --variant <id> --rationale "<why>" --confidence high\` and re-confirm). `
1722
+ : "") +
1723
+ (omitted.length
1724
+ ? `This blueprint has ${omitted.length} deliberately trimmed surface(s): ${omitted.map((item) => `${item.id} (${item.reason})`).join("; ")}. Confirm the human approved dropping them. `
1725
+ : "") +
1726
+ `After they approve, re-run this command with --answer blueprint_confirmation=${digest}. ` +
1727
+ "Surfaces cannot be started or recorded until the blueprint is signed off; prepare identity, natural target discovery, tenant data, and Console prerequisites before feeding the workplan to a dogfood agent. Ask the user before running runtime proof because browser/simulator/emulator proof can consume local resources and agent tokens.",
1728
+ };
1729
+ }
1730
+ async function blueprintExperience(repoRoot) {
1731
+ const dir = sidecarPath(repoRoot);
1732
+ let candidates = [];
1733
+ try {
1734
+ const brief = JSON.parse(await readFile(path.join(dir, "creative-brief.json"), "utf8"));
1735
+ candidates = Array.isArray(brief.candidateSolutions) ? brief.candidateSolutions : [];
1736
+ }
1737
+ catch {
1738
+ return null;
1739
+ }
1740
+ if (candidates.length === 0)
1741
+ return null;
1742
+ let selected = null;
1743
+ try {
1744
+ const sel = JSON.parse(await readFile(path.join(dir, "creative-selection.json"), "utf8"));
1745
+ const id = sel.selectedVariant?.id;
1746
+ if (typeof id === "string") {
1747
+ selected = { id, title: typeof sel.selectedVariant?.title === "string" ? sel.selectedVariant.title : id };
1748
+ }
1749
+ }
1750
+ catch {
1751
+ }
1752
+ const alternatives = candidates
1753
+ .filter((c) => c != null && typeof c.id === "string" && c.id !== selected?.id)
1754
+ .slice(0, 4)
1755
+ .map((c) => ({ id: c.id, title: typeof c.title === "string" ? c.title : c.id }));
1756
+ if (!selected && alternatives.length === 0)
1757
+ return null;
1758
+ return { selected, alternatives };
1759
+ }
1368
1760
  async function workplanStatus(args) {
1369
1761
  const repoRoot = path.resolve(positionalRepoPath(args));
1370
1762
  const request = requiredFlagValue(args, "request", "workplan next/status requires --request.");
1371
1763
  const plan = await workplanPlan(repoRoot, request, args);
1372
- const sequence = socialWorkplanSequence(plan);
1764
+ const { sequence, omitted } = await activeBlueprint(repoRoot, request, plan);
1373
1765
  const progress = await readWorkplanProgress(repoRoot);
1374
1766
  const completed = progress?.request === request ? progress.completed : [];
1375
1767
  const completedIds = new Set(completed.map((item) => item.surface));
@@ -1382,12 +1774,19 @@ async function workplanStatus(args) {
1382
1774
  progressPath: workplanProgressPath(repoRoot),
1383
1775
  };
1384
1776
  }
1777
+ const blueprintGate = await requireBlueprintConfirmation(repoRoot, request, plan, sequence, args, new Date().toISOString(), omitted);
1778
+ if (blueprintGate) {
1779
+ return blueprintGate;
1780
+ }
1781
+ const runtimeReadiness = runtimeReadinessForStages(blueprintStagesForSequence(sequence));
1782
+ const nextSurfaceReadiness = nextSurface ? runtimeReadiness.surfaces.find((surface) => surface.id === nextSurface.id) : undefined;
1385
1783
  return {
1386
1784
  status: nextSurface ? "next-surface" : "complete",
1387
1785
  request,
1388
1786
  progressPath: workplanProgressPath(repoRoot),
1389
1787
  creativeContext: plan.socialWorkplan?.creativeContext ?? plan.creativeContext,
1390
1788
  uxHarness: plan.socialWorkplan?.uxHarness ?? plan.uxHarness,
1789
+ runtimeReadiness,
1391
1790
  completed,
1392
1791
  sequence: sequence.map((surface) => ({
1393
1792
  id: surface.id,
@@ -1396,6 +1795,9 @@ async function workplanStatus(args) {
1396
1795
  label: surface.label,
1397
1796
  completed: completedIds.has(surface.id),
1398
1797
  })),
1798
+ ...(omitted.length
1799
+ ? { omitted: omitted.map((item) => ({ id: item.id, outcome: item.outcome, reason: item.reason })) }
1800
+ : {}),
1399
1801
  nextSurface: nextSurface
1400
1802
  ? {
1401
1803
  id: nextSurface.id,
@@ -1405,9 +1807,12 @@ async function workplanStatus(args) {
1405
1807
  intake: nextSurface.intake,
1406
1808
  validation: nextSurface.validation,
1407
1809
  uxHarness: nextSurface.uxHarness,
1810
+ runtimeReadiness: nextSurfaceReadiness,
1408
1811
  commands: {
1409
1812
  plan: nextSurface.planCommand,
1410
- init: nextSurface.initCommand,
1813
+ init: plan.designReview?.status === "needs-confirmation"
1814
+ ? `${nextSurface.initCommand} --answer design_contract_confirmation=yes`
1815
+ : nextSurface.initCommand,
1411
1816
  check: "vise check .",
1412
1817
  sync: "vise sync .",
1413
1818
  validate: "vise validate .",
@@ -1417,23 +1822,101 @@ async function workplanStatus(args) {
1417
1822
  }
1418
1823
  : undefined,
1419
1824
  nextStep: nextSurface
1420
- ? `Resolve the ${nextSurface.id} surface intake, run its focused plan/init command, implement it, then run check/sync/validate/sensors before marking it complete.`
1825
+ ? `Resolve the ${nextSurface.id} surface intake, confirm runtime readiness and natural target discovery for that next pending surface, run only its focused plan/init command, implement it, then run check/sync/validate/sensors and workplan complete before moving to any later surface. Before runtime proof, ask the user whether to spend browser/simulator/emulator resources; if they decline, record the expected unproven runtime risks instead of claiming product proof.`
1421
1826
  : "All workplan surfaces are marked complete. Run one final `vise check .`, `vise validate .`, and `vise run-sensors .` before handoff.",
1422
1827
  };
1423
1828
  }
1829
+ async function trimWorkplanSurface(args) {
1830
+ const repoRoot = path.resolve(positionalRepoPath(args));
1831
+ const request = requiredFlagValue(args, "request", "workplan trim requires --request.");
1832
+ const surfaceId = requiredFlagValue(args, "surface", "workplan trim requires --surface <surface-id>.");
1833
+ const reason = (flagValue(args, "reason") ?? "").trim();
1834
+ if (!reason) {
1835
+ throw new Error('workplan trim requires --reason "<why this companion is out of scope>". An omission without a recorded reason is invalid.');
1836
+ }
1837
+ const plan = await workplanPlan(repoRoot, request, args);
1838
+ const full = socialWorkplanSequence(plan);
1839
+ if (full.length < 2) {
1840
+ throw new Error("This request did not produce a multi-surface blueprint, so there is nothing to trim. Use `vise plan` for the single-outcome flow.");
1841
+ }
1842
+ const surface = full.find((item) => item.id === surfaceId);
1843
+ if (!surface) {
1844
+ throw new Error(`Surface "${surfaceId}" is not in this blueprint. Trimmable surfaces: ${full.map((item) => item.id).join(", ")}.`);
1845
+ }
1846
+ const existingOmitted = await readOmittedSurfaces(repoRoot, request);
1847
+ if (existingOmitted.some((item) => item.id === surfaceId)) {
1848
+ return {
1849
+ payload: {
1850
+ status: "already-trimmed",
1851
+ request,
1852
+ surface: surfaceId,
1853
+ omitted: existingOmitted.map((item) => ({ id: item.id, outcome: item.outcome, reason: item.reason })),
1854
+ nextStep: `Surface "${surfaceId}" is already trimmed; no change. Run \`vise workplan status\` to see the active blueprint.`,
1855
+ },
1856
+ exitCode: 0,
1857
+ };
1858
+ }
1859
+ const progress = await readWorkplanProgress(repoRoot);
1860
+ const completedIds = new Set(progress?.request === request ? progress.completed.map((item) => item.surface) : []);
1861
+ if (completedIds.has(surfaceId)) {
1862
+ throw new Error(`Surface "${surfaceId}" has already been built and recorded; it cannot be trimmed. Trim applies only to not-yet-built companion surfaces.`);
1863
+ }
1864
+ const newOmitted = [
1865
+ ...existingOmitted,
1866
+ { id: surface.id, outcome: surface.outcome, ...(surface.label ? { label: surface.label } : {}), reason, omitted_at: new Date().toISOString() },
1867
+ ];
1868
+ const newOmittedIds = new Set(newOmitted.map((item) => item.id));
1869
+ const activeAfter = full.filter((item) => !newOmittedIds.has(item.id));
1870
+ if (activeAfter.length < 2) {
1871
+ throw new Error(`Trimming "${surfaceId}" would leave ${activeAfter.length} active surface(s). The alignment gate covers multi-surface journeys; at least 2 must remain. To build a single surface, re-select that experience via \`vise creative\` instead of trimming a journey down to one.`);
1872
+ }
1873
+ const gateArgs = stripAnswerKey(args, "blueprint_confirmation");
1874
+ const gate = await requireBlueprintConfirmation(repoRoot, request, plan, activeAfter, gateArgs, new Date().toISOString(), newOmitted);
1875
+ if (!gate) {
1876
+ return {
1877
+ payload: {
1878
+ status: "trimmed",
1879
+ request,
1880
+ surface: surfaceId,
1881
+ reason,
1882
+ note: "Omission recorded; the blueprint is already in a confirmed state for the resulting digest.",
1883
+ omitted: newOmitted.map((item) => ({ id: item.id, outcome: item.outcome, reason: item.reason })),
1884
+ },
1885
+ exitCode: 0,
1886
+ };
1887
+ }
1888
+ return {
1889
+ payload: {
1890
+ status: "trimmed",
1891
+ request,
1892
+ trimmed: { id: surface.id, outcome: surface.outcome, reason },
1893
+ ...gate,
1894
+ nextStep: `Recorded the deliberate omission of "${surfaceId}" (${reason}). The blueprint changed and must be re-signed before any surface proceeds. ` +
1895
+ (typeof gate.nextStep === "string" ? gate.nextStep : ""),
1896
+ },
1897
+ exitCode: 7,
1898
+ };
1899
+ }
1424
1900
  async function completeWorkplanSurface(args) {
1425
1901
  const repoRoot = path.resolve(positionalRepoPath(args));
1426
1902
  const request = requiredFlagValue(args, "request", "workplan complete requires --request.");
1427
1903
  const surfaceId = requiredFlagValue(args, "surface", "workplan complete requires --surface <surface-id>.");
1428
1904
  const note = flagValue(args, "note");
1429
1905
  const plan = await workplanPlan(repoRoot, request, args);
1430
- const sequence = socialWorkplanSequence(plan);
1906
+ const { sequence, omitted } = await activeBlueprint(repoRoot, request, plan);
1431
1907
  const surface = sequence.find((item) => item.id === surfaceId);
1432
1908
  if (!surface) {
1909
+ if (omitted.some((item) => item.id === surfaceId)) {
1910
+ throw new Error(`Surface "${surfaceId}" was deliberately trimmed from this blueprint and is not built (recorded omission). Un-trim is not yet supported; to build it, re-select an experience that includes it via \`vise creative\`.`);
1911
+ }
1433
1912
  throw new Error(`Surface "${surfaceId}" is not in this social workplan. Available surfaces: ${sequence.map((item) => item.id).join(", ") || "(none)"}.`);
1434
1913
  }
1435
1914
  const now = new Date().toISOString();
1436
- const check = await greenWorkplanCheck(repoRoot, surfaceId);
1915
+ const blueprintGate = await requireBlueprintConfirmation(repoRoot, request, plan, sequence, args, now, omitted);
1916
+ if (blueprintGate) {
1917
+ return blueprintGate;
1918
+ }
1919
+ const check = await greenWorkplanCheck(repoRoot, surfaceId, surface.outcome);
1437
1920
  const snapshot = await writeWorkplanSurfaceSnapshot(repoRoot, surfaceId, check, now);
1438
1921
  const existing = await readWorkplanProgress(repoRoot);
1439
1922
  const base = existing?.request === request
@@ -1475,15 +1958,45 @@ async function completeWorkplanSurface(args) {
1475
1958
  };
1476
1959
  await writeWorkplanProgress(repoRoot, progress);
1477
1960
  const status = await workplanStatus(args);
1961
+ const sequenceStatus = workplanSequenceStatus(status);
1962
+ const completedSurfaces = sequenceStatus.filter((item) => item.completed);
1963
+ const pendingSurfaces = sequenceStatus.filter((item) => !item.completed);
1964
+ const finalHandoffAllowed = pendingSurfaces.length === 0;
1965
+ const nextSurface = status.nextSurface;
1966
+ const pendingSummary = pendingSurfaces.map((item) => item.id).join(", ");
1478
1967
  return {
1479
1968
  status: "recorded",
1480
1969
  progressPath: workplanProgressPath(repoRoot),
1481
1970
  recorded: progress.completed.find((item) => item.surface === surfaceId),
1482
- nextSurface: status.nextSurface,
1483
- nextStep: status.nextStep,
1971
+ completedSurfaces,
1972
+ pendingSurfaces,
1973
+ workplanComplete: finalHandoffAllowed,
1974
+ finalHandoffAllowed,
1975
+ ...(pendingSurfaces.length
1976
+ ? {
1977
+ blockedHandoffReason: `The broad social request is not complete. Pending surfaces remain: ${pendingSummary}.`,
1978
+ }
1979
+ : {}),
1980
+ nextSurface,
1981
+ nextStep: pendingSurfaces.length
1982
+ ? `Recorded "${surfaceId}" only. Do not hand off the broad request as complete: pending surfaces remain (${pendingSummary}). Continue with the next pending surface using the focused init/check/complete commands from \`nextSurface.commands\`, then rerun \`vise workplan status\` until \`workplanComplete\` is true.`
1983
+ : status.nextStep,
1484
1984
  };
1485
1985
  }
1486
- async function greenWorkplanCheck(repoRoot, surfaceId) {
1986
+ function workplanSequenceStatus(status) {
1987
+ const sequence = Array.isArray(status.sequence) ? status.sequence : [];
1988
+ return sequence
1989
+ .filter((item) => item != null && typeof item === "object")
1990
+ .map((item) => ({
1991
+ id: typeof item.id === "string" ? item.id : "",
1992
+ outcome: typeof item.outcome === "string" ? item.outcome : undefined,
1993
+ label: typeof item.label === "string" ? item.label : undefined,
1994
+ order: typeof item.order === "number" ? item.order : undefined,
1995
+ completed: item.completed === true,
1996
+ }))
1997
+ .filter((item) => item.id.length > 0);
1998
+ }
1999
+ async function greenWorkplanCheck(repoRoot, surfaceId, expectedOutcome) {
1487
2000
  let check;
1488
2001
  try {
1489
2002
  check = await checkCompliance(repoRoot);
@@ -1492,6 +2005,9 @@ async function greenWorkplanCheck(repoRoot, surfaceId) {
1492
2005
  const message = error instanceof Error ? error.message : String(error);
1493
2006
  throw new Error(`Cannot mark "${surfaceId}" complete because the current compliance check could not run. Run the focused \`vise init\` command for this surface first, then \`vise check .\`. ${message}`);
1494
2007
  }
2008
+ if (expectedOutcome && check.outcome !== expectedOutcome) {
2009
+ throw new Error(`Cannot mark "${surfaceId}" complete: the sp-vise sidecar is currently scoped to outcome "${check.outcome}", not this surface's outcome "${expectedOutcome}". Each workplan surface has its own sidecar scope — run this surface's focused \`vise init\` command (it sets \`--answer feature_surface=${surfaceId}\`) and \`vise check .\` before recording it. (The check reported "${check.status}" only because it ran against the other surface's contract.)`);
2010
+ }
1495
2011
  if (check.status !== "green") {
1496
2012
  throw new Error(`Cannot mark "${surfaceId}" complete because \`vise check\` returned "${check.status}" (exit ${check.exitCode}). Resolve the check result before recording workplan progress.`);
1497
2013
  }
@@ -1541,6 +2057,86 @@ function socialWorkplanSequence(plan) {
1541
2057
  }
1542
2058
  return workplan.sequence;
1543
2059
  }
2060
+ async function requireNextWorkplanSurfaceInit(repoRoot, request, sequence, answers) {
2061
+ const progress = await readWorkplanProgress(repoRoot);
2062
+ const completedIds = new Set(progress?.request === request ? progress.completed.map((item) => item.surface) : []);
2063
+ const nextSurface = sequence.find((surface) => !completedIds.has(surface.id));
2064
+ const selectedSurface = resolveWorkplanSurfaceAnswer(sequence, answers.feature_surface);
2065
+ if (!nextSurface) {
2066
+ return {
2067
+ status: "workplan-complete",
2068
+ exitCode: 7,
2069
+ request,
2070
+ sequence: sequence.map((surface) => ({
2071
+ id: surface.id,
2072
+ order: surface.order,
2073
+ outcome: surface.outcome,
2074
+ label: surface.label,
2075
+ completed: completedIds.has(surface.id),
2076
+ })),
2077
+ nextStep: "All active blueprint surfaces are already recorded complete. Run one final `vise check .`, `vise validate .`, and `vise run-sensors .` instead of initializing another surface.",
2078
+ };
2079
+ }
2080
+ if (!answers.feature_surface || !selectedSurface) {
2081
+ return {
2082
+ status: "needs-workplan-surface",
2083
+ exitCode: 7,
2084
+ request,
2085
+ expectedSurface: {
2086
+ id: nextSurface.id,
2087
+ order: nextSurface.order,
2088
+ outcome: nextSurface.outcome,
2089
+ label: nextSurface.label,
2090
+ },
2091
+ sequence: sequence.map((surface) => ({
2092
+ id: surface.id,
2093
+ order: surface.order,
2094
+ outcome: surface.outcome,
2095
+ label: surface.label,
2096
+ completed: completedIds.has(surface.id),
2097
+ })),
2098
+ nextStep: `This request has a signed multi-surface blueprint. Initialize the next pending surface with: ${nextSurface.initCommand}. ` +
2099
+ "After implementing it and getting `vise check .` green, run the matching `vise workplan complete` command before moving to the next surface.",
2100
+ };
2101
+ }
2102
+ if (selectedSurface.id !== nextSurface.id) {
2103
+ return {
2104
+ status: "needs-workplan-order",
2105
+ exitCode: 7,
2106
+ request,
2107
+ attemptedSurface: {
2108
+ id: selectedSurface.id,
2109
+ order: selectedSurface.order,
2110
+ outcome: selectedSurface.outcome,
2111
+ label: selectedSurface.label,
2112
+ },
2113
+ expectedSurface: {
2114
+ id: nextSurface.id,
2115
+ order: nextSurface.order,
2116
+ outcome: nextSurface.outcome,
2117
+ label: nextSurface.label,
2118
+ },
2119
+ sequence: sequence.map((surface) => ({
2120
+ id: surface.id,
2121
+ order: surface.order,
2122
+ outcome: surface.outcome,
2123
+ label: surface.label,
2124
+ completed: completedIds.has(surface.id),
2125
+ })),
2126
+ nextStep: `Do not jump to "${selectedSurface.id}" before "${nextSurface.id}" is complete. Run: ${nextSurface.initCommand}. ` +
2127
+ "Each active blueprint surface must be implemented, checked, synced, and recorded in order so the final app does not collapse to one narrow social surface.",
2128
+ };
2129
+ }
2130
+ return null;
2131
+ }
2132
+ function resolveWorkplanSurfaceAnswer(sequence, answer) {
2133
+ const normalized = (answer ?? "").trim().toLowerCase();
2134
+ if (!normalized)
2135
+ return undefined;
2136
+ return sequence.find((surface) => surface.id.toLowerCase() === normalized)
2137
+ ?? sequence.find((surface) => surface.label.toLowerCase() === normalized)
2138
+ ?? sequence.find((surface) => surface.label.toLowerCase().includes(normalized));
2139
+ }
1544
2140
  async function readWorkplanProgress(repoRoot) {
1545
2141
  try {
1546
2142
  return JSON.parse(await readFile(workplanProgressPath(repoRoot), "utf8"));
@@ -1564,9 +2160,6 @@ function workplanSurfaceSnapshotDir(repoRoot, surfaceId) {
1564
2160
  }
1565
2161
  return path.join(sidecarPath(repoRoot), "workplan-snapshots", safeSurfaceId);
1566
2162
  }
1567
- function sidecarPath(repoRoot) {
1568
- return path.join(repoRoot, "sp-vise");
1569
- }
1570
2163
  function repoRelativePath(repoRoot, filePath) {
1571
2164
  return path.relative(repoRoot, filePath).split(path.sep).join("/");
1572
2165
  }
@@ -1584,7 +2177,14 @@ function ciCheckResult(result) {
1584
2177
  enabled: true,
1585
2178
  passed: result.exitCode === 0,
1586
2179
  exitCode: result.exitCode,
1587
- blockingResultStatuses: ["contract-drift", "blocked", "deterministic-failures", "needs-attestation"],
2180
+ blockingResultStatuses: [
2181
+ "contract-drift",
2182
+ "blocked",
2183
+ "deterministic-failures",
2184
+ "needs-attestation",
2185
+ "completeness-gap",
2186
+ "selected-capability-failures",
2187
+ ],
1588
2188
  message: result.exitCode === 0 ? "Compliance green for CI." : `Compliance failed for CI with status: ${result.status}.`,
1589
2189
  },
1590
2190
  };
@@ -1594,7 +2194,7 @@ function positionalRepoPath(args) {
1594
2194
  return values[0] ?? ".";
1595
2195
  }
1596
2196
  function positionalValues(args) {
1597
- const flagsWithValues = new Set(["request", "requirements", "prototype", "variant", "variant-id", "brief", "brief-path", "surface", "surface-path", "platform", "capability", "surface-dir", "format", "include", "timeout-ms", "query", "path", "limit", "answer", "target", "dest", "destination", "rule", "confidence", "signer", "identity", "evidence-file", "rationale", "repo", "reference", "registry", "block", "package-source", "note", "kind", "sentiment", "metric"]);
2197
+ const flagsWithValues = new Set(["request", "requirements", "prototype", "variant", "variant-id", "brief", "brief-path", "surface", "surface-path", "platform", "capability", "surface-dir", "format", "include", "timeout-ms", "query", "path", "limit", "answer", "target", "dest", "destination", "rule", "confidence", "signer", "identity", "evidence-file", "rationale", "repo", "reference", "registry", "block", "package-source", "note", "kind", "sentiment", "metric", "tier", "customer-id", "scope", "target-completion", "reviewer-name", "reviewer-email"]);
1598
2198
  const values = [];
1599
2199
  for (let index = 0; index < args.length; index += 1) {
1600
2200
  const arg = args[index];
@@ -1720,6 +2320,22 @@ function hasFlag(args, name) {
1720
2320
  }
1721
2321
  return false;
1722
2322
  }
2323
+ function stripAnswerKey(args, key) {
2324
+ const out = [];
2325
+ const valuePrefix = `${key}=`;
2326
+ for (let index = 0; index < args.length; index += 1) {
2327
+ const arg = args[index];
2328
+ if (arg === "--answer" && typeof args[index + 1] === "string" && args[index + 1].startsWith(valuePrefix)) {
2329
+ index += 1;
2330
+ continue;
2331
+ }
2332
+ if (arg?.startsWith("--answer=") && arg.slice("--answer=".length).startsWith(valuePrefix)) {
2333
+ continue;
2334
+ }
2335
+ out.push(arg);
2336
+ }
2337
+ return out;
2338
+ }
1723
2339
  function keyValueFlag(args, name) {
1724
2340
  const pairs = flagValues(args, name);
1725
2341
  const result = {};
@@ -1767,6 +2383,8 @@ function doctorResult() {
1767
2383
  return {
1768
2384
  package: packageName,
1769
2385
  version: packageVersion,
2386
+ buildSource,
2387
+ packageRoot,
1770
2388
  status: nodeMajor >= 18 ? "ok" : "unsupported-node",
1771
2389
  node: process.versions.node,
1772
2390
  requiredNodeMajor: ">=18",
@@ -1775,5 +2393,6 @@ function doctorResult() {
1775
2393
  docsBaseUrl: process.env.SOCIAL_PLUS_DOCS_BASE_URL ?? "https://learn.social.plus",
1776
2394
  transport: "stdio",
1777
2395
  tools: Array.from(tools.keys()),
2396
+ support: SUPPORT_URL,
1778
2397
  };
1779
2398
  }