- What is Lasso?
- Why Lasso exists
- What Lasso does
- How it works
- Slash commands
- Bundled workflows
- Request examples
- Custom workflows
- HarnessSpec reference
- Library API
- Compiler
- Compiler feedback
- Verification engine
- Compiler optimizations
- Harness mutations
- Guardrails
- Failure mode generation
- Risk assessment
- Per-node harnesses
- Trace-based synthesis
- Adaptive runtime
- Lineage persistence
- Harness memory
- Environment model
- Failure ontology
- Capabilities
- Meta-harness
- Multi-harness composition
- How Lasso fits with pi-duroxide
- Non-goals
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)
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:
- Boots pi-duroxide (the durable workflow runtime)
- Registers 5 slash commands (
/lasso:plan,/lasso:run, etc.) - 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 |
# From this repository
pi install .
# Or from npm (once published)
pi install @mhingston5/lasso# 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:inspectimport { 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.
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:
- discovers the execution environment (available tools, auth, constraints)
- synthesizes a workflow graph from intent
- predicts failures before they happen
- mutates the harness to prevent them
- compiles into a replay-safe durable workflow
- repairs the harness at runtime based on observed failures
- 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
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 workflows —
patch-validationandpr-review-merge - Slash commands —
/lasso:plan,/lasso:replan,/lasso:compile,/lasso:run,/lasso:inspect - A library API — for programmatic use from TypeScript
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
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
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
| 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 |
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.
Deterministic, draft-only. Accepts the original request plus an
observedOutcome and returns one of:
- a revised draft request
needs_operator_input— human must provide new factsstop— auto-retrying would be wrong
/lasso:compile and /lasso:run accept four input forms:
- Bundled workflow request JSON —
{ "workflow": "patch-validation", "input": {...} } - Raw
HarnessSpecJSON — the full spec - Envelope with spec or specPath —
{ "spec": {...}, "input": {...} }or{ "specPath": "/path/to/spec.json" } - Direct path —
/tmp/custom-spec.json
Both operate entirely against a local repository or worktree.
Validates a candidate fix against a known-bad baseline:
- Check out
baselineRefand runreproduceCommandsto confirm the bug - Apply the candidate from
candidateSource - Re-run
reproduceCommands— expect them to pass - Run
verificationCommandsas a broader regression check - Optionally route to human approval
Terminal outcomes: validated-fix, not-reproduced, apply-failed, candidate-failed, rejected
Local rehearsal of a review-and-merge flow:
- Inspect the repo
- Run verification commands
- Generate an LLM review summary
- Route through human approval
- Perform local merge
- Re-run verification after merge
{
"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
}
}{
"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)\""]
}
}{
"workflow": "patch-validation",
"originalRequest": {
"workflow": "patch-validation",
"input": { "..." : "..." }
},
"observedOutcome": {
"terminalNodeId": "validated-fix",
"notes": ["prod hotfix"]
}
}For aborted attempts: { "aborted": true, "abortReason": "retry-exhaustion" }
{
"name": "custom-echo",
"graph": {
"entryNodeId": "echo",
"nodes": [
{ "id": "echo", "kind": "tool", "tool": "bash", "args": ["-lc", "echo hello"] }
],
"edges": []
}
}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 workflowArbitrary workflow families are supported. The planner accepts custom families
via skill markdown with an explicit workflow name.
Canonical sources: src/spec/types.ts, src/spec/schema.ts, src/spec/validate.ts
{
"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.
| 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 |
- Node IDs must be unique
entryNodeIdmust exist- Every edge
from/tomust reference an existing node condition.thenNodeIdandcondition.elseNodeIdmust existmerge.waitFormust not be emptyhumannodes withinteractionType: "choice"must haveoptions- Unreachable nodes are rejected
retryPolicyonly ontool,llm,subworkflow- Verification rules cannot reference missing nodes
- Circular verification dependencies are rejected
import { compileHarnessSpec, type CompiledHarnessWorkflow } from "lasso";
const compiled = compileHarnessSpec(spec);
// compiled.name, compiled.spec, compiled.cir, compiled.optimizations
// compiled.register(pi) — registers with pi-duroxidePipeline: validate → lower → optimize → validate CIR → build generator.
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 |
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.
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.
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 |
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.
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.
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 + durationThree passes between lowering and CIR validation:
- Dead-node elimination — removes unreachable nodes
- Single-branch merge elision — simplifies single-branch merges
- Tool-node fusion — merges adjacent
bash/shnodes
import { optimizeCirWorkflow } from "lasso/cir/optimize";
const { optimized, passes } = optimizeCirWorkflow(cir);
// passes — ["dead-node-elimination", "merge-elision", "tool-fusion"]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
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.
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 });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"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 steps7 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);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);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// 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.
Lasso is distributed as a pi extension (package.json has a "pi" field
pointing to ./src/index.ts). When you pi install it:
- pi loads
src/index.ts, which exports a default extension function - That function (
src/pi/extension.ts) first boots pi-duroxide - 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.
Lasso does not currently aim to provide:
- live GitHub or
ghintegration - autonomous code authoring or patch generation
- LLM-backed planning or replanning (all planning is deterministic)
- automatic compile/run behavior from
/lasso:planor/lasso:replan - arbitrary generated TypeScript
