@amityco/social-plus-vise 1.4.5 → 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/CHANGELOG.md +33 -9
- package/README.md +65 -21
- package/dist/capabilities.js +1 -1
- package/dist/flow.js +2 -2
- package/dist/humanFormat.js +2 -0
- package/dist/outcomes.js +24 -10
- package/dist/server.js +421 -62
- package/dist/tools/compliance.js +318 -12
- package/dist/tools/debug.js +16 -1
- package/dist/tools/design.js +47 -16
- package/dist/tools/integration.js +40 -7
- package/dist/tools/project.js +52 -10
- package/dist/tools/smoke.js +169 -1
- package/docs/vise-how-it-helps.html +2187 -0
- package/package.json +3 -2
- package/rules/feed.yaml +37 -15
- package/rules/live-data.yaml +10 -5
- package/rules/sdk-lifecycle.yaml +23 -0
- package/skills/social-plus-vise/SKILL.md +33 -13
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
const omitted = await readOmittedSurfaces(repoRoot,
|
|
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
|
|
1556
|
+
async function socialWorkplanSequenceFromFlow(repoRoot, request) {
|
|
1521
1557
|
const state = await readFlowState(repoRoot);
|
|
1522
|
-
if (state
|
|
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
|
-
|
|
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
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
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,
|
|
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(
|
|
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
|
|
1860
|
-
const completedIds = new Set(progress
|
|
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
|
|
1901
|
-
const
|
|
1902
|
-
const
|
|
1903
|
-
const
|
|
1904
|
-
|
|
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 !==
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
2062
|
-
const completedIds = new Set(progress
|
|
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",
|