Back to skills
SkillHub ClubShip Full StackFull Stack

ost

Use when running or maintaining an Opportunity Solution Tree (OST) workflow with a lightweight graph store and CLI. Provides a single entry skill that routes to outcome, opportunity, solution, and assumption/experiment phases via progressive disclosure.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
410
Hot score
99
Updated
March 20, 2026
Overall rating
C4.6
Composite score
4.6
Best-practice grade
B81.2

Install command

npx @skill-hub/cli install kasperjunge-agent-resources-ost

Repository

kasperjunge/agent-resources

Skill path: .opencode/skill/ost

Use when running or maintaining an Opportunity Solution Tree (OST) workflow with a lightweight graph store and CLI. Provides a single entry skill that routes to outcome, opportunity, solution, and assumption/experiment phases via progressive disclosure.

Open repository

Best for

Primary workflow: Ship Full Stack.

Technical facets: Full Stack.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: kasperjunge.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install ost into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/kasperjunge/agent-resources before adding ost to shared team environments
  • Use ost for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: ost
description: Use when running or maintaining an Opportunity Solution Tree (OST) workflow with a lightweight graph store and CLI. Provides a single entry skill that routes to outcome, opportunity, solution, and assumption/experiment phases via progressive disclosure.
---

# OST

Run a full Opportunity Solution Tree workflow from a single skill, backed by a lightweight graph database and CLI.

## Entry Flow (Minimize File Loading)

Goal: Start a session, pick a workspace, and select or create an outcome.

1) Ensure DB exists
- If `.agr/ost.db` is missing, initialize it:
  - `uv run python scripts/ost.py init --path .agr/ost.db`

2) Select workspace
- List workspaces:
  - `uv run python scripts/ost.py workspace list`
- If needed, create one:
  - `uv run python scripts/ost.py workspace create \"<name>\"`

3) Select outcome
- List outcomes:
  - `uv run python scripts/ost.py outcome list --workspace <name>`
- If needed, create one:
  - `uv run python scripts/ost.py outcome add --workspace <name> \"<title>\"`

4) Route to phase (load only the relevant file)
- Outcomes: `references/outcomes.md`
- Opportunities: `references/opportunities.md`
- Solutions: `references/solutions.md`
- Assumptions/Experiments: `references/assumptions.md`

## Data Model + Storage

Use a lightweight graph DB at `.agr/ost.db` with multi-workspace support.
The CLI manages nodes and edges; do not edit DB files manually.

## CLI

Use the CLI to read/write the OST graph. The CLI is expected to live in this skill’s `scripts/` directory and be run via `uv run`.

Commands (intended):
- `uv run python scripts/ost.py init --path .agr/ost.db`
- `uv run python scripts/ost.py workspace list`
- `uv run python scripts/ost.py workspace create "<name>"`
- `uv run python scripts/ost.py outcome list --workspace <name>`
- `uv run python scripts/ost.py outcome add --workspace <name> "<title>"`
- `uv run python scripts/ost.py opportunity add --outcome <id> "<title>"`
- `uv run python scripts/ost.py solution add --opportunity <id> "<title>"`
- `uv run python scripts/ost.py assumption add --solution <id> "<title>"`
- `uv run python scripts/ost.py show --outcome <id>`

If the CLI is not yet implemented, document the intended command and proceed with non-destructive guidance only.

## Output Format

```
## OST Session

### Selected Workspace
- Name: ...

### Selected Outcome
- ID: ...
- Title: ...

### Next Action
- [What the user wants to do next]

### Next Step
- Load the relevant phase reference file
```

## What NOT to Do

- Do NOT edit `.agr/ost.db` directly.
- Do NOT invent node IDs.
- Do NOT run destructive commands without explicit user intent.


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### scripts/ost.py

```python
#!/usr/bin/env python3
import argparse
import json
import sqlite3
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Iterable, List, Optional


SCHEMA = """
CREATE TABLE IF NOT EXISTS workspaces (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL UNIQUE,
    created_at TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS nodes (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    workspace_id INTEGER NOT NULL,
    type TEXT NOT NULL,
    title TEXT NOT NULL,
    data TEXT NOT NULL DEFAULT '{}',
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL,
    FOREIGN KEY (workspace_id) REFERENCES workspaces(id)
);

CREATE TABLE IF NOT EXISTS edges (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    from_id INTEGER NOT NULL,
    to_id INTEGER NOT NULL,
    type TEXT NOT NULL,
    created_at TEXT NOT NULL,
    FOREIGN KEY (from_id) REFERENCES nodes(id),
    FOREIGN KEY (to_id) REFERENCES nodes(id)
);

CREATE INDEX IF NOT EXISTS idx_nodes_workspace_type ON nodes(workspace_id, type);
CREATE INDEX IF NOT EXISTS idx_edges_from ON edges(from_id);
CREATE INDEX IF NOT EXISTS idx_edges_to ON edges(to_id);
"""


def now_iso() -> str:
    return datetime.now(timezone.utc).replace(microsecond=0).isoformat()


def ensure_parent_dir(path: Path) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)


def connect(db_path: Path) -> sqlite3.Connection:
    conn = sqlite3.connect(str(db_path))
    conn.row_factory = sqlite3.Row
    return conn


def init_db(db_path: Path) -> None:
    ensure_parent_dir(db_path)
    conn = connect(db_path)
    with conn:
        conn.executescript(SCHEMA)
    conn.close()


def load_json_arg(value: Optional[str]) -> str:
    if value is None:
        return "{}"
    try:
        json.loads(value)
    except json.JSONDecodeError as exc:
        raise ValueError(f"Invalid JSON for --data: {exc}")
    return value


def get_workspace_id(conn: sqlite3.Connection, name: str) -> int:
    row = conn.execute("SELECT id FROM workspaces WHERE name = ?", (name,)).fetchone()
    if row is None:
        raise ValueError(f"Workspace not found: {name}")
    return int(row["id"])


def create_workspace(conn: sqlite3.Connection, name: str) -> int:
    created_at = now_iso()
    cur = conn.execute(
        "INSERT INTO workspaces (name, created_at) VALUES (?, ?)",
        (name, created_at),
    )
    return _require_lastrowid(cur)


def add_node(
    conn: sqlite3.Connection,
    workspace_id: int,
    node_type: str,
    title: str,
    data: str,
) -> int:
    created_at = now_iso()
    cur = conn.execute(
        "INSERT INTO nodes (workspace_id, type, title, data, created_at, updated_at) "
        "VALUES (?, ?, ?, ?, ?, ?)",
        (workspace_id, node_type, title, data, created_at, created_at),
    )
    return _require_lastrowid(cur)


def _require_lastrowid(cur: sqlite3.Cursor) -> int:
    lastrowid = cur.lastrowid
    if lastrowid is None:
        raise ValueError("Insert failed: no lastrowid returned")
    return int(lastrowid)


def update_node_data(conn: sqlite3.Connection, node_id: int, data: str) -> None:
    updated_at = now_iso()
    cur = conn.execute(
        "UPDATE nodes SET data = ?, updated_at = ? WHERE id = ?",
        (data, updated_at, node_id),
    )
    if cur.rowcount == 0:
        raise ValueError(f"Node not found: {node_id}")


def add_edge(
    conn: sqlite3.Connection, from_id: int, to_id: int, edge_type: str
) -> None:
    created_at = now_iso()
    conn.execute(
        "INSERT INTO edges (from_id, to_id, type, created_at) VALUES (?, ?, ?, ?)",
        (from_id, to_id, edge_type, created_at),
    )


def list_nodes(
    conn: sqlite3.Connection,
    workspace_id: int,
    node_type: str,
) -> List[sqlite3.Row]:
    return conn.execute(
        "SELECT id, title, data FROM nodes WHERE workspace_id = ? AND type = ? ORDER BY id",
        (workspace_id, node_type),
    ).fetchall()


def list_children(
    conn: sqlite3.Connection,
    parent_id: int,
    child_type: str,
    edge_type: str,
) -> List[sqlite3.Row]:
    return conn.execute(
        "SELECT n.id, n.title, n.data FROM nodes n "
        "JOIN edges e ON e.to_id = n.id "
        "WHERE e.from_id = ? AND e.type = ? AND n.type = ? "
        "ORDER BY n.id",
        (parent_id, edge_type, child_type),
    ).fetchall()


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(prog="ost", description="OST graph CLI")
    parser.add_argument("--path", default=".agr/ost.db", help="Path to OST SQLite DB")

    subparsers = parser.add_subparsers(dest="command", required=True)

    subparsers.add_parser("init", help="Initialize database")

    ws = subparsers.add_parser("workspace", help="Workspace operations")
    ws_sub = ws.add_subparsers(dest="ws_cmd", required=True)
    ws_sub.add_parser("list", help="List workspaces")
    ws_create = ws_sub.add_parser("create", help="Create workspace")
    ws_create.add_argument("name")

    outcome = subparsers.add_parser("outcome", help="Outcome operations")
    outcome_sub = outcome.add_subparsers(dest="outcome_cmd", required=True)
    outcome_list = outcome_sub.add_parser("list", help="List outcomes")
    outcome_list.add_argument("--workspace", required=True)
    outcome_add = outcome_sub.add_parser("add", help="Add outcome")
    outcome_add.add_argument("--workspace", required=True)
    outcome_add.add_argument("title")
    outcome_add.add_argument("--data")
    outcome_update = outcome_sub.add_parser("update", help="Update outcome data")
    outcome_update.add_argument("--id", required=True, type=int)
    outcome_update.add_argument("--data", required=True)

    opportunity = subparsers.add_parser("opportunity", help="Opportunity operations")
    opp_sub = opportunity.add_subparsers(dest="opp_cmd", required=True)
    opp_list = opp_sub.add_parser("list", help="List opportunities")
    opp_list.add_argument("--outcome", required=True, type=int)
    opp_add = opp_sub.add_parser("add", help="Add opportunity")
    opp_add.add_argument("--outcome", required=True, type=int)
    opp_add.add_argument("title")
    opp_add.add_argument("--data")

    solution = subparsers.add_parser("solution", help="Solution operations")
    sol_sub = solution.add_subparsers(dest="sol_cmd", required=True)
    sol_list = sol_sub.add_parser("list", help="List solutions")
    sol_list.add_argument("--opportunity", required=True, type=int)
    sol_add = sol_sub.add_parser("add", help="Add solution")
    sol_add.add_argument("--opportunity", required=True, type=int)
    sol_add.add_argument("title")
    sol_add.add_argument("--data")

    assumption = subparsers.add_parser("assumption", help="Assumption operations")
    as_sub = assumption.add_subparsers(dest="ass_cmd", required=True)
    as_list = as_sub.add_parser("list", help="List assumptions")
    as_list.add_argument("--solution", required=True, type=int)
    as_add = as_sub.add_parser("add", help="Add assumption")
    as_add.add_argument("--solution", required=True, type=int)
    as_add.add_argument("title")
    as_add.add_argument("--data")

    show = subparsers.add_parser("show", help="Show OST subtree")
    show.add_argument("--outcome", required=True, type=int)

    return parser.parse_args()


def require_db(path: Path) -> None:
    if not path.exists():
        raise ValueError(f"Database not found at {path}. Run 'init' first.")


def print_rows(rows: Iterable[sqlite3.Row]) -> None:
    for row in rows:
        data = row["data"]
        print(f"{row['id']}: {row['title']} {data}")


def show_tree(conn: sqlite3.Connection, outcome_id: int) -> None:
    outcome = conn.execute(
        "SELECT id, title, data FROM nodes WHERE id = ? AND type = 'outcome'",
        (outcome_id,),
    ).fetchone()
    if outcome is None:
        raise ValueError(f"Outcome not found: {outcome_id}")

    print(f"Outcome {outcome['id']}: {outcome['title']} {outcome['data']}")
    opportunities = list_children(
        conn, outcome_id, "opportunity", "outcome_opportunity"
    )
    for opp in opportunities:
        print(f"  Opportunity {opp['id']}: {opp['title']} {opp['data']}")
        solutions = list_children(conn, opp["id"], "solution", "opportunity_solution")
        for sol in solutions:
            print(f"    Solution {sol['id']}: {sol['title']} {sol['data']}")
            assumptions = list_children(
                conn, sol["id"], "assumption", "solution_assumption"
            )
            for ass in assumptions:
                print(f"      Assumption {ass['id']}: {ass['title']} {ass['data']}")


def main() -> int:
    args = parse_args()
    db_path = Path(args.path)

    try:
        if args.command == "init":
            init_db(db_path)
            print(f"Initialized OST DB at {db_path}")
            return 0

        require_db(db_path)
        conn = connect(db_path)
        with conn:
            if args.command == "workspace":
                if args.ws_cmd == "list":
                    rows = conn.execute(
                        "SELECT id, name, created_at FROM workspaces ORDER BY name"
                    ).fetchall()
                    for row in rows:
                        print(f"{row['id']}: {row['name']} ({row['created_at']})")
                    return 0
                if args.ws_cmd == "create":
                    ws_id = create_workspace(conn, args.name)
                    print(f"Created workspace {args.name} (id {ws_id})")
                    return 0

            if args.command == "outcome":
                if args.outcome_cmd == "list":
                    ws_id = get_workspace_id(conn, args.workspace)
                    rows = list_nodes(conn, ws_id, "outcome")
                    print_rows(rows)
                    return 0
                if args.outcome_cmd == "add":
                    ws_id = get_workspace_id(conn, args.workspace)
                    data = load_json_arg(args.data)
                    node_id = add_node(conn, ws_id, "outcome", args.title, data)
                    print(f"Created outcome {node_id}")
                    return 0
                if args.outcome_cmd == "update":
                    data = load_json_arg(args.data)
                    update_node_data(conn, args.id, data)
                    print(f"Updated outcome {args.id}")
                    return 0

            if args.command == "opportunity":
                if args.opp_cmd == "list":
                    rows = list_children(
                        conn, args.outcome, "opportunity", "outcome_opportunity"
                    )
                    print_rows(rows)
                    return 0
                if args.opp_cmd == "add":
                    data = load_json_arg(args.data)
                    outcome = conn.execute(
                        "SELECT workspace_id FROM nodes WHERE id = ? AND type = 'outcome'",
                        (args.outcome,),
                    ).fetchone()
                    if outcome is None:
                        raise ValueError(f"Outcome not found: {args.outcome}")
                    node_id = add_node(
                        conn,
                        int(outcome["workspace_id"]),
                        "opportunity",
                        args.title,
                        data,
                    )
                    add_edge(conn, args.outcome, node_id, "outcome_opportunity")
                    print(f"Created opportunity {node_id}")
                    return 0

            if args.command == "solution":
                if args.sol_cmd == "list":
                    rows = list_children(
                        conn, args.opportunity, "solution", "opportunity_solution"
                    )
                    print_rows(rows)
                    return 0
                if args.sol_cmd == "add":
                    data = load_json_arg(args.data)
                    opp = conn.execute(
                        "SELECT workspace_id FROM nodes WHERE id = ? AND type = 'opportunity'",
                        (args.opportunity,),
                    ).fetchone()
                    if opp is None:
                        raise ValueError(f"Opportunity not found: {args.opportunity}")
                    node_id = add_node(
                        conn, int(opp["workspace_id"]), "solution", args.title, data
                    )
                    add_edge(conn, args.opportunity, node_id, "opportunity_solution")
                    print(f"Created solution {node_id}")
                    return 0

            if args.command == "assumption":
                if args.ass_cmd == "list":
                    rows = list_children(
                        conn, args.solution, "assumption", "solution_assumption"
                    )
                    print_rows(rows)
                    return 0
                if args.ass_cmd == "add":
                    data = load_json_arg(args.data)
                    sol = conn.execute(
                        "SELECT workspace_id FROM nodes WHERE id = ? AND type = 'solution'",
                        (args.solution,),
                    ).fetchone()
                    if sol is None:
                        raise ValueError(f"Solution not found: {args.solution}")
                    node_id = add_node(
                        conn, int(sol["workspace_id"]), "assumption", args.title, data
                    )
                    add_edge(conn, args.solution, node_id, "solution_assumption")
                    print(f"Created assumption {node_id}")
                    return 0

            if args.command == "show":
                show_tree(conn, args.outcome)
                return 0

        return 0
    except ValueError as exc:
        print(str(exc), file=sys.stderr)
        return 1


if __name__ == "__main__":
    raise SystemExit(main())

```

### references/outcomes.md

```markdown
# Outcomes Phase

Use when defining or refining outcomes for an OST.

## Inputs
- Business goal / north star
- Target segment or market
- Baseline metrics
- Time horizon
- Constraints

## Process
1) Define outcome statements (actor + behavior change + context + metric).
2) Ladder outcomes (top outcome → supporting outcomes).
3) Add metrics (baseline, target, time window).
4) Save outcomes to the OST graph.

## CLI Actions
- List outcomes: `uv run python scripts/ost.py outcome list --workspace <name>`
- Add outcome: `uv run python scripts/ost.py outcome add --workspace <name> "<title>" --data <json>`
- Update outcome: `uv run python scripts/ost.py outcome update --id <id> --data <json>`

## Output
- Updated outcome nodes with metrics and ladder links.
- Next step: opportunities.

```

### references/opportunities.md

```markdown
# Opportunities Phase

Use when discovering and ranking opportunities tied to outcomes.

## Inputs
- Selected outcome
- Target users / JTBD
- Evidence of frictions and alternatives
- Constraints

## Process
1) Write 5-10 opportunity statements.
2) Score and rank (impact, urgency, underservedness, feasibility, WTP).
3) Save top opportunities to the OST graph, linked to the outcome.

## CLI Actions
- Add opportunity: `uv run python scripts/ost.py opportunity add --outcome <id> "<title>" --data <json>`
- List opportunities: `uv run python scripts/ost.py opportunity list --outcome <id>`

## Output
- Ranked opportunity nodes linked to the outcome.
- Next step: solutions.

```

### references/solutions.md

```markdown
# Solutions Phase

Use when ideating solution concepts tied to top opportunities.

## Inputs
- Selected opportunity
- Constraints and priorities
- Differentiation requirements

## Process
1) Generate 3-5 distinct solution concepts.
2) Evaluate trade-offs (pros/cons, feasibility, differentiation).
3) Select a leading concept.
4) Save solutions to the OST graph linked to the opportunity.

## CLI Actions
- Add solution: `uv run python scripts/ost.py solution add --opportunity <id> "<title>" --data <json>`
- List solutions: `uv run python scripts/ost.py solution list --opportunity <id>`

## Output
- Solution nodes linked to the opportunity.
- Next step: assumptions/experiments.

```

### references/assumptions.md

```markdown
# Assumptions and Experiments Phase

Use when validating solution concepts with the riskiest assumptions first.

## Inputs
- Selected solution
- Evidence so far
- Constraints (time, budget, ethics)

## Process
1) List assumptions (desirability, usability, feasibility, viability, risk).
2) Prioritize by impact x uncertainty.
3) Design smallest experiments with decision thresholds.
4) Save assumptions and experiment plans to the OST graph linked to the solution.

## CLI Actions
- Add assumption: `uv run python scripts/ost.py assumption add --solution <id> "<title>" --data <json>`
- List assumptions: `uv run python scripts/ost.py assumption list --solution <id>`

## Output
- Assumption nodes linked to the solution.
- Next step: decide whether to proceed, pivot, or stop.

```