@amityco/social-plus-vise 1.4.6 → 1.5.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.
package/dist/server.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { createHash } from "node:crypto";
2
3
  import { copyFile, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
3
4
  import os from "node:os";
4
5
  import path from "node:path";
@@ -20,7 +21,7 @@ import { recordLearningTool, showLearningTool } from "./tools/learning.js";
20
21
  import { inspectProjectTool, validateSetupTool } from "./tools/project.js";
21
22
  import { resolveRequestTool, suggestPatchTool } from "./tools/resolve.js";
22
23
  import { runSensorsTool } from "./tools/sensors.js";
23
- import { smokeTool } from "./tools/smoke.js";
24
+ import { smokeTool, writeRuntimeProofWaiver } from "./tools/smoke.js";
24
25
  import { getSdkFactsTool } from "./tools/sdkFacts.js";
25
26
  import { addBlockInstall, listRegistryBlocks, planBlockInstall, validateBlockInstall } from "./tools/blocks.js";
26
27
  import { debugIssueTool, debugIssue } from "./tools/debug.js";
@@ -29,6 +30,7 @@ import { uxHarnessTool } from "./tools/uxHarness.js";
29
30
  import { exploreRequest } from "./explore.js";
30
31
  import { renderHuman, wantsHumanFormat } from "./humanFormat.js";
31
32
  import { buildSource, packageName, packageRoot, packageVersion } from "./version.js";
33
+ import { resolveOutcome, relatedOutcomes } from "./outcomes.js";
32
34
  const SUPPORT_URL = "mailto:support@social.plus";
33
35
  const tools = new Map([
34
36
  searchDocsTool,
@@ -314,12 +316,16 @@ async function handleCli(args) {
314
316
  const subArgs = args.slice(2);
315
317
  if (sub === "next" || sub === "status") {
316
318
  assertOnlyKnownFlags(subArgs, ["request", "surface", "surface-path", "answer"], `workplan ${sub}`);
317
- console.log(JSON.stringify(await workplanStatus(subArgs), null, 2));
319
+ const payload = await workplanStatus(subArgs);
320
+ console.log(JSON.stringify(payload, null, 2));
321
+ process.exitCode = workplanExitCode(payload);
318
322
  return "exit";
319
323
  }
320
324
  if (sub === "complete") {
321
325
  assertOnlyKnownFlags(subArgs, ["request", "surface", "surface-path", "answer", "note"], "workplan complete");
322
- console.log(JSON.stringify(await completeWorkplanSurface(subArgs), null, 2));
326
+ const payload = await completeWorkplanSurface(subArgs);
327
+ console.log(JSON.stringify(payload, null, 2));
328
+ process.exitCode = workplanExitCode(payload);
323
329
  return "exit";
324
330
  }
325
331
  if (sub === "trim") {
@@ -356,6 +362,18 @@ async function handleCli(args) {
356
362
  }
357
363
  return "exit";
358
364
  }
365
+ if (command === "smoke" && args[1] === "waive") {
366
+ const waiveArgs = args.slice(2);
367
+ assertOnlyKnownFlags(waiveArgs, ["reason", "mode"], "smoke waive");
368
+ const reason = requiredFlagValue(waiveArgs, "reason", "smoke waive requires --reason \"<why runtime proof is not being performed>\".");
369
+ const modeRaw = flagValue(waiveArgs, "mode") ?? "declined";
370
+ if (modeRaw !== "declined" && modeRaw !== "deviceless") {
371
+ throw new Error('smoke waive --mode must be "declined" (user opted out) or "deviceless" (no device/emulator available).');
372
+ }
373
+ const waiver = await writeRuntimeProofWaiver(positionalRepoPath(waiveArgs), modeRaw, reason, new Date().toISOString());
374
+ console.log(JSON.stringify({ status: "waived", waiver, note: "Runtime proof waived — NOT runtime-proven. `vise check` will report runtime-proof-waived (exit 9; pass --allow-proof-waiver to exit 0). Remove sp-vise/evidence/runtime-proof-waiver.json to re-require proof." }, null, 2));
375
+ return "exit";
376
+ }
359
377
  if (command === "smoke") {
360
378
  const printed = await printToolResult(smokeTool, {
361
379
  repoPath: positionalRepoPath(args.slice(1)),
@@ -366,7 +384,7 @@ async function handleCli(args) {
366
384
  if (status === "failed") {
367
385
  process.exitCode = 1;
368
386
  }
369
- else if (status === "no-config" || status === "no-log" || status === "no-surfaces") {
387
+ else if (status === "no-config" || status === "no-log" || status === "no-surfaces" || status === "no-source-provenance") {
370
388
  process.exitCode = 2;
371
389
  }
372
390
  return "exit";
@@ -448,15 +466,15 @@ async function handleCli(args) {
448
466
  const initRepo = positionalRepoPath(args.slice(1));
449
467
  const initRequest = requiredFlagValue(args, "request", "init requires --request.");
450
468
  const initWorkplanPlan = await workplanPlan(initRepo, initRequest, args);
451
- const { sequence: initSequence, omitted: initOmitted } = await activeBlueprint(initRepo, initRequest, initWorkplanPlan);
469
+ const { sequence: initSequence, omitted: initOmitted, canonicalRequest: initCanonicalRequest } = await activeBlueprint(initRepo, initRequest, initWorkplanPlan);
452
470
  if (initSequence.length >= 2 && initWorkplanPlan.platform !== "unknown") {
453
- const blueprintGate = await requireBlueprintConfirmation(initRepo, initRequest, initWorkplanPlan, initSequence, args, new Date().toISOString(), initOmitted);
471
+ const blueprintGate = await requireBlueprintConfirmation(initRepo, initCanonicalRequest, initWorkplanPlan, initSequence, args, new Date().toISOString(), initOmitted);
454
472
  if (blueprintGate) {
455
473
  console.log(JSON.stringify(blueprintGate, null, 2));
456
474
  process.exitCode = 7;
457
475
  return "exit";
458
476
  }
459
- const workplanInitGate = await requireNextWorkplanSurfaceInit(initRepo, initRequest, initSequence, keyValueFlag(args, "answer"));
477
+ const workplanInitGate = await requireNextWorkplanSurfaceInit(initRepo, initCanonicalRequest, initSequence, keyValueFlag(args, "answer"), initOmitted, initRequest);
460
478
  if (workplanInitGate) {
461
479
  console.log(JSON.stringify(workplanInitGate, null, 2));
462
480
  process.exitCode = 7;
@@ -474,8 +492,8 @@ async function handleCli(args) {
474
492
  return "exit";
475
493
  }
476
494
  if (command === "check") {
477
- assertOnlyKnownFlags(args, ["ci", "format", "new-only"], "check");
478
- const result = await checkCompliance(positionalRepoPath(args.slice(1)), { newOnly: hasFlag(args, "new-only") });
495
+ assertOnlyKnownFlags(args, ["ci", "format", "new-only", "allow-proof-waiver"], "check");
496
+ const result = await checkCompliance(positionalRepoPath(args.slice(1)), { newOnly: hasFlag(args, "new-only"), allowProofWaiver: hasFlag(args, "allow-proof-waiver") });
479
497
  emitResult("check", hasFlag(args, "ci") ? ciCheckResult(result) : result, args);
480
498
  process.exitCode = result.exitCode;
481
499
  return "exit";
@@ -1472,6 +1490,24 @@ async function fileExists(filePath) {
1472
1490
  return false;
1473
1491
  }
1474
1492
  }
1493
+ async function digestFile(filePath) {
1494
+ return `sha256:${createHash("sha256").update(await readFile(filePath)).digest("hex")}`;
1495
+ }
1496
+ function digestJson(value) {
1497
+ return `sha256:${createHash("sha256").update(stableStringify(value)).digest("hex")}`;
1498
+ }
1499
+ function stableStringify(value) {
1500
+ if (Array.isArray(value)) {
1501
+ return `[${value.map(stableStringify).join(",")}]`;
1502
+ }
1503
+ if (value && typeof value === "object") {
1504
+ const entries = Object.entries(value)
1505
+ .filter(([, entryValue]) => entryValue !== undefined)
1506
+ .sort(([a], [b]) => a.localeCompare(b));
1507
+ return `{${entries.map(([key, entryValue]) => `${JSON.stringify(key)}:${stableStringify(entryValue)}`).join(",")}}`;
1508
+ }
1509
+ return JSON.stringify(value) ?? "undefined";
1510
+ }
1475
1511
  function expandHome(input) {
1476
1512
  if (input === "~") {
1477
1513
  return os.homedir();
@@ -1510,25 +1546,31 @@ function blueprintStagesForSequence(sequence) {
1510
1546
  }
1511
1547
  async function activeBlueprint(repoRoot, request, plan) {
1512
1548
  const planSequence = socialWorkplanSequence(plan);
1513
- const full = planSequence.length
1514
- ? planSequence
1515
- : await socialWorkplanSequenceFromSignedFlow(repoRoot, request);
1516
- const omitted = await readOmittedSurfaces(repoRoot, request);
1549
+ const flowFallback = await socialWorkplanSequenceFromFlow(repoRoot, request);
1550
+ const canonicalRequest = flowFallback?.request ?? request;
1551
+ const full = planSequence.length ? planSequence : flowFallback?.sequence ?? [];
1552
+ const omitted = await readOmittedSurfaces(repoRoot, canonicalRequest);
1517
1553
  const omittedIds = new Set(omitted.map((item) => item.id));
1518
- return { sequence: full.filter((surface) => !omittedIds.has(surface.id)), omitted };
1554
+ return { sequence: full.filter((surface) => !omittedIds.has(surface.id)), omitted, canonicalRequest };
1519
1555
  }
1520
- async function socialWorkplanSequenceFromSignedFlow(repoRoot, request) {
1556
+ async function socialWorkplanSequenceFromFlow(repoRoot, request) {
1521
1557
  const state = await readFlowState(repoRoot);
1522
- if (state?.request !== request) {
1523
- return [];
1558
+ if (!state) {
1559
+ return null;
1524
1560
  }
1525
1561
  const stages = state.blueprint?.stages ?? [];
1526
1562
  if (stages.length <= 1) {
1527
- return [];
1563
+ return null;
1564
+ }
1565
+ const omitted = state.blueprint?.omitted ?? [];
1566
+ const omittedIds = new Set(omitted.map((item) => item.id));
1567
+ const activeStages = stages.filter((stage) => !omittedIds.has(stage.id || stage.outcome));
1568
+ if (activeStages.length <= 1 || !signedFlowAppliesToRequest(state, request, activeStages, omitted)) {
1569
+ return null;
1528
1570
  }
1529
- return stages.map((stage, index) => {
1571
+ const sequence = activeStages.map((stage, index) => {
1530
1572
  const id = stage.id || stage.outcome;
1531
- const commonArgs = `. --request ${shellQuoteForCommand(request)} --answer feature_surface=${id}`;
1573
+ const commonArgs = `. --request ${shellQuoteForCommand(state.request)} --answer feature_surface=${id}`;
1532
1574
  return {
1533
1575
  id,
1534
1576
  order: index + 1,
@@ -1544,6 +1586,23 @@ async function socialWorkplanSequenceFromSignedFlow(repoRoot, request) {
1544
1586
  validation: ["validate_setup", "run_sensors"],
1545
1587
  };
1546
1588
  });
1589
+ return { request: state.request, sequence };
1590
+ }
1591
+ function signedFlowAppliesToRequest(state, request, activeStages, omitted) {
1592
+ const current = normalizeWorkflowRequest(request);
1593
+ const stored = normalizeWorkflowRequest(state.request);
1594
+ if (!current || !stored) {
1595
+ return false;
1596
+ }
1597
+ if (current === stored || current.includes(stored) || stored.includes(current)) {
1598
+ return true;
1599
+ }
1600
+ const requestedOutcome = resolveOutcome(request, {});
1601
+ const stageOutcomes = [...activeStages, ...omitted].map((stage) => stage.outcome);
1602
+ return relatedOutcomes(stageOutcomes).has(requestedOutcome);
1603
+ }
1604
+ function normalizeWorkflowRequest(value) {
1605
+ return value.toLowerCase().replace(/[^a-z0-9\s]+/g, " ").replace(/\s+/g, " ").trim();
1547
1606
  }
1548
1607
  async function requireBlueprintConfirmation(repoRoot, request, plan, sequence, args, now, omitted = []) {
1549
1608
  if (sequence.length === 0) {
@@ -1761,33 +1820,67 @@ async function workplanStatus(args) {
1761
1820
  const repoRoot = path.resolve(positionalRepoPath(args));
1762
1821
  const request = requiredFlagValue(args, "request", "workplan next/status requires --request.");
1763
1822
  const plan = await workplanPlan(repoRoot, request, args);
1764
- const { sequence, omitted } = await activeBlueprint(repoRoot, request, plan);
1765
- const progress = await readWorkplanProgress(repoRoot);
1766
- const completed = progress?.request === request ? progress.completed : [];
1767
- const completedIds = new Set(completed.map((item) => item.surface));
1768
- const nextSurface = sequence.find((surface) => !completedIds.has(surface.id));
1823
+ const { sequence, omitted, canonicalRequest } = await activeBlueprint(repoRoot, request, plan);
1824
+ let progress = await verifiedWorkplanProgress(repoRoot, canonicalRequest, sequence);
1825
+ let completed = progress.completed;
1826
+ let completedIds = new Set(completed.map((item) => item.surface));
1827
+ let nextSurface = sequence.find((surface) => !completedIds.has(surface.id));
1769
1828
  if (sequence.length === 0) {
1829
+ const existingFlow = await readFlowState(repoRoot);
1830
+ const signedStages = existingFlow?.blueprint?.confirmed_digest ? (existingFlow.blueprint?.stages ?? []) : [];
1831
+ const signedFlowAvailable = signedStages.length >= 2 && existingFlow
1832
+ ? {
1833
+ request: existingFlow.request,
1834
+ nextStep: `A signed multi-surface flow is already in progress for a different request; this request was treated as a separate single-outcome build. To resume that journey, re-run with its original request: vise workplan status . --request ${shellQuoteForCommand(existingFlow.request)}`,
1835
+ }
1836
+ : undefined;
1770
1837
  return {
1771
1838
  status: "not-applicable",
1772
1839
  reason: "The request did not produce a multi-surface social workplan. Use `vise plan` for the single-outcome flow.",
1773
1840
  request,
1774
1841
  progressPath: workplanProgressPath(repoRoot),
1842
+ ...(signedFlowAvailable ? { signedFlowAvailable } : {}),
1775
1843
  };
1776
1844
  }
1777
- const blueprintGate = await requireBlueprintConfirmation(repoRoot, request, plan, sequence, args, new Date().toISOString(), omitted);
1845
+ const blueprintGate = await requireBlueprintConfirmation(repoRoot, canonicalRequest, plan, sequence, args, new Date().toISOString(), omitted);
1778
1846
  if (blueprintGate) {
1779
1847
  return blueprintGate;
1780
1848
  }
1849
+ let autoCompletedSurface;
1850
+ const scopeOutcome = await readSidecarOutcome(repoRoot);
1851
+ const scopePending = scopeOutcome
1852
+ ? sequence.find((surface) => surface.outcome === scopeOutcome && !completedIds.has(surface.id))
1853
+ : undefined;
1854
+ if (scopePending) {
1855
+ try {
1856
+ const live = await checkCompliance(repoRoot);
1857
+ if (live.outcome === scopePending.outcome && isCompletableCheckStatus(live.status)) {
1858
+ await recordWorkplanSurfaceCompletion(repoRoot, canonicalRequest, sequence, scopePending, live, new Date().toISOString(), "auto-recorded by vise workplan status from a green check for the in-scope surface");
1859
+ autoCompletedSurface = scopePending.id;
1860
+ progress = await verifiedWorkplanProgress(repoRoot, canonicalRequest, sequence);
1861
+ completed = progress.completed;
1862
+ completedIds = new Set(completed.map((item) => item.surface));
1863
+ nextSurface = sequence.find((surface) => !completedIds.has(surface.id));
1864
+ }
1865
+ }
1866
+ catch {
1867
+ }
1868
+ }
1781
1869
  const runtimeReadiness = runtimeReadinessForStages(blueprintStagesForSequence(sequence));
1782
1870
  const nextSurfaceReadiness = nextSurface ? runtimeReadiness.surfaces.find((surface) => surface.id === nextSurface.id) : undefined;
1783
1871
  return {
1784
1872
  status: nextSurface ? "next-surface" : "complete",
1785
- request,
1873
+ request: canonicalRequest,
1874
+ ...(canonicalRequest !== request ? { requestedAs: request } : {}),
1786
1875
  progressPath: workplanProgressPath(repoRoot),
1787
1876
  creativeContext: plan.socialWorkplan?.creativeContext ?? plan.creativeContext,
1788
1877
  uxHarness: plan.socialWorkplan?.uxHarness ?? plan.uxHarness,
1789
1878
  runtimeReadiness,
1790
1879
  completed,
1880
+ ...(autoCompletedSurface
1881
+ ? { autoCompleted: { surface: autoCompletedSurface, reason: "in-scope surface had a green check; recorded from evidence so you don't need a separate `vise workplan complete` for it" } }
1882
+ : {}),
1883
+ ...(progress.ignored.length ? { ignoredProgress: progress.ignored } : {}),
1791
1884
  sequence: sequence.map((surface) => ({
1792
1885
  id: surface.id,
1793
1886
  order: surface.order,
@@ -1817,7 +1910,7 @@ async function workplanStatus(args) {
1817
1910
  sync: "vise sync .",
1818
1911
  validate: "vise validate .",
1819
1912
  sensors: "vise run-sensors .",
1820
- complete: `vise workplan complete . --request ${shellQuoteForCommand(request)} --surface ${nextSurface.id}`,
1913
+ complete: `vise workplan complete . --request ${shellQuoteForCommand(canonicalRequest)} --surface ${nextSurface.id}`,
1821
1914
  },
1822
1915
  }
1823
1916
  : undefined,
@@ -1856,8 +1949,8 @@ async function trimWorkplanSurface(args) {
1856
1949
  exitCode: 0,
1857
1950
  };
1858
1951
  }
1859
- const progress = await readWorkplanProgress(repoRoot);
1860
- const completedIds = new Set(progress?.request === request ? progress.completed.map((item) => item.surface) : []);
1952
+ const progress = await verifiedWorkplanProgress(repoRoot, request, full);
1953
+ const completedIds = new Set(progress.completed.map((item) => item.surface));
1861
1954
  if (completedIds.has(surfaceId)) {
1862
1955
  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
1956
  }
@@ -1897,39 +1990,20 @@ async function trimWorkplanSurface(args) {
1897
1990
  exitCode: 7,
1898
1991
  };
1899
1992
  }
1900
- async function completeWorkplanSurface(args) {
1901
- const repoRoot = path.resolve(positionalRepoPath(args));
1902
- const request = requiredFlagValue(args, "request", "workplan complete requires --request.");
1903
- const surfaceId = requiredFlagValue(args, "surface", "workplan complete requires --surface <surface-id>.");
1904
- const note = flagValue(args, "note");
1905
- const plan = await workplanPlan(repoRoot, request, args);
1906
- const { sequence, omitted } = await activeBlueprint(repoRoot, request, plan);
1907
- const surface = sequence.find((item) => item.id === surfaceId);
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
- }
1912
- throw new Error(`Surface "${surfaceId}" is not in this social workplan. Available surfaces: ${sequence.map((item) => item.id).join(", ") || "(none)"}.`);
1913
- }
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
- }
1919
- const check = await greenWorkplanCheck(repoRoot, surfaceId, surface.outcome);
1920
- const snapshot = await writeWorkplanSurfaceSnapshot(repoRoot, surfaceId, check, now);
1921
- const existing = await readWorkplanProgress(repoRoot);
1922
- const base = existing?.request === request
1923
- ? existing
1993
+ async function recordWorkplanSurfaceCompletion(repoRoot, canonicalRequest, sequence, surface, check, now, note) {
1994
+ const snapshot = await writeWorkplanSurfaceSnapshot(repoRoot, surface.id, check, now);
1995
+ const existing = await verifiedWorkplanProgress(repoRoot, canonicalRequest, sequence);
1996
+ const base = existing.raw?.request === canonicalRequest
1997
+ ? { ...existing.raw, completed: existing.completed }
1924
1998
  : {
1925
1999
  schema_version: 2,
1926
2000
  vise_version: packageVersion,
1927
- request,
2001
+ request: canonicalRequest,
1928
2002
  generated_at: now,
1929
2003
  updated_at: now,
1930
2004
  completed: [],
1931
2005
  };
1932
- const withoutSurface = base.completed.filter((item) => item.surface !== surfaceId);
2006
+ const withoutSurface = base.completed.filter((item) => item.surface !== surface.id);
1933
2007
  const progress = {
1934
2008
  ...base,
1935
2009
  schema_version: 2,
@@ -1938,7 +2012,7 @@ async function completeWorkplanSurface(args) {
1938
2012
  completed: [
1939
2013
  ...withoutSurface,
1940
2014
  {
1941
- surface: surfaceId,
2015
+ surface: surface.id,
1942
2016
  outcome: surface.outcome,
1943
2017
  completed_at: now,
1944
2018
  ...(note ? { note } : {}),
@@ -1957,7 +2031,31 @@ async function completeWorkplanSurface(args) {
1957
2031
  ].sort((a, b) => sequenceIndex(sequence, a.surface) - sequenceIndex(sequence, b.surface)),
1958
2032
  };
1959
2033
  await writeWorkplanProgress(repoRoot, progress);
1960
- const status = await workplanStatus(args);
2034
+ return progress;
2035
+ }
2036
+ async function completeWorkplanSurface(args) {
2037
+ const repoRoot = path.resolve(positionalRepoPath(args));
2038
+ const request = requiredFlagValue(args, "request", "workplan complete requires --request.");
2039
+ const surfaceId = requiredFlagValue(args, "surface", "workplan complete requires --surface <surface-id>.");
2040
+ const note = flagValue(args, "note");
2041
+ const plan = await workplanPlan(repoRoot, request, args);
2042
+ const { sequence, omitted, canonicalRequest } = await activeBlueprint(repoRoot, request, plan);
2043
+ const surface = sequence.find((item) => item.id === surfaceId);
2044
+ if (!surface) {
2045
+ if (omitted.some((item) => item.id === surfaceId)) {
2046
+ 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\`.`);
2047
+ }
2048
+ throw new Error(`Surface "${surfaceId}" is not in this social workplan. Available surfaces: ${sequence.map((item) => item.id).join(", ") || "(none)"}.`);
2049
+ }
2050
+ const now = new Date().toISOString();
2051
+ const blueprintGate = await requireBlueprintConfirmation(repoRoot, canonicalRequest, plan, sequence, args, now, omitted);
2052
+ if (blueprintGate) {
2053
+ return blueprintGate;
2054
+ }
2055
+ const check = await greenWorkplanCheck(repoRoot, surfaceId, surface.outcome);
2056
+ const progress = await recordWorkplanSurfaceCompletion(repoRoot, canonicalRequest, sequence, surface, check, now, note);
2057
+ const statusArgs = canonicalRequest === request ? args : replaceFlagValue(args, "request", canonicalRequest);
2058
+ const status = await workplanStatus(statusArgs);
1961
2059
  const sequenceStatus = workplanSequenceStatus(status);
1962
2060
  const completedSurfaces = sequenceStatus.filter((item) => item.completed);
1963
2061
  const pendingSurfaces = sequenceStatus.filter((item) => !item.completed);
@@ -2008,11 +2106,14 @@ async function greenWorkplanCheck(repoRoot, surfaceId, expectedOutcome) {
2008
2106
  if (expectedOutcome && check.outcome !== expectedOutcome) {
2009
2107
  throw new Error(`Cannot mark "${surfaceId}" complete: the sp-vise sidecar is currently scoped to outcome "${check.outcome}", not this surface's outcome "${expectedOutcome}". Each workplan surface has its own sidecar scope — run this surface's focused \`vise init\` command (it sets \`--answer feature_surface=${surfaceId}\`) and \`vise check .\` before recording it. (The check reported "${check.status}" only because it ran against the other surface's contract.)`);
2010
2108
  }
2011
- if (check.status !== "green") {
2109
+ if (!isCompletableCheckStatus(check.status)) {
2012
2110
  throw new Error(`Cannot mark "${surfaceId}" complete because \`vise check\` returned "${check.status}" (exit ${check.exitCode}). Resolve the check result before recording workplan progress.`);
2013
2111
  }
2014
2112
  return check;
2015
2113
  }
2114
+ function isCompletableCheckStatus(status) {
2115
+ return status === "green" || status === "runtime-proof-waived";
2116
+ }
2016
2117
  async function writeWorkplanSurfaceSnapshot(repoRoot, surfaceId, check, snapshotAt) {
2017
2118
  const snapshotDir = workplanSurfaceSnapshotDir(repoRoot, surfaceId);
2018
2119
  await mkdir(snapshotDir, { recursive: true });
@@ -2034,9 +2135,37 @@ async function writeWorkplanSurfaceSnapshot(repoRoot, surfaceId, check, snapshot
2034
2135
  check,
2035
2136
  }, null, 2)}\n`, "utf8");
2036
2137
  files.push(repoRelativePath(repoRoot, checkPath));
2138
+ const fileDigests = [];
2139
+ for (const relPath of files.sort()) {
2140
+ fileDigests.push({ path: relPath, sha256: await digestFile(path.join(repoRoot, relPath)) });
2141
+ }
2142
+ const manifestPayload = {
2143
+ schema_version: 1,
2144
+ generated_by: "vise workplan complete",
2145
+ generated_at: snapshotAt,
2146
+ surface: surfaceId,
2147
+ outcome: check.outcome,
2148
+ check_digest: digestJson(check),
2149
+ files: fileDigests,
2150
+ };
2151
+ const manifestDigest = digestJson(manifestPayload);
2152
+ const manifestPath = path.join(snapshotDir, "snapshot-manifest.json");
2153
+ const manifestRelPath = repoRelativePath(repoRoot, manifestPath);
2154
+ await writeFile(manifestPath, `${JSON.stringify({
2155
+ ...manifestPayload,
2156
+ manifest_digest: manifestDigest,
2157
+ }, null, 2)}\n`, "utf8");
2158
+ files.push(manifestRelPath);
2037
2159
  return {
2038
2160
  directory: repoRelativePath(repoRoot, snapshotDir),
2039
2161
  files: files.sort(),
2162
+ integrity: {
2163
+ schema_version: 1,
2164
+ manifest_path: manifestRelPath,
2165
+ manifest_digest: manifestDigest,
2166
+ check_digest: manifestPayload.check_digest,
2167
+ files: fileDigests,
2168
+ },
2040
2169
  };
2041
2170
  }
2042
2171
  async function workplanPlan(repoRoot, request, args) {
@@ -2057,16 +2186,18 @@ function socialWorkplanSequence(plan) {
2057
2186
  }
2058
2187
  return workplan.sequence;
2059
2188
  }
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) : []);
2189
+ async function requireNextWorkplanSurfaceInit(repoRoot, request, sequence, answers, omitted = [], requestedAs) {
2190
+ const progress = await verifiedWorkplanProgress(repoRoot, request, sequence);
2191
+ const completedIds = new Set(progress.completed.map((item) => item.surface));
2063
2192
  const nextSurface = sequence.find((surface) => !completedIds.has(surface.id));
2064
2193
  const selectedSurface = resolveWorkplanSurfaceAnswer(sequence, answers.feature_surface);
2194
+ const selectedOmittedSurface = resolveOmittedSurfaceAnswer(omitted, answers.feature_surface);
2065
2195
  if (!nextSurface) {
2066
2196
  return {
2067
2197
  status: "workplan-complete",
2068
2198
  exitCode: 7,
2069
2199
  request,
2200
+ ...(requestedAs && requestedAs !== request ? { requestedAs } : {}),
2070
2201
  sequence: sequence.map((surface) => ({
2071
2202
  id: surface.id,
2072
2203
  order: surface.order,
@@ -2077,11 +2208,48 @@ async function requireNextWorkplanSurfaceInit(repoRoot, request, sequence, answe
2077
2208
  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
2209
  };
2079
2210
  }
2211
+ if (selectedOmittedSurface) {
2212
+ return {
2213
+ status: "needs-workplan-order",
2214
+ exitCode: 7,
2215
+ request,
2216
+ ...(requestedAs && requestedAs !== request ? { requestedAs } : {}),
2217
+ attemptedSurface: {
2218
+ id: selectedOmittedSurface.id,
2219
+ outcome: selectedOmittedSurface.outcome,
2220
+ label: selectedOmittedSurface.label,
2221
+ trimmed: true,
2222
+ reason: selectedOmittedSurface.reason,
2223
+ },
2224
+ expectedSurface: nextSurface
2225
+ ? {
2226
+ id: nextSurface.id,
2227
+ order: nextSurface.order,
2228
+ outcome: nextSurface.outcome,
2229
+ label: nextSurface.label,
2230
+ }
2231
+ : undefined,
2232
+ omitted: omitted.map((item) => ({ id: item.id, outcome: item.outcome, reason: item.reason })),
2233
+ sequence: sequence.map((surface) => ({
2234
+ id: surface.id,
2235
+ order: surface.order,
2236
+ outcome: surface.outcome,
2237
+ label: surface.label,
2238
+ completed: completedIds.has(surface.id),
2239
+ })),
2240
+ nextStep: `Surface "${selectedOmittedSurface.id}" was deliberately trimmed from the signed blueprint (${selectedOmittedSurface.reason}). ` +
2241
+ (nextSurface
2242
+ ? `Continue the active blueprint with "${nextSurface.id}" using: ${nextSurface.initCommand}. `
2243
+ : "All active blueprint surfaces are already recorded complete. ") +
2244
+ "To build the trimmed surface, re-select an experience that includes it via `vise creative` and collect a fresh blueprint sign-off.",
2245
+ };
2246
+ }
2080
2247
  if (!answers.feature_surface || !selectedSurface) {
2081
2248
  return {
2082
2249
  status: "needs-workplan-surface",
2083
2250
  exitCode: 7,
2084
2251
  request,
2252
+ ...(requestedAs && requestedAs !== request ? { requestedAs } : {}),
2085
2253
  expectedSurface: {
2086
2254
  id: nextSurface.id,
2087
2255
  order: nextSurface.order,
@@ -2104,6 +2272,7 @@ async function requireNextWorkplanSurfaceInit(repoRoot, request, sequence, answe
2104
2272
  status: "needs-workplan-order",
2105
2273
  exitCode: 7,
2106
2274
  request,
2275
+ ...(requestedAs && requestedAs !== request ? { requestedAs } : {}),
2107
2276
  attemptedSurface: {
2108
2277
  id: selectedSurface.id,
2109
2278
  order: selectedSurface.order,
@@ -2137,6 +2306,150 @@ function resolveWorkplanSurfaceAnswer(sequence, answer) {
2137
2306
  ?? sequence.find((surface) => surface.label.toLowerCase() === normalized)
2138
2307
  ?? sequence.find((surface) => surface.label.toLowerCase().includes(normalized));
2139
2308
  }
2309
+ function resolveOmittedSurfaceAnswer(omitted, answer) {
2310
+ const normalized = (answer ?? "").trim().toLowerCase();
2311
+ if (!normalized)
2312
+ return undefined;
2313
+ return omitted.find((surface) => surface.id.toLowerCase() === normalized)
2314
+ ?? omitted.find((surface) => surface.label?.toLowerCase() === normalized)
2315
+ ?? omitted.find((surface) => surface.label?.toLowerCase().includes(normalized));
2316
+ }
2317
+ async function verifiedWorkplanProgress(repoRoot, request, sequence) {
2318
+ const raw = await readWorkplanProgress(repoRoot);
2319
+ if (!raw || raw.request !== request) {
2320
+ return { raw, completed: [], ignored: [] };
2321
+ }
2322
+ const surfacesById = new Map(sequence.map((surface) => [surface.id, surface]));
2323
+ const completed = [];
2324
+ const ignored = [];
2325
+ let liveOutcome;
2326
+ let liveCompletable = false;
2327
+ const currentScopeOutcome = await readSidecarOutcome(repoRoot);
2328
+ const hasCompletedForCurrentScope = !!currentScopeOutcome && (raw.completed ?? []).some((item) => surfacesById.get(item.surface)?.outcome === currentScopeOutcome);
2329
+ if (hasCompletedForCurrentScope) {
2330
+ try {
2331
+ const live = await checkCompliance(repoRoot);
2332
+ liveOutcome = live.outcome;
2333
+ liveCompletable = isCompletableCheckStatus(live.status);
2334
+ }
2335
+ catch {
2336
+ liveOutcome = undefined;
2337
+ }
2338
+ }
2339
+ for (const item of raw.completed ?? []) {
2340
+ const surface = surfacesById.get(item.surface);
2341
+ if (!surface) {
2342
+ ignored.push({ surface: item.surface, reason: "surface is not in the active signed blueprint" });
2343
+ continue;
2344
+ }
2345
+ const check = item.evidence?.check;
2346
+ if (!check || !isCompletableCheckStatus(check.status) || check.outcome !== surface.outcome) {
2347
+ ignored.push({ surface: item.surface, reason: "progress entry is missing a green (or proof-waived) check summary for this surface/outcome" });
2348
+ continue;
2349
+ }
2350
+ const snapshotDir = item.evidence?.snapshot?.directory;
2351
+ if (typeof snapshotDir !== "string" || snapshotDir.length === 0) {
2352
+ ignored.push({ surface: item.surface, reason: "progress entry is missing a workplan snapshot directory" });
2353
+ continue;
2354
+ }
2355
+ const snapshotPath = path.resolve(repoRoot, snapshotDir, "check.json");
2356
+ if (!pathInside(repoRoot, snapshotPath)) {
2357
+ ignored.push({ surface: item.surface, reason: "workplan snapshot path points outside the repository" });
2358
+ continue;
2359
+ }
2360
+ const integrity = item.evidence?.snapshot?.integrity;
2361
+ if (!integrity || integrity.schema_version !== 1 || typeof integrity.manifest_path !== "string" || typeof integrity.manifest_digest !== "string") {
2362
+ ignored.push({ surface: item.surface, reason: "workplan snapshot is missing an integrity manifest; rerun workplan complete after a green check" });
2363
+ continue;
2364
+ }
2365
+ const manifestPath = path.resolve(repoRoot, integrity.manifest_path);
2366
+ if (!pathInside(repoRoot, manifestPath) || !pathInside(path.resolve(repoRoot, snapshotDir), manifestPath)) {
2367
+ ignored.push({ surface: item.surface, reason: "workplan snapshot integrity manifest path is outside the snapshot directory" });
2368
+ continue;
2369
+ }
2370
+ let manifest;
2371
+ try {
2372
+ manifest = JSON.parse(await readFile(manifestPath, "utf8"));
2373
+ }
2374
+ catch {
2375
+ ignored.push({ surface: item.surface, reason: "workplan snapshot integrity manifest is missing or unreadable" });
2376
+ continue;
2377
+ }
2378
+ const { manifest_digest: manifestDigest, ...manifestPayload } = manifest;
2379
+ if (manifest.schema_version !== 1 ||
2380
+ manifest.generated_by !== "vise workplan complete" ||
2381
+ manifest.surface !== item.surface ||
2382
+ manifest.outcome !== surface.outcome ||
2383
+ manifest.check_digest !== integrity.check_digest ||
2384
+ manifestDigest !== integrity.manifest_digest ||
2385
+ digestJson(manifestPayload) !== integrity.manifest_digest) {
2386
+ ignored.push({ surface: item.surface, reason: "workplan snapshot integrity manifest does not match this surface/outcome" });
2387
+ continue;
2388
+ }
2389
+ const manifestFiles = Array.isArray(manifest.files) ? manifest.files : [];
2390
+ const manifestFileDigests = manifestFiles
2391
+ .map((entry) => entry && typeof entry === "object" && typeof entry.path === "string" && typeof entry.sha256 === "string"
2392
+ ? { path: entry.path, sha256: entry.sha256 }
2393
+ : null)
2394
+ .filter((entry) => entry != null);
2395
+ if (manifestFileDigests.length !== manifestFiles.length || manifestFileDigests.length === 0) {
2396
+ ignored.push({ surface: item.surface, reason: "workplan snapshot integrity manifest has no valid file digests" });
2397
+ continue;
2398
+ }
2399
+ const expectedFileDigest = digestJson(manifestFileDigests);
2400
+ const recordedFileDigest = digestJson(integrity.files ?? []);
2401
+ if (expectedFileDigest !== recordedFileDigest) {
2402
+ ignored.push({ surface: item.surface, reason: "workplan snapshot integrity files differ from the recorded progress manifest" });
2403
+ continue;
2404
+ }
2405
+ let fileDigestMismatch = null;
2406
+ for (const fileDigest of manifestFileDigests) {
2407
+ const filePath = path.resolve(repoRoot, fileDigest.path);
2408
+ if (!pathInside(repoRoot, filePath) || !pathInside(path.resolve(repoRoot, snapshotDir), filePath)) {
2409
+ fileDigestMismatch = `${fileDigest.path} is outside the snapshot directory`;
2410
+ break;
2411
+ }
2412
+ try {
2413
+ const actual = await digestFile(filePath);
2414
+ if (actual !== fileDigest.sha256) {
2415
+ fileDigestMismatch = `${fileDigest.path} changed`;
2416
+ break;
2417
+ }
2418
+ }
2419
+ catch {
2420
+ fileDigestMismatch = `${fileDigest.path} is missing`;
2421
+ break;
2422
+ }
2423
+ }
2424
+ if (fileDigestMismatch) {
2425
+ ignored.push({ surface: item.surface, reason: `workplan snapshot integrity mismatch: ${fileDigestMismatch}` });
2426
+ continue;
2427
+ }
2428
+ let snapshot;
2429
+ try {
2430
+ snapshot = JSON.parse(await readFile(snapshotPath, "utf8"));
2431
+ }
2432
+ catch {
2433
+ ignored.push({ surface: item.surface, reason: "workplan snapshot check.json is missing or unreadable" });
2434
+ continue;
2435
+ }
2436
+ if (!snapshot.check || digestJson(snapshot.check) !== integrity.check_digest) {
2437
+ ignored.push({ surface: item.surface, reason: "workplan snapshot check digest does not match the recorded integrity manifest" });
2438
+ continue;
2439
+ }
2440
+ const snapshotExitOk = snapshot.check?.status === "runtime-proof-waived" || snapshot.check?.exitCode === 0;
2441
+ if (snapshot.surface !== item.surface || !isCompletableCheckStatus(snapshot.check?.status) || snapshot.check?.outcome !== surface.outcome || !snapshotExitOk) {
2442
+ ignored.push({ surface: item.surface, reason: "workplan snapshot check.json is not a green (or proof-waived) check for this surface/outcome" });
2443
+ continue;
2444
+ }
2445
+ if (liveOutcome && item.outcome === liveOutcome && !liveCompletable) {
2446
+ ignored.push({ surface: item.surface, reason: "live `vise check` for this surface's current scope is no longer green/proof-waived — the recorded completion is stale or was not produced by a real check" });
2447
+ continue;
2448
+ }
2449
+ completed.push(item);
2450
+ }
2451
+ return { raw, completed, ignored };
2452
+ }
2140
2453
  async function readWorkplanProgress(repoRoot) {
2141
2454
  try {
2142
2455
  return JSON.parse(await readFile(workplanProgressPath(repoRoot), "utf8"));
@@ -2145,6 +2458,15 @@ async function readWorkplanProgress(repoRoot) {
2145
2458
  return null;
2146
2459
  }
2147
2460
  }
2461
+ async function readSidecarOutcome(repoRoot) {
2462
+ try {
2463
+ const parsed = JSON.parse(await readFile(path.join(sidecarPath(repoRoot), "compliance.json"), "utf8"));
2464
+ return typeof parsed.outcome === "string" ? parsed.outcome : undefined;
2465
+ }
2466
+ catch {
2467
+ return undefined;
2468
+ }
2469
+ }
2148
2470
  async function writeWorkplanProgress(repoRoot, progress) {
2149
2471
  const filePath = workplanProgressPath(repoRoot);
2150
2472
  await mkdir(path.dirname(filePath), { recursive: true });
@@ -2160,13 +2482,49 @@ function workplanSurfaceSnapshotDir(repoRoot, surfaceId) {
2160
2482
  }
2161
2483
  return path.join(sidecarPath(repoRoot), "workplan-snapshots", safeSurfaceId);
2162
2484
  }
2485
+ function workplanExitCode(payload) {
2486
+ if (typeof payload.exitCode === "number") {
2487
+ return payload.exitCode;
2488
+ }
2489
+ const status = typeof payload.status === "string" ? payload.status : "";
2490
+ return status === "needs-blueprint-confirmation" || status === "needs-workplan-surface" || status === "needs-workplan-order" || status === "blocked"
2491
+ ? 7
2492
+ : 0;
2493
+ }
2163
2494
  function repoRelativePath(repoRoot, filePath) {
2164
2495
  return path.relative(repoRoot, filePath).split(path.sep).join("/");
2165
2496
  }
2497
+ function pathInside(root, candidate) {
2498
+ const relative = path.relative(root, candidate);
2499
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
2500
+ }
2166
2501
  function sequenceIndex(sequence, surfaceId) {
2167
2502
  const index = sequence.findIndex((surface) => surface.id === surfaceId);
2168
2503
  return index >= 0 ? index : Number.MAX_SAFE_INTEGER;
2169
2504
  }
2505
+ function replaceFlagValue(args, flag, value) {
2506
+ const out = [];
2507
+ let replaced = false;
2508
+ for (let i = 0; i < args.length; i++) {
2509
+ const arg = args[i];
2510
+ if (arg === `--${flag}`) {
2511
+ out.push(arg, value);
2512
+ i += 1;
2513
+ replaced = true;
2514
+ continue;
2515
+ }
2516
+ if (arg.startsWith(`--${flag}=`)) {
2517
+ out.push(`--${flag}=${value}`);
2518
+ replaced = true;
2519
+ continue;
2520
+ }
2521
+ out.push(arg);
2522
+ }
2523
+ if (!replaced) {
2524
+ out.push(`--${flag}`, value);
2525
+ }
2526
+ return out;
2527
+ }
2170
2528
  function shellQuoteForCommand(value) {
2171
2529
  return `'${value.replace(/'/g, "'\\''")}'`;
2172
2530
  }
@@ -2179,6 +2537,7 @@ function ciCheckResult(result) {
2179
2537
  exitCode: result.exitCode,
2180
2538
  blockingResultStatuses: [
2181
2539
  "contract-drift",
2540
+ "no-platform",
2182
2541
  "blocked",
2183
2542
  "deterministic-failures",
2184
2543
  "needs-attestation",