@amityco/social-plus-vise 1.3.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +51 -2
  2. package/README.md +36 -19
  3. package/dist/capabilities.js +29 -1
  4. package/dist/entryState.js +71 -0
  5. package/dist/experience.js +70 -0
  6. package/dist/explore.js +1 -1
  7. package/dist/flow.js +382 -0
  8. package/dist/humanFormat.js +25 -0
  9. package/dist/intake.js +117 -0
  10. package/dist/intelligence/placement.js +2 -1
  11. package/dist/outcomes.js +126 -28
  12. package/dist/productExpectations.js +15 -0
  13. package/dist/requestReadiness.js +99 -0
  14. package/dist/server.js +566 -19
  15. package/dist/sidecar.js +5 -0
  16. package/dist/solutionPath.js +1 -1
  17. package/dist/tools/compliance.js +752 -125
  18. package/dist/tools/creative.js +14 -12
  19. package/dist/tools/design.js +321 -11
  20. package/dist/tools/experienceCompiler.js +1 -1
  21. package/dist/tools/experienceSensors.js +1 -1
  22. package/dist/tools/harness.js +13 -0
  23. package/dist/tools/integration.js +263 -90
  24. package/dist/tools/learning.js +1 -3
  25. package/dist/tools/project.js +963 -66
  26. package/dist/tools/smoke.js +134 -0
  27. package/dist/tools/uxHarness.js +54 -6
  28. package/dist/uikitCustomization.js +3 -1
  29. package/dist/version.js +7 -3
  30. package/package.json +1 -1
  31. package/packages/intelligence/catalog/catalog.schema.json +1 -1
  32. package/packages/intelligence/catalog/experience-objects.json +2 -2
  33. package/packages/intelligence/catalog/variants.json +24 -0
  34. package/rules/event.yaml +3 -0
  35. package/rules/feed.yaml +251 -0
  36. package/rules/invitation.yaml +4 -0
  37. package/rules/live-data.yaml +110 -0
  38. package/rules/notification-tray.yaml +4 -0
  39. package/rules/poll.yaml +5 -0
  40. package/rules/sdk-lifecycle.yaml +559 -0
  41. package/rules/search.yaml +5 -0
  42. package/rules/security.yaml +12 -0
  43. package/rules/story.yaml +5 -0
  44. package/rules/user-blocking.yaml +5 -0
  45. 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
12
  import { attestRule, attestRuleTool, checkCompliance, checkComplianceTool, explainRule, explainRuleTool, experienceReportTool, initCompliance, initComplianceTool, recordBaseline, initEngagement, initEngagementTool, listRules, showEngagement, showEngagementTool, statusCompliance, syncCompliance, syncComplianceTool, } from "./tools/compliance.js";
10
- import { designCheckTool, designExtractTool, designInitTokensTool, designPreviewTool, designReferenceTool } from "./tools/design.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,6 +20,7 @@ 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";
@@ -24,7 +28,7 @@ import { creativeAcceptTool, creativeBriefTool } from "./tools/creative.js";
24
28
  import { uxHarnessTool } from "./tools/uxHarness.js";
25
29
  import { exploreRequest } from "./explore.js";
26
30
  import { renderHuman, wantsHumanFormat } from "./humanFormat.js";
27
- import { packageName, packageVersion } from "./version.js";
31
+ import { buildSource, packageName, packageRoot, packageVersion } from "./version.js";
28
32
  const SUPPORT_URL = "mailto:support@social.plus";
29
33
  const tools = new Map([
30
34
  searchDocsTool,
@@ -49,11 +53,13 @@ const tools = new Map([
49
53
  showEngagementTool,
50
54
  resolveRequestTool,
51
55
  runSensorsTool,
56
+ smokeTool,
52
57
  validateSetupTool,
53
58
  suggestPatchTool,
54
59
  debugIssueTool,
55
60
  designExtractTool,
56
61
  designCheckTool,
62
+ designContrastTool,
57
63
  designPreviewTool,
58
64
  designReferenceTool,
59
65
  designInitTokensTool,
@@ -316,7 +322,14 @@ async function handleCli(args) {
316
322
  console.log(JSON.stringify(await completeWorkplanSurface(subArgs), null, 2));
317
323
  return "exit";
318
324
  }
319
- 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".`);
320
333
  process.exitCode = 1;
321
334
  return "exit";
322
335
  }
@@ -343,6 +356,21 @@ async function handleCli(args) {
343
356
  }
344
357
  return "exit";
345
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
+ }
346
374
  if (command === "resolve") {
347
375
  await printToolResult(resolveRequestTool, {
348
376
  repoPath: positionalRepoPath(args.slice(1)),
@@ -418,7 +446,24 @@ async function handleCli(args) {
418
446
  if (command === "init") {
419
447
  assertOnlyKnownFlags(args, ["request", "surface", "surface-path", "answer", "allow-unresolved-intake", "baseline"], "init");
420
448
  const initRepo = positionalRepoPath(args.slice(1));
421
- const result = await initCompliance(initRepo, requiredFlagValue(args, "request", "init requires --request."), flagValue(args, "surface") ?? flagValue(args, "surface-path"), keyValueFlag(args, "answer"), { allowUnresolvedIntake: hasFlag(args, "allow-unresolved-intake") });
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") });
422
467
  if (result.status === "needs-clarification" && typeof result.exitCode === "number") {
423
468
  process.exitCode = result.exitCode;
424
469
  }
@@ -532,6 +577,17 @@ async function handleCli(args) {
532
577
  await printToolResult(designCheckTool, { repoPath: positionalRepoPath(subArgs) });
533
578
  return "exit";
534
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
+ }
535
591
  if (sub === "preview") {
536
592
  assertOnlyKnownFlags(subArgs, ["reference", "no-write"], "design preview");
537
593
  await printToolResult(designPreviewTool, {
@@ -558,7 +614,7 @@ async function handleCli(args) {
558
614
  });
559
615
  return "exit";
560
616
  }
561
- 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".`);
562
618
  process.exitCode = 1;
563
619
  return "exit";
564
620
  }
@@ -632,8 +688,8 @@ Usage:
632
688
  vise creative [repoPath] --request "Add engagement" --prototype ./prototype.html
633
689
  vise creative [repoPath] --request "Add engagement" --ranking-preview
634
690
  vise creative [repoPath] --request "Add engagement" --no-write
635
- vise creative accept [repoPath] --variant community-first
636
- 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
637
693
 
638
694
  Output:
639
695
  Writes sp-vise/creative-brief.json and sp-vise/creative-brief.md unless --no-write is set.
@@ -717,12 +773,17 @@ Usage:
717
773
  vise workplan next [repoPath] --request "Add feed, comments, chat, and profile"
718
774
  vise workplan status [repoPath] --request "Add feed, comments, chat, and profile"
719
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"
720
777
 
721
778
  Notes:
722
779
  next/status re-run the broad plan, ignore any feature_surface answer, and combine it
723
780
  with sp-vise/workplan.json progress. complete records a host-agent progress marker;
724
781
  run vise check, vise sync, vise validate, and vise run-sensors before marking a
725
- 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.`;
726
787
  }
727
788
  if (command === "search-docs" || command === "search_docs") {
728
789
  return `${packageName} search-docs
@@ -811,6 +872,23 @@ Run detected project command sensors such as typecheck, test, build, Flutter, or
811
872
 
812
873
  Usage:
813
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.`;
814
892
  }
815
893
  if (command === "resolve") {
816
894
  return `${packageName} resolve
@@ -943,7 +1021,8 @@ extract Build a graded design contract and write it to sp-vise/design-contrac
943
1021
  With --from-project (no external prototype), derive the contract from the host
944
1022
  project's OWN design system: CSS custom properties (incl. shadcn :root and
945
1023
  Tailwind v4 @theme), TS/JS token modules, inline tailwind configs, Android
946
- 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 +
947
1026
  Swift colors. Reference values (var()/theme()/calc()) are skipped, so a
948
1027
  var-mapped config contributes nothing rather than wrong tokens.
949
1028
  check Advisory, non-blocking report on how closely the project's UI code
@@ -999,7 +1078,7 @@ Usage:
999
1078
  vise print-skill Print bundled skill markdown
1000
1079
  vise inspect [repoPath] Inspect platform and design signals
1001
1080
  vise creative [repoPath] --request "..." Create an Engagement Intelligence brief
1002
- 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
1003
1082
  vise ux-harness [repoPath] Generate UX Harness expectations from creative selection
1004
1083
  vise experience-report [repoPath] Write the advisory dimensioned Experience Report
1005
1084
  vise experience compile [repoPath] Compile selected variant into implementation artifacts
@@ -1426,11 +1505,263 @@ function assertOnlyKnownFlags(args, allowed, commandName) {
1426
1505
  const allowedList = allowed.length > 0 ? `Allowed flags: ${allowed.map((flag) => `--${flag}`).join(", ")}.` : "This command takes no flags.";
1427
1506
  throw new Error(`${commandName} does not accept ${unknown.join(", ")}. ${allowedList} Run \`vise ${commandName} --help\` for usage.`);
1428
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
+ }
1429
1760
  async function workplanStatus(args) {
1430
1761
  const repoRoot = path.resolve(positionalRepoPath(args));
1431
1762
  const request = requiredFlagValue(args, "request", "workplan next/status requires --request.");
1432
1763
  const plan = await workplanPlan(repoRoot, request, args);
1433
- const sequence = socialWorkplanSequence(plan);
1764
+ const { sequence, omitted } = await activeBlueprint(repoRoot, request, plan);
1434
1765
  const progress = await readWorkplanProgress(repoRoot);
1435
1766
  const completed = progress?.request === request ? progress.completed : [];
1436
1767
  const completedIds = new Set(completed.map((item) => item.surface));
@@ -1443,12 +1774,19 @@ async function workplanStatus(args) {
1443
1774
  progressPath: workplanProgressPath(repoRoot),
1444
1775
  };
1445
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;
1446
1783
  return {
1447
1784
  status: nextSurface ? "next-surface" : "complete",
1448
1785
  request,
1449
1786
  progressPath: workplanProgressPath(repoRoot),
1450
1787
  creativeContext: plan.socialWorkplan?.creativeContext ?? plan.creativeContext,
1451
1788
  uxHarness: plan.socialWorkplan?.uxHarness ?? plan.uxHarness,
1789
+ runtimeReadiness,
1452
1790
  completed,
1453
1791
  sequence: sequence.map((surface) => ({
1454
1792
  id: surface.id,
@@ -1457,6 +1795,9 @@ async function workplanStatus(args) {
1457
1795
  label: surface.label,
1458
1796
  completed: completedIds.has(surface.id),
1459
1797
  })),
1798
+ ...(omitted.length
1799
+ ? { omitted: omitted.map((item) => ({ id: item.id, outcome: item.outcome, reason: item.reason })) }
1800
+ : {}),
1460
1801
  nextSurface: nextSurface
1461
1802
  ? {
1462
1803
  id: nextSurface.id,
@@ -1466,9 +1807,12 @@ async function workplanStatus(args) {
1466
1807
  intake: nextSurface.intake,
1467
1808
  validation: nextSurface.validation,
1468
1809
  uxHarness: nextSurface.uxHarness,
1810
+ runtimeReadiness: nextSurfaceReadiness,
1469
1811
  commands: {
1470
1812
  plan: nextSurface.planCommand,
1471
- init: nextSurface.initCommand,
1813
+ init: plan.designReview?.status === "needs-confirmation"
1814
+ ? `${nextSurface.initCommand} --answer design_contract_confirmation=yes`
1815
+ : nextSurface.initCommand,
1472
1816
  check: "vise check .",
1473
1817
  sync: "vise sync .",
1474
1818
  validate: "vise validate .",
@@ -1478,22 +1822,100 @@ async function workplanStatus(args) {
1478
1822
  }
1479
1823
  : undefined,
1480
1824
  nextStep: nextSurface
1481
- ? `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.`
1482
1826
  : "All workplan surfaces are marked complete. Run one final `vise check .`, `vise validate .`, and `vise run-sensors .` before handoff.",
1483
1827
  };
1484
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
+ }
1485
1900
  async function completeWorkplanSurface(args) {
1486
1901
  const repoRoot = path.resolve(positionalRepoPath(args));
1487
1902
  const request = requiredFlagValue(args, "request", "workplan complete requires --request.");
1488
1903
  const surfaceId = requiredFlagValue(args, "surface", "workplan complete requires --surface <surface-id>.");
1489
1904
  const note = flagValue(args, "note");
1490
1905
  const plan = await workplanPlan(repoRoot, request, args);
1491
- const sequence = socialWorkplanSequence(plan);
1906
+ const { sequence, omitted } = await activeBlueprint(repoRoot, request, plan);
1492
1907
  const surface = sequence.find((item) => item.id === surfaceId);
1493
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
+ }
1494
1912
  throw new Error(`Surface "${surfaceId}" is not in this social workplan. Available surfaces: ${sequence.map((item) => item.id).join(", ") || "(none)"}.`);
1495
1913
  }
1496
1914
  const now = new Date().toISOString();
1915
+ const blueprintGate = await requireBlueprintConfirmation(repoRoot, request, plan, sequence, args, now, omitted);
1916
+ if (blueprintGate) {
1917
+ return blueprintGate;
1918
+ }
1497
1919
  const check = await greenWorkplanCheck(repoRoot, surfaceId, surface.outcome);
1498
1920
  const snapshot = await writeWorkplanSurfaceSnapshot(repoRoot, surfaceId, check, now);
1499
1921
  const existing = await readWorkplanProgress(repoRoot);
@@ -1536,14 +1958,44 @@ async function completeWorkplanSurface(args) {
1536
1958
  };
1537
1959
  await writeWorkplanProgress(repoRoot, progress);
1538
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(", ");
1539
1967
  return {
1540
1968
  status: "recorded",
1541
1969
  progressPath: workplanProgressPath(repoRoot),
1542
1970
  recorded: progress.completed.find((item) => item.surface === surfaceId),
1543
- nextSurface: status.nextSurface,
1544
- 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,
1545
1984
  };
1546
1985
  }
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
+ }
1547
1999
  async function greenWorkplanCheck(repoRoot, surfaceId, expectedOutcome) {
1548
2000
  let check;
1549
2001
  try {
@@ -1605,6 +2057,86 @@ function socialWorkplanSequence(plan) {
1605
2057
  }
1606
2058
  return workplan.sequence;
1607
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
+ }
1608
2140
  async function readWorkplanProgress(repoRoot) {
1609
2141
  try {
1610
2142
  return JSON.parse(await readFile(workplanProgressPath(repoRoot), "utf8"));
@@ -1628,9 +2160,6 @@ function workplanSurfaceSnapshotDir(repoRoot, surfaceId) {
1628
2160
  }
1629
2161
  return path.join(sidecarPath(repoRoot), "workplan-snapshots", safeSurfaceId);
1630
2162
  }
1631
- function sidecarPath(repoRoot) {
1632
- return path.join(repoRoot, "sp-vise");
1633
- }
1634
2163
  function repoRelativePath(repoRoot, filePath) {
1635
2164
  return path.relative(repoRoot, filePath).split(path.sep).join("/");
1636
2165
  }
@@ -1791,6 +2320,22 @@ function hasFlag(args, name) {
1791
2320
  }
1792
2321
  return false;
1793
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
+ }
1794
2339
  function keyValueFlag(args, name) {
1795
2340
  const pairs = flagValues(args, name);
1796
2341
  const result = {};
@@ -1838,6 +2383,8 @@ function doctorResult() {
1838
2383
  return {
1839
2384
  package: packageName,
1840
2385
  version: packageVersion,
2386
+ buildSource,
2387
+ packageRoot,
1841
2388
  status: nodeMajor >= 18 ? "ok" : "unsupported-node",
1842
2389
  node: process.versions.node,
1843
2390
  requiredNodeMajor: ">=18",