@agent-native/recap-cli 0.5.67 → 0.5.68-nightly-20261009034138

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.
@@ -790,12 +790,13 @@ jobs:
790
790
  # preserving path-prefixed self-hosted mounts.
791
791
  if [ -z "$URL_REASON" ]; then
792
792
  URL_RESULT=$(PLAN_URL="$PLAN_URL" node <<'NODE'
793
+ void (() => {
793
794
  const emit = (value) => process.stdout.write(JSON.stringify(value));
794
795
  try {
795
796
  const raw = process.env.PLAN_URL || "";
796
797
  if (!raw) {
797
798
  emit({ url: "", reason: "recap-url.txt was empty" });
798
- process.exit(0);
799
+ return;
799
800
  }
800
801
  const trusted = new URL(process.env.PLAN_RECAP_APP_URL || "https://plan.agent-native.com");
801
802
  const parsed = /^https?:\/\//i.test(raw)
@@ -803,7 +804,7 @@ jobs:
803
804
  : new URL(raw, trusted);
804
805
  if (parsed.origin !== trusted.origin) {
805
806
  emit({ url: "", reason: `recap-url.txt points at ${parsed.origin}, expected ${trusted.origin}` });
806
- process.exit(0);
807
+ return;
807
808
  }
808
809
 
809
810
  const base = trusted.pathname.replace(/\/$/, "");
@@ -816,13 +817,14 @@ jobs:
816
817
  const match = path.match(/^\/(?:plans|recaps)\/([A-Za-z0-9_-]+)\/?$/);
817
818
  if (match) {
818
819
  emit({ url: `${trusted.origin}${base}/recaps/${match[1]}`, reason: "" });
819
- process.exit(0);
820
+ return;
820
821
  }
821
822
  }
822
823
  emit({ url: "", reason: "recap-url.txt did not contain a valid /plans/<id> or /recaps/<id> URL for the configured plan app" });
823
824
  } catch {
824
825
  emit({ url: "", reason: "recap-url.txt was not a valid URL or recap path" });
825
826
  }
827
+ })();
826
828
  NODE
827
829
  )
828
830
  CANONICAL_URL=$(node -e 'try{process.stdout.write(JSON.parse(process.argv[1]).url||"")}catch{process.stdout.write("")}' "$URL_RESULT")
@@ -926,14 +928,130 @@ jobs:
926
928
  path: ~/.cache/ms-playwright
927
929
  key: playwright-1-${{ runner.os }}
928
930
 
931
+ - name: Verify published recap readback
932
+ id: readback
933
+ if: always() && !cancelled() && steps.url.outputs.ok == 'true'
934
+ env:
935
+ PLAN_URL: ${{ steps.url.outputs.plan_url }}
936
+ run: |
937
+ set -uo pipefail
938
+ READBACK_JSON=$(node <<'NODE'
939
+ void (async () => {
940
+ const emit = (value) => process.stdout.write(JSON.stringify(value));
941
+ let requestAttempts = 0;
942
+ try {
943
+ const base = new URL(process.env.PLAN_RECAP_APP_URL || "https://plan.agent-native.com");
944
+ const recap = new URL(process.env.PLAN_URL || "");
945
+ if (recap.origin !== base.origin) {
946
+ emit({ ok: false, reason: "published recap URL origin does not match the configured Plan app" });
947
+ return;
948
+ }
949
+ const basePath = base.pathname.replace(/\/+$/, "");
950
+ let recapPath = recap.pathname;
951
+ if (basePath && recapPath.startsWith(`${basePath}/`)) recapPath = recapPath.slice(basePath.length);
952
+ const match = recapPath.match(/^\/(?:plans|recaps)\/([A-Za-z0-9_-]+)\/?$/);
953
+ if (!match) {
954
+ emit({ ok: false, reason: "published recap URL does not contain a valid Plan ID" });
955
+ return;
956
+ }
957
+ const actionUrl = new URL(base.href);
958
+ actionUrl.pathname = `${basePath}/_agent-native/actions/get-visual-plan`;
959
+ actionUrl.search = "";
960
+ actionUrl.hash = "";
961
+ if (actionUrl.origin !== base.origin) {
962
+ emit({ ok: false, reason: "get-visual-plan URL origin does not match the configured Plan app" });
963
+ return;
964
+ }
965
+ actionUrl.searchParams.set("id", match[1]);
966
+ actionUrl.searchParams.set("includeHtml", "false");
967
+ actionUrl.searchParams.set("includeMdx", "false");
968
+ let response;
969
+ let payload;
970
+ for (requestAttempts = 1; requestAttempts <= 3; requestAttempts++) {
971
+ try {
972
+ response = await fetch(actionUrl, {
973
+ headers: { authorization: `Bearer ${process.env.PLAN_RECAP_TOKEN || ""}` },
974
+ signal: AbortSignal.timeout(15000),
975
+ });
976
+ const retryableStatus = response.status === 408 || response.status === 429 || response.status >= 500;
977
+ if (retryableStatus && requestAttempts < 3) {
978
+ await new Promise((resolve) => setTimeout(resolve, requestAttempts * 250));
979
+ continue;
980
+ }
981
+ if (!response.ok) break;
982
+ payload = await response.json();
983
+ break;
984
+ } catch (error) {
985
+ const retryable = error?.name === "TimeoutError" || error instanceof TypeError;
986
+ if (!retryable || requestAttempts === 3) throw error;
987
+ await new Promise((resolve) => setTimeout(resolve, requestAttempts * 250));
988
+ }
989
+ }
990
+ if (!response.ok) {
991
+ const hint = response.status === 401 || response.status === 403
992
+ ? "the configured token cannot read this published recap"
993
+ : response.status === 404
994
+ ? "the Plan read action or published recap is not available"
995
+ : "the Plan read action returned an error";
996
+ const attempts = requestAttempts > 1 ? ` after ${requestAttempts} attempts` : "";
997
+ emit({ ok: false, reason: `get-visual-plan returned HTTP ${response.status}${attempts}; ${hint}` });
998
+ return;
999
+ }
1000
+ const returnedId = payload?.plan?.id ?? payload?.planId ?? payload?.data?.plan?.id;
1001
+ if (returnedId !== match[1]) {
1002
+ emit({ ok: false, reason: "get-visual-plan returned a response without the published Plan ID" });
1003
+ return;
1004
+ }
1005
+ emit({ ok: true, reason: "" });
1006
+ } catch (error) {
1007
+ const attempts = requestAttempts > 1 ? ` after ${requestAttempts} attempts` : "";
1008
+ const reason = error?.name === "TimeoutError"
1009
+ ? `get-visual-plan readback timed out${attempts}`
1010
+ : `get-visual-plan readback request failed${attempts}`;
1011
+ emit({ ok: false, reason });
1012
+ }
1013
+ })();
1014
+ NODE
1015
+ )
1016
+ READBACK_OK=$(node -e 'try{process.stdout.write(JSON.parse(process.argv[1]).ok===true?"true":"false")}catch{process.stdout.write("false")}' "$READBACK_JSON")
1017
+ READBACK_REASON=$(node -e 'try{process.stdout.write(JSON.parse(process.argv[1]).reason||"")}catch{process.stdout.write("invalid readback result")}' "$READBACK_JSON")
1018
+ if [ "$READBACK_OK" = "true" ]; then
1019
+ echo "[recap readback] published plan is readable"
1020
+ else
1021
+ echo "::warning::Published recap readback failed: $READBACK_REASON"
1022
+ fi
1023
+ echo "ok=$READBACK_OK" >> "$GITHUB_OUTPUT"
1024
+ {
1025
+ echo 'reason<<__RECAP_READBACK_REASON_EOF__'
1026
+ echo "$READBACK_REASON"
1027
+ echo '__RECAP_READBACK_REASON_EOF__'
1028
+ } >> "$GITHUB_OUTPUT"
1029
+
929
1030
  - name: Screenshot + upload
930
1031
  id: shot
931
- if: steps.url.outputs.ok == 'true'
1032
+ if: always() && !cancelled() && steps.url.outputs.ok == 'true'
932
1033
  continue-on-error: true
933
1034
  env:
934
1035
  PLAN_URL: ${{ steps.url.outputs.plan_url }}
1036
+ PLAN_READBACK_OK: ${{ steps.readback.outputs.ok }}
1037
+ PLAN_READBACK_REASON: ${{ steps.readback.outputs.reason }}
935
1038
  run: |
936
1039
  set -uo pipefail
1040
+ if [ "${PLAN_READBACK_OK:-false}" != "true" ]; then
1041
+ SHOT_REASON="Published recap readback failed: ${PLAN_READBACK_REASON:-the readback step did not complete.}"
1042
+ echo "::warning::$SHOT_REASON"
1043
+ echo "shot_ok=false" >> "$GITHUB_OUTPUT"
1044
+ echo "captured=false" >> "$GITHUB_OUTPUT"
1045
+ echo "image_url=" >> "$GITHUB_OUTPUT"
1046
+ echo "light_image_url=" >> "$GITHUB_OUTPUT"
1047
+ echo "dark_image_url=" >> "$GITHUB_OUTPUT"
1048
+ {
1049
+ echo 'shot_reason<<__RECAP_SHOT_REASON_EOF__'
1050
+ echo "$SHOT_REASON"
1051
+ echo '__RECAP_SHOT_REASON_EOF__'
1052
+ } >> "$GITHUB_OUTPUT"
1053
+ exit 0
1054
+ fi
937
1055
  if [ -n "${RECAP_PLAYWRIGHT:-}" ] && [ -x "$RECAP_PLAYWRIGHT" ]; then
938
1056
  "$RECAP_PLAYWRIGHT" install --with-deps chromium || true
939
1057
  elif command -v pnpm >/dev/null 2>&1; then
@@ -1027,6 +1145,8 @@ jobs:
1027
1145
  RECAP_IMAGE_URL: ${{ steps.shot.outputs.image_url }}
1028
1146
  RECAP_LIGHT_IMAGE_URL: ${{ steps.shot.outputs.light_image_url }}
1029
1147
  RECAP_DARK_IMAGE_URL: ${{ steps.shot.outputs.dark_image_url }}
1148
+ RECAP_READBACK_OK: ${{ steps.readback.outputs.ok }}
1149
+ RECAP_READBACK_REASON: ${{ steps.readback.outputs.reason }}
1030
1150
  RECAP_SHOT_OK: ${{ steps.shot.outputs.shot_ok }}
1031
1151
  RECAP_SHOT_REASON: ${{ steps.shot.outputs.shot_reason }}
1032
1152
  SUPPRESSED: ${{ steps.scan.outputs.suppressed }}
@@ -1046,8 +1166,13 @@ jobs:
1046
1166
  continue-on-error: true
1047
1167
  env:
1048
1168
  CHECK_RUN_ID: ${{ steps.recap_check.outputs.check_run_id }}
1049
- PLAN_OK: ${{ steps.url.outputs.ok }}
1169
+ PLAN_OK: ${{ steps.url.outputs.ok == 'true' && steps.shot.outputs.shot_ok == 'true' }}
1170
+ URL_OK: ${{ steps.url.outputs.ok }}
1050
1171
  PLAN_URL: ${{ steps.url.outputs.plan_url }}
1172
+ READBACK_OK: ${{ steps.readback.outputs.ok }}
1173
+ READBACK_REASON: ${{ steps.readback.outputs.reason }}
1174
+ SHOT_OK: ${{ steps.shot.outputs.shot_ok }}
1175
+ SHOT_REASON: ${{ steps.shot.outputs.shot_reason }}
1051
1176
  SUPPRESSED: ${{ steps.scan.outputs.suppressed }}
1052
1177
  SUPPRESSED_JSON: ${{ steps.scan.outputs.json }}
1053
1178
  DIFF_HUGE: ${{ steps.diff.outputs.huge }}
@@ -1056,14 +1181,153 @@ jobs:
1056
1181
  RECAP_URL_REASON: ${{ steps.route_health.outputs.reason || steps.source_status.outputs.reason || steps.url.outputs.reason }}
1057
1182
  run: |
1058
1183
  set -uo pipefail
1184
+ CLI_SHOT_REASON=""
1185
+ CLI_SHOT_OK="$SHOT_OK"
1186
+ CLI_READBACK_REASON=""
1187
+ CLI_FAILURE_SUMMARY="$RECAP_AGENT_SUMMARY"
1188
+ FAILURE_STAGE=""
1189
+ FAILURE_REASON=""
1190
+ if [ "$URL_OK" = "true" ] && [ "$READBACK_OK" != "true" ]; then
1191
+ FAILURE_STAGE="readback"
1192
+ FAILURE_REASON="$READBACK_REASON"
1193
+ elif [ "$URL_OK" = "true" ] && [ "$SHOT_OK" != "true" ]; then
1194
+ FAILURE_STAGE="screenshot"
1195
+ FAILURE_REASON="$SHOT_REASON"
1196
+ fi
1197
+ if [ -n "$FAILURE_STAGE" ]; then
1198
+ FAILURE_DIAGNOSTIC=$(FAILURE_REASON="$FAILURE_REASON" node <<'NODE'
1199
+ const patterns = [
1200
+ /\bsk-(?:proj-)?[A-Za-z0-9_-]{24,}\b/,
1201
+ /\b(?:sk|rk)_live_[A-Za-z0-9]{16,}\b/,
1202
+ /\bSG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b/,
1203
+ /\bGOCSPX-[A-Za-z0-9_-]{20,}\b/,
1204
+ /\bbpk-[A-Za-z0-9_-]{16,}\b/,
1205
+ /\bghp_[A-Za-z0-9]{20,}\b/,
1206
+ /\bgithub_pat_[A-Za-z0-9_]{20,}\b/,
1207
+ /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/,
1208
+ /\bAKIA[0-9A-Z]{16}\b/,
1209
+ /\bAIza[0-9A-Za-z_-]{20,}\b/,
1210
+ /authorization\s*[:=]\s*['"]?bearer\s+[A-Za-z0-9._-]{20,}/i,
1211
+ /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/,
1212
+ ];
1213
+ const redact = (line) => line
1214
+ .replace(/Authorization\s*[:=]\s*Bearer\s+\S+/gi, "Authorization: Bearer [redacted]")
1215
+ .replace(/Bearer\s+\S+/gi, "Bearer [redacted]")
1216
+ .replace(/Authorization\s*[:=]\s*(?!Bearer\s+\[redacted\])(?:\S+\s+)?\S+/gi, "Authorization: [redacted]")
1217
+ .replace(/\b[A-Z0-9_]*(?:SECRET|TOKEN|PASSWORD|API_KEY|PRIVATE_KEY|ACCESS_KEY)[A-Z0-9_]*\s*=\s*[^\s]+/gi, "[redacted credential]");
1218
+ const text = (process.env.FAILURE_REASON || "")
1219
+ .replace(/\u001b\[[0-9;]*m/g, "")
1220
+ .split("\n")
1221
+ .map(redact)
1222
+ .map((line) => patterns.some((pattern) => pattern.test(line)) ? "[redacted sensitive line]" : line)
1223
+ .join(" ")
1224
+ .replace(/\s+/g, " ")
1225
+ .trim();
1226
+ const summary = text.length <= 400 ? text : `${text.slice(0, 399).trimEnd()}…`;
1227
+ process.stdout.write(summary);
1228
+ NODE
1229
+ )
1230
+ FAILURE_DIAGNOSTIC=${FAILURE_DIAGNOSTIC:-$FAILURE_STAGE verification failed}
1231
+ if [ "$FAILURE_STAGE" = "readback" ]; then
1232
+ CLI_READBACK_REASON="$FAILURE_DIAGNOSTIC"
1233
+ FAILURE_TITLE="Visual recap readback failed"
1234
+ FAILURE_SUMMARY="The recap was published, but the workflow could not verify it. Screenshot capture was skipped. This informational check does not block the PR."
1235
+ FAILURE_TEXT="[Open visual recap]($PLAN_URL) — readback failed: $FAILURE_DIAGNOSTIC. Screenshot capture was skipped. See this workflow run for details."
1236
+ else
1237
+ CLI_SHOT_REASON="$FAILURE_DIAGNOSTIC"
1238
+ FAILURE_TITLE="Visual recap screenshot failed"
1239
+ FAILURE_SUMMARY="The recap was published, but its screenshot failed: $FAILURE_DIAGNOSTIC. This informational check does not block the PR."
1240
+ FAILURE_TEXT="[Open visual recap]($PLAN_URL) — screenshot failed: $FAILURE_DIAGNOSTIC. See this workflow run for details."
1241
+ fi
1242
+ if gh api --method PATCH \
1243
+ "repos/$GITHUB_REPOSITORY/check-runs/$CHECK_RUN_ID" \
1244
+ -f status=completed \
1245
+ -f conclusion=failure \
1246
+ -f completed_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
1247
+ -f details_url="$PLAN_URL" \
1248
+ -f "output[title]=$FAILURE_TITLE" \
1249
+ -f "output[summary]=$FAILURE_SUMMARY" \
1250
+ -f "output[text]=$FAILURE_TEXT" \
1251
+ > /dev/null; then
1252
+ exit 0
1253
+ fi
1254
+ echo "::warning::Could not set $FAILURE_STAGE failure on the Visual Recap check with gh; retrying through the workflow-owned API fallback."
1255
+ if node --input-type=module - "$FAILURE_STAGE" "$FAILURE_TITLE" "$FAILURE_SUMMARY" "$FAILURE_TEXT" <<'NODE'
1256
+ const [failureStage, failureTitle, failureSummary, failureText] = process.argv.slice(2);
1257
+ const token = process.env.GH_TOKEN;
1258
+ const repository = process.env.GITHUB_REPOSITORY;
1259
+ const [owner, name, ...extra] = (repository || '').split('/');
1260
+ const checkRunId = process.env.CHECK_RUN_ID;
1261
+ if (!token || !owner || !name || extra.length || !/^\d+$/.test(checkRunId || '')) {
1262
+ process.stderr.write('Visual Recap check fallback is missing required GitHub API context.\n');
1263
+ process.exit(1);
1264
+ }
1265
+
1266
+ const apiBase = (process.env.GITHUB_API_URL || 'https://api.github.com').replace(/\/+$/, '');
1267
+ const endpoint = `${apiBase}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/check-runs/${checkRunId}`;
1268
+ const payload = {
1269
+ status: 'completed',
1270
+ conclusion: 'failure',
1271
+ completed_at: new Date().toISOString(),
1272
+ details_url: process.env.PLAN_URL,
1273
+ output: {
1274
+ title: failureTitle,
1275
+ summary: failureSummary,
1276
+ text: failureText,
1277
+ },
1278
+ };
1279
+ async function updateCheckRun() {
1280
+ const delays = [0, 500, 1500];
1281
+ let lastStatus;
1282
+
1283
+ for (let attempt = 0; attempt < delays.length; attempt += 1) {
1284
+ if (delays[attempt]) await new Promise((resolve) => setTimeout(resolve, delays[attempt]));
1285
+ try {
1286
+ const response = await fetch(endpoint, {
1287
+ method: 'PATCH',
1288
+ headers: {
1289
+ Accept: 'application/vnd.github+json',
1290
+ Authorization: `Bearer ${token}`,
1291
+ 'Content-Type': 'application/json',
1292
+ },
1293
+ body: JSON.stringify(payload),
1294
+ });
1295
+ if (response.ok) {
1296
+ process.stdout.write('Visual Recap failure detail was written through the workflow-owned API fallback.\n');
1297
+ return true;
1298
+ }
1299
+ lastStatus = response.status;
1300
+ if (response.status !== 408 && response.status !== 429 && response.status < 500) break;
1301
+ } catch {
1302
+ lastStatus = 'network error';
1303
+ }
1304
+ }
1305
+
1306
+ process.stderr.write(`Visual Recap API fallback could not record the ${failureStage} failure (${lastStatus || 'unknown error'}).\n`);
1307
+ return false;
1308
+ }
1309
+
1310
+ if (await updateCheckRun()) process.exit(0);
1311
+ process.exit(1);
1312
+ NODE
1313
+ then
1314
+ exit 0
1315
+ fi
1316
+ echo "::error::Visual Recap failure details could not be written; leaving the informational check unresolved rather than replacing the stage-specific failure with a generic legacy CLI result."
1317
+ exit 1
1318
+ fi
1059
1319
  $RECAP_CLI recap check complete \
1060
1320
  --check-run-id "$CHECK_RUN_ID" \
1061
1321
  --plan-ok "$PLAN_OK" \
1062
1322
  --plan-url "$PLAN_URL" \
1323
+ --readback-ok "$READBACK_OK" \
1324
+ --readback-reason "$CLI_READBACK_REASON" \
1325
+ --shot-ok "$CLI_SHOT_OK" \
1326
+ --shot-reason "$CLI_SHOT_REASON" \
1063
1327
  --suppressed "$SUPPRESSED" \
1064
1328
  --suppressed-json "$SUPPRESSED_JSON" \
1065
1329
  --huge "$DIFF_HUGE" \
1066
1330
  --tiny "$DIFF_TINY" \
1067
- --failure-summary "$RECAP_AGENT_SUMMARY" \
1331
+ --failure-summary "$CLI_FAILURE_SUMMARY" \
1068
1332
  --url-reason "$RECAP_URL_REASON" \
1069
1333
  --workflow-url "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/recap-cli",
3
- "version": "0.5.67",
3
+ "version": "0.5.68-nightly-20261009034138",
4
4
  "description": "Dependency-light PR visual recap CLI for Agent-Native workflows.",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {