@amityco/social-plus-vise 1.3.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 (45) hide show
  1. package/CHANGELOG.md +45 -2
  2. package/README.md +19 -9
  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
@@ -1,10 +1,10 @@
1
1
  import { access, readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { BROAD_SOCIAL_REGEX, DESIGN_REGEX, getOutcomeDefinition, hasAnswer, planContextFor, resolveOutcome, } from "../outcomes.js";
3
+ import { BROAD_SOCIAL_REGEX, DESIGN_REGEX, getOutcomeDefinition, hasAnswer, planContextFor, platformFeatureGapsFor, resolveOutcome, } from "../outcomes.js";
4
4
  import { objectInput, optionalStringField, stringField, textResult } from "../types.js";
5
5
  import { availableOptionalCapabilityIds, capabilityChecklist, optionalCapabilityChecklist, platformCapabilityAvailability, selectedOptionalCapabilityIds, slimCapabilityAvailability, slimCapabilityItem, } from "../capabilities.js";
6
6
  import { applicableCompliancePlanRuleSummaries } from "./compliance.js";
7
- import { DESIGN_CONTRACT_CONFIRMATION_ANSWER_ID, buildDesignBrief, designContractConfirmationFromAnswers, designPreviewPath, readDesignContract, } from "./design.js";
7
+ import { DESIGN_CONTRACT_CONFIRMATION_ANSWER_ID, buildDesignBrief, designContractConfirmationFromAnswers, designPreviewPath, readDesignContract, readDesignProfile, buildDesignReviewLoop, } from "./design.js";
8
8
  import { sdkVersionGuidance } from "./sdkVersion.js";
9
9
  import { detectCommandSensors } from "./harness.js";
10
10
  import { inspectProject } from "./project.js";
@@ -12,6 +12,11 @@ import { creativeSurfaceHints, readCreativeSelection } from "./creative.js";
12
12
  import { buildUxHarness, uxHarnessPlanContext } from "./uxHarness.js";
13
13
  import { recommendSolutionPath } from "../solutionPath.js";
14
14
  import { recommendUIKitCustomization } from "../uikitCustomization.js";
15
+ import { classifyEntryState } from "../entryState.js";
16
+ import { readFlowState } from "../flow.js";
17
+ import { clarifyDimensions, clarifyGuidance } from "../requestReadiness.js";
18
+ import { assembleIntake } from "../intake.js";
19
+ import { experienceBundleFor } from "../experience.js";
15
20
  export const planIntegrationTool = {
16
21
  name: "plan_integration",
17
22
  description: "Create a grounded, evidence-backed implementation packet before an AI coding agent edits a customer project.",
@@ -60,6 +65,13 @@ function answersFromInput(raw) {
60
65
  }
61
66
  async function buildIntegrationPlan(repoPath, request, surfacePath, answers = {}) {
62
67
  const repoRoot = path.resolve(repoPath);
68
+ if (!hasAnswer(answers, "solution_path")) {
69
+ const flow = await readFlowState(repoRoot);
70
+ const confirmedPath = flow?.request === request && flow.blueprint?.confirmed_digest ? flow.blueprint?.path ?? null : null;
71
+ if (confirmedPath) {
72
+ answers = { ...answers, solution_path: confirmedPath };
73
+ }
74
+ }
63
75
  const inspection = await inspectProject(repoRoot, surfacePath);
64
76
  const root = inspection.effectiveRoot;
65
77
  const storedCreativeSelection = await readCreativeSelection(repoRoot);
@@ -74,6 +86,14 @@ async function buildIntegrationPlan(repoPath, request, surfacePath, answers = {}
74
86
  const uxHarnessOutcome = BROAD_SOCIAL_REGEX.test(request) && !hasAnswer(answers, "feature_surface") ? undefined : outcome;
75
87
  const uxHarnessContext = uxHarness ? uxHarnessPlanContext(uxHarness, uxHarnessOutcome) : undefined;
76
88
  const platform = preferredPlatform(inspection.platforms);
89
+ const platformAvailability = platformFeatureGapsFor(request, platform).map((gap) => ({
90
+ feature: gap.feature,
91
+ status: "unavailable-on-platform",
92
+ platform,
93
+ availableOn: gap.availableOn,
94
+ sdkSource: gap.sdkSource,
95
+ guidance: gap.guidance,
96
+ }));
77
97
  const baseSupportLevel = supportFor(outcome, platform);
78
98
  const sensors = await detectCommandSensors(root, inspection.platforms);
79
99
  const ctx = planContextFor({
@@ -92,7 +112,18 @@ async function buildIntegrationPlan(repoPath, request, surfacePath, answers = {}
92
112
  const designContract = await readDesignContract(repoRoot);
93
113
  const designReview = designReviewGuidance(repoRoot, designContract, answers);
94
114
  const acceptedDesignContract = designReview.status === "accepted" ? designContract : null;
95
- const designBrief = acceptedDesignContract ? buildDesignBrief(acceptedDesignContract) : undefined;
115
+ const designProfile = acceptedDesignContract ? await readDesignProfile(repoRoot) : null;
116
+ const designBrief = acceptedDesignContract ? buildDesignBrief(acceptedDesignContract, designProfile) : undefined;
117
+ const designReviewLoop = designProfile
118
+ ? solutionPath.recommendation === "uikit"
119
+ ? {
120
+ status: "uikit",
121
+ note: "Your social.plus UI comes from themed prebuilt UIKit components — there's no custom render to critique. Run `vise design check` to verify your UIKit theme config (`uikit.config.json` — its `theme.light`/`theme.dark` palette) matches the brand tokens; off-brand colours in it are reported as off-contract. Tune the UIKit theme rather than hand-building screens.",
122
+ }
123
+ : solutionPath.recommendation === "sdk" || solutionPath.recommendation === "hybrid"
124
+ ? buildDesignReviewLoop(designProfile)
125
+ : undefined
126
+ : undefined;
96
127
  const socialWorkplan = await socialWorkplanFor({
97
128
  request,
98
129
  answers,
@@ -112,7 +143,9 @@ async function buildIntegrationPlan(repoPath, request, surfacePath, answers = {}
112
143
  const intake = intakeFor(ctx, outcomeQuestions, outcome, designBrief, capabilityAvailability, designReview);
113
144
  const packageJsonText = await readFile(path.join(root, "package.json"), "utf8").catch(() => undefined);
114
145
  const sdkVersion = await sdkVersionGuidance(platform, packageJsonText);
146
+ const crossPlatformHandoffSteps = crossPlatformImplementationSteps(platform, packageJsonText);
115
147
  const decisionsRequired = [
148
+ ...platformAvailability.map((notice) => `[platform limitation] ${notice.feature} is not exposed by the ${notice.platform} SDK (supported on ${notice.availableOn.join(", ")}). Decide an alternative — see platformAvailability — and state it; do not implement against a non-existent SDK API.`),
116
149
  ...solutionPathDecisions(solutionPath, answers),
117
150
  ...uikitCustomizationDecisions(uikitCustomization, answers),
118
151
  ...(creativeContext
@@ -129,13 +162,32 @@ async function buildIntegrationPlan(repoPath, request, surfacePath, answers = {}
129
162
  ? `[resolve before building] ${q.question}`
130
163
  : `[decide & state in your summary] ${q.question}`),
131
164
  ];
165
+ const entryState = classifyEntryState({
166
+ outcome,
167
+ broadSocialRequest: ctx.broadSocialRequest,
168
+ mentionsDesign: ctx.mentionsDesign,
169
+ designSignalCount: ctx.designSignals.length,
170
+ solutionPath,
171
+ hasExplicitPathAnswer: hasAnswer(answers, "solution_path"),
172
+ multiSurface: Boolean(socialWorkplan),
173
+ creativeSelected: Boolean(creativeSelection),
174
+ });
175
+ const platformDetected = inspection.platforms.length > 0;
176
+ const clarify = clarifyGuidance(clarifyDimensions({
177
+ outcome,
178
+ platformDetected,
179
+ uikitGuidedUnknown: platformDetected && outcome === "unknown" && Boolean(uikitCustomization),
180
+ creativeSelected: Boolean(creativeSelection),
181
+ }));
132
182
  return {
133
183
  outcome,
134
184
  platform,
135
185
  solutionPath,
186
+ entryState,
136
187
  uikitCustomization,
137
188
  supportLevel,
138
189
  intent: intentFor(request, definition.interpretation, uikitCustomization),
190
+ ...(platformAvailability.length > 0 ? { platformAvailability } : {}),
139
191
  creativeContext,
140
192
  uxHarness: uxHarnessContext,
141
193
  socialWorkplan,
@@ -147,10 +199,12 @@ async function buildIntegrationPlan(repoPath, request, surfacePath, answers = {}
147
199
  ...uxHarnessImplementationSteps(uxHarnessContext),
148
200
  ...solutionPathImplementationSteps(solutionPath),
149
201
  ...uikitCustomizationImplementationSteps(uikitCustomization),
202
+ ...crossPlatformHandoffSteps,
150
203
  ...definition.implementationSteps(ctx),
151
204
  ],
152
205
  validation: ["validate_setup", "run_sensors", ...definition.validation(platform)],
153
- nextStep: nextStepForPlan(outcome, uikitCustomization, solutionPath),
206
+ ...(clarify ? { clarify } : {}),
207
+ nextStep: clarify ? clarify.nextStep : nextStepForPlan(outcome, uikitCustomization, solutionPath),
154
208
  requiredInputs: composeRequiredInputs(ctx, definition.requiredInputs(ctx)),
155
209
  targetFiles: await targetFilesFor(root, outcome, platform, inspection.designSignals),
156
210
  implementationRules: [
@@ -169,11 +223,168 @@ async function buildIntegrationPlan(repoPath, request, surfacePath, answers = {}
169
223
  stopConditions: composeStopConditions(ctx, definition.stopConditions(ctx), inspection.surfaces, surfacePath, Boolean(socialWorkplan), uikitCustomization),
170
224
  evidencePolicy: "Every implementation step must cite at least one detected file, docs page, validator rule, or required user input. If evidence is missing, stop and ask the user instead of inventing details.",
171
225
  designContract: acceptedDesignContract ? designContractGuidance(acceptedDesignContract) : undefined,
226
+ brandGrounding: designBrief?.brandGrounding,
227
+ designReviewLoop,
172
228
  completenessChecklist: completenessChecklistFor(outcome),
173
229
  optionalCapabilities: optionalCapabilitiesFor(outcome, ctx.answers, request, capabilityAvailability),
230
+ engagement: engagementMenuFor(outcome),
174
231
  sdkVersion,
175
232
  };
176
233
  }
234
+ function crossPlatformImplementationSteps(platform, packageJsonText) {
235
+ if (platform === "flutter") {
236
+ return [
237
+ {
238
+ step: "Flutter product-entry pacing handoff: immediately route `lib/main.dart` to a product-native social surface, or create and import a visible product social widget, before broad pub-cache/source-internals research. In bounded dogfood, do not create a shell-first widget; the first social widget should already import `package:amity_sdk/amity_sdk.dart` and contain SDK setup/login plus at least one SDK data API call. Do not stop at static feature-preview cards that merely describe feed/comments/reactions/profile/chat; the surface must render SDK-returned/discovered/created objects, or honestly mark populated proof failed. Do not delete `lib/main.dart` as a standalone operation while preparing a replacement; edit it in place or add a new widget file and import it so interruptions never leave the app without an entry point. Do not spend the implementation budget only on `sp-vise/` artifacts, dependency installs, or reading SDK internals before the running app has a visible social route. If `flutter.sdk.source-used` fires, the app is still a dependency-only/static shell; import/use `package:amity_sdk/amity_sdk.dart` in product source before polishing UI. In dogfood/benchmark work, do not attest `flutter.sdk.source-used`; fix the source, rerun `vise check .`, and remove stale source-used attestations before claiming SDK-backed runtime proof.",
239
+ evidence: [
240
+ "detectedFiles.lib/main.dart",
241
+ "runtimeReadiness.targetDiscovery",
242
+ "validate_setup.flutter.sdk.source-used",
243
+ "implementationSteps",
244
+ ],
245
+ },
246
+ {
247
+ step: "Flutter dependency handoff: when using amity_sdk 7.x, add a direct `dio: 5.9.2` dependency or another bounded `<5.10.0` constraint before Android build proof. amity_sdk 7.x allows `dio ^5.4.0`, and dio 5.10.0 adds `DioExceptionType.transformTimeout`, which can make the SDK switch non-exhaustive during `assembleDebug` even when `flutter analyze` passes.",
248
+ evidence: [
249
+ "validate_setup.flutter.dependency.dio-compatible",
250
+ "pubspec.yaml",
251
+ "pubspec.lock",
252
+ ],
253
+ },
254
+ {
255
+ step: "Flutter runtime-readiness handoff: before any login, feed, message, or live-collection query, call `WidgetsFlutterBinding.ensureInitialized()` and await `AmityCoreClient.setup(option: AmityCoreClientOption.create(apiKey: ..., endpoint: AmityEndpoint.<region>), sycInitialization: true)`. The synchronous flag waits for SDK database adapters such as MessageDbAdapter; without it, builds can pass while runtime fails with `MessageDbAdapter not ready`.",
256
+ evidence: [
257
+ "social-plus-sdk/getting-started/platform-setup/mobile/flutter-quick-start",
258
+ "social-plus-sdk/getting-started/authentication",
259
+ "validate_setup.flutter.setup.sync-initialization",
260
+ ],
261
+ },
262
+ {
263
+ step: "Flutter query handoff: expose a reactive session-ready state from the login owner, seed it from the current SDK/session value, and trigger community/feed/chat queries only when that state becomes ready. Do not rely on `AmityCoreClient.observeSessionState().listen(...)` to emit the initial `NotLoggedIn` state; after subscribing, also kick the idempotent login/session branch immediately from `initState` (for example `Future.microtask(_login)` or an equivalent current-state branch), then let later observer emissions re-run post-login queries. A one-shot `initState` check that returns before login/setup completes, or an observer-only login path that never receives the initial state, leaves the screen stuck on signing-in/empty forever; render setup/login/query errors with a retry affordance.",
264
+ evidence: [
265
+ "social-plus-sdk/getting-started/authentication",
266
+ "social-plus-sdk/core-concepts/realtime-communication/live-objects-collections/flutter",
267
+ "validate_setup.flutter.session.initial-login-kickoff",
268
+ ],
269
+ },
270
+ {
271
+ step: "Flutter populated-proof handoff: do not count a polished empty state as populated runtime proof for community/feed/comments. If discovery returns empty in dogfood, create safe seed data through SDK functions, select the returned community/feed/post in app state, re-query, and render at least one visible community plus one visible post/comment/reaction path. After seed creation, either update UI state from the returned SDK objects or wait for the live collection to emit the created content before handoff; a default screen still showing `0 posts`, `0 comments` on the only visible thread, `syncing`, `waiting on fresh community content`, or a create/prepare failure is not populated proof. If the exact session/coach/squad target cannot be created or populated because of tenant permissions/review mode, fall back to the nearest discovered readable community/feed that already has visible posts only when it passes the product-owned filter below. Do not dump arbitrary readable tenant data into product UI: skip/hide discovered objects whose names or descriptions are raw IDs, numeric-only labels, short random strings such as `rand2`, lorem/test data, `sampleapp` fixtures, or unrelated generated business copy, then keep searching or seed a product-native target. Prefer a product-owned namespace/allowlist such as app-created names, metadata tags, session/program identifiers, or cached IDs returned by the app's own create flow; if the SDK query returns mixed tenant data, render only product-safe matches and never let an unsafe target become the active selected context. Deduplicate discovered/seeded targets by stable SDK ID or normalized product label, and prioritize populated product-owned targets before empty clones; a long rail of repeated zero-post targets is not polished populated proof. Do not rewrite unsafe tenant labels into product copy to make them look acceptable — exclude the object. Apply that filter to every visible rail/list/card and author fallback, not only the selected target. Do not swallow seed/query exceptions silently; record failures in handoff notes and render a product-native retry/error state. If neither seeding nor discovered populated fallback works, record resolved empty-state proof only and do not claim populated proof. Runtime screenshots with yellow/black Flutter overflow banners, red error boxes, clipped action rows, or `BOTTOM OVERFLOWED BY ... PIXELS` are runtime failures; fix layout with bounded cards, Flexible/Expanded/Wrap, maxLines, ellipsis, and responsive spacing before handoff.",
272
+ evidence: [
273
+ "runtimeReadiness.targetDiscovery",
274
+ "runtimeReadiness.surfaces.smokeExpectation",
275
+ ],
276
+ },
277
+ ];
278
+ }
279
+ if (platform === "react-native") {
280
+ const rnVersion = reactNativeVersionFromPackage(packageJsonText);
281
+ const nodeMajor = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
282
+ const toolchainNote = rnVersion && compareSemver(rnVersion, "0.82.0") >= 0
283
+ ? ` Detected React Native ${rnVersion}; local Node is ${process.version}. Run npm install/build before Social+ work and treat any RN/Gradle/New-Architecture engine warning as benchmark setup friction until the clean baseline also launches.`
284
+ : "";
285
+ const nodeNote = nodeMajor >= 23
286
+ ? " Node 23+ is ahead of most React Native LTS guidance; if install/build emits engine warnings, prefer an LTS Node for implementation proof or record the warning separately from Social+ correctness."
287
+ : "";
288
+ return [
289
+ {
290
+ step: "React Native product-entry pacing handoff: immediately route `App.tsx` to a product-native social surface, or create and import a visible product social component, before broad node_modules/source-internals research. In bounded dogfood, do not create a shell-first component; the first social component should already import `@amityco/ts-sdk-react-native` from the package root and contain SDK setup/login plus at least one SDK data API call such as community/feed/comment/reaction retrieval or creation. Do not stop at static feature-preview cards that merely describe feed/comments/reactions/profile/chat; the surface must render SDK-returned/discovered/created objects, or honestly mark populated proof failed. If `react-native.sdk.source-used` fires, the app is still a dependency-only/static shell; import/use the SDK in product source before polishing UI. In dogfood/benchmark work, do not attest `react-native.sdk.source-used`; fix the source, rerun `vise check .`, and remove stale source-used attestations before claiming SDK-backed runtime proof.",
291
+ evidence: [
292
+ "detectedFiles.App.tsx",
293
+ "runtimeReadiness.targetDiscovery",
294
+ "validate_setup.react-native.sdk.source-used",
295
+ "implementationSteps",
296
+ ],
297
+ },
298
+ {
299
+ step: `React Native runtime-readiness handoff: use \`@amityco/ts-sdk-react-native\`, add an SDK-compatible \`@react-native-async-storage/async-storage\` v1.x app dependency (for example \`^1.24.0\`, not latest 2.x/3.x) and \`@react-native-community/netinfo\` as app dependencies so npm dedupes to one native-backed storage package and React Native autolinks the required native modules, initialize with positional region \`Client.createClient(apiKey, API_REGIONS.SG)\` or the app's configured region, retain the session handler for the app lifetime, and call \`renewal.renew()\` from \`sessionWillRenewAccessToken\`. After \`createClient\`, set a stateful \`clientReady\` flag, call \`Client.login(..., sessionHandler)\` from an effect keyed by \`clientReady\` and the runtime user, set an explicit \`authReady\` / \`loggedIn\` state from the login promise resolution, and drive SDK queries from \`authReady\` or a normalized established session helper; do not rely on \`Client.onSessionStateChange\` alone to advance a cold launch. Add bounded retry/error handling when login stays \`notLoggedIn\`, so runtime proof never ends on a permanent spinner. Never deep-import SDK internals like \`@amityco/ts-sdk-react-native/dist/...\`; Metro/release bundling can fail even when TypeScript or Jest passes. Never seed React state by calling \`Client.isConnected()\` before the createClient path has run; return a safe not-ready state until a local \`clientCreated\` / \`clientReady\` flag is true, because \`Client.isConnected()\` can throw \`Amity SDK (800000): There is no active client\` at launch. Do not use the web-only package or an options-object createClient call.${toolchainNote}${nodeNote}`,
300
+ evidence: [
301
+ "social-plus-sdk/getting-started/platform-setup/web/web-quick-start",
302
+ "social-plus-sdk/getting-started/authentication",
303
+ "validate_setup.react-native.dependency.async-storage",
304
+ "validate_setup.react-native.dependency.async-storage-compatible",
305
+ "validate_setup.react-native.dependency.netinfo",
306
+ "validate_setup.react-native.session.renewal",
307
+ "validate_setup.react-native.client.session-state-after-create",
308
+ "validate_setup.react-native.sdk.no-internal-imports",
309
+ "sdkVersion",
310
+ ],
311
+ },
312
+ {
313
+ step: "React Native target-discovery handoff: bridge only local API key/user/region config through ignored native build config; never bridge communityId, postId, channelId, targetId, or feedId. Resolve those from SDK search/list/create results or host navigation/app state, then pass the returned SDK object/id through normal React state. Treat native config bridge and native module output as unproven until a release/debug runtime smoke shows non-empty config, no `NativeModule: AsyncStorage is null` or `NativeModule.RNCNetInfo is null` log, and an established SDK session; a screen that says credentials/config are missing is failed populated proof even if BuildConfig and TypeScript checks pass. Record runtime proof in evidence/notes, not end-user UI: do not render `CLIENT`, `SESSION`, `RUNTIME`, SDK, env, region, setup/readiness-debug labels, visible runtime/probe/status panels, or hidden no-op controls in the product screen.",
314
+ evidence: [
315
+ "requiredInputs.concrete feed target",
316
+ "runtimeReadiness.targetDiscovery",
317
+ "runtimeReadiness.runtimeProof",
318
+ ],
319
+ },
320
+ {
321
+ step: "React Native populated-proof handoff: an empty community/feed screen is not populated proof. If tenant discovery returns empty, create safe dogfood community/post/comment data through SDK calls, keep the returned objects in runtime state, re-query, and make the first runtime screen show at least one selected social target and one visible feed/comment/reaction path. Prefer a product-owned namespace/allowlist such as app-created names, metadata tags, program/session identifiers, or cached IDs returned by the app's own create flow. Filter every visible discovered rail/list/card and author fallback so raw IDs, numeric-only labels, short random strings like `rand2`, lorem/test fixtures, `sampleapp`, or unrelated tenant copy never appear in the product UI; never let an unsafe discovered target become the active selection, and do not rewrite bad labels/descriptions into product copy. Deduplicate discovered/seeded targets by stable SDK ID or normalized product label, and prioritize populated product-owned targets before empty clones. Runtime screenshots with red boxes, clipped action rows, or React Native layout/debug artifacts are runtime failures to repair. If data creation is not possible, label the run as resolved empty-state proof instead of product-proven populated proof.",
322
+ evidence: [
323
+ "runtimeReadiness.targetDiscovery",
324
+ "runtimeReadiness.surfaces.smokeExpectation",
325
+ ],
326
+ },
327
+ ];
328
+ }
329
+ if (platform === "ios") {
330
+ return [
331
+ {
332
+ step: "iOS product-entry pacing handoff: immediately route the SwiftUI root view or target UIViewController to a product-native social surface before broad SwiftPM/source-interface research. In bounded dogfood, the first social source edit should import `AmitySDK`, retain the client/session handler, login/register the runtime user, and use at least one real SDK data API such as `AmityCommunityRepository`, `AmityPostRepository`, `AmityUserRepository`, or `AmityChannelRepository`; `AmityClient` initialization plus static social cards is not enough. If `ios.sdk.source-used` fires, the app is still a dependency-only/static social shell; fix product Swift source and rerun `vise check .` before polishing UI or claiming runtime proof.",
333
+ evidence: [
334
+ "runtimeReadiness.targetDiscovery",
335
+ "validate_setup.ios.sdk.source-used",
336
+ "implementationSteps",
337
+ ],
338
+ },
339
+ {
340
+ step: "iOS runtime-readiness handoff: read API key, region, runtime user, and display name from local-only environment or existing host config, never from committed literals. Do not add communityId, postId, channelId, targetId, or feedId to Info.plist/xcconfig/env; discover or create those targets through SDK queries/create flows, store returned objects in app state, and keep VISE_SMOKE markers log-only. For populated proof, the launched social route must show SDK-backed product-safe community/feed/profile/chat data, not static fallback cards or setup/debug labels.",
341
+ evidence: [
342
+ "runtimeReadiness.targetDiscovery",
343
+ "runtimeReadiness.runtimeProof",
344
+ "runtimeReadiness.surfaces.smokeExpectation",
345
+ ],
346
+ },
347
+ ];
348
+ }
349
+ return [];
350
+ }
351
+ function reactNativeVersionFromPackage(packageJsonText) {
352
+ if (!packageJsonText)
353
+ return undefined;
354
+ try {
355
+ const pkg = JSON.parse(packageJsonText);
356
+ const raw = pkg.dependencies?.["react-native"] ?? pkg.devDependencies?.["react-native"];
357
+ if (!raw)
358
+ return undefined;
359
+ const match = String(raw).match(/(\d+\.\d+\.\d+)/);
360
+ return match?.[1];
361
+ }
362
+ catch {
363
+ return undefined;
364
+ }
365
+ }
366
+ function compareSemver(a, b) {
367
+ const pa = a.split(".").map((part) => Number.parseInt(part, 10) || 0);
368
+ const pb = b.split(".").map((part) => Number.parseInt(part, 10) || 0);
369
+ for (let i = 0; i < 3; i += 1) {
370
+ if (pa[i] !== pb[i])
371
+ return pa[i] - pb[i];
372
+ }
373
+ return 0;
374
+ }
375
+ function engagementMenuFor(outcome) {
376
+ const baselineLevers = capabilityChecklist(outcome).map((capability) => capability.id);
377
+ const optionalLevers = optionalCapabilityChecklist(outcome).map((capability) => capability.id);
378
+ if (baselineLevers.length === 0 && optionalLevers.length === 0) {
379
+ return undefined;
380
+ }
381
+ return {
382
+ note: "Engagement levers for this surface (advisory). Build the baseline for completeness; treat the optional levers as the menu that makes the experience compelling — don't ship the bare minimum (e.g. a text-only feed with no reactions or polls).",
383
+ baselineLevers,
384
+ optionalLevers,
385
+ deepenWith: 'Run `vise creative . --request "<intent>"` for Engagement Intelligence (candidate solutions, engagement options, and tradeoffs for this surface) before committing to scope.',
386
+ };
387
+ }
177
388
  function completenessChecklistFor(outcome) {
178
389
  const items = capabilityChecklist(outcome);
179
390
  if (items.length === 0) {
@@ -292,13 +503,23 @@ function supportLevelForPlan(outcome, platform, baseSupportLevel, uikitCustomiza
292
503
  return baseSupportLevel;
293
504
  }
294
505
  function nextStepForPlan(outcome, uikitCustomization, solutionPath) {
506
+ const ENGAGEMENT_HINT = " For engagement options (post types, polls, reactions, conversation and following patterns, and other ways to deepen the experience), run `vise creative . --request \"<intent>\"` before building — it surfaces levers `vise plan` lists only tersely under `optionalCapabilities`/`engagement`.";
507
+ const engagementHint = outcome === "add-feed" ||
508
+ outcome === "add-community" ||
509
+ outcome === "add-comments" ||
510
+ outcome === "add-chat" ||
511
+ outcome === "add-follow" ||
512
+ outcome === "add-notifications"
513
+ ? ENGAGEMENT_HINT
514
+ : "";
295
515
  if (outcome === "unknown" && uikitCustomization) {
296
- return "Treat this as UIKit customization guidance first: resolve the UIKit route and concrete app/platform target, then implement customer-owned config, localization, behavior, fork, or SDK custom UI changes with local build evidence. Do not claim `vise check` deterministically validates hidden UIKit internals.";
516
+ const uikitEngagementHint = solutionPath.decision.requiredBeforeHandRolledUi ? ENGAGEMENT_HINT : "";
517
+ return "Treat this as UIKit customization guidance first: resolve the UIKit route and concrete app/platform target, then implement customer-owned config, localization, behavior, fork, or SDK custom UI changes with local build evidence. Do not claim `vise check` deterministically validates hidden UIKit internals." + uikitEngagementHint;
297
518
  }
298
519
  if (solutionPath.decision.requiredBeforeHandRolledUi) {
299
- return "First resolve the solution-path decision (UIKit vs SDK vs hybrid). The SDK implementation steps above apply only if you build this surface directly with the SDK (or the SDK side of a hybrid); if the customer uses social.plus UIKit for the standard surface, follow the UIKit guidance instead of hand-rolling those steps. Once the route is settled, implement the applicable steps, then run `vise check .` and fix findings until green — you are not done until the check passes or each finding is explicitly attested.";
520
+ return "First resolve the solution-path decision (UIKit vs SDK vs hybrid). The SDK implementation steps above apply only if you build this surface directly with the SDK (or the SDK side of a hybrid); if the customer uses social.plus UIKit for the standard surface, follow the UIKit guidance instead of hand-rolling those steps. Once the route is settled, implement the applicable steps, then run `vise check .` and fix findings until green — you are not done until the check passes or each finding is explicitly attested." + engagementHint;
300
521
  }
301
- return "After implementing every step above, run `vise check .` and fix findings until green. You are not done until the check passes or each finding is explicitly attested.";
522
+ return "After implementing every step above, run `vise check .` and fix findings until green. You are not done until the check passes or each finding is explicitly attested." + engagementHint;
302
523
  }
303
524
  function dedupeDocsByPath(docs) {
304
525
  const seen = new Set();
@@ -434,6 +655,12 @@ const SOCIAL_SURFACE_SEQUENCE = [
434
655
  aliases: [/\b(feed|timeline|news feed|post list|posts?|create post|post creation|compose post|composer|reactions?)\b/i],
435
656
  defaultForGenericBroad: true,
436
657
  },
658
+ {
659
+ id: "livestream",
660
+ outcome: "setup-live-data",
661
+ label: "Live room and broadcast",
662
+ aliases: [/\b(live\s?stream(?:ing)?|live\s+video|live\s+room|broadcast(?:ing)?|go(?:ing)?\s+live)\b/i],
663
+ },
437
664
  {
438
665
  id: "comments",
439
666
  outcome: "add-comments",
@@ -472,20 +699,17 @@ async function socialWorkplanFor(args) {
472
699
  if (hasAnswer(args.answers, "feature_surface")) {
473
700
  return undefined;
474
701
  }
702
+ const experience = experienceBundleFor(args.request);
475
703
  const creativeHints = args.creativeSelection ? creativeSurfaceHints(args.creativeSelection) : [];
476
704
  const useCreativeSequence = creativeHints.length > 1;
477
- if (!BROAD_SOCIAL_REGEX.test(args.request) && !useCreativeSequence) {
705
+ if (!useCreativeSequence) {
478
706
  return undefined;
479
707
  }
480
708
  const matched = SOCIAL_SURFACE_SEQUENCE.filter((surface) => surface.aliases.some((pattern) => pattern.test(args.request)));
481
- const selected = useCreativeSequence
482
- ? creativeHints.flatMap((hint) => {
483
- const surface = SOCIAL_SURFACE_SEQUENCE.find((item) => item.id === hint.id);
484
- return surface ? [{ surface, creativeHint: hint }] : [];
485
- })
486
- : (matched.length > 0
487
- ? matched
488
- : SOCIAL_SURFACE_SEQUENCE.filter((surface) => surface.defaultForGenericBroad)).map((surface) => ({ surface, creativeHint: undefined }));
709
+ const selected = creativeHints.flatMap((hint) => {
710
+ const surface = SOCIAL_SURFACE_SEQUENCE.find((item) => item.id === hint.id);
711
+ return surface ? [{ surface, creativeHint: hint }] : [];
712
+ });
489
713
  if (selected.length <= 1) {
490
714
  return undefined;
491
715
  }
@@ -505,8 +729,10 @@ async function socialWorkplanFor(args) {
505
729
  }),
506
730
  broadSocialRequest: false,
507
731
  };
508
- const intake = intakeFor(surfaceCtx, definition.intakeQuestions(surfaceCtx), surface.outcome, args.designBrief, capabilityAvailability, args.designReview);
732
+ const intake = intakeFor(surfaceCtx, definition.intakeQuestions(surfaceCtx), surface.outcome, args.designBrief, capabilityAvailability, args.designReview, [DESIGN_CONTRACT_CONFIRMATION_ANSWER_ID]);
509
733
  const matchedPrompt = matched.some((match) => match.id === surface.id);
734
+ const companionEntry = experience && !matchedPrompt ? experience.surfaces.find((entry) => entry.id === surface.id) : undefined;
735
+ const companion = companionEntry ? { tier: companionEntry.tier, viaExperience: experience.id } : undefined;
510
736
  const commonArgs = `. --request ${shellQuote(args.request)} --answer feature_surface=${surface.id}`;
511
737
  sequence.push({
512
738
  id: surface.id,
@@ -514,9 +740,12 @@ async function socialWorkplanFor(args) {
514
740
  outcome: surface.outcome,
515
741
  label: surface.label,
516
742
  matchedPrompt,
517
- reason: creativeHint?.reason ?? (matchedPrompt
518
- ? `The request explicitly mentions ${surface.label.toLowerCase()}.`
519
- : "The request is generic broad social work, so Vise includes this core surface in the default sequence."),
743
+ ...(companion ? { companion } : {}),
744
+ reason: companion
745
+ ? `Included to make the ${companion.viaExperience} experience coherent (${companion.tier}); confirm or trim at the blueprint sign-off.`
746
+ : creativeHint?.reason ?? (matchedPrompt
747
+ ? `The request explicitly mentions ${surface.label.toLowerCase()}.`
748
+ : "The request is generic broad social work, so Vise includes this core surface in the default sequence."),
520
749
  planCommand: `vise plan ${commonArgs}`,
521
750
  initCommand: `vise init ${commonArgs}`,
522
751
  intake: {
@@ -537,8 +766,8 @@ async function socialWorkplanFor(args) {
537
766
  kind: "social-multi-surface",
538
767
  status,
539
768
  note: useCreativeSequence
540
- ? "This accepted creative variant is decomposed into ordered per-surface plans from its experience objects. Initialize and check one surface at a time so each sidecar has one deterministic outcome, while using this sequence as the coordinated product workplan."
541
- : "This broad social request is decomposed into ordered per-surface plans. Initialize and check one surface at a time so each sidecar has one outcome, while using this sequence as the coordinated product workplan.",
769
+ ? "This accepted creative variant is decomposed into ordered per-surface plans from its experience objects. Initialize and check one surface at a time so each sidecar has one deterministic outcome, while using this sequence as the coordinated product workplan. Do not skip ahead to a later surface; after blueprint sign-off `vise init` enforces the next pending surface from `vise workplan next/status`."
770
+ : "This broad social request is decomposed into ordered per-surface plans. Initialize and check one surface at a time so each sidecar has one outcome, while using this sequence as the coordinated product workplan. Do not skip ahead to a later surface; after blueprint sign-off `vise init` enforces the next pending surface from `vise workplan next/status`.",
542
771
  creativeContext: args.creativeSelection
543
772
  ? {
544
773
  selectedVariant: creativeContextForPlan(args.creativeSelection).selectedVariant,
@@ -547,7 +776,7 @@ async function socialWorkplanFor(args) {
547
776
  : undefined,
548
777
  uxHarness: args.uxHarness ? uxHarnessPlanContext(args.uxHarness) : undefined,
549
778
  sequence,
550
- nextStep: "Resolve each surface's blocking questions, then run the listed plan/init command for that surface before implementation. After each surface build, run `vise check .`, `vise sync .`, `vise validate .`, and `vise run-sensors .`.",
779
+ nextStep: "Run `vise workplan next/status` and implement only the next pending surface it prints. Resolve that surface's blocking questions, run its listed plan/init command, implement it, then run `vise check .`, `vise sync .`, `vise validate .`, `vise run-sensors .`, and `vise workplan complete` before moving to the next surface.",
551
780
  };
552
781
  }
553
782
  function shellQuote(value) {
@@ -641,74 +870,18 @@ function intentFor(request, interpretation, uikitCustomization) {
641
870
  ambiguity: broadSocialRequest || designRequest || uikitCustomization?.status === "needs-decision" || uikitOnlyCustomization ? "high" : "medium",
642
871
  };
643
872
  }
644
- function intakeFor(ctx, outcomeQuestions, outcome, brief, availability, designReview) {
645
- const questions = [...outcomeQuestions];
646
- if (designReview?.status === "needs-confirmation" && !hasAnswer(ctx.answers, DESIGN_CONTRACT_CONFIRMATION_ANSWER_ID)) {
647
- questions.push({
648
- id: DESIGN_CONTRACT_CONFIRMATION_ANSWER_ID,
649
- question: `Open ${designReview.previewPath} and confirm whether this design preview/contract should be used for the social UI.`,
650
- why: "A design contract must be explicitly accepted before Vise can feed it forward or let the agent claim design conformance. If it is wrong, reject it and provide the correct design source.",
651
- required: true,
652
- blocksImplementationWhenMissing: true,
653
- options: ["yes", "no"],
654
- });
655
- }
656
- if (designReview?.status === "rejected" && !hasAnswer(ctx.answers, "design_source")) {
657
- questions.push({
658
- id: "design_source",
659
- question: "What design source should replace the rejected preview/contract?",
660
- why: "The current design preview was rejected, so Vise must not use it. Provide a prototype, screenshot, theme/token file, or explicitly say to proceed without a design-conformance claim.",
661
- required: true,
662
- blocksImplementationWhenMissing: true,
663
- });
664
- }
665
- if (ctx.mentionsDesign && ctx.designSignals.length === 0 && !hasAnswer(ctx.answers, "design_source")) {
666
- questions.push({
667
- id: "design_source",
668
- question: "Where are the app's design tokens, theme, or reusable UI components defined?",
669
- why: "The user asked to match the existing design, and no local design source was detected.",
670
- required: true,
671
- blocksImplementationWhenMissing: true,
672
- });
673
- }
674
- if (ctx.mentionsDesign && ctx.designSignals.length > 0 && !hasAnswer(ctx.answers, "confirm_design_source")) {
675
- questions.push({
676
- id: "confirm_design_source",
677
- question: `Should the social UI use the detected design source(s): ${ctx.designSignals.map((signal) => signal.file).join(", ")}?`,
678
- why: "Vise found likely design evidence, but the user or host agent should confirm it is the right source before UI edits.",
679
- required: true,
680
- blocksImplementationWhenMissing: false,
681
- options: ["yes", "use another source"],
682
- });
683
- }
684
- if (brief &&
685
- designReview?.status === "accepted" &&
686
- (outcome === "add-feed" || outcome === "add-chat") &&
687
- !brief.roles.some((r) => r.role === "primaryAction") &&
688
- !hasAnswer(ctx.answers, "primary_action_token")) {
689
- questions.push({
690
- id: "primary_action_token",
691
- question: "Which design token (or color value) should be used as the primary action color? No primary-action token was confidently identified in the design contract.",
692
- why: "A primary action colour is needed for interactive elements (composer button, own-message bubble). Without a confident token, the agent must guess or omit it.",
693
- required: false,
694
- blocksImplementationWhenMissing: false,
695
- });
696
- }
697
- const availableOptionalIds = availableOptionalCapabilityIds(availability);
698
- const optionalChoices = outcome === "add-feed" ? optionalCapabilityChecklist(outcome, availableOptionalIds) : [];
699
- if (outcome === "add-feed" && optionalChoices.length > 0 && !hasAnswer(ctx.answers, "feed_optional_capabilities")) {
700
- questions.push({
701
- id: "feed_optional_capabilities",
702
- question: `Which optional feed capabilities should be in scope on ${ctx.platform}: ${optionalChoices.map((capability) => capability.id).join(", ")}, or none?`,
703
- why: "These capabilities are useful for full feeds, but should become enforceable only after the customer explicitly opts in. Vise only lists capabilities that appear available in the platform SDK surface; unsupported capabilities must be treated as out of scope or confirmed from docs.",
704
- required: false,
705
- blocksImplementationWhenMissing: false,
706
- options: ["none", ...optionalChoices.map((capability) => capability.id)],
707
- });
708
- }
709
- const remainingBlocking = questions.filter((question) => question.blocksImplementationWhenMissing).length;
873
+ function intakeFor(ctx, outcomeQuestions, outcome, brief, availability, designReview, suppressGateIds) {
874
+ const { questions, remainingBlocking, status } = assembleIntake({
875
+ ctx,
876
+ outcomeQuestions,
877
+ outcome,
878
+ designBrief: brief,
879
+ capabilityAvailability: availability,
880
+ designReview,
881
+ suppressGateIds,
882
+ });
710
883
  return {
711
- status: remainingBlocking > 0 ? "needs-clarification" : "ready",
884
+ status,
712
885
  questions,
713
886
  answers: ctx.answers,
714
887
  remainingBlocking,
@@ -1,5 +1,6 @@
1
1
  import { mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { sidecarDir } from "../sidecar.js";
3
4
  import { objectInput, optionalBooleanField, optionalStringField, stringField, textResult } from "../types.js";
4
5
  import { packageVersion } from "../version.js";
5
6
  import { readCreativeSelection } from "./creative.js";
@@ -475,9 +476,6 @@ async function writeLearningSummary(repoRoot, summary) {
475
476
  await mkdir(path.dirname(filePath), { recursive: true });
476
477
  await writeFile(filePath, `${JSON.stringify(summary, null, 2)}\n`, "utf8");
477
478
  }
478
- function sidecarDir(repoRoot) {
479
- return path.join(repoRoot, "sp-vise");
480
- }
481
479
  function privacyBlock() {
482
480
  return {
483
481
  mode: "local-only",