Skip to content

mhingston/lasso

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

43 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Lasso

Agent Wrangling

Table of contents


Lasso goes from intent to executable workflow — and repairs the harness while it runs.

Intent
  → Environment discovery (tools, resources, constraints)
  → Memory query (past patterns, what worked/failed)
  → Graph synthesis (planner + capabilities)
  → Failure prediction (auth, tool, network, resource)
  → Risk assessment (probability × impact, threshold filtering)
  → Policy synthesis (mutations: add verification, retry, approval)
  → Compilation (validate → lower → optimize → execute)
  → Per-node harnesses (guardrails, verification hooks)
  → Runtime adaptation (trace → synthesize → continueAsNew)

What is Lasso?

Lasso is a runtime harness synthesizer built on pi-duroxide. It synthesizes deterministic scaffolding around non-deterministic parts — predicting failures, assessing risks, and generating per-node guardrails before execution. It's a TypeScript package that plugs into pi via the pi field in package.json. When installed, it:

  1. Boots pi-duroxide (the durable workflow runtime)
  2. Registers 5 slash commands (/lasso:plan, /lasso:run, etc.)
  3. Exports a library API for programmatic use

There are two ways to use it:

Mode How When
Chat mode Slash commands inside pi's coding agent UI Interactive workflow planning and execution
Library mode import { compileHarnessSpec } from "lasso" Building custom tooling, CI pipelines, or other extensions

Quick start

Install

# From this repository
pi install .

# Or from npm (once published)
pi install @mhingston5/lasso

Chat mode (inside pi)

# 1. Plan a workflow from a freeform brief
/lasso:plan Validate that the bug fix in fix.patch works against main

# 2. Run it (paste the JSON output from step 1)
/lasso:run {"workflow":"patch-validation","input":{...}}

# 3. Inspect what happened
/lasso:inspect

Library mode (TypeScript)

import { compileHarnessSpec, mutateHarness, classifyFailure } from "lasso";

const compiled = compileHarnessSpec(spec);
const signature = classifyFailure(error, { nodeId: "deploy" });

Safety: Lasso checks out refs, applies patches, and merges branches in the target repo. Use a throwaway clone or disposable worktree, not your primary checkout.


Why Lasso exists

pi-duroxide gives you a durable workflow runtime. That is the right layer when you already know what workflow you want to run.

Lasso sits one level higher. It:

  1. discovers the execution environment (available tools, auth, constraints)
  2. synthesizes a workflow graph from intent
  3. predicts failures before they happen
  4. mutates the harness to prevent them
  5. compiles into a replay-safe durable workflow
  6. repairs the harness at runtime based on observed failures
  7. remembers what worked across sessions

Use Lasso when you want workflow automation that is:

  • more reusable than an ad hoc prompt
  • more inspectable than hidden agent logic
  • safer to validate before execution
  • adaptive — it repairs itself when things go wrong
  • aware of its environment — it knows what tools are available

What Lasso does

Lasso takes a declarative HarnessSpec, validates it, lowers it to CIR, optimizes it, and compiles it into a replay-safe workflow that runs on pi-duroxide.

Out of the box, it ships with:

  • Two bundled workflowspatch-validation and pr-review-merge
  • Slash commands/lasso:plan, /lasso:replan, /lasso:compile, /lasso:run, /lasso:inspect
  • A library API — for programmatic use from TypeScript

How it works

The generation pipeline

Intent (brief or skill markdown)
  ↓
parsePromptOrSkill() → IntentIR
  ↓
buildTaskGraph() → TaskGraph
  ↓
analyzeRisks() → RiskModel
  ↓
generateFailureModes() → FailureMode[] + Risk[]
  ↓
assessRisks() → RiskAssessment (overallScore, threshold filtering)
  ↓
synthesizePolicy() → PolicyBundle
  ↓
synthesizeHarness() → HarnessSpec (with per-node guardrails & verification hooks)
  ↓
compileHarnessSpec() → CompiledWorkflow → pi-duroxide

The adaptation loop

Workflow executes
  ↓
Execution trace captured (timestamps, I/O snapshots, failures)
  ↓
synthesizeFromTrace(trace, currentSpec, env) → HarnessSynthesisResult
  → classifies repeated failures, slow nodes, cost spikes
  → derives mutations
  ↓
mutateHarness(spec, mutations) → new spec
  ↓
prepareRuntimeReplan() → continue_as_new / needs_operator_input / stop
  ↓
New version with repaired harness

The feedback loop

compileHarnessSpec()
  ↓
analyzeCompiledWorkflow()
  → CostEstimate (LLM calls, duration, USD)
  → RiskAssessment (cost, failure, quality, complexity)
  → HarnessMutation[] (executable, with triggers)
  ↓
mutateHarness(spec, mutations)
  → replace expensive models
  → add retry policies
  → add verification hooks
  ↓
Recompile with improvements

Slash commands

Command Use it when What it does
/lasso:plan <brief> You have an English brief and want a draft request Returns a draft JSON request or a clarification result
/lasso:replan <JSON> You have a previous request plus a real outcome Returns a revised draft, needs_operator_input, or stop
/lasso:compile <input> You want to inspect what Lasso will register Compiles and stores the artifact in memory
/lasso:run <input> You want to execute a workflow locally Compiles, registers, and starts the workflow
/lasso:inspect [name] You want to see compiled spec, CIR, and runtime state Shows the latest or named compiled workflow

/lasso:plan

Deterministic, draft-only. Classifies a brief into patch-validation, pr-review-merge, or custom and returns a draft JSON envelope you can pass to /lasso:compile or /lasso:run. Does not compile, register, or run anything.

For the two bundled families, it extracts structured fields with strict validation. For custom families (via skill markdown with an explicit workflow name), it builds a sequential graph from parsed steps.

/lasso:replan

Deterministic, draft-only. Accepts the original request plus an observedOutcome and returns one of:

  1. a revised draft request
  2. needs_operator_input — human must provide new facts
  3. stop — auto-retrying would be wrong

Custom compile/run input shapes

/lasso:compile and /lasso:run accept four input forms:

  1. Bundled workflow request JSON{ "workflow": "patch-validation", "input": {...} }
  2. Raw HarnessSpec JSON — the full spec
  3. Envelope with spec or specPath{ "spec": {...}, "input": {...} } or { "specPath": "/path/to/spec.json" }
  4. Direct path/tmp/custom-spec.json

Bundled workflows

Both operate entirely against a local repository or worktree.

patch-validation

Validates a candidate fix against a known-bad baseline:

  1. Check out baselineRef and run reproduceCommands to confirm the bug
  2. Apply the candidate from candidateSource
  3. Re-run reproduceCommands — expect them to pass
  4. Run verificationCommands as a broader regression check
  5. Optionally route to human approval

Terminal outcomes: validated-fix, not-reproduced, apply-failed, candidate-failed, rejected

pr-review-merge

Local rehearsal of a review-and-merge flow:

  1. Inspect the repo
  2. Run verification commands
  3. Generate an LLM review summary
  4. Route through human approval
  5. Perform local merge
  6. Re-run verification after merge

Request examples

patch-validation

{
  "workflow": "patch-validation",
  "input": {
    "repoPath": "/absolute/path/to/disposable-worktree",
    "baselineRef": "main",
    "candidateSource": { "kind": "patchFile", "value": "/path/to/fix.patch" },
    "reproduceCommands": ["npm test -- --grep 'the broken test'"],
    "verificationCommands": ["npm test"],
    "reviewInstructions": "Approve if the patch applies cleanly and verification passes.",
    "approvalRequired": false
  }
}

pr-review-merge

{
  "workflow": "pr-review-merge",
  "input": {
    "repoPath": "/absolute/path/to/disposable-worktree",
    "sourceBranch": "feature/pr-change",
    "targetBranch": "main",
    "reviewInstructions": "Approve only if verification passes and the diff looks safe.",
    "verificationCommands": ["node -e \"process.exit(0)\""]
  }
}

replan

{
  "workflow": "patch-validation",
  "originalRequest": {
    "workflow": "patch-validation",
    "input": { "..." : "..." }
  },
  "observedOutcome": {
    "terminalNodeId": "validated-fix",
    "notes": ["prod hotfix"]
  }
}

For aborted attempts: { "aborted": true, "abortReason": "retry-exhaustion" }

Custom HarnessSpec compile

{
  "name": "custom-echo",
  "graph": {
    "entryNodeId": "echo",
    "nodes": [
      { "id": "echo", "kind": "tool", "tool": "bash", "args": ["-lc", "echo hello"] }
    ],
    "edges": []
  }
}

Custom workflows

Use /lasso:compile and /lasso:run with any HarnessSpec, or use Lasso as a library:

import { validateHarnessSpec, lowerHarnessSpecToCir, compileHarnessSpec } from "lasso";

validateHarnessSpec(spec);       // structural validation
lowerHarnessSpecToCir(spec);     // inspect lowered IR
compileHarnessSpec(spec);        // produce replay-safe workflow

Arbitrary workflow families are supported. The planner accepts custom families via skill markdown with an explicit workflow name.


HarnessSpec reference

Canonical sources: src/spec/types.ts, src/spec/schema.ts, src/spec/validate.ts

Top-level shape

{
  "name": "workflow-name",
  "graph": { "entryNodeId": "start", "nodes": [], "edges": [] },
  "executionPolicy": {},
  "humanPolicy": {},
  "observabilityPolicy": {}
}
Field Required Type Notes
name Yes string Unique workflow name
graph Yes object Contains entryNodeId, nodes, edges
executionPolicy No object Global execution settings
humanPolicy No object Human interaction defaults
observabilityPolicy No object Trace / metrics / logging

All top-level objects are strict. Unknown fields are rejected.

Node kinds

Kind Key fields Maps to
tool tool, args, env, cwd ctx.pi.tool()
llm provider, model, prompt, system ctx.pi.llm()
human prompt, interactionType, options ctx.waitForEvent()
condition condition, thenNodeId, elseNodeId Branch evaluation
merge waitFor, strategy Fork-join synchronization
subworkflow specRef, inputs ctx.scheduleSubOrchestration()

Per-node fields (available on all node kinds via BaseNode):

Field Type Notes
guardrails NodeGuardrails Per-node limits (timeout, retries, cost, constraints)
verificationHooks VerificationHook[] Inline checks that run after this node completes

Validation rules

  1. Node IDs must be unique
  2. entryNodeId must exist
  3. Every edge from/to must reference an existing node
  4. condition.thenNodeId and condition.elseNodeId must exist
  5. merge.waitFor must not be empty
  6. human nodes with interactionType: "choice" must have options
  7. Unreachable nodes are rejected
  8. retryPolicy only on tool, llm, subworkflow
  9. Verification rules cannot reference missing nodes
  10. Circular verification dependencies are rejected

Library API

Compiler

import { compileHarnessSpec, type CompiledHarnessWorkflow } from "lasso";

const compiled = compileHarnessSpec(spec);
// compiled.name, compiled.spec, compiled.cir, compiled.optimizations
// compiled.register(pi) — registers with pi-duroxide

Pipeline: validate → lower → optimize → validate CIR → build generator.

Compiler feedback

Analyzes compiled workflows and emits executable mutations (not just advisory suggestions):

import { analyzeCompiledWorkflow, mutateHarness } from "lasso";

const analysis = analyzeCompiledWorkflow(compiled);
// analysis.cost — LLM calls, duration, USD estimate
// analysis.risk — cost, failure, quality, complexity
// analysis.mutations — executable HarnessMutation[] with triggers

// Apply mutations directly
const { spec: improvedSpec } = mutateHarness(spec, analysis.mutations);

Each mutation carries a trigger (why it was emitted) and description (human-readable reason):

Trigger Mutation Effect
cost_high replace-node Swap expensive model for cheaper one
retry_exhausted modify-node Add retry policy with exponential backoff
verification_failed add-verification Add verification hook
loop_detected modify-node Flag adjacent nodes for merge

Guardrails

Enforce execution limits at runtime. The compiler stops execution when limits are exceeded, throwing a GuardrailExceededError with a descriptive message.

{
  "name": "limited-workflow",
  "executionPolicy": {
    "maxSteps": 25,
    "costLimitUsd": 0.25,
    "timeout": 300000
  },
  "graph": { "..." : "..." }
}
Field Type Enforcement
maxSteps number (positive integer) Stops after N node executions
costLimitUsd number (positive) Stops when estimated LLM cost exceeds limit
timeout number (ms) Stops after wall-clock time

Step count resets on continueAsNew (adaptive evolution). Cost accumulates across versions.

Failure mode generation

Before execution, Lasso generates plausible failure modes from the task description and environment. This answers "Where am I likely to fail?" before acting.

import { generateFailureModes } from "lasso";

const generation = generateFailureModes("Deploy my app to staging", env);
// generation.failureModes — array of FailureMode
// generation.riskSummary — "HIGH RISK: auth failures likely (env constraint detected)"
Task keyword Generated failure modes
deploy auth expiry, network timeout, config drift
test flaky tests, timeout, environment mismatch
build dependency failure, disk full, OOM
merge conflict, verification failure
database connection timeout, migration failure
api rate limit, auth expiry, schema mismatch
file permission denied, disk full, path not found

Failure modes are cross-referenced with environment constraints: if auth constraint detected, auth failure probability is boosted. Each mode includes triggers, mitigations, and recovery actions.

generateFailureModes() now returns risks: Risk[] alongside failureModes, converting each failure mode into a quantified risk with probability, impact, and score.

Risk assessment

First-class Risk type with quantitative scoring. Each risk carries probability (0-1), impact (0-1), and a composite score. assessRisks() filters by threshold and returns a structured assessment.

import { generateFailureModes, assessRisks } from "lasso";

const generation = generateFailureModes("Deploy my app to staging", env);
// generation.risks — Risk[] converted from failure modes

const assessment = assessRisks(generation.risks);
// assessment.overallScore — average risk score (0-1)
// assessment.risksAboveThreshold — risks scoring >= highRiskThreshold (default 0.7)
// assessment.highRiskThreshold — the threshold used

// Custom threshold
const strict = assessRisks(generation.risks, { highRiskThreshold: 0.5 });

Risk interface:

Field Type Description
id string Unique risk identifier
probability number (0-1) Likelihood of occurrence
impact number (0-1) Severity if it occurs
score number probability × impact
signals string[] Triggers or indicators
mitigations HarnessMutation[] Suggested mitigations as executable mutations
failureClass FailureClass Classification (auth, tool, network, etc.)
description string Human-readable description

Per-node harnesses

Every node in a HarnessSpec can carry its own guardrails and verification hooks. These override global settings and run only during that node's execution.

{
  "id": "deploy",
  "kind": "tool",
  "tool": "bash",
  "args": ["./deploy.sh"],
  "guardrails": {
    "timeoutSeconds": 120,
    "maxRetries": 2,
    "maxCostUsd": 0.10,
    "constraints": ["exit_code == 0"]
  },
  "verificationHooks": [
    {
      "name": "health-check",
      "kind": "tool",
      "check": "curl -sf http://localhost:3000/health",
      "onFail": "block",
      "maxAttempts": 3
    }
  ]
}

NodeGuardrails:

Field Type Description
timeoutSeconds number Max execution time for this node
maxRetries number Max retries (overrides global retryPolicy)
maxCostUsd number Max LLM cost for this node
constraints string[] Custom expressions that must hold true

VerificationHook:

Field Type Description
name string Hook identifier
kind "tool" | "llm" | "expression" Type of check
check string Tool name, LLM prompt, or expression
onFail "block" | "warn" | "retry" Action on failure
maxAttempts number Max verification attempts (optional)

Per-node guardrails override global executionPolicy settings. Verification hooks run inline after the node completes, with retry/block/warn semantics.

Trace-based synthesis

synthesizeFromTrace() analyzes an execution trace mid-flight, classifies failures, and derives mutations — wired into the compiler's adaptation loop.

import { DefaultMetaHarness } from "lasso";

const meta = new DefaultMetaHarness(config);

const trace = {
  completedNodes: [
    { nodeId: "build", startedAt: 1, completedAt: 2, costUsd: 0.05 },
  ],
  failedNodes: [
    { nodeId: "deploy", startedAt: 2, failedAt: 3, error: "auth expired", failureClass: "auth", retryCount: 3 },
  ],
  totalCostUsd: 0.15,
  capturedAt: Date.now(),
};

const result = await meta.synthesizeFromTrace(trace, currentSpec, environment);
// result.mutations — HarnessMutation[] derived from trace analysis
// result.spec — mutated HarnessSpec
// result.rationale — human-readable explanation of changes
// result.decision — "continue" | "needs_operator_input" | "stop"

The synthesis classifies:

  • Repeated failures — same node failing across retries → add verification or block
  • Slow nodes — duration spikes → tighten timeout guardrails
  • Cost spikes — LLM cost above expected → swap to cheaper model

This feeds directly into the continueAsNew path, producing a new harness version with repairs applied.

Verification engine

Standalone module with compositional strategies:

import { runVerification } from "lasso/verification/engine";

// Strategies: "all-must-pass" (default), "first-pass", "any-block"
const report = yield* runVerification(nodeId, hooks, nodeMap, state, ctx, "first-pass");
// report.overallStatus — "pass" | "warn" | "block"
// report.hookResults — per-hook outcome + duration

Compiler optimizations

Three passes between lowering and CIR validation:

  1. Dead-node elimination — removes unreachable nodes
  2. Single-branch merge elision — simplifies single-branch merges
  3. Tool-node fusion — merges adjacent bash/sh nodes
import { optimizeCirWorkflow } from "lasso/cir/optimize";

const { optimized, passes } = optimizeCirWorkflow(cir);
// passes — ["dead-node-elimination", "merge-elision", "tool-fusion"]

Harness mutations

Structural spec modifications from execution traces or compiler feedback:

import { deriveMutationsFromTrace, deriveMutationsFromFailure, mutateHarness } from "lasso";

// From execution trace
const mutations = deriveMutationsFromTrace(trace, spec);

// From classified failure
const mutations = deriveMutationsFromFailure(signature, spec, { nodeId: "deploy" });

// Apply
const { spec: newSpec, diff } = mutateHarness(spec, mutations);

Mutation types: add-node, remove-node, modify-node, add-edge, toggle-approval, add-verification, replace-node, tighten-guardrail

Triggers: node_failed, confidence_low, cost_high, loop_detected, retry_exhausted, verification_failed, tool_missing, auth_expired

Adaptive runtime

Reference workflows get automatic version evolution:

import { prepareRuntimeReplan, MAX_ADAPTIVE_VERSIONS } from "lasso";

const decision = await prepareRuntimeReplan(metadata, input, result);
// decision.type — "continue_as_new" | "needs_operator_input" | "stop"

Capped at 5 versions (MAX_ADAPTIVE_VERSIONS). Each version records a HarnessVersion with full lineage.

Lineage persistence

import { FileLineageStore } from "lasso";

const store = new FileLineageStore("/path/to/store");
await store.saveVersion(version);
await store.saveLineage(entry);

const chain = await store.getLineageChain(3);
const recent = await store.queryLineage({ terminalNodeId: "validated-fix", limit: 10 });

Harness memory

Tracks patterns across sessions:

import { FileMemoryStore, adviseFromMemory } from "lasso";

const store = new FileMemoryStore("/path/to/memory");
const advice = await adviseFromMemory("deploy-staging", store);
// advice.suggestions — "Previously, auth-check-before-deploy improved success rate"
// advice.warnings — "Pattern deploy-without-auth failed 6 times"

Environment model

Discovers execution environment before generating a harness:

import { discoverEnvironment, analyzeEnvironment } from "lasso";

const env = await discoverEnvironment("/path/to/repo");
// env.tools — bash, git, node, etc.
// env.constraints — auth, network, rate-limit
// env.repoState — branch, uncommitted changes, remotes

const analysis = analyzeEnvironment(env, ["git", "node"]);
// analysis.readinessScore — 0-100
// analysis.preparatorySteps — actionable prep steps

Failure ontology

7 failure classes with evidence and recovery:

import { classifyFailure, suggestRecovery } from "lasso";

const signature = classifyFailure(error, { nodeId: "deploy" });
// signature.class — "auth" | "tool" | "resource" | "semantic" | "human" | "environment-drift" | "network" | "unknown"
// signature.confidence — 0-1
// signature.suggestedRecovery — actionable steps

const recovery = suggestRecovery(signature);

Capabilities

Dynamic graph generation from required tools:

import { DefaultCapabilityRegistry, planWorkflowRequest } from "lasso";

const registry = new DefaultCapabilityRegistry();
// Pre-registered: bash, git, node, llm-review, human-approval

const result = planWorkflowRequest(brief, registry);

Meta-harness

Full generation pipeline — discover, predict, synthesize, compile:

import { DefaultMetaHarness, DefaultCapabilityRegistry, FileMemoryStore } from "lasso";

const meta = new DefaultMetaHarness({
  capabilityRegistry: new DefaultCapabilityRegistry(),
  memoryStore: new FileMemoryStore("/path/to/memory"),
});

const result = await meta.generateHarness("Deploy my app to staging");
// result.spec — generated HarnessSpec
// result.environmentAnalysis — tool/resource availability
// result.predictedFailures — anticipated failures with confidence
// result.compilerAnalysis — cost, risk, mutations
// result.readinessScore — 0-100
// result.appliedMutations — what was changed

Multi-harness composition

// Sequential chain
const chained = meta.composeHarnesses([
  { name: "research", spec: researchSpec },
  { name: "plan", spec: planSpec },
  { name: "execute", spec: executeSpec },
]);

// Parallel execution
const parallel = meta.composeParallel([verificationSpec, notificationSpec]);

// Conditional branching
const conditional = meta.composeConditional("isProduction", prodSpec, stagingSpec);

Node IDs are prefixed with stage names to avoid collisions.


How Lasso fits with pi-duroxide

Lasso is distributed as a pi extension (package.json has a "pi" field pointing to ./src/index.ts). When you pi install it:

  1. pi loads src/index.ts, which exports a default extension function
  2. That function (src/pi/extension.ts) first boots pi-duroxide
  3. Then it registers the 5 slash commands with pi's ExtensionAPI

The layering:

  • pi-duroxide owns workflow lifecycle, replay, timers, events, and runtime registration
  • Lasso owns spec validation, CIR lowering, optimization, compilation, and operator-facing commands

In other words: pi-duroxide is the durable runtime engine; Lasso is the harness generation, optimization, and adaptation layer built on top of it.

Non-goals

Lasso does not currently aim to provide:

  • live GitHub or gh integration
  • autonomous code authoring or patch generation
  • LLM-backed planning or replanning (all planning is deterministic)
  • automatic compile/run behavior from /lasso:plan or /lasso:replan
  • arbitrary generated TypeScript

About

Lasso is a runtime harness synthesizer built on pi-duroxide

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors