pagegraph 0.8.2 → 0.8.4

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/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  import * as NFS from "node:fs";
3
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { existsSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
4
4
  import * as NodeChildProcess from "node:child_process";
5
5
  import { execFileSync, spawn } from "node:child_process";
6
6
  import { PassThrough } from "node:stream";
@@ -9,7 +9,7 @@ import { createHash, randomUUID } from "node:crypto";
9
9
  import * as OS from "node:os";
10
10
  import { tmpdir } from "node:os";
11
11
  import * as Path from "node:path";
12
- import { dirname, join, resolve } from "node:path";
12
+ import { dirname, join, relative, resolve } from "node:path";
13
13
  import * as NodeUrl from "node:url";
14
14
  import { pathToFileURL } from "node:url";
15
15
  import * as readline from "node:readline";
@@ -35710,7 +35710,7 @@ const runWith = (command, config) => {
35710
35710
  };
35711
35711
  //#endregion
35712
35712
  //#region package.json
35713
- var version = "0.8.2";
35713
+ var version = "0.8.4";
35714
35714
  //#endregion
35715
35715
  //#region src/core/projections.ts
35716
35716
  const escapeXml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
@@ -36166,12 +36166,13 @@ const findConfigFile = (from) => {
36166
36166
  const isStringArray = (value) => Array.isArray(value) && value.every(isString);
36167
36167
  const isCoverageRules = (value) => Array.isArray(value) && value.every((rule) => isObject$1(rule) && isString(rule["path"]) && Number.isSafeInteger(rule["minInbound"]) && rule["minInbound"] > 0);
36168
36168
  const isStringRecord = (value) => isObject$1(value) && Object.values(value).every(isString);
36169
+ const isWorkflowTimeout = (value) => Number.isSafeInteger(value) && value > 0 && value <= 2147483647;
36169
36170
  const isStringArrayRecord = (value) => isObject$1(value) && Object.values(value).every(isStringArray);
36170
36171
  const isWorkflowConfig = (value) => {
36171
36172
  if (!isObject$1(value) || !isObject$1(value["opencode"])) return false;
36172
36173
  const opencode = value["opencode"];
36173
36174
  const context = value["context"];
36174
- return isString(opencode["configDirectory"]) && isString(opencode["defaultModel"]) && (opencode["models"] === void 0 || isStringRecord(opencode["models"])) && (context === void 0 || isObject$1(context) && (context["files"] === void 0 || isStringArray(context["files"])) && (context["byWorkflow"] === void 0 || isStringArrayRecord(context["byWorkflow"]))) && (value["runsDirectory"] === void 0 || isString(value["runsDirectory"]));
36175
+ return isString(opencode["configDirectory"]) && isString(opencode["defaultModel"]) && (opencode["models"] === void 0 || isStringRecord(opencode["models"])) && (opencode["timeoutMs"] === void 0 || isWorkflowTimeout(opencode["timeoutMs"])) && (context === void 0 || isObject$1(context) && (context["files"] === void 0 || isStringArray(context["files"])) && (context["byWorkflow"] === void 0 || isStringArrayRecord(context["byWorkflow"]))) && (value["runsDirectory"] === void 0 || isString(value["runsDirectory"]));
36175
36176
  };
36176
36177
  /**
36177
36178
  * `seo.config.ts` is the consumer's file and may be plain JS, so its types are
@@ -42084,6 +42085,13 @@ const writeRunBundle = (root, runsDirectory, run) => {
42084
42085
  writeFileSync(resolve(directory, "summary.md"), summary(run), "utf8");
42085
42086
  return directory;
42086
42087
  };
42088
+ const writeResearchCheckpoint = (root, runsDirectory, checkpoint) => {
42089
+ const directory = resolve(root, runsDirectory, checkpoint.id);
42090
+ mkdirSync(directory, { recursive: true });
42091
+ const path = resolve(directory, "research.json");
42092
+ writeFileSync(path, `${JSON.stringify(checkpoint, null, 2)}\n`, "utf8");
42093
+ return path;
42094
+ };
42087
42095
  //#endregion
42088
42096
  //#region node_modules/effect/dist/unstable/ai/Decision.js
42089
42097
  /**
@@ -43281,6 +43289,30 @@ const selectGraph = (graph, options) => {
43281
43289
  collisions: graph.collisions
43282
43290
  };
43283
43291
  };
43292
+ const selectNeighborhood = (graph, targetPaths) => {
43293
+ const inbound = [];
43294
+ const outbound = [];
43295
+ for (const edge of graph.edges) {
43296
+ if (targetPaths.has(edge.to) && !targetPaths.has(edge.from)) {
43297
+ const node = graph.nodes.get(edge.from);
43298
+ if (node !== void 0) inbound.push({
43299
+ edge,
43300
+ node: serializeNode(node)
43301
+ });
43302
+ }
43303
+ if (targetPaths.has(edge.from) && !targetPaths.has(edge.to)) {
43304
+ const node = graph.nodes.get(edge.to);
43305
+ if (node !== void 0) outbound.push({
43306
+ edge,
43307
+ node: serializeNode(node)
43308
+ });
43309
+ }
43310
+ }
43311
+ return {
43312
+ inbound,
43313
+ outbound
43314
+ };
43315
+ };
43284
43316
  const collectContextFiles = (root, workflow, context) => {
43285
43317
  const paths = [...context?.files ?? [], ...context?.byWorkflow?.[workflow] ?? []];
43286
43318
  return [...new Set(paths)].map((path) => ({
@@ -43288,14 +43320,18 @@ const collectContextFiles = (root, workflow, context) => {
43288
43320
  content: readFileSync(resolve(root, path), "utf8")
43289
43321
  }));
43290
43322
  };
43291
- const collectWorkflowEvidence = (graph, options, root, workflow, context) => ({
43292
- graph: serializeGraph(selectGraph(graph, options)),
43293
- sources: collectContextFiles(root, workflow, context)
43294
- });
43323
+ const collectWorkflowEvidence = (graph, options, root, workflow, context) => {
43324
+ const selected = selectGraph(graph, options);
43325
+ return {
43326
+ graph: serializeGraph(selected),
43327
+ neighborhood: selectNeighborhood(graph, new Set(selected.nodes.keys())),
43328
+ sources: collectContextFiles(root, workflow, context)
43329
+ };
43330
+ };
43295
43331
  //#endregion
43296
43332
  //#region src/workflows/git.ts
43297
- const run$1 = (root, args) => execFileSync("git", args, {
43298
- cwd: root,
43333
+ const run$1 = (cwd, args) => execFileSync("git", args, {
43334
+ cwd,
43299
43335
  encoding: "utf8",
43300
43336
  stdio: [
43301
43337
  "ignore",
@@ -43304,38 +43340,36 @@ const run$1 = (root, args) => execFileSync("git", args, {
43304
43340
  ]
43305
43341
  }).trim();
43306
43342
  const nul = (value) => value.split("\0").filter((item) => item.length > 0);
43307
- const fingerprint = (root, path) => {
43343
+ const fingerprint = (root, gitRoot, path) => {
43308
43344
  const absolute = resolve(root, path);
43309
- const content = existsSync(absolute) ? readFileSync(absolute) : Buffer.from("<deleted>");
43310
- const status = run$1(root, [
43345
+ const gitPath = relative(gitRoot, absolute);
43346
+ const status = run$1(gitRoot, [
43311
43347
  "status",
43312
43348
  "--short",
43313
43349
  "--",
43314
- path
43350
+ gitPath
43315
43351
  ]);
43352
+ let content;
43353
+ if (!existsSync(absolute)) content = Buffer.from("<deleted>");
43354
+ else if (statSync(absolute).isDirectory()) {
43355
+ const stage = run$1(gitRoot, [
43356
+ "ls-files",
43357
+ "--stage",
43358
+ "--",
43359
+ gitPath
43360
+ ]);
43361
+ if (stage.split(/\s+/, 1)[0] !== "160000") throw new Error(`Cannot fingerprint directory reported as a Git file: ${path}`);
43362
+ const submoduleRoot = realpathSync(run$1(absolute, ["rev-parse", "--show-toplevel"]));
43363
+ if (submoduleRoot !== realpathSync(absolute)) throw new Error(`Git submodule is unavailable: ${path}`);
43364
+ content = Buffer.from(`gitlink:${stage}\0${JSON.stringify(inspectGit(submoduleRoot))}`);
43365
+ } else content = readFileSync(absolute);
43316
43366
  return createHash("sha256").update(status).update("\0").update(content).digest("hex");
43317
43367
  };
43318
43368
  const inspectGit = (root) => {
43369
+ const absoluteRoot = realpathSync(root);
43370
+ let gitTopLevel;
43319
43371
  try {
43320
- const tracked = nul(run$1(root, [
43321
- "diff",
43322
- "--name-only",
43323
- "-z",
43324
- "HEAD"
43325
- ]));
43326
- const untracked = nul(run$1(root, [
43327
- "ls-files",
43328
- "--others",
43329
- "--exclude-standard",
43330
- "-z"
43331
- ]));
43332
- const files = [.../* @__PURE__ */ new Set([...tracked, ...untracked])].sort();
43333
- return {
43334
- head: run$1(root, ["rev-parse", "HEAD"]),
43335
- dirty: files.length > 0,
43336
- files,
43337
- fingerprints: Object.fromEntries(files.map((path) => [path, fingerprint(root, path)]))
43338
- };
43372
+ gitTopLevel = run$1(absoluteRoot, ["rev-parse", "--show-toplevel"]);
43339
43373
  } catch {
43340
43374
  return {
43341
43375
  dirty: false,
@@ -43343,6 +43377,27 @@ const inspectGit = (root) => {
43343
43377
  fingerprints: {}
43344
43378
  };
43345
43379
  }
43380
+ const gitRoot = realpathSync(gitTopLevel);
43381
+ const tracked = nul(run$1(gitRoot, [
43382
+ "diff",
43383
+ "--name-only",
43384
+ "-z",
43385
+ "HEAD"
43386
+ ]));
43387
+ const untracked = nul(run$1(gitRoot, [
43388
+ "ls-files",
43389
+ "--others",
43390
+ "--exclude-standard",
43391
+ "--full-name",
43392
+ "-z"
43393
+ ]));
43394
+ const files = [.../* @__PURE__ */ new Set([...tracked, ...untracked])].map((path) => relative(absoluteRoot, resolve(gitRoot, path))).sort();
43395
+ return {
43396
+ head: run$1(gitRoot, ["rev-parse", "HEAD"]),
43397
+ dirty: files.length > 0,
43398
+ files,
43399
+ fingerprints: Object.fromEntries(files.map((path) => [path, fingerprint(absoluteRoot, gitRoot, path)]))
43400
+ };
43346
43401
  };
43347
43402
  const changedFiles = (before, after) => [.../* @__PURE__ */ new Set([...before.files, ...after.files])].filter((path) => before.fingerprints[path] !== after.fingerprints[path]).sort();
43348
43403
  //#endregion
@@ -43478,9 +43533,20 @@ const decodeToolPart = decodeUnknownOption(Struct({
43478
43533
  state: Struct({
43479
43534
  status: String$6,
43480
43535
  input: optionalKey(Unknown),
43481
- content: optionalKey(Unknown)
43536
+ content: optionalKey(Unknown),
43537
+ error: optionalKey(Unknown),
43538
+ metadata: optionalKey(Unknown)
43482
43539
  })
43483
43540
  }));
43541
+ const decodeCodeModeMetadata = decodeUnknownOption(Struct({ toolCalls: ArraySchema(Struct({
43542
+ tool: String$6,
43543
+ status: Literals$2([
43544
+ "running",
43545
+ "completed",
43546
+ "error"
43547
+ ]),
43548
+ input: optionalKey(Unknown)
43549
+ })) }));
43484
43550
  const toolParts = (transcript) => decodeTranscript(transcript).messages.flatMap((message) => {
43485
43551
  const assistant = decodeAssistantMessage(message);
43486
43552
  if (isNone(assistant)) return [];
@@ -43489,39 +43555,78 @@ const toolParts = (transcript) => decodeTranscript(transcript).messages.flatMap(
43489
43555
  return isSome(decoded) ? [decoded.value] : [];
43490
43556
  });
43491
43557
  });
43492
- const toolText = (value) => {
43493
- if (typeof value === "string") return value;
43494
- if (Array.isArray(value)) return value.map(toolText).join("\n");
43495
- if (value === null || typeof value !== "object") return "";
43496
- const record = value;
43497
- if (record["type"] === "text" && typeof record["text"] === "string") return record["text"];
43498
- return Object.values(record).map(toolText).join("\n");
43558
+ const toolPathPattern = /^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)+$/;
43559
+ const pathsInSearchOutput = (value) => {
43560
+ const found = /* @__PURE__ */ new Set();
43561
+ const visit = (current, pathField = false) => {
43562
+ if (typeof current === "string") {
43563
+ if (pathField) {
43564
+ const candidate = current.startsWith("tools.") ? current.slice(6) : current;
43565
+ if (toolPathPattern.test(candidate)) found.add(candidate);
43566
+ }
43567
+ try {
43568
+ visit(JSON.parse(current));
43569
+ } catch {}
43570
+ return;
43571
+ }
43572
+ if (Array.isArray(current)) {
43573
+ for (const item of current) visit(item);
43574
+ return;
43575
+ }
43576
+ if (current === null || typeof current !== "object") return;
43577
+ const record = current;
43578
+ for (const [key, nested] of Object.entries(record)) visit(nested, key === "path");
43579
+ };
43580
+ visit(value);
43581
+ return [...found];
43499
43582
  };
43500
- const pathsIn = (value) => [...toolText(value).matchAll(/tools\.([A-Za-z0-9_.-]+)/g)].map((match) => match[1] ?? "").filter(Boolean);
43501
43583
  const evidenceRecord = (part) => ({
43502
43584
  tool: part.name,
43503
43585
  input: part.state.input ?? null,
43504
- output: part.state.content ?? null
43586
+ output: part.state.content ?? part.state.error ?? null
43505
43587
  });
43506
43588
  const collectExecutorEvidence = (transcript) => {
43507
43589
  const discovered = /* @__PURE__ */ new Set();
43508
43590
  const searches = [];
43509
43591
  const calls = [];
43510
43592
  for (const part of toolParts(transcript)) {
43511
- const inputPaths = pathsIn(part.state.input);
43512
- const inputText = toolText(part.state.input);
43513
- if (part.name === "executor_search" || part.name === "executor.search" || inputPaths.some((path) => path === "executor.search")) {
43514
- for (const path of pathsIn(part.state.content)) if (path !== "executor.search") discovered.add(path);
43593
+ const metadata = decodeCodeModeMetadata(part.state.metadata);
43594
+ if (isSome(metadata)) {
43595
+ const invocations = metadata.value.toolCalls;
43596
+ for (const invocation of invocations) {
43597
+ if (invocation.status !== "completed") continue;
43598
+ const record = codeModeEvidenceRecord(invocation, part.state.content);
43599
+ if (invocation.tool === "executor.search") {
43600
+ for (const path of pathsInSearchOutput(part.state.content)) if (path !== "executor.search") discovered.add(path);
43601
+ searches.push(record);
43602
+ continue;
43603
+ }
43604
+ if ([...discovered].some((path) => matchesToolPath(invocation.tool, path))) calls.push(record);
43605
+ }
43606
+ continue;
43607
+ }
43608
+ if (part.state.status === "completed" && (part.name === "executor_search" || part.name === "executor.search")) {
43609
+ for (const path of pathsInSearchOutput(part.state.content)) if (path !== "executor.search") discovered.add(path);
43515
43610
  searches.push(evidenceRecord(part));
43516
43611
  continue;
43517
43612
  }
43518
- if ([...discovered].some((path) => part.name === path || part.name === path.replaceAll(".", "_") || inputPaths.includes(path) || inputText.includes(path))) calls.push(evidenceRecord(part));
43613
+ if (part.state.status === "completed" && [...discovered].some((path) => matchesToolPath(part.name, path))) calls.push(evidenceRecord(part));
43519
43614
  }
43520
43615
  return {
43521
43616
  searches,
43522
43617
  calls
43523
43618
  };
43524
43619
  };
43620
+ const codeModeEvidenceRecord = (invocation, codeModeOutput) => ({
43621
+ tool: invocation.tool,
43622
+ input: invocation.input ?? null,
43623
+ output: {
43624
+ status: invocation.status,
43625
+ scope: "shared CodeMode execution result",
43626
+ content: codeModeOutput ?? null
43627
+ }
43628
+ });
43629
+ const matchesToolPath = (tool, path) => tool === path || tool === path.replaceAll(".", "_");
43525
43630
  const interactionEventError = (event, sessionId) => {
43526
43631
  if (event === null || typeof event !== "object") return void 0;
43527
43632
  const record = event;
@@ -43534,15 +43639,61 @@ const interactionEventError = (event, sessionId) => {
43534
43639
  if (form !== null && typeof form === "object" && form["sessionID"] === sessionId) return /* @__PURE__ */ new Error("OpenCode requested a form; embedded PageGraph workflows are noninteractive.");
43535
43640
  }
43536
43641
  };
43642
+ const recordWorkflowActivity = (activity, event) => {
43643
+ if (event.type === "log.synced") return;
43644
+ activity.lastActivity = {
43645
+ type: event.type,
43646
+ at: (/* @__PURE__ */ new Date()).toISOString(),
43647
+ seq: event.durable.seq
43648
+ };
43649
+ if (event.type === "session.step.started") {
43650
+ activity.modelSteps += 1;
43651
+ activity.lastModel = `${event.data.model.providerID}/${event.data.model.id}`;
43652
+ return;
43653
+ }
43654
+ if (event.type === "session.tool.input.started") {
43655
+ activity.toolNames.set(event.data.id, event.data.name);
43656
+ activity.lastTool = {
43657
+ id: event.data.id,
43658
+ name: event.data.name,
43659
+ status: "input"
43660
+ };
43661
+ return;
43662
+ }
43663
+ if (event.type === "session.tool.called") {
43664
+ activity.lastTool = {
43665
+ id: event.data.id,
43666
+ name: activity.toolNames.get(event.data.id) ?? event.data.id,
43667
+ status: "called"
43668
+ };
43669
+ return;
43670
+ }
43671
+ if (event.type === "session.tool.success" || event.type === "session.tool.failed") {
43672
+ activity.completedTools += 1;
43673
+ activity.lastTool = {
43674
+ id: event.data.id,
43675
+ name: activity.toolNames.get(event.data.id) ?? event.data.id,
43676
+ status: event.type === "session.tool.success" ? "succeeded" : "failed"
43677
+ };
43678
+ }
43679
+ };
43680
+ const describeWorkflowActivity = (activity, stage) => {
43681
+ const lastActivity = activity.lastActivity ? `${activity.lastActivity.type} at ${activity.lastActivity.at}${activity.lastActivity.seq === void 0 ? "" : ` (seq ${activity.lastActivity.seq})`}` : `none observed during ${stage}`;
43682
+ const lastTool = activity.lastTool ? `; last tool ${activity.lastTool.name} (${activity.lastTool.id}, ${activity.lastTool.status})` : "";
43683
+ const model = activity.lastModel ? `; last model ${activity.lastModel}` : "";
43684
+ return `Model steps: ${activity.modelSteps}; completed tools: ${activity.completedTools}; last activity: ${lastActivity}${model}${lastTool}.`;
43685
+ };
43537
43686
  const waitForIdle = async (host, sessionId, options) => {
43538
- const controller = new AbortController();
43539
- const timer = setTimeout(() => controller.abort(), options.timeoutMs);
43687
+ const controller = options.signal === void 0 ? new AbortController() : void 0;
43688
+ const signal = options.signal ?? controller.signal;
43689
+ const timer = controller === void 0 ? void 0 : setTimeout(() => controller.abort(), options.timeoutMs);
43540
43690
  try {
43541
43691
  for await (const event of host.sessions.log({
43542
43692
  sessionID: sessionId,
43543
43693
  follow: true,
43544
43694
  after: options.after
43545
- }, { signal: controller.signal })) {
43695
+ }, { signal })) {
43696
+ if (options.activity !== void 0) recordWorkflowActivity(options.activity, event);
43546
43697
  const interactionError = interactionEventError(event, sessionId);
43547
43698
  if (interactionError !== void 0) throw interactionError;
43548
43699
  if (event.type === "session.execution.succeeded" && event.data.sessionID === sessionId) return event.durable.seq;
@@ -43550,13 +43701,44 @@ const waitForIdle = async (host, sessionId, options) => {
43550
43701
  if (event.type === "session.execution.interrupted" && event.data.sessionID === sessionId) throw new Error(`OpenCode session was interrupted: ${event.data.reason}.`);
43551
43702
  }
43552
43703
  } catch (cause) {
43553
- if (controller.signal.aborted) throw new Error(`OpenCode session did not complete within ${options.configuredTimeoutMs ?? options.timeoutMs}ms.`, { cause });
43704
+ if (controller?.signal.aborted) throw new Error(`OpenCode session did not complete within ${options.configuredTimeoutMs ?? options.timeoutMs}ms.`, { cause });
43554
43705
  throw cause;
43555
43706
  } finally {
43556
- clearTimeout(timer);
43707
+ if (timer !== void 0) clearTimeout(timer);
43557
43708
  }
43558
43709
  throw new Error("OpenCode session log ended before the SEO agent completed.");
43559
43710
  };
43711
+ const runWorkflowTurn = async (host, sessionId, prompt, options) => {
43712
+ const controller = new AbortController();
43713
+ const timer = setTimeout(() => controller.abort(), options.timeoutMs);
43714
+ let stage = options.stage;
43715
+ try {
43716
+ await host.sessions.prompt({
43717
+ sessionID: sessionId,
43718
+ text: prompt,
43719
+ ...options.skills === void 0 ? {} : { skills: options.skills.map((id) => ({ id })) }
43720
+ }, { signal: controller.signal });
43721
+ if (stage === "prompt admission") stage = "model execution";
43722
+ return await waitForIdle(host, sessionId, {
43723
+ timeoutMs: options.timeoutMs,
43724
+ configuredTimeoutMs: options.configuredTimeoutMs,
43725
+ after: options.after,
43726
+ signal: controller.signal,
43727
+ activity: options.activity
43728
+ });
43729
+ } catch (cause) {
43730
+ if (!controller.signal.aborted) throw cause;
43731
+ let interruption = "unknown";
43732
+ try {
43733
+ interruption = (await host.sessions.interrupt({ sessionID: sessionId }, { signal: AbortSignal.timeout(5e3) })).interrupted ? "active execution interrupted" : "no active execution to interrupt";
43734
+ } catch (interruptCause) {
43735
+ interruption = `interrupt request failed: ${interruptCause instanceof Error ? interruptCause.message : String(interruptCause)}`;
43736
+ }
43737
+ throw new Error(`OpenCode session did not complete within ${options.configuredTimeoutMs}ms during ${stage}; ${describeWorkflowActivity(options.activity, stage)} Timeout handling: ${interruption}.`, { cause });
43738
+ } finally {
43739
+ clearTimeout(timer);
43740
+ }
43741
+ };
43560
43742
  const acquireWorkflowHost = async (options) => {
43561
43743
  const model = parseModel(options.model ?? options.config.defaultModel);
43562
43744
  const configDirectory = resolve(options.root, options.config.configDirectory);
@@ -43578,18 +43760,21 @@ const acquireWorkflowHost = async (options) => {
43578
43760
  sessionID: sessionId,
43579
43761
  permissions: [...workflowOptions.permissions]
43580
43762
  });
43581
- const configuredTimeoutMs = options.timeoutMs ?? 18e4;
43763
+ const configuredTimeoutMs = options.timeoutMs ?? options.config.timeoutMs ?? 18e4;
43582
43764
  const deadline = Date.now() + configuredTimeoutMs;
43583
43765
  const remaining = () => Math.max(0, deadline - Date.now());
43584
- await host.sessions.prompt({
43585
- sessionID: sessionId,
43586
- text: prompt,
43587
- skills: workflowOptions.skills.map((id) => ({ id }))
43588
- });
43589
- let cursor = await waitForIdle(host, sessionId, {
43766
+ const activity = {
43767
+ modelSteps: 0,
43768
+ completedTools: 0,
43769
+ toolNames: /* @__PURE__ */ new Map()
43770
+ };
43771
+ let cursor = await runWorkflowTurn(host, sessionId, prompt, {
43590
43772
  timeoutMs: remaining(),
43591
43773
  configuredTimeoutMs,
43592
- after: cursors.get(sessionId)
43774
+ stage: "prompt admission",
43775
+ after: cursors.get(sessionId),
43776
+ skills: workflowOptions.skills,
43777
+ activity
43593
43778
  });
43594
43779
  cursors.set(sessionId, cursor);
43595
43780
  let transcript = await host.sessions.export({
@@ -43607,14 +43792,12 @@ const acquireWorkflowHost = async (options) => {
43607
43792
  }]
43608
43793
  });
43609
43794
  try {
43610
- await host.sessions.prompt({
43611
- sessionID: sessionId,
43612
- text: STATE_REPAIR_PROMPT
43613
- });
43614
- cursor = await waitForIdle(host, sessionId, {
43795
+ cursor = await runWorkflowTurn(host, sessionId, STATE_REPAIR_PROMPT, {
43615
43796
  timeoutMs: remaining(),
43616
43797
  configuredTimeoutMs,
43617
- after: cursor
43798
+ stage: "workflow state repair",
43799
+ after: cursor,
43800
+ activity
43618
43801
  });
43619
43802
  cursors.set(sessionId, cursor);
43620
43803
  transcript = await host.sessions.export({
@@ -43650,7 +43833,7 @@ const acquireWorkflowHost = async (options) => {
43650
43833
  permissions: workflowOptions.permissions === void 0 ? void 0 : [...workflowOptions.permissions]
43651
43834
  });
43652
43835
  const result = await complete(session.id, prompt, workflowOptions);
43653
- if (result.executor.searches.length === 0 || result.executor.calls.length === 0) throw new Error("The SEO agent returned without searching Executor and calling a discovered tool.");
43836
+ if (result.executor.searches.length === 0 || result.executor.calls.length === 0) throw new Error(`The SEO agent returned without the required Executor evidence (searches: ${result.executor.searches.length}, calls: ${result.executor.calls.length}).`);
43654
43837
  return result;
43655
43838
  },
43656
43839
  continue: (sessionId, prompt, workflowOptions) => complete(sessionId, prompt, workflowOptions),
@@ -43666,7 +43849,7 @@ const acquireWorkflowHost = async (options) => {
43666
43849
  var action_default = "Continue PageGraph's {{workflow}} workflow.\n\n{{instructions}}\n\nObserved state:\n{{state}}\n\nJev decisions:\n{{decisions}}\n\nApply only records in the resolved set whose verdict calls for a change. Never apply review records;\nleave them in the artifact for a human. If nothing is safely resolved, return no-change.\n\nRepository mutation mode: {{mutationMode}}. {{mutationInstruction}}\n\nReturn only one JSON object with this shape:\n{\"summary\":\"...\",\"files\":[\"relative/path\"],\"outcome\":\"applied|dry-run|no-change\"}\n";
43667
43850
  //#endregion
43668
43851
  //#region src/workflows/prompts/research.md
43669
- var research_default = "You are running PageGraph's {{workflow}} workflow.\n\n{{instructions}}\n\nUse these skills: {{skills}}. Treat the supplied project evidence as the deterministic baseline.\nSearch Executor's live catalog in one Code Mode turn, then call at least one exact returned tool\npath in a later turn. Compose whichever current integrations best support this workflow. Do not\nassume fixed provider or tool names. Preserve source references in the structured state.\n\nReturn only one JSON object matching this JSON Schema:\n{{stateSchema}}\n\nMarket: {{market}}\nLanguage: {{language}}\nDevice: {{device}}\nRefresh requested: {{refresh}}\nDo not ask questions, request permissions, or create interactive forms. Fail clearly if required\ninput is missing.\nSeed queries: {{queries}}\nCompetitor seeds: {{competitors}}\nAuthority-domain seeds: {{domains}}\nDeterministic evidence:\n{{evidence}}\n";
43852
+ var research_default = "You are running PageGraph's {{workflow}} workflow.\n\n{{instructions}}\n\nUse these skills: {{skills}}. Treat the supplied project evidence as the deterministic baseline.\nThe graph contains the selected target pages. Its neighborhood preserves incoming and outgoing\nrelationships to pages outside that selection; those neighbors are context, not additional targets.\nReturn at most {{limit}} items or opportunities. Keep exploration scoped to these targets and stop\nwhen the collected evidence supports the requested decision; do not expand into a site-wide audit.\n\nSearch Executor's live catalog with a short intent phrase, then use Code Mode's local `search` for\nthe exact returned path to inspect its current argument signature before calling it in a later turn.\nCatalog discovery returns tool names and descriptions, not complete input schemas. Never guess\narguments from a description. If discovery returns no matches, simplify the query. Compose whichever\ncurrent integrations best support this workflow; tool names and providers are discovered dynamically.\nCall at least one relevant discovered tool and preserve source references and provider errors in the\nstructured state. Do not treat an errored call or empty dataset as proof of indexing or traffic.\nDistinguish project market settings from the country, language, device, and date filters actually\napplied to provider data; report only the segments supported by the call and its response.\n\nReturn only one JSON object matching this JSON Schema:\n{{stateSchema}}\n\nMarket: {{market}}\nLanguage: {{language}}\nDevice: {{device}}\nRefresh requested: {{refresh}}\nDo not ask questions, request permissions, or create interactive forms. Fail clearly if required\ninput is missing.\nSeed queries: {{queries}}\nCompetitor seeds: {{competitors}}\nAuthority-domain seeds: {{domains}}\nDeterministic evidence:\n{{evidence}}\n";
43670
43853
  //#endregion
43671
43854
  //#region src/workflows/run.ts
43672
43855
  const defaultDecide = async (inputs, spec) => {
@@ -43695,6 +43878,7 @@ const capState = (state, limit) => {
43695
43878
  };
43696
43879
  const researchPrompt = (spec, evidence, options) => TextTemplate.from(research_default).values({
43697
43880
  workflow: spec.id,
43881
+ limit: options.limit,
43698
43882
  instructions: spec.researchInstructions,
43699
43883
  skills: spec.skills.join(", "),
43700
43884
  stateSchema: JSON.stringify(spec.stateJsonSchema),
@@ -43757,54 +43941,88 @@ const runWorkflow = async (input, dependencies = {}) => {
43757
43941
  const error = spec.validateDecisionInput(item);
43758
43942
  if (error !== void 0) throw new Error(error);
43759
43943
  }
43760
- const decisionReport = await (dependencies.decide ?? defaultDecide)(decisionInputs, spec);
43761
- const acted = spec.mutatesFiles ? await host.continue(researched.sessionId, actionPrompt(spec, state, decisionReport, mutation), {
43762
- skills: spec.skills,
43763
- permissions: actionPermissions
43764
- }) : researched;
43765
- const gitAfter = inspectGit(input.root);
43766
- const files = changedFiles(gitAtStart, gitAfter);
43767
- if ((!spec.mutatesFiles || input.options.dryRun) && files.length > 0) throw new Error(`${spec.id} changed repository files while running in read-only mode: ${files.join(", ")}`);
43768
- const run = {
43769
- kind: "pagegraph-workflow-run",
43944
+ const runsDirectory = input.out ?? workflows.runsDirectory ?? ".pagegraph/runs";
43945
+ const checkpoint = {
43946
+ kind: "pagegraph-workflow-research-checkpoint",
43770
43947
  schemaVersion: 1,
43771
43948
  id,
43772
43949
  workflow: spec.id,
43773
43950
  startedAt: started.toISOString(),
43774
- finishedAt: now().toISOString(),
43951
+ checkpointedAt: now().toISOString(),
43775
43952
  project: {
43776
43953
  root: input.root,
43777
43954
  head: gitAtStart.head,
43778
- dirtyAtStart: gitAtStart.dirty
43955
+ dirtyAtStart: gitAtStart.dirty,
43956
+ filesAtStart: gitAtStart.files
43779
43957
  },
43780
43958
  options: input.options,
43781
- model: host.model,
43782
- targets: {
43783
- pages: evidence.graph.nodes.map((node) => node.path),
43784
- queries: input.options.queries,
43785
- kinds: input.options.kinds
43786
- },
43787
43959
  evidence: {
43788
43960
  ...evidence,
43789
- executor: acted.executor
43790
- },
43791
- decisions: [decisionArtifact(spec, decisionInputs, decisionReport)],
43792
- changes: {
43793
- files,
43794
- providerCalls: acted.executor.calls.map((call) => call.tool)
43961
+ executor: researched.executor
43795
43962
  },
43796
- result: spec.mutatesFiles ? acted.state : state,
43963
+ state,
43964
+ decisionInputs,
43797
43965
  opencode: {
43798
43966
  agent: "seo",
43799
- sessionId: acted.sessionId,
43800
- transcript: acted.transcript
43967
+ sessionId: researched.sessionId,
43968
+ model: host.model,
43969
+ transcript: researched.transcript
43801
43970
  }
43802
43971
  };
43803
- const runsDirectory = input.out ?? workflows.runsDirectory ?? ".pagegraph/runs";
43804
- return {
43805
- run,
43806
- directory: writeRunBundle(input.root, runsDirectory, run)
43807
- };
43972
+ const checkpointPath = writeResearchCheckpoint(input.root, runsDirectory, checkpoint);
43973
+ const gitAtCheckpoint = inspectGit(input.root);
43974
+ try {
43975
+ const decisionReport = await (dependencies.decide ?? defaultDecide)(decisionInputs, spec);
43976
+ const acted = spec.mutatesFiles ? await host.continue(researched.sessionId, actionPrompt(spec, state, decisionReport, mutation), {
43977
+ skills: spec.skills,
43978
+ permissions: actionPermissions
43979
+ }) : researched;
43980
+ const gitAfter = inspectGit(input.root);
43981
+ const files = changedFiles(gitAtCheckpoint, gitAfter);
43982
+ if ((!spec.mutatesFiles || input.options.dryRun) && files.length > 0) throw new Error(`${spec.id} changed repository files while running in read-only mode: ${files.join(", ")}`);
43983
+ const run = {
43984
+ kind: "pagegraph-workflow-run",
43985
+ schemaVersion: 1,
43986
+ id,
43987
+ workflow: spec.id,
43988
+ startedAt: started.toISOString(),
43989
+ finishedAt: now().toISOString(),
43990
+ project: {
43991
+ root: input.root,
43992
+ head: gitAtStart.head,
43993
+ dirtyAtStart: gitAtStart.dirty
43994
+ },
43995
+ options: input.options,
43996
+ model: host.model,
43997
+ targets: {
43998
+ pages: evidence.graph.nodes.map((node) => node.path),
43999
+ queries: input.options.queries,
44000
+ kinds: input.options.kinds
44001
+ },
44002
+ evidence: {
44003
+ ...evidence,
44004
+ executor: acted.executor
44005
+ },
44006
+ decisions: [decisionArtifact(spec, decisionInputs, decisionReport)],
44007
+ changes: {
44008
+ files,
44009
+ providerCalls: acted.executor.calls.map((call) => call.tool)
44010
+ },
44011
+ result: spec.mutatesFiles ? acted.state : state,
44012
+ opencode: {
44013
+ agent: "seo",
44014
+ sessionId: acted.sessionId,
44015
+ transcript: acted.transcript
44016
+ }
44017
+ };
44018
+ return {
44019
+ run,
44020
+ directory: writeRunBundle(input.root, runsDirectory, run)
44021
+ };
44022
+ } catch (cause) {
44023
+ const message = cause instanceof Error ? cause.message : String(cause);
44024
+ throw new Error(`${message}\nResearch checkpoint: ${checkpointPath}`, { cause });
44025
+ }
43808
44026
  } finally {
43809
44027
  await host.close();
43810
44028
  }
@@ -44336,7 +44554,7 @@ var executor_default$1 = "# Executor Starters for SERP Analysis\n\nThese snippet
44336
44554
  var SKILL_default = "---\nname: site-architecture\ndescription: Plan clear route ownership, hierarchy, collections, and contextual navigation.\n---\n\n# Site Architecture\n\nUse PageGraph's deterministic route nodes and edges as the primary model. Read\n[Executor Starters](references/executor.md) when owned search or crawl evidence can resolve an\nownership or discoverability question.\n\nAssign one clear intent owner, identify useful collections and parent-child relationships, and\nprefer consolidation when multiple pages compete for the same need. Propose new routes only when a\ndistinct supported intent lacks an adequate owner. Treat URL changes as consequential and preserve\nredirect and canonical requirements in the recommendation.\n\nReturn the structured state requested by the workflow. This planning workflow does not move routes,\nedit navigation, or submit sitemaps.\n";
44337
44555
  //#endregion
44338
44556
  //#region src/cli/preset/opencode/skills/site-architecture/references/executor.md
44339
- var executor_default = "# Executor Starters for Site Architecture\n\nThese snippets are starting points, not an inventory. Discover owned page/query, crawl, sitemap,\nand URL evidence only when the deterministic graph does not answer the question.\n\n~~~ts\nreturn Promise.all([\n tools.executor.search({ query: \"Google Search Console page query canonical indexing\", limit: 8 }),\n tools.executor.search({ query: \"Bing Webmaster crawl sitemap URL information\", limit: 8 }),\n]);\n~~~\n\nIn a later turn:\n\n~~~ts\nconst pages = (await search({ query: \"tools.<exact-owned-pages-path>\", limit: 1 })).items[0];\nreturn pages.path.split(\".\").reduce((node, segment) => node[segment], tools)({\n siteUrl,\n startDate,\n endDate,\n dimensions: [\"PAGE\", \"QUERY\"],\n});\n~~~\n\nKeep submitted, crawled, indexed, ranking, and clicked evidence distinct. Record reporting dates,\nfilters, canonical state, task status, errors, and cost. Do not request indexing, submit sitemaps,\nor update provider projects.\n";
44557
+ var executor_default = "# Executor Starters for Site Architecture\n\nThese snippets are starting points, not an inventory. Discover owned page/query, crawl, sitemap,\nand URL evidence only when the deterministic graph does not answer the question.\n\n~~~ts\nreturn Promise.all([\n tools.executor.search({ query: \"Search Console page performance\", limit: 8 }),\n tools.executor.search({ query: \"Bing Webmaster crawl\", limit: 8 }),\n]);\n~~~\n\nInspect a selected tool's signature before executing it. This is Code Mode's local schema lookup;\nit does not call the provider:\n\n~~~ts\nreturn await search({ query: \"tools.<exact-path-returned-by-executor>\", limit: 1 });\n~~~\n\nIn a later turn, call that exact path with the required fields from its returned signature. Property,\nproject, URL, date, and dimension arguments differ between integrations. Do not copy guessed fields\nfrom a recipe. Prefer page/query performance for demand and ownership questions; use URL inspection\nwhen the question actually concerns crawl, canonical, or indexing state.\n\nKeep submitted, crawled, indexed, ranking, and clicked evidence distinct. Record reporting dates,\nfilters, canonical state, task status, errors, and cost. Do not request indexing, submit sitemaps,\nor update provider projects.\n";
44340
44558
  //#endregion
44341
44559
  //#region src/cli/preset/opencode/preset.ts
44342
44560
  const skill = (name, definition, executor) => ({