Skip to content

economics.tasks

One record per task, folded from the ledger of every checkout `git worktree list` reports and the archives a ledger rotation left: its issue and intent, checkout and branch, first start, last finish or refusal, wall seconds, refusal count, and the episodes that carried it with their spans. Interventions, CI reruns, spend and acceptance are present on every record as not_observed, each with the reason and the issue that will supply it. Lists every ledger read with its line and skipped-line counts and every checkout not read; an optional since/until window filters by each task's start.

One record per task, folded from the ledger of every checkout `git worktree list` reports and the archives a ledger rotation left: its issue and intent, checkout and branch, first start, last finish or refusal, wall seconds, refusal count, and the episodes that carried it with their spans. Interventions, CI reruns, spend and acceptance are present on every record as not_observed, each with the reason and the issue that will supply it. Lists every ledger read with its line and skipped-line counts and every checkout not read; an optional since/until window filters by each task's start.

Exposure

Where this one definition is reachable. Absence is explicit: a surface not listed does not carry it.

surfaceas
MCPtool majordomus_economics_tasks
HTTPGET /api/v1/economics/tasks operationId economics.tasks
command linemajordomus economics tasks

Schemas

The canonical input and output, from the Rust types; the MCP tool schema and the OpenAPI parameters and responses are derived from these.

input · TaskEconomicsQuery

Which tasks to answer. A window filters by each task's task.started time, so a task with no start line in the ledgers read is answered only when no window is given.

use majordomus_cli::economics::tasks::TaskEconomicsQuery;
let q: TaskEconomicsQuery = serde_json::from_str(r#"{"since":"2026-10-21T00:00:00Z"}"#).unwrap();
assert_eq!(q.since.as_deref(), Some("2026-10-21T00:00:00Z"));
assert!(q.until.is_none());
assert_eq!(serde_json::from_str::<TaskEconomicsQuery>("{}").unwrap(), TaskEconomicsQuery::default());
// a misspelt bound is refused, never read as no bound
assert!(serde_json::from_str::<TaskEconomicsQuery>(r#"{"after":"x"}"#).is_err());
propertytyperequireddescription
since string | null no Answer only tasks started at or after this time (YYYY-MM-DDTHH:MM:SSZ).
until string | null no Answer only tasks started before this time (YYYY-MM-DDTHH:MM:SSZ).
JSON Schema
{
  "additionalProperties": false,
  "description": "Which tasks to answer. A window filters by each task's `task.started` time, so a task\nwith no start line in the ledgers read is answered only when no window is given.\n\n```\nuse majordomus_cli::economics::tasks::TaskEconomicsQuery;\nlet q: TaskEconomicsQuery = serde_json::from_str(r#\"{\"since\":\"2026-10-21T00:00:00Z\"}\"#).unwrap();\nassert_eq!(q.since.as_deref(), Some(\"2026-10-21T00:00:00Z\"));\nassert!(q.until.is_none());\nassert_eq!(serde_json::from_str::<TaskEconomicsQuery>(\"{}\").unwrap(), TaskEconomicsQuery::default());\n// a misspelt bound is refused, never read as no bound\nassert!(serde_json::from_str::<TaskEconomicsQuery>(r#\"{\"after\":\"x\"}\"#).is_err());\n```",
  "properties": {
    "since": {
      "description": "Answer only tasks started at or after this time (`YYYY-MM-DDTHH:MM:SSZ`).",
      "type": [
        "string",
        "null"
      ]
    },
    "until": {
      "description": "Answer only tasks started before this time (`YYYY-MM-DDTHH:MM:SSZ`).",
      "type": [
        "string",
        "null"
      ]
    }
  },
  "title": "TaskEconomicsQuery",
  "type": "object"
}

output · TaskEconomicsList

Every task the ledgers read carry, and what was read to answer it.

use majordomus_cli::economics::tasks::{fold, TaskEconomicsList, TaskEconomicsQuery, TaskEconomicsSource};
let text = r#"{"ts":"2026-10-21T10:00:00Z","event":"task.started","head":"h","branch":"b","by":"me","session":"s-1","task_id":"t-1"}"#;
let source = TaskEconomicsSource::parse("repo", "ledger.jsonl", text);
let list: TaskEconomicsList = fold(vec![source], &TaskEconomicsQuery::default());
assert_eq!((list.count, list.tasks.len()), (1, 1));
assert_eq!(list.ledgers[0].path, "ledger.jsonl");
assert!(list.unread.is_empty(), "fold reads no checkout, so it names none unread");
propertytyperequireddescription
count integer yes How many tasks are answered: the number of distinct task ids within the window.
ledgers array yes Every ledger file read, with its line and skipped-line counts.
tasks array yes One record per task, in the order the tasks started.
unread array yes Every checkout whose ledger could not be read, and why.
window reference yes The window the tasks were filtered by, as given.
JSON Schema
{
  "$defs": {
    "TaskEconomicsEpisode": {
      "description": "One episode that carried a task, with the span of the lines it wrote about it.\n\n```\nuse majordomus_cli::economics::tasks::{fold, TaskEconomicsEpisode, TaskEconomicsSource};\nlet line = |ts: &str, event: &str, session: &str| {\n    format!(r#\"{{\"ts\":\"{ts}\",\"event\":\"{event}\",\"head\":\"h\",\"branch\":\"b\",\"by\":\"me\",\"session\":\"{session}\",\"task_id\":\"t-1\"}}\"#)\n};\nlet text = [\n    line(\"2026-10-21T10:00:00Z\", \"task.started\", \"s-1\"),\n    line(\"2026-10-21T11:00:00Z\", \"task.finished\", \"s-2\"),\n]\n.join(\"\\n\");\nlet list = fold(vec![TaskEconomicsSource::parse(\"repo\", \"ledger.jsonl\", &text)], &Default::default());\nlet episodes: &[TaskEconomicsEpisode] = &list.tasks[0].episodes;\nassert_eq!(episodes.len(), 2, \"two session stamps are two episodes\");\nassert_eq!((episodes[1].session.as_str(), episodes[1].lines), (\"s-2\", 1));\nassert_eq!(episodes[1].first_at, \"2026-10-21T11:00:00Z\");\n```",
      "properties": {
        "first_at": {
          "description": "The time of the episode's first line about the task.",
          "type": "string"
        },
        "last_at": {
          "description": "The time of the episode's last line about the task.",
          "type": "string"
        },
        "lines": {
          "description": "How many of the episode's lines are about the task.",
          "format": "uint",
          "minimum": 0,
          "type": "integer"
        },
        "provider": {
          "description": "The provider that ran the episode, when the ledger names it. Left out of the answer\nwhen it does not, so it reads back as empty: without `default` the text form, which\nreads the capability's JSON, refused every episode whose provider is unknown.",
          "type": "string"
        },
        "session": {
          "description": "The episode's session id.",
          "type": "string"
        }
      },
      "required": [
        "session",
        "first_at",
        "last_at",
        "lines"
      ],
      "type": "object"
    },
    "TaskEconomicsField": {
      "description": "A field of the record that nothing observes yet, carrying the reason and the issue that\nwill supply it in place of a value.\n\n```\nuse majordomus_cli::economics::tasks::{fold, TaskEconomicsField, TaskEconomicsSource};\nlet text = r#\"{\"ts\":\"2026-10-21T10:00:00Z\",\"event\":\"task.started\",\"head\":\"h\",\"branch\":\"b\",\"by\":\"me\",\"task_id\":\"t-1\"}\"#;\nlet list = fold(vec![TaskEconomicsSource::parse(\"repo\", \"ledger.jsonl\", text)], &Default::default());\nlet field: &TaskEconomicsField = &list.tasks[0].ci_reruns;\nlet v = serde_json::to_value(field).unwrap();\nassert_eq!(v[\"status\"], \"not_observed\");\nassert!(field.reason.contains(\"I3378\"), \"{}\", field.reason);\n```",
      "properties": {
        "reason": {
          "description": "Why there is no value, naming what will supply it.",
          "type": "string"
        },
        "status": {
          "$ref": "#/$defs/TaskEconomicsFieldStatus",
          "description": "Always `not_observed` until the field's supplier lands."
        }
      },
      "required": [
        "status",
        "reason"
      ],
      "type": "object"
    },
    "TaskEconomicsFieldStatus": {
      "description": "Whether a field holds an observation. Its one variant says there is none, which is not a\nzero.\n\n```\nuse majordomus_cli::economics::tasks::TaskEconomicsFieldStatus;\nlet v = serde_json::to_value(TaskEconomicsFieldStatus::NotObserved).unwrap();\nassert_eq!(v, \"not_observed\");\n```",
      "oneOf": [
        {
          "const": "not_observed",
          "description": "Nothing observes the field yet: there is no value, which is not a zero.",
          "type": "string"
        }
      ]
    },
    "TaskEconomicsLedger": {
      "description": "One ledger file that was read, with how many of its lines were entries and how many\ncould not be read as one.\n\n```\nuse majordomus_cli::economics::tasks::{TaskEconomicsLedger, TaskEconomicsSource};\nlet s = TaskEconomicsSource::parse(\"repo\", \".ai/local/state/ledger.jsonl\", \"{}\\nnot json\\n\");\nlet ledger: &TaskEconomicsLedger = &s.ledgers[0];\nassert_eq!(ledger.checkout, \"repo\");\nassert_eq!(ledger.path, \".ai/local/state/ledger.jsonl\");\nassert_eq!((ledger.lines, ledger.skipped), (0, 2), \"neither line is a ledger entry\");\n```",
      "properties": {
        "checkout": {
          "description": "The checkout the ledger belongs to.",
          "type": "string"
        },
        "lines": {
          "description": "How many lines were read as ledger entries.",
          "format": "uint",
          "minimum": 0,
          "type": "integer"
        },
        "path": {
          "description": "The file, relative to the checkout.",
          "type": "string"
        },
        "skipped": {
          "description": "How many non-empty lines could not be read as ledger entries.",
          "format": "uint",
          "minimum": 0,
          "type": "integer"
        }
      },
      "required": [
        "checkout",
        "path",
        "lines",
        "skipped"
      ],
      "type": "object"
    },
    "TaskEconomicsQuery": {
      "additionalProperties": false,
      "description": "Which tasks to answer. A window filters by each task's `task.started` time, so a task\nwith no start line in the ledgers read is answered only when no window is given.\n\n```\nuse majordomus_cli::economics::tasks::TaskEconomicsQuery;\nlet q: TaskEconomicsQuery = serde_json::from_str(r#\"{\"since\":\"2026-10-21T00:00:00Z\"}\"#).unwrap();\nassert_eq!(q.since.as_deref(), Some(\"2026-10-21T00:00:00Z\"));\nassert!(q.until.is_none());\nassert_eq!(serde_json::from_str::<TaskEconomicsQuery>(\"{}\").unwrap(), TaskEconomicsQuery::default());\n// a misspelt bound is refused, never read as no bound\nassert!(serde_json::from_str::<TaskEconomicsQuery>(r#\"{\"after\":\"x\"}\"#).is_err());\n```",
      "properties": {
        "since": {
          "description": "Answer only tasks started at or after this time (`YYYY-MM-DDTHH:MM:SSZ`).",
          "type": [
            "string",
            "null"
          ]
        },
        "until": {
          "description": "Answer only tasks started before this time (`YYYY-MM-DDTHH:MM:SSZ`).",
          "type": [
            "string",
            "null"
          ]
        }
      },
      "type": "object"
    },
    "TaskEconomicsRecord": {
      "description": "One task's delivery economics: who carried it, when, how often `finish` refused it, and\nthe fields nothing observes yet, each saying why it has no value.\n\n```\nuse majordomus_cli::economics::tasks::{\n    fold, TaskEconomicsRecord, TaskEconomicsSource, TaskEconomicsState,\n};\nlet text = r#\"{\"ts\":\"2026-10-21T10:00:00Z\",\"event\":\"task.started\",\"head\":\"h\",\"branch\":\"b\",\n    \"by\":\"me\",\"session\":\"s-1\",\"task_id\":\"t-1\",\"issue\":\"I3376\"}\"#;\nlet source = TaskEconomicsSource::parse(\"repo\", \"ledger.jsonl\", &text.replace('\\n', \"\"));\nlet list = fold(vec![source], &Default::default());\nlet r: &TaskEconomicsRecord = &list.tasks[0];\nassert_eq!((r.task_id.as_str(), r.branch.as_str()), (\"t-1\", \"b\"));\nassert_eq!(r.state, TaskEconomicsState::Open);\nassert_eq!(r.outcome, \"active\");\nassert_eq!(r.wall_seconds, None, \"an open task with no refusal has no end\");\nassert_eq!(r.issue.as_deref(), Some(\"I3376\"));\nassert!(r.spend.reason.contains(\"I3379\"));\n```",
      "properties": {
        "accepted": {
          "$ref": "#/$defs/TaskEconomicsField",
          "description": "Whether the task's result was accepted."
        },
        "branch": {
          "description": "The branch the task started on.",
          "type": "string"
        },
        "checkout": {
          "description": "The checkout whose ledger carries the task.",
          "type": "string"
        },
        "ci_reruns": {
          "$ref": "#/$defs/TaskEconomicsField",
          "description": "CI runs of the task's work that were rerun."
        },
        "ended_at": {
          "description": "The time of the task's last `task.finished` line, or of its last `task.refused` line\nwhile it has none.",
          "type": [
            "string",
            "null"
          ]
        },
        "episodes": {
          "description": "The episodes that carried the task, each with the span of its lines about it.",
          "items": {
            "$ref": "#/$defs/TaskEconomicsEpisode"
          },
          "type": "array"
        },
        "intent": {
          "description": "The intent the task was started under, when it named one.",
          "type": [
            "string",
            "null"
          ]
        },
        "interventions": {
          "$ref": "#/$defs/TaskEconomicsField",
          "description": "Human interventions during the task."
        },
        "issue": {
          "description": "The issue the task was started under, when it named one.",
          "type": [
            "string",
            "null"
          ]
        },
        "outcome": {
          "description": "The outcome as recorded: the finish's outcome, or `active` / `handed_over` while open.",
          "type": "string"
        },
        "refusals": {
          "description": "How many times `finish` refused the task.",
          "format": "uint32",
          "minimum": 0,
          "type": "integer"
        },
        "spend": {
          "$ref": "#/$defs/TaskEconomicsField",
          "description": "What the task cost."
        },
        "started_at": {
          "description": "The time of the task's first `task.started` line.",
          "type": [
            "string",
            "null"
          ]
        },
        "state": {
          "$ref": "#/$defs/TaskEconomicsState",
          "description": "`finished` once a `task.finished` line is read, `open` otherwise."
        },
        "task_id": {
          "description": "The task's id, as `start` wrote it.",
          "type": "string"
        },
        "wall_seconds": {
          "description": "Seconds from `started_at` to `ended_at`; absent while either is.",
          "format": "int64",
          "type": [
            "integer",
            "null"
          ]
        }
      },
      "required": [
        "task_id",
        "checkout",
        "branch",
        "state",
        "outcome",
        "refusals",
        "episodes",
        "interventions",
        "ci_reruns",
        "spend",
        "accepted"
      ],
      "type": "object"
    },
    "TaskEconomicsState": {
      "description": "Whether a task has finished, as the command line, MCP and HTTP spell it.\n\n```\nuse majordomus_cli::economics::tasks::TaskEconomicsState;\nassert_eq!(serde_json::to_value(TaskEconomicsState::Open).unwrap(), \"open\");\nassert_eq!(serde_json::to_value(TaskEconomicsState::Finished).unwrap(), \"finished\");\n```",
      "oneOf": [
        {
          "const": "open",
          "description": "No `task.finished` line was read for the task.",
          "type": "string"
        },
        {
          "const": "finished",
          "description": "A `task.finished` line was read for the task.",
          "type": "string"
        }
      ]
    },
    "TaskEconomicsUnread": {
      "description": "A checkout whose ledger was not read, and the reason, so a missing ledger is never read\nas a checkout with no tasks.\n\n```\nuse majordomus_cli::economics::tasks::{tasks, TaskEconomicsQuery, TaskEconomicsUnread};\nlet dir = tempfile::tempdir().unwrap();\nlet list = tasks(dir.path(), &TaskEconomicsQuery::default());\nlet unread: &TaskEconomicsUnread = &list.unread[0];\nassert_eq!(unread.checkout, dir.path().display().to_string());\nassert_eq!(unread.reason, \"no ledger under .ai/local/state\");\n```",
      "properties": {
        "checkout": {
          "description": "The checkout.",
          "type": "string"
        },
        "reason": {
          "description": "Why its ledger was not read.",
          "type": "string"
        }
      },
      "required": [
        "checkout",
        "reason"
      ],
      "type": "object"
    }
  },
  "description": "Every task the ledgers read carry, and what was read to answer it.\n\n```\nuse majordomus_cli::economics::tasks::{fold, TaskEconomicsList, TaskEconomicsQuery, TaskEconomicsSource};\nlet text = r#\"{\"ts\":\"2026-10-21T10:00:00Z\",\"event\":\"task.started\",\"head\":\"h\",\"branch\":\"b\",\"by\":\"me\",\"session\":\"s-1\",\"task_id\":\"t-1\"}\"#;\nlet source = TaskEconomicsSource::parse(\"repo\", \"ledger.jsonl\", text);\nlet list: TaskEconomicsList = fold(vec![source], &TaskEconomicsQuery::default());\nassert_eq!((list.count, list.tasks.len()), (1, 1));\nassert_eq!(list.ledgers[0].path, \"ledger.jsonl\");\nassert!(list.unread.is_empty(), \"fold reads no checkout, so it names none unread\");\n```",
  "properties": {
    "count": {
      "description": "How many tasks are answered: the number of distinct task ids within the window.",
      "format": "uint",
      "minimum": 0,
      "type": "integer"
    },
    "ledgers": {
      "description": "Every ledger file read, with its line and skipped-line counts.",
      "items": {
        "$ref": "#/$defs/TaskEconomicsLedger"
      },
      "type": "array"
    },
    "tasks": {
      "description": "One record per task, in the order the tasks started.",
      "items": {
        "$ref": "#/$defs/TaskEconomicsRecord"
      },
      "type": "array"
    },
    "unread": {
      "description": "Every checkout whose ledger could not be read, and why.",
      "items": {
        "$ref": "#/$defs/TaskEconomicsUnread"
      },
      "type": "array"
    },
    "window": {
      "$ref": "#/$defs/TaskEconomicsQuery",
      "description": "The window the tasks were filtered by, as given."
    }
  },
  "required": [
    "count",
    "window",
    "tasks",
    "ledgers",
    "unread"
  ],
  "title": "TaskEconomicsList",
  "type": "object"
}

Policies

benchmark
required — a target on every transport the exposure declares; the cases are the input type's
cache
disabled — every call runs the handler

Benchmark targets

Derived from the registry for this repository: one requirement per transport, and the cases the input type provides. The whole matrix is on the benchmarks page.

transportstatecasestargets
directcovered2all
window
mcpcovered2all
window
httpcovered2all
window

Searches the titles and descriptions of every page this site publishes.