pagegraph 0.8.3 → 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 +294 -101
- 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/README.md
CHANGED
|
@@ -229,6 +229,8 @@ export default defineSeoConfig({
|
|
|
229
229
|
opencode: {
|
|
230
230
|
configDirectory: ".pagegraph/opencode",
|
|
231
231
|
defaultModel: "openrouter/example/model",
|
|
232
|
+
// Optional per-completion budget in milliseconds; default 180000.
|
|
233
|
+
timeoutMs: 360_000,
|
|
232
234
|
models: {
|
|
233
235
|
"research.keywords": "openrouter/example/research-model",
|
|
234
236
|
},
|
|
@@ -254,9 +256,23 @@ pagegraph research keywords \
|
|
|
254
256
|
```
|
|
255
257
|
|
|
256
258
|
Every workflow starts from the selected graph and context files, asks the SEO agent to search
|
|
257
|
-
Executor's live catalog and
|
|
258
|
-
Jev. Tool paths are discovered at runtime; the generated skills contain
|
|
259
|
-
not a fixed integration list.
|
|
259
|
+
Executor's live catalog and inspect the discovered tools' argument schemas, then evaluates its
|
|
260
|
+
structured observations with Jev. Tool paths are discovered at runtime; the generated skills contain
|
|
261
|
+
editable starter recipes, not a fixed integration list.
|
|
262
|
+
|
|
263
|
+
`workflows.opencode.timeoutMs` sets the positive integer deadline in milliseconds for each OpenCode
|
|
264
|
+
completion (maximum `2147483647`). It covers prompt admission, model execution, and any workflow JSON
|
|
265
|
+
repair prompt. A later action or continuation turn gets a separate budget. The default is `180000`.
|
|
266
|
+
|
|
267
|
+
Selecting pages preserves their incoming and outgoing relationships to non-selected pages in
|
|
268
|
+
`evidence.neighborhood`. Those neighbors supply architecture context without becoming workflow
|
|
269
|
+
targets or consuming `--limit`. The agent receives the limit before research, and PageGraph also caps
|
|
270
|
+
the decoded result before decisions.
|
|
271
|
+
|
|
272
|
+
After research is validated, the run directory contains `research.json`: an incomplete checkpoint
|
|
273
|
+
with the collected evidence, decision inputs, and OpenCode transcript. A downstream failure reports
|
|
274
|
+
its path so the evidence remains available for diagnosis. Only a completed workflow writes
|
|
275
|
+
`run.json` and `summary.md`; a research checkpoint is not a final recommendation.
|
|
260
276
|
|
|
261
277
|
| Command | Result |
|
|
262
278
|
| --- | --- |
|
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,15 +43555,6 @@ 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");
|
|
43499
|
-
};
|
|
43500
|
-
const pathsIn = (value) => [...toolText(value).matchAll(/tools\.([A-Za-z0-9_.-]+)/g)].map((match) => match[1] ?? "").filter(Boolean);
|
|
43501
43558
|
const toolPathPattern = /^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)+$/;
|
|
43502
43559
|
const pathsInSearchOutput = (value) => {
|
|
43503
43560
|
const found = /* @__PURE__ */ new Set();
|
|
@@ -43526,27 +43583,50 @@ const pathsInSearchOutput = (value) => {
|
|
|
43526
43583
|
const evidenceRecord = (part) => ({
|
|
43527
43584
|
tool: part.name,
|
|
43528
43585
|
input: part.state.input ?? null,
|
|
43529
|
-
output: part.state.content ?? null
|
|
43586
|
+
output: part.state.content ?? part.state.error ?? null
|
|
43530
43587
|
});
|
|
43531
43588
|
const collectExecutorEvidence = (transcript) => {
|
|
43532
43589
|
const discovered = /* @__PURE__ */ new Set();
|
|
43533
43590
|
const searches = [];
|
|
43534
43591
|
const calls = [];
|
|
43535
43592
|
for (const part of toolParts(transcript)) {
|
|
43536
|
-
const
|
|
43537
|
-
|
|
43538
|
-
|
|
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")) {
|
|
43539
43609
|
for (const path of pathsInSearchOutput(part.state.content)) if (path !== "executor.search") discovered.add(path);
|
|
43540
43610
|
searches.push(evidenceRecord(part));
|
|
43541
43611
|
continue;
|
|
43542
43612
|
}
|
|
43543
|
-
if ([...discovered].some((path) => part.name
|
|
43613
|
+
if (part.state.status === "completed" && [...discovered].some((path) => matchesToolPath(part.name, path))) calls.push(evidenceRecord(part));
|
|
43544
43614
|
}
|
|
43545
43615
|
return {
|
|
43546
43616
|
searches,
|
|
43547
43617
|
calls
|
|
43548
43618
|
};
|
|
43549
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(".", "_");
|
|
43550
43630
|
const interactionEventError = (event, sessionId) => {
|
|
43551
43631
|
if (event === null || typeof event !== "object") return void 0;
|
|
43552
43632
|
const record = event;
|
|
@@ -43559,15 +43639,61 @@ const interactionEventError = (event, sessionId) => {
|
|
|
43559
43639
|
if (form !== null && typeof form === "object" && form["sessionID"] === sessionId) return /* @__PURE__ */ new Error("OpenCode requested a form; embedded PageGraph workflows are noninteractive.");
|
|
43560
43640
|
}
|
|
43561
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
|
+
};
|
|
43562
43686
|
const waitForIdle = async (host, sessionId, options) => {
|
|
43563
|
-
const controller = new AbortController();
|
|
43564
|
-
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);
|
|
43565
43690
|
try {
|
|
43566
43691
|
for await (const event of host.sessions.log({
|
|
43567
43692
|
sessionID: sessionId,
|
|
43568
43693
|
follow: true,
|
|
43569
43694
|
after: options.after
|
|
43570
|
-
}, { signal
|
|
43695
|
+
}, { signal })) {
|
|
43696
|
+
if (options.activity !== void 0) recordWorkflowActivity(options.activity, event);
|
|
43571
43697
|
const interactionError = interactionEventError(event, sessionId);
|
|
43572
43698
|
if (interactionError !== void 0) throw interactionError;
|
|
43573
43699
|
if (event.type === "session.execution.succeeded" && event.data.sessionID === sessionId) return event.durable.seq;
|
|
@@ -43575,13 +43701,44 @@ const waitForIdle = async (host, sessionId, options) => {
|
|
|
43575
43701
|
if (event.type === "session.execution.interrupted" && event.data.sessionID === sessionId) throw new Error(`OpenCode session was interrupted: ${event.data.reason}.`);
|
|
43576
43702
|
}
|
|
43577
43703
|
} catch (cause) {
|
|
43578
|
-
if (controller
|
|
43704
|
+
if (controller?.signal.aborted) throw new Error(`OpenCode session did not complete within ${options.configuredTimeoutMs ?? options.timeoutMs}ms.`, { cause });
|
|
43579
43705
|
throw cause;
|
|
43580
43706
|
} finally {
|
|
43581
|
-
clearTimeout(timer);
|
|
43707
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
43582
43708
|
}
|
|
43583
43709
|
throw new Error("OpenCode session log ended before the SEO agent completed.");
|
|
43584
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
|
+
};
|
|
43585
43742
|
const acquireWorkflowHost = async (options) => {
|
|
43586
43743
|
const model = parseModel(options.model ?? options.config.defaultModel);
|
|
43587
43744
|
const configDirectory = resolve(options.root, options.config.configDirectory);
|
|
@@ -43603,18 +43760,21 @@ const acquireWorkflowHost = async (options) => {
|
|
|
43603
43760
|
sessionID: sessionId,
|
|
43604
43761
|
permissions: [...workflowOptions.permissions]
|
|
43605
43762
|
});
|
|
43606
|
-
const configuredTimeoutMs = options.timeoutMs ?? 18e4;
|
|
43763
|
+
const configuredTimeoutMs = options.timeoutMs ?? options.config.timeoutMs ?? 18e4;
|
|
43607
43764
|
const deadline = Date.now() + configuredTimeoutMs;
|
|
43608
43765
|
const remaining = () => Math.max(0, deadline - Date.now());
|
|
43609
|
-
|
|
43610
|
-
|
|
43611
|
-
|
|
43612
|
-
|
|
43613
|
-
}
|
|
43614
|
-
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, {
|
|
43615
43772
|
timeoutMs: remaining(),
|
|
43616
43773
|
configuredTimeoutMs,
|
|
43617
|
-
|
|
43774
|
+
stage: "prompt admission",
|
|
43775
|
+
after: cursors.get(sessionId),
|
|
43776
|
+
skills: workflowOptions.skills,
|
|
43777
|
+
activity
|
|
43618
43778
|
});
|
|
43619
43779
|
cursors.set(sessionId, cursor);
|
|
43620
43780
|
let transcript = await host.sessions.export({
|
|
@@ -43632,14 +43792,12 @@ const acquireWorkflowHost = async (options) => {
|
|
|
43632
43792
|
}]
|
|
43633
43793
|
});
|
|
43634
43794
|
try {
|
|
43635
|
-
await host
|
|
43636
|
-
sessionID: sessionId,
|
|
43637
|
-
text: STATE_REPAIR_PROMPT
|
|
43638
|
-
});
|
|
43639
|
-
cursor = await waitForIdle(host, sessionId, {
|
|
43795
|
+
cursor = await runWorkflowTurn(host, sessionId, STATE_REPAIR_PROMPT, {
|
|
43640
43796
|
timeoutMs: remaining(),
|
|
43641
43797
|
configuredTimeoutMs,
|
|
43642
|
-
|
|
43798
|
+
stage: "workflow state repair",
|
|
43799
|
+
after: cursor,
|
|
43800
|
+
activity
|
|
43643
43801
|
});
|
|
43644
43802
|
cursors.set(sessionId, cursor);
|
|
43645
43803
|
transcript = await host.sessions.export({
|
|
@@ -43691,7 +43849,7 @@ const acquireWorkflowHost = async (options) => {
|
|
|
43691
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";
|
|
43692
43850
|
//#endregion
|
|
43693
43851
|
//#region src/workflows/prompts/research.md
|
|
43694
|
-
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";
|
|
43695
43853
|
//#endregion
|
|
43696
43854
|
//#region src/workflows/run.ts
|
|
43697
43855
|
const defaultDecide = async (inputs, spec) => {
|
|
@@ -43720,6 +43878,7 @@ const capState = (state, limit) => {
|
|
|
43720
43878
|
};
|
|
43721
43879
|
const researchPrompt = (spec, evidence, options) => TextTemplate.from(research_default).values({
|
|
43722
43880
|
workflow: spec.id,
|
|
43881
|
+
limit: options.limit,
|
|
43723
43882
|
instructions: spec.researchInstructions,
|
|
43724
43883
|
skills: spec.skills.join(", "),
|
|
43725
43884
|
stateSchema: JSON.stringify(spec.stateJsonSchema),
|
|
@@ -43782,54 +43941,88 @@ const runWorkflow = async (input, dependencies = {}) => {
|
|
|
43782
43941
|
const error = spec.validateDecisionInput(item);
|
|
43783
43942
|
if (error !== void 0) throw new Error(error);
|
|
43784
43943
|
}
|
|
43785
|
-
const
|
|
43786
|
-
const
|
|
43787
|
-
|
|
43788
|
-
permissions: actionPermissions
|
|
43789
|
-
}) : researched;
|
|
43790
|
-
const gitAfter = inspectGit(input.root);
|
|
43791
|
-
const files = changedFiles(gitAtStart, gitAfter);
|
|
43792
|
-
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(", ")}`);
|
|
43793
|
-
const run = {
|
|
43794
|
-
kind: "pagegraph-workflow-run",
|
|
43944
|
+
const runsDirectory = input.out ?? workflows.runsDirectory ?? ".pagegraph/runs";
|
|
43945
|
+
const checkpoint = {
|
|
43946
|
+
kind: "pagegraph-workflow-research-checkpoint",
|
|
43795
43947
|
schemaVersion: 1,
|
|
43796
43948
|
id,
|
|
43797
43949
|
workflow: spec.id,
|
|
43798
43950
|
startedAt: started.toISOString(),
|
|
43799
|
-
|
|
43951
|
+
checkpointedAt: now().toISOString(),
|
|
43800
43952
|
project: {
|
|
43801
43953
|
root: input.root,
|
|
43802
43954
|
head: gitAtStart.head,
|
|
43803
|
-
dirtyAtStart: gitAtStart.dirty
|
|
43955
|
+
dirtyAtStart: gitAtStart.dirty,
|
|
43956
|
+
filesAtStart: gitAtStart.files
|
|
43804
43957
|
},
|
|
43805
43958
|
options: input.options,
|
|
43806
|
-
model: host.model,
|
|
43807
|
-
targets: {
|
|
43808
|
-
pages: evidence.graph.nodes.map((node) => node.path),
|
|
43809
|
-
queries: input.options.queries,
|
|
43810
|
-
kinds: input.options.kinds
|
|
43811
|
-
},
|
|
43812
43959
|
evidence: {
|
|
43813
43960
|
...evidence,
|
|
43814
|
-
executor:
|
|
43815
|
-
},
|
|
43816
|
-
decisions: [decisionArtifact(spec, decisionInputs, decisionReport)],
|
|
43817
|
-
changes: {
|
|
43818
|
-
files,
|
|
43819
|
-
providerCalls: acted.executor.calls.map((call) => call.tool)
|
|
43961
|
+
executor: researched.executor
|
|
43820
43962
|
},
|
|
43821
|
-
|
|
43963
|
+
state,
|
|
43964
|
+
decisionInputs,
|
|
43822
43965
|
opencode: {
|
|
43823
43966
|
agent: "seo",
|
|
43824
|
-
sessionId:
|
|
43825
|
-
|
|
43967
|
+
sessionId: researched.sessionId,
|
|
43968
|
+
model: host.model,
|
|
43969
|
+
transcript: researched.transcript
|
|
43826
43970
|
}
|
|
43827
43971
|
};
|
|
43828
|
-
const
|
|
43829
|
-
|
|
43830
|
-
|
|
43831
|
-
|
|
43832
|
-
|
|
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
|
+
}
|
|
43833
44026
|
} finally {
|
|
43834
44027
|
await host.close();
|
|
43835
44028
|
}
|
|
@@ -44361,7 +44554,7 @@ var executor_default$1 = "# Executor Starters for SERP Analysis\n\nThese snippet
|
|
|
44361
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";
|
|
44362
44555
|
//#endregion
|
|
44363
44556
|
//#region src/cli/preset/opencode/skills/site-architecture/references/executor.md
|
|
44364
|
-
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";
|
|
44365
44558
|
//#endregion
|
|
44366
44559
|
//#region src/cli/preset/opencode/preset.ts
|
|
44367
44560
|
const skill = (name, definition, executor) => ({
|