@riddledc/riddle-proof 0.7.201 → 0.7.202

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/README.md CHANGED
@@ -202,6 +202,31 @@ generic inline-script warning threshold. Use `--strict=true` when you
202
202
  deliberately want Riddle's non-critical script-safety warnings to block the run.
203
203
  Critical script-safety violations remain blocked by Riddle either way.
204
204
 
205
+ Use `--viewport-name <name>` to run only one named viewport from a
206
+ multi-viewport profile while preserving viewport-scoped setup actions and
207
+ checks:
208
+
209
+ ```sh
210
+ riddle-proof-loop run-profile \
211
+ --profile .riddle-proof/profiles/pricing.json \
212
+ --url https://example.com \
213
+ --viewport-name ipad-mini \
214
+ --output artifacts/riddle-proof/pricing-ipad-mini
215
+ ```
216
+
217
+ When `--output` / `--output-dir` is set, hosted profile runs write
218
+ `riddle-job.json` as soon as Riddle returns a job id. If the local process is
219
+ interrupted or pruned, recover the profile artifacts from the hosted job:
220
+
221
+ ```sh
222
+ riddle-proof-loop run-profile recover \
223
+ --profile .riddle-proof/profiles/pricing.json \
224
+ --url https://example.com \
225
+ --job job_abc123 \
226
+ --viewport-name ipad-mini \
227
+ --output artifacts/riddle-proof/pricing-ipad-mini-recovered
228
+ ```
229
+
205
230
  When promoting proof artifacts into a durable public profile, avoid guessing
206
231
  which backend or runner tokens are preserved inside `proof.json`. Derive the
207
232
  `body_contains` fragments from the artifact body first:
package/dist/cli.cjs CHANGED
@@ -16223,7 +16223,8 @@ function usage() {
16223
16223
  " riddle-proof-loop respond --state-path <path> --response-json <file|json|->",
16224
16224
  " riddle-proof-loop respond --state-path <path> --decision <decision> --summary <text> [--payload-json <file|json|->]",
16225
16225
  " riddle-proof-loop status --state-path <path>",
16226
- " riddle-proof-loop run-profile --profile <file|json|-> --url <base-url> [--runner riddle] [--strict true|false; default false] [--split-viewports true|false; default false] [--poll-attempts n] [--output <dir>|--output-dir <dir>] [--result-format json|compact-json|summary|none; default json] [--quiet]",
16226
+ " riddle-proof-loop run-profile --profile <file|json|-> --url <base-url> [--runner riddle] [--viewport-name <name[,name...]>] [--strict true|false; default false] [--split-viewports true|false; default false] [--poll-attempts n] [--output <dir>|--output-dir <dir>] [--result-format json|compact-json|summary|none; default json] [--quiet]",
16227
+ " riddle-proof-loop run-profile recover --profile <file|json|-> --url <base-url> --job <job-id> [--viewport-name <name[,name...]>] [--output <dir>|--output-dir <dir>] [--result-format json|compact-json|summary|none; default json]",
16227
16228
  " riddle-proof-loop profile-body-assertions --artifact <file|url|-> --candidates-json <file|json|-> [--required-json <file|json|->] [--format json|body-contains]",
16228
16229
  " riddle-proof-loop profile-http-status-preflight --profile <file|json|-> --url <base-url> [--format json|summary]",
16229
16230
  " riddle-proof-loop riddle-preview-deploy <build-dir> <label> [--framework spa|static]",
@@ -16283,6 +16284,11 @@ function runProfileStrictOption(options) {
16283
16284
  function runProfileSplitViewportsOption(options) {
16284
16285
  return optionBoolean(options, "splitViewports") ?? false;
16285
16286
  }
16287
+ function runProfileViewportNamesOption(options) {
16288
+ const raw = optionString(options, "viewportName") ?? optionString(options, "viewportNames");
16289
+ if (!raw) return [];
16290
+ return raw.split(",").map((part) => part.trim()).filter(Boolean);
16291
+ }
16286
16292
  var DEFAULT_PROFILE_UNSUBMITTED_RETRY_TIMEOUT_MS = 9e4;
16287
16293
  var DEFAULT_PROFILE_UNSUBMITTED_RETRIES = 2;
16288
16294
  function optionNumber(options, ...keys) {
@@ -16883,6 +16889,61 @@ function profileHasRouteExitAffordanceReceipt(receipts) {
16883
16889
  return routeFields.some((name) => setupReturnSummaryValue(receipt, [name]) !== void 0) || haystack.includes("route=") || haystack.includes("browserpath=");
16884
16890
  });
16885
16891
  }
16892
+ function profileCleanupLabelMatches(value) {
16893
+ if (!value) return false;
16894
+ return /\b(cleanup|clean|clear|reset|undo|discard|new)\b/i.test(value);
16895
+ }
16896
+ function profileHasCleanupBoundaryAffordanceReceipt(receipts) {
16897
+ const visibleFields = [
16898
+ "cleanupControlVisible",
16899
+ "cleanupVisible",
16900
+ "clearControlVisible",
16901
+ "resetControlVisible",
16902
+ "undoVisible",
16903
+ "discardVisible",
16904
+ "newControlVisible",
16905
+ "exitControlVisible"
16906
+ ];
16907
+ const textFields = [
16908
+ "cleanupControlText",
16909
+ "cleanupText",
16910
+ "clearControlText",
16911
+ "resetControlText",
16912
+ "undoText",
16913
+ "discardText",
16914
+ "newControlText",
16915
+ "exitControlText",
16916
+ "controlText",
16917
+ "affordanceText"
16918
+ ];
16919
+ return receipts.some((receipt) => {
16920
+ const storedTo = cliString(receipt.return_stored_to) || "";
16921
+ const label = cliString(receipt.label) || "";
16922
+ const path7 = cliString(receipt.path) || cliString(receipt.function_name) || "";
16923
+ const summary = cliReturnSummaryLabel(receipt.return_summary) || "";
16924
+ const haystack = `${storedTo} ${label} ${path7} ${summary}`.toLowerCase();
16925
+ const mentionsCleanupBoundary = haystack.includes("cleanup") || haystack.includes("precleanup") || haystack.includes("pre-cleanup") || haystack.includes("boundary") || haystack.includes("undo") || haystack.includes("clear") || haystack.includes("reset") || haystack.includes("discard");
16926
+ const visibleControl = visibleFields.some((name) => setupReturnSummaryValue(receipt, [name]) === true);
16927
+ const controlText = textFields.map((name) => cliString(setupReturnSummaryValue(receipt, [name]))).find((value) => profileCleanupLabelMatches(value));
16928
+ return mentionsCleanupBoundary && (visibleControl || Boolean(controlText));
16929
+ });
16930
+ }
16931
+ function profileVisibleCleanupActionCount(setupViewports) {
16932
+ const keys = /* @__PURE__ */ new Set();
16933
+ const clickedReceipts = setupViewports.flatMap((viewport) => [
16934
+ ...setupReceiptArray(viewport, "clicked"),
16935
+ ...setupReceiptArray(viewport, "tap"),
16936
+ ...setupReceiptArray(viewport, "tap_until")
16937
+ ]);
16938
+ clickedReceipts.forEach((receipt, index) => {
16939
+ if (receipt.ok === false) return;
16940
+ const text = cliString(receipt.text) || cliString(receipt.label) || cliString(receipt.target) || cliString(receipt.selector);
16941
+ if (!profileCleanupLabelMatches(text)) return;
16942
+ const ordinal = cliFiniteNumber(receipt.ordinal);
16943
+ keys.add(ordinal === void 0 ? `idx:${index}:${text}` : `ord:${ordinal}:${text}`);
16944
+ });
16945
+ return keys.size;
16946
+ }
16886
16947
  function profileHasOfflineAudioMetricsReceipt(receipts) {
16887
16948
  const metricFields = [
16888
16949
  "mixPeak",
@@ -16984,7 +17045,7 @@ function profileHasRecoveredStateReceipt(receipts) {
16984
17045
  const path7 = cliString(receipt.path) || cliString(receipt.function_name) || "";
16985
17046
  const summary = cliReturnSummaryLabel(receipt.return_summary) || "";
16986
17047
  const haystack = `${storedTo} ${label} ${path7} ${summary}`.toLowerCase();
16987
- const labelsRecovery = haystack.includes("recover") || haystack.includes("repaired") || haystack.includes("repair") || haystack.includes("try fix") || haystack.includes("tryfix") || haystack.includes("after-fix") || haystack.includes("fixed");
17048
+ const labelsRecovery = haystack.includes("recover") || haystack.includes("repaired") || haystack.includes("repair") || haystack.includes("restart") || haystack.includes("play again") || haystack.includes("playagain") || haystack.includes("play-again") || haystack.includes("try fix") || haystack.includes("tryfix") || haystack.includes("after-fix") || haystack.includes("fixed");
16988
17049
  if (!labelsRecovery) return false;
16989
17050
  const status = profileLowerSummaryValue(receipt, ["status", "state", "phase"]);
16990
17051
  const outcome = profileLowerSummaryValue(receipt, ["lastOutcome", "outcome", "result"]);
@@ -16995,6 +17056,38 @@ function profileHasRecoveredStateReceipt(receipts) {
16995
17056
  return hasRecoveredState || success || hasValid && hasInvalid === false;
16996
17057
  });
16997
17058
  }
17059
+ function profileMetadataHasGeneratedOutputContract(metadata) {
17060
+ const contract = cliRecord(metadata.declared_state_contract);
17061
+ if (!contract) return false;
17062
+ const keys = Object.keys(contract).join(" ").toLowerCase();
17063
+ const values = Object.values(contract).map((value) => cliString(value)?.toLowerCase() || "").join(" ");
17064
+ const haystack = `${keys} ${values}`;
17065
+ return haystack.includes("generated_output") || haystack.includes("generated output") || haystack.includes("output-size") || haystack.includes("output size") || haystack.includes("output result") || haystack.includes("optimizer size");
17066
+ }
17067
+ function profileHasGeneratedOutputReceipt(receipts) {
17068
+ let outputReady = false;
17069
+ let outputChanged = false;
17070
+ for (const receipt of receipts) {
17071
+ const storedTo = cliString(receipt.return_stored_to) || "";
17072
+ const label = cliString(receipt.label) || "";
17073
+ const path7 = cliString(receipt.path) || cliString(receipt.function_name) || "";
17074
+ const summary = cliReturnSummaryLabel(receipt.return_summary) || "";
17075
+ const haystack = `${storedTo} ${label} ${path7} ${summary}`.toLowerCase();
17076
+ const readySignal = setupReturnSummaryValue(receipt, ["outputReady", "outputStillReady"]) === true || cliFiniteNumber(setupReturnSummaryValue(receipt, ["surfaceCount", "before.surfaceCount", "after.surfaceCount"])) !== void 0 || setupReturnSummaryValue(receipt, ["size", "before.size", "after.size", "size.text", "before.size.text", "after.size.text"]) !== void 0;
17077
+ if (readySignal && (haystack.includes("output") || haystack.includes("size") || haystack.includes("result"))) {
17078
+ outputReady = true;
17079
+ }
17080
+ const beforeBytes = cliFiniteNumber(setupReturnSummaryValue(receipt, ["before.size.outputBytes", "before.outputBytes", "beforeBytes"]));
17081
+ const afterBytes = cliFiniteNumber(setupReturnSummaryValue(receipt, ["after.size.outputBytes", "after.outputBytes", "afterBytes"]));
17082
+ const beforeText = cliString(setupReturnSummaryValue(receipt, ["before.size.text", "before.outputText", "beforeText"]));
17083
+ const afterText = cliString(setupReturnSummaryValue(receipt, ["after.size.text", "after.outputText", "afterText"]));
17084
+ const explicitChange = setupReturnSummaryValue(receipt, ["sizeChanged", "outputChanged", "resultChanged"]) === true;
17085
+ const byteChange = beforeBytes !== void 0 && afterBytes !== void 0 && beforeBytes !== afterBytes;
17086
+ const textChange = Boolean(beforeText && afterText && beforeText !== afterText);
17087
+ if (explicitChange || byteChange || textChange) outputChanged = true;
17088
+ }
17089
+ return outputReady && outputChanged;
17090
+ }
16998
17091
  function profilePackReceiptStatus(result, metadata, receipt) {
16999
17092
  const text = receipt.toLowerCase();
17000
17093
  const setupSummary = profileSetupSummaryRecord(result);
@@ -17025,6 +17118,7 @@ function profilePackReceiptStatus(result, metadata, receipt) {
17025
17118
  const clickFallbackTapCount = clickFallbackTapKeys.size;
17026
17119
  const tapUntilCount = profileSetupReceiptTotal(setupViewports, "tap_until");
17027
17120
  const visibleUiActionCount = clickCount + profileSetupReceiptTotal(setupViewports, "tap") + tapUntilCount;
17121
+ const visibleCleanupActionCount = profileVisibleCleanupActionCount(setupViewports);
17028
17122
  const setupFailureCount = profileSetupFailureCount(setupViewports);
17029
17123
  const setupObstructionCount = profileSetupObstructionCount(setupViewports);
17030
17124
  const inputDispatchCount = profileSetupReceiptTotal(setupViewports, "drag") + profileSetupReceiptTotal(setupViewports, "tap") + tapUntilCount + profileSetupReceiptTotal(setupViewports, "press") + profileSetupReceiptTotal(setupViewports, "keyboard_sequence");
@@ -17060,6 +17154,7 @@ function profilePackReceiptStatus(result, metadata, receipt) {
17060
17154
  const hasTextAbsence = profileHasPassedCheck(result, ["text_absent", "selector_text_absent"]);
17061
17155
  const hasMeasuredStateChange = hasNaturalInput || hasCanvasChange || valueReceipts.some((item) => setupReturnSummaryValue(item, ["changed"]) === true || setupReturnSummaryValue(item, ["nonWhiteDelta", "darkDelta", "pixelDelta", "movementDelta"]) !== void 0);
17062
17156
  const hasRouteExitAffordanceReceipt = profileHasRouteExitAffordanceReceipt(valueReceipts);
17157
+ const hasCleanupBoundaryAffordanceReceipt = profileHasCleanupBoundaryAffordanceReceipt(valueReceipts);
17063
17158
  const hasOfflineAudioMetricsReceipt = profileHasOfflineAudioMetricsReceipt(valueReceipts);
17064
17159
  const hasActiveRouteLocalProofReceipt = profileHasActiveRouteLocalProofReceipt(valueReceipts);
17065
17160
  const hasTerminalLossReceipt = profileHasTerminalLossReceipt(valueReceipts);
@@ -17068,6 +17163,8 @@ function profilePackReceiptStatus(result, metadata, receipt) {
17068
17163
  const hasControlledSuccessLaunchReceipt = profileHasControlledLaunchReceipt(valueReceipts, "success");
17069
17164
  const hasRouteContinuationReceipt = profileHasRouteContinuationReceipt(valueReceipts);
17070
17165
  const hasRecoveredStateReceipt = profileHasRecoveredStateReceipt(valueReceipts);
17166
+ const hasGeneratedOutputContract = profileMetadataHasGeneratedOutputContract(metadata);
17167
+ const hasGeneratedOutputReceipt = profileHasGeneratedOutputReceipt(valueReceipts);
17071
17168
  const failedCleanupInventoryReason = profileFailedCleanupInventoryReason(setupViewports);
17072
17169
  const passedCleanupInventoryReason = profilePassedCleanupInventoryReason(setupViewports);
17073
17170
  if (text.includes("artifact link") || text.includes("artifact path")) {
@@ -17121,6 +17218,13 @@ function profilePackReceiptStatus(result, metadata, receipt) {
17121
17218
  }
17122
17219
  return profileReceiptSignalStatus(hasTextAbsence, "absence check passed", "absence check missing");
17123
17220
  }
17221
+ if (text.includes("generated-output") || text.includes("generated output") || text.includes("output-size") || text.includes("output size") || (text.includes("output") || text.includes("result")) && (text.includes("mutation") || text.includes("final"))) {
17222
+ return profileReceiptSignalStatus(
17223
+ hasGeneratedOutputContract && hasGeneratedOutputReceipt,
17224
+ "generated-output mutation receipt present",
17225
+ "generated-output mutation receipt missing"
17226
+ );
17227
+ }
17124
17228
  if (text.includes("recovered") || text.includes("final state")) {
17125
17229
  return profileReceiptSignalStatus(hasStateContract || hasTextVisibility, "final state receipt present", "final state receipt missing");
17126
17230
  }
@@ -17177,6 +17281,13 @@ function profilePackReceiptStatus(result, metadata, receipt) {
17177
17281
  "route continuation receipt missing"
17178
17282
  );
17179
17283
  }
17284
+ if (text.includes("cleanup") && text.includes("action") && (text.includes("visible ui") || text.includes("visible"))) {
17285
+ return profileReceiptSignalStatus(
17286
+ visibleCleanupActionCount > 0,
17287
+ `visible cleanup action receipt present (${visibleCleanupActionCount})`,
17288
+ "visible cleanup action receipt missing"
17289
+ );
17290
+ }
17180
17291
  if (text.includes("through visible ui") || text.includes("visible ui action") || text.includes("ui-routed") || text.includes("ui routed") || text.includes("visible") && text.includes("route") && text.includes("exit") && text.includes("action") || text.includes("visible") && text.includes("mode") && text.includes("exit") && text.includes("action")) {
17181
17292
  return profileReceiptSignalStatus(
17182
17293
  visibleUiActionCount > 0,
@@ -17191,6 +17302,13 @@ function profilePackReceiptStatus(result, metadata, receipt) {
17191
17302
  "affordance receipt missing"
17192
17303
  );
17193
17304
  }
17305
+ if (text.includes("cleanup") && (text.includes("affordance") || text.includes("control") || text.includes("boundary") || text.includes("inventory"))) {
17306
+ return profileReceiptSignalStatus(
17307
+ hasCleanupBoundaryAffordanceReceipt,
17308
+ "visible cleanup affordance receipt present",
17309
+ "visible cleanup affordance receipt missing"
17310
+ );
17311
+ }
17194
17312
  if (text.includes("retry") || text.includes("repair") || text.includes("reset") || text.includes("affordance")) {
17195
17313
  return profileReceiptSignalStatus(hasStateContract || clickCount > 0, "affordance or transition receipt present", "affordance receipt missing");
17196
17314
  }
@@ -18528,6 +18646,21 @@ function writeProfileOutput(outputDir, result) {
18528
18646
  if (result.evidence?.dom_summary) (0, import_node_fs6.writeFileSync)(import_node_path6.default.join(outputDir, "dom-summary.json"), `${JSON.stringify(result.evidence.dom_summary, null, 2)}
18529
18647
  `);
18530
18648
  }
18649
+ function writeRiddleJobReceipt(outputDir, input) {
18650
+ if (!outputDir) return;
18651
+ (0, import_node_fs6.mkdirSync)(outputDir, { recursive: true });
18652
+ (0, import_node_fs6.writeFileSync)(import_node_path6.default.join(outputDir, "riddle-job.json"), `${JSON.stringify({
18653
+ version: "riddle-proof.riddle-job-receipt.v1",
18654
+ profile_name: input.profile.name,
18655
+ job_id: input.jobId,
18656
+ target_url: input.targetUrl,
18657
+ viewport: input.viewport || null,
18658
+ captured_at: (/* @__PURE__ */ new Date()).toISOString(),
18659
+ created: input.created || null,
18660
+ recovery_command: `riddle-proof-loop run-profile recover --profile <profile> --job ${input.jobId} --output-dir ${outputDir}`
18661
+ }, null, 2)}
18662
+ `);
18663
+ }
18531
18664
  async function readArtifactJson(artifact) {
18532
18665
  const target = artifact.url || artifact.path;
18533
18666
  if (!target) return void 0;
@@ -18731,6 +18864,43 @@ function profileForSplitViewport(profile, viewport) {
18731
18864
  }
18732
18865
  };
18733
18866
  }
18867
+ function profileItemAppliesToAnySelectedViewport(item, viewports) {
18868
+ if (!item.viewports?.length) return true;
18869
+ const names = new Set(viewports.map((viewport) => viewport.name).filter(Boolean));
18870
+ return item.viewports.some((name) => names.has(name));
18871
+ }
18872
+ function profileForSelectedViewports(profile, viewports) {
18873
+ const suffix = viewports.map((viewport) => viewport.name || `${viewport.width}x${viewport.height}`).join("-");
18874
+ const setupActions = profile.target.setup_actions?.filter((action) => profileItemAppliesToAnySelectedViewport(action, viewports));
18875
+ return {
18876
+ ...profile,
18877
+ name: `${profile.name}-${suffix}`,
18878
+ checks: profile.checks.filter((check) => profileItemAppliesToAnySelectedViewport(check, viewports)),
18879
+ target: {
18880
+ ...profile.target,
18881
+ viewports,
18882
+ ...setupActions ? { setup_actions: setupActions } : {}
18883
+ },
18884
+ metadata: {
18885
+ ...profile.metadata || {},
18886
+ selected_parent_profile: profile.name,
18887
+ selected_viewports: viewports.map((viewport) => viewport.name || `${viewport.width}x${viewport.height}`)
18888
+ }
18889
+ };
18890
+ }
18891
+ function profileWithSelectedViewportNamesForCli(profile, options) {
18892
+ const names = runProfileViewportNamesOption(options);
18893
+ if (!names.length) return profile;
18894
+ const requested = new Set(names);
18895
+ const viewports = profile.target.viewports.filter((viewport) => viewport.name && requested.has(viewport.name));
18896
+ const matched = new Set(viewports.map((viewport) => viewport.name).filter(Boolean));
18897
+ const missing = names.filter((name) => !matched.has(name));
18898
+ if (missing.length) {
18899
+ const available = profile.target.viewports.map((viewport) => viewport.name).filter(Boolean).join(", ") || "none";
18900
+ throw new Error(`Unknown --viewport-name ${missing.join(", ")}. Available viewport names: ${available}.`);
18901
+ }
18902
+ return profileForSelectedViewports(profile, viewports);
18903
+ }
18734
18904
  function safeProfileOutputSegment(value) {
18735
18905
  const safe = value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
18736
18906
  return safe || "viewport";
@@ -18919,6 +19089,13 @@ async function runSingleRiddleProfileForCli(profile, options, input) {
18919
19089
  const directResult = extractRiddleProofProfileResult(created);
18920
19090
  return directResult ? withRiddleMetadata(withProfileMetadata(profile, directResult), { artifacts: collectRiddleProfileArtifactRefs(created) }) : createRiddleProofProfileInsufficientResult({ profile, runner, error: "Riddle run response was missing job_id.", artifacts: collectRiddleProfileArtifactRefs(created) });
18921
19091
  }
19092
+ writeRiddleJobReceipt(input.outputDir, {
19093
+ profile,
19094
+ jobId,
19095
+ targetUrl,
19096
+ viewport: profile.target.viewports[0],
19097
+ created
19098
+ });
18922
19099
  poll = await client.pollJob(jobId, pollOptions);
18923
19100
  if (attempt < retryLimit && shouldRetryUnsubmittedRiddleJob(poll)) {
18924
19101
  const recoveredResult = await recoverProfileResultFromRiddleArtifacts(profile, {
@@ -18995,10 +19172,9 @@ async function runSplitViewportProfileForCli(profile, options, input) {
18995
19172
  const childRuns = [];
18996
19173
  for (const viewport of profile.target.viewports) {
18997
19174
  const childProfile = profileForSplitViewport(profile, viewport);
18998
- const result2 = await runSingleRiddleProfileForCli(childProfile, options, input);
18999
- if (outputDir) {
19000
- writeProfileOutput(splitViewportOutputDir(outputDir, viewport.name, seenOutputNames), result2);
19001
- }
19175
+ const childOutputDir = outputDir ? splitViewportOutputDir(outputDir, viewport.name, seenOutputNames) : void 0;
19176
+ const result2 = await runSingleRiddleProfileForCli(childProfile, options, { ...input, outputDir: childOutputDir });
19177
+ if (childOutputDir) writeProfileOutput(childOutputDir, result2);
19002
19178
  childRuns.push({ viewport, profile: childProfile, result: result2 });
19003
19179
  }
19004
19180
  const artifacts = childRuns.flatMap(splitViewportArtifactRefs);
@@ -19020,6 +19196,51 @@ async function runSplitViewportProfileForCli(profile, options, input) {
19020
19196
  });
19021
19197
  return withSplitViewportWarnings(profile, withSplitViewportChildStatusCheck(profile, result, childRuns));
19022
19198
  }
19199
+ async function recoverProfileForCli(profile, options) {
19200
+ const runner = optionString(options, "runner") || "riddle";
19201
+ if (runner !== "riddle") {
19202
+ throw new Error(`Unsupported --runner ${runner}. The current CLI supports --runner riddle.`);
19203
+ }
19204
+ const jobId = optionString(options, "job") ?? optionString(options, "jobId");
19205
+ if (!jobId) throw new Error("run-profile recover requires --job <job-id>.");
19206
+ const client = createRiddleApiClient(riddleClientConfig(options));
19207
+ let artifactPayload;
19208
+ try {
19209
+ artifactPayload = await client.requestJson(`/v1/jobs/${jobId}/artifacts`);
19210
+ } catch (error) {
19211
+ return createRiddleProofProfileEnvironmentBlockedResult({
19212
+ profile,
19213
+ runner,
19214
+ error,
19215
+ riddle: { job_id: jobId, terminal: false }
19216
+ });
19217
+ }
19218
+ const artifacts = collectRiddleProfileArtifactRefs(artifactPayload);
19219
+ const artifactStatus = riddleArtifactsPayloadStatus(artifactPayload);
19220
+ const terminal = artifactStatus ? isTerminalRiddleJobStatus(artifactStatus) : artifacts.length > 0;
19221
+ const recovered = await profileResultFromRiddleArtifacts(profile, artifacts, [artifactPayload]);
19222
+ if (recovered) {
19223
+ return withRiddleMetadata(recovered, {
19224
+ job_id: jobId,
19225
+ status: artifactStatus,
19226
+ terminal,
19227
+ artifacts,
19228
+ artifactRecovery: true
19229
+ });
19230
+ }
19231
+ return createRiddleProofProfileInsufficientResult({
19232
+ profile,
19233
+ runner,
19234
+ error: artifacts.length ? `Riddle job ${jobId} artifacts were recovered without a proof result.` : `Riddle job ${jobId} had no recoverable artifacts.`,
19235
+ riddle: {
19236
+ job_id: jobId,
19237
+ status: artifactStatus,
19238
+ terminal,
19239
+ artifact_recovery: artifacts.length > 0
19240
+ },
19241
+ artifacts
19242
+ });
19243
+ }
19023
19244
  async function runProfileForCli(profile, options) {
19024
19245
  const runner = optionString(options, "runner") || "riddle";
19025
19246
  if (runner !== "riddle") {
@@ -19029,7 +19250,7 @@ async function runProfileForCli(profile, options) {
19029
19250
  if (runProfileSplitViewportsOption(options) && profile.target.viewports.length > 1) {
19030
19251
  return runSplitViewportProfileForCli(profile, options, { client, runner });
19031
19252
  }
19032
- return runSingleRiddleProfileForCli(profile, options, { client, runner });
19253
+ return runSingleRiddleProfileForCli(profile, options, { client, runner, outputDir: profileOutputDirOption(options) });
19033
19254
  }
19034
19255
  function requestForRun(options) {
19035
19256
  const statePath = optionString(options, "statePath");
@@ -19078,8 +19299,8 @@ async function main() {
19078
19299
  return;
19079
19300
  }
19080
19301
  if (command === "run-profile") {
19081
- const profile = normalizeProfileForCli(options);
19082
- const result = await runProfileForCli(profile, options);
19302
+ const profile = profileWithSelectedViewportNamesForCli(normalizeProfileForCli(options), options);
19303
+ const result = positional[1] === "recover" ? await recoverProfileForCli(profile, options) : await runProfileForCli(profile, options);
19083
19304
  writeProfileOutput(profileOutputDirOption(options), result);
19084
19305
  const diagnosticLine = profileCliDiagnosticLine(result);
19085
19306
  if (diagnosticLine && optionBoolean(options, "quiet") !== true) {
package/dist/cli.js CHANGED
@@ -48,7 +48,8 @@ function usage() {
48
48
  " riddle-proof-loop respond --state-path <path> --response-json <file|json|->",
49
49
  " riddle-proof-loop respond --state-path <path> --decision <decision> --summary <text> [--payload-json <file|json|->]",
50
50
  " riddle-proof-loop status --state-path <path>",
51
- " riddle-proof-loop run-profile --profile <file|json|-> --url <base-url> [--runner riddle] [--strict true|false; default false] [--split-viewports true|false; default false] [--poll-attempts n] [--output <dir>|--output-dir <dir>] [--result-format json|compact-json|summary|none; default json] [--quiet]",
51
+ " riddle-proof-loop run-profile --profile <file|json|-> --url <base-url> [--runner riddle] [--viewport-name <name[,name...]>] [--strict true|false; default false] [--split-viewports true|false; default false] [--poll-attempts n] [--output <dir>|--output-dir <dir>] [--result-format json|compact-json|summary|none; default json] [--quiet]",
52
+ " riddle-proof-loop run-profile recover --profile <file|json|-> --url <base-url> --job <job-id> [--viewport-name <name[,name...]>] [--output <dir>|--output-dir <dir>] [--result-format json|compact-json|summary|none; default json]",
52
53
  " riddle-proof-loop profile-body-assertions --artifact <file|url|-> --candidates-json <file|json|-> [--required-json <file|json|->] [--format json|body-contains]",
53
54
  " riddle-proof-loop profile-http-status-preflight --profile <file|json|-> --url <base-url> [--format json|summary]",
54
55
  " riddle-proof-loop riddle-preview-deploy <build-dir> <label> [--framework spa|static]",
@@ -108,6 +109,11 @@ function runProfileStrictOption(options) {
108
109
  function runProfileSplitViewportsOption(options) {
109
110
  return optionBoolean(options, "splitViewports") ?? false;
110
111
  }
112
+ function runProfileViewportNamesOption(options) {
113
+ const raw = optionString(options, "viewportName") ?? optionString(options, "viewportNames");
114
+ if (!raw) return [];
115
+ return raw.split(",").map((part) => part.trim()).filter(Boolean);
116
+ }
111
117
  var DEFAULT_PROFILE_UNSUBMITTED_RETRY_TIMEOUT_MS = 9e4;
112
118
  var DEFAULT_PROFILE_UNSUBMITTED_RETRIES = 2;
113
119
  function optionNumber(options, ...keys) {
@@ -708,6 +714,61 @@ function profileHasRouteExitAffordanceReceipt(receipts) {
708
714
  return routeFields.some((name) => setupReturnSummaryValue(receipt, [name]) !== void 0) || haystack.includes("route=") || haystack.includes("browserpath=");
709
715
  });
710
716
  }
717
+ function profileCleanupLabelMatches(value) {
718
+ if (!value) return false;
719
+ return /\b(cleanup|clean|clear|reset|undo|discard|new)\b/i.test(value);
720
+ }
721
+ function profileHasCleanupBoundaryAffordanceReceipt(receipts) {
722
+ const visibleFields = [
723
+ "cleanupControlVisible",
724
+ "cleanupVisible",
725
+ "clearControlVisible",
726
+ "resetControlVisible",
727
+ "undoVisible",
728
+ "discardVisible",
729
+ "newControlVisible",
730
+ "exitControlVisible"
731
+ ];
732
+ const textFields = [
733
+ "cleanupControlText",
734
+ "cleanupText",
735
+ "clearControlText",
736
+ "resetControlText",
737
+ "undoText",
738
+ "discardText",
739
+ "newControlText",
740
+ "exitControlText",
741
+ "controlText",
742
+ "affordanceText"
743
+ ];
744
+ return receipts.some((receipt) => {
745
+ const storedTo = cliString(receipt.return_stored_to) || "";
746
+ const label = cliString(receipt.label) || "";
747
+ const path2 = cliString(receipt.path) || cliString(receipt.function_name) || "";
748
+ const summary = cliReturnSummaryLabel(receipt.return_summary) || "";
749
+ const haystack = `${storedTo} ${label} ${path2} ${summary}`.toLowerCase();
750
+ const mentionsCleanupBoundary = haystack.includes("cleanup") || haystack.includes("precleanup") || haystack.includes("pre-cleanup") || haystack.includes("boundary") || haystack.includes("undo") || haystack.includes("clear") || haystack.includes("reset") || haystack.includes("discard");
751
+ const visibleControl = visibleFields.some((name) => setupReturnSummaryValue(receipt, [name]) === true);
752
+ const controlText = textFields.map((name) => cliString(setupReturnSummaryValue(receipt, [name]))).find((value) => profileCleanupLabelMatches(value));
753
+ return mentionsCleanupBoundary && (visibleControl || Boolean(controlText));
754
+ });
755
+ }
756
+ function profileVisibleCleanupActionCount(setupViewports) {
757
+ const keys = /* @__PURE__ */ new Set();
758
+ const clickedReceipts = setupViewports.flatMap((viewport) => [
759
+ ...setupReceiptArray(viewport, "clicked"),
760
+ ...setupReceiptArray(viewport, "tap"),
761
+ ...setupReceiptArray(viewport, "tap_until")
762
+ ]);
763
+ clickedReceipts.forEach((receipt, index) => {
764
+ if (receipt.ok === false) return;
765
+ const text = cliString(receipt.text) || cliString(receipt.label) || cliString(receipt.target) || cliString(receipt.selector);
766
+ if (!profileCleanupLabelMatches(text)) return;
767
+ const ordinal = cliFiniteNumber(receipt.ordinal);
768
+ keys.add(ordinal === void 0 ? `idx:${index}:${text}` : `ord:${ordinal}:${text}`);
769
+ });
770
+ return keys.size;
771
+ }
711
772
  function profileHasOfflineAudioMetricsReceipt(receipts) {
712
773
  const metricFields = [
713
774
  "mixPeak",
@@ -809,7 +870,7 @@ function profileHasRecoveredStateReceipt(receipts) {
809
870
  const path2 = cliString(receipt.path) || cliString(receipt.function_name) || "";
810
871
  const summary = cliReturnSummaryLabel(receipt.return_summary) || "";
811
872
  const haystack = `${storedTo} ${label} ${path2} ${summary}`.toLowerCase();
812
- const labelsRecovery = haystack.includes("recover") || haystack.includes("repaired") || haystack.includes("repair") || haystack.includes("try fix") || haystack.includes("tryfix") || haystack.includes("after-fix") || haystack.includes("fixed");
873
+ const labelsRecovery = haystack.includes("recover") || haystack.includes("repaired") || haystack.includes("repair") || haystack.includes("restart") || haystack.includes("play again") || haystack.includes("playagain") || haystack.includes("play-again") || haystack.includes("try fix") || haystack.includes("tryfix") || haystack.includes("after-fix") || haystack.includes("fixed");
813
874
  if (!labelsRecovery) return false;
814
875
  const status = profileLowerSummaryValue(receipt, ["status", "state", "phase"]);
815
876
  const outcome = profileLowerSummaryValue(receipt, ["lastOutcome", "outcome", "result"]);
@@ -820,6 +881,38 @@ function profileHasRecoveredStateReceipt(receipts) {
820
881
  return hasRecoveredState || success || hasValid && hasInvalid === false;
821
882
  });
822
883
  }
884
+ function profileMetadataHasGeneratedOutputContract(metadata) {
885
+ const contract = cliRecord(metadata.declared_state_contract);
886
+ if (!contract) return false;
887
+ const keys = Object.keys(contract).join(" ").toLowerCase();
888
+ const values = Object.values(contract).map((value) => cliString(value)?.toLowerCase() || "").join(" ");
889
+ const haystack = `${keys} ${values}`;
890
+ return haystack.includes("generated_output") || haystack.includes("generated output") || haystack.includes("output-size") || haystack.includes("output size") || haystack.includes("output result") || haystack.includes("optimizer size");
891
+ }
892
+ function profileHasGeneratedOutputReceipt(receipts) {
893
+ let outputReady = false;
894
+ let outputChanged = false;
895
+ for (const receipt of receipts) {
896
+ const storedTo = cliString(receipt.return_stored_to) || "";
897
+ const label = cliString(receipt.label) || "";
898
+ const path2 = cliString(receipt.path) || cliString(receipt.function_name) || "";
899
+ const summary = cliReturnSummaryLabel(receipt.return_summary) || "";
900
+ const haystack = `${storedTo} ${label} ${path2} ${summary}`.toLowerCase();
901
+ const readySignal = setupReturnSummaryValue(receipt, ["outputReady", "outputStillReady"]) === true || cliFiniteNumber(setupReturnSummaryValue(receipt, ["surfaceCount", "before.surfaceCount", "after.surfaceCount"])) !== void 0 || setupReturnSummaryValue(receipt, ["size", "before.size", "after.size", "size.text", "before.size.text", "after.size.text"]) !== void 0;
902
+ if (readySignal && (haystack.includes("output") || haystack.includes("size") || haystack.includes("result"))) {
903
+ outputReady = true;
904
+ }
905
+ const beforeBytes = cliFiniteNumber(setupReturnSummaryValue(receipt, ["before.size.outputBytes", "before.outputBytes", "beforeBytes"]));
906
+ const afterBytes = cliFiniteNumber(setupReturnSummaryValue(receipt, ["after.size.outputBytes", "after.outputBytes", "afterBytes"]));
907
+ const beforeText = cliString(setupReturnSummaryValue(receipt, ["before.size.text", "before.outputText", "beforeText"]));
908
+ const afterText = cliString(setupReturnSummaryValue(receipt, ["after.size.text", "after.outputText", "afterText"]));
909
+ const explicitChange = setupReturnSummaryValue(receipt, ["sizeChanged", "outputChanged", "resultChanged"]) === true;
910
+ const byteChange = beforeBytes !== void 0 && afterBytes !== void 0 && beforeBytes !== afterBytes;
911
+ const textChange = Boolean(beforeText && afterText && beforeText !== afterText);
912
+ if (explicitChange || byteChange || textChange) outputChanged = true;
913
+ }
914
+ return outputReady && outputChanged;
915
+ }
823
916
  function profilePackReceiptStatus(result, metadata, receipt) {
824
917
  const text = receipt.toLowerCase();
825
918
  const setupSummary = profileSetupSummaryRecord(result);
@@ -850,6 +943,7 @@ function profilePackReceiptStatus(result, metadata, receipt) {
850
943
  const clickFallbackTapCount = clickFallbackTapKeys.size;
851
944
  const tapUntilCount = profileSetupReceiptTotal(setupViewports, "tap_until");
852
945
  const visibleUiActionCount = clickCount + profileSetupReceiptTotal(setupViewports, "tap") + tapUntilCount;
946
+ const visibleCleanupActionCount = profileVisibleCleanupActionCount(setupViewports);
853
947
  const setupFailureCount = profileSetupFailureCount(setupViewports);
854
948
  const setupObstructionCount = profileSetupObstructionCount(setupViewports);
855
949
  const inputDispatchCount = profileSetupReceiptTotal(setupViewports, "drag") + profileSetupReceiptTotal(setupViewports, "tap") + tapUntilCount + profileSetupReceiptTotal(setupViewports, "press") + profileSetupReceiptTotal(setupViewports, "keyboard_sequence");
@@ -885,6 +979,7 @@ function profilePackReceiptStatus(result, metadata, receipt) {
885
979
  const hasTextAbsence = profileHasPassedCheck(result, ["text_absent", "selector_text_absent"]);
886
980
  const hasMeasuredStateChange = hasNaturalInput || hasCanvasChange || valueReceipts.some((item) => setupReturnSummaryValue(item, ["changed"]) === true || setupReturnSummaryValue(item, ["nonWhiteDelta", "darkDelta", "pixelDelta", "movementDelta"]) !== void 0);
887
981
  const hasRouteExitAffordanceReceipt = profileHasRouteExitAffordanceReceipt(valueReceipts);
982
+ const hasCleanupBoundaryAffordanceReceipt = profileHasCleanupBoundaryAffordanceReceipt(valueReceipts);
888
983
  const hasOfflineAudioMetricsReceipt = profileHasOfflineAudioMetricsReceipt(valueReceipts);
889
984
  const hasActiveRouteLocalProofReceipt = profileHasActiveRouteLocalProofReceipt(valueReceipts);
890
985
  const hasTerminalLossReceipt = profileHasTerminalLossReceipt(valueReceipts);
@@ -893,6 +988,8 @@ function profilePackReceiptStatus(result, metadata, receipt) {
893
988
  const hasControlledSuccessLaunchReceipt = profileHasControlledLaunchReceipt(valueReceipts, "success");
894
989
  const hasRouteContinuationReceipt = profileHasRouteContinuationReceipt(valueReceipts);
895
990
  const hasRecoveredStateReceipt = profileHasRecoveredStateReceipt(valueReceipts);
991
+ const hasGeneratedOutputContract = profileMetadataHasGeneratedOutputContract(metadata);
992
+ const hasGeneratedOutputReceipt = profileHasGeneratedOutputReceipt(valueReceipts);
896
993
  const failedCleanupInventoryReason = profileFailedCleanupInventoryReason(setupViewports);
897
994
  const passedCleanupInventoryReason = profilePassedCleanupInventoryReason(setupViewports);
898
995
  if (text.includes("artifact link") || text.includes("artifact path")) {
@@ -946,6 +1043,13 @@ function profilePackReceiptStatus(result, metadata, receipt) {
946
1043
  }
947
1044
  return profileReceiptSignalStatus(hasTextAbsence, "absence check passed", "absence check missing");
948
1045
  }
1046
+ if (text.includes("generated-output") || text.includes("generated output") || text.includes("output-size") || text.includes("output size") || (text.includes("output") || text.includes("result")) && (text.includes("mutation") || text.includes("final"))) {
1047
+ return profileReceiptSignalStatus(
1048
+ hasGeneratedOutputContract && hasGeneratedOutputReceipt,
1049
+ "generated-output mutation receipt present",
1050
+ "generated-output mutation receipt missing"
1051
+ );
1052
+ }
949
1053
  if (text.includes("recovered") || text.includes("final state")) {
950
1054
  return profileReceiptSignalStatus(hasStateContract || hasTextVisibility, "final state receipt present", "final state receipt missing");
951
1055
  }
@@ -1002,6 +1106,13 @@ function profilePackReceiptStatus(result, metadata, receipt) {
1002
1106
  "route continuation receipt missing"
1003
1107
  );
1004
1108
  }
1109
+ if (text.includes("cleanup") && text.includes("action") && (text.includes("visible ui") || text.includes("visible"))) {
1110
+ return profileReceiptSignalStatus(
1111
+ visibleCleanupActionCount > 0,
1112
+ `visible cleanup action receipt present (${visibleCleanupActionCount})`,
1113
+ "visible cleanup action receipt missing"
1114
+ );
1115
+ }
1005
1116
  if (text.includes("through visible ui") || text.includes("visible ui action") || text.includes("ui-routed") || text.includes("ui routed") || text.includes("visible") && text.includes("route") && text.includes("exit") && text.includes("action") || text.includes("visible") && text.includes("mode") && text.includes("exit") && text.includes("action")) {
1006
1117
  return profileReceiptSignalStatus(
1007
1118
  visibleUiActionCount > 0,
@@ -1016,6 +1127,13 @@ function profilePackReceiptStatus(result, metadata, receipt) {
1016
1127
  "affordance receipt missing"
1017
1128
  );
1018
1129
  }
1130
+ if (text.includes("cleanup") && (text.includes("affordance") || text.includes("control") || text.includes("boundary") || text.includes("inventory"))) {
1131
+ return profileReceiptSignalStatus(
1132
+ hasCleanupBoundaryAffordanceReceipt,
1133
+ "visible cleanup affordance receipt present",
1134
+ "visible cleanup affordance receipt missing"
1135
+ );
1136
+ }
1019
1137
  if (text.includes("retry") || text.includes("repair") || text.includes("reset") || text.includes("affordance")) {
1020
1138
  return profileReceiptSignalStatus(hasStateContract || clickCount > 0, "affordance or transition receipt present", "affordance receipt missing");
1021
1139
  }
@@ -2353,6 +2471,21 @@ function writeProfileOutput(outputDir, result) {
2353
2471
  if (result.evidence?.dom_summary) writeFileSync(path.join(outputDir, "dom-summary.json"), `${JSON.stringify(result.evidence.dom_summary, null, 2)}
2354
2472
  `);
2355
2473
  }
2474
+ function writeRiddleJobReceipt(outputDir, input) {
2475
+ if (!outputDir) return;
2476
+ mkdirSync(outputDir, { recursive: true });
2477
+ writeFileSync(path.join(outputDir, "riddle-job.json"), `${JSON.stringify({
2478
+ version: "riddle-proof.riddle-job-receipt.v1",
2479
+ profile_name: input.profile.name,
2480
+ job_id: input.jobId,
2481
+ target_url: input.targetUrl,
2482
+ viewport: input.viewport || null,
2483
+ captured_at: (/* @__PURE__ */ new Date()).toISOString(),
2484
+ created: input.created || null,
2485
+ recovery_command: `riddle-proof-loop run-profile recover --profile <profile> --job ${input.jobId} --output-dir ${outputDir}`
2486
+ }, null, 2)}
2487
+ `);
2488
+ }
2356
2489
  async function readArtifactJson(artifact) {
2357
2490
  const target = artifact.url || artifact.path;
2358
2491
  if (!target) return void 0;
@@ -2556,6 +2689,43 @@ function profileForSplitViewport(profile, viewport) {
2556
2689
  }
2557
2690
  };
2558
2691
  }
2692
+ function profileItemAppliesToAnySelectedViewport(item, viewports) {
2693
+ if (!item.viewports?.length) return true;
2694
+ const names = new Set(viewports.map((viewport) => viewport.name).filter(Boolean));
2695
+ return item.viewports.some((name) => names.has(name));
2696
+ }
2697
+ function profileForSelectedViewports(profile, viewports) {
2698
+ const suffix = viewports.map((viewport) => viewport.name || `${viewport.width}x${viewport.height}`).join("-");
2699
+ const setupActions = profile.target.setup_actions?.filter((action) => profileItemAppliesToAnySelectedViewport(action, viewports));
2700
+ return {
2701
+ ...profile,
2702
+ name: `${profile.name}-${suffix}`,
2703
+ checks: profile.checks.filter((check) => profileItemAppliesToAnySelectedViewport(check, viewports)),
2704
+ target: {
2705
+ ...profile.target,
2706
+ viewports,
2707
+ ...setupActions ? { setup_actions: setupActions } : {}
2708
+ },
2709
+ metadata: {
2710
+ ...profile.metadata || {},
2711
+ selected_parent_profile: profile.name,
2712
+ selected_viewports: viewports.map((viewport) => viewport.name || `${viewport.width}x${viewport.height}`)
2713
+ }
2714
+ };
2715
+ }
2716
+ function profileWithSelectedViewportNamesForCli(profile, options) {
2717
+ const names = runProfileViewportNamesOption(options);
2718
+ if (!names.length) return profile;
2719
+ const requested = new Set(names);
2720
+ const viewports = profile.target.viewports.filter((viewport) => viewport.name && requested.has(viewport.name));
2721
+ const matched = new Set(viewports.map((viewport) => viewport.name).filter(Boolean));
2722
+ const missing = names.filter((name) => !matched.has(name));
2723
+ if (missing.length) {
2724
+ const available = profile.target.viewports.map((viewport) => viewport.name).filter(Boolean).join(", ") || "none";
2725
+ throw new Error(`Unknown --viewport-name ${missing.join(", ")}. Available viewport names: ${available}.`);
2726
+ }
2727
+ return profileForSelectedViewports(profile, viewports);
2728
+ }
2559
2729
  function safeProfileOutputSegment(value) {
2560
2730
  const safe = value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
2561
2731
  return safe || "viewport";
@@ -2744,6 +2914,13 @@ async function runSingleRiddleProfileForCli(profile, options, input) {
2744
2914
  const directResult = extractRiddleProofProfileResult(created);
2745
2915
  return directResult ? withRiddleMetadata(withProfileMetadata(profile, directResult), { artifacts: collectRiddleProfileArtifactRefs(created) }) : createRiddleProofProfileInsufficientResult({ profile, runner, error: "Riddle run response was missing job_id.", artifacts: collectRiddleProfileArtifactRefs(created) });
2746
2916
  }
2917
+ writeRiddleJobReceipt(input.outputDir, {
2918
+ profile,
2919
+ jobId,
2920
+ targetUrl,
2921
+ viewport: profile.target.viewports[0],
2922
+ created
2923
+ });
2747
2924
  poll = await client.pollJob(jobId, pollOptions);
2748
2925
  if (attempt < retryLimit && shouldRetryUnsubmittedRiddleJob(poll)) {
2749
2926
  const recoveredResult = await recoverProfileResultFromRiddleArtifacts(profile, {
@@ -2820,10 +2997,9 @@ async function runSplitViewportProfileForCli(profile, options, input) {
2820
2997
  const childRuns = [];
2821
2998
  for (const viewport of profile.target.viewports) {
2822
2999
  const childProfile = profileForSplitViewport(profile, viewport);
2823
- const result2 = await runSingleRiddleProfileForCli(childProfile, options, input);
2824
- if (outputDir) {
2825
- writeProfileOutput(splitViewportOutputDir(outputDir, viewport.name, seenOutputNames), result2);
2826
- }
3000
+ const childOutputDir = outputDir ? splitViewportOutputDir(outputDir, viewport.name, seenOutputNames) : void 0;
3001
+ const result2 = await runSingleRiddleProfileForCli(childProfile, options, { ...input, outputDir: childOutputDir });
3002
+ if (childOutputDir) writeProfileOutput(childOutputDir, result2);
2827
3003
  childRuns.push({ viewport, profile: childProfile, result: result2 });
2828
3004
  }
2829
3005
  const artifacts = childRuns.flatMap(splitViewportArtifactRefs);
@@ -2845,6 +3021,51 @@ async function runSplitViewportProfileForCli(profile, options, input) {
2845
3021
  });
2846
3022
  return withSplitViewportWarnings(profile, withSplitViewportChildStatusCheck(profile, result, childRuns));
2847
3023
  }
3024
+ async function recoverProfileForCli(profile, options) {
3025
+ const runner = optionString(options, "runner") || "riddle";
3026
+ if (runner !== "riddle") {
3027
+ throw new Error(`Unsupported --runner ${runner}. The current CLI supports --runner riddle.`);
3028
+ }
3029
+ const jobId = optionString(options, "job") ?? optionString(options, "jobId");
3030
+ if (!jobId) throw new Error("run-profile recover requires --job <job-id>.");
3031
+ const client = createRiddleApiClient(riddleClientConfig(options));
3032
+ let artifactPayload;
3033
+ try {
3034
+ artifactPayload = await client.requestJson(`/v1/jobs/${jobId}/artifacts`);
3035
+ } catch (error) {
3036
+ return createRiddleProofProfileEnvironmentBlockedResult({
3037
+ profile,
3038
+ runner,
3039
+ error,
3040
+ riddle: { job_id: jobId, terminal: false }
3041
+ });
3042
+ }
3043
+ const artifacts = collectRiddleProfileArtifactRefs(artifactPayload);
3044
+ const artifactStatus = riddleArtifactsPayloadStatus(artifactPayload);
3045
+ const terminal = artifactStatus ? isTerminalRiddleJobStatus(artifactStatus) : artifacts.length > 0;
3046
+ const recovered = await profileResultFromRiddleArtifacts(profile, artifacts, [artifactPayload]);
3047
+ if (recovered) {
3048
+ return withRiddleMetadata(recovered, {
3049
+ job_id: jobId,
3050
+ status: artifactStatus,
3051
+ terminal,
3052
+ artifacts,
3053
+ artifactRecovery: true
3054
+ });
3055
+ }
3056
+ return createRiddleProofProfileInsufficientResult({
3057
+ profile,
3058
+ runner,
3059
+ error: artifacts.length ? `Riddle job ${jobId} artifacts were recovered without a proof result.` : `Riddle job ${jobId} had no recoverable artifacts.`,
3060
+ riddle: {
3061
+ job_id: jobId,
3062
+ status: artifactStatus,
3063
+ terminal,
3064
+ artifact_recovery: artifacts.length > 0
3065
+ },
3066
+ artifacts
3067
+ });
3068
+ }
2848
3069
  async function runProfileForCli(profile, options) {
2849
3070
  const runner = optionString(options, "runner") || "riddle";
2850
3071
  if (runner !== "riddle") {
@@ -2854,7 +3075,7 @@ async function runProfileForCli(profile, options) {
2854
3075
  if (runProfileSplitViewportsOption(options) && profile.target.viewports.length > 1) {
2855
3076
  return runSplitViewportProfileForCli(profile, options, { client, runner });
2856
3077
  }
2857
- return runSingleRiddleProfileForCli(profile, options, { client, runner });
3078
+ return runSingleRiddleProfileForCli(profile, options, { client, runner, outputDir: profileOutputDirOption(options) });
2858
3079
  }
2859
3080
  function requestForRun(options) {
2860
3081
  const statePath = optionString(options, "statePath");
@@ -2903,8 +3124,8 @@ async function main() {
2903
3124
  return;
2904
3125
  }
2905
3126
  if (command === "run-profile") {
2906
- const profile = normalizeProfileForCli(options);
2907
- const result = await runProfileForCli(profile, options);
3127
+ const profile = profileWithSelectedViewportNamesForCli(normalizeProfileForCli(options), options);
3128
+ const result = positional[1] === "recover" ? await recoverProfileForCli(profile, options) : await runProfileForCli(profile, options);
2908
3129
  writeProfileOutput(profileOutputDirOption(options), result);
2909
3130
  const diagnosticLine = profileCliDiagnosticLine(result);
2910
3131
  if (diagnosticLine && optionBoolean(options, "quiet") !== true) {
@@ -292,7 +292,7 @@ declare function executeWorkflow(params: WorkflowParams, pluginConfig: any, reso
292
292
  blocking?: boolean;
293
293
  details?: Record<string, unknown>;
294
294
  ok: boolean;
295
- action: "author" | "recon" | "ship" | "implement" | "verify" | "setup" | "run";
295
+ action: "setup" | "recon" | "author" | "implement" | "verify" | "ship" | "run";
296
296
  state_path: string;
297
297
  stage: any;
298
298
  summary: string;
@@ -382,7 +382,7 @@ declare function executeWorkflow(params: WorkflowParams, pluginConfig: any, reso
382
382
  continueWithStage?: WorkflowStage | null;
383
383
  blocking?: boolean;
384
384
  details?: Record<string, unknown>;
385
- action: "author" | "recon" | "ship" | "implement" | "verify" | "setup" | "run";
385
+ action: "setup" | "recon" | "author" | "implement" | "verify" | "ship" | "run";
386
386
  state_path: string;
387
387
  stage: any;
388
388
  checkpoint: string;
@@ -659,7 +659,7 @@ declare function executeWorkflow(params: WorkflowParams, pluginConfig: any, reso
659
659
  error?: undefined;
660
660
  } | {
661
661
  ok: boolean;
662
- action: "author" | "recon" | "ship" | "implement" | "verify" | "setup" | "run";
662
+ action: "setup" | "recon" | "author" | "implement" | "verify" | "ship" | "run";
663
663
  state_path: string;
664
664
  stage: any;
665
665
  summary: string;
@@ -292,7 +292,7 @@ declare function executeWorkflow(params: WorkflowParams, pluginConfig: any, reso
292
292
  blocking?: boolean;
293
293
  details?: Record<string, unknown>;
294
294
  ok: boolean;
295
- action: "author" | "recon" | "ship" | "implement" | "verify" | "setup" | "run";
295
+ action: "setup" | "recon" | "author" | "implement" | "verify" | "ship" | "run";
296
296
  state_path: string;
297
297
  stage: any;
298
298
  summary: string;
@@ -382,7 +382,7 @@ declare function executeWorkflow(params: WorkflowParams, pluginConfig: any, reso
382
382
  continueWithStage?: WorkflowStage | null;
383
383
  blocking?: boolean;
384
384
  details?: Record<string, unknown>;
385
- action: "author" | "recon" | "ship" | "implement" | "verify" | "setup" | "run";
385
+ action: "setup" | "recon" | "author" | "implement" | "verify" | "ship" | "run";
386
386
  state_path: string;
387
387
  stage: any;
388
388
  checkpoint: string;
@@ -659,7 +659,7 @@ declare function executeWorkflow(params: WorkflowParams, pluginConfig: any, reso
659
659
  error?: undefined;
660
660
  } | {
661
661
  ok: boolean;
662
- action: "author" | "recon" | "ship" | "implement" | "verify" | "setup" | "run";
662
+ action: "setup" | "recon" | "author" | "implement" | "verify" | "ship" | "run";
663
663
  state_path: string;
664
664
  stage: any;
665
665
  summary: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riddledc/riddle-proof",
3
- "version": "0.7.201",
3
+ "version": "0.7.202",
4
4
  "description": "Reusable Riddle Proof contracts and helpers for evidence-backed agent changes.",
5
5
  "license": "MIT",
6
6
  "author": "RiddleDC",