@quintinshaw/pi-dynamic-workflows 1.4.0 → 1.6.0

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 CHANGED
@@ -55,9 +55,19 @@ Ask for a background workflow (the model passes `background: true`) and it runs
55
55
  /workflows status <id> # show a run's progress
56
56
  /workflows stop <id> # abort a running run
57
57
  /workflows pause <id> # pause a running run
58
+ /workflows resume <id> # resume an interrupted run (replays cached results)
58
59
  /workflows rm <id> # remove a run from the list
59
60
  ```
60
61
 
62
+ ### Bundled workflows
63
+
64
+ ```text
65
+ /deep-research <question> # web-researched, source-cross-checked report
66
+ /adversarial-review <task> # findings cross-checked by skeptical reviewers
67
+ ```
68
+
69
+ `/deep-research` fans out web searches across several angles, fetches the top sources with real `web_search` / `web_fetch` tools, keeps only claims supported by multiple sources, and writes a cited report.
70
+
61
71
  ## Workflow script shape
62
72
 
63
73
  A workflow is plain JavaScript. The first statement must export literal metadata:
@@ -99,6 +109,7 @@ return { inventory, summary }
99
109
  | `phase` | string | Override the current phase for this agent |
100
110
  | `schema` | object | JSON Schema for structured output |
101
111
  | `model` | string | Run this agent on a specific model — `provider/modelId` or a bare `modelId` |
112
+ | `isolation` | `"worktree"` | Run this agent in its own throwaway git worktree (parallel edits without conflict) |
102
113
  | `timeoutMs` | number | Override the default 5-minute agent timeout |
103
114
 
104
115
  Models can also be set per phase via `meta.phases[].model`. Precedence is `opts.model` > phase model > session default; an unknown model logs a warning and falls back to the default.
@@ -134,7 +145,9 @@ Scripts run inside a Node `vm` sandbox. Intentionally unavailable: `Date.now()`,
134
145
  - **Real token & cost accounting** — read from each subagent's SDK session (input / output / total / cost), with a character estimate only as fallback when a provider reports no usage; `budget` gates on the real total
135
146
  - **Real per-agent / per-phase model routing** — `opts.model` and `meta.phases[].model` actually select the model (resolved against your authed model registry), with graceful fallback
136
147
  - **`/workflows` command** — list, inspect, stop, pause, **resume**, and remove background runs; runs started with `background: true` are reachable from the command
148
+ - **Bundled `/deep-research` & `/adversarial-review`** — `/deep-research` runs real web searches (via built-in `web_search` / `web_fetch` tools), extracts claims, cross-checks them across sources, and reports only what survived; `/adversarial-review` investigates a task then has independent skeptics try to refute each finding, keeping only those that clear an agreement threshold
137
149
  - **Resume** — each agent result is journaled by a deterministic call index; resuming replays the unchanged prefix from cache (no re-run, no tokens) and runs only new or edited calls live
150
+ - **Worktree isolation** — `isolation: "worktree"` runs an agent in its own git worktree on a throwaway branch, so parallel agents can edit the same files without conflict; the worktree is torn down after (results are not auto-merged), and it falls back to a logged no-op outside a git repo
138
151
  - **Safety limits** — 1000-agent cap (`maxAgents`), per-agent timeout (`agentTimeoutMs`), recoverable-vs-fatal error classification
139
152
  - **Live progress + token/cost display**, `Esc` to abort
140
153
  - **Log persistence** to `.pi/workflows/runs/`
@@ -143,8 +156,8 @@ Scripts run inside a Node `vm` sandbox. Intentionally unavailable: `Date.now()`,
143
156
 
144
157
  Tracked toward closer parity with Claude Code dynamic workflows:
145
158
 
146
- - **Worktree isolation** for parallel edits, and **bundled `/deep-research`**
147
159
  - **Saved workflows** as `/<name>` slash commands
160
+ - **Nested `workflow()`** to compose saved workflows inline
148
161
 
149
162
  ## How it works
150
163
 
@@ -11,9 +11,14 @@ export interface AdversarialReviewConfig {
11
11
  agreementThreshold: number;
12
12
  }
13
13
  /**
14
- * Generate an adversarial review workflow script.
14
+ * Generate an adversarial-review workflow. The script is static and reads its
15
+ * inputs from `args` (task/reviewers/threshold) — no string interpolation.
16
+ *
17
+ * Each finding is judged independently by N reviewers who are told to REFUTE it;
18
+ * a finding survives only when the share of reviewers calling it real meets the
19
+ * agreement threshold.
15
20
  */
16
- export declare function generateAdversarialReviewWorkflow(taskDescription: string, config?: Partial<AdversarialReviewConfig>): string;
21
+ export declare function generateAdversarialReviewWorkflow(): string;
17
22
  /**
18
23
  * Generate a multi-perspective analysis workflow.
19
24
  */
@@ -2,56 +2,64 @@
2
2
  * Adversarial review mode for workflows.
3
3
  * Agents cross-check each other's findings for higher quality results.
4
4
  */
5
- const DEFAULT_CONFIG = {
6
- reviewerCount: 2,
7
- filterContested: true,
8
- agreementThreshold: 0.5,
9
- };
10
5
  /**
11
- * Generate an adversarial review workflow script.
6
+ * Generate an adversarial-review workflow. The script is static and reads its
7
+ * inputs from `args` (task/reviewers/threshold) — no string interpolation.
8
+ *
9
+ * Each finding is judged independently by N reviewers who are told to REFUTE it;
10
+ * a finding survives only when the share of reviewers calling it real meets the
11
+ * agreement threshold.
12
12
  */
13
- export function generateAdversarialReviewWorkflow(taskDescription, config = {}) {
14
- const cfg = { ...DEFAULT_CONFIG, ...config };
13
+ export function generateAdversarialReviewWorkflow() {
15
14
  return `export const meta = {
16
15
  name: 'adversarial_review',
17
- description: 'Adversarial review with ${cfg.reviewerCount} independent reviewers',
16
+ description: 'Adversarial review: findings cross-checked by independent skeptics',
18
17
  phases: [
19
- { title: 'Initial Investigation' },
20
- { title: 'Independent Review' },
21
- { title: 'Cross-Check' },
18
+ { title: 'Investigate' },
19
+ { title: 'Refute' },
22
20
  { title: 'Consensus' },
23
21
  ],
24
- };
22
+ }
25
23
 
26
- phase('Initial Investigation');
27
- const findings = await agent(
28
- 'Investigate and document findings for: ${taskDescription.replace(/'/g, "\\'").slice(0, 80)}',
29
- { label: 'investigator' }
30
- );
24
+ const task = (args && args.task) || ''
25
+ const reviewers = (args && args.reviewers) || 2
26
+ const threshold = (args && args.threshold) || 0.5
31
27
 
32
- phase('Independent Review');
33
- const reviews = await parallel(Array.from({ length: ${cfg.reviewerCount} }, (_, i) => () =>
34
- agent(
35
- 'Independently review these findings. Agree or disagree with each point, and explain why:\\n\\n' + findings,
36
- { label: 'reviewer-' + (i + 1) }
37
- )
38
- ));
28
+ phase('Investigate')
29
+ const investigation = await agent(
30
+ 'Investigate the following and list concrete, individually-checkable findings:\\n' + task,
31
+ { label: 'investigate', schema: { type: 'object', properties: { findings: { type: 'array', items: { type: 'string' } } }, required: ['findings'] } }
32
+ )
33
+ const findings = investigation.findings || []
39
34
 
40
- phase('Cross-Check');
41
- const crossCheck = await agent(
42
- 'Compare these independent reviews and identify points of agreement and disagreement:\\n' +
43
- 'Reviews: ' + JSON.stringify(reviews) + '\\n' +
44
- 'Original findings: ' + findings,
45
- { label: 'cross-checker' }
46
- );
35
+ phase('Refute')
36
+ const judged = await parallel(findings.map((f, i) => () =>
37
+ parallel(Array.from({ length: reviewers }, (_, r) => () =>
38
+ agent(
39
+ 'You are a skeptical reviewer. Try to REFUTE this finding for the task below. ' +
40
+ 'Default to real=false when uncertain. Investigate with the available tools if needed.\\n\\n' +
41
+ 'TASK: ' + task + '\\nFINDING: ' + f,
42
+ { label: 'refute ' + (i + 1) + '.' + (r + 1), schema: { type: 'object', properties: { real: { type: 'boolean' }, reason: { type: 'string' } }, required: ['real'] } }
43
+ )
44
+ )).then((votes) => {
45
+ const valid = votes.filter(Boolean)
46
+ const realCount = valid.filter((v) => v && v.real).length
47
+ const ratio = valid.length ? realCount / valid.length : 0
48
+ return { finding: f, realVotes: realCount, totalVotes: valid.length, survives: ratio >= threshold }
49
+ })
50
+ ))
47
51
 
48
- phase('Consensus');
49
- const consensus = await agent(
50
- 'Based on the cross-check, produce a final verified report. Only include findings that survived independent review:\\n' + crossCheck,
51
- { label: 'consensus-builder' }
52
- );
52
+ const survivors = judged.filter((j) => j && j.survives)
53
+
54
+ phase('Consensus')
55
+ const report = await agent(
56
+ 'Write a final review report. Include ONLY the findings that survived adversarial review (listed below), ' +
57
+ 'each with a short justification. Note how many were discarded.\\n\\n' +
58
+ 'SURVIVING FINDINGS JSON:\\n' + JSON.stringify(survivors),
59
+ { label: 'consensus' }
60
+ )
53
61
 
54
- return { findings, reviews, crossCheck, consensus };`;
62
+ return { total: findings.length, survivors, report }`;
55
63
  }
56
64
  /**
57
65
  * Generate a multi-perspective analysis workflow.
package/dist/agent.d.ts CHANGED
@@ -38,6 +38,8 @@ export interface AgentRunOptions<TSchemaDef extends TSchema | undefined = undefi
38
38
  model?: string;
39
39
  /** Called with the resolved model id once known (for display/telemetry). */
40
40
  onModelResolved?: (modelId: string) => void;
41
+ /** Run this agent in a different working directory (e.g. an isolated worktree). */
42
+ cwd?: string;
41
43
  }
42
44
  export type AgentRunResult<TSchemaDef extends TSchema | undefined> = TSchemaDef extends TSchema ? Static<TSchemaDef> : string;
43
45
  export declare class WorkflowAgent {
package/dist/agent.js CHANGED
@@ -39,7 +39,11 @@ export class WorkflowAgent {
39
39
  }
40
40
  async run(prompt, options = {}) {
41
41
  const capture = { called: false, value: undefined };
42
- const customTools = [...this.baseTools, ...(options.tools ?? [])];
42
+ // Per-call cwd (e.g. a worktree) needs coding tools bound to that directory,
43
+ // since tools capture their cwd at construction and can't be relocated.
44
+ const runCwd = options.cwd ?? this.cwd;
45
+ const baseTools = runCwd === this.cwd ? this.baseTools : createCodingTools(runCwd);
46
+ const customTools = [...baseTools, ...(options.tools ?? [])];
43
47
  if (options.schema) {
44
48
  customTools.push(createStructuredOutputTool({ schema: options.schema, capture }));
45
49
  }
@@ -57,7 +61,7 @@ export class WorkflowAgent {
57
61
  }
58
62
  const agentDir = getAgentDir();
59
63
  const { session } = await createAgentSession({
60
- cwd: this.cwd,
64
+ cwd: runCwd,
61
65
  agentDir,
62
66
  sessionManager: SessionManager.inMemory(),
63
67
  // Use real SettingsManager to inherit user's default provider/model settings.
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Bundled workflow commands: `/deep-research` and `/adversarial-review`.
3
+ * They run a generated workflow script and print the final report.
4
+ */
5
+ import { type ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
+ export declare function registerBuiltinWorkflows(pi: ExtensionAPI, opts: {
7
+ cwd: string;
8
+ }): void;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Bundled workflow commands: `/deep-research` and `/adversarial-review`.
3
+ * They run a generated workflow script and print the final report.
4
+ */
5
+ import { createCodingTools } from "@earendil-works/pi-coding-agent";
6
+ import { generateAdversarialReviewWorkflow } from "./adversarial-review.js";
7
+ import { generateDeepResearchWorkflow } from "./deep-research.js";
8
+ import { createWebTools } from "./web-tools.js";
9
+ import { runWorkflow } from "./workflow.js";
10
+ function alreadyRegistered(pi, name) {
11
+ try {
12
+ return (pi.getCommands?.() ?? []).some((c) => c.name === name);
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ }
18
+ function reportText(result) {
19
+ const r = result.result;
20
+ if (r && typeof r.report === "string" && r.report.trim())
21
+ return r.report;
22
+ return JSON.stringify(result.result, null, 2);
23
+ }
24
+ export function registerBuiltinWorkflows(pi, opts) {
25
+ const cwd = opts.cwd;
26
+ if (!alreadyRegistered(pi, "deep-research")) {
27
+ pi.registerCommand("deep-research", {
28
+ description: "Research a question across the web with cross-checked sources",
29
+ async handler(args, ctx) {
30
+ const question = args.trim();
31
+ if (!question)
32
+ return ctx.ui.notify("Usage: /deep-research <question>", "warning");
33
+ ctx.ui.notify("Researching — running web searches across several angles…", "info");
34
+ try {
35
+ const result = await runWorkflow(generateDeepResearchWorkflow(), {
36
+ cwd,
37
+ args: { question },
38
+ // Research agents need real web access on top of the coding tools.
39
+ tools: [...createCodingTools(cwd), ...createWebTools()],
40
+ onPhase: (title) => ctx.ui.setStatus("deep-research", `research: ${title}`),
41
+ });
42
+ ctx.ui.setStatus("deep-research", undefined);
43
+ await pi.sendMessage({ customType: "deep-research", content: reportText(result), display: true });
44
+ }
45
+ catch (error) {
46
+ ctx.ui.setStatus("deep-research", undefined);
47
+ ctx.ui.notify(`deep-research failed: ${error instanceof Error ? error.message : error}`, "error");
48
+ }
49
+ },
50
+ });
51
+ }
52
+ if (!alreadyRegistered(pi, "adversarial-review")) {
53
+ pi.registerCommand("adversarial-review", {
54
+ description: "Investigate a task, then cross-check each finding with skeptical reviewers",
55
+ async handler(args, ctx) {
56
+ const task = args.trim();
57
+ if (!task)
58
+ return ctx.ui.notify("Usage: /adversarial-review <task or question>", "warning");
59
+ ctx.ui.notify("Reviewing — investigating then refuting each finding…", "info");
60
+ try {
61
+ const result = await runWorkflow(generateAdversarialReviewWorkflow(), {
62
+ cwd,
63
+ args: { task },
64
+ tools: createCodingTools(cwd),
65
+ onPhase: (title) => ctx.ui.setStatus("adversarial-review", `review: ${title}`),
66
+ });
67
+ ctx.ui.setStatus("adversarial-review", undefined);
68
+ await pi.sendMessage({ customType: "adversarial-review", content: reportText(result), display: true });
69
+ }
70
+ catch (error) {
71
+ ctx.ui.setStatus("adversarial-review", undefined);
72
+ ctx.ui.notify(`adversarial-review failed: ${error instanceof Error ? error.message : error}`, "error");
73
+ }
74
+ },
75
+ });
76
+ }
77
+ }
@@ -3,19 +3,19 @@
3
3
  * Built-in workflow for comprehensive research across multiple sources.
4
4
  */
5
5
  export interface DeepResearchConfig {
6
- /** Number of search angles to explore. */
7
- searchAngles: number;
8
- /** Number of sources to fetch per angle. */
9
- sourcesPerAngle: number;
10
- /** Whether to cross-check claims across sources. */
11
- crossCheck: boolean;
12
- /** Maximum number of agents to use. */
13
- maxAgents: number;
6
+ /** Number of distinct search angles/queries to explore. */
7
+ angles: number;
8
+ /** Minimum distinct sources required for a claim to survive cross-checking. */
9
+ minSupport: number;
14
10
  }
15
11
  /**
16
- * Generate a deep research workflow script.
12
+ * Generate a deep-research workflow that uses the real web_search/web_fetch tools.
13
+ *
14
+ * The script is static and reads its inputs from `args` (question/angles/minSupport),
15
+ * so the question is never string-interpolated into source — no escaping hazards.
16
+ * Inject the web tools at run time via the agent's `tools` option.
17
17
  */
18
- export declare function generateDeepResearchWorkflow(question: string, config?: Partial<DeepResearchConfig>): string;
18
+ export declare function generateDeepResearchWorkflow(): string;
19
19
  /**
20
20
  * Generate a codebase audit workflow.
21
21
  */
@@ -2,66 +2,66 @@
2
2
  * Deep research workflow.
3
3
  * Built-in workflow for comprehensive research across multiple sources.
4
4
  */
5
- const DEFAULT_CONFIG = {
6
- searchAngles: 4,
7
- sourcesPerAngle: 3,
8
- crossCheck: true,
9
- maxAgents: 20,
10
- };
11
5
  /**
12
- * Generate a deep research workflow script.
6
+ * Generate a deep-research workflow that uses the real web_search/web_fetch tools.
7
+ *
8
+ * The script is static and reads its inputs from `args` (question/angles/minSupport),
9
+ * so the question is never string-interpolated into source — no escaping hazards.
10
+ * Inject the web tools at run time via the agent's `tools` option.
13
11
  */
14
- export function generateDeepResearchWorkflow(question, config = {}) {
15
- const cfg = { ...DEFAULT_CONFIG, ...config };
16
- const escapedQuestion = question.replace(/'/g, "\\'").slice(0, 80);
17
- const crossCheckPhase = cfg.crossCheck
18
- ? `phase('Cross-Check');
19
- const crossCheck = await agent(
20
- 'Cross-check these research findings. Identify claims that are supported by multiple sources vs. claims that appear in only one source:\\n' +
21
- 'Sources: ' + JSON.stringify(sources),
22
- { label: 'cross-checker' }
23
- );`
24
- : "";
25
- const crossCheckRef = cfg.crossCheck ? "'Cross-check: ' + crossCheck + '\\n' + " : "";
26
- const crossCheckReturn = cfg.crossCheck ? "crossCheck, " : "";
12
+ export function generateDeepResearchWorkflow() {
27
13
  return `export const meta = {
28
14
  name: 'deep_research',
29
- description: 'Deep research: ${escapedQuestion}',
15
+ description: 'Deep research with real web search and cross-checked claims',
30
16
  phases: [
31
- { title: 'Search Planning' },
32
- { title: 'Source Gathering' },
33
- { title: 'Cross-Check' },
17
+ { title: 'Queries' },
18
+ { title: 'Gather' },
19
+ { title: 'Verify' },
34
20
  { title: 'Report' },
35
21
  ],
36
- };
22
+ }
37
23
 
38
- phase('Search Planning');
39
- const question = '${escapedQuestion}';
40
- const searchPlan = await agent(
41
- 'Plan ${cfg.searchAngles} different search angles to research this question comprehensively: ' + question,
42
- { label: 'search-planner' }
43
- );
24
+ const question = (args && args.question) || ''
25
+ const angles = (args && args.angles) || 4
26
+ const minSupport = (args && args.minSupport) || 2
44
27
 
45
- phase('Source Gathering');
46
- const sources = await parallel(Array.from({ length: ${cfg.searchAngles} }, (_, i) => () =>
28
+ phase('Queries')
29
+ const plan = await agent(
30
+ 'You are planning web research for this question:\\n' + question +
31
+ '\\n\\nProduce ' + angles + ' diverse, specific search queries that together cover the question from different angles.',
32
+ { label: 'plan queries', schema: { type: 'object', properties: { queries: { type: 'array', items: { type: 'string' } } }, required: ['queries'] } }
33
+ )
34
+ const queries = (plan.queries || []).slice(0, angles)
35
+
36
+ phase('Gather')
37
+ const gathered = await parallel(queries.map((q, i) => () =>
47
38
  agent(
48
- 'Research angle ' + (i + 1) + ' for this question: ' + question + '\\n\\nPlan: ' + searchPlan + '\\n\\nFind and summarize ${cfg.sourcesPerAngle} relevant sources.',
49
- { label: 'researcher-' + (i + 1) }
39
+ 'Research this query using the web_search and web_fetch tools.\\nQuery: ' + q +
40
+ '\\n\\nSteps: (1) call web_search with the query; (2) web_fetch the 2 most relevant result URLs; ' +
41
+ '(3) extract concrete, verifiable factual claims, each tagged with the exact source URL it came from. ' +
42
+ 'Do NOT invent sources or claims — report only what the fetched pages actually say.',
43
+ { label: 'research ' + (i + 1), schema: { type: 'object', properties: { sources: { type: 'array', items: { type: 'object', properties: { url: { type: 'string' }, claims: { type: 'array', items: { type: 'string' } } }, required: ['url', 'claims'] } } }, required: ['sources'] } }
50
44
  )
51
- ));
45
+ ))
46
+ const allSources = gathered.filter(Boolean).flatMap((g) => (g && g.sources) || [])
52
47
 
53
- ${crossCheckPhase}
48
+ phase('Verify')
49
+ const verdict = await agent(
50
+ 'Cross-check these research sources. Group claims that assert the same fact across different source URLs. ' +
51
+ 'Keep a claim only if it is supported by at least ' + minSupport + ' distinct source URLs OR by one clearly authoritative source. ' +
52
+ 'Discard claims found in a single weak source or that conflict with others.\\n\\nSOURCES JSON:\\n' + JSON.stringify(allSources),
53
+ { label: 'cross-check', schema: { type: 'object', properties: { supported: { type: 'array', items: { type: 'object', properties: { claim: { type: 'string' }, sources: { type: 'array', items: { type: 'string' } } }, required: ['claim', 'sources'] } }, discarded: { type: 'array', items: { type: 'string' } } }, required: ['supported'] } }
54
+ )
54
55
 
55
- phase('Report');
56
+ phase('Report')
56
57
  const report = await agent(
57
- 'Synthesize a comprehensive research report from these findings:\\n' +
58
- 'Question: ' + question + '\\n' +
59
- 'Sources: ' + JSON.stringify(sources) + '\\n' +
60
- ${crossCheckRef}'\\n\\nProduce a well-structured report with citations and confidence levels.',
61
- { label: 'report-writer' }
62
- );
58
+ 'Write a concise, well-structured research report that answers the question using ONLY the supported claims below. ' +
59
+ 'Cite source URLs inline next to each claim. If the evidence is thin, say so explicitly.\\n\\n' +
60
+ 'QUESTION: ' + question + '\\n\\nSUPPORTED CLAIMS JSON:\\n' + JSON.stringify((verdict && verdict.supported) || []),
61
+ { label: 'write report' }
62
+ )
63
63
 
64
- return { searchPlan, sources, ${crossCheckReturn}report };`;
64
+ return { question, queries, supported: (verdict && verdict.supported) || [], report }`;
65
65
  }
66
66
  /**
67
67
  * Generate a codebase audit workflow.
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ export type { AgentRunOptions, AgentRunResult, WorkflowAgentOptions } from "./ag
4
4
  export { WorkflowAgent } from "./agent.js";
5
5
  export type { AutoWorkflowConfig } from "./auto-workflow.js";
6
6
  export { shouldUseWorkflow, suggestWorkflowScript } from "./auto-workflow.js";
7
+ export { registerBuiltinWorkflows } from "./builtin-commands.js";
7
8
  export * from "./config.js";
8
9
  export type { DeepResearchConfig } from "./deep-research.js";
9
10
  export { generateCodebaseAuditWorkflow, generateDeepResearchWorkflow } from "./deep-research.js";
@@ -18,7 +19,8 @@ export type { PersistedRunState, RunPersistence, RunStatus } from "./run-persist
18
19
  export { createRunPersistence, generateRunId } from "./run-persistence.js";
19
20
  export type { StructuredOutputCapture, StructuredOutputToolOptions } from "./structured-output.js";
20
21
  export { createStructuredOutputTool } from "./structured-output.js";
21
- export type { AgentOptions, WorkflowMeta, WorkflowMetaPhase, WorkflowRunOptions, WorkflowRunResult, } from "./workflow.js";
22
+ export { createWebFetchTool, createWebSearchTool, createWebTools } from "./web-tools.js";
23
+ export type { AgentOptions, JournalEntry, WorkflowMeta, WorkflowMetaPhase, WorkflowRunOptions, WorkflowRunResult, } from "./workflow.js";
22
24
  export { parseWorkflowScript, runWorkflow } from "./workflow.js";
23
25
  export { registerWorkflowCommands } from "./workflow-commands.js";
24
26
  export type { ManagedRun, WorkflowManagerOptions } from "./workflow-manager.js";
@@ -27,3 +29,5 @@ export type { SavedWorkflow, WorkflowStorage } from "./workflow-saved.js";
27
29
  export { createWorkflowStorage } from "./workflow-saved.js";
28
30
  export type { WorkflowToolInput, WorkflowToolOptions } from "./workflow-tool.js";
29
31
  export { createWorkflowTool } from "./workflow-tool.js";
32
+ export type { Worktree } from "./worktree.js";
33
+ export { createWorktree, removeWorktree } from "./worktree.js";
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export { generateAdversarialReviewWorkflow, generateMultiPerspectiveWorkflow } from "./adversarial-review.js";
2
2
  export { WorkflowAgent } from "./agent.js";
3
3
  export { shouldUseWorkflow, suggestWorkflowScript } from "./auto-workflow.js";
4
+ export { registerBuiltinWorkflows } from "./builtin-commands.js";
4
5
  export * from "./config.js";
5
6
  export { generateCodebaseAuditWorkflow, generateDeepResearchWorkflow } from "./deep-research.js";
6
7
  export { createToolUpdateWorkflowDisplay, createWidgetWorkflowDisplay, createWorkflowSnapshot, preview, recomputeWorkflowSnapshot, renderWorkflowLines, renderWorkflowText, } from "./display.js";
@@ -9,8 +10,10 @@ export { createWorkflowLogger } from "./logger.js";
9
10
  export { buildModelRoutingInstructions, parseModelRoutingFromMeta, resolveModelForPhase } from "./model-routing.js";
10
11
  export { createRunPersistence, generateRunId } from "./run-persistence.js";
11
12
  export { createStructuredOutputTool } from "./structured-output.js";
13
+ export { createWebFetchTool, createWebSearchTool, createWebTools } from "./web-tools.js";
12
14
  export { parseWorkflowScript, runWorkflow } from "./workflow.js";
13
15
  export { registerWorkflowCommands } from "./workflow-commands.js";
14
16
  export { WorkflowManager } from "./workflow-manager.js";
15
17
  export { createWorkflowStorage } from "./workflow-saved.js";
16
18
  export { createWorkflowTool } from "./workflow-tool.js";
19
+ export { createWorktree, removeWorktree } from "./worktree.js";
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Real web tools for research workflows. These execute in the extension host
3
+ * process (which has network access), not in a subagent sandbox, so they perform
4
+ * genuine HTTP requests via Node's fetch.
5
+ *
6
+ * - web_search: best-effort Bing HTML scrape -> result {url, title}
7
+ * - web_fetch: fetch a URL and return readable text (HTML stripped, truncated)
8
+ */
9
+ import { type ToolDefinition } from "@earendil-works/pi-coding-agent";
10
+ /** A tool that searches the web (best-effort) and returns result URLs + titles. */
11
+ export declare function createWebSearchTool(): ToolDefinition;
12
+ /** A tool that fetches a URL and returns readable text. */
13
+ export declare function createWebFetchTool(maxChars?: number): ToolDefinition;
14
+ /** Both web tools, for injecting into a research workflow's agents. */
15
+ export declare function createWebTools(): ToolDefinition[];
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Real web tools for research workflows. These execute in the extension host
3
+ * process (which has network access), not in a subagent sandbox, so they perform
4
+ * genuine HTTP requests via Node's fetch.
5
+ *
6
+ * - web_search: best-effort Bing HTML scrape -> result {url, title}
7
+ * - web_fetch: fetch a URL and return readable text (HTML stripped, truncated)
8
+ */
9
+ import { defineTool } from "@earendil-works/pi-coding-agent";
10
+ import { Type } from "typebox";
11
+ const UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120 Safari/537.36";
12
+ async function fetchText(url, timeoutMs = 15000) {
13
+ const controller = new AbortController();
14
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
15
+ try {
16
+ const res = await fetch(url, { headers: { "user-agent": UA }, signal: controller.signal, redirect: "follow" });
17
+ return { status: res.status, body: await res.text() };
18
+ }
19
+ finally {
20
+ clearTimeout(timer);
21
+ }
22
+ }
23
+ function htmlToText(html) {
24
+ return html
25
+ .replace(/<script[\s\S]*?<\/script>/gi, " ")
26
+ .replace(/<style[\s\S]*?<\/style>/gi, " ")
27
+ .replace(/<\/(p|div|li|h[1-6]|tr|br)>/gi, "\n")
28
+ .replace(/<[^>]+>/g, " ")
29
+ .replace(/&nbsp;/g, " ")
30
+ .replace(/&amp;/g, "&")
31
+ .replace(/&lt;/g, "<")
32
+ .replace(/&gt;/g, ">")
33
+ .replace(/&#39;|&apos;/g, "'")
34
+ .replace(/&quot;/g, '"')
35
+ .replace(/[ \t]+/g, " ")
36
+ .replace(/\n{3,}/g, "\n\n")
37
+ .trim();
38
+ }
39
+ function parseBingResults(html, limit) {
40
+ const out = [];
41
+ const seen = new Set();
42
+ for (const m of html.matchAll(/<h2[^>]*>\s*<a[^>]+href="(https?:\/\/[^"]+)"[^>]*>([\s\S]*?)<\/a>/g)) {
43
+ const url = m[1];
44
+ if (/\.bing\.com|go\.microsoft\.com/.test(url) || seen.has(url))
45
+ continue;
46
+ seen.add(url);
47
+ out.push({ url, title: m[2].replace(/<[^>]+>/g, "").trim() });
48
+ if (out.length >= limit)
49
+ break;
50
+ }
51
+ return out;
52
+ }
53
+ /** A tool that searches the web (best-effort) and returns result URLs + titles. */
54
+ export function createWebSearchTool() {
55
+ return defineTool({
56
+ name: "web_search",
57
+ label: "Web Search",
58
+ description: "Search the web and return a list of result URLs and titles. Use before web_fetch to find sources.",
59
+ promptSnippet: "Search the web for sources",
60
+ parameters: Type.Object({
61
+ query: Type.String({ description: "The search query." }),
62
+ count: Type.Optional(Type.Number({ description: "Max results (default 6)." })),
63
+ }),
64
+ async execute(_id, params) {
65
+ const limit = Math.min(Math.max(params.count ?? 6, 1), 10);
66
+ try {
67
+ const { status, body } = await fetchText(`https://www.bing.com/search?q=${encodeURIComponent(params.query)}`);
68
+ const results = parseBingResults(body, limit);
69
+ const text = results.length
70
+ ? results.map((r, i) => `${i + 1}. ${r.title}\n ${r.url}`).join("\n")
71
+ : `No results parsed (HTTP ${status}). Try a different query or fetch a known URL directly.`;
72
+ return { content: [{ type: "text", text }], details: { results } };
73
+ }
74
+ catch (error) {
75
+ return {
76
+ content: [{ type: "text", text: `web_search failed: ${error instanceof Error ? error.message : error}` }],
77
+ details: { results: [] },
78
+ };
79
+ }
80
+ },
81
+ });
82
+ }
83
+ /** A tool that fetches a URL and returns readable text. */
84
+ export function createWebFetchTool(maxChars = 6000) {
85
+ return defineTool({
86
+ name: "web_fetch",
87
+ label: "Web Fetch",
88
+ description: "Fetch a URL and return its readable text content (HTML stripped, truncated).",
89
+ promptSnippet: "Fetch a URL's text",
90
+ parameters: Type.Object({
91
+ url: Type.String({ description: "The absolute URL to fetch." }),
92
+ }),
93
+ async execute(_id, params) {
94
+ try {
95
+ const { status, body } = await fetchText(params.url);
96
+ const text = htmlToText(body).slice(0, maxChars);
97
+ return {
98
+ content: [{ type: "text", text: `HTTP ${status} ${params.url}\n\n${text}` }],
99
+ details: { status, url: params.url },
100
+ };
101
+ }
102
+ catch (error) {
103
+ return {
104
+ content: [
105
+ {
106
+ type: "text",
107
+ text: `web_fetch failed for ${params.url}: ${error instanceof Error ? error.message : error}`,
108
+ },
109
+ ],
110
+ details: { status: 0, url: params.url },
111
+ };
112
+ }
113
+ },
114
+ });
115
+ }
116
+ /** Both web tools, for injecting into a research workflow's agents. */
117
+ export function createWebTools() {
118
+ return [createWebSearchTool(), createWebFetchTool()];
119
+ }
@@ -51,6 +51,7 @@ export interface WorkflowRunOptions extends WorkflowAgentOptions {
51
51
  phase?: string;
52
52
  result: unknown;
53
53
  tokens?: number;
54
+ worktree?: string;
54
55
  }) => void;
55
56
  onTokenUsage?: (usage: {
56
57
  input: number;