Aletheia

32. Requests, goals, story and economics

Status: implemented, 2026-10-27. This page describes the code that exists: src/hub/requests.mjs, goals-rollup.mjs, artifacts.mjs, story.mjs, economics.mjs, rule-suggest.mjs, rules-decide.mjs. Every number comes from a stored row. An empty store returns empty arrays and empty: true; there are no placeholder counts. The rule-based path needs no model. The Work and Memory tabs read these routes; the mapping is in §32.11.

32.1 Requests (T-152)

A request is a group of sessions in one project that share a goal and are close in time. The goal is the owner's own imperative prompt (goals.imperativeTopic); sessions with no imperative prompt share a no-goal key. Within a (project, goal) group, sessions become separate requests when the gap since the previous session exceeds maxGapHours (default 24).

Status is derived from rows, in this order:

StatusRule
Workingthe latest session is not ended and its last activity is within activeMinutes (default 5)
Shippedthe latest session has a recorded session-end event
Waiting on younot ended, and the latest activity is a prompt (the owner spoke last)
Idlenone of the above

Each request also carries stage (the goal object), startedAt, runningTimeMs, session/prompt/tool counts and evidence (session, prompt and tool ids). requestDetail adds whereThisCameFrom: sessions, the owner's prompts and source facts.

Dismiss and restore write an id to work.requests.dismissed (settings); nothing is deleted.

32.2 Progress and ETA (T-166)

progress is the share of a request's sessions that ended or produced a summary, capped at 99 unless the request is Shipped (then 100). progressEvidence lists the completed session ids, fact ids and summary ids.

eta is reported only when the request is not shipped, has at least two sessions with distinct starts, has at least one completed session, and has remaining sessions. The pace is the mean gap between consecutive session starts. Otherwise eta is null and etaEvidence is null.

32.3 What came out of it (T-167)

artifacts.detectArtifacts is conservative: a commit needs a commit <sha> lead-in, a full 40-hex sha, or a bracketed [branch sha]; a PR needs a GitHub pull URL or pull request #N; a branch needs a git push ... origin lead-in or a feat/fix/chore-style name. Each request carries cameOutOfIt (the findings, with evidenceId, source, sessionId and at) and outcome counts.

32.4 Goals roll-up (T-168)

proposeGoalsRollup links each request to the goal whose topic key and project match. A goal is:

StatusRule
Nothing on itno linked request
Movinga linked request is Working, Waiting on you or Shipped
Stalledlinked requests exist, all idle

It adds aggregateProgress (mean of the linked requests' progress), requests, request counts and whatCameOutOfIt (merged artifacts). noGoal holds requests serving none of the goals. openAsks splits non-shipped goals into beingWorked (Moving) and nothingWorking (Stalled or no request).

32.5 Build story (T-169)

buildStory returns counts (shipped, working, waiting on you, idle), a velocity object with perWeek (ISO week -> shipped count from stored dates), averageDaysToShip, and learned: repeated corrections in the owner's prompts, grouped by rule text with timesSeen, latestAt and dated quotes.

32.6 Context economics (T-170)

economics lists context memories (kind observation, summary or note) with readTokens from one documented counter, estimateTokens: ceil(text.length / 4). Per-observation workTokens is null, labelled "usage is recorded per extraction call, not per observation". Totals take work tokens from fact_usage input + output, or null with "no usage record". Savings is only computed when work tokens exist and exceed zero.

32.7 Rule strength and decide flow (T-171, T-172)

Each suggestion stores times_seen, latest_at_epoch and dated rows in rule_suggestion_quotes. Strength is documented in code: Light at 1-2, Steady at 3-4, Strong at 5+. listSuggestions returns timesSeen, strength, latestAt, quotes and moreQuotes (quotes beyond the first two).

The decide flow approves, edits-then-approves or rejects a suggestion, returns decided counts, toggles an active rule on or off, records the last learning run, and records replaced_by / replaced_at_epoch when one rule replaces another. Nothing here deletes a suggestion or a rule.

32.8 Query params (T-173)

GET /v1/work/requests accepts filter (all, needs-you, shipped), sort (needs-you, newest, most-progress) and group (agent, tool). counts always describes the whole set: all, needsYou, shipped, working, idle, waitingOnYou, total. group returns groups buckets keyed by the request's latest tool.

32.9 Routes

RouteMethodWhat
/v1/work/requestsGETthe request list; ?project=&filter=&sort=&group=&maxGapHours=&activeMinutes=&limit=
/v1/work/requests/:idGETone request plus whereThisCameFrom; 404 for an unknown or dismissed id
/api/hub/requests/:idPOST`{action: "dismiss" \"restore"}` (reversible)
/v1/work/goalsGETthe goals roll-up
/v1/work/storyGETcounts, velocity and learned habits
/v1/context/economicsGETper-observation read tokens and work tokens
/api/hub/rule-suggestionsGETsuggestions with strength, quotes, decided counts
/api/hub/rule-suggestions/:idPOST`{action: "approve" \"edit" \"reject" \"restore", text?}`
/api/hub/rules/:id/togglePOST{enabled: boolean}
/api/hub/rules/:id/replacePOST{then, when?, scopeTier?, scopeName?}
/api/hub/rules/learning-runGET, POSTread, or run suggestRules and record the run

CLI: aletheia context economics [--project p] [--limit n].

32.10 Real sample (seeded temp database, 2026-10-27)

The seed has five requests in project shop: one Working, one Shipped (with a commit, PR and branch), one Idle and two Waiting on you. GET /v1/work/requests?project=shop (abbreviated):

{
  "project": "shop",
  "filter": "all",
  "sort": "newest",
  "group": null,
  "counts": { "all": 5, "needsYou": 2, "shipped": 1, "working": 1, "idle": 1, "waitingOnYou": 2, "total": 5 },
  "requests": [
    {
      "id": "02a99a159fe3462b", "project": "shop", "tool": "claude-code",
      "goalKey": "build retry button", "status": "Shipped", "progress": 100, "eta": null,
      "cameOutOfIt": [
        { "kind": "pull", "value": "#42", "number": 42, "url": "https://github.com/acme/shop/pull/42", "evidenceId": 4, "sessionId": "s1" },
        { "kind": "commit", "value": "abc1234", "sha": "abc1234", "evidenceId": 2, "sessionId": "s1" },
        { "kind": "branch", "value": "feat/retry-button", "name": "feat/retry-button", "evidenceId": 3, "sessionId": "s1" }
      ],
      "outcome": { "commits": 1, "pulls": 1, "branches": 1 }
    }
  ],
  "empty": false
}

GET /v1/work/requests/:id adds whereThisCameFrom.sessions[], each with prompts[] and facts[] (ids and times).

GET /v1/work/goals?project=shop (abbreviated):

{
  "goals": [
    { "object": "onboarding guide", "status": "Moving", "aggregateProgress": 0, "requestCount": 1 },
    { "object": "retry button checkout", "status": "Moving", "aggregateProgress": 100, "requestCount": 1 },
    { "object": "retry latency", "status": "Stalled", "aggregateProgress": 0, "requestCount": 1 }
  ],
  "noGoal": { "count": 2 },
  "openAsks": { "total": 2, "beingWorked": [ { "object": "onboarding guide" } ], "nothingWorking": [ { "object": "retry latency" } ] }
}

GET /v1/work/story?project=shop:

{
  "counts": { "shipped": 1, "working": 1, "waitingOnYou": 2, "idle": 1, "total": 5 },
  "velocity": { "totalShipped": 1, "averageDaysToShip": 0, "perWeek": [ { "week": "2026-W44", "shipped": 1 } ] },
  "learned": [
    { "text": "Use pnpm, not npm", "timesSeen": 2, "latestAt": "2026-10-26T11:15:00.000Z",
      "quotes": [ { "text": "Use pnpm, not npm.", "at": "2026-10-26T10:35:00.000Z" } ] }
  ]
}

GET /v1/context/economics?project=shop (abbreviated):

{
  "counter": "estimateTokens: ceil(text.length / 4)",
  "observations": [
    { "id": 17, "kind": "observation", "readTokens": 28, "workTokens": null, "workTokensSource": "usage is recorded per extraction call, not per observation" }
  ],
  "totals": { "observations": 2, "readTokens": 45, "workTokens": 1500, "workTokensSource": "fact_usage input + output tokens", "savingsPercent": 97 }
}

GET /api/hub/rule-suggestions?project=shop (abbreviated):

{
  "total": 1, "decided": 0, "pending": 1,
  "items": [
    { "id": 1, "text": "Use pnpm, not npm", "timesSeen": 2, "strength": "Light",
      "latestAt": "2026-10-26T11:15:00.000Z", "moreQuotes": 0,
      "quotes": [ { "text": "No, use pnpm instead of npm.", "at": "2026-10-26T11:15:00.000Z", "sessionId": "s2" } ] }
  ]
}

GET /api/hub/rules/learning-run:

{ "learningRun": { "at": "2026-10-27T10:10:44.419Z", "found": 2, "proposed": 1, "errors": 0 } }

GET /v1/work/requests?project=shop&filter=needs-you&sort=most-progress&group=tool (abbreviated):

{
  "filter": "needs-you", "sort": "most-progress", "group": "tool",
  "requests": [ { "id": "c7f3fad4a28d22a2", "status": "Waiting on you", "progress": 0 } ],
  "groups": [ { "key": "claude-code", "count": 1 }, { "key": "codex", "count": 1 } ]
}

32.11 UI wiring (T-218 to T-222)

The UI is wired; no new tab was added. The existing modules changed shape:

RouteWhere it appears
GET /v1/work/requestsWork tab, Requests section: filter, sort and group controls, one card per request
GET /v1/work/requests/:id#/work/request/<id>: where this came from, with links to the session page
POST /api/hub/requests/:idWork tab: Dismiss with Undo; dismissed requests have Restore
GET /v1/work/goalsWork tab, Goals section: roll-up, open asks and the no-goal bucket
GET /v1/work/storyWork tab, Build story section
GET /v1/context/economicsMemory tab, Economics page
GET/POST /api/hub/rules/learning-runRules tab, Suggested page: last run and Run learning now
POST /api/hub/rule-suggestions/:idRules tab: Approve, Edit, Reject and Restore
POST /api/hub/rules/:id/toggleRules tab: Turn on or off, optimistic with Undo
POST /api/hub/rules/:id/replaceRules tab: Replace, which says the old rule is kept

Progress, ETA, request grouping, goal progress, rule strength and read tokens carry an "estimated" link to section 32.12. The request route has no offset, so the Work tab offers a page size and the server's counts, not a pager that would repeat the same rows.

32.12 What is not done

32.13 Sources