@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.
- package/CHANGELOG.md +51 -2
- package/README.md +36 -19
- package/dist/capabilities.js +29 -1
- package/dist/entryState.js +71 -0
- package/dist/experience.js +70 -0
- package/dist/explore.js +1 -1
- package/dist/flow.js +382 -0
- package/dist/humanFormat.js +25 -0
- package/dist/intake.js +117 -0
- package/dist/intelligence/placement.js +2 -1
- package/dist/outcomes.js +126 -28
- package/dist/productExpectations.js +15 -0
- package/dist/requestReadiness.js +99 -0
- package/dist/server.js +566 -19
- package/dist/sidecar.js +5 -0
- package/dist/solutionPath.js +1 -1
- package/dist/tools/compliance.js +752 -125
- package/dist/tools/creative.js +14 -12
- package/dist/tools/design.js +321 -11
- package/dist/tools/experienceCompiler.js +1 -1
- package/dist/tools/experienceSensors.js +1 -1
- package/dist/tools/harness.js +13 -0
- package/dist/tools/integration.js +263 -90
- package/dist/tools/learning.js +1 -3
- package/dist/tools/project.js +963 -66
- package/dist/tools/smoke.js +134 -0
- package/dist/tools/uxHarness.js +54 -6
- package/dist/uikitCustomization.js +3 -1
- package/dist/version.js +7 -3
- package/package.json +1 -1
- package/packages/intelligence/catalog/catalog.schema.json +1 -1
- package/packages/intelligence/catalog/experience-objects.json +2 -2
- package/packages/intelligence/catalog/variants.json +24 -0
- package/rules/event.yaml +3 -0
- package/rules/feed.yaml +251 -0
- package/rules/invitation.yaml +4 -0
- package/rules/live-data.yaml +110 -0
- package/rules/notification-tray.yaml +4 -0
- package/rules/poll.yaml +5 -0
- package/rules/sdk-lifecycle.yaml +559 -0
- package/rules/search.yaml +5 -0
- package/rules/security.yaml +12 -0
- package/rules/story.yaml +5 -0
- package/rules/user-blocking.yaml +5 -0
- 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
|
-
|
|
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
|
|
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,
|
|
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>
|
|
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 =
|
|
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:
|
|
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
|
|
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 =
|
|
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
|
-
|
|
1544
|
-
|
|
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",
|