@amityco/social-plus-vise 1.6.0 → 1.8.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/CHANGELOG.md CHANGED
@@ -4,6 +4,50 @@ All notable changes to `@amityco/social-plus-vise` are documented in this file.
4
4
 
5
5
  The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## 1.8.0 — 2026-07-10
8
+
9
+ ### Added
10
+ - **Complete-scan receipts and gates.** Project inspection and platform validators now report bounded scan coverage. A truncated inventory cannot initialize a compliance sidecar, select sensors, or produce a green `vise check`; large monorepos must select the concrete app surface.
11
+ - **Pre-launch release contracts.** Hosted CI now smokes the declared Node 20 minimum, release metadata/docs are regression-tested, and tag publishing uses npm trusted publishing with OIDC, a modern npm CLI, main-ancestry validation, and no long-lived token fallback.
12
+ - **Critical regression coverage.** New tests lock scan truncation, mixed-platform inventory detection, strict sensor selection, bounded command output, descendant cleanup on timeout, block mount proof, and the release workflow contract.
13
+
14
+ ### Changed
15
+ - **CI covers finished work by default.** `vise check --ci` now runs the active gate plus every recorded engagement re-verdict. A previously completed surface that drifted exits `11` even when the active engagement is green; explicit `--all-engagements` remains valid with `--ci`.
16
+ - **Block capabilities require a live mount.** The blocks sidecar is reset to `2026-07-10.vise-blocks-sidecar.v2`. `blocks add --apply` records `pending-mount`; `vise check` counts `providesCapabilities` only while the dependency, touched files, package import, and actual block-symbol usage remain present. `blocks validate` persists `verified` or `invalid` and exits non-zero on failure.
17
+ - **Explicit sensor selection is strict.** A requested `--include` name that does not match a detected sensor returns `invalid-selection` and exits non-zero. Sensor stdout/stderr is bounded, and timeouts terminate the command process group rather than leaving descendants running.
18
+ - **Node.js 20 is the minimum supported runtime.** Package metadata, `vise doctor`, release documentation, and hosted compatibility proof now agree.
19
+ - **The compatibility corpus is reset to the 1.8.0 launch contract.** Experimental pre-launch sidecars are not grandfathered; future published versions must remain backward-readable from this baseline.
20
+
21
+ ### Fixed
22
+ - **Ducati dogfood false positives.** Built-in `{ text }` shorthand payloads no longer look like undeclared custom post types, and comment modules that carry a bare `postId` no longer look like post cards missing their body.
23
+ - **Engagement-aware evidence hygiene.** Matching attestations retained by completed engagement contracts are no longer reported as generic stale files. Off-contract attestations with changed source fingerprints are identified explicitly.
24
+ - **Flutter hosted block dependencies.** Hosted constraints now render as `social_plus_blocks: ^0.1.0`; only local sources render a nested `path:` dependency.
25
+ - **Fixture-symmetry parsing.** The supplementary symmetry gate parses only `failingFixturesByFinding`, so later no-fire maps cannot overwrite the failing fixture associated with a rule.
26
+
27
+ ### Compatibility
28
+ - This is an intentional pre-launch contract hardening release. The stricter scan, sensor, block-mount, Node, CI, and sidecar behaviors differ from the experimental 1.7.0 line, but Vise and Blocks have not launched and have no customer contract to migrate. The release version remains 1.8.0 as requested; the new sidecar schema keeps its independent `.v2` schema identifier.
29
+
30
+ ## 1.7.0 — 2026-07-09
31
+
32
+ ### Added
33
+ - **Day-2 governance: adding features to an app that already uses social.plus is now a first-class, governed flow.**
34
+ - **Brownfield adoption honesty.** `vise init` on a repo with pre-existing non-green findings surfaces a `brownfield` hint: record `vise baseline .` before writing code, then gate with `vise check --new-only`. A baseline excludes pre-existing *rule findings only* — the requested outcome's completeness checklist and selected capabilities are never baselined, so a baselined repo can't go green without the feature actually built. Baseline files recorded by older versions that carried deliverable entries are ignored and disclosed (`ignored_legacy_deliverable_entries`).
35
+ - **Engagement ledger.** Initializing a different outcome records the outgoing engagement's final check, contract, and intake to `sp-vise/engagements/<outcome>/` (latest per outcome) and discloses the switch in the init result. `vise status` lists superseded engagements under `previousEngagements`. Replacing a **green** engagement asks for an explicit `--supersede-engagement`.
36
+ - **Re-verification of finished work.** `vise check --engagement <outcome>` re-verdicts one recorded engagement against the current rules and code (exit `11` on drift, `0` when it holds; `ruleset_upgraded` disclosed). `vise check --all-engagements` runs the normal gate plus re-verdicts of every recorded engagement — active gate non-green keeps its own exit code; active green with a drifted recorded engagement exits `11` (`engagement-drift`). Findings already gating the active engagement are counted once there (`shared_with_active`).
37
+ - **Attestations are bound to their engagement.** `vise attest` stamps the active outcome; after an outcome switch, a rule-global attestation re-gates (`stale_engagement_attestation`) until re-verified — file-scoped attestations survive the switch because new files re-gate them anyway. Attestations recorded by older versions keep their historical behavior.
38
+ - **Completed journeys don't dead-end new requests.** After a finished multi-surface journey, `vise init` for a genuinely new outcome now proceeds (recording the completed journey to the ledger) instead of stopping at `workplan-complete`; re-initializing a surface the journey already completed still gets the honest stop.
39
+ - New skill section — "Adding Features To An App That Already Uses social.plus" — teaches agents both variants (hand-rolled integration and prior governed engagement).
40
+ - **SDK facts: broader capability coverage and deeper model grounding.** `vise sdk-facts` (now listed in the public `vise --help`) gains capability anchors for `community`, `chat`, `livestream` (Room API only), `notification-tray`, and `events` on TypeScript, plus per-platform anchor sets with Flutter coverage for six families (`comments`, `reactions`, `posts`, `users`, `community`, `chat`) — a partially anchored platform's result names its coverage explicitly. Field-level model schemas grew from 6 to 14 TypeScript models (`Amity.Community`, `Amity.Channel`, `Amity.Message`, `Amity.SubChannel`, `Amity.Room`, `Amity.NotificationTrayItem`, `Amity.NotificationTraySeen`, `Amity.Event`), all extraction-grounded with real field names and types.
41
+ - **Development-workspace guard.** Customer-integration commands now recognize the Vise / social.plus blocks development workspace itself: `vise init` refuses with `status: "internal-workspace"` (exit `10`, `--allow-internal-workspace` to override for harness runs), and `inspect`/`plan` attach an advisory warning — so the projectless `vise sdk-facts` persona and the customer persona can't be mixed up silently.
42
+
43
+ ### Fixed
44
+ - **Blocks installer on native projects.** `vise blocks plan`/`add`/`validate` pointed at a project without a package manifest (an iOS or Android app) now return the graceful `needs-review` plan with unsupported-platform stop conditions instead of crashing on a missing `package.json`.
45
+
46
+ ### Compatibility
47
+ - **Additive exit codes.** `10` (`internal-workspace`, init-only) and `11` (`engagement-drift`, only under the new `--engagement`/`--all-engagements` flags). The existing 0–9 exit surface and precedence are unchanged; default `vise check` behavior is unchanged, so pipelines gating on `--ci` or `exitCode === 0` are unaffected.
48
+ - **One intentional init behavior change.** `vise init` replacing a **green** engagement with a different outcome now requires `--supersede-engagement` (exit `7` otherwise); red or in-progress engagements switch as before, and the outgoing engagement's final state is recorded to the ledger either way.
49
+ - **Sidecar additions are additive.** `sp-vise/engagements/` and the new result fields (`brownfield`, `engagement_switch`, `previousEngagements`, `stale_engagement_attestation`) are new; no existing file changes shape. Legacy baselines and attestations continue to parse — with the honesty adjustments disclosed above.
50
+
7
51
  ## 1.6.0 — 2026-07-06
8
52
 
9
53
  ### Added
package/README.md CHANGED
@@ -206,7 +206,7 @@ Run `vise <command> --help` for full flags. JSON output is the default for agent
206
206
  | `vise plan [path] --request "..." [--summary]` | Grounded implementation plan with intake questions and docs citations; `--summary` prints a compact route/intake view |
207
207
  | `vise plan --summary "..."` | Shortcut for quick routing or discovery when the current directory is the repo |
208
208
  | `vise plan-harness [path] --request "..."` | Pre-planning step: build the harness around the request |
209
- | `vise init [path] --request "..." [--answer key=value]` | Write the `sp-vise/` compliance contract once blocking intake is answered; exits 7 (`needs-clarification`) otherwise |
209
+ | `vise init [path] --request "..." [--answer key=value]` | Write the `sp-vise/` compliance contract once blocking intake is answered; exits 7 (`needs-clarification`) otherwise. Refuses to initialize the Vise / social.plus blocks development workspace itself (`status: "internal-workspace"`, exit 10 — use `vise sdk-facts` there, or `--allow-internal-workspace` for an intentional harness run). Replacing a **green** engagement with a different outcome requires `--supersede-engagement` (exit 7 otherwise); the outgoing engagement's final state is recorded to `sp-vise/engagements/` either way |
210
210
  | `vise workplan next [path] --request "..."` | For broad social requests: print the next uncompleted surface and its focused commands |
211
211
  | `vise workplan status [path] --request "..."` | Show the workplan sequence and completed surfaces; ignores unverifiable or drifted snapshot evidence. If the in-scope surface's live `vise check` is already green, auto-records that one surface (same evidence bar as `workplan complete`) so a working, check-green surface isn't left uncounted |
212
212
  | `vise workplan complete [path] --request "..." --surface <id>` | Record a green-checked surface; snapshots evidence plus an integrity manifest under `sp-vise/workplan-snapshots/<surface>/` |
@@ -255,11 +255,13 @@ Everything in this group is local and advisory: no uploads, no `vise check` exit
255
255
  | `vise check [path]` | Re-validate against the recorded contract: `green`, `needs-attestation`, `deterministic-failures`, `blocked`, `contract-drift`, `completeness-gap`, `selected-capability-failures`, `no-platform` (no SDK platform detected — reported as a failure, not a vacuous green), or `runtime-proof-waived` |
256
256
  | `vise check [path] --ci` | Read-only variant that exits non-zero unless green (for CI) |
257
257
  | `vise check [path] --allow-proof-waiver` | Accept an honest `runtime-proof-waived` result (from `vise smoke waive`) as passing (exit 0) instead of blocking; without it a waiver blocks CI by default |
258
- | `vise check [path] --new-only` | **Brownfield gate.** With a recorded baseline, gate only on findings introduced *since* the baseline (pre-existing ones are reported but excluded). Default `check` always gates on everything |
259
- | `vise baseline [path]` | Snapshot the current pre-existing findings to `sp-vise/baseline.json` so `check --new-only` can separate legacy debt from new gaps. `vise init --baseline` records it at init time |
258
+ | `vise check [path] --new-only` | **Brownfield gate.** With a recorded baseline, gate only on rule findings introduced *since* the baseline (pre-existing ones are reported but excluded). The outcome's completeness checklist and selected optional capabilities are **never** excluded — they are the engagement's deliverables and gate like a default check. Default `check` always gates on everything |
259
+ | `vise check [path] --engagement <outcome>` | **Re-verdict one recorded engagement** (from `sp-vise/engagements/`) against the current ruleset and code. Exit `11` on drift, `0` when it still holds; never the active engagement's codes. `ruleset_upgraded` discloses when the installed ruleset moved since it was recorded |
260
+ | `vise check [path] --all-engagements` | Run the normal active gate **plus** re-verdicts of every recorded engagement. Active gate non-green → its normal exit code; active green but a recorded engagement drifted → exit `11` (`engagement-drift`). Findings already gating the active engagement are counted once there (`shared_with_active`) |
261
+ | `vise baseline [path]` | Snapshot the current pre-existing rule findings to `sp-vise/baseline.json` so `check --new-only` can separate legacy debt from new gaps. Deliverables (completeness/selected capabilities) are never baselined. Record it right after `vise init`, before writing code — a mid-build baseline would swallow your own findings. `vise init --baseline` records it at init time |
260
262
  | `vise validate [path]` | Run the deterministic validators only (no attestation comparison) |
261
263
  | `vise sync [path]` | Persist deterministic-pass evidence to `sp-vise/attestations/` |
262
- | `vise attest [path] --rule <id> --signer host-agent --confidence high --evidence-file evidence.json --rationale "..."` | Record an attestation when a rule passes through architecture the deterministic check can't see |
264
+ | `vise attest [path] --rule <id> --signer host-agent --confidence high --evidence-file evidence.json --rationale "..."` | Record an attestation when a rule passes through architecture the deterministic check can't see. Attestations are bound to the engagement outcome they were reviewed under: after `vise init` switches the sidecar to a different outcome, a rule-global attestation re-gates (`stale_engagement_attestation`) until re-attested — file-scoped attestations survive the switch because new files re-gate them anyway |
263
265
  | `vise explain <ruleId>` | Print a rule's rationale, evidence requirements, and remediation |
264
266
  | `vise status [path]` | Summarize the current compliance state |
265
267
 
@@ -330,6 +332,9 @@ jobs:
330
332
  | `4` | Contract drift — recorded rules no longer match the current ruleset |
331
333
  | `5` | Baseline capability neither implemented nor opted-out (add it or place `// vise: scope-omit <id> — <reason>`) |
332
334
  | `6` | An explicitly selected optional capability failed its source sensors |
335
+ | `8` | No SDK platform detected (`no-platform`) — reported as a failure, never a vacuous green |
336
+ | `9` | Runtime proof waived (`runtime-proof-waived`) — accept explicitly with `--allow-proof-waiver` |
337
+ | `11` | Engagement drift (`--engagement` / `--all-engagements` only): the active gate is green but a recorded engagement re-verdicts non-green — a previously finished surface no longer holds |
333
338
 
334
339
  ## Compliance Contract
335
340
 
@@ -341,6 +346,7 @@ Vise writes local planning, compliance, design, and evidence artifacts under `sp
341
346
  | `sp-vise/intake.json` | `vise init` | Request, outcome, intake answers, design-review status |
342
347
  | `sp-vise/inspection.json` | `vise init` | Platform, surface, and design signals detected at init |
343
348
  | `sp-vise/attestations/*.json` | `vise sync` / `vise attest` | Per-rule evidence: signer, confidence, rationale, source fingerprints for drift detection |
349
+ | `sp-vise/engagements/<outcome>/*.json` | `vise init` (on an outcome switch) | Engagement ledger: the superseded engagement's final check, contract, and intake (latest per outcome) — surfaced by `vise status` as `previousEngagements` |
344
350
  | `sp-vise/creative-brief.json` + `creative-brief.md` | `vise creative` | Advisory brief: goals, archetypes, candidate variants (JSON + human-readable) |
345
351
  | `sp-vise/candidate-ranking-preview.json` | `vise creative --ranking-preview` | Opt-in local ranking preview for review context; `experience_score: null`, no uploads, no default-order change, no top-1 confidence claim |
346
352
  | `sp-vise/creative-selection.json` | `vise creative accept` | Accepted variant and plan/workplan feed-forward context |
package/dist/flow.js CHANGED
@@ -2,6 +2,12 @@ import { createHash } from "node:crypto";
2
2
  import { mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { sidecarDir } from "./sidecar.js";
5
+ export function completedWorkplanBlocksOutcome(stages, requestedOutcome) {
6
+ if (!requestedOutcome || requestedOutcome === "unknown") {
7
+ return true;
8
+ }
9
+ return stages.some((stage) => stage.outcome === requestedOutcome);
10
+ }
5
11
  export function blueprintDigest(input) {
6
12
  const canonical = JSON.stringify({
7
13
  path: input.path ?? null,
package/dist/server.js CHANGED
@@ -5,18 +5,19 @@ import os from "node:os";
5
5
  import path from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { sidecarDir as sidecarPath } from "./sidecar.js";
8
- import { blueprintDigest, readFlowState, readOmittedSurfaces, renderFlowBlueprint, runtimeReadinessForStages, writeFlowBlueprintHtml, writeFlowState, } from "./flow.js";
8
+ import { blueprintDigest, completedWorkplanBlocksOutcome, readFlowState, readOmittedSurfaces, renderFlowBlueprint, runtimeReadinessForStages, writeFlowBlueprintHtml, writeFlowState, } from "./flow.js";
9
9
  import { normalizeSolutionPathAnswer } from "./solutionPath.js";
10
10
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
11
11
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12
12
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
13
- import { attestRule, attestRuleTool, checkCompliance, checkComplianceTool, explainRule, explainRuleTool, experienceReportTool, initCompliance, initComplianceTool, recordBaseline, initEngagement, initEngagementTool, listRules, showEngagement, showEngagementTool, statusCompliance, syncCompliance, syncComplianceTool, } from "./tools/compliance.js";
13
+ import { attestRule, attestRuleTool, checkAllEngagements, checkCompliance, checkEngagementReVerdict, checkComplianceTool, explainRule, explainRuleTool, experienceReportTool, initCompliance, initComplianceTool, recordBaseline, initEngagement, initEngagementTool, listRules, showEngagement, showEngagementTool, statusCompliance, syncCompliance, syncComplianceTool, } from "./tools/compliance.js";
14
14
  import { designCheckTool, designContrastTool, designExtractTool, designInitTokensTool, designPreviewTool, designReferenceTool } from "./tools/design.js";
15
15
  import { getDocPageTool, searchDocsTool } from "./tools/docs.js";
16
16
  import { compileExperienceTool } from "./tools/experienceCompiler.js";
17
17
  import { experienceSensorsTool } from "./tools/experienceSensors.js";
18
18
  import { planHarnessTool } from "./tools/harness.js";
19
19
  import { planIntegrationTool } from "./tools/integration.js";
20
+ import { detectInternalWorkspace, internalWorkspaceWarning } from "./tools/internalWorkspace.js";
20
21
  import { recordLearningTool, showLearningTool } from "./tools/learning.js";
21
22
  import { inspectProjectTool, validateSetupTool } from "./tools/project.js";
22
23
  import { resolveRequestTool, suggestPatchTool } from "./tools/resolve.js";
@@ -153,10 +154,17 @@ async function handleCli(args) {
153
154
  return "exit";
154
155
  }
155
156
  if (command === "inspect") {
156
- await printToolResult(inspectProjectTool, {
157
- repoPath: positionalRepoPath(args.slice(1)),
157
+ const inspectRepo = positionalRepoPath(args.slice(1));
158
+ const inspectResult = await inspectProjectTool.call({
159
+ repoPath: inspectRepo,
158
160
  surfacePath: flagValue(args, "surface") ?? flagValue(args, "surface-path"),
159
161
  });
162
+ const inspectPayload = JSON.parse(inspectResult.content.map((item) => item.text).join("\n"));
163
+ const inspectWorkspace = await detectInternalWorkspace(inspectRepo);
164
+ if (inspectWorkspace.isInternalWorkspace) {
165
+ inspectPayload.internalWorkspaceWarning = internalWorkspaceWarning(inspectWorkspace);
166
+ }
167
+ console.log(JSON.stringify(inspectPayload, null, 2));
160
168
  return "exit";
161
169
  }
162
170
  if (command === "creative") {
@@ -300,6 +308,10 @@ async function handleCli(args) {
300
308
  const result = await planIntegrationTool.call(input);
301
309
  const text = result.content.map((item) => item.text).join("\n");
302
310
  const payload = JSON.parse(text);
311
+ const planWorkspace = await detectInternalWorkspace(positionalRepoPath(args.slice(1)));
312
+ if (planWorkspace.isInternalWorkspace) {
313
+ payload.internalWorkspaceWarning = internalWorkspaceWarning(planWorkspace);
314
+ }
303
315
  emitResult("plan", hasFlag(args, "summary") ? planSummary(payload) : payload, args);
304
316
  return "exit";
305
317
  }
@@ -357,7 +369,7 @@ async function handleCli(args) {
357
369
  surfacePath: flagValue(args, "surface") ?? flagValue(args, "surface-path"),
358
370
  });
359
371
  const status = toolResultStatus(printed);
360
- if (status === "failed" || status === "timed-out") {
372
+ if (status === "failed" || status === "timed-out" || status === "invalid-selection" || status === "incomplete-analysis") {
361
373
  process.exitCode = 1;
362
374
  }
363
375
  return "exit";
@@ -449,12 +461,16 @@ async function handleCli(args) {
449
461
  if (sub === "validate") {
450
462
  assertOnlyKnownFlags(subArgs, ["block", "surface", "surface-path", "registry", "format"], "blocks validate");
451
463
  assertJsonFormat(subArgs, "blocks validate");
452
- console.log(JSON.stringify(await validateBlockInstall({
464
+ const validation = await validateBlockInstall({
453
465
  repoPath: positionalRepoPath(subArgs),
454
466
  blockId: flagValue(subArgs, "block"),
455
467
  registryPath: requiredFlagValue(subArgs, "registry", "blocks validate requires --registry."),
456
468
  surfacePath: flagValue(subArgs, "surface") ?? flagValue(subArgs, "surface-path"),
457
- }), null, 2));
469
+ });
470
+ console.log(JSON.stringify(validation, null, 2));
471
+ if (validation.validationStatus === "failed" || validation.status === "needs-review") {
472
+ process.exitCode = 1;
473
+ }
458
474
  return "exit";
459
475
  }
460
476
  console.error(`Unknown blocks subcommand: ${sub ?? "(none)"}. Expected "list", "plan", "add", or "validate".`);
@@ -462,9 +478,19 @@ async function handleCli(args) {
462
478
  return "exit";
463
479
  }
464
480
  if (command === "init") {
465
- assertOnlyKnownFlags(args, ["request", "surface", "surface-path", "answer", "allow-unresolved-intake", "baseline"], "init");
481
+ assertOnlyKnownFlags(args, ["request", "surface", "surface-path", "answer", "allow-unresolved-intake", "baseline", "allow-internal-workspace", "supersede-engagement"], "init");
466
482
  const initRepo = positionalRepoPath(args.slice(1));
467
483
  const initRequest = requiredFlagValue(args, "request", "init requires --request.");
484
+ const initWorkspace = await detectInternalWorkspace(initRepo);
485
+ if (initWorkspace.isInternalWorkspace && !hasFlag(args, "allow-internal-workspace")) {
486
+ console.log(JSON.stringify({
487
+ status: "internal-workspace",
488
+ ...internalWorkspaceWarning(initWorkspace),
489
+ instruction: "Refusing to initialize the compliance sidecar here. If this really is intentional (e.g. a harness run at the workspace root), re-run with --allow-internal-workspace.",
490
+ }, null, 2));
491
+ process.exitCode = 10;
492
+ return "exit";
493
+ }
468
494
  const initWorkplanPlan = await workplanPlan(initRepo, initRequest, args);
469
495
  const { sequence: initSequence, omitted: initOmitted, canonicalRequest: initCanonicalRequest } = await activeBlueprint(initRepo, initRequest, initWorkplanPlan);
470
496
  if (initSequence.length >= 2 && initWorkplanPlan.platform !== "unknown") {
@@ -474,15 +500,16 @@ async function handleCli(args) {
474
500
  process.exitCode = 7;
475
501
  return "exit";
476
502
  }
477
- const workplanInitGate = await requireNextWorkplanSurfaceInit(initRepo, initCanonicalRequest, initSequence, keyValueFlag(args, "answer"), initOmitted, initRequest);
503
+ const workplanInitGate = await requireNextWorkplanSurfaceInit(initRepo, initCanonicalRequest, initSequence, keyValueFlag(args, "answer"), initOmitted, initRequest, initWorkplanPlan.outcome);
478
504
  if (workplanInitGate) {
479
505
  console.log(JSON.stringify(workplanInitGate, null, 2));
480
506
  process.exitCode = 7;
481
507
  return "exit";
482
508
  }
483
509
  }
484
- const result = await initCompliance(initRepo, initRequest, flagValue(args, "surface") ?? flagValue(args, "surface-path"), keyValueFlag(args, "answer"), { allowUnresolvedIntake: hasFlag(args, "allow-unresolved-intake") });
485
- if (result.status === "needs-clarification" && typeof result.exitCode === "number") {
510
+ const result = await initCompliance(initRepo, initRequest, flagValue(args, "surface") ?? flagValue(args, "surface-path"), keyValueFlag(args, "answer"), { allowUnresolvedIntake: hasFlag(args, "allow-unresolved-intake"), baselinePlanned: hasFlag(args, "baseline"), supersedeEngagement: hasFlag(args, "supersede-engagement") });
511
+ if ((result.status === "needs-clarification" || result.status === "needs-engagement-supersede-confirmation") &&
512
+ typeof result.exitCode === "number") {
486
513
  process.exitCode = result.exitCode;
487
514
  }
488
515
  if (hasFlag(args, "baseline") && result.status === "initialized") {
@@ -492,9 +519,26 @@ async function handleCli(args) {
492
519
  return "exit";
493
520
  }
494
521
  if (command === "check") {
495
- assertOnlyKnownFlags(args, ["ci", "format", "new-only", "allow-proof-waiver"], "check");
522
+ assertOnlyKnownFlags(args, ["ci", "format", "new-only", "allow-proof-waiver", "engagement", "all-engagements"], "check");
523
+ const engagementOutcome = flagValue(args, "engagement");
524
+ if (engagementOutcome) {
525
+ if (hasFlag(args, "new-only") || hasFlag(args, "ci") || hasFlag(args, "all-engagements")) {
526
+ throw new Error("--engagement re-verdicts one recorded engagement and cannot combine with --new-only, --ci, or --all-engagements.");
527
+ }
528
+ const reVerdict = await checkEngagementReVerdict(positionalRepoPath(args.slice(1)), engagementOutcome);
529
+ emitResult("check", reVerdict, args);
530
+ process.exitCode = typeof reVerdict.exitCode === "number" ? reVerdict.exitCode : 0;
531
+ return "exit";
532
+ }
533
+ const ciMode = hasFlag(args, "ci");
534
+ if (hasFlag(args, "all-engagements") || ciMode) {
535
+ const combined = await checkAllEngagements(positionalRepoPath(args.slice(1)), { newOnly: hasFlag(args, "new-only"), allowProofWaiver: hasFlag(args, "allow-proof-waiver") });
536
+ emitResult("check", ciMode ? ciCheckResult(combined) : combined, args);
537
+ process.exitCode = typeof combined.exitCode === "number" ? combined.exitCode : 0;
538
+ return "exit";
539
+ }
496
540
  const result = await checkCompliance(positionalRepoPath(args.slice(1)), { newOnly: hasFlag(args, "new-only"), allowProofWaiver: hasFlag(args, "allow-proof-waiver") });
497
- emitResult("check", hasFlag(args, "ci") ? ciCheckResult(result) : result, args);
541
+ emitResult("check", result, args);
498
542
  process.exitCode = result.exitCode;
499
543
  return "exit";
500
544
  }
@@ -657,7 +701,6 @@ function internalCommandsSection() {
657
701
  return `
658
702
 
659
703
  Internal commands (not part of the public surface — see docs/TOOLS.md):
660
- vise sdk-facts --platform <p> [--capability <c>] Read bundled SDK surface facts. Projectless, read-only.
661
704
  vise blocks <list|plan|add> ... Install and validate social.plus blocks in customer projects.
662
705
  vise resolve --request "..." Resolve a request into the closest supported Vise outcome.
663
706
  vise skill-path Print the bundled skill source path and install targets.
@@ -943,7 +986,9 @@ Usage:
943
986
  vise blocks validate <repoPath> [--block comments] --registry <path> --format json
944
987
 
945
988
  Safety:
946
- Dry-run is the default. Apply mode only edits package manifests, explicit source anchors, and sp-vise/blocks.json.`;
989
+ Dry-run is the default. Apply mode only edits package manifests, explicit source anchors, and sp-vise/blocks.json.
990
+ Apply records pending-mount. A block capability counts only after the imported symbol is mounted/used;
991
+ run blocks validate after mounting it to persist verified evidence.`;
947
992
  }
948
993
  if (command === "init") {
949
994
  return `${packageName} init
@@ -960,6 +1005,7 @@ Usage:
960
1005
  return `${packageName} check
961
1006
 
962
1007
  Check the current source and recorded attestations against the compliance contract. Read-only.
1008
+ --ci also re-verdicts every recorded engagement and exits 11 if completed work drifted.
963
1009
 
964
1010
  Usage:
965
1011
  vise check [repoPath] [--ci] [--format human]
@@ -1105,6 +1151,7 @@ Usage:
1105
1151
  vise learning show [repoPath] Show local learning summary
1106
1152
  vise debug [repoPath] --error ... Debug an SDK-specific runtime error and emit a repair brief
1107
1153
  vise explore "<request>" Discover what social.plus offers for a request (no project/credentials needed)
1154
+ vise sdk-facts --platform <p> Read proven SDK surface facts (symbols, capabilities, model schemas). Projectless, read-only
1108
1155
  vise plan [repoPath] --request "..." Create an implementation plan
1109
1156
  vise workplan next [repoPath] --request "..." Get the next broad-social surface to implement
1110
1157
  vise init [repoPath] --request "..." Initialize compliance sidecar (add --baseline on a brownfield app)
@@ -2186,13 +2233,16 @@ function socialWorkplanSequence(plan) {
2186
2233
  }
2187
2234
  return workplan.sequence;
2188
2235
  }
2189
- async function requireNextWorkplanSurfaceInit(repoRoot, request, sequence, answers, omitted = [], requestedAs) {
2236
+ async function requireNextWorkplanSurfaceInit(repoRoot, request, sequence, answers, omitted = [], requestedAs, requestedOutcome) {
2190
2237
  const progress = await verifiedWorkplanProgress(repoRoot, request, sequence);
2191
2238
  const completedIds = new Set(progress.completed.map((item) => item.surface));
2192
2239
  const nextSurface = sequence.find((surface) => !completedIds.has(surface.id));
2193
2240
  const selectedSurface = resolveWorkplanSurfaceAnswer(sequence, answers.feature_surface);
2194
2241
  const selectedOmittedSurface = resolveOmittedSurfaceAnswer(omitted, answers.feature_surface);
2195
2242
  if (!nextSurface) {
2243
+ if (!completedWorkplanBlocksOutcome(sequence, requestedOutcome)) {
2244
+ return null;
2245
+ }
2196
2246
  return {
2197
2247
  status: "workplan-complete",
2198
2248
  exitCode: 7,
@@ -2543,8 +2593,11 @@ function ciCheckResult(result) {
2543
2593
  "needs-attestation",
2544
2594
  "completeness-gap",
2545
2595
  "selected-capability-failures",
2596
+ "engagement-drift",
2546
2597
  ],
2547
- message: result.exitCode === 0 ? "Compliance green for CI." : `Compliance failed for CI with status: ${result.status}.`,
2598
+ message: result.exitCode === 0
2599
+ ? "Active and recorded engagement compliance is green for CI."
2600
+ : `Compliance failed for CI with status: ${result.status}.`,
2548
2601
  },
2549
2602
  };
2550
2603
  }
@@ -2746,7 +2799,7 @@ function doctorResult() {
2746
2799
  packageRoot,
2747
2800
  status: nodeMajor >= 18 ? "ok" : "unsupported-node",
2748
2801
  node: process.versions.node,
2749
- requiredNodeMajor: ">=18",
2802
+ requiredNodeMajor: ">=20",
2750
2803
  docsSource: process.env.SOCIAL_PLUS_DOCS_ROOT ? "local" : "hosted",
2751
2804
  docsRoot: process.env.SOCIAL_PLUS_DOCS_ROOT,
2752
2805
  docsBaseUrl: process.env.SOCIAL_PLUS_DOCS_BASE_URL ?? "https://learn.social.plus",
@@ -5,7 +5,7 @@ import { packageVersion } from "../version.js";
5
5
  import { inspectProject } from "./project.js";
6
6
  import { detectCommandSensors } from "./harness.js";
7
7
  import { readDesignContract } from "./design.js";
8
- const blocksSidecarSchemaVersion = "2026-06-10.vise-blocks-sidecar.v1";
8
+ const blocksSidecarSchemaVersion = "2026-07-10.vise-blocks-sidecar.v2";
9
9
  const registryPlatformByVisePlatform = {
10
10
  typescript: "react",
11
11
  "react-native": "react-native",
@@ -74,11 +74,21 @@ export async function validateBlockInstall(options) {
74
74
  if (installed.length === 0) {
75
75
  findings.push({
76
76
  ruleId: "blocks.sidecar.installed",
77
- severity: "warning",
77
+ severity: "error",
78
78
  message: "No installed block sidecar entries exist.",
79
79
  recommendation: "Run `vise blocks add <repoPath> --block <id> --registry <path> --apply` after reviewing the dry-run plan.",
80
80
  });
81
81
  }
82
+ for (const entry of installed) {
83
+ if (!(await installedEntryLocallyValid(repoPath, entry))) {
84
+ findings.push({
85
+ ruleId: "blocks.install.verified",
86
+ severity: "error",
87
+ message: `Installed block ${entry.blockId} is not currently verified: its dependency, touched files, or live mount evidence is missing.`,
88
+ recommendation: `Run \`vise blocks validate <repoPath> --block ${entry.blockId} --registry <path>\` and resolve every reported error.`,
89
+ });
90
+ }
91
+ }
82
92
  return {
83
93
  status: findings.some((finding) => finding.severity === "error") ? "needs-review" : "ready",
84
94
  mode: "validate",
@@ -96,7 +106,7 @@ export async function validateBlockInstall(options) {
96
106
  if (installed.length === 0) {
97
107
  findings.push({
98
108
  ruleId: "blocks.sidecar.installed",
99
- severity: "warning",
109
+ severity: "error",
100
110
  message: options.blockId ? `No sidecar entry exists for block ${options.blockId}.` : "No installed block sidecar entries exist.",
101
111
  recommendation: "Run `vise blocks add <repoPath> --block <id> --registry <path> --apply` after reviewing the dry-run plan.",
102
112
  });
@@ -119,12 +129,30 @@ export async function validateBlockInstall(options) {
119
129
  });
120
130
  }
121
131
  }
132
+ const mountEvidence = await findBlockMountEvidence(repoPath, {
133
+ platform: plan.registryPlatform,
134
+ publicEntrypoint: plan.package.publicEntrypoint,
135
+ importName: plan.package.importName,
136
+ mountFiles: plan.targetFiles.map((targetFile) => targetFile.path),
137
+ });
138
+ if (!mountEvidence) {
139
+ findings.push({
140
+ ruleId: "blocks.mount.present",
141
+ severity: "error",
142
+ message: `Block ${plan.block.id} is installed but ${plan.package.importName} is not mounted in the target source.`,
143
+ recommendation: `Render or invoke ${plan.package.importName} in ${plan.targetFiles.map((targetFile) => targetFile.path).join(", ")}, then re-run block validation. An import alone does not deliver the block capability.`,
144
+ });
145
+ }
122
146
  const hasError = findings.some((finding) => finding.severity === "error");
147
+ await updateBlockValidationState(repoPath, plan.block.id, hasError ? "invalid" : "verified", mountEvidence);
148
+ const updatedSidecar = await readSidecar(repoPath);
149
+ const updatedInstalled = (updatedSidecar?.installed ?? []).filter((entry) => entry.blockId === options.blockId);
123
150
  return {
124
151
  ...plan,
125
152
  status: hasError ? "needs-review" : plan.status,
126
153
  validationStatus: hasError ? "failed" : "passed",
127
- sidecarInstalled: installed,
154
+ mountEvidence,
155
+ sidecarInstalled: updatedInstalled,
128
156
  findings,
129
157
  };
130
158
  }
@@ -242,7 +270,7 @@ async function packageChangeFor(root, platform, packageInfo, packageSource) {
242
270
  };
243
271
  }
244
272
  const file = "package.json";
245
- const packageJson = await readPackageJson(path.join(root, file));
273
+ const packageJson = await readPackageJson(path.join(root, file)).catch(() => ({}));
246
274
  return {
247
275
  file,
248
276
  dependency: packageInfo.dependencyName,
@@ -319,10 +347,13 @@ async function writeBlocksSidecar(repoPath, plan, packageSource, filesTouched) {
319
347
  packageSource,
320
348
  dependencyName: plan.package.dependencyName,
321
349
  providesCapabilities: plan.providesCapabilities,
350
+ publicEntrypoint: plan.package.publicEntrypoint,
351
+ importName: plan.package.importName,
352
+ mountFiles: plan.targetFiles.map((targetFile) => targetFile.path),
322
353
  filesTouched,
323
354
  designContractDigest: designContract?.digest,
324
355
  sdkFactsVersion: plan.block.version,
325
- validationStatus: "installed",
356
+ validationStatus: "pending-mount",
326
357
  };
327
358
  sidecar.schemaVersion = blocksSidecarSchemaVersion;
328
359
  sidecar.viseVersion = packageVersion;
@@ -376,7 +407,46 @@ async function installedEntryLocallyValid(repoRoot, entry) {
376
407
  return false;
377
408
  }
378
409
  }
379
- return true;
410
+ return Boolean(await findBlockMountEvidence(repoRoot, entry));
411
+ }
412
+ async function findBlockMountEvidence(repoRoot, entry) {
413
+ if (!entry.publicEntrypoint || !entry.importName || !Array.isArray(entry.mountFiles) || entry.mountFiles.length === 0) {
414
+ return undefined;
415
+ }
416
+ for (const relativeFile of entry.mountFiles) {
417
+ if (typeof relativeFile !== "string" || relativeFile.trim() === "") {
418
+ continue;
419
+ }
420
+ const source = await readFile(path.join(repoRoot, relativeFile), "utf8").catch(() => "");
421
+ if (!source.includes(entry.publicEntrypoint) || !blockUsagePresent(source, entry.platform, entry.importName)) {
422
+ continue;
423
+ }
424
+ return { file: relativeFile, kind: "source-usage", importName: entry.importName };
425
+ }
426
+ return undefined;
427
+ }
428
+ function blockUsagePresent(source, platform, importName) {
429
+ const name = escapeRegExp(importName);
430
+ if (platform === "flutter") {
431
+ return new RegExp(`\\b${name}\\s*\\(`).test(source);
432
+ }
433
+ return [
434
+ new RegExp(`<\\s*${name}(?:\\s|/|>)`),
435
+ new RegExp(`(?:React\\.)?createElement\\s*\\(\\s*${name}\\b`),
436
+ new RegExp(`\\b${name}\\s*\\(`),
437
+ ].some((pattern) => pattern.test(source));
438
+ }
439
+ async function updateBlockValidationState(repoRoot, blockId, validationStatus, mountEvidence) {
440
+ const sidecar = await readSidecar(repoRoot);
441
+ const entry = sidecar?.installed.find((item) => item.blockId === blockId);
442
+ if (!sidecar || !entry) {
443
+ return;
444
+ }
445
+ entry.validationStatus = validationStatus;
446
+ entry.mountEvidence = mountEvidence;
447
+ sidecar.viseVersion = packageVersion;
448
+ sidecar.generatedAt = new Date().toISOString();
449
+ await writeFile(path.join(repoRoot, "sp-vise", "blocks.json"), `${JSON.stringify(sidecar, null, 2)}\n`, "utf8");
380
450
  }
381
451
  async function blockDependencyDeclared(repoRoot, entry) {
382
452
  const dependencyName = entry.dependencyName;
@@ -433,12 +503,15 @@ function insertBlockSnippet(source, plan) {
433
503
  return source;
434
504
  }
435
505
  function addPubspecDependency(source, dependencyName, value) {
506
+ const dependencyLines = value.startsWith("file:")
507
+ ? [` ${dependencyName}:`, ` path: ${value.replace(/^file:/, "")}`]
508
+ : [` ${dependencyName}: ${value}`];
436
509
  if (!source.includes("dependencies:")) {
437
- return `${source.trimEnd()}\n\ndependencies:\n ${dependencyName}:\n path: ${value.replace(/^file:/, "")}\n`;
510
+ return `${source.trimEnd()}\n\ndependencies:\n${dependencyLines.join("\n")}\n`;
438
511
  }
439
512
  const lines = source.split(/\r?\n/);
440
513
  const insertIndex = lines.findIndex((line) => line.trim() === "dependencies:") + 1;
441
- lines.splice(insertIndex, 0, ` ${dependencyName}:`, ` path: ${value.replace(/^file:/, "")}`);
514
+ lines.splice(insertIndex, 0, ...dependencyLines);
442
515
  return `${lines.join("\n").trimEnd()}\n`;
443
516
  }
444
517
  async function readPackageJson(filePath) {