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/README.md +19 -3
- package/dist/cli.js +320 -102
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.js.map +1 -1
- package/package.json +1 -1
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.
|
|
35713
|
+
var version = "0.8.4";
|
|
35714
35714
|
//#endregion
|
|
35715
35715
|
//#region src/core/projections.ts
|
|
35716
35716
|
const escapeXml = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
@@ -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
|
-
|
|
43293
|
-
|
|
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 = (
|
|
43298
|
-
cwd
|
|
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
|
|
43310
|
-
const status = run$1(
|
|
43345
|
+
const gitPath = relative(gitRoot, absolute);
|
|
43346
|
+
const status = run$1(gitRoot, [
|
|
43311
43347
|
"status",
|
|
43312
43348
|
"--short",
|
|
43313
43349
|
"--",
|
|
43314
|
-
|
|
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
|
-
|
|
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
|
|
43493
|
-
|
|
43494
|
-
|
|
43495
|
-
|
|
43496
|
-
|
|
43497
|
-
|
|
43498
|
-
|
|
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
|
|
43512
|
-
|
|
43513
|
-
|
|
43514
|
-
for (const
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
43585
|
-
|
|
43586
|
-
|
|
43587
|
-
|
|
43588
|
-
}
|
|
43589
|
-
let cursor = await
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
43761
|
-
const
|
|
43762
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
43963
|
+
state,
|
|
43964
|
+
decisionInputs,
|
|
43797
43965
|
opencode: {
|
|
43798
43966
|
agent: "seo",
|
|
43799
|
-
sessionId:
|
|
43800
|
-
|
|
43967
|
+
sessionId: researched.sessionId,
|
|
43968
|
+
model: host.model,
|
|
43969
|
+
transcript: researched.transcript
|
|
43801
43970
|
}
|
|
43802
43971
|
};
|
|
43803
|
-
const
|
|
43804
|
-
|
|
43805
|
-
|
|
43806
|
-
|
|
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: \"
|
|
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) => ({
|