pullfrog 0.1.1 → 0.1.3

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.
@@ -1 +1,20 @@
1
+ import type { TodoTracker } from "../utils/todoTracking.ts";
2
+ import { type AgentResult } from "./shared.ts";
3
+ type RunParams = {
4
+ label: string;
5
+ args: string[];
6
+ cwd: string;
7
+ env: Record<string, string | undefined>;
8
+ todoTracker?: TodoTracker | undefined;
9
+ onActivityTimeout?: (() => void) | undefined;
10
+ onToolUse?: ((event: {
11
+ toolName: string;
12
+ input: unknown;
13
+ }) => void) | undefined;
14
+ };
15
+ type ClaudeRunResult = AgentResult & {
16
+ sessionId?: string | undefined;
17
+ };
18
+ export declare function runClaude(params: RunParams): Promise<ClaudeRunResult>;
1
19
  export declare const claude: import("./shared.ts").Agent;
20
+ export {};
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Source for the opencode plugin we drop into the per-run tmpdir at
3
+ * `<XDG_CONFIG_HOME>/opencode/plugin/pullfrog-events.ts`. The harness already
4
+ * redirects `XDG_CONFIG_HOME` to `ctx.tmpdir/.config` (see `opencode.ts`
5
+ * `homeEnv`), so opencode's auto-discovery scans the tmpdir, never the user's
6
+ * working tree. opencode's `Global.Path.config` resolves to
7
+ * `path.join(xdgConfig, "opencode")` and the config layer auto-discovers
8
+ * plugins from every directory in its scan list — including
9
+ * `Global.Path.config` — by globbing `{plugin,plugins}/*.{ts,js}` via
10
+ * `ConfigPlugin.load(dir)`.
11
+ *
12
+ * We MUST NOT write into the user's repo working tree. The repo is a checkout
13
+ * the agent operates on; only the agent's own tools (gated by
14
+ * `OPENCODE_PERMISSION`) may modify it. The whole reason we redirect HOME and
15
+ * XDG_CONFIG_HOME is so harness-side files (config, plugins, scratch state)
16
+ * land in the tmpdir.
17
+ *
18
+ * Why this plugin exists: opencode's `task` tool runs subagents in-process and
19
+ * the CLI's `cli/cmd/run.ts` event loop filters `part.sessionID !== sessionID`,
20
+ * so subagent-internal `message.part.updated` events are silently discarded
21
+ * before reaching our parent NDJSON stream. plugins, by contrast, receive
22
+ * EVERY bus event via `bus.subscribeAll()` regardless of session.
23
+ *
24
+ * The plugin re-emits every relevant bus event onto opencode's stdout as a
25
+ * single JSON line wrapped in a sentinel envelope. our `runOpenCode` parser
26
+ * recognises the envelope, unpacks it, and routes the inner part through the
27
+ * existing handlers with a per-session label from `SessionLabeler` so each
28
+ * subagent's tool calls / text appear inline alongside the orchestrator's.
29
+ *
30
+ * Dumb plugin / smart parent split: the plugin emits every part for every
31
+ * session. the parent dedupes against the orchestrator's own session id (which
32
+ * it already knows from the `init` event). this keeps the plugin trivial and
33
+ * keeps the per-session attribution logic on the parent side where the
34
+ * SessionLabeler already lives.
35
+ *
36
+ * Event-name prefixing: the wrapped event-type sentinel is
37
+ * `pullfrog_bus_event` — picked to be unmistakably ours so a future opencode
38
+ * release that introduces a coincidentally-named event type won't collide.
39
+ */
40
+ export declare const PULLFROG_BUS_EVENT_TYPE: "pullfrog_bus_event";
41
+ export declare const PULLFROG_OPENCODE_PLUGIN_FILENAME: "pullfrog-events.ts";
42
+ /**
43
+ * Source written verbatim to `<XDG_CONFIG_HOME>/opencode/plugin/pullfrog-events.ts`.
44
+ *
45
+ * - Structural typing only (no runtime import of `@opencode-ai/plugin`):
46
+ * opencode installs that dep into the directory containing the plugin
47
+ * alongside discovery, but a) the dep isn't required for the structural
48
+ * shape we use, and b) keeping zero imports avoids any module-resolution
49
+ * coupling to opencode's plugin-loader internals across versions.
50
+ * - default export is the plugin factory (opencode's plugin loader accepts
51
+ * default exports as the server entrypoint).
52
+ * - we only forward `message.part.updated`. that's where the user-visible
53
+ * subagent activity (tool calls, text, step transitions) lives. add more
54
+ * event types here if the parent needs them.
55
+ * - JSON.stringify+single write keeps the line atomic up to PIPE_BUF (4KB on
56
+ * Linux). longer parts may interleave with concurrent stdout writers; the
57
+ * parser tolerates non-JSON lines (logs them at debug) so a torn line is a
58
+ * missed event, not a crash.
59
+ */
60
+ export declare const PULLFROG_OPENCODE_PLUGIN_SOURCE: string;
@@ -1,5 +1,15 @@
1
- import { type AgentId } from "../external.ts";
2
- import { type AgentResult, type AgentUsage, type PostRunIssues, type StopHookFailure } from "./shared.ts";
1
+ import type { ToolState } from "../toolState.ts";
2
+ import { type AgentResult, type AgentRunContext, type AgentUsage, type PostRunIssues, type StopHookFailure } from "./shared.ts";
3
+ /**
4
+ * derive "agent picked a review mode but never produced visible output" from
5
+ * the literal facts on `toolState`. returns the selected mode when the gate
6
+ * should fire, `null` otherwise — pure read, no side effects, safe to invoke
7
+ * after every agent attempt.
8
+ *
9
+ * the gate is anchored to `hadProgressComment` so silent runs (non-issue
10
+ * events, dispatcher skipped seeding) don't fire a nudge there's no UI for.
11
+ */
12
+ export declare function getUnsubmittedReview(toolState: ToolState): "Review" | "IncrementalReview" | null;
3
13
  /**
4
14
  * run the user-configured stop hook.
5
15
  *
@@ -16,35 +26,38 @@ import { type AgentResult, type AgentUsage, type PostRunIssues, type StopHookFai
16
26
  export declare function executeStopHook(script: string): Promise<StopHookFailure | null>;
17
27
  export declare function buildStopHookPrompt(failure: StopHookFailure): string;
18
28
  export declare function buildSummaryStalePrompt(filePath: string): string;
29
+ export declare function buildUnsubmittedReviewPrompt(mode: "Review" | "IncrementalReview"): string;
19
30
  /**
20
31
  * check the post-run gates: did the stop hook pass, is the working tree
21
32
  * clean, and (when applicable) did the agent touch the rolling PR summary
22
- * snapshot? returns everything that still needs nudging so the caller can
23
- * render a single combined resume prompt.
33
+ * snapshot or produce review output? returns everything that still needs
34
+ * nudging so the caller can render a single combined resume prompt.
24
35
  *
25
- * the summary-stale check is skipped when `summaryFilePath` / `summarySeed`
26
- * are not provided; this is the common case (non-PR runs, runs where the
27
- * dispatcher didn't request snapshot generation, runs where the seed step
28
- * failed). loop callers also pass these as undefined after the agent has
29
- * already been nudged once, to avoid burning the retry budget on a soft
30
- * non-blocking gate.
36
+ * reads run state directly off `ctx.toolState` so each invocation sees the
37
+ * latest mutations from MCP tool calls. `skipSummaryStale` lets the loop
38
+ * suppress the summary-stale check after the one-shot nudge has been
39
+ * delivered (re-firing it would burn the retry budget on a soft gate the
40
+ * agent has already decided not to act on).
31
41
  */
32
- export declare function collectPostRunIssues(params: {
33
- stopScript: string | null | undefined;
34
- summaryFilePath?: string | undefined;
35
- summarySeed?: string | undefined;
42
+ export declare function collectPostRunIssues(ctx: AgentRunContext, options?: {
43
+ skipSummaryStale?: boolean;
36
44
  }): Promise<PostRunIssues>;
37
45
  export declare function buildPostRunPrompt(issues: PostRunIssues): string;
38
46
  /**
39
- * prompt for a dedicated post-run reflection turn nudging the agent to call
40
- * `update_learnings` if it discovered anything worth persisting.
47
+ * prompt for a dedicated post-run reflection turn nudging the agent to edit
48
+ * the rolling learnings file if it discovered anything worth persisting.
49
+ *
50
+ * this exists because passive "if you learned something, write it down"
51
+ * instructions baked into mode checklists are frequently ignored — the agent
52
+ * stays focused on the task and the meta-ask falls through. delivering it
53
+ * as its own resume turn, with nothing competing for attention, raises the
54
+ * fire rate substantially.
41
55
  *
42
- * this exists because the learnings step baked into mode checklists is
43
- * frequently ignored the agent stays focused on the task and the meta-ask
44
- * falls through. delivering it as its own resume turn, with nothing competing
45
- * for attention, raises the fire rate substantially.
56
+ * the file is the single source of truth there is no separate MCP tool
57
+ * call. the server reads the file at end-of-run and persists any edits to
58
+ * `Repo.learnings`.
46
59
  */
47
- export declare function buildLearningsReflectionPrompt(agentId: AgentId): string;
60
+ export declare function buildLearningsReflectionPrompt(filePath: string): string;
48
61
  /**
49
62
  * shared post-run retry loop used by every agent harness.
50
63
  *
@@ -65,17 +78,9 @@ export declare function buildLearningsReflectionPrompt(agentId: AgentId): string
65
78
  * behavior: they're logged but don't fail the run.
66
79
  */
67
80
  export declare function runPostRunRetryLoop<R extends AgentResult>(params: {
81
+ ctx: AgentRunContext;
68
82
  initialResult: R;
69
83
  initialUsage: AgentUsage | undefined;
70
- stopScript: string | null | undefined;
71
- /** absolute path to the seeded PR summary file. when set together with
72
- * `summarySeed`, the loop checks after each agent attempt whether the
73
- * file has been edited; if not, it nudges the agent ONCE via a resume
74
- * turn (subsequent iterations skip the check so we don't keep burning
75
- * retries on a soft gate when the agent has decided no edit is warranted). */
76
- summaryFilePath?: string | undefined;
77
- /** exact bytes of the seeded summary file used for the unchanged-check. */
78
- summarySeed?: string | undefined;
79
84
  resume: (context: {
80
85
  prompt: string;
81
86
  previousResult: R;
@@ -1,4 +1,5 @@
1
1
  import type { AgentId } from "../external.ts";
2
+ import type { ToolState } from "../toolState.ts";
2
3
  import type { ResolvedInstructions } from "../utils/instructions.ts";
3
4
  import type { ResolvedPayload } from "../utils/payload.ts";
4
5
  import type { TodoTracker } from "../utils/todoTracking.ts";
@@ -25,6 +26,17 @@ export interface PostRunIssues {
25
26
  * seed, i.e. the agent never touched it. soft gate — nudges once via a
26
27
  * resume turn but never fails the run, parallel to dirtyTree semantics. */
27
28
  summaryStale?: SummaryStale;
29
+ /**
30
+ * populated when the agent selected a review mode but the post-run check
31
+ * over toolState shows neither a `create_pull_request_review` submission
32
+ * nor a final `report_progress` write happened. derived inline from
33
+ * `toolState.selectedMode` + `toolState.review` + `toolState.finalSummaryWritten`
34
+ * via {@link getUnsubmittedReview} — no parallel toolState flag is stored.
35
+ * carries the mode name so the resume prompt can reference it. handled like
36
+ * `stopHook`: nudge via resume, hard-fail if still unsatisfied after
37
+ * `MAX_POST_RUN_RETRIES`.
38
+ */
39
+ unsubmittedReview?: "Review" | "IncrementalReview";
28
40
  }
29
41
  export declare function hasPostRunIssues(issues: PostRunIssues): boolean;
30
42
  /**
@@ -64,7 +76,14 @@ export interface AgentResult {
64
76
  usage?: AgentUsage | undefined;
65
77
  }
66
78
  /**
67
- * Minimal context passed to agent.run()
79
+ * Context passed to agent.run() and threaded through the post-run loop.
80
+ *
81
+ * design rule: this is the single object that flows through the harness and
82
+ * downstream utilities by reference. derived predicates (e.g.
83
+ * `getUnsubmittedReview`), tmpfile paths, and seed bytes live on
84
+ * `toolState` — read them at the call site, do not duplicate them onto this
85
+ * interface. utilities that need run state should accept `ctx` whole, not
86
+ * destructure a narrow subset.
68
87
  */
69
88
  export interface AgentRunContext {
70
89
  payload: ResolvedPayload;
@@ -80,19 +99,13 @@ export interface AgentRunContext {
80
99
  */
81
100
  stopScript?: string | null | undefined;
82
101
  /**
83
- * absolute path to the rolling PR summary tmpfile, when one was seeded
84
- * for this run (Review / IncrementalReview / pr-summary Task). enables
85
- * a post-run sanity nudge that prompts the agent if the file is still
86
- * byte-identical to its seed.
87
- */
88
- summaryFilePath?: string | undefined;
89
- /**
90
- * exact bytes of the seeded summary file. compared against the current
91
- * file content after each agent attempt to detect "agent forgot to edit
92
- * the summary" — particularly common with smaller models that lose
93
- * track of multi-step instructions.
102
+ * mutable per-run state shared with the MCP server (by reference). post-run
103
+ * gates read fresh values from it after each agent attempt — `summaryFilePath`,
104
+ * `summarySeed`, `selectedMode`, `review`, `finalSummaryWritten`,
105
+ * `hadProgressComment` are all consulted by `collectPostRunIssues`. see
106
+ * `action/toolState.ts` for the literal-state design rule.
94
107
  */
95
- summarySeed?: string | undefined;
108
+ toolState: ToolState;
96
109
  /**
97
110
  * called synchronously when the agent subprocess is killed for inner
98
111
  * activity timeout. lets main.ts tear down shared resources (MCP HTTP