Skip to content

Milestone 2: HttpRunner + end-to-end session (Tasks 14, 15)#5

Merged
inhaq merged 2 commits into
mainfrom
m2-runners
Jun 13, 2026
Merged

Milestone 2: HttpRunner + end-to-end session (Tasks 14, 15)#5
inhaq merged 2 commits into
mainfrom
m2-runners

Conversation

@inhaq

@inhaq inhaq commented Jun 13, 2026

Copy link
Copy Markdown
Owner

This pull request was created by @kiro-agent on behalf of @inhaq 👻

Comment with /kiro fix to address specific feedback or /kiro all to address everything.
Learn about Kiro Web


Summary

Completes Milestone 2 (Runners) from .kiro/specs/loop-engine — Tasks 14 and 15.

Stacked PR. Based on task-13-role-binding (PR #4). Review/merge #4 first; this targets that branch so the diff shows only Milestone 2 work. Retarget to main once #4 merges.

Task 14 — HttpRunner

Vendor-neutral runner for OpenAI-compatible chat endpoints, behind the same AgentRunner interface. Entirely profile-driven:

  • baseUrl + path, model from the profile, secret-by-reference auth (apiKeyEnv names an env var → Authorization: Bearer), ${VAR}-expanded custom headers, optional temperature/maxTokens/extraBody.
  • Quota detection via HTTP status codes (default 429) and/or an error-body regex → sets quotaExhausted so the engine's degraded path kicks in.
  • AbortController timeout, bounded output capture, strict option validation at construction.
  • Transport (fetch) is injectable for tests; transport/HTTP failures resolve to a diagnostic meta result rather than throwing. Provider usage is surfaced in meta for the Milestone 5 cost ledger.
  • Registered in runnerFactory (the http kind now builds).

Task 15 — end-to-end session

  • session.runGoal(goal, config, opts): headless entrypoint that wires config → createRoles → plan review → per-task loop, runs tasks in dependency order, and skips dependents whose prerequisites didn't reach a usable terminal state. (Bounded concurrency + worktrees arrive in Milestone 4 behind this same entrypoint.)
  • src/run.ts CLI: npm start -- "your goal", fully config-driven, non-zero exit when anything needs human attention.

Testing

  • npm run typecheck clean.
  • npm test: 91 passing (+12: test/httpRunner.test.ts, test/session.test.ts, plus updated factory test). Covers request shaping, secret-by-reference auth, quota/error/timeout handling, and full multi-task sessions (all-verified path + dependent-skip-on-needs-human).

Summary by CodeRabbit

  • New Features

    • Added CLI entrypoint to run goals and task plans headlessly
    • Added HTTP runner for OpenAI-compatible endpoints with configurable base URL, headers, API keys, timeouts, and quota detection
    • Added session runner for multi-task execution with automatic dependency ordering, task skipping, and human blocking
  • Tests

    • Added comprehensive test coverage for HTTP runner and session execution
  • Chores

    • Updated Milestone 2 task checklist

Task 14 - HttpRunner: vendor-neutral runner for OpenAI-compatible chat
endpoints behind the AgentRunner interface. Profile-driven (baseUrl, path,
model from profile, env-referenced API key + headers, temperature/maxTokens,
extraBody), with HTTP-status + body-pattern quota detection, AbortController
timeout, bounded output capture, and strict option validation. Transport is
injectable for testing; transport failures resolve to a diagnostic result
rather than throwing. Registered in runnerFactory (http is now built).

Task 15 - session.runGoal: headless end-to-end entrypoint wiring config ->
createRoles -> plan review -> per-task loop, executing tasks in dependency
order and skipping dependents of unsatisfied prerequisites. Adds src/run.ts
CLI (npm start -- "goal") driven entirely by config.

Adds 12 tests (91 total); typecheck clean.
@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (1)
  • '.*'

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8f54f25a-0772-49a6-8a78-06d80f88e798

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch m2-runners

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/runners/httpRunner.ts`:
- Line 63: HttpRunner's options schema currently allows any string for
quota.pattern so invalid regexes slip past construction and only throw inside
run(); change construction to validate and precompile quota.pattern: in the
constructor (where HttpRunnerOptionsSchema.parse(...) is called) attempt to new
RegExp(this.opts.quota.pattern, "i") if a pattern is present, store the compiled
RegExp on the instance (e.g., this.quotaPatternRegex) and surface any syntax
error so construction fails fast; then update HttpRunner.run() (which currently
does new RegExp(this.opts.quota.pattern, "i") on non-ok responses) to use the
precompiled this.quotaPatternRegex (and skip matching if undefined) so runtime
exceptions are avoided and invalid patterns are caught at startup.

In `@src/session.ts`:
- Around line 57-76: dependencyOrder silently ignores cycles by returning early
in the inner visit when stack.has(task.id); change this to surface an explicit
error instead of dropping edges: in dependencyOrder (function dependencyOrder
and inner visit) detect a back-edge via stack.has(task.id) and throw a
descriptive Error (or a custom Error type) that includes the cycle (e.g.,
reconstruct the cycle path from the stack plus task.id) so callers know the plan
is invalid; keep the rest of the DFS logic and ordering behavior the same so
acyclic inputs still return a stable topological order.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0a80ef54-526b-4c4c-9e60-634883fc8bd3

📥 Commits

Reviewing files that changed from the base of the PR and between 08ca80e and 6850b4a.

📒 Files selected for processing (9)
  • .kiro/specs/loop-engine/tasks.md
  • package.json
  • src/run.ts
  • src/runners/httpRunner.ts
  • src/runners/runnerFactory.ts
  • src/session.ts
  • test/httpRunner.test.ts
  • test/roleBindings.test.ts
  • test/session.test.ts

Comment thread src/runners/httpRunner.ts Outdated
/** HTTP status codes that indicate an exhausted usage/rate window */
statusCodes: z.array(z.number().int()).default([429]),
/** case-insensitive regex tested against an error response body */
pattern: z.string().optional(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read-only verification:
# 1) Confirm schema currently accepts unvalidated regex strings.
# 2) Confirm runtime compilation happens in the hot error path.
rg -n 'pattern:\s*z\.string\(\)\.optional\(\)' src/runners/httpRunner.ts
rg -n 'new RegExp\(this\.opts\.quota\.pattern,\s*"i"\)' src/runners/httpRunner.ts

Repository: inhaq/loopwright

Length of output: 176


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Print schema and surrounding area
nl -ba src/runners/httpRunner.ts | sed -n '40,95p'

# Print the non-ok/error handling and regex usage area
nl -ba src/runners/httpRunner.ts | sed -n '120,190p'

Repository: inhaq/loopwright

Length of output: 103


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Schema area (lines 40-95) ==="
cat -n src/runners/httpRunner.ts | sed -n '40,95p'

echo "=== Error/quota area (lines 120-190) ==="
cat -n src/runners/httpRunner.ts | sed -n '120,190p'

Repository: inhaq/loopwright

Length of output: 5341


🏁 Script executed:

#!/bin/bash
set -euo pipefail
cat -n src/runners/httpRunner.ts | sed -n '190,270p'

Repository: inhaq/loopwright

Length of output: 2059


🏁 Script executed:

#!/bin/bash
set -euo pipefail
cat -n src/runners/httpRunner.ts | sed -n '190,270p'

Repository: inhaq/loopwright

Length of output: 2059


🏁 Script executed:

#!/bin/bash
set -euo pipefail
cat -n src/runners/httpRunner.ts | sed -n '95,140p'

Repository: inhaq/loopwright

Length of output: 2243


🏁 Script executed:

#!/bin/bash
set -euo pipefail
cat -n src/runners/httpRunner.ts | sed -n '95,140p'

Repository: inhaq/loopwright

Length of output: 2243


Validate and precompile quota.pattern during construction.

HttpRunnerOptionsSchema.parse(...) runs in the constructor, but quota.pattern is only z.string().optional() (Line 63), so invalid regexes pass startup validation. In run(), the runner compiles new RegExp(this.opts.quota.pattern, "i") (Line 152) on every non-res.ok response; if the regex is invalid, that exception is thrown inside the try and is handled by the catch, returning text: "" / quotaExhausted: false instead of the normal non-OK response meta.

Suggested fix
 export const HttpRunnerOptionsSchema = z
   .object({
@@
-        pattern: z.string().optional(),
+        pattern: z
+          .string()
+          .optional()
+          .refine(
+            (p) => {
+              if (p === undefined) return true;
+              try {
+                new RegExp(p, "i");
+                return true;
+              } catch {
+                return false;
+              }
+            },
+            "quota.pattern must be a valid regular expression",
+          ),
@@
 export class HttpRunner implements AgentRunner {
@@
+  private readonly quotaPattern?: RegExp;
@@
   constructor(profile: RunnerProfile, deps: HttpRunnerDeps = {}) {
@@
+    this.quotaPattern =
+      this.opts.quota.pattern !== undefined
+        ? new RegExp(this.opts.quota.pattern, "i")
+        : undefined;
@@
       if (!res.ok) {
         const errText = await safeText(res);
+        const quotaProbe = errText.slice(0, 8_000);
         const quotaExhausted =
           this.opts.quota.statusCodes.includes(res.status) ||
-          (this.opts.quota.pattern !== undefined &&
-            new RegExp(this.opts.quota.pattern, "i").test(errText));
+          (this.quotaPattern !== undefined && this.quotaPattern.test(quotaProbe));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runners/httpRunner.ts` at line 63, HttpRunner's options schema currently
allows any string for quota.pattern so invalid regexes slip past construction
and only throw inside run(); change construction to validate and precompile
quota.pattern: in the constructor (where HttpRunnerOptionsSchema.parse(...) is
called) attempt to new RegExp(this.opts.quota.pattern, "i") if a pattern is
present, store the compiled RegExp on the instance (e.g.,
this.quotaPatternRegex) and surface any syntax error so construction fails fast;
then update HttpRunner.run() (which currently does new
RegExp(this.opts.quota.pattern, "i") on non-ok responses) to use the precompiled
this.quotaPatternRegex (and skip matching if undefined) so runtime exceptions
are avoided and invalid patterns are caught at startup.

Source: Linters/SAST tools

Comment thread src/session.ts Outdated
Comment on lines +57 to +76
/** Orders tasks so dependencies precede dependents (stable; assumes acyclic). */
function dependencyOrder(tasks: TaskSpec[]): TaskSpec[] {
const byId = new Map(tasks.map((t) => [t.id, t]));
const visited = new Set<string>();
const ordered: TaskSpec[] = [];
const visit = (task: TaskSpec, stack: Set<string>): void => {
if (visited.has(task.id)) return;
if (stack.has(task.id)) return; // cycle guard; engine validates elsewhere
stack.add(task.id);
for (const depId of task.dependencies) {
const dep = byId.get(depId);
if (dep) visit(dep, stack);
}
stack.delete(task.id);
visited.add(task.id);
ordered.push(task);
};
for (const t of tasks) visit(t, new Set());
return ordered;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for cycle detection/validation in plan schema and engine

rg -n -C3 'cycle|acyclic|circular' --type=ts -g '!test' -g '!node_modules'

Repository: inhaq/loopwright

Length of output: 5440


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Find where dependencyOrder is used (to see if callers validate first)
rg -n "dependencyOrder\(" -S src

# 2) Search for any real acyclic/cycle validation logic in plan/schema/engine
rg -n -S "acyclic|circular|cycle guard|topolog|topolog|topological|dependencies.*acyclic|graph.*cycle|detect.*cycle" src

# 3) Search for task dependency normalization/validation
rg -n -S "dependencies.*(validate|coherent|check|schema|normalize|ordering)|TaskSpec" src/schemas src/domain src/engine src/session.ts

# 4) Inspect plan schema files that mention dependencies
ls -1 src/schemas || true
rg -n -S "dependencies|TaskSpec|dependency" src/schemas

Repository: inhaq/loopwright

Length of output: 1709


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show plan schema for any refinement around dependencies/acyclicity
echo "---- src/schemas/plan.ts (head) ----"
sed -n '1,140p' src/schemas/plan.ts

echo "---- src/session.ts (relevant sections) ----"
sed -n '1,220p' src/session.ts

echo "---- src/engine/loop.ts (search for dependencies/order usage) ----"
rg -n "dependencies|dependencyOrder|topolog|acyclic|cycle" src/engine/loop.ts

Repository: inhaq/loopwright

Length of output: 6199


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find runPlanReview implementation
rg -n "function runPlanReview|runPlanReview\s*\(" -S src

# Show the portion of src/engine/loop.ts that defines runPlanReview and plan validation
ls -1 src/engine
sed -n '1,260p' src/engine/loop.ts
sed -n '260,520p' src/engine/loop.ts

# Search for any cycle/acyclic validation logic beyond prompt text
rg -n -S "acyclic|circular|cycle|topolog|topological|graph.*cycle|detect.*cycle" src

Repository: inhaq/loopwright

Length of output: 15548


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -S "acyclic|circular|coherent|dependency(s)? .*coher|DAG|topolog" src

# Inspect critic schema/normalization
sed -n '1,220p' src/schemas/critic.ts
rg -n "normalizeReview|blockers as|nits as" src/schemas/critic.ts
sed -n '220,520p' src/schemas/critic.ts

# Inspect plan-review prompt wording that mentions acyclic dependencies
sed -n '200,280p' src/adapters/prompts.ts

Repository: inhaq/loopwright

Length of output: 5375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look for Zod refinements anywhere
rg -n "refine\(|superRefine\(" src/schemas

# Look for explicit dependency/cycle validation logic
rg -n -S "cycle|acyclic|circular|topolog|topological|graph|DAG|detect.*cycle" src

# Inspect critic JSON contract parsing/validation
rg -n "CRITIC_JSON_CONTRACT|parseCriticResponse|jsonExtract" -S src/engine src/adapters src/schemas

# Find where draftPlan parses/validates plan JSON
rg -n "draftPlan|parse.*plan|PlanSchema|TaskSpecSchema" src/engine src/adapters src

Repository: inhaq/loopwright

Length of output: 42


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Where PlanSchema/TaskSpecSchema are actually used/parsed
rg -n -S "PlanSchema\.|TaskSpecSchema\.|\.safeParse\(|\.parse\(" src

# 2) Find actor.draftPlan implementations (where plan JSON is produced/validated)
rg -n -S "draftPlan\(" src

# 3) Search for any dependency coherence/cycle logic outside prompts/session
rg -n -S "dependencies|dependencyOrder|blockedBy|acyclic|circular|cycle guard|topolog|topological|DAG|detect.*cycle" src

Repository: inhaq/loopwright

Length of output: 3060


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' src/adapters/runnerRoles.ts
echo "----"
sed -n '220,520p' src/adapters/runnerRoles.ts

echo "---- src/engine/jsonExtract.ts ----"
sed -n '1,220p' src/engine/jsonExtract.ts

echo "---- src/adapters/mocks.ts ----"
sed -n '1,200p' src/adapters/mocks.ts

echo "---- src/adapters/agents.ts (draftPlan interface) ----"
sed -n '1,120p' src/adapters/agents.ts

Repository: inhaq/loopwright

Length of output: 18063


Fix dependency-cycle handling in dependencyOrder (don’t silently accept cycles)

  • src/session.ts’s dependencyOrder() cycle guard (if (stack.has(task.id)) return;) silently drops cycle edges, which can lead to incomplete ordering and cascading task skips.
  • Upstream plan parsing doesn’t enforce acyclicity: src/adapters/runnerRoles.ts validates via PlanSchema.parse(...) / TaskSpecSchema (structure only; no dependency graph checks/refinements).
  • The only “acyclic” enforcement visible is a prompt instruction to the critic, which is not a machine-checked DAG validation—so cyclic plans can reach scheduling without a clear error.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/session.ts` around lines 57 - 76, dependencyOrder silently ignores cycles
by returning early in the inner visit when stack.has(task.id); change this to
surface an explicit error instead of dropping edges: in dependencyOrder
(function dependencyOrder and inner visit) detect a back-edge via
stack.has(task.id) and throw a descriptive Error (or a custom Error type) that
includes the cycle (e.g., reconstruct the cycle path from the stack plus
task.id) so callers know the plan is invalid; keep the rest of the DFS logic and
ordering behavior the same so acyclic inputs still return a stable topological
order.

…pendency cycles

- HttpRunner: refine quota.pattern to a valid regex at schema parse time and
  precompile it once in the constructor, so an invalid pattern fails fast at
  construction instead of throwing inside run()'s try/catch (where it was
  misclassified as a transport failure).
- session.dependencyOrder: throw a descriptive error naming the cycle instead
  of silently dropping the back-edge (which produced a partial order and
  cascading skips).
Base automatically changed from task-13-role-binding to main June 13, 2026 16:37
@inhaq inhaq merged commit 0db8edc into main Jun 13, 2026
2 checks passed
@inhaq inhaq deleted the m2-runners branch June 13, 2026 16:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant