
Theory Of Constraints
- 68 installs
- 191 repo stars
- Updated July 24, 2026
- pproenca/dot-skills
theory-of-constraints is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
Key points
- theory-of-constraints
- AI & Agent Building
- AI-coding skill
Theory Of Constraints by the numbers
- 68 all-time installs (skills.sh)
- +6 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #5,858 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/pproenca/dot-skills --skill theory-of-constraintsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 68 |
|---|---|
| repo stars | ★ 191 |
| Last updated | July 24, 2026 |
| Repository | pproenca/dot-skills ↗ |
How do I helps with ai & agent building tasks during ai-assisted development?
Helps with ai & agent building tasks during AI-assisted development.
Who is it for?
Best when you're working on ai & agent building and need structured help with theory-of-constraints.
Skip if: Teams with no ai & agent building needs, or anyone wanting a generic chat assistant without this specific workflow.
When should I use this skill?
When you need to helps with ai & agent building tasks during ai-assisted development, or when theory-of-constraints is a claude code skill for ai & agent building. it helps solo builders move faster with ai-assisted coding.
What you get
Structured output aligned to theory-of-constraints: theory-of-constraints; AI & Agent Building; AI-coding skill.
Files
Theory of Constraints
A diagnostic runbook for optimizing any process — a CI/build pipeline, a dev value stream, an Agent Skill or plugin, or a runtime code path — through Eliyahu Goldratt's Theory of Constraints (ToC).
The one idea that makes ToC powerful: every system has exactly one binding constraint at a time, and global throughput rises only when you improve that constraint. Effort spent anywhere else produces nothing — "an hour saved at a non-bottleneck is a mirage." This runbook finds the constraint with measurement, then walks the Five Focusing Steps to relieve it without creating new problems.
When to Apply
Use this skill when the user wants to optimize a process or workflow and any of these are true:
- They want to optimize but don't know where to start ("make this faster", "why is this slow", "reduce the cost of this").
- A local speedup didn't move the end-to-end result (you optimized the wrong thing).
- Work, queues, or WIP pile up at one stage while others sit idle.
- Everything looks 100% busy but little ships (high utilization, low throughput).
- They added capacity, parallelism, or resources and it didn't help.
- Fixing one bottleneck just surfaces another.
- The real limiter is a policy or rule, not a resource (a mandatory full test suite, serialized review, a "read every reference" instruction, batch-everything releases).
- They're stuck in a tradeoff that blocks the obvious fix ("faster vs safer", "smaller context vs more coverage").
Do not use this skill for:
- A specific, already-localized hotspot where the constraint is known — go straight to the fix (e.g. an O(n²) loop →
complexity-optimizer). - Pure correctness/quality review with no throughput goal — use a code-review skill.
- Micro-optimizing a stage you have not yet proven is the constraint. That is the cardinal ToC error; this skill exists to stop it.
The Process of Ongoing Improvement (POOGI)
ToC is a loop, not a one-shot. Always run it in order — exploiting before elevating is what separates ToC from "just throw resources at it."
┌─────────────────────────────────────────────────┐
│ │
▼ │
1. IDENTIFY the constraint ── the one stage that gates global throughput
│
▼
2. EXPLOIT it ── get the most from it with NO new spend
│ (remove waste/idle on the constraint)
▼
3. SUBORDINATE everything ── pace all non-constraints to the constraint;
else to the constraint let them idle rather than build WIP
│
▼
4. ELEVATE the constraint ── only now add capacity / invest / parallelize
│
▼
5. REPEAT ── the constraint has moved; go to step 1.
│ Do NOT let inertia (old policies) be the
└───────────────────────new constraint.The most common mistakes map directly to skipped steps: jumping to step 4 (elevate) before step 2 (exploit), or never reaching step 1 (optimizing whatever is most visible instead of what is binding).
Common Symptoms
Start here. Match the symptom, open its decision tree, run the quick check.
| Symptom | Usual constraint | Quick check | Tree |
|---|---|---|---|
| "Optimize this, don't know where" | Unknown — measure first | queries/measure-stage-times.sh | find-the-constraint |
| Local speedup, no global gain | You optimized a non-constraint | queries/throughput-accounting.py | local-optimum |
| Queue/WIP piling up at a stage | Constraint is at/just downstream of the pile | queries/measure-wip.sh | wip-accumulation |
| Busy everywhere, little ships | Non-constraints over-activated | queries/utilization-vs-throughput.py | utilization-trap |
| Added capacity, no improvement | Elevated the wrong thing / skipped exploit | queries/measure-stage-times.sh | elevation-misfire |
| Fix one bottleneck, another appears | Constraint moved (expected) | queries/measure-stage-times.sh | moving-constraint |
| A rule/process is the limiter | Policy constraint | queries/five-focusing-steps.sh | policy-constraint |
| Stuck in a tradeoff | A surfaced dilemma blocks the fix | (reasoning — Evaporating Cloud) | conflict-resolution |
For Agent Skills / plugins specifically, the constraint is most often the always-loaded context budget (a bloated SKILL.md) or a weak description (the skill never triggers, so its throughput is zero). Run queries/skill-context-cost.sh <skill-dir>.
How to Use
1. Identify the symptom in the table above and open its tree in references/. 2. Define the goal and throughput metric first. ToC is meaningless without a global metric to improve (PRs merged/week, requests/sec, task completions per agent session, build wall-clock). The trees assume you have one — set it via config.json or ask the user. 3. Run the quick-check query the tree points to. Queries live in references/queries/; each has a header explaining parameters and how to read the output. 4. Follow the tree to a terminal action. Every path ends in a concrete ToC move (exploit / subordinate / elevate / change-the-policy / repeat). 5. Record the finding using assets/templates/report.md. Append a one-line entry to the investigation log so recurring constraints become visible over time.
Read references/symptoms.md for the full catalog with entry points and severity.
Setup
This skill uses config.json to know what "throughput" means for the system under study and how to measure each stage. On first use, if goal_metric or value_stream_stages are empty, ask the user (via AskUserQuestion) for:
- the goal metric (the global throughput to maximize), and
- the ordered stages of the workflow being optimized.
Then save them to config.json. If config cannot be filled, the skill still works — fall back to asking the user inline and measuring stages manually. Never block on missing config.
Gotchas
The classic ToC traps (measuring local efficiency, elevating before exploiting, optimizing the most-visible stage) are captured in gotchas.md. Read it before your first investigation — these errors are the whole reason ToC exists.
Related Skills
complexity-optimizer— once ToC identifies a code stage as the constraint, use this to find and fix the algorithmic hotspot inside it.dx-harness— when the constraint is a developer-experience chore (slow bootstrap, manual steps), this audits and fixes the harness.dev-skill:evolve— when the constraint is an Agent Skill itself (bloated context, weak triggering), this improves it.
Constraint Analysis Report: {system / workflow name}
Date: {YYYY-MM-DD} Investigator: {agent/user} System under study: {CI pipeline | dev value stream | Agent Skill | runtime path} Goal metric (global throughput): {e.g. PRs merged/week, requests/sec, builds/hour, task completions/session} Severity: {P1 effort being wasted | P2 a step skipped | P3 meta-process}
Summary
{1–2 sentences: what the constraint was, which focusing step applied, and the measured effect on the goal metric.}
System map
{The ordered stages, with the constraint marked. Example:}
install → build → [TEST ← constraint] → deployConstraint identified
| Field | Value |
|---|---|
| Constraint | {stage / resource / policy} |
| Type | {physical resource |
| Evidence | {slowest stage X% of total |
| Quick check used | {queries/measure-stage-times.sh \ |
| Entry tree | {find-the-constraint |
Timeline
- {HH:MM} — Goal metric defined: {metric} = {baseline value}
- {HH:MM} — Measured stages: {tool}, found {result}
- {HH:MM} — Constraint confirmed: {what} ({evidence})
- {HH:MM} — Focusing step applied: {EXPLOIT / SUBORDINATE / ELEVATE / change policy}
- {HH:MM} — Re-measured: {metric} = {new value}
Focusing step applied
{Which of the Five Focusing Steps, and exactly what was done.}
- [ ] 1. IDENTIFY — constraint confirmed with evidence (not guessed)
- [ ] 2. EXPLOIT — removed idle/waste/rework on the constraint (no new spend)
- [ ] 3. SUBORDINATE — paced non-constraints to the constraint; capped WIP
- [ ] 4. ELEVATE — added capacity (only after exploit + subordinate)
- [ ] 5. REPEAT / inertia check — removed any policy tuned to the old constraint
Throughput accounting (before / after)
| Measure | Before | After | Delta | Verdict |
|---|---|---|---|---|
| T (throughput ↑) | ||||
| I (inventory ↓) | ||||
| OE (operating expense ↓) | {THROUGHPUT IMPROVED / LOCAL OPTIMUM / WIP INFLATED / REGRESSION} |
Run queries/throughput-accounting.py --before T I OE --after T I OE to fill this in and get the verdict. A T-flat / OE-down result is a LOCAL OPTIMUM — do not claim success.Necessary conditions verified (only if a policy was relaxed)
{If you shrank a test gate, removed an approval, or changed a metric: confirm the protected outcome still holds — escaped-defect rate, incident rate, output correctness. If it regressed, the policy was real, not stale; restore it.}
Next predicted constraint
{When this constraint is broken, the system limit moves. Where to next? This is POOGI step 5 — name the likely new constraint so the next investigation starts ahead.}
Action items
- [ ] {Intervention to keep / make permanent}
- [ ] {Stale policy to remove (inertia)}
- [ ] {Metric/instrumentation to add so the next constraint is measurable}
- [ ] {Append a one-line entry to the investigation log}
{
"goal_metric": "",
"value_stream_stages": [],
"wip_probes": {},
"stage_time_commands": {},
"investigation_log": "${CLAUDE_PLUGIN_DATA:-$HOME/.claude}/toc-investigations.log",
"_setup_instructions": {
"goal_metric": "The single global throughput metric you are maximizing for this system. ToC is meaningless without one. Examples: 'PRs merged/week', 'requests/sec at p99<300ms', 'builds/hour', 'agent task completions/session', 'tokens spent per completion'. If empty, ask the user before measuring anything.",
"value_stream_stages": "Ordered list of the stages work flows through, first to last. Example: [\"plan\", \"code\", \"review\", \"ci\", \"deploy\"]. The constraint is one of these.",
"wip_probes": "Map of stage name -> shell command that lists items waiting at that stage, one per line (its line count is the WIP). Passed to queries/measure-wip.sh. Example: {\"review\": \"gh pr list --state open --search review:required\", \"ci\": \"gh run list --status queued\"}.",
"stage_time_commands": "Map of stage name -> shell command that performs that stage, for queries/measure-stage-times.sh. The commands ARE EXECUTED. Example: {\"build\": \"npm run build\", \"test\": \"npm test\"}.",
"investigation_log": "Append-only log of investigations (one line each). Defaults under ${CLAUDE_PLUGIN_DATA} so it survives skill upgrades. No secrets are stored here."
},
"_notes": "This skill works without config: if goal_metric or value_stream_stages are empty, ask the user inline (AskUserQuestion) and measure stages manually. Never block on missing config. Secrets are never stored here — reference environment variables from the commands instead."
}
Gotchas
The diagnostic dead-ends and recurring errors of applying Theory of Constraints. The first five are the classic ToC traps — they are the entire reason the method exists. Append new ones (with dates) as investigations surface them.
Optimizing without a defined global metric
The most common failure: jumping to "make it faster" before naming the global throughput metric. Without one, every local speedup looks like progress and local optima get rewarded. Always pin the goal metric first (see find-the-constraint-tree.md precondition); if the user can't state one, that absence is the first thing to fix. Added: 2026-05-21
Optimizing the most-visible stage instead of the binding one
Teams optimize whatever is easiest to see or measure (compile time, a noisy log), not what is binding. An improvement to a non-constraint produces zero global gain — "an hour saved at a non-bottleneck is a mirage." Confirm the constraint with measurement (measure-stage-times.sh + measure-wip.sh) before touching anything. See local-optimum-tree.md. Added: 2026-05-21
Elevating before exploiting
Adding capacity (Step 4) before getting the most from existing capacity (Step 2) buys idle resources: OE rises, T stays flat. Always exhaust exploit + subordinate before spending. A misfired elevation is the strongest signal you skipped a step. See elevation-misfire-tree.md. Added: 2026-05-21
Mistaking high utilization for productivity
"Activating a resource is not the same as utilizing it." Driving a non-constraint to 100% only inflates WIP. A correctly-subordinated non-constraint should idle part of the time. Green dashboards everywhere with low throughput is the trap, not the goal. See utilization-trap-tree.md. Added: 2026-05-21
Leaving stale policies in place after the constraint moves (inertia)
After two or three POOGI cycles the binding constraint is usually a policy that was rational for a former constraint — a full-suite gate from when tests were fast, a buffer sized for an old bottleneck, a "read all references" instruction from when references were few. These are invisible to stage timing. Run the inertia check every cycle. See moving-constraint-tree.md and policy-constraint-tree.md. Added: 2026-05-21
Relaxing a policy without verifying its necessary condition
Policy constraints are high-leverage because changing a rule is free — but rules often protect something real (regressions, incidents, correctness). Shrinking a test gate or removing an approval can raise throughput while quietly raising escaped defects. ToC maximizes throughput subject to necessary conditions still holding. Verify the protected outcome for a few cycles before declaring success; if it regresses, the policy was real, not stale. See policy-constraint-tree.md. Added: 2026-05-21
Reacting to run-to-run noise as if the constraint moved
A constraint that "jumps around" is often measurement variance, not a moving constraint. Average over several runs (measure-stage-times.sh --runs 3+) before declaring a new constraint, especially when stage times are close together (a balanced, flow-gated system). See moving-constraint-tree.md. Added: 2026-05-21
Treating throughput accounting units inconsistently
throughput-accounting.py compares before vs after — the verdict is only meaningful if T, I, and OE use the same units in both readings (e.g. both T in PRs/week, both OE in CI-minutes/run). Mixing units (local stage time before, global metric after) produces a false verdict. Measure the same global metric on both sides. Added: 2026-05-21
{
"version": "0.1.0",
"organization": "dot-skills",
"technology": "process / workflow optimization (Theory of Constraints)",
"discipline": "investigation",
"type": "runbook",
"date": "May 2026",
"abstract": "A diagnostic runbook that applies Eliyahu Goldratt's Theory of Constraints to optimizing processes and workflows — CI/build pipelines, dev value streams, Agent Skills/plugins, and runtime code paths. Maps eight throughput symptoms to decision trees that locate the single binding constraint, then prescribe the Five Focusing Steps (exploit, subordinate, elevate, repeat) while guarding against optimizing non-constraints. Ships six runnable measurement queries (stage timing, WIP, throughput accounting, skill context cost, utilization vs throughput, a POOGI checklist) and a constraint-analysis report template.",
"references": [
"https://www.goldratt.com/",
"https://www.tocico.org/",
"https://en.wikipedia.org/wiki/Theory_of_constraints",
"https://en.wikipedia.org/wiki/The_Goal_(novel)",
"https://en.wikipedia.org/wiki/Drum-buffer-rope",
"https://en.wikipedia.org/wiki/Evaporating_Cloud",
"https://itrevolution.com/product/the-phoenix-project/",
"https://itrevolution.com/product/the-devops-handbook/"
]
}
Decision Tree: Conflict Resolution (the Evaporating Cloud)
Symptom: You know the constraint, but the obvious fix is blocked by a tradeoff that feels unresolvable — "we must move faster but we must stay safe", "shrink the agent's context but keep full coverage", "deploy continuously but protect stability." The team is stuck choosing between two bad options or splitting the difference.
Core principle: ToC's Evaporating Cloud (Conflict Resolution Diagram) holds that a persistent conflict is never a real tradeoff — it survives only because of a hidden, false assumption. You don't compromise the cloud; you evaporate it by breaking one assumption, producing a win-win "injection." Compromise leaves both needs partly unmet; evaporation meets both.
State the conflict as a cloud, then attack the assumptions.
│
├── Step 1 — Build the cloud (5 boxes):
│ A (Objective) ← the shared goal both sides actually want
│ B (Need) ← what side 1 must protect → requires D
│ C (Need) ← what side 2 must protect → requires D'
│ D vs D' (Wants) ← the two conflicting actions
│
│ Read it as logic:
│ "To achieve A we need B; to have B we want D."
│ "To achieve A we need C; to have C we want D'."
│ "But D and D' conflict."
│
├── Step 2 — Surface the assumption behind EACH arrow. Ask "why must this be
│ true?" for all five:
│ A→B : "to reach the goal we really need B because…"
│ A→C : "…we really need C because…"
│ B→D : "to get B we must do D because…" ← most assumptions hide here
│ C→D': "to get C we must do D' because…" ← and here
│ D↔D': "D and D' can't coexist because…" ← and here
│
├── Step 3 — Find the WEAKEST assumption (the one that isn't actually a law of
│ nature) and break it with an INJECTION — a change that makes the assumption
│ false, so both needs are met without choosing.
│ │
│ ├── Found a breakable assumption
│ │ └── State the injection. Verify it doesn't create new negative effects
│ │ (a mini Future Reality Tree: "if we do this, then… does anything
│ │ bad follow?").
│ │ ├── No new negatives → adopt the injection; the conflict evaporates.
│ │ │ Return to the constraint fix you were blocked on →
│ │ │ find-the-constraint-tree.md.
│ │ └── New negative effect appears → it's a different assumption or a
│ │ real necessary condition; pick the next-weakest assumption and
│ │ repeat Step 3.
│ │
│ └── Every assumption seems ironclad (it's a genuine physical tradeoff)
│ └── This is rare. If truly no assumption breaks, it is a real
│ constraint, not a false conflict → make the tradeoff explicit,
│ pick the side that maximizes the GOAL METRIC, and elevate later →
│ find-the-constraint-tree.md.
│
└── Step 4 — Record the cloud, the broken assumption, and the injection so the
same dilemma isn't re-litigated next time → ../assets/templates/report.mdWorked examples (the injection is the payoff)
"Move faster" vs "stay safe" (CI gate):
- A = ship value reliably; B = catch regressions → D = run full suite on every change; C = fast feedback → D' = skip/shorten the suite.
- Weak assumption (B→D): "the only way to catch regressions is to run everything."
- Injection: run the diff-affected tests on every change + the full suite nightly. Both needs met; no compromise.
"Shrink agent context" vs "keep full coverage" (Agent Skill):
- A = correct task completion; B = enough knowledge → D = load all references; C = fit the context budget → D' = drop references.
- Weak assumption (B→D): "the agent needs all references loaded up front to be correct."
- Injection: progressive disclosure — a lean SKILL.md that points to on-demand references. Full coverage, small always-loaded budget.
"Deploy continuously" vs "protect stability" (release policy):
- Weak assumption: "more frequent deploys mean more risk."
- Injection: small, automated, reversible deploys with canary + auto-rollback. Smaller batches are lower risk per deploy, not higher.
When to use this tree vs the others
Reach for the Evaporating Cloud when the constraint is known but a dilemma blocks the intervention — most often after policy-constraint-tree.md (a policy persists because of a "we have to" assumption) or when an exploit/elevate move is resisted as "too risky." For finding the constraint itself, use the measurement-driven trees.
Decision criteria
| Signal | Reading |
|---|---|
| Is there a shared objective (A)? | If the two sides don't share a goal, it's a priorities conflict, not a cloud — escalate to the goal owner |
| Does an assumption break cleanly? | Yes = injection evaporates the conflict; no new negatives = adopt |
| Does breaking it create new problems? | Yes = real necessary condition; try the next assumption |
| All assumptions ironclad? | Genuine tradeoff (rare) — choose by goal metric, make it explicit |
Terminal actions
- Adopt the injection — assumption broken, no new negatives; conflict evaporated → resume the blocked constraint fix.
- Try the next assumption — the injection created a new negative effect.
- Make the tradeoff explicit — genuinely no breakable assumption; decide by the goal metric and revisit after elevating.
- Escalate — no shared objective exists; this is a goal/priority disagreement for the owner, not a ToC cloud.
Record the cloud and chosen injection in ../assets/templates/report.md.
Decision Tree: Elevation Misfire (added capacity, no gain)
Symptom: You added capacity — more workers, parallelism, hardware, a bigger machine, an extra reviewer — and throughput barely changed.
Core principle: Elevation (Step 4) is the last of the Five Focusing Steps for a reason. It is the only step that costs money, and it pays off only when (a) you have already exploited and subordinated, (b) you elevated the actual constraint, and (c) the constraint hasn't already moved. A misfire means one of those three failed.
You elevated something and throughput didn't rise. Which failure?
│
├── Did you EXPLOIT before elevating?
│ │ (Was the constraint already running with zero idle/waste?)
│ │
│ └── No → you bought capacity to do work the existing capacity wasn't
│ fully doing. Revert or pause the spend. Go EXPLOIT first
│ (remove idle, batching, rework, waiting on the constraint) →
│ find-the-constraint-tree.md Step 2. Re-measure before spending again.
│
├── Did you elevate the ACTUAL constraint?
│ │ Run queries/measure-stage-times.sh (and measure-wip.sh) again, AFTER
│ │ the elevation, on the goal metric.
│ │
│ ├── The stage you elevated is still the slowest / still accumulates WIP
│ │ └── You under-elevated: added capacity but it's still the constraint.
│ │ Elevate further (more shards/workers) OR switch to a structural
│ │ exploit (e.g. cache, algorithmic fix → complexity-optimizer).
│ │ Re-measure.
│ │
│ └── A DIFFERENT stage is now slowest / now accumulates WIP
│ └── You elevated a non-constraint (it never was binding), OR the
│ constraint MOVED to the new slowest stage. Either way, the elevation
│ you paid for did nothing for T →
│ │
│ ├── The elevated stage was never the constraint
│ │ └── Local optimum via spending. Roll back the spend if it adds
│ │ ongoing OE. Redirect → find-the-constraint-tree.md.
│ │
│ └── The constraint genuinely moved to a new stage
│ └── Expected and good — the system improved. Continue POOGI on
│ the NEW constraint → moving-constraint-tree.md.
│
└── Confirm with throughput accounting:
run queries/throughput-accounting.py with before/after T, I, OE.
│
├── T rose but you only looked at the local stage → not a misfire; the
│ elevation worked. Re-identify the new constraint →
│ find-the-constraint-tree.md.
│
├── T flat, OE up (you now pay for idle capacity)
│ └── Confirmed misfire. The capacity sits idle because it isn't the
│ constraint or wasn't exploited. Roll back the ongoing cost.
│
└── T flat, I up (more parallelism created more WIP, not more output)
└── You parallelized a non-constraint; it floods the real constraint →
subordinate it and cap WIP → wip-accumulation-tree.md.Common misfires by domain
- CI: added runners, but the bottleneck is a serialized DB-migration test that can't parallelize → exploit (isolate/mock it) before adding hardware.
- Dev value stream: added a reviewer, but reviews wait on flaky CI, not reviewer time → CI was the constraint.
- Runtime: scaled out web workers, but all hit one undersized database → DB is the constraint; workers now just queue on it.
- Agent Skill: added more reference detail "to be thorough", inflating context — that's negative elevation; it enlarged the very constraint (context budget).
Decision criteria (measurable)
| Signal | Reading |
|---|---|
| Post-elevation T delta | Flat = misfire; rose proportionally = worked |
| Which stage is now slowest | Same = under-elevated; different = wrong target or moved |
| OE delta | Up with T flat = paying for idle capacity; roll back |
| I delta | Up with T flat = parallelized a non-constraint; subordinate + cap WIP |
Terminal actions
- Exploit first — you skipped Step 2; recover free capacity before spending.
- Elevate further / structurally — right target, not enough; add more or change the approach.
- Roll back the spend — elevated a non-constraint; stop paying for idle capacity.
- Continue POOGI — constraint moved (a win) → moving-constraint-tree.md.
- Subordinate + cap WIP — parallelism created WIP, not output → wip-accumulation-tree.md.
Record the elevation, its cost, and the measured T delta in ../assets/templates/report.md — elevations that don't hold are the strongest signal of a hidden policy constraint.
Decision Tree: Find the Constraint (master)
Symptom: "Optimize this / make it faster / cheaper" with no obvious target. Start here whenever the constraint is unknown — every other tree assumes you have already identified one.
Precondition — define the goal metric. ToC is meaningless without a global throughput metric. Before measuring anything, pin down what you are maximizing:
- CI/build pipeline → end-to-end wall-clock per run, or runs/hour at capacity.
- Dev value stream → PRs merged/week, or lead time from first commit to deploy.
- Agent Skill/plugin → useful task completions per session, or tokens spent per completion.
- Runtime code path → requests/sec sustained, or p99 latency at target load.
If the user has not given one, ask. Optimizing without a global metric is how local optima get rewarded.
Define goal metric + list the ordered stages
│
├── Can you time each stage? → run queries/measure-stage-times.sh
│ │
│ ├── One stage dominates wall-clock (>40% of total, or ≥2x the next)
│ │ └── That stage is the constraint candidate → CONFIRM with WIP check below
│ │
│ └── No single stage dominates (times are even)
│ └── Throughput is likely gated by flow, not a slow stage →
│ run queries/measure-wip.sh and queries/utilization-vs-throughput.py
│ │
│ ├── WIP accumulates before one stage (queue grows run-over-run)
│ │ └── That stage is the constraint → go to wip-accumulation-tree.md
│ │
│ └── A non-constraint runs >85% busy while a downstream stage starves
│ └── Over-activation → go to utilization-trap-tree.md
│
├── Can't time stages, but work flows through a queue/board →
│ run queries/measure-wip.sh
│ └── The stage with the largest / fastest-growing inbox is the constraint
│ (work waits in front of the slowest resource) → CONFIRM below
│
└── It's an Agent Skill / plugin → run queries/skill-context-cost.sh <skill-dir>
│
├── SKILL.md + always-loaded content > ~500 lines or dwarfs references
│ └── Constraint = always-loaded context budget → EXPLOIT: move detail into
│ on-demand references; the entry point should navigate, not embed
│
└── Description is generic/passive (skill rarely triggers)
└── Constraint = triggering — throughput is ZERO when the skill never fires →
EXPLOIT: rewrite the description (intent-focused, pushy). Defer to
dev-skill:evolve. This beats any internal optimization.
CONFIRM the constraint, then walk the Five Focusing Steps:
│
├── Step 2 — EXPLOIT (no new spend): is the constraint ever idle, blocked, or
│ doing avoidable work? Remove that first. (Examples: constraint waits on a
│ serialized upstream step; runs work that could be cached; re-does rejected work.)
│ └── Re-measure. Did the goal metric improve?
│ ├── Yes, and it's now "good enough" → STOP. Record in report.md.
│ └── Still binding → continue to Step 3.
│
├── Step 3 — SUBORDINATE: pace every non-constraint to the constraint's rate.
│ Non-constraints should idle rather than pile WIP in front of the constraint.
│ (Drum-Buffer-Rope: release work only as fast as the constraint consumes it.)
│ ├── Non-constraints resist idling / stay over-activated →
│ │ go to utilization-trap-tree.md.
│ └── Else (they subordinate cleanly) → continue to Step 4.
│
├── Step 4 — ELEVATE (only after exploit + subordinate): add capacity to the
│ constraint — parallelize it, add a worker, upgrade it, split the batch.
│ └── Re-measure with queries/throughput-accounting.py (verify T rose, not just OE).
│ Go to elevation-misfire-tree.md if it didn't help.
│
└── Step 5 — REPEAT: the constraint has moved. Re-run this tree from the top.
Beware inertia: kill policies built around the OLD constraint →
go to moving-constraint-tree.md.Usual suspects (most frequent constraints, by domain)
1. CI/build: a serialized full test suite or a single-threaded build step gating every run. 2. Dev value stream: code review (work waits longest in the review queue). 3. Agent Skill: the always-loaded context budget, or a non-triggering description. 4. Runtime: an N+1 query or a synchronous external call on the hot path.
Decision criteria (measurable)
| Node | "Constraint" looks like | "Not it" looks like |
|---|---|---|
| Stage timing | One stage ≥40% of total wall-clock or ≥2x next | Even spread, <25% each |
| WIP | Inbox grows run-over-run; items wait here longest | Inbox drains each cycle |
| Utilization | Constraint ~100% busy; downstream starves | All stages <70% — system has spare capacity, look for a policy constraint |
| Skill context | Always-loaded ≫ on-demand; SKILL.md >500 lines | Lean entry point, detail in references |
Terminal actions
- Exploit / Subordinate / Elevate — proceed through the Five Focusing Steps above.
- Repeat — constraint moved; restart at the top.
- Dismiss — all stages <70% utilized and no WIP builds anywhere → the constraint is not a resource. Go to policy-constraint-tree.md.
Record every confirmed constraint and intervention in ../assets/templates/report.md.
Decision Tree: Local Optimum (the mirage of the non-bottleneck)
Symptom: A change measurably sped up one stage, but end-to-end throughput or lead time barely moved. The user is frustrated that "the optimization didn't work."
Core principle: "An hour saved at a non-bottleneck is a mirage." Improving a stage that is not the constraint cannot raise global throughput — the constraint still gates the system. The fix is rarely to optimize harder; it is to redirect effort to the actual constraint.
A change sped up stage X. Did global throughput improve?
│
├── Did you measure the GLOBAL metric before and after?
│ │ (not just stage X's local time — the end-to-end goal metric)
│ │
│ ├── No → run queries/throughput-accounting.py with before/after T, I, OE
│ │ │ to get the real delta. Then re-enter this tree with the numbers.
│ │
│ └── Yes → compare global throughput (T) delta:
│ │
│ ├── T rose ≥ the stage's local improvement, proportionally
│ │ └── X WAS the constraint. Not a local optimum — keep the change.
│ │ Re-identify (constraint has likely moved) →
│ │ find-the-constraint-tree.md.
│ │
│ ├── T flat (<5% change) but OE dropped or stage X is faster
│ │ └── LOCAL OPTIMUM CONFIRMED. X was not the constraint.
│ │ │
│ │ ├── Was the change cheap and side-effect-free (e.g. a cache)?
│ │ │ └── Keep it (harmless), but STOP investing here. Redirect all
│ │ │ further effort → find-the-constraint-tree.md.
│ │ │
│ │ └── Did the change add complexity, risk, or new WIP?
│ │ └── REVERT it. Local optimizations that add inventory or
│ │ complexity make the system worse (more I and OE, same T).
│ │ Then → find-the-constraint-tree.md.
│ │
│ └── T fell after the change
│ └── The change pushed MORE WIP at the real constraint (a faster
│ non-constraint floods the bottleneck). Revert, then apply
│ Drum-Buffer-Rope → wip-accumulation-tree.md.
│
└── "We can't measure global throughput"
└── You cannot do ToC without a global metric — that absence is itself the
first thing to fix. Define the goal metric (see find-the-constraint-tree.md
precondition), instrument it, then return.Why this happens (so you can prevent it)
Local efficiency feels like progress and is easy to measure, so teams optimize whatever they can see. Goldratt's measurement rule: the worth of any local improvement is judged solely by its effect on the global goal. A 50%-faster compile stage does nothing if tests (the constraint) gate every merge.
Decision criteria (measurable)
| Signal | Reading |
|---|---|
| Global T delta | <5% = local optimum; ≈ proportional to stage gain = real |
| OE delta | OE down + T flat = classic mirage (you bought nothing) |
| I delta | I up (more WIP/complexity) + T flat = net negative; revert |
Use queries/throughput-accounting.py — it explicitly flags the OE↓/T-flat pattern as a local optimum.
Terminal actions
- Keep + redirect — change was harmless; stop investing here, go find the real constraint.
- Revert — change added I/OE/complexity without raising T.
- Re-identify — X actually was the constraint; it has moved → find-the-constraint-tree.md.
- Apply DBR — change flooded the constraint → wip-accumulation-tree.md.
Record the false start in ../assets/templates/report.md — repeated local optima in the log signal a measurement-culture problem worth naming.
Decision Tree: Moving Constraint (and the inertia trap)
Symptom: You fix one bottleneck and another immediately appears; the constraint seems to "jump around," and it's unclear whether you're making progress or chasing your tail.
Core principle: A moving constraint is the expected, healthy outcome of POOGI Step 5 — when you break a constraint, some other stage becomes the new limit, and you repeat. The danger is twofold: (1) thrashing — chasing a constraint that oscillates because it isn't stable, and (2) inertia — leaving in place policies that were built around an old constraint, which then become the new constraint themselves. Goldratt's warning: "do not allow inertia to cause a system's constraint."
The constraint moved. Is this healthy progress or thrashing?
│
├── Did the GOAL METRIC improve each time the constraint moved?
│ run queries/throughput-accounting.py across the iterations.
│ │
│ ├── Yes — T rose with each move
│ │ └── Healthy POOGI. The system is genuinely improving. Decide whether to
│ │ continue:
│ │ │
│ │ ├── Current T is "good enough" vs the goal
│ │ │ └── STOP iterating. Lock in the current constraint as a
│ │ │ deliberate control point (you WANT a known, managed
│ │ │ constraint — e.g. a strategic capacity you scale on demand).
│ │ │ Stabilize it with Drum-Buffer-Rope → wip-accumulation-tree.md.
│ │ │
│ │ └── Still short of the goal
│ │ └── Continue: re-identify and exploit the NEW constraint →
│ │ find-the-constraint-tree.md.
│ │
│ └── No — T flat or oscillating while the "constraint" jumps around
│ └── Thrashing, not progress. The most likely causes:
│ │
│ ├── Constraints are close together (several stages near-equal)
│ │ └── Whichever you touch becomes "not slowest" by a hair, so it
│ │ looks like it moved. Stop optimizing single stages; the
│ │ system is balanced and gated by FLOW → install a WIP cap and
│ │ subordinate to one chosen pacing stage →
│ │ wip-accumulation-tree.md / utilization-trap-tree.md.
│ │
│ ├── Measurement noise (you're reacting to run-to-run variance)
│ │ └── Average over multiple runs before declaring a constraint.
│ │ Re-measure with queries/measure-stage-times.sh (several runs).
│ │
│ └── The real constraint is a POLICY that re-creates the symptom
│ each time (e.g. a batch-everything rule, a metric that rewards
│ local utilization) → policy-constraint-tree.md.
│
└── INERTIA CHECK (run after every successful move): are any policies, rules,
schedules, buffers, or instructions still tuned to the OLD constraint?
Run queries/five-focusing-steps.sh and answer the Step 5 prompts.
│
├── Yes — e.g. a buffer sized for the old bottleneck, a "always run full
│ suite" rule from when tests were fast, a review process built around a
│ slow stage that's now fast
│ └── REMOVE/resize the stale policy. Left in place it becomes the new
│ constraint. Then re-identify → find-the-constraint-tree.md.
│
└── No stale policies found
└── Clean. Continue POOGI on the new constraint →
find-the-constraint-tree.md.Inertia: the most expensive constraint
After two or three POOGI cycles, the binding constraint is frequently no longer a resource at all — it's a rule that used to make sense. Examples:
- A CI gate that runs the entire suite "to be safe" — rational when the suite was 30s, now the constraint at 30min.
- A buffer/queue sized large for a slow stage that has since been elevated — now it just inflates lead time.
- An Agent Skill instruction to "read all references before acting" — sensible when references were few, now the context constraint.
These are invisible to stage timing because the waste is structural. The inertia check above and policy-constraint-tree.md exist to catch them.
Decision criteria (measurable)
| Signal | Reading |
|---|---|
| T trend across moves | Rising = healthy POOGI; flat/oscillating = thrashing |
| Stage time spread | Several stages within ~15% = balanced; flow-gated, stop chasing one |
| Run-to-run variance | High variance = average more runs before acting |
| Stale-policy check | Any rule tuned to a former constraint = inertia; remove it |
Terminal actions
- Continue POOGI — healthy improvement; re-identify the new constraint.
- Stop + stabilize — good enough; lock the constraint as a managed control point with DBR.
- Stop chasing, cap WIP — system is balanced/flow-gated; subordinate to one pacing stage.
- Average more runs — you're reacting to noise.
- Remove stale policy — inertia; a former-constraint rule is now binding → policy-constraint-tree.md.
Record the constraint's path (stage A → B → policy C) in ../assets/templates/report.md; the trajectory tells you where to invest structurally.
Decision Tree: Policy Constraint (the limiter is a rule, not a resource)
Symptom: No physical resource is maxed out — all stages have spare capacity, nothing is starved for hardware — yet throughput is still capped. Or: you keep elevating resources and the system refuses to go faster. The binding constraint is a policy: a rule, process, metric, or instruction.
Core principle: Goldratt's most consequential claim is that the vast majority of real-world constraints are policy constraints, not physical ones — and they are the highest-leverage to fix because changing a rule usually costs nothing. A policy constraint is invisible to stage timing because the waste is structural: the resource isn't slow, it's forbidden from flowing.
Throughput is capped but no resource is maxed. Find the binding policy.
│
├── Run queries/utilization-vs-throughput.py.
│ Are ALL stages well under 100% busy (e.g. <70%) yet throughput is low?
│ │
│ ├── No — some resource IS maxed → this isn't (only) a policy constraint →
│ │ find-the-constraint-tree.md (treat the maxed resource first).
│ │
│ └── Yes — spare capacity everywhere, low throughput
│ └── A policy is gating flow. Identify which class:
│ │
│ ├── BATCHING — work is held and processed in large batches
│ │ (deploy weekly, run the full test suite on every change, review
│ │ only at end of sprint, regenerate everything on any edit)
│ │ └── EXPLOIT by shrinking the batch: deploy per-PR, gate only on
│ │ tests affected by the diff, review continuously, regenerate
│ │ only changed artifacts. Smaller batches cut lead time and
│ │ unlock idle capacity. Re-measure with throughput-accounting.py.
│ │
│ ├── SERIALIZATION — steps that could overlap are forced sequential
│ │ (one reviewer at a time, single-threaded gate, mandatory handoff
│ │ ordering, "read every reference before acting")
│ │ └── EXPLOIT by parallelizing or removing the ordering constraint:
│ │ multiple reviewers, fan-out the gate, read references on
│ │ demand instead of all up front. Re-measure.
│ │
│ ├── WRONG METRIC — local efficiency/utilization is rewarded over
│ │ global throughput, so people re-create the utilization trap
│ │ └── CHANGE THE METRIC to a global one (throughput, lead time).
│ │ Until the measurement changes, the behavior won't. This is
│ │ the root of recurring utilization-trap-tree.md symptoms.
│ │
│ ├── GATE/APPROVAL — work waits on a sign-off that adds little
│ │ (mandatory manual QA on low-risk changes, change-advisory board
│ │ for routine deploys, an approval step that rubber-stamps)
│ │ └── EXPLOIT by right-sizing the gate: risk-tier it (auto-pass
│ │ low-risk), make it asynchronous, or remove it where it adds
│ │ no protection. Verify quality doesn't regress, then keep.
│ │
│ └── STALE RULE — a policy that was rational for a FORMER constraint
│ (see moving-constraint-tree.md inertia check)
│ └── REMOVE/resize it to match the current system.
│
└── Can't tell which policy is binding?
└── Apply the Evaporating Cloud to surface the assumption that keeps the
policy in place — most policies persist because of an unexamined
"we have to do it this way" → conflict-resolution-tree.md.Verifying a policy change (don't trade throughput for safety blindly)
A policy often exists for a reason (the full suite catches regressions; the gate catches bad deploys). Before keeping a relaxation, confirm the protected outcome still holds:
- Shrinking the test gate? Confirm escaped-defect rate doesn't rise (track it for a few cycles).
- Removing an approval? Confirm the incident rate for that change class stays flat.
- Reading references on demand? Confirm the agent still produces correct output (run
dev-skill:eval).
If quality regresses, you found a real (not stale) policy — restore it and elevate elsewhere. ToC improves throughput subject to the system still meeting its necessary conditions (quality, safety).
Why policy constraints are highest-leverage
| Physical constraint | Policy constraint | |
|---|---|---|
| Cost to elevate | Money (hardware, headcount) | Usually free (change the rule) |
| Visibility | Shows up in stage timing | Invisible to timing; needs reasoning |
| Frequency | Minority of real cases | Majority of real cases |
| Risk | Low (more capacity) | Must verify necessary conditions still hold |
Decision criteria (measurable)
| Signal | Reading |
|---|---|
| Utilization across stages | All <70% + low T = a policy gates flow |
| Batch size | Large batches + idle capacity = batching constraint |
| Parallelizable-but-serial steps | Forced ordering with spare capacity = serialization constraint |
| Metric in use | Local utilization/efficiency rewarded = wrong-metric constraint |
Terminal actions
- Shrink the batch / parallelize / right-size the gate / change the metric — relax the binding policy, then verify necessary conditions hold.
- Remove the stale rule — inertia from a former constraint.
- Restore + elevate elsewhere — the policy is real (quality regressed); it's protecting something.
- Surface the assumption — can't tell which policy binds → conflict-resolution-tree.md.
Record the policy changed and the necessary-condition you verified in ../assets/templates/report.md.
#!/usr/bin/env bash
# five-focusing-steps.sh — Walk the POOGI (Process Of Ongoing Improvement) for a constraint.
#
# Theory of Constraints is a loop, run strictly in order. This script prints the
# Five Focusing Steps as answerable prompts for a named constraint and goal
# metric, and appends a timestamped entry to the investigation log so recurring
# constraints (and elevations that didn't hold) become visible over time.
#
# It is non-interactive by design: it emits the prompts for the agent/user to
# answer, and logs that an investigation was opened. Do NOT skip steps —
# exploiting before elevating is what separates ToC from "throw resources at it."
#
# Usage:
# five-focusing-steps.sh --constraint "<name>" [--metric "<goal metric>"] [--log <path>]
#
# Parameters:
# --constraint "<name>" The constraint you have identified (required).
# --metric "<goal>" The global throughput metric you are improving
# (e.g. "PRs merged/week"). Optional but recommended.
# --log <path> Investigation log file. Default:
# "${CLAUDE_PLUGIN_DATA:-$HOME/.claude}/toc-investigations.log".
#
# Example:
# five-focusing-steps.sh --constraint "CI test stage" --metric "builds/hour"
#
# Expected output: the five step prompts to answer in order, plus confirmation
# that an entry was appended to the log. Record answers in
# ../assets/templates/report.md.
set -euo pipefail
CONSTRAINT=""
METRIC=""
LOG="${CLAUDE_PLUGIN_DATA:-$HOME/.claude}/toc-investigations.log"
while [[ $# -gt 0 ]]; do
case "$1" in
--constraint) CONSTRAINT="${2:-}"; shift 2 || { echo "error: --constraint needs a value." >&2; exit 1; } ;;
--metric) METRIC="${2:-}"; shift 2 || { echo "error: --metric needs a value." >&2; exit 1; } ;;
--log) LOG="${2:-}"; shift 2 || { echo "error: --log needs a value." >&2; exit 1; } ;;
*) echo "error: unknown argument '$1'." >&2; echo "usage: $0 --constraint \"<name>\" [--metric \"<goal>\"] [--log <path>]" >&2; exit 1 ;;
esac
done
if [[ -z "$CONSTRAINT" ]]; then
echo "error: --constraint is required." >&2
echo "usage: $0 --constraint \"<name>\" [--metric \"<goal>\"] [--log <path>]" >&2
exit 1
fi
metric_line="${METRIC:-(not set — define the global metric you are maximizing)}"
cat <<EOF
Five Focusing Steps for constraint: $CONSTRAINT
Goal metric: $metric_line
-----------------------------------------------------------------------------
1. IDENTIFY — Confirm "$CONSTRAINT" is the binding constraint.
- What evidence? (slowest stage, growing WIP in front of it, ~100% busy)
- If unsure, stop and run measure-stage-times.sh / measure-wip.sh first.
2. EXPLOIT — Get the MOST from it with NO new spend.
- Is it ever idle, blocked, or doing avoidable/rework? Remove that.
- Can its inputs be pre-staged so it never starves?
- Re-measure $metric_line. Improved enough? If yes, STOP here.
3. SUBORDINATE — Pace every NON-constraint to this constraint.
- Make non-constraints idle rather than build WIP in front of it (Drum-Buffer-Rope).
- Cap released work to the constraint's actual rate.
4. ELEVATE — Only now, add capacity to the constraint.
- Parallelize / add a worker / shard / upgrade / split the batch.
- Verify with throughput-accounting.py that T rose (not just OE).
5. REPEAT — The constraint has moved. Re-identify from step 1.
- INERTIA CHECK: remove any policy/buffer/rule tuned to the OLD constraint,
or it becomes the new one. See moving-constraint-tree.md.
-----------------------------------------------------------------------------
Record your answers in ../assets/templates/report.md.
EOF
# Append a log entry (best-effort; never fail the run on logging issues).
ts="$(date '+%Y-%m-%d %H:%M:%S')"
if mkdir -p "$(dirname "$LOG")" 2>/dev/null && printf '%s\tconstraint=%s\tmetric=%s\n' "$ts" "$CONSTRAINT" "${METRIC:-unset}" >> "$LOG" 2>/dev/null; then
echo ""
echo "Logged investigation to: $LOG"
else
echo "" >&2
echo "warn: could not write log at '$LOG' (continuing without logging)." >&2
fi
#!/usr/bin/env bash
# measure-stage-times.sh — Time each stage of a pipeline to find the constraint candidate.
#
# Theory of Constraints: the stage that dominates end-to-end wall-clock is the
# first suspect for the system's constraint. This script runs each stage you
# name, times it (optionally averaged over multiple runs), and ranks stages by
# mean duration with each stage's share of total wall-clock.
#
# Usage:
# measure-stage-times.sh [--runs N] "<name>=<command>" ["<name>=<command>" ...]
#
# Parameters:
# --runs N How many times to run each stage; reports the mean.
# Default: 1. Use >=3 when run-to-run variance is high
# (see moving-constraint-tree.md).
# "<name>=<cmd>" A stage label and the shell command that performs it. The
# command IS EXECUTED, so pass real, side-effect-acceptable
# commands (a build, a test run, a script). Quote each pair.
#
# Example:
# measure-stage-times.sh --runs 3 \
# "install=npm ci" "build=npm run build" "test=npm test" "lint=npm run lint"
#
# Expected output (ranked slowest first):
# STAGE MEAN(s) SHARE RUNS
# test 82.4000 61.2% 3 <-- CONSTRAINT CANDIDATE
# build 28.1000 20.9% 3
# install 18.3000 13.6% 3
# lint 5.7000 4.2% 3
# TOTAL 134.5000
#
# A stage flagged CONSTRAINT CANDIDATE (>=40% of total wall-clock, OR the top
# stage at >=2x the next) is your first suspect. Confirm it with measure-wip.sh,
# then apply the Five Focusing Steps (see find-the-constraint-tree.md).
set -euo pipefail
RUNS=1
if [[ "${1:-}" == "--runs" ]]; then
RUNS="${2:-}"
shift 2 || { echo "error: --runs needs a value." >&2; exit 1; }
fi
case "$RUNS" in
''|*[!0-9]*) echo "error: --runs must be a positive integer (got '$RUNS')." >&2; exit 1 ;;
esac
[[ "$RUNS" -ge 1 ]] || { echo "error: --runs must be >= 1." >&2; exit 1; }
if [[ $# -lt 1 ]]; then
echo "error: provide at least one stage as \"name=command\"." >&2
echo "usage: $0 [--runs N] \"name=command\" [\"name=command\" ...]" >&2
exit 1
fi
# High-resolution epoch seconds; falls back to whole seconds if python3 absent.
_now() { python3 -c 'import time; print(time.time())' 2>/dev/null || date +%s; }
names=()
means=()
total=0
for pair in "$@"; do
if [[ "$pair" != *=* ]]; then
echo "error: '$pair' is not in name=command form." >&2
exit 1
fi
name="${pair%%=*}"
cmd="${pair#*=}"
sum=0
for ((i=1; i<=RUNS; i++)); do
start="$(_now)"
if ! bash -c "$cmd" >/dev/null 2>&1; then
echo "warn: stage '$name' exited non-zero on run $i (timing still recorded)." >&2
fi
end="$(_now)"
dur="$(awk -v a="$start" -v b="$end" 'BEGIN{printf "%.4f", b-a}')"
sum="$(awk -v s="$sum" -v d="$dur" 'BEGIN{printf "%.4f", s+d}')"
done
mean="$(awk -v s="$sum" -v r="$RUNS" 'BEGIN{printf "%.4f", s/r}')"
names+=("$name")
means+=("$mean")
total="$(awk -v t="$total" -v m="$mean" 'BEGIN{printf "%.4f", t+m}')"
done
# Sort stages by mean (descending) into parallel arrays, kept in this shell.
s_means=()
s_names=()
while read -r m idx; do
[[ -z "$m" ]] && continue
s_means+=("$m")
s_names+=("${names[$idx]}")
done <<< "$(for i in "${!means[@]}"; do echo "${means[$i]} $i"; done | sort -rn)"
printf '%-16s %9s %8s %6s\n' "STAGE" "MEAN(s)" "SHARE" "RUNS"
for j in "${!s_means[@]}"; do
m="${s_means[$j]}"
name="${s_names[$j]}"
share="$(awk -v m="$m" -v t="$total" 'BEGIN{ if (t>0) printf "%.1f", (m/t)*100; else print "0.0" }')"
flag=""
if awk -v m="$m" -v t="$total" 'BEGIN{exit !(t>0 && (m/t)>=0.40)}'; then
flag=" <-- CONSTRAINT CANDIDATE (>=40% of total)"
elif [[ "$j" -eq 0 && "${#s_means[@]}" -gt 1 ]]; then
next="${s_means[1]}"
if awk -v m="$m" -v n="$next" 'BEGIN{exit !(n>0 && (m/n)>=2)}'; then
flag=" <-- CONSTRAINT CANDIDATE (>=2x next stage)"
fi
fi
printf '%-16s %9s %7s%% %6s%s\n' "$name" "$m" "$share" "$RUNS" "$flag"
done
printf '%-16s %9s\n' "TOTAL" "$total"
#!/usr/bin/env bash
# measure-wip.sh — Count work-in-progress (WIP) waiting at each stage.
#
# Theory of Constraints: WIP accumulates immediately in front of the constraint,
# because the constraint can't consume work as fast as upstream produces it. The
# largest / fastest-growing inbox points straight at the bottleneck. Run this
# twice (use --again) to see which inbox GROWS — growth, not size alone, is the
# constraint signal.
#
# Each "probe" is a command whose STDOUT LINE COUNT is the WIP at that stage
# (e.g. open PRs awaiting review, files in a queue directory, pending jobs).
#
# Usage:
# measure-wip.sh [--again SECONDS] "<name>=<command>" ["<name>=<command>" ...]
#
# Parameters:
# --again SECONDS Take a second reading after sleeping SECONDS, then report
# the delta per stage (+N = growing). Omit for a single
# snapshot. Default: single reading.
# "<name>=<cmd>" A stage label and a command that lists the waiting items,
# one per line. The command IS EXECUTED (read-only is
# expected). Quote each pair.
#
# Examples:
# measure-wip.sh \
# "review=gh pr list --state open --search 'review:required'" \
# "ci-queue=gh run list --status queued" \
# "deploy-queue=ls -1 ./deploy-queue"
# measure-wip.sh --again 60 "review=gh pr list --state open"
#
# Expected output:
# STAGE WIP DELTA
# review 27 +4 <-- GROWING: constraint is at/just downstream
# ci-queue 9 +0
# deploy-queue 2 -1
#
# A stage whose WIP is largest AND growing (positive DELTA across readings)
# means the stage consuming that inbox is the constraint. Confirm it is busy
# (utilization-vs-throughput.py); if it is idle while its inbox grows, it is
# BLOCKED, not slow. See wip-accumulation-tree.md.
set -euo pipefail
AGAIN=""
if [[ "${1:-}" == "--again" ]]; then
AGAIN="${2:-}"
shift 2 || { echo "error: --again needs a value in seconds." >&2; exit 1; }
case "$AGAIN" in
''|*[!0-9]*) echo "error: --again must be a positive integer (seconds)." >&2; exit 1 ;;
esac
fi
if [[ $# -lt 1 ]]; then
echo "error: provide at least one stage probe as \"name=command\"." >&2
echo "usage: $0 [--again SECONDS] \"name=command\" [\"name=command\" ...]" >&2
exit 1
fi
# Count lines emitted by a probe command (WIP at that stage).
_probe() {
local cmd="$1"
bash -c "$cmd" 2>/dev/null | grep -c '' || true
}
names=()
first=()
for pair in "$@"; do
if [[ "$pair" != *=* ]]; then
echo "error: '$pair' is not in name=command form." >&2
exit 1
fi
names+=("${pair%%=*}")
first+=("$(_probe "${pair#*=}")")
done
second=()
if [[ -n "$AGAIN" ]]; then
echo "First reading taken; sleeping ${AGAIN}s for the second reading..." >&2
sleep "$AGAIN"
for pair in "$@"; do
second+=("$(_probe "${pair#*=}")")
done
fi
printf '%-16s %6s %8s\n' "STAGE" "WIP" "DELTA"
for i in "${!names[@]}"; do
wip="${first[$i]}"
delta="n/a"
flag=""
if [[ -n "$AGAIN" ]]; then
d=$(( ${second[$i]} - ${first[$i]} ))
wip="${second[$i]}"
if [[ "$d" -gt 0 ]]; then
delta="+$d"
flag=" <-- GROWING: constraint at/just downstream"
elif [[ "$d" -lt 0 ]]; then
delta="$d"
else
delta="+0"
fi
fi
printf '%-16s %6s %8s%s\n' "${names[$i]}" "$wip" "$delta" "$flag"
done
if [[ -z "$AGAIN" ]]; then
echo "" >&2
echo "Single snapshot only. Re-run with --again SECONDS to detect which inbox is GROWING" >&2
echo "(growth, not size, identifies the constraint). See wip-accumulation-tree.md." >&2
fi
#!/usr/bin/env bash
# skill-context-cost.sh — Find the constraint inside an Agent Skill: its context budget.
#
# Theory of Constraints applied to an Agent Skill: the scarce resource is the
# agent's context window. Content that is ALWAYS loaded (SKILL.md body + its
# frontmatter description, plus AGENTS.md if present) is paid on every trigger,
# whether or not it is used — that always-loaded budget is the usual constraint.
# On-demand references cost nothing until read (progressive disclosure), so a
# skill EXPLOITS its constraint by keeping the entry point lean and pushing
# detail into references/.
#
# This script measures lines and approximate tokens (bytes / 4) for the
# always-loaded surface vs each on-demand reference, and flags an over-budget
# entry point.
#
# Usage:
# skill-context-cost.sh <skill-dir>
#
# Parameters:
# <skill-dir> Path to a skill directory containing SKILL.md (and optionally
# AGENTS.md and references/). Required.
#
# Example:
# skill-context-cost.sh skills/.experimental/theory-of-constraints
#
# Expected output:
# ALWAYS-LOADED (paid every trigger) LINES ~TOKENS
# SKILL.md 180 1100
# AGENTS.md 90 540
# always-loaded total 270 1640
#
# ON-DEMAND references/ (paid only when read) LINES ~TOKENS
# find-the-constraint-tree.md 120 760
# ...
#
# VERDICT: <ok | constraint = context budget>
#
# A skill whose SKILL.md exceeds ~500 lines, or whose always-loaded total dwarfs
# its on-demand references, is constrained by its context budget. EXPLOIT: move
# detail into references/, leave a navigational entry point. See
# find-the-constraint-tree.md (Agent Skill branch) and dev-skill:evolve.
set -euo pipefail
DIR="${1:-}"
if [[ -z "$DIR" ]]; then
echo "error: provide a skill directory." >&2
echo "usage: $0 <skill-dir>" >&2
exit 1
fi
if [[ ! -d "$DIR" ]]; then
echo "error: '$DIR' is not a directory." >&2
exit 1
fi
if [[ ! -f "$DIR/SKILL.md" ]]; then
echo "error: '$DIR' has no SKILL.md — is this a skill directory?" >&2
exit 1
fi
# Print "lines tokens" for a file (tokens approximated as bytes / 4).
_cost() {
local f="$1"
local lines bytes
lines="$(grep -c '' "$f" 2>/dev/null || echo 0)"
bytes="$(wc -c < "$f" 2>/dev/null | tr -d ' ' || echo 0)"
echo "$lines $((bytes / 4))"
}
always_lines=0
always_tokens=0
skill_lines=0
printf '%-42s %7s %9s\n' "ALWAYS-LOADED (paid every trigger)" "LINES" "~TOKENS"
for f in "$DIR/SKILL.md" "$DIR/AGENTS.md"; do
[[ -f "$f" ]] || continue
read -r l t <<< "$(_cost "$f")"
printf '%-42s %7s %9s\n' "$(basename "$f")" "$l" "$t"
always_lines=$((always_lines + l))
always_tokens=$((always_tokens + t))
[[ "$(basename "$f")" == "SKILL.md" ]] && skill_lines="$l"
done
printf '%-42s %7s %9s\n' "always-loaded total" "$always_lines" "$always_tokens"
echo ""
ondemand_tokens=0
ref_count=0
if [[ -d "$DIR/references" ]]; then
printf '%-42s %7s %9s\n' "ON-DEMAND references/ (paid when read)" "LINES" "~TOKENS"
# Largest references first.
while IFS= read -r f; do
[[ -z "$f" ]] && continue
read -r l t <<< "$(_cost "$f")"
name="${f#"$DIR"/references/}"
printf '%-42s %7s %9s\n' "$name" "$l" "$t"
ondemand_tokens=$((ondemand_tokens + t))
ref_count=$((ref_count + 1))
done <<< "$(find "$DIR/references" -type f \( -name '*.md' -o -name '*.sh' -o -name '*.py' -o -name '*.sql' \) | sort)"
echo ""
fi
echo "always-loaded ~tokens: $always_tokens | on-demand ~tokens: $ondemand_tokens (across $ref_count files)"
echo ""
verdict="ok — entry point is lean; context budget is not the constraint"
if [[ "$skill_lines" -gt 500 ]]; then
verdict="CONSTRAINT = context budget — SKILL.md is ${skill_lines} lines (>500). EXPLOIT: move detail into references/."
elif [[ "$ondemand_tokens" -gt 0 && "$always_tokens" -gt "$ondemand_tokens" ]]; then
verdict="CONSTRAINT = context budget — always-loaded (~$always_tokens tok) exceeds on-demand (~$ondemand_tokens tok). EXPLOIT: shift detail to references/."
fi
echo "VERDICT: $verdict"
#!/usr/bin/env python3
"""throughput-accounting.py — Judge a change by its effect on the global goal.
Theory of Constraints measures a system with three numbers, not local efficiency:
T (Throughput) — rate the system generates its goal unit
(PRs merged/week, requests/sec, task completions/session,
builds/hour). MORE is better.
I (Inventory/Investment) — work or money tied up in the system right now
(open WIP, queued items, capital). LESS is better.
OE (Operating Expense) — rate of spend to turn I into T
(tokens/run, CI minutes, $/month, person-hours). LESS is better.
Goal: increase T while holding or reducing I and OE. A change that lowers OE or
speeds a stage but leaves T flat is a LOCAL OPTIMUM — "an hour saved at a
non-bottleneck is a mirage." This script computes the before/after deltas and
flags that pattern.
Usage:
throughput-accounting.py --before T I OE --after T I OE [--tol PCT]
Parameters:
--before T I OE The three measures BEFORE the change (floats, any unit; keep
units consistent between before and after).
--after T I OE The three measures AFTER the change.
--tol PCT Throughput-change tolerance, percent. A |dT| below this is
treated as "no real throughput change." Default: 5.
Example:
# Build got 30% faster (OE down) but merges/week unchanged:
throughput-accounting.py --before 20 12 100 --after 20 12 70
# -> LOCAL OPTIMUM: OE fell 30% but throughput is flat.
Expected output: a before/after table, deltas, derived Net (T - OE) and a
verdict — one of: THROUGHPUT IMPROVED, LOCAL OPTIMUM, WIP INFLATED, or REGRESSION.
"""
import argparse
import sys
def pct(before: float, after: float) -> float:
if before == 0:
return float("inf") if after != 0 else 0.0
return (after - before) / abs(before) * 100.0
def fmt_pct(p: float) -> str:
if p == float("inf"):
return "+inf%"
sign = "+" if p >= 0 else ""
return f"{sign}{p:.1f}%"
def main() -> int:
ap = argparse.ArgumentParser(
description="Throughput accounting: judge a change by global T/I/OE deltas."
)
ap.add_argument("--before", nargs=3, type=float, required=True,
metavar=("T", "I", "OE"), help="Throughput, Inventory, Operating Expense BEFORE.")
ap.add_argument("--after", nargs=3, type=float, required=True,
metavar=("T", "I", "OE"), help="Throughput, Inventory, Operating Expense AFTER.")
ap.add_argument("--tol", type=float, default=5.0,
help="Throughput-change tolerance in percent (default 5).")
args = ap.parse_args()
t0, i0, oe0 = args.before
t1, i1, oe1 = args.after
for label, val in (("Inventory", i0), ("Inventory", i1)):
if val < 0:
print(f"error: {label} cannot be negative.", file=sys.stderr)
return 1
dT, dI, dOE = pct(t0, t1), pct(i0, i1), pct(oe0, oe1)
net0, net1 = t0 - oe0, t1 - oe1
print(f"{'MEASURE':<22}{'BEFORE':>12}{'AFTER':>12}{'DELTA':>10}")
print(f"{'T Throughput (↑)':<22}{t0:>12.3f}{t1:>12.3f}{fmt_pct(dT):>10}")
print(f"{'I Inventory (↓)':<22}{i0:>12.3f}{i1:>12.3f}{fmt_pct(dI):>10}")
print(f"{'OE Operating Exp (↓)':<22}{oe0:>12.3f}{oe1:>12.3f}{fmt_pct(dOE):>10}")
print(f"{'Net (T - OE) (↑)':<22}{net0:>12.3f}{net1:>12.3f}{fmt_pct(pct(net0, net1)):>10}")
print()
t_flat = abs(dT) < args.tol
verdict, detail = "", ""
if dT >= args.tol and t1 > t0:
verdict = "THROUGHPUT IMPROVED"
detail = ("T rose above tolerance. If the changed stage was the constraint, "
"this is real — re-identify the new constraint (find-the-constraint-tree.md).")
elif t_flat and dOE <= -args.tol:
verdict = "LOCAL OPTIMUM"
detail = ("OE fell but throughput is flat — the classic mirage of the "
"non-bottleneck. You optimized a non-constraint. Keep the change only "
"if it is cheap and side-effect-free; redirect effort to the real "
"constraint (local-optimum-tree.md).")
elif t_flat and dI >= args.tol:
verdict = "WIP INFLATED"
detail = ("Inventory grew with no throughput gain — you likely sped up or "
"parallelized a non-constraint, flooding the bottleneck. Subordinate it "
"and cap WIP (wip-accumulation-tree.md).")
elif dT <= -args.tol:
verdict = "REGRESSION"
detail = ("Throughput fell. Revert, or check whether the change pushed more "
"work at the constraint (wip-accumulation-tree.md).")
else:
verdict = "NO MATERIAL CHANGE"
detail = ("Nothing moved beyond tolerance. Confirm you measured the GLOBAL goal "
"metric, not a local stage time (local-optimum-tree.md).")
print(f"VERDICT: {verdict}")
print(f" {detail}")
return 0
if __name__ == "__main__":
sys.exit(main())
#!/usr/bin/env python3
"""utilization-vs-throughput.py — Detect the utilization trap (busy != productive).
Theory of Constraints: high utilization is NOT a goal. "Activating a resource is
not the same as utilizing it" — running a non-constraint flat-out only piles up
WIP. A correctly-subordinated non-constraint SHOULD idle part of the time. Only
the constraint should approach 100% busy. This script ranks stages by
utilization, identifies the likely constraint (highest utilization), and flags
over-activated non-constraints and the no-resource-saturated case (a policy
constraint).
For each stage you supply busy time, available time, and items completed over
the same window. Utilization = busy / available. Throughput = completed / window.
Usage:
utilization-vs-throughput.py "<name>:<busy>:<available>:<completed>" [more...]
Parameters:
"<name>:<busy>:<available>:<completed>"
name stage label
busy time the stage was actively working (any unit, consistent)
available total time the stage could have worked (same unit)
completed items the stage finished in that window
Provide one quoted argument per stage.
Example:
utilization-vs-throughput.py \
"lint:5:60:40" "build:28:60:40" "test:59:60:38" "deploy:9:60:38"
# -> test ~98% busy = constraint; lint/build flagged if >85% but not constraint.
Expected output: a per-stage table (utilization%, throughput), the identified
constraint, and a verdict — over-activated non-constraints to subordinate, or
"no resource saturated -> suspect a policy constraint."
"""
import argparse
import sys
OVER_ACTIVATED = 85.0 # % utilization considered "running hot"
SATURATED = 95.0 # % utilization considered effectively the constraint
SLACK_SYSTEM = 70.0 # if every stage is below this, suspect a policy constraint
def parse_stage(spec: str):
parts = spec.split(":")
if len(parts) != 4:
raise ValueError(f"'{spec}' must be name:busy:available:completed")
name, busy_s, avail_s, done_s = parts
busy, avail, done = float(busy_s), float(avail_s), float(done_s)
if avail <= 0:
raise ValueError(f"'{name}': available time must be > 0")
if busy < 0 or done < 0:
raise ValueError(f"'{name}': busy and completed must be >= 0")
if busy > avail:
raise ValueError(f"'{name}': busy ({busy}) cannot exceed available ({avail})")
return {
"name": name,
"util": busy / avail * 100.0,
"tput": done / avail,
"done": done,
}
def main() -> int:
ap = argparse.ArgumentParser(
description="Detect the utilization trap: high utilization, low throughput."
)
ap.add_argument("stages", nargs="+",
help='One per stage: "name:busy:available:completed".')
args = ap.parse_args()
try:
stages = [parse_stage(s) for s in args.stages]
except ValueError as e:
print(f"error: {e}", file=sys.stderr)
return 1
constraint = max(stages, key=lambda s: s["util"])
print(f"{'STAGE':<14}{'UTIL%':>8}{'THROUGHPUT':>12}{'':>4}NOTE")
for s in sorted(stages, key=lambda x: x["util"], reverse=True):
note = ""
if s is constraint and s["util"] >= SATURATED:
note = "<-- CONSTRAINT (saturated)"
elif s is constraint:
note = "<-- highest utilization (likely constraint)"
elif s["util"] >= OVER_ACTIVATED:
note = "OVER-ACTIVATED non-constraint -> subordinate (let it idle)"
print(f"{s['name']:<14}{s['util']:>7.1f}%{s['tput']:>12.3f} {note}")
print()
max_util = constraint["util"]
over = [s["name"] for s in stages
if s is not constraint and s["util"] >= OVER_ACTIVATED]
if max_util < SLACK_SYSTEM:
print("VERDICT: NO RESOURCE SATURATED")
print(f" Every stage is below {SLACK_SYSTEM:.0f}% utilization yet throughput is "
"limited. No physical resource is the constraint -> suspect a POLICY "
"constraint (batching, serialization, a gate, or a wrong metric). "
"See policy-constraint-tree.md.")
elif over:
print("VERDICT: UTILIZATION TRAP")
print(f" '{constraint['name']}' is the constraint ({max_util:.0f}% busy), but "
f"{', '.join(over)} run hot (>={OVER_ACTIVATED:.0f}%) without being the "
"constraint — activating, not utilizing. SUBORDINATE them to the "
"constraint's pace; let them idle. See utilization-trap-tree.md.")
else:
print("VERDICT: BALANCED")
print(f" '{constraint['name']}' is the constraint ({max_util:.0f}% busy) and "
"non-constraints have appropriate slack. Apply the Five Focusing Steps to "
"the constraint (find-the-constraint-tree.md).")
return 0
if __name__ == "__main__":
sys.exit(main())
Symptom Catalog
Each symptom is an entry point into a decision tree. Match the user's situation to a row, then open the tree. Severity reflects how badly the symptom misdirects optimization effort — P1 symptoms mean effort is actively being wasted right now.
| # | Symptom / trigger | Entry tree | Severity | What it usually means |
|---|---|---|---|---|
| 1 | "Optimize this / make it faster / cheaper" but no obvious target; general slowness | find-the-constraint-tree.md | P1 | No constraint identified yet. This is the master tree — start here when in doubt. |
| 2 | A change sped up one stage but end-to-end throughput or lead time did not move | local-optimum-tree.md | P1 | You optimized a non-constraint — "an hour saved at a non-bottleneck is a mirage." |
| 3 | Work, queue depth, or WIP keeps growing at one stage; lead time climbing | wip-accumulation-tree.md | P2 | WIP piles up immediately upstream of the constraint. The pile points to it. |
| 4 | Everything looks 100% busy / fully utilized, yet little actually ships | utilization-trap-tree.md | P2 | Non-constraints are over-activated. "Activating a resource ≠ utilizing it." |
| 5 | We added capacity, parallelism, workers, or hardware and throughput barely changed | elevation-misfire-tree.md | P2 | Elevated the wrong stage, skipped exploit, or the constraint moved after elevating. |
| 6 | We fix one bottleneck and another immediately appears; the constraint oscillates | moving-constraint-tree.md | P3 | Expected under POOGI step 5 — but watch for thrashing and inertia (stale policies). |
| 7 | The limiter is a rule/process/policy, not a resource (mandatory full test gate, serialized review, "read every reference", batch-everything releases) | policy-constraint-tree.md | P2 | A policy constraint — the highest-leverage kind, because changing it costs nothing. |
| 8 | Stuck in a tradeoff that blocks the obvious fix ("faster vs safer", "smaller context vs more coverage") | conflict-resolution-tree.md | P3 | A surfaced dilemma. Resolve with the Evaporating Cloud, not compromise. |
How severity maps to action
- P1 — Effort is being wasted right now (either undirected, or aimed at a non-constraint). Stop and identify the real constraint before any more changes.
- P2 — A specific ToC step is being skipped or misapplied (subordination, exploitation, elevation). Correct the step.
- P3 — The system is improving but the meta-process needs attention (constraint moved, or a dilemma/policy needs a thinking-process tool).
Terminal states (every tree ends in one)
1. Exploit — get more from the constraint with no new spend (remove idle/waste on it). 2. Subordinate — pace non-constraints to the constraint; let them idle rather than build WIP. 3. Elevate — add capacity to the constraint (only after exploiting). 4. Change the policy — relax, reorder, or right-size a rule that is the constraint. 5. Repeat — the constraint has moved; re-identify (and check for inertia). 6. Dismiss — not a constraint problem; redirect to the appropriate skill or declare "no action; this stage is not binding."
Investigation history
Append a one-line entry per investigation to the log (default ${CLAUDE_PLUGIN_DATA}/toc-investigations.log, configurable in config.json). Over time this reveals which constraints recur and whether elevations actually held — the signal that a deeper policy constraint is in play.
Decision Tree: Utilization Trap (busy ≠ productive)
Symptom: Every stage/resource looks fully utilized — dashboards green, everyone "at capacity" — yet little actually ships and throughput is low.
Core principle: Two Goldratt rules collide here. "Activating a resource is not the same as utilizing it" — running a non-constraint flat-out only produces excess WIP, not throughput. And "the level of utilization of a non-constraint is determined by the constraint, not by its own potential." A correctly-subordinated non-constraint should sit idle part of the time. High utilization everywhere is a symptom, not a goal: balance flow, not capacity.
Everything is busy but throughput is low.
│
├── Run queries/utilization-vs-throughput.py with each stage's busy-time and
│ items-completed.
│ │
│ ├── A non-constraint stage runs >85% busy while the constraint STARVES
│ │ (constraint idle, waiting for input)
│ │ └── Over-activation. The busy non-constraint is producing the WRONG
│ │ work or work the constraint can't yet use → SUBORDINATE: throttle
│ │ the non-constraint to feed the constraint exactly what it needs,
│ │ when it needs it. Let it idle the rest of the time.
│ │
│ ├── A non-constraint runs >85% busy and the constraint is BLOCKED
│ │ (constraint can't release output downstream)
│ │ └── The downstream stage is the real constraint, not the busy one →
│ │ go to find-the-constraint-tree.md to re-identify, then subordinate
│ │ the busy stage to it.
│ │
│ ├── The CONSTRAINT itself is <100% busy while everything waits on it
│ │ └── The constraint has idle/waste on it — the highest-value fix →
│ │ EXPLOIT it (remove setup time, batching delays, rework, waiting on
│ │ approvals). An hour recovered on the constraint is an hour of system
│ │ throughput. See find-the-constraint-tree.md, Step 2.
│ │
│ └── ALL stages >85% busy and WIP growing between them
│ └── You are pushing work in faster than it can flow (a "push" system).
│ High utilization is inflating WIP and lead time, not throughput →
│ install a WIP cap / rope → wip-accumulation-tree.md.
│
└── "But idle resources look wasteful / management rewards utilization"
└── This is a POLICY constraint masquerading as a capacity problem: the
measurement system rewards local utilization over global throughput →
go to policy-constraint-tree.md. Until the metric changes, people will
re-create the trap.Why local utilization is the wrong target
If a non-constraint is faster than the constraint, forcing it to 100% just builds inventory in front of (or behind) the constraint — more I, more lead time, zero extra T. The only resource whose utilization should approach 100% is the constraint. Everywhere else, idle time is the correct state of a balanced flow.
Decision criteria (measurable)
| Signal | Reading |
|---|---|
| Non-constraint utilization | >85% while constraint starves = over-activated; subordinate |
| Constraint utilization | <100% with everything waiting = exploitable idle/waste on the constraint |
| Throughput vs utilization | Utilization up but T flat = the trap is active |
| WIP between stages | Growing while all busy = push system; cap WIP |
queries/utilization-vs-throughput.py reports utilization% and throughput per stage side by side and flags any stage that is "busy but not the constraint."
Terminal actions
- Subordinate — throttle the over-activated non-constraint to the constraint's rate; let it idle.
- Exploit — the constraint has recoverable idle/waste; remove it (highest leverage).
- Cap WIP — push system flooding itself → wip-accumulation-tree.md.
- Re-identify — the busy stage isn't the constraint and you're unsure which is → find-the-constraint-tree.md.
- Change the metric — utilization is rewarded over throughput → policy-constraint-tree.md.
Record which stages you allowed to idle in ../assets/templates/report.md — it is counterintuitive and will be questioned.
Decision Tree: WIP Accumulation (let inventory point at the constraint)
Symptom: Work-in-progress, queue depth, or an inbox keeps growing at one stage; lead time is climbing even though "everyone is working."
Core principle: In any flow, WIP piles up immediately in front of the constraint — the constraint can't consume work as fast as upstream produces it. The growing pile is a free pointer to the bottleneck. The remedy is Drum-Buffer-Rope (DBR): the constraint sets the drumbeat, a small buffer protects it from starving, and a "rope" ties the release of new work to the constraint's consumption so WIP stops growing.
WIP is growing somewhere. Where?
│
├── Run queries/measure-wip.sh across the stages.
│ │ (Counts items waiting per stage: open PRs in review, files in a queue
│ │ dir, jobs pending, branches awaiting CI, etc. Run it twice, minutes
│ │ or a cycle apart, to see which inbox GROWS.)
│ │
│ ├── One stage's inbox is largest AND growing run-over-run
│ │ └── The stage CONSUMING that inbox is the constraint. Confirm: is it
│ │ ~100% busy? (queries/utilization-vs-throughput.py)
│ │ │
│ │ ├── Yes, ~100% busy → constraint confirmed. Apply DBR (below).
│ │ │
│ │ └── No, it's idle while its inbox grows
│ │ └── The stage is BLOCKED, not slow — it waits on something
│ │ (a lock, an approval, an external dependency, a serialized
│ │ handoff). That dependency is the real constraint →
│ │ EXPLOIT by removing the block (parallelize the handoff,
│ │ pre-fetch the dependency). Then re-measure.
│ │
│ ├── WIP is growing at MULTIPLE stages
│ │ └── You are releasing work faster than the system can flow it. The
│ │ constraint is the FURTHEST-DOWNSTREAM growing stage; upstream piles
│ │ are secondary. Throttle release (rope) to that stage's rate, then
│ │ re-measure — upstream piles should drain.
│ │
│ └── No stage's WIP is growing (inboxes drain each cycle)
│ └── Not a flow/constraint problem in steady state. The slowness is
│ elsewhere → find-the-constraint-tree.md (look at stage timing).
│
└── Apply Drum-Buffer-Rope at the confirmed constraint:
│
├── DRUM — set the system's pace to the constraint's actual throughput.
│ Measure how many items/hour it really completes.
│
├── BUFFER — keep a SMALL, bounded queue right before the constraint so it
│ never starves (e.g. 1–2 items, or a few minutes of work). Bigger is not
│ safer — it just inflates lead time (I) with no T gain.
│
└── ROPE — release new work into the system only when the constraint pulls
the next item. This caps total WIP.
└── Re-run queries/measure-wip.sh: upstream inboxes should stop growing
and lead time should fall while throughput holds.
├── Lead time fell, T unchanged → success (you removed inventory waste).
│ Now consider EXPLOIT/ELEVATE on the constraint itself →
│ find-the-constraint-tree.md (Five Focusing Steps).
└── T dropped after adding the rope → buffer too small (constraint
starving). Increase buffer by one item and re-measure.Worked examples
- Dev value stream: 30 PRs open, all waiting on 2 reviewers. Review is the constraint. Rope = don't start new feature work until a review slot frees (WIP limit on "in review"). Buffer = a small ready-for-review queue so reviewers never idle.
- CI: builds queue behind a single shared test runner. Runner is the constraint. Rope = cap concurrent pipeline triggers; buffer = a short job queue; then EXPLOIT (shard tests) or ELEVATE (add a runner).
- Agent pipeline: generation steps outrun a slow verification step; partial outputs accumulate. Verification is the constraint. Rope = generate the next item only when verification frees.
Decision criteria (measurable)
| Signal | Reading |
|---|---|
| Inbox size (two readings) | Growing run-over-run = constraint is just downstream |
| Constraint utilization | ~100% busy = slow constraint; idle = blocked constraint |
| Lead time after rope | Should fall sharply with T held — that's WIP waste removed |
Terminal actions
- Apply DBR — drum/buffer/rope as above; the default fix.
- Unblock — constraint is idle-but-blocked; remove the dependency (an exploit move).
- Throttle release — multiple growing piles; cap WIP to the downstream constraint's rate.
- Dismiss — no inbox grows; not a flow problem → find-the-constraint-tree.md.
Record buffer sizes and the WIP cap you set in ../assets/templates/report.md so the next investigation can tune them.
Related skills
FAQ
What does theory-of-constraints do?
theory-of-constraints is a Claude Code skill for ai & agent building. It helps developers move faster with AI-assisted coding.
When should I use theory-of-constraints?
When you need to helps with ai & agent building tasks during ai-assisted development, or when theory-of-constraints is a claude code skill for ai & agent building. it helps developers move faster with ai-assisted coding.
What are the main capabilities?
theory-of-constraints; AI & Agent Building; AI-coding skill.