@agent-native/core 0.49.22 → 0.49.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/production-agent.d.ts +1 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +15 -0
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/tool-search.d.ts.map +1 -1
- package/dist/agent/tool-search.js +32 -7
- package/dist/agent/tool-search.js.map +1 -1
- package/dist/cli/connect.d.ts +2 -3
- package/dist/cli/connect.d.ts.map +1 -1
- package/dist/cli/connect.js +60 -37
- package/dist/cli/connect.js.map +1 -1
- package/dist/cli/pr-visual-recap-workflow.d.ts +5 -7
- package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
- package/dist/cli/pr-visual-recap-workflow.js +5 -7
- package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
- package/dist/cli/recap.d.ts +44 -52
- package/dist/cli/recap.d.ts.map +1 -1
- package/dist/cli/recap.js +441 -414
- package/dist/cli/recap.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +6 -3
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +1 -1
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +23 -3
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/agent-chat.d.ts +8 -0
- package/dist/client/agent-chat.d.ts.map +1 -1
- package/dist/client/agent-chat.js +43 -1
- package/dist/client/agent-chat.js.map +1 -1
- package/dist/client/blocks/library/AnnotatedCodeBlock.d.ts.map +1 -1
- package/dist/client/blocks/library/AnnotatedCodeBlock.js +4 -1
- package/dist/client/blocks/library/AnnotatedCodeBlock.js.map +1 -1
- package/dist/client/blocks/library/DiffBlock.d.ts.map +1 -1
- package/dist/client/blocks/library/DiffBlock.js +20 -7
- package/dist/client/blocks/library/DiffBlock.js.map +1 -1
- package/dist/client/blocks/library/annotation-rail.js +5 -5
- package/dist/client/blocks/library/annotation-rail.js.map +1 -1
- package/dist/client/builder-frame.d.ts +2 -0
- package/dist/client/builder-frame.d.ts.map +1 -1
- package/dist/client/builder-frame.js +2 -0
- package/dist/client/builder-frame.js.map +1 -1
- package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/client/composer/TiptapComposer.js +15 -2
- package/dist/client/composer/TiptapComposer.js.map +1 -1
- package/dist/client/mcp-app-host.d.ts +3 -0
- package/dist/client/mcp-app-host.d.ts.map +1 -1
- package/dist/client/mcp-app-host.js +13 -0
- package/dist/client/mcp-app-host.js.map +1 -1
- package/dist/coding-tools/run-code.d.ts.map +1 -1
- package/dist/coding-tools/run-code.js +69 -17
- package/dist/coding-tools/run-code.js.map +1 -1
- package/dist/integrations/plugin.d.ts.map +1 -1
- package/dist/integrations/plugin.js +2 -0
- package/dist/integrations/plugin.js.map +1 -1
- package/dist/mcp/build-server.d.ts +12 -10
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +53 -89
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/mcp/connect-route.d.ts.map +1 -1
- package/dist/mcp/connect-route.js +5 -4
- package/dist/mcp/connect-route.js.map +1 -1
- package/dist/mcp/oauth-token.d.ts +6 -5
- package/dist/mcp/oauth-token.d.ts.map +1 -1
- package/dist/mcp/oauth-token.js.map +1 -1
- package/dist/mcp/stdio.d.ts.map +1 -1
- package/dist/mcp/stdio.js +9 -2
- package/dist/mcp/stdio.js.map +1 -1
- package/dist/provider-api/staging.d.ts.map +1 -1
- package/dist/provider-api/staging.js +6 -4
- package/dist/provider-api/staging.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts +10 -7
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/docs/content/actions.md +1 -1
- package/docs/content/external-agents.md +53 -40
- package/docs/content/mcp-protocol.md +16 -11
- package/docs/content/pr-visual-recap.md +1 -1
- package/package.json +1 -1
package/dist/cli/recap.js
CHANGED
|
@@ -14,13 +14,12 @@
|
|
|
14
14
|
* collect-diff Collect the bounded base...head diff (excluding lockfiles,
|
|
15
15
|
* build output, snapshots), cap it at ~600KB, and classify the
|
|
16
16
|
* huge/tiny flags.
|
|
17
|
-
* mcp-config Write the plan MCP client config for the chosen backend
|
|
18
|
-
* (Claude Code JSON or Codex config.toml).
|
|
19
|
-
* mcp-smoke Verify the configured Plan MCP endpoint exposes the publish
|
|
20
|
-
* tools before spending runner time on Claude/Codex.
|
|
21
17
|
* scan Refuse to hand a secret-leaking diff to the agent.
|
|
18
|
+
* block-reference
|
|
19
|
+
* Fetch the live get-plan-blocks reference for the target app.
|
|
22
20
|
* build-prompt Assemble the agent prompt = latest visual-recap skill bundle
|
|
23
21
|
* + a task wrapper (or repo-pinned skill with --skill-source).
|
|
22
|
+
* publish Publish the agent-authored recap-source.json over HTTP.
|
|
24
23
|
* shot Screenshot the published plan and upload it to the plan app's
|
|
25
24
|
* signed public image route (for an inline PR-comment image).
|
|
26
25
|
* usage Parse and emit agent token-usage/cost from stdout.
|
|
@@ -35,8 +34,8 @@
|
|
|
35
34
|
* Node built-ins only (plus an optional dynamic `playwright` import for `shot`).
|
|
36
35
|
*/
|
|
37
36
|
import { execFileSync } from "node:child_process";
|
|
37
|
+
import { createHash } from "node:crypto";
|
|
38
38
|
import fs from "node:fs";
|
|
39
|
-
import os from "node:os";
|
|
40
39
|
import path from "node:path";
|
|
41
40
|
import { readPlanPublishAuth } from "./plan-publish-store.js";
|
|
42
41
|
import { PR_VISUAL_RECAP_WORKFLOW_YML } from "./pr-visual-recap-workflow.js";
|
|
@@ -196,12 +195,6 @@ export function writePrVisualRecapReusableCallerWorkflow(baseDir, options = {})
|
|
|
196
195
|
return { status: "written", path: rel, existed: false };
|
|
197
196
|
}
|
|
198
197
|
const DEFAULT_RECAP_APP_URL = "https://plan.agent-native.com";
|
|
199
|
-
const RECAP_MCP_CLIENT_HEADER = "agent-native-pr-visual-recap";
|
|
200
|
-
export const RECAP_MCP_REQUIRED_TOOLS = [
|
|
201
|
-
"get-plan-blocks",
|
|
202
|
-
"create-visual-recap",
|
|
203
|
-
"set-resource-visibility",
|
|
204
|
-
];
|
|
205
198
|
export function normalizeRecapAgent(value) {
|
|
206
199
|
const agent = (value || "claude").toLowerCase();
|
|
207
200
|
if (agent === "codex")
|
|
@@ -1109,371 +1102,6 @@ function runCollectDiff(args) {
|
|
|
1109
1102
|
process.stdout.write(`${JSON.stringify({ bytes, changed, huge, tiny })}\n`);
|
|
1110
1103
|
}
|
|
1111
1104
|
/* -------------------------------------------------------------------------- */
|
|
1112
|
-
/* MCP config writers — were the two `node -e` one-liners in the agent steps */
|
|
1113
|
-
/* -------------------------------------------------------------------------- */
|
|
1114
|
-
/**
|
|
1115
|
-
* The Claude Code MCP config the recap agent loads: a single HTTP `plan` server
|
|
1116
|
-
* pointing at the app's `/_agent-native/mcp` endpoint, authorized with the
|
|
1117
|
-
* PLAN_RECAP_TOKEN. Pure (returns the JSON string) so it can be unit-tested.
|
|
1118
|
-
*/
|
|
1119
|
-
export function buildRecapClaudeMcpConfig(appUrl, token) {
|
|
1120
|
-
const url = appUrl.replace(/\/$/, "") + "/_agent-native/mcp";
|
|
1121
|
-
return JSON.stringify({
|
|
1122
|
-
mcpServers: {
|
|
1123
|
-
plan: {
|
|
1124
|
-
type: "http",
|
|
1125
|
-
url,
|
|
1126
|
-
headers: {
|
|
1127
|
-
Authorization: "Bearer " + token,
|
|
1128
|
-
"X-Agent-Native-MCP-Client": RECAP_MCP_CLIENT_HEADER,
|
|
1129
|
-
"X-Agent-Native-MCP-Full-Catalog": "1",
|
|
1130
|
-
},
|
|
1131
|
-
},
|
|
1132
|
-
"agent-native-plans": {
|
|
1133
|
-
type: "http",
|
|
1134
|
-
url,
|
|
1135
|
-
headers: {
|
|
1136
|
-
Authorization: "Bearer " + token,
|
|
1137
|
-
"X-Agent-Native-MCP-Client": RECAP_MCP_CLIENT_HEADER,
|
|
1138
|
-
"X-Agent-Native-MCP-Full-Catalog": "1",
|
|
1139
|
-
},
|
|
1140
|
-
},
|
|
1141
|
-
},
|
|
1142
|
-
});
|
|
1143
|
-
}
|
|
1144
|
-
/**
|
|
1145
|
-
* The Codex `config.toml` the recap agent loads. JSON.stringify the URL value so
|
|
1146
|
-
* a stray quote/newline in the app URL can't break out of the TOML basic string
|
|
1147
|
-
* (TOML shares JSON's escaping); the key and env-var name stay literal. Pure so
|
|
1148
|
-
* it can be unit-tested.
|
|
1149
|
-
*/
|
|
1150
|
-
export function buildRecapCodexMcpConfig(appUrl) {
|
|
1151
|
-
const url = appUrl.replace(/\/$/, "") + "/_agent-native/mcp";
|
|
1152
|
-
return ("[mcp_servers.plan]\n" +
|
|
1153
|
-
"url = " +
|
|
1154
|
-
JSON.stringify(url) +
|
|
1155
|
-
"\n" +
|
|
1156
|
-
'bearer_token_env_var = "PLAN_RECAP_TOKEN"\n' +
|
|
1157
|
-
'http_headers = { "X-Agent-Native-MCP-Client" = "agent-native-pr-visual-recap", "X-Agent-Native-MCP-Full-Catalog" = "1" }\n');
|
|
1158
|
-
}
|
|
1159
|
-
/**
|
|
1160
|
-
* Per-attempt timeout for the non-mutating MCP smoke probe. Short enough that
|
|
1161
|
-
* the workflow's 3-attempt retry loop can recover from a cold-start hang within
|
|
1162
|
-
* the job budget, long enough not to flake on a warm-but-slow response. The
|
|
1163
|
-
* plan app is serverless and intermittently 404s / stalls during a cold start
|
|
1164
|
-
* or mid-deploy; without a bound, undici's multi-minute default header/body
|
|
1165
|
-
* timeout would burn the whole job on a single stuck attempt.
|
|
1166
|
-
*/
|
|
1167
|
-
const RECAP_MCP_SMOKE_TIMEOUT_MS = 15_000;
|
|
1168
|
-
function recapMcpUrl(appUrl) {
|
|
1169
|
-
return appUrl.replace(/\/$/, "") + "/_agent-native/mcp";
|
|
1170
|
-
}
|
|
1171
|
-
function recapMcpSmokeHeaders(token) {
|
|
1172
|
-
return {
|
|
1173
|
-
authorization: `Bearer ${token}`,
|
|
1174
|
-
accept: "application/json, text/event-stream",
|
|
1175
|
-
"content-type": "application/json",
|
|
1176
|
-
"mcp-protocol-version": "2025-06-18",
|
|
1177
|
-
"x-agent-native-mcp-client": RECAP_MCP_CLIENT_HEADER,
|
|
1178
|
-
"x-agent-native-mcp-full-catalog": "1",
|
|
1179
|
-
};
|
|
1180
|
-
}
|
|
1181
|
-
function parseSseJsonPayload(raw) {
|
|
1182
|
-
const dataLines = raw
|
|
1183
|
-
.split(/\r?\n/)
|
|
1184
|
-
.map((line) => line.trim())
|
|
1185
|
-
.filter((line) => line.startsWith("data:"))
|
|
1186
|
-
.map((line) => line.slice("data:".length).trim())
|
|
1187
|
-
.filter((line) => line && line !== "[DONE]");
|
|
1188
|
-
for (let i = dataLines.length - 1; i >= 0; i -= 1) {
|
|
1189
|
-
try {
|
|
1190
|
-
return JSON.parse(dataLines[i]);
|
|
1191
|
-
}
|
|
1192
|
-
catch {
|
|
1193
|
-
// Keep looking for a parseable event payload.
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
return null;
|
|
1197
|
-
}
|
|
1198
|
-
function parseMcpJsonPayload(raw) {
|
|
1199
|
-
const trimmed = raw.trim();
|
|
1200
|
-
if (!trimmed)
|
|
1201
|
-
return null;
|
|
1202
|
-
try {
|
|
1203
|
-
return JSON.parse(trimmed);
|
|
1204
|
-
}
|
|
1205
|
-
catch {
|
|
1206
|
-
return parseSseJsonPayload(trimmed);
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
function mcpRpcErrorMessage(payload) {
|
|
1210
|
-
if (!payload || typeof payload !== "object")
|
|
1211
|
-
return null;
|
|
1212
|
-
const error = payload.error;
|
|
1213
|
-
if (!error || typeof error !== "object")
|
|
1214
|
-
return null;
|
|
1215
|
-
const message = error.message;
|
|
1216
|
-
return typeof message === "string" && message.trim()
|
|
1217
|
-
? message.trim()
|
|
1218
|
-
: JSON.stringify(error);
|
|
1219
|
-
}
|
|
1220
|
-
async function postRecapMcpRpc(input) {
|
|
1221
|
-
const response = await input.fetchFn(input.mcpUrl, {
|
|
1222
|
-
method: "POST",
|
|
1223
|
-
headers: recapMcpSmokeHeaders(input.token),
|
|
1224
|
-
body: JSON.stringify({
|
|
1225
|
-
jsonrpc: "2.0",
|
|
1226
|
-
id: input.id,
|
|
1227
|
-
method: input.method,
|
|
1228
|
-
params: input.params ?? {},
|
|
1229
|
-
}),
|
|
1230
|
-
// Bound each attempt so a cold-start hang fails fast and the workflow's
|
|
1231
|
-
// smoke retry loop can re-probe a (by-then warm) endpoint, instead of
|
|
1232
|
-
// blocking on undici's multi-minute default timeout.
|
|
1233
|
-
signal: AbortSignal.timeout(RECAP_MCP_SMOKE_TIMEOUT_MS),
|
|
1234
|
-
});
|
|
1235
|
-
const raw = await response.text().catch((err) => String(err));
|
|
1236
|
-
if (!response.ok) {
|
|
1237
|
-
const detail = sanitizeAgentFailureSummary(raw, 300);
|
|
1238
|
-
return {
|
|
1239
|
-
ok: false,
|
|
1240
|
-
status: response.status,
|
|
1241
|
-
raw,
|
|
1242
|
-
reason: `Plan MCP ${input.method} returned HTTP ${response.status}${detail ? `: ${detail}` : ""}`,
|
|
1243
|
-
};
|
|
1244
|
-
}
|
|
1245
|
-
const payload = parseMcpJsonPayload(raw);
|
|
1246
|
-
if (!payload) {
|
|
1247
|
-
return {
|
|
1248
|
-
ok: false,
|
|
1249
|
-
raw,
|
|
1250
|
-
reason: `Plan MCP ${input.method} returned an unreadable response`,
|
|
1251
|
-
};
|
|
1252
|
-
}
|
|
1253
|
-
const rpcError = mcpRpcErrorMessage(payload);
|
|
1254
|
-
if (rpcError) {
|
|
1255
|
-
return {
|
|
1256
|
-
ok: false,
|
|
1257
|
-
raw,
|
|
1258
|
-
reason: `Plan MCP ${input.method} failed: ${sanitizeAgentFailureSummary(rpcError, 300)}`,
|
|
1259
|
-
};
|
|
1260
|
-
}
|
|
1261
|
-
return { ok: true, payload };
|
|
1262
|
-
}
|
|
1263
|
-
function extractMcpToolNames(payload) {
|
|
1264
|
-
const result = payload && typeof payload === "object"
|
|
1265
|
-
? payload.result
|
|
1266
|
-
: undefined;
|
|
1267
|
-
const tools = result && typeof result === "object"
|
|
1268
|
-
? result.tools
|
|
1269
|
-
: undefined;
|
|
1270
|
-
if (!Array.isArray(tools))
|
|
1271
|
-
return [];
|
|
1272
|
-
return tools
|
|
1273
|
-
.map((tool) => tool && typeof tool === "object"
|
|
1274
|
-
? tool.name
|
|
1275
|
-
: undefined)
|
|
1276
|
-
.filter((name) => typeof name === "string" && !!name)
|
|
1277
|
-
.sort();
|
|
1278
|
-
}
|
|
1279
|
-
function recapMcpSmokeFailure(input) {
|
|
1280
|
-
const tools = input.tools ?? [];
|
|
1281
|
-
const reason = sanitizeAgentFailureSummary(input.reason, 500);
|
|
1282
|
-
return {
|
|
1283
|
-
ok: false,
|
|
1284
|
-
appUrl: input.appUrl,
|
|
1285
|
-
mcpUrl: input.mcpUrl,
|
|
1286
|
-
toolCount: tools.length,
|
|
1287
|
-
tools,
|
|
1288
|
-
requiredTools: [...input.requiredTools],
|
|
1289
|
-
reason,
|
|
1290
|
-
summary: `Plan MCP smoke check failed: ${reason}`,
|
|
1291
|
-
};
|
|
1292
|
-
}
|
|
1293
|
-
/**
|
|
1294
|
-
* Non-mutating live contract check for PR Visual Recap publishing.
|
|
1295
|
-
*
|
|
1296
|
-
* The previous workflow discovered a broken Plan MCP catalog only after the
|
|
1297
|
-
* agent tried to publish and failed to create `recap-url.txt`. This probes the
|
|
1298
|
-
* same authenticated MCP endpoint first and requires the three tools that the
|
|
1299
|
-
* visual-recap skill needs to publish a hosted recap.
|
|
1300
|
-
*/
|
|
1301
|
-
export async function smokeRecapMcpTools(input) {
|
|
1302
|
-
const appUrl = input.appUrl ?? DEFAULT_RECAP_APP_URL;
|
|
1303
|
-
const mcpUrl = recapMcpUrl(appUrl);
|
|
1304
|
-
const requiredTools = input.requiredTools ?? RECAP_MCP_REQUIRED_TOOLS;
|
|
1305
|
-
const token = input.token?.trim();
|
|
1306
|
-
if (!token) {
|
|
1307
|
-
return recapMcpSmokeFailure({
|
|
1308
|
-
appUrl,
|
|
1309
|
-
mcpUrl,
|
|
1310
|
-
requiredTools,
|
|
1311
|
-
reason: "PLAN_RECAP_TOKEN is empty",
|
|
1312
|
-
});
|
|
1313
|
-
}
|
|
1314
|
-
const fetchFn = input.fetchFn ?? fetch;
|
|
1315
|
-
try {
|
|
1316
|
-
const init = await postRecapMcpRpc({
|
|
1317
|
-
fetchFn,
|
|
1318
|
-
mcpUrl,
|
|
1319
|
-
token,
|
|
1320
|
-
method: "initialize",
|
|
1321
|
-
id: 1,
|
|
1322
|
-
params: {
|
|
1323
|
-
protocolVersion: "2025-06-18",
|
|
1324
|
-
capabilities: {},
|
|
1325
|
-
clientInfo: {
|
|
1326
|
-
name: RECAP_MCP_CLIENT_HEADER,
|
|
1327
|
-
version: "1.0.0",
|
|
1328
|
-
},
|
|
1329
|
-
},
|
|
1330
|
-
});
|
|
1331
|
-
if (!init.ok) {
|
|
1332
|
-
return recapMcpSmokeFailure({
|
|
1333
|
-
appUrl,
|
|
1334
|
-
mcpUrl,
|
|
1335
|
-
requiredTools,
|
|
1336
|
-
reason: init.reason,
|
|
1337
|
-
});
|
|
1338
|
-
}
|
|
1339
|
-
const listed = await postRecapMcpRpc({
|
|
1340
|
-
fetchFn,
|
|
1341
|
-
mcpUrl,
|
|
1342
|
-
token,
|
|
1343
|
-
method: "tools/list",
|
|
1344
|
-
id: 2,
|
|
1345
|
-
});
|
|
1346
|
-
if (!listed.ok) {
|
|
1347
|
-
return recapMcpSmokeFailure({
|
|
1348
|
-
appUrl,
|
|
1349
|
-
mcpUrl,
|
|
1350
|
-
requiredTools,
|
|
1351
|
-
reason: listed.reason,
|
|
1352
|
-
});
|
|
1353
|
-
}
|
|
1354
|
-
const tools = extractMcpToolNames(listed.payload);
|
|
1355
|
-
const missing = requiredTools.filter((name) => !tools.includes(name));
|
|
1356
|
-
if (missing.length > 0) {
|
|
1357
|
-
return recapMcpSmokeFailure({
|
|
1358
|
-
appUrl,
|
|
1359
|
-
mcpUrl,
|
|
1360
|
-
tools,
|
|
1361
|
-
requiredTools,
|
|
1362
|
-
reason: `Plan MCP tools/list returned ${tools.length} tools but is missing required publishing tools: ${missing.join(", ")}`,
|
|
1363
|
-
});
|
|
1364
|
-
}
|
|
1365
|
-
return {
|
|
1366
|
-
ok: true,
|
|
1367
|
-
appUrl,
|
|
1368
|
-
mcpUrl,
|
|
1369
|
-
toolCount: tools.length,
|
|
1370
|
-
tools,
|
|
1371
|
-
requiredTools: [...requiredTools],
|
|
1372
|
-
summary: `Plan MCP smoke check passed: tools/list exposes ${requiredTools.join(", ")}.`,
|
|
1373
|
-
};
|
|
1374
|
-
}
|
|
1375
|
-
catch (err) {
|
|
1376
|
-
return recapMcpSmokeFailure({
|
|
1377
|
-
appUrl,
|
|
1378
|
-
mcpUrl,
|
|
1379
|
-
requiredTools,
|
|
1380
|
-
reason: `Plan MCP smoke check could not reach ${mcpUrl}: ${String(err)}`,
|
|
1381
|
-
});
|
|
1382
|
-
}
|
|
1383
|
-
}
|
|
1384
|
-
async function runMcpSmoke(args) {
|
|
1385
|
-
const appUrl = optionalArg(args, "app-url") ??
|
|
1386
|
-
process.env.PLAN_RECAP_APP_URL ??
|
|
1387
|
-
DEFAULT_RECAP_APP_URL;
|
|
1388
|
-
const token = optionalArg(args, "token") ?? process.env.PLAN_RECAP_TOKEN;
|
|
1389
|
-
const result = await smokeRecapMcpTools({ appUrl, token });
|
|
1390
|
-
writeGitHubOutput("ok", result.ok ? "true" : "false");
|
|
1391
|
-
writeGitHubOutput("summary", result.summary);
|
|
1392
|
-
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
1393
|
-
if (!result.ok)
|
|
1394
|
-
process.exitCode = 1;
|
|
1395
|
-
}
|
|
1396
|
-
/**
|
|
1397
|
-
* `recap mcp-config` — write the plan MCP client config for the chosen backend,
|
|
1398
|
-
* replacing the two `node -e '...'` one-liners that previously lived inline in
|
|
1399
|
-
* the agent steps. PLAN_RECAP_TOKEN is read from the environment (claude only),
|
|
1400
|
-
* exactly as before.
|
|
1401
|
-
*/
|
|
1402
|
-
function runMcpConfig(args) {
|
|
1403
|
-
const agent = stringArg(args, "agent").toLowerCase();
|
|
1404
|
-
const appUrl = stringArg(args, "app-url");
|
|
1405
|
-
const force = Boolean(args["force"]);
|
|
1406
|
-
if (agent === "claude") {
|
|
1407
|
-
const token = process.env.PLAN_RECAP_TOKEN;
|
|
1408
|
-
if (!token) {
|
|
1409
|
-
process.stderr.write(`recap mcp-config: PLAN_RECAP_TOKEN is not set.\n` +
|
|
1410
|
-
`Set it in the workflow environment before running this step.\n`);
|
|
1411
|
-
process.exit(1);
|
|
1412
|
-
}
|
|
1413
|
-
const out = stringArg(args, "out");
|
|
1414
|
-
fs.writeFileSync(path.resolve(out), buildRecapClaudeMcpConfig(appUrl, token));
|
|
1415
|
-
process.stdout.write(`${JSON.stringify({ ok: true, agent, out })}\n`);
|
|
1416
|
-
return;
|
|
1417
|
-
}
|
|
1418
|
-
if (agent === "codex") {
|
|
1419
|
-
const out = optionalArg(args, "out") ??
|
|
1420
|
-
path.join(os.homedir(), ".codex", "config.toml");
|
|
1421
|
-
const absOut = path.resolve(out);
|
|
1422
|
-
fs.mkdirSync(path.dirname(absOut), { recursive: true });
|
|
1423
|
-
const newEntry = buildRecapCodexMcpConfig(appUrl);
|
|
1424
|
-
const SECTION_MARKER = "[mcp_servers.plan]";
|
|
1425
|
-
// If the file already exists and is non-empty, merge rather than overwrite.
|
|
1426
|
-
let existing = "";
|
|
1427
|
-
try {
|
|
1428
|
-
const raw = fs.readFileSync(absOut, "utf8");
|
|
1429
|
-
if (raw.trim())
|
|
1430
|
-
existing = raw;
|
|
1431
|
-
}
|
|
1432
|
-
catch {
|
|
1433
|
-
/* file absent — write fresh */
|
|
1434
|
-
}
|
|
1435
|
-
if (existing) {
|
|
1436
|
-
if (existing.includes(SECTION_MARKER)) {
|
|
1437
|
-
// Section already present — skip unless --force was passed.
|
|
1438
|
-
if (!force) {
|
|
1439
|
-
process.stdout.write(`${JSON.stringify({ ok: true, agent, out, skipped: true, reason: "plan entry already present; pass --force to overwrite" })}\n`);
|
|
1440
|
-
return;
|
|
1441
|
-
}
|
|
1442
|
-
// --force: replace the existing [mcp_servers.plan] block.
|
|
1443
|
-
// Remove lines from the section header until the next `[` header or EOF.
|
|
1444
|
-
const lines = existing.split("\n");
|
|
1445
|
-
const startIdx = lines.findIndex((l) => l.trim() === SECTION_MARKER);
|
|
1446
|
-
let endIdx = lines.length;
|
|
1447
|
-
for (let i = startIdx + 1; i < lines.length; i++) {
|
|
1448
|
-
if (lines[i].trimStart().startsWith("[")) {
|
|
1449
|
-
endIdx = i;
|
|
1450
|
-
break;
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
|
-
const without = [
|
|
1454
|
-
...lines.slice(0, startIdx),
|
|
1455
|
-
...lines.slice(endIdx),
|
|
1456
|
-
].join("\n");
|
|
1457
|
-
const merged = (without.trimEnd() ? without.trimEnd() + "\n\n" : "") + newEntry;
|
|
1458
|
-
fs.writeFileSync(absOut, merged, { mode: 0o600 });
|
|
1459
|
-
}
|
|
1460
|
-
else {
|
|
1461
|
-
// Append the new section to the existing config.
|
|
1462
|
-
const separator = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
1463
|
-
fs.writeFileSync(absOut, existing + separator + newEntry, {
|
|
1464
|
-
mode: 0o600,
|
|
1465
|
-
});
|
|
1466
|
-
}
|
|
1467
|
-
}
|
|
1468
|
-
else {
|
|
1469
|
-
fs.writeFileSync(absOut, newEntry);
|
|
1470
|
-
}
|
|
1471
|
-
process.stdout.write(`${JSON.stringify({ ok: true, agent, out })}\n`);
|
|
1472
|
-
return;
|
|
1473
|
-
}
|
|
1474
|
-
throw new Error(`Unknown --agent "${agent}" (expected "claude" or "codex")`);
|
|
1475
|
-
}
|
|
1476
|
-
/* -------------------------------------------------------------------------- */
|
|
1477
1105
|
/* Prompt builder — repo SKILL.md + task wrapper */
|
|
1478
1106
|
/* -------------------------------------------------------------------------- */
|
|
1479
1107
|
/**
|
|
@@ -1591,6 +1219,9 @@ export function buildRecapPrompt(input) {
|
|
|
1591
1219
|
}
|
|
1592
1220
|
if (input.statPath)
|
|
1593
1221
|
lines.push(`- Diff stat: \`${input.statPath}\` (read this file)`);
|
|
1222
|
+
if (!input.localFiles) {
|
|
1223
|
+
lines.push(`- Live plan block reference: \`${input.blockReferencePath ?? "recap-blocks.md"}\` (read this before authoring; it is the workflow-fetched \`get-plan-blocks\` output for the target Plan app).`);
|
|
1224
|
+
}
|
|
1594
1225
|
if (input.huge) {
|
|
1595
1226
|
lines.push(`- The diff is LARGE — produce a **summarized** recap (top files + schema/API deltas), not an exhaustive one. The diff was truncated at the size cap — \`${input.statPath ?? "recap.stat"}\` contains the complete file list with per-file stats; for any file missing from \`${input.diffPath}\`, fetch it directly with \`git diff <base>...<head> -- <path>\`.`);
|
|
1596
1227
|
}
|
|
@@ -1603,21 +1234,18 @@ export function buildRecapPrompt(input) {
|
|
|
1603
1234
|
lines.push("3. Write the returned `url` from that command to `recap-url.txt` at the repo root, containing exactly one line. This file is the workflow's only hand-off.");
|
|
1604
1235
|
}
|
|
1605
1236
|
else {
|
|
1606
|
-
lines.push("##
|
|
1607
|
-
lines.push(`The
|
|
1608
|
-
lines.push("
|
|
1609
|
-
lines.push("
|
|
1610
|
-
lines.push(
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
? `, and also passing \`sourceUrl: "${prSourceUrl}"\` so the hosted recap page can link back to the PR`
|
|
1614
|
-
: ""}.`);
|
|
1615
|
-
lines.push("If `create-visual-recap` returns validation feedback about empty or invalid wireframes, make one immediate correction pass in this same process: revise the named WireframeBlock/Artboard MDX so each frame has real visible product text/controls, then call `create-visual-recap` again. Do not write `recap-url.txt` until the tool succeeds.");
|
|
1616
|
-
lines.push(`2. Write the plan URL to a file named \`recap-url.txt\` at the repo root, containing exactly one line: \`${appUrl}/recaps/<the returned plan id>\`. This file is the workflow's only hand-off — do not print anything else as the deliverable.`);
|
|
1617
|
-
lines.push(`3. (Fallback only — skip if step 1 succeeded) If \`create-visual-recap\` does not accept a \`visibility\` parameter (older server), call the **set-resource-visibility** tool with \`{ resourceType: "plan", resourceId: <the returned plan id>, visibility: "org" }\` after publishing.`);
|
|
1237
|
+
lines.push("## Author Source (this is the only way to produce output)");
|
|
1238
|
+
lines.push(`The workflow has already fetched the live \`get-plan-blocks\` output into \`${input.blockReferencePath ?? "recap-blocks.md"}\`. Read that file and treat it as the authoritative block/tag/schema reference for this run.`);
|
|
1239
|
+
lines.push("Do NOT call the Plan MCP server and do NOT try to publish the recap yourself. CI publishes deterministically after you write the source file, which avoids host MCP registration flake.");
|
|
1240
|
+
lines.push("This is a one-shot GitHub Actions run. Do not wait, sleep, back off, schedule wakeups, reminders, follow-ups, or retries in another turn. Either write `recap-source.json` in this process, or report why source authoring failed plainly.");
|
|
1241
|
+
lines.push("1. Author grounded MDX recap source derived ONLY from the real diff. The final file must be valid JSON, not Markdown, not prose, and not a tool-call transcript.");
|
|
1242
|
+
lines.push('2. Write a file named `recap-source.json` at the repo root with exactly this shape: `{ "title": string, "brief": string, "mdx": { "plan.mdx": string, "canvas.mdx"?: string, "prototype.mdx"?: string, ".plan-state.json"?: string, "assets/"?: { [filename: string]: string } } }`.');
|
|
1243
|
+
lines.push("3. Do not write `recap-url.txt`; the deterministic CLI publisher writes that after it successfully POSTs your source to `create-visual-recap`.");
|
|
1618
1244
|
}
|
|
1619
1245
|
lines.push("");
|
|
1620
|
-
lines.push(
|
|
1246
|
+
lines.push(input.localFiles
|
|
1247
|
+
? "Do not invent file names, schema fields, or endpoints. Redact anything that looks like a secret. If the diff has no reviewable substance, still create a minimal local recap and write recap-url.txt from the local preview command. (CI already gated tiny diffs before invoking you — ignore the skill's advice to skip small diffs; always produce output.)"
|
|
1248
|
+
: "Do not invent file names, schema fields, or endpoints. Redact anything that looks like a secret. If the diff has no reviewable substance, still write a minimal `recap-source.json`. (CI already gated tiny diffs before invoking you — ignore the skill's advice to skip small diffs; always produce output.)");
|
|
1621
1249
|
lines.push("");
|
|
1622
1250
|
lines.push("## Depth preflight");
|
|
1623
1251
|
lines.push("Before authoring the recap, read the diff/stat and make a quick surface/state inventory of changed files, routes/actions, rendered UI surfaces, popovers/dialogs, role/access states, empty/error states, and shared abstractions. The published recap must cover each meaningful item with a structured block or intentionally omit it because it is tiny, redundant, or not user-visible.");
|
|
@@ -1625,7 +1253,9 @@ export function buildRecapPrompt(input) {
|
|
|
1625
1253
|
lines.push("");
|
|
1626
1254
|
lines.push("---");
|
|
1627
1255
|
lines.push("");
|
|
1628
|
-
lines.push("# visual-recap skill
|
|
1256
|
+
lines.push("# visual-recap skill — use for recap CONTENT and structure");
|
|
1257
|
+
lines.push("");
|
|
1258
|
+
lines.push("Follow the skill below for WHAT makes a good recap: which blocks to use, grounding, house style, and review depth. IGNORE its publishing and hand-off instructions — in this run you have NO Plan MCP tools and must NOT publish the recap yourself. Publishing is handled exactly as described above (write the source file; CI publishes it deterministically).");
|
|
1629
1259
|
lines.push("");
|
|
1630
1260
|
lines.push(input.skillMd.trim());
|
|
1631
1261
|
lines.push("");
|
|
@@ -1664,6 +1294,20 @@ async function githubRequest(token, apiPath, init = {}, fetchFn = fetch) {
|
|
|
1664
1294
|
return undefined;
|
|
1665
1295
|
return (await res.json());
|
|
1666
1296
|
}
|
|
1297
|
+
export async function isPullRequestHeadCurrent(input) {
|
|
1298
|
+
const expected = input.headSha.trim();
|
|
1299
|
+
if (!expected)
|
|
1300
|
+
return null;
|
|
1301
|
+
const fn = input.fetchFn ?? fetch;
|
|
1302
|
+
try {
|
|
1303
|
+
const pr = await githubRequest(input.token, `/repos/${encodeURIComponent(input.owner)}/${encodeURIComponent(input.repo)}/pulls/${encodeURIComponent(input.issue)}`, {}, fn);
|
|
1304
|
+
const current = pr.head?.sha?.trim();
|
|
1305
|
+
return current ? current === expected : null;
|
|
1306
|
+
}
|
|
1307
|
+
catch {
|
|
1308
|
+
return null;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1667
1311
|
export async function findExistingComment(input) {
|
|
1668
1312
|
const fn = input.fetchFn ?? fetch;
|
|
1669
1313
|
for (let page = 1;; page += 1) {
|
|
@@ -1738,6 +1382,10 @@ function trustedRecapImageUrl(raw, base) {
|
|
|
1738
1382
|
/** Build the sticky comment body from the workflow's environment. */
|
|
1739
1383
|
export function buildCommentBody(env = process.env) {
|
|
1740
1384
|
const lines = [MARKER];
|
|
1385
|
+
const headSha = (env.HEAD_SHA || "").trim();
|
|
1386
|
+
const headMarker = /^[a-f0-9]{7,64}$/i.test(headSha)
|
|
1387
|
+
? `<!-- head-sha: ${headSha} -->`
|
|
1388
|
+
: "";
|
|
1741
1389
|
// Last-known plan id threaded from the previous run (supplied via PREV_PLAN_ID
|
|
1742
1390
|
// when the comment is rebuilt from scratch, or parsed from the env on upsert).
|
|
1743
1391
|
// We always emit the plan-id marker when any plan id is known so that a
|
|
@@ -1760,6 +1408,8 @@ export function buildCommentBody(env = process.env) {
|
|
|
1760
1408
|
lines.push(`Reason: \`${reason}\`.`);
|
|
1761
1409
|
if (prevPlanId)
|
|
1762
1410
|
lines.push("", `<!-- plan-id: ${prevPlanId} -->`);
|
|
1411
|
+
if (headMarker)
|
|
1412
|
+
lines.push("", headMarker);
|
|
1763
1413
|
return lines.join("\n");
|
|
1764
1414
|
}
|
|
1765
1415
|
// Tiny diffs aren't worth a recap. The workflow upserts this state as a sticky
|
|
@@ -1771,6 +1421,8 @@ export function buildCommentBody(env = process.env) {
|
|
|
1771
1421
|
lines.push("The change in this pull request is too small to be worth a visual recap. This is informational only and does **not** block the PR.");
|
|
1772
1422
|
if (prevPlanId)
|
|
1773
1423
|
lines.push("", `<!-- plan-id: ${prevPlanId} -->`);
|
|
1424
|
+
if (headMarker)
|
|
1425
|
+
lines.push("", headMarker);
|
|
1774
1426
|
return lines.join("\n");
|
|
1775
1427
|
}
|
|
1776
1428
|
const planUrl = (env.PLAN_URL || "").trim();
|
|
@@ -1813,6 +1465,8 @@ export function buildCommentBody(env = process.env) {
|
|
|
1813
1465
|
}
|
|
1814
1466
|
if (markerPlanId)
|
|
1815
1467
|
lines.push("", `<!-- plan-id: ${markerPlanId} -->`);
|
|
1468
|
+
if (headMarker)
|
|
1469
|
+
lines.push("", headMarker);
|
|
1816
1470
|
return lines.join("\n");
|
|
1817
1471
|
}
|
|
1818
1472
|
// Image URLs are produced by our own recap-image route, but validate each is
|
|
@@ -1840,6 +1494,8 @@ export function buildCommentBody(env = process.env) {
|
|
|
1840
1494
|
lines.push("> Large diff — this recap is a **summarized** view (top files + schema/API deltas).");
|
|
1841
1495
|
}
|
|
1842
1496
|
lines.push("", `<!-- plan-id: ${planId} -->`);
|
|
1497
|
+
if (headMarker)
|
|
1498
|
+
lines.push("", headMarker);
|
|
1843
1499
|
return lines.join("\n");
|
|
1844
1500
|
}
|
|
1845
1501
|
/* -------------------------------------------------------------------------- */
|
|
@@ -1898,6 +1554,7 @@ function runBuildPrompt(args) {
|
|
|
1898
1554
|
appUrl: optionalArg(args, "app-url") ?? "https://plan.agent-native.com",
|
|
1899
1555
|
diffPath,
|
|
1900
1556
|
statPath: optionalArg(args, "stat"),
|
|
1557
|
+
blockReferencePath: optionalArg(args, "block-reference"),
|
|
1901
1558
|
prevPlanId: optionalArg(args, "prev-plan-id"),
|
|
1902
1559
|
huge: args.huge === true || args.huge === "true",
|
|
1903
1560
|
localFiles: args["local-files"] === true || args["local-files"] === "true",
|
|
@@ -1910,6 +1567,298 @@ function runBuildPrompt(args) {
|
|
|
1910
1567
|
fs.writeFileSync(path.resolve(out), prompt);
|
|
1911
1568
|
process.stdout.write(`${JSON.stringify({ ok: true, out, skillSource: skill.source, bytes: prompt.length })}\n`);
|
|
1912
1569
|
}
|
|
1570
|
+
const RECAP_SOURCE_FILENAME = "recap-source.json";
|
|
1571
|
+
const RECAP_URL_REASON_FILENAME = "recap-url-reason.txt";
|
|
1572
|
+
const RECAP_HTTP_TIMEOUT_MS = 45_000;
|
|
1573
|
+
function writeRecapUrlReason(reason, cwd = process.cwd()) {
|
|
1574
|
+
fs.writeFileSync(path.join(cwd, RECAP_URL_REASON_FILENAME), `${sanitizeAgentFailureSummary(reason, 1000)}\n`);
|
|
1575
|
+
}
|
|
1576
|
+
function readRecapUrlReason(cwd = process.cwd()) {
|
|
1577
|
+
return readTextIfExists(path.join(cwd, RECAP_URL_REASON_FILENAME));
|
|
1578
|
+
}
|
|
1579
|
+
function validateRecapSourcePayload(value) {
|
|
1580
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1581
|
+
throw new Error(`${RECAP_SOURCE_FILENAME} must contain a JSON object.`);
|
|
1582
|
+
}
|
|
1583
|
+
const obj = value;
|
|
1584
|
+
if (obj.title !== undefined && typeof obj.title !== "string") {
|
|
1585
|
+
throw new Error(`${RECAP_SOURCE_FILENAME} title must be a string.`);
|
|
1586
|
+
}
|
|
1587
|
+
if (obj.brief !== undefined && typeof obj.brief !== "string") {
|
|
1588
|
+
throw new Error(`${RECAP_SOURCE_FILENAME} brief must be a string.`);
|
|
1589
|
+
}
|
|
1590
|
+
if (!obj.mdx || typeof obj.mdx !== "object" || Array.isArray(obj.mdx)) {
|
|
1591
|
+
throw new Error(`${RECAP_SOURCE_FILENAME} must include an mdx object.`);
|
|
1592
|
+
}
|
|
1593
|
+
const mdx = obj.mdx;
|
|
1594
|
+
if (typeof mdx["plan.mdx"] !== "string" || !mdx["plan.mdx"].trim()) {
|
|
1595
|
+
throw new Error(`${RECAP_SOURCE_FILENAME} mdx["plan.mdx"] must be a non-empty string.`);
|
|
1596
|
+
}
|
|
1597
|
+
for (const key of ["canvas.mdx", "prototype.mdx", ".plan-state.json"]) {
|
|
1598
|
+
if (mdx[key] !== undefined && typeof mdx[key] !== "string") {
|
|
1599
|
+
throw new Error(`${RECAP_SOURCE_FILENAME} mdx["${key}"] must be a string when present.`);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
const assets = mdx["assets/"];
|
|
1603
|
+
if (assets !== undefined) {
|
|
1604
|
+
if (!assets || typeof assets !== "object" || Array.isArray(assets)) {
|
|
1605
|
+
throw new Error(`${RECAP_SOURCE_FILENAME} mdx["assets/"] must be an object when present.`);
|
|
1606
|
+
}
|
|
1607
|
+
for (const [name, body] of Object.entries(assets)) {
|
|
1608
|
+
if (typeof body !== "string") {
|
|
1609
|
+
throw new Error(`${RECAP_SOURCE_FILENAME} asset ${JSON.stringify(name)} must be a string.`);
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
return {
|
|
1614
|
+
...(typeof obj.title === "string" ? { title: obj.title } : {}),
|
|
1615
|
+
...(typeof obj.brief === "string" ? { brief: obj.brief } : {}),
|
|
1616
|
+
mdx,
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
export function readRecapSourcePayload(filePath = RECAP_SOURCE_FILENAME) {
|
|
1620
|
+
const abs = path.resolve(filePath);
|
|
1621
|
+
let text;
|
|
1622
|
+
try {
|
|
1623
|
+
text = fs.readFileSync(abs, "utf8");
|
|
1624
|
+
}
|
|
1625
|
+
catch (err) {
|
|
1626
|
+
throw new Error(`${RECAP_SOURCE_FILENAME} was not created by the agent (${String(err)}).`);
|
|
1627
|
+
}
|
|
1628
|
+
let parsed;
|
|
1629
|
+
try {
|
|
1630
|
+
parsed = JSON.parse(text);
|
|
1631
|
+
}
|
|
1632
|
+
catch (err) {
|
|
1633
|
+
throw new Error(`${RECAP_SOURCE_FILENAME} was not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
1634
|
+
}
|
|
1635
|
+
return validateRecapSourcePayload(parsed);
|
|
1636
|
+
}
|
|
1637
|
+
function recapActionEndpoint(appUrl, action) {
|
|
1638
|
+
return `${appUrl.replace(/\/$/, "")}/_agent-native/actions/${action}`;
|
|
1639
|
+
}
|
|
1640
|
+
async function fetchJsonWithTimeout(url, init, fetchFn) {
|
|
1641
|
+
return await fetchFn(url, {
|
|
1642
|
+
...init,
|
|
1643
|
+
signal: init.signal ?? AbortSignal.timeout(RECAP_HTTP_TIMEOUT_MS),
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
export async function fetchRecapBlockReference(input) {
|
|
1647
|
+
const fetchFn = input.fetchFn ?? fetch;
|
|
1648
|
+
const out = input.out ?? "recap-blocks.md";
|
|
1649
|
+
const endpoint = new URL(recapActionEndpoint(input.appUrl, "get-plan-blocks"));
|
|
1650
|
+
endpoint.searchParams.set("format", "reference");
|
|
1651
|
+
const response = await fetchJsonWithTimeout(endpoint.toString(), { method: "GET", headers: { accept: "application/json" } }, fetchFn);
|
|
1652
|
+
if (!response.ok) {
|
|
1653
|
+
const detail = await response.text().catch(() => "");
|
|
1654
|
+
throw new Error(`get-plan-blocks failed ${response.status} ${response.statusText}: ${sanitizeAgentFailureSummary(detail, 500)}`);
|
|
1655
|
+
}
|
|
1656
|
+
const json = (await response.json().catch(() => null));
|
|
1657
|
+
if (!json?.reference) {
|
|
1658
|
+
throw new Error("get-plan-blocks returned no reference text.");
|
|
1659
|
+
}
|
|
1660
|
+
fs.writeFileSync(path.resolve(out), json.reference);
|
|
1661
|
+
return { ok: true, out, count: json.count };
|
|
1662
|
+
}
|
|
1663
|
+
function recapUrlFromPublishResult(result, appUrl) {
|
|
1664
|
+
const candidates = [];
|
|
1665
|
+
const ids = [];
|
|
1666
|
+
const visit = (value, depth = 0) => {
|
|
1667
|
+
if (!value || typeof value !== "object" || depth > 3)
|
|
1668
|
+
return;
|
|
1669
|
+
const obj = value;
|
|
1670
|
+
for (const key of ["webUrl", "url", "path", "href"]) {
|
|
1671
|
+
const candidate = obj[key];
|
|
1672
|
+
if (typeof candidate === "string")
|
|
1673
|
+
candidates.push(candidate);
|
|
1674
|
+
}
|
|
1675
|
+
for (const key of ["planId", "id"]) {
|
|
1676
|
+
const candidate = obj[key];
|
|
1677
|
+
if (typeof candidate === "string" &&
|
|
1678
|
+
/^[A-Za-z0-9_-]{1,80}$/.test(candidate)) {
|
|
1679
|
+
ids.push(candidate);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
for (const key of ["plan", "openLink", "link", "result"]) {
|
|
1683
|
+
visit(obj[key], depth + 1);
|
|
1684
|
+
}
|
|
1685
|
+
};
|
|
1686
|
+
visit(result);
|
|
1687
|
+
for (const candidate of candidates) {
|
|
1688
|
+
const canonical = canonicalRecapUrl(candidate, appUrl);
|
|
1689
|
+
if (canonical)
|
|
1690
|
+
return canonical;
|
|
1691
|
+
}
|
|
1692
|
+
for (const id of ids) {
|
|
1693
|
+
const canonical = canonicalRecapUrl(`/recaps/${id}`, appUrl);
|
|
1694
|
+
if (canonical)
|
|
1695
|
+
return canonical;
|
|
1696
|
+
}
|
|
1697
|
+
return "";
|
|
1698
|
+
}
|
|
1699
|
+
function shouldRetryRecapPublish(status) {
|
|
1700
|
+
return (status === 408 ||
|
|
1701
|
+
status === 409 ||
|
|
1702
|
+
status === 425 ||
|
|
1703
|
+
status === 429 ||
|
|
1704
|
+
status >= 500);
|
|
1705
|
+
}
|
|
1706
|
+
function recapPublishIdempotencyKey(input) {
|
|
1707
|
+
const identity = input.prevPlanId
|
|
1708
|
+
? `plan:${input.prevPlanId}`
|
|
1709
|
+
: input.repo && input.pr
|
|
1710
|
+
? `github-pr:${input.repo}:${input.pr}`
|
|
1711
|
+
: input.sourceUrl
|
|
1712
|
+
? `source-url:${input.sourceUrl}`
|
|
1713
|
+
: `source-path:${path.resolve(input.sourcePath)}`;
|
|
1714
|
+
return `visual-recap-${createHash("sha256").update(identity).digest("hex")}`;
|
|
1715
|
+
}
|
|
1716
|
+
export async function publishRecapSource(input) {
|
|
1717
|
+
const cwd = input.cwd ?? process.cwd();
|
|
1718
|
+
const sourcePath = input.sourcePath ?? path.join(cwd, RECAP_SOURCE_FILENAME);
|
|
1719
|
+
const out = input.out ?? path.join(cwd, "recap-url.txt");
|
|
1720
|
+
const token = input.token.trim();
|
|
1721
|
+
if (!token)
|
|
1722
|
+
throw new Error("PLAN_RECAP_TOKEN is empty.");
|
|
1723
|
+
const source = readRecapSourcePayload(sourcePath);
|
|
1724
|
+
const sourceUrl = input.sourceUrl ??
|
|
1725
|
+
(input.repo && input.pr
|
|
1726
|
+
? `https://github.com/${input.repo}/pull/${input.pr}`
|
|
1727
|
+
: undefined);
|
|
1728
|
+
const idempotencyKey = recapPublishIdempotencyKey({
|
|
1729
|
+
prevPlanId: input.prevPlanId,
|
|
1730
|
+
repo: input.repo,
|
|
1731
|
+
pr: input.pr,
|
|
1732
|
+
sourcePath,
|
|
1733
|
+
sourceUrl,
|
|
1734
|
+
});
|
|
1735
|
+
const body = {
|
|
1736
|
+
...(input.prevPlanId ? { planId: input.prevPlanId } : {}),
|
|
1737
|
+
idempotencyKey,
|
|
1738
|
+
...(source.title ? { title: source.title } : {}),
|
|
1739
|
+
...(source.brief ? { brief: source.brief } : {}),
|
|
1740
|
+
visibility: "org",
|
|
1741
|
+
source: "imported",
|
|
1742
|
+
...(input.repo ? { repoPath: input.repo } : {}),
|
|
1743
|
+
...(sourceUrl ? { sourceUrl } : {}),
|
|
1744
|
+
currentFocus: "visual recap review",
|
|
1745
|
+
status: "review",
|
|
1746
|
+
mdx: source.mdx,
|
|
1747
|
+
};
|
|
1748
|
+
const endpoint = recapActionEndpoint(input.appUrl, "create-visual-recap");
|
|
1749
|
+
const fetchFn = input.fetchFn ?? fetch;
|
|
1750
|
+
let lastError = "";
|
|
1751
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
1752
|
+
try {
|
|
1753
|
+
const response = await fetchJsonWithTimeout(endpoint, {
|
|
1754
|
+
method: "POST",
|
|
1755
|
+
headers: {
|
|
1756
|
+
accept: "application/json",
|
|
1757
|
+
"content-type": "application/json",
|
|
1758
|
+
authorization: `Bearer ${token}`,
|
|
1759
|
+
"Idempotency-Key": idempotencyKey,
|
|
1760
|
+
"X-Idempotency-Key": idempotencyKey,
|
|
1761
|
+
},
|
|
1762
|
+
body: JSON.stringify(body),
|
|
1763
|
+
}, fetchFn);
|
|
1764
|
+
const text = await response.text().catch((err) => String(err));
|
|
1765
|
+
if (!response.ok) {
|
|
1766
|
+
lastError = `create-visual-recap failed ${response.status} ${response.statusText}: ${sanitizeAgentFailureSummary(text, 800)}`;
|
|
1767
|
+
if (attempt < 3 && shouldRetryRecapPublish(response.status)) {
|
|
1768
|
+
await delay(attempt * 2000);
|
|
1769
|
+
continue;
|
|
1770
|
+
}
|
|
1771
|
+
throw new Error(lastError);
|
|
1772
|
+
}
|
|
1773
|
+
let result = null;
|
|
1774
|
+
try {
|
|
1775
|
+
result = text ? JSON.parse(text) : null;
|
|
1776
|
+
}
|
|
1777
|
+
catch {
|
|
1778
|
+
throw new Error("create-visual-recap returned non-JSON output.");
|
|
1779
|
+
}
|
|
1780
|
+
const url = recapUrlFromPublishResult(result, input.appUrl);
|
|
1781
|
+
if (!url) {
|
|
1782
|
+
throw new Error("create-visual-recap succeeded but did not return a usable /recaps/<id> URL or plan id.");
|
|
1783
|
+
}
|
|
1784
|
+
fs.writeFileSync(path.resolve(out), `${url}\n`);
|
|
1785
|
+
try {
|
|
1786
|
+
fs.rmSync(path.join(cwd, RECAP_URL_REASON_FILENAME), { force: true });
|
|
1787
|
+
}
|
|
1788
|
+
catch {
|
|
1789
|
+
/* ignore */
|
|
1790
|
+
}
|
|
1791
|
+
return { ok: true, url, out };
|
|
1792
|
+
}
|
|
1793
|
+
catch (err) {
|
|
1794
|
+
lastError = err instanceof Error ? err.message : String(err);
|
|
1795
|
+
if (attempt < 3 &&
|
|
1796
|
+
/fetch failed|network|timeout|timed out|ECONNRESET|ETIMEDOUT/i.test(lastError)) {
|
|
1797
|
+
await delay(attempt * 2000);
|
|
1798
|
+
continue;
|
|
1799
|
+
}
|
|
1800
|
+
throw new Error(lastError);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
throw new Error(lastError || "create-visual-recap failed.");
|
|
1804
|
+
}
|
|
1805
|
+
async function runBlockReference(args) {
|
|
1806
|
+
const appUrl = optionalArg(args, "app-url") ??
|
|
1807
|
+
process.env.PLAN_RECAP_APP_URL ??
|
|
1808
|
+
DEFAULT_RECAP_APP_URL;
|
|
1809
|
+
const out = optionalArg(args, "out") ?? "recap-blocks.md";
|
|
1810
|
+
try {
|
|
1811
|
+
const result = await fetchRecapBlockReference({ appUrl, out });
|
|
1812
|
+
writeGitHubOutput("ok", "true");
|
|
1813
|
+
writeGitHubOutput("out", result.out);
|
|
1814
|
+
writeGitHubOutput("reason", "");
|
|
1815
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
1816
|
+
}
|
|
1817
|
+
catch (err) {
|
|
1818
|
+
const reason = sanitizeAgentFailureSummary(err instanceof Error ? err.message : String(err), 1000);
|
|
1819
|
+
writeRecapUrlReason(reason);
|
|
1820
|
+
writeGitHubOutput("ok", "false");
|
|
1821
|
+
writeGitHubOutput("out", "");
|
|
1822
|
+
writeGitHubOutput("reason", reason);
|
|
1823
|
+
process.stdout.write(`${JSON.stringify({ ok: false, reason })}\n`);
|
|
1824
|
+
process.exitCode = 1;
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
async function runPublish(args) {
|
|
1828
|
+
const appUrl = optionalArg(args, "app-url") ??
|
|
1829
|
+
process.env.PLAN_RECAP_APP_URL ??
|
|
1830
|
+
DEFAULT_RECAP_APP_URL;
|
|
1831
|
+
const token = optionalArg(args, "token") ?? process.env.PLAN_RECAP_TOKEN ?? "";
|
|
1832
|
+
const out = optionalArg(args, "out") ?? "recap-url.txt";
|
|
1833
|
+
const done = (obj) => {
|
|
1834
|
+
process.stdout.write(`${JSON.stringify(obj)}\n`);
|
|
1835
|
+
};
|
|
1836
|
+
try {
|
|
1837
|
+
const result = await publishRecapSource({
|
|
1838
|
+
appUrl,
|
|
1839
|
+
token,
|
|
1840
|
+
sourcePath: optionalArg(args, "source") ?? RECAP_SOURCE_FILENAME,
|
|
1841
|
+
out,
|
|
1842
|
+
prevPlanId: optionalArg(args, "prev-plan-id"),
|
|
1843
|
+
repo: optionalArg(args, "repo") ?? process.env.GITHUB_REPOSITORY,
|
|
1844
|
+
pr: optionalArg(args, "pr") ?? process.env.PR_NUMBER,
|
|
1845
|
+
sourceUrl: optionalArg(args, "source-url"),
|
|
1846
|
+
});
|
|
1847
|
+
writeGitHubOutput("ok", "true");
|
|
1848
|
+
writeGitHubOutput("plan_url", result.url);
|
|
1849
|
+
writeGitHubOutput("reason", "");
|
|
1850
|
+
done(result);
|
|
1851
|
+
}
|
|
1852
|
+
catch (err) {
|
|
1853
|
+
const reason = sanitizeAgentFailureSummary(err instanceof Error ? err.message : String(err), 1000);
|
|
1854
|
+
writeRecapUrlReason(reason);
|
|
1855
|
+
writeGitHubOutput("ok", "false");
|
|
1856
|
+
writeGitHubOutput("plan_url", "");
|
|
1857
|
+
writeGitHubOutput("reason", reason);
|
|
1858
|
+
done({ ok: false, reason });
|
|
1859
|
+
process.exitCode = 1;
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1913
1862
|
function delay(ms) {
|
|
1914
1863
|
return ms > 0
|
|
1915
1864
|
? new Promise((resolve) => setTimeout(resolve, ms))
|
|
@@ -2012,6 +1961,47 @@ async function defaultImportPlaywright() {
|
|
|
2012
1961
|
return (await import("@playwright/test"));
|
|
2013
1962
|
}
|
|
2014
1963
|
}
|
|
1964
|
+
const RECAP_SYSTEM_CHROME_EXECUTABLES = [
|
|
1965
|
+
"/usr/bin/google-chrome-stable",
|
|
1966
|
+
"/usr/bin/google-chrome",
|
|
1967
|
+
"/usr/bin/chromium-browser",
|
|
1968
|
+
"/usr/bin/chromium",
|
|
1969
|
+
];
|
|
1970
|
+
function shouldTrySystemChromeFallback(err) {
|
|
1971
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1972
|
+
return /Executable doesn't exist|playwright install|browser.*not found|chromium.*not found/i.test(message);
|
|
1973
|
+
}
|
|
1974
|
+
export async function launchRecapChromium(chromium) {
|
|
1975
|
+
const launchOptions = { args: ["--no-sandbox"] };
|
|
1976
|
+
try {
|
|
1977
|
+
return await chromium.launch(launchOptions);
|
|
1978
|
+
}
|
|
1979
|
+
catch (err) {
|
|
1980
|
+
if (!shouldTrySystemChromeFallback(err))
|
|
1981
|
+
throw err;
|
|
1982
|
+
const fallbackErrors = [];
|
|
1983
|
+
for (const executablePath of RECAP_SYSTEM_CHROME_EXECUTABLES) {
|
|
1984
|
+
if (!fs.existsSync(executablePath))
|
|
1985
|
+
continue;
|
|
1986
|
+
try {
|
|
1987
|
+
process.stderr.write(`[recap shot] Playwright browser unavailable; trying system Chrome at ${executablePath}\n`);
|
|
1988
|
+
return await chromium.launch({ ...launchOptions, executablePath });
|
|
1989
|
+
}
|
|
1990
|
+
catch (fallbackErr) {
|
|
1991
|
+
const message = fallbackErr instanceof Error
|
|
1992
|
+
? fallbackErr.message
|
|
1993
|
+
: String(fallbackErr);
|
|
1994
|
+
fallbackErrors.push(`${executablePath}: ${message}`);
|
|
1995
|
+
process.stderr.write(`[recap shot] system Chrome launch failed at ${executablePath}: ${message}\n`);
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
if (fallbackErrors.length) {
|
|
1999
|
+
const originalMessage = err instanceof Error ? err.message : String(err);
|
|
2000
|
+
throw new Error(`${originalMessage}; system Chrome fallback failed (${fallbackErrors.join("; ")})`, { cause: err });
|
|
2001
|
+
}
|
|
2002
|
+
throw err;
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2015
2005
|
function parseRecapScreenshotTheme(value) {
|
|
2016
2006
|
if (value === undefined)
|
|
2017
2007
|
return undefined;
|
|
@@ -2080,13 +2070,14 @@ importPlaywright = defaultImportPlaywright) {
|
|
|
2080
2070
|
return;
|
|
2081
2071
|
}
|
|
2082
2072
|
let captured = false;
|
|
2073
|
+
let reason = "";
|
|
2083
2074
|
let browser;
|
|
2084
2075
|
const hardTimer = setTimeout(() => {
|
|
2085
2076
|
done({ ok: false, reason: "hard 60s timeout reached" });
|
|
2086
2077
|
process.exit(0);
|
|
2087
2078
|
}, 60_000);
|
|
2088
2079
|
try {
|
|
2089
|
-
browser = await chromium
|
|
2080
|
+
browser = await launchRecapChromium(chromium);
|
|
2090
2081
|
const context = await browser.newContext({
|
|
2091
2082
|
viewport: RECAP_SHOT_VIEWPORT,
|
|
2092
2083
|
deviceScaleFactor: RECAP_SHOT_DEVICE_SCALE_FACTOR,
|
|
@@ -2211,12 +2202,20 @@ importPlaywright = defaultImportPlaywright) {
|
|
|
2211
2202
|
});
|
|
2212
2203
|
await page.waitForTimeout(250);
|
|
2213
2204
|
await page.screenshot({ path: out });
|
|
2214
|
-
// If the captured PNG is over the upload cap,
|
|
2215
|
-
//
|
|
2205
|
+
// If the captured PNG is over the upload cap, retry at CSS-pixel scale
|
|
2206
|
+
// before giving up. The server route rejects oversized files, and the
|
|
2207
|
+
// GitHub comment can only embed an image after a successful upload.
|
|
2216
2208
|
const firstSize = fs.existsSync(out) ? fs.statSync(out).size : 0;
|
|
2217
2209
|
if (firstSize > RECAP_SHOT_MAX_BYTES) {
|
|
2218
|
-
process.stderr.write(`[recap shot] PNG is ${firstSize} bytes (cap ${RECAP_SHOT_MAX_BYTES}) —
|
|
2210
|
+
process.stderr.write(`[recap shot] PNG is ${firstSize} bytes (cap ${RECAP_SHOT_MAX_BYTES}) — retrying at CSS-pixel scale\n`);
|
|
2219
2211
|
fs.unlinkSync(out);
|
|
2212
|
+
await page.screenshot({ path: out, scale: "css" });
|
|
2213
|
+
const retrySize = fs.existsSync(out) ? fs.statSync(out).size : 0;
|
|
2214
|
+
if (retrySize > RECAP_SHOT_MAX_BYTES) {
|
|
2215
|
+
reason = `screenshot PNG exceeded upload cap (${retrySize} bytes > ${RECAP_SHOT_MAX_BYTES})`;
|
|
2216
|
+
process.stderr.write(`[recap shot] ${reason}; skipping upload\n`);
|
|
2217
|
+
fs.unlinkSync(out);
|
|
2218
|
+
}
|
|
2220
2219
|
}
|
|
2221
2220
|
captured = fs.existsSync(out);
|
|
2222
2221
|
await browser.close();
|
|
@@ -2240,8 +2239,12 @@ importPlaywright = defaultImportPlaywright) {
|
|
|
2240
2239
|
let imageUrl = null;
|
|
2241
2240
|
if (captured && token && appUrl) {
|
|
2242
2241
|
imageUrl = await uploadRecapImage({ appUrl, token, pngPath: out });
|
|
2242
|
+
if (!imageUrl) {
|
|
2243
|
+
reason = "screenshot captured but image upload failed";
|
|
2244
|
+
}
|
|
2243
2245
|
}
|
|
2244
|
-
|
|
2246
|
+
const ok = captured && (!(token && appUrl) || !!imageUrl);
|
|
2247
|
+
done({ ok, out, imageUrl, ...(reason ? { reason } : {}) });
|
|
2245
2248
|
}
|
|
2246
2249
|
async function runComment(args, sub) {
|
|
2247
2250
|
const token = stringArg(args, "token");
|
|
@@ -2259,6 +2262,24 @@ async function runComment(args, sub) {
|
|
|
2259
2262
|
return;
|
|
2260
2263
|
}
|
|
2261
2264
|
if (sub === "upsert") {
|
|
2265
|
+
const headSha = optionalArg(args, "head-sha") ?? process.env.HEAD_SHA ?? "";
|
|
2266
|
+
if (headSha) {
|
|
2267
|
+
const current = await isPullRequestHeadCurrent({
|
|
2268
|
+
token,
|
|
2269
|
+
owner,
|
|
2270
|
+
repo,
|
|
2271
|
+
issue,
|
|
2272
|
+
headSha,
|
|
2273
|
+
});
|
|
2274
|
+
if (current === false) {
|
|
2275
|
+
process.stdout.write(`${JSON.stringify({
|
|
2276
|
+
action: "skipped",
|
|
2277
|
+
id: 0,
|
|
2278
|
+
reason: "stale head sha",
|
|
2279
|
+
})}\n`);
|
|
2280
|
+
return;
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2262
2283
|
const result = await upsertComment({
|
|
2263
2284
|
token,
|
|
2264
2285
|
owner,
|
|
@@ -2617,13 +2638,16 @@ export function canonicalRecapUrl(rawUrl, appUrl) {
|
|
|
2617
2638
|
}
|
|
2618
2639
|
}
|
|
2619
2640
|
export function inferLocalRecapUrlFailureReason(input = {}) {
|
|
2620
|
-
const
|
|
2641
|
+
const cwd = input.cwd ?? process.cwd();
|
|
2642
|
+
const explicitReason = readRecapUrlReason(cwd);
|
|
2643
|
+
const recapUrlPath = path.join(cwd, "recap-url.txt");
|
|
2621
2644
|
const raw = readTextIfExists(recapUrlPath);
|
|
2622
|
-
if (raw === null)
|
|
2623
|
-
return "recap-url.txt was not created
|
|
2645
|
+
if (raw === null) {
|
|
2646
|
+
return explicitReason?.trim() || "recap-url.txt was not created.";
|
|
2647
|
+
}
|
|
2624
2648
|
const value = raw.replace(/[\r\n\s]/g, "");
|
|
2625
2649
|
if (!value)
|
|
2626
|
-
return "recap-url.txt was empty.";
|
|
2650
|
+
return explicitReason?.trim() || "recap-url.txt was empty.";
|
|
2627
2651
|
const appUrl = input.appUrl ||
|
|
2628
2652
|
process.env.PLAN_RECAP_APP_URL ||
|
|
2629
2653
|
"https://plan.agent-native.com";
|
|
@@ -2637,10 +2661,12 @@ export function inferLocalRecapUrlFailureReason(input = {}) {
|
|
|
2637
2661
|
if (parsed.origin !== trusted.origin) {
|
|
2638
2662
|
return `recap-url.txt points at ${parsed.origin}, expected ${trusted.origin}.`;
|
|
2639
2663
|
}
|
|
2640
|
-
return
|
|
2664
|
+
return (explicitReason?.trim() ||
|
|
2665
|
+
"recap-url.txt did not contain a valid /plans/<id> or /recaps/<id> URL for the configured plan app.");
|
|
2641
2666
|
}
|
|
2642
2667
|
catch {
|
|
2643
|
-
return
|
|
2668
|
+
return (explicitReason?.trim() ||
|
|
2669
|
+
"recap-url.txt was not a valid URL or recap path.");
|
|
2644
2670
|
}
|
|
2645
2671
|
}
|
|
2646
2672
|
export function buildRecapFailureDiagnostic(input) {
|
|
@@ -3063,10 +3089,10 @@ Usage:
|
|
|
3063
3089
|
npx @agent-native/core@latest recap setup [--repo owner/name] [--agent claude|codex] [--app-url <url>] [--skip-secrets] [--dry-run] [--force]
|
|
3064
3090
|
npx @agent-native/core@latest recap doctor [--repo owner/name] [--agent claude|codex] [--app-url <url>]
|
|
3065
3091
|
npx @agent-native/core@latest recap collect-diff --base <baseSha> --head <headSha> [--out recap.diff] [--stat recap.stat]
|
|
3066
|
-
npx @agent-native/core@latest recap
|
|
3067
|
-
npx @agent-native/core@latest recap mcp-smoke [--app-url <url>] [--token <planToken>]
|
|
3092
|
+
npx @agent-native/core@latest recap block-reference [--app-url <url>] [--out recap-blocks.md]
|
|
3068
3093
|
npx @agent-native/core@latest recap scan --diff <path> [--mode off|high-confidence|strict]
|
|
3069
|
-
npx @agent-native/core@latest recap build-prompt --pr <n> [--repo owner/name] [--head <sha>] [--app-url <url>] [--diff <path>] [--stat <path>] [--prev-plan-id <id>] [--huge] [--local-files] [--local-dir <folder>] [--skill-source auto|latest|repo] [--out <path>]
|
|
3094
|
+
npx @agent-native/core@latest recap build-prompt --pr <n> [--repo owner/name] [--head <sha>] [--app-url <url>] [--diff <path>] [--stat <path>] [--block-reference recap-blocks.md] [--prev-plan-id <id>] [--huge] [--local-files] [--local-dir <folder>] [--skill-source auto|latest|repo] [--out <path>]
|
|
3095
|
+
npx @agent-native/core@latest recap publish [--source recap-source.json] [--out recap-url.txt] [--repo owner/name] [--pr <n>] [--prev-plan-id <id>] [--app-url <url>] [--token <planToken>]
|
|
3070
3096
|
npx @agent-native/core@latest recap shot --url <planUrl> [--token <planToken>] [--app-url <url>] [--out recap.png] [--theme light|dark]
|
|
3071
3097
|
npx @agent-native/core@latest recap usage --plan-url <planUrl> --result-file <path> --app-url <url> --token <planToken> [--agent claude|codex] [--model <id>]
|
|
3072
3098
|
npx @agent-native/core@latest recap agent-summary --result-file <path> [--stderr-file <path>] [--exit-code-file <path>] [--agent claude|codex]
|
|
@@ -3103,11 +3129,12 @@ Usage:
|
|
|
3103
3129
|
shapes such as private key blocks and known provider token prefixes. Set
|
|
3104
3130
|
VISUAL_RECAP_SECRET_SCAN=strict, or pass --mode strict, to restore generic
|
|
3105
3131
|
TOKEN/SECRET assignment suppression; set off to disable this preflight.
|
|
3106
|
-
npx @agent-native/core@latest recap
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3132
|
+
npx @agent-native/core@latest recap block-reference
|
|
3133
|
+
Fetch the target Plan app's live get-plan-blocks reference over the public
|
|
3134
|
+
action route and write it to recap-blocks.md for the CI agent to read.
|
|
3135
|
+
npx @agent-native/core@latest recap publish
|
|
3136
|
+
Validate recap-source.json from the CI agent, publish it by POSTing the
|
|
3137
|
+
authenticated create-visual-recap action, and write recap-url.txt.
|
|
3111
3138
|
npx @agent-native/core@latest recap setup
|
|
3112
3139
|
Write/refresh .github/workflows/pr-visual-recap.yml, then configure GitHub
|
|
3113
3140
|
Actions secrets and variables with gh when values are available from env or
|
|
@@ -3130,11 +3157,8 @@ export async function runRecap(argv) {
|
|
|
3130
3157
|
case "collect-diff":
|
|
3131
3158
|
runCollectDiff(args);
|
|
3132
3159
|
return;
|
|
3133
|
-
case "
|
|
3134
|
-
|
|
3135
|
-
return;
|
|
3136
|
-
case "mcp-smoke":
|
|
3137
|
-
await runMcpSmoke(args);
|
|
3160
|
+
case "block-reference":
|
|
3161
|
+
await runBlockReference(args);
|
|
3138
3162
|
return;
|
|
3139
3163
|
case "scan":
|
|
3140
3164
|
runScan(args);
|
|
@@ -3142,6 +3166,9 @@ export async function runRecap(argv) {
|
|
|
3142
3166
|
case "build-prompt":
|
|
3143
3167
|
runBuildPrompt(args);
|
|
3144
3168
|
return;
|
|
3169
|
+
case "publish":
|
|
3170
|
+
await runPublish(args);
|
|
3171
|
+
return;
|
|
3145
3172
|
case "shot":
|
|
3146
3173
|
await runShot(args);
|
|
3147
3174
|
return;
|