toolroll 0.9.91 → 0.9.92

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/approval-authority.d.ts +9 -1
  3. package/dist/approval-authority.js +15 -2
  4. package/dist/build-review.js +2 -0
  5. package/dist/chat-actions.d.ts +11 -3
  6. package/dist/chat-actions.js +47 -2
  7. package/dist/chat-decide.js +2 -0
  8. package/dist/chat-review.d.ts +2 -1
  9. package/dist/chat-review.js +2 -2
  10. package/dist/contracts/chat-actions.d.ts +10 -0
  11. package/dist/contracts/chat-actions.js +4 -0
  12. package/dist/contracts/console-api.d.ts +1 -0
  13. package/dist/contracts/console-api.js +3 -0
  14. package/dist/contracts/route-results.d.ts +121 -0
  15. package/dist/contracts/route-results.js +71 -0
  16. package/dist/contracts/route.d.ts +343 -5
  17. package/dist/contracts/route.js +36 -7
  18. package/dist/contracts/store-json.d.ts +2 -0
  19. package/dist/dispose.js +6 -0
  20. package/dist/flow-engine.js +1 -1
  21. package/dist/flow-pull-request.js +1 -1
  22. package/dist/lead-doors.js +4 -2
  23. package/dist/lead-tools.js +10 -2
  24. package/dist/local-owner.d.ts +1 -1
  25. package/dist/local-owner.js +11 -0
  26. package/dist/memory-cli.d.ts +3 -0
  27. package/dist/memory-cli.js +3 -3
  28. package/dist/memory-pass.js +5 -1
  29. package/dist/model-choices.d.ts +7 -3
  30. package/dist/model-choices.js +26 -11
  31. package/dist/models-cli.d.ts +205 -0
  32. package/dist/models-cli.js +143 -4
  33. package/dist/models-ui.d.ts +57 -0
  34. package/dist/models-ui.js +44 -1
  35. package/dist/operate-remote-arguments.js +4 -4
  36. package/dist/operate-remote.js +1 -1
  37. package/dist/operate.js +53 -5
  38. package/dist/phase-routing.d.ts +82 -2
  39. package/dist/phase-routing.js +132 -18
  40. package/dist/project-delete.d.ts +4 -0
  41. package/dist/project-delete.js +12 -4
  42. package/dist/project-knowledge.d.ts +6 -6
  43. package/dist/project-memory.d.ts +8 -0
  44. package/dist/project-memory.js +20 -1
  45. package/dist/pull-request-flow.js +1 -1
  46. package/dist/release-auto.js +12 -3
  47. package/dist/result-actions.d.ts +4 -0
  48. package/dist/result-actions.js +3 -0
  49. package/dist/route-hooks.d.ts +54 -0
  50. package/dist/route-hooks.js +179 -0
  51. package/dist/route-results.d.ts +70 -0
  52. package/dist/route-results.js +340 -0
  53. package/dist/route-rules.d.ts +324 -0
  54. package/dist/route-rules.js +781 -0
  55. package/dist/route-savings.d.ts +136 -0
  56. package/dist/route-savings.js +193 -0
  57. package/dist/route-store.d.ts +300 -0
  58. package/dist/route-store.js +598 -0
  59. package/dist/scope.d.ts +3 -1
  60. package/dist/scope.js +4 -1
  61. package/dist/server/render-chat.js +2 -2
  62. package/dist/server/render-tasks.d.ts +7 -0
  63. package/dist/server/render-tasks.js +13 -0
  64. package/dist/server/route-table.js +12 -0
  65. package/dist/server/settings.d.ts +2 -1
  66. package/dist/server/settings.js +87 -1
  67. package/dist/server/task-views.js +6 -0
  68. package/dist/server/tasks.js +3 -0
  69. package/dist/settings-proofs.js +2 -0
  70. package/dist/setup-guide.d.ts +2 -2
  71. package/dist/spend-ui.d.ts +11 -0
  72. package/dist/spend-ui.js +49 -2
  73. package/dist/store.d.ts +38 -2
  74. package/dist/store.js +320 -35
  75. package/dist/surface.js +16 -4
  76. package/dist/task-outcome-cli.js +5 -2
  77. package/dist/toolroll-update.js +3 -2
  78. package/package.json +1 -1
package/dist/models-ui.js CHANGED
@@ -59,7 +59,7 @@ ${formRow("Automatic checks", `${view.watch.enabled ? "On" : "Off"}. Every 6 hou
59
59
  : note(`Checked ${when(view.watch.checkedAt)}.`);
60
60
  const fresh = view.fresh.length === 0 ? "" : sec("new", "New models", "", view.fresh.map(model => formRow(model.name, `${priceWords(model)}${model.releasedAt === null ? "" : ` · released ${model.releasedAt.slice(0, 10)}`}`, chip("New", "", { dot: false }), "inline")));
61
61
  const chat = view.chat === null ? "" : formRow("Chat", view.chat.words, btn("Change", { sm: true, variant: "ghost", href: "/settings/lead" }), "inline");
62
- const body = html `<div class="models">${outcome}${preview}${choiceSection(view, manage, previewing)}${recentSection(view)}${sec("tools", "AI tools on this computer", "", html `${tools}${checks}`)}${fresh}${chat}</div>`;
62
+ const body = html `<div class="models">${outcome}${preview}${choiceSection(view, manage, previewing)}${suggestedSection(view)}${recentSection(view)}${sec("tools", "AI tools on this computer", "", html `${tools}${checks}`)}${fresh}${chat}</div>`;
63
63
  return settingsShell({ group: "agents", sub: true, title: "Models", lead: "Which model plans, builds and reviews each task. Changes apply to tasks filed from now on.", body });
64
64
  }
65
65
  /** Filters one long picker as you type; the saved choice always stays listed. */
@@ -123,3 +123,46 @@ function recentSection(view) {
123
123
  return "";
124
124
  return sec("recent", "Recent work", "", view.recent.map(one => formRow(one.role, `Chosen ${one.chosen} → ran on ${one.ranOn}`, html `<a class="k-link k-mono" href="/t/${encodeURIComponent(one.taskId)}">${one.taskId}</a>`, "inline")));
125
125
  }
126
+ /** Settings → Models → Suggested (docs/plans/results-routing.md slice 2): what a project's own results suggest, each
127
+ * reviewed before it's saved with a step-up; the rules in force with Turn off (Keep or Drop when paused); and the
128
+ * project's "Suggest cheaper models" switch. Turning off and Not now only take authority away: no password. */
129
+ function suggestedSection(view) {
130
+ const s = view.suggested;
131
+ if (s === undefined)
132
+ return "";
133
+ const manage = view.csrf !== "";
134
+ const scoped = (repo) => `/settings/models?scope=${encodeURIComponent(repo)}`;
135
+ const review = s.review === null ? ""
136
+ : "stale" in s.review
137
+ ? html `${notice("This suggestion is out of date: its results, a price or Models changed since. Nothing was saved.")}${acts(btn("Back", { sm: true, href: `${scoped(s.review.repo)}#models-suggested` }))}`
138
+ : html `<div class="models-diff">${factList([
139
+ ["Change", s.review.title],
140
+ ["Evidence", html `${s.review.evidence.map(line => html `<span>${line}</span><br>`)}`],
141
+ ["Prices", s.review.prices],
142
+ ["Switches back", html `${s.review.terms.map(line => html `<span>${line}</span><br>`)}`],
143
+ ])}${manage ? postForm("/settings/models/accept", html `${note(`Every task is still approved, and keeps the model it was approved with. ${termsOf("step-up", factsOf(view))}`)}${password(view, "models-accept-pw")}${acts(btn(s.review.save, { variant: "primary", type: "submit" }), btn("Cancel", { variant: "ghost", href: `${scoped(s.review.repo)}#models-suggested` }))}`, { hidden: { scope: s.review.repo, id: s.review.id }, attrs: { class: "models-editor" } }) : ""}</div>`;
144
+ const offers = s.offers.map(one => formRow(s.repo === null ? `${one.title} · ${one.project}` : one.title, one.words, manage
145
+ ? acts(btn("Review change", { sm: true, href: `${scoped(one.repo)}&suggestion=${encodeURIComponent(one.id)}#models-suggested` }), postForm("/settings/models/not-now", btn("Not now", { sm: true, variant: "ghost", type: "submit" }), { hidden: { scope: one.repo, id: one.id } }))
146
+ : html ``, "top"));
147
+ const rules = s.rules.map(one => formRow(s.repo === null ? `${one.title} · ${one.project}` : one.title, one.words, !manage || one.state === "ended" ? html ``
148
+ : one.state === "paused"
149
+ ? acts(postForm("/settings/models/keep", html `${password(view, `models-keep-${one.id}`)}${btn("Keep", { sm: true, type: "submit" })}`, { hidden: { scope: one.repo, rule: String(one.id) } }), postForm("/settings/models/turn-off", btn("Drop", { sm: true, variant: "ghost", type: "submit" }), { hidden: { scope: one.repo, rule: String(one.id) } }))
150
+ : postForm("/settings/models/turn-off", btn("Turn off", { sm: true, type: "submit" }), { hidden: { scope: one.repo, rule: String(one.id) } }), "top"));
151
+ // Trials (slice 3): an offer to try a cheaper model on the next few tasks — started with a step-up, each task still
152
+ // approved by a person — and the trials in use, each with its progress and Stop (stopping only lowers: no password).
153
+ const trialOffers = (s.trialOffers ?? []).map(one => formRow(s.repo === null ? `${one.title} · ${one.project}` : one.title, one.words, manage
154
+ ? acts(postForm("/settings/models/try", html `${password(view, `models-try-${one.id.replace(/[^a-z0-9]/gi, "-")}`)}${btn(one.save, { sm: true, type: "submit" })}`, { hidden: { scope: one.repo, id: one.id } }), postForm("/settings/models/not-now", btn("Not now", { sm: true, variant: "ghost", type: "submit" }), { hidden: { scope: one.repo, id: one.id } }))
155
+ : html ``, "top"));
156
+ const trials = (s.trials ?? []).map(one => formRow(s.repo === null ? `${one.title} · ${one.project}` : one.title, one.words, !manage || !one.live ? html ``
157
+ : postForm("/settings/models/stop", btn("Stop", { sm: true, type: "submit" }), { hidden: { scope: one.repo, rule: String(one.id) } }), "top"));
158
+ const toggle = s.suggest === null || s.repo === null ? "" : formRow("Suggest cheaper models", s.suggest.on
159
+ ? "On. When a cheaper model does as well on this project's small or routine tasks, it's suggested here. Nothing changes until you accept it."
160
+ : "Off. No new suggestions for this project.", !manage ? html `` : s.suggest.on
161
+ ? postForm("/settings/models/suggest-off", btn("Turn off", { sm: true, type: "submit" }), { hidden: { scope: s.repo } })
162
+ : postForm("/settings/models/suggest-on", html `${password(view, "models-suggest-pw")}${btn("Turn on", { sm: true, type: "submit" })}`, { hidden: { scope: s.repo } }), "inline");
163
+ const emptyWords = s.repo === null ? "No suggestions yet. Choose a project above to see its own." : "No suggestions yet. When a cheaper model's results on this project's small or routine tasks match the configured model's, it's suggested here.";
164
+ const nothing = offers.length === 0 && rules.length === 0 && trialOffers.length === 0 && trials.length === 0 && s.review === null;
165
+ const body = html `${review}${nothing ? note(emptyWords) : ""}${offers}${trialOffers}${rules.length === 0 ? "" : html `${group("Rules in use")}${rules}`}${trials.length === 0 ? "" : html `${group("Trials")}${trials}`}${toggle}`;
166
+ const waiting = s.offers.length + trialOffers.length;
167
+ return sec("suggested", "Suggested", waiting === 0 ? "" : `${waiting} to review`, body);
168
+ }
@@ -57,10 +57,10 @@ export const REMOTE_ARGUMENTS = new Map([
57
57
  ['template show', args('name')],
58
58
  ...['search', 'impact'].map(action => [`knowledge ${action}`, args('query', 'base')]),
59
59
  ['knowledge refresh', args('', 'base')],
60
- // Decision reads and supersession are already project-bound in project-memory.ts. Proposal IDs are
61
- // accepted only by memory apply (step-up), never by the listing commands below.
62
- ...['search', 'decisions', 'show', 'decide', 'retire', 'review', 'status'].map(action => [`memory ${action}`,
63
- args(['search', 'decide'].includes(action) ? 'query*' : ['show', 'retire'].includes(action) ? 'query' : '', 'why reason supersedes source all decision sessions no-local')]),
60
+ // Decision reads are already project-bound in project-memory.ts. Memory writes (decide, retire, apply) are a person's
61
+ // step-up, never a token's act, so none is listed here.
62
+ ...['search', 'decisions', 'show', 'review', 'status'].map(action => [`memory ${action}`,
63
+ args(action === 'search' ? 'query*' : action === 'show' ? 'query' : '', 'why reason supersedes source all decision sessions no-local')]),
64
64
  ['flows list', args()],
65
65
  ['flows show', args('flow:flow')], ['flows export', args('flow:flow')],
66
66
  ['flows create', args('', 'yes name template', { steps: 'steps' })],
@@ -50,7 +50,7 @@ export const REMOTE_SCOPES = new Map([
50
50
  // The person's own remote actions, or everyone's within their projects for an approver (remote-audit.ts).
51
51
  ["audit", self],
52
52
  ["knowledge search", repo], ["knowledge impact", repo], ["knowledge refresh", repo],
53
- ...["search", "decisions", "show", "decide", "retire", "review", "status"].map(action => [`memory ${action}`, self]),
53
+ ...["search", "decisions", "show", "review", "status"].map(action => [`memory ${action}`, self]),
54
54
  ...["list", "show", "export", "create", "edit", "trigger add", "trigger pause", "trigger resume", "trigger remove", "card add", "archive"]
55
55
  .map(action => [`flows ${action}`, self]),
56
56
  ["models status", global], ["models list", global],
package/dist/operate.js CHANGED
@@ -50,6 +50,7 @@ import { startLiveBackups } from "./live-backup.js";
50
50
  import { pushLimitSink } from "./provider-limits.js";
51
51
  import { heuristicSizing, installFilingSizer, ownerSizer, refineFiledSizing, settleSizings } from "./task-sizing.js";
52
52
  import { tierLines, tierReport } from "./tier-report.js";
53
+ import { savingsLines, savingsReportOrError } from "./route-savings.js";
53
54
  import { limitsView } from "./limits-ui.js";
54
55
  import { startCodexLimits } from "./codex-limits.js";
55
56
  import { packageVersion, startMonitoring, targetOf } from "./monitoring.js";
@@ -262,6 +263,9 @@ import { latestRelease, newerRelease, notifySecurityRelease, notifyVersionLive,
262
263
  import { installMethod } from "./install-method.js";
263
264
  import { PACKAGE_VERSION } from "./version.js";
264
265
  import { machineActing } from "./scope.js";
266
+ import { reevaluateResultRecords } from "./route-results.js";
267
+ import { holdRuleOff, routeRulesPass, taskEvidenceWords } from "./route-rules.js";
268
+ import { ruleHoldOf } from "./route-store.js";
265
269
  import { loginFileOf, migrateLegacyLogin, savedLoginFile, writableLoginFile } from "./login-secrets.js";
266
270
  /**
267
271
  * 0 done · 1 broke · 2 bad usage · 3 ran fine, the answer is no.
@@ -1296,14 +1300,25 @@ async function dispatch(command, positional, flags, context) {
1296
1300
  return runKnowledgeApply(flags, { ...context, now: context.clock(), actor: verified !== null && verified.ok ? acting.name : null });
1297
1301
  }
1298
1302
  case "models": {
1299
- const acting = await askCredentials(flags, context);
1303
+ // A local owner's lowering act (models turn-off / dismiss, through the terminal UI) asks for no credential: the
1304
+ // rule door re-checks the owner and its exact act itself.
1305
+ const localOnly = currentLocalOwner() !== null && !flags.has("as") && !flags.has("token") && ["turn-off", "dismiss", "suggestions", "stop"].includes(positional[0] ?? "");
1306
+ const acting = localOnly ? null : await askCredentials(flags, context);
1307
+ // The credential generation the proof is checked against, read BEFORE it (as `config set` does): a rotation in
1308
+ // between makes the rule door refuse, never borrow the new generation.
1309
+ const provedGeneration = acting === null ? -1 : context.store.accountOf(acting.name)?.generation ?? -1;
1300
1310
  const verified = acting === null ? null : authenticateApprover(context.store, acting.name, acting.token);
1301
- return runModelsCommand(positional, flags, { store: context.store, write: context.write, json: context.json, now: context.clock(), actor: verified !== null && verified.ok ? acting.name : null });
1311
+ const proof = verified !== null && verified.ok ? { actor: acting.name, generation: provedGeneration, method: verified.method } : null;
1312
+ return runModelsCommand(positional, flags, { store: context.store, write: context.write, json: context.json, now: context.clock(), actor: proof?.actor ?? null, proof });
1302
1313
  }
1303
1314
  case "memory": {
1304
1315
  // The remembered local login (or --as/--token) names the person; reads
1305
- // and writes both record who asked.
1316
+ // and writes both record who asked. Writes are a person's: never the lead's token or chain (like knowledge apply).
1317
+ if (memoryWriteVerb(positional) && currentActor()?.lead === true)
1318
+ return fail(context.write, context.json, `memory ${positional[0]}`, "unauthenticated", MEMORY_WRITE_REFUSAL, EXIT.refused);
1306
1319
  const acting = await askCredentials(flags, context);
1320
+ if (memoryWriteVerb(positional) && acting !== null && parseLeadToken(acting.token) !== null)
1321
+ return fail(context.write, context.json, `memory ${positional[0]}`, "unauthenticated", MEMORY_WRITE_REFUSAL, EXIT.refused);
1307
1322
  const verified = acting === null ? null : verifyApproverByPassword(context.store, acting.name, acting.token, context.store.knownRepos().filter(repo => context.store.accountCanAccess(acting.name, repo)));
1308
1323
  const who = verified !== null && verified.ok ? verified.who : null;
1309
1324
  return runMemoryCommand(positional, flags, { store: context.store, write: context.write, json: context.json, now: context.clock(), actor: who?.name ?? null, repos: who?.repos ?? [], configDir: dirname(context.databaseFile), ...(context.evidenceRoot === undefined ? {} : { evidenceRoot: context.evidenceRoot }) });
@@ -2703,6 +2718,17 @@ async function tickCommand(flags, context) {
2703
2718
  continue;
2704
2719
  }
2705
2720
  }
2721
+ // THE RULE HOLD, before any claim or row (results routing, route v3): a task approved under an earned rule that
2722
+ // then switched itself off on its sealed trigger never starts on that model. Nothing opens, so nothing strikes;
2723
+ // the task is held, in words, until a person approves it again (the hold goes with that yes).
2724
+ if (!wantsPlan && !wantsScout) {
2725
+ const ruleHeld = ruleHoldOf(store, id);
2726
+ if (ruleHeld !== null) {
2727
+ holdRuleOff(store, ref.id, id, ruleHeld.model, clock());
2728
+ dispatched.push({ id, outcome: "skipped", reason: "rule-switched-off", detail: `${id}: results for ${ruleHeld.model} dropped after this was approved — approve it again or re-file it` });
2729
+ continue;
2730
+ }
2731
+ }
2706
2732
  // THE READINESS HALT (v47): a provider THIS runner has reported
2707
2733
  // unavailable never claims and never spends — no substitution, no
2708
2734
  // second-best; the skip names the observation and the ways out.
@@ -3630,6 +3656,18 @@ async function reconcileCommand(flags, context) {
3630
3656
  tidied = runTidy(store, clock(), releaseFactsFor(context.databaseFile), TIDY_PASS_LIMIT, { principal: "operator", repos: [repo], includeUnplaced: false });
3631
3657
  }
3632
3658
  catch { /* the next pass tries again */ }
3659
+ // Result records (results-routing.md, slice 1): each finished family's record is evaluated again from its rows —
3660
+ // a completion, a follow-up check, a revision, a proof, a merge or the task ending each change what it says.
3661
+ try {
3662
+ reevaluateResultRecords(store, canonicalProject(repo) ?? resolve(repo), context.evidenceRoot, clock());
3663
+ }
3664
+ catch { /* the next pass tries again */ }
3665
+ // Earned rules (results-routing.md, slice 2): each live rule checked against its sealed terms on the records just
3666
+ // evaluated; tasks approved under one that switched itself off are held for a new approval.
3667
+ try {
3668
+ routeRulesPass(store, canonicalProject(repo) ?? resolve(repo), clock());
3669
+ }
3670
+ catch { /* the next pass tries again */ }
3633
3671
  // Checkout cleanup (Settings → Storage): finished work's clean checkouts go as the setting says (by default when
3634
3672
  // the task is complete or cancelled); their branches stay. Each removal is in the ledger.
3635
3673
  // Completing or cancelling a task stops anything still running from its checkout first.
@@ -9321,7 +9359,7 @@ function showTask(positional, context) {
9321
9359
  ...(detail.automaticReview === null ? reviewStatusLines(detail.review, latestFinished?.id ?? null) : buildReviewLines(detail.automaticReview)),
9322
9360
  ...(scope === null
9323
9361
  ? [" no scope — nothing will build this until one is written and approved"]
9324
- : describeScope(scope, readiness)),
9362
+ : describeScope(scope, readiness, taskEvidenceWords(store, task.id, now))),
9325
9363
  ...planContractLines(store, context.evidenceRoot, id),
9326
9364
  // The route's standing (v47): sealed, proposed, legacy, or unreadable —
9327
9365
  // the projection itself is in describeScope's lines above.
@@ -11132,12 +11170,15 @@ function spendCommand(flags, context) {
11132
11170
  const windows = limitsView(limits, [], { project: repo => basename(repo), subagent: id => String(id) }, context.clock())?.tiles ?? [];
11133
11171
  // Sized routing: each tier's tasks, time to a result and plan use, and what the tiers saved.
11134
11172
  const tiers = tierReport(context.store.handle, month.from, month.to, items);
11135
- return succeed(context.write, context.json, command, { month: month.name, totalMicrousd: total, unpriced, items: items.length, budgets, limits, tiers: { rows: tiers.rows, saved: tiers.saved } }, () => [
11173
+ // Suggestions and rules (results-routing slice 4): what each rule, trial and suggestion observed, over 90 days a side.
11174
+ const suggestions = savingsReportOrError(context.store, context.store.knownRepos(), context.clock());
11175
+ return succeed(context.write, context.json, command, { month: month.name, totalMicrousd: total, unpriced, items: items.length, budgets, limits, tiers: { rows: tiers.rows, saved: tiers.saved }, suggestions }, () => [
11136
11176
  `${month.name}: ${spendUsd(total)} (${items.length} pieces of work${unpriced > 0 ? `, ${unpriced} unpriced` : ""}; subscription work is $0)`,
11137
11177
  ...windows.map(tile => ` ${`${tile.name} ${tile.window}`.padEnd(24)} ${tile.value}% · ${tile.detail}`),
11138
11178
  ...[...byProject.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10).map(([repo, microusd]) => ` ${basename(repo).padEnd(24)} ${spendUsd(microusd)}`),
11139
11179
  ...budgets.map(one => ` budget ${budgetLabel(one, names.get(Number(one.key))).replace(/'s$/, "").padEnd(24)} ${spendUsd(one.spentMicrousd)} of ${spendUsd(one.limitMicrousd)} (${one.percent}%)${one.hardStop ? "" : " alerts only"}`),
11140
11180
  ...tierLines(tiers, spendUsd),
11181
+ ...savingsLines(suggestions),
11141
11182
  ]);
11142
11183
  }
11143
11184
  /** `backup now | list` (sprint 8): back the database up now, or list backups and how the latest ones went. */
@@ -13418,3 +13459,10 @@ export function upLaptopPortCandidates(store, fromSeam) {
13418
13459
  return [range[0] ?? 4180];
13419
13460
  return [...range];
13420
13461
  }
13462
+ /** The memory verbs that change project memory (memory-cli.ts), and `propose`, which files the proposals a person then
13463
+ * applies: a person's act, never the lead's. The reads stay as they are. */
13464
+ const MEMORY_WRITE_VERBS = new Set(["decide", "retire", "apply", "propose"]);
13465
+ function memoryWriteVerb(positional) {
13466
+ return MEMORY_WRITE_VERBS.has(positional[0] ?? "");
13467
+ }
13468
+ const MEMORY_WRITE_REFUSAL = "Project memory is changed by a person with their own sign-in, not the lead. The lead proposes decisions in chat for you to confirm.";
@@ -49,7 +49,7 @@
49
49
  * a legacy approval governed by its sealed profile alone.
50
50
  */
51
51
  import type { EvidenceKind } from "./contracts/acceptance-terms.js";
52
- import { MODEL_ID_SHAPE, RISKS, ROUTE_VERSION, type PhaseRoute, type RouteLeg, type RouteOverride, type RouteVersion, type TaskSizing } from "./contracts/route.js";
52
+ import { MODEL_ID_SHAPE, PLAIN_ROUTE_VERSION, RISKS, ROUTE_VERSION, type PhaseRoute, type RouteLeg, type RouteOverride, type RouteVersion, type TaskSizing } from "./contracts/route.js";
53
53
  import type { Phase, ProviderId } from "./provider.js";
54
54
  import type { QualityMode } from "./quality.js";
55
55
  /** The sealed route's shape is its contract (contracts/route.ts); these are its types. */
@@ -62,7 +62,7 @@ export declare function exactModelId(v: unknown): v is string;
62
62
  /** v2 adds the task's size (small / medium / large, risky or not), the
63
63
  * light tier, and plan headroom. A v1 route — sealed before sizing — still
64
64
  * reads back exactly and keeps its own digest domain. */
65
- export { ROUTE_VERSION };
65
+ export { PLAIN_ROUTE_VERSION, ROUTE_VERSION };
66
66
  /** The durable route ERA a scope row carries once it was filed under this
67
67
  * policy: NULL on a row proven to predate v47 (legacy — the sealed profile
68
68
  * alone governs), this value on every row filed since. A row with an era
@@ -191,6 +191,37 @@ export type RouteInput = {
191
191
  * candidates on more than one provider, the one with more room runs; a
192
192
  * provider past its limits gives way even to a stronger tier. */
193
193
  headroom?: readonly ProviderRoom[];
194
+ /** Results routing (route v3, docs/plans/results-routing.md slice 2): the project's live earned rules, and the two
195
+ * eligibility facts the route itself doesn't carry (a release or candidate task; declared paths that touch the
196
+ * project's protected paths). Absent or null: no rule applies. */
197
+ earned?: EarnedInput | null;
198
+ };
199
+ /** One live earned rule, as routing reads it: which project-size cohort it covers, the exact Claude model it builds
200
+ * on, and the configured builder it was earned against (a configuration change pauses the rule; a mismatch here
201
+ * skips it all the same). */
202
+ export type EarnedRule = {
203
+ id: number;
204
+ generation: number;
205
+ cohortSize: "small" | "medium";
206
+ provider: ProviderId;
207
+ model: string;
208
+ configured: {
209
+ provider: string;
210
+ model: string | null;
211
+ };
212
+ };
213
+ /** A live trial (results routing slice 3): try a cheaper Claude model on the next `limit` eligible tasks of one
214
+ * cohort, each still approved by a person. `reservation` is this filing's place in it (route-store.ts
215
+ * reserveTrialPlace): its id, or null when every place is taken, so the configured builder runs it. */
216
+ export type EarnedTrial = EarnedRule & {
217
+ limit: number;
218
+ reservation: number | null;
219
+ };
220
+ export type EarnedInput = {
221
+ rules: readonly EarnedRule[];
222
+ release: boolean;
223
+ protected: boolean;
224
+ trial?: EarnedTrial | null;
194
225
  };
195
226
  export declare function specWords(spec: {
196
227
  provider: string;
@@ -203,6 +234,55 @@ export declare function specWords(spec: {
203
234
  * guess.
204
235
  */
205
236
  export declare function recommendRoute(input: RouteInput): PhaseRoute;
237
+ /**
238
+ * THE BUILD LEG'S ONE PRECEDENCE ORDER (results routing, docs/plans/results-routing.md slice 2), pure:
239
+ *
240
+ * pin > person override > trial > earned rule > configured row (light / routine / strong by size)
241
+ * > headroom move
242
+ *
243
+ * Headroom moves only a configured leg, never a rule's or a trial's: their evidence is about one exact model. A rule
244
+ * or trial whose provider is past the headroom limits is skipped for this filing, and its reason says so. A trial
245
+ * applies only with a place reserved for this task (slice 3). A rule or trial applies only to an
246
+ * eligible task — small or medium, not risky, default quality, no screenshot or manual-review evidence, not a
247
+ * merging mode, not a release or candidate, nothing protected declared — and only over the configured builder it was
248
+ * earned against. Its leg says how it was selected (`selection`), so the route seals as version 3 and every later
249
+ * check reads the sealed selection, never the current rules or the reason text.
250
+ */
251
+ export declare function selectBuildLeg(input: RouteInput, configured: {
252
+ spec: RouteCandidate;
253
+ tier: CandidateTier;
254
+ reasons: string[];
255
+ }): {
256
+ leg: RouteLeg;
257
+ moved: boolean;
258
+ };
259
+ /** The earned rule this task's build takes (pure): null when none could apply (no rule for its size, or the task
260
+ * isn't eligible), or a rule skipped for this filing with the words why. */
261
+ export declare function earnedRuleFor(input: RouteInput, configured: {
262
+ spec: RouteCandidate;
263
+ tier: CandidateTier;
264
+ }): {
265
+ kind: "rule";
266
+ rule: EarnedRule;
267
+ } | {
268
+ kind: "skipped";
269
+ reason: string;
270
+ } | null;
271
+ /** The trial this task's build takes (pure, results routing slice 3): null when none could apply (no live trial for
272
+ * its size, or the task isn't eligible: the same facts an earned rule needs), a trial with this task's place, or a
273
+ * trial skipped for this filing with the words why (another configured builder, no place left, no headroom). */
274
+ export declare function trialFor(input: RouteInput, configured: {
275
+ spec: RouteCandidate;
276
+ tier: CandidateTier;
277
+ }): {
278
+ kind: "trial";
279
+ trial: EarnedTrial & {
280
+ reservation: number;
281
+ };
282
+ } | {
283
+ kind: "skipped";
284
+ reason: string;
285
+ } | null;
206
286
  export declare function legOf(route: PhaseRoute, phase: Phase): RouteLeg;
207
287
  /** Every stated problem on the route, in phase order — a non-empty list
208
288
  * means the scope files unresolved with these words. */
@@ -50,7 +50,7 @@
50
50
  */
51
51
  import { ACP_AGENTS, isAcpProvider } from "./acp-agents.js";
52
52
  import { createHash } from "node:crypto";
53
- import { MODEL_ID_SHAPE, postureOf, readRouteOverrides, readSealedRoute, RISKS, ROUTE_PHASES, ROUTE_PROVIDERS, ROUTE_VERSION, TASK_SIZE_WORDS, } from "./contracts/route.js";
53
+ import { MODEL_ID_SHAPE, postureOf, readRouteOverrides, PLAIN_ROUTE_VERSION, readSealedRoute, RISKS, ROUTE_PHASES, ROUTE_PROVIDERS, ROUTE_VERSION, TASK_SIZE_WORDS, } from "./contracts/route.js";
54
54
  import { canonicalJson } from "./canonical-json.js";
55
55
  export { MODEL_ID_SHAPE };
56
56
  // Type-only imports from provider.ts: it sits under evidence.ts and scope.ts
@@ -72,7 +72,7 @@ export function exactModelId(v) {
72
72
  /** v2 adds the task's size (small / medium / large, risky or not), the
73
73
  * light tier, and plan headroom. A v1 route — sealed before sizing — still
74
74
  * reads back exactly and keeps its own digest domain. */
75
- export { ROUTE_VERSION };
75
+ export { PLAIN_ROUTE_VERSION, ROUTE_VERSION };
76
76
  /** The durable route ERA a scope row carries once it was filed under this
77
77
  * policy: NULL on a row proven to predate v47 (legacy — the sealed profile
78
78
  * alone governs), this value on every row filed since. A row with an era
@@ -270,23 +270,13 @@ export function recommendRoute(input) {
270
270
  legs.push({ phase: "plan", provider: rec.spec.provider, model: rec.spec.model, tier: rec.tier, chosen: "recommended", recommended, reasons: rec.reasons, problem: planProblem(rec.spec) });
271
271
  }
272
272
  }
273
- // build — pin > override > recommendation.
273
+ // build — one precedence order (selectBuildLeg): pin > override > trial > earned rule > configured > headroom.
274
274
  let buildMoved = false;
275
275
  const build = (() => {
276
- const rec = pick("build");
277
- buildMoved = rec.moved;
278
- const pin = input.pins?.build ?? null;
279
- const override = overrideFor("build");
280
- const recommended = { provider: rec.spec.provider, model: rec.spec.model, tier: rec.tier };
281
- if (pin !== null || override !== null)
282
- buildMoved = false;
283
- const leg = pin !== null
284
- ? { phase: "build", provider: pin.provider, model: pin.model, tier: rec.tier, chosen: "pinned", recommended, reasons: [`pinned to ${specWords(pin)} by the firing that filed this task — nothing overrides a pin`], problem: null }
285
- : override !== null
286
- ? { phase: "build", provider: override.provider, model: override.model, tier: rec.tier, chosen: "override", recommended, reasons: [`overridden by ${override.by} to ${specWords(override)} (recommended ${specWords(rec.spec)})`], problem: null }
287
- : { phase: "build", provider: rec.spec.provider, model: rec.spec.model, tier: rec.tier, chosen: "recommended", recommended, reasons: rec.reasons, problem: null };
288
- legs.push(leg);
289
- return leg;
276
+ const chosen = selectBuildLeg(input, pickTier("build"));
277
+ buildMoved = chosen.moved;
278
+ legs.push(chosen.leg);
279
+ return chosen.leg;
290
280
  })();
291
281
  // repair — ALWAYS the build provider (repair resumes the builder's
292
282
  // session; cross-provider repair does not exist). Only the model routes:
@@ -383,7 +373,8 @@ export function recommendRoute(input) {
383
373
  }
384
374
  }
385
375
  return {
386
- version: ROUTE_VERSION,
376
+ // Version 3 only for a build leg that carries a selection: every other route's bytes stay version 2's.
377
+ version: build.selection !== undefined ? ROUTE_VERSION : PLAIN_ROUTE_VERSION,
387
378
  risk: "routine",
388
379
  qualityMode: input.qualityMode,
389
380
  publication: input.publication,
@@ -398,6 +389,127 @@ export function recommendRoute(input) {
398
389
  size: input.size ?? null,
399
390
  };
400
391
  }
392
+ /**
393
+ * THE BUILD LEG'S ONE PRECEDENCE ORDER (results routing, docs/plans/results-routing.md slice 2), pure:
394
+ *
395
+ * pin > person override > trial > earned rule > configured row (light / routine / strong by size)
396
+ * > headroom move
397
+ *
398
+ * Headroom moves only a configured leg, never a rule's or a trial's: their evidence is about one exact model. A rule
399
+ * or trial whose provider is past the headroom limits is skipped for this filing, and its reason says so. A trial
400
+ * applies only with a place reserved for this task (slice 3). A rule or trial applies only to an
401
+ * eligible task — small or medium, not risky, default quality, no screenshot or manual-review evidence, not a
402
+ * merging mode, not a release or candidate, nothing protected declared — and only over the configured builder it was
403
+ * earned against. Its leg says how it was selected (`selection`), so the route seals as version 3 and every later
404
+ * check reads the sealed selection, never the current rules or the reason text.
405
+ */
406
+ export function selectBuildLeg(input, configured) {
407
+ const pin = input.pins?.build ?? null;
408
+ const override = input.overrides.find(one => one.phase === "build") ?? null;
409
+ if (pin !== null || override !== null) {
410
+ // What would have run otherwise is said as it always was: the configured leg after headroom.
411
+ const rec = withHeadroom(input, "build", configured);
412
+ const recommended = { provider: rec.spec.provider, model: rec.spec.model, tier: rec.tier };
413
+ const leg = pin !== null
414
+ ? { phase: "build", provider: pin.provider, model: pin.model, tier: rec.tier, chosen: "pinned", recommended, reasons: [`pinned to ${specWords(pin)} by the firing that filed this task — nothing overrides a pin`], problem: null }
415
+ : { phase: "build", provider: override.provider, model: override.model, tier: rec.tier, chosen: "override", recommended, reasons: [`overridden by ${override.by} to ${specWords(override)} (recommended ${specWords(rec.spec)})`], problem: null };
416
+ return { leg, moved: false };
417
+ }
418
+ const trial = trialFor(input, configured);
419
+ if (trial !== null && trial.kind === "trial") {
420
+ const one = trial.trial;
421
+ const spec = { provider: one.provider, model: one.model };
422
+ return {
423
+ leg: {
424
+ phase: "build", provider: one.provider, model: one.model, tier: configured.tier, chosen: "recommended",
425
+ recommended: { ...spec, tier: configured.tier },
426
+ reasons: [...configured.reasons.slice(0, 1), `trial: ${specWords(spec)} is being tried on this project's next ${one.limit} eligible ${one.cohortSize === "small" ? "small" : "routine"} tasks instead of the configured ${specWords({ provider: one.configured.provider, model: one.configured.model ?? "default" })}; a person approves each one`],
427
+ problem: null,
428
+ selection: { kind: "trial", rule: one.id, reservation: one.reservation },
429
+ },
430
+ moved: false,
431
+ };
432
+ }
433
+ const earned = earnedRuleFor(input, configured);
434
+ if (earned !== null && earned.kind === "rule") {
435
+ const rule = earned.rule;
436
+ const spec = { provider: rule.provider, model: rule.model };
437
+ return {
438
+ leg: {
439
+ phase: "build", provider: rule.provider, model: rule.model, tier: configured.tier, chosen: "recommended",
440
+ recommended: { ...spec, tier: configured.tier },
441
+ reasons: [...configured.reasons.slice(0, 1), `earned rule: ${specWords(spec)} builds this project's eligible ${rule.cohortSize === "small" ? "small" : "routine"} tasks instead of the configured ${specWords({ provider: rule.configured.provider, model: rule.configured.model ?? "default" })}, on its recorded results here`],
442
+ problem: null,
443
+ selection: { kind: "rule", rule: rule.id, ruleGeneration: rule.generation },
444
+ },
445
+ moved: false,
446
+ };
447
+ }
448
+ const rec = withHeadroom(input, "build", configured);
449
+ const reasons = [...rec.reasons, ...(trial === null ? [] : [trial.reason]), ...(earned === null ? [] : [earned.reason])];
450
+ return {
451
+ leg: { phase: "build", provider: rec.spec.provider, model: rec.spec.model, tier: rec.tier, chosen: "recommended", recommended: { provider: rec.spec.provider, model: rec.spec.model, tier: rec.tier }, reasons, problem: null },
452
+ moved: rec.moved,
453
+ };
454
+ }
455
+ /** The earned rule this task's build takes (pure): null when none could apply (no rule for its size, or the task
456
+ * isn't eligible), or a rule skipped for this filing with the words why. */
457
+ export function earnedRuleFor(input, configured) {
458
+ const earned = input.earned ?? null;
459
+ const size = input.size ?? null;
460
+ if (earned === null || size === null || size.risky || (size.size !== "small" && size.size !== "medium"))
461
+ return null;
462
+ const rule = earned.rules.find(one => one.cohortSize === size.size) ?? null;
463
+ if (rule === null)
464
+ return null;
465
+ if (earned.release || earned.protected || input.qualityMode !== "default" || input.publication === "automerge")
466
+ return null;
467
+ if (input.evidence.some(kind => kind === "screenshot" || kind === "manual-review"))
468
+ return null;
469
+ if (demandedTier(input, "build").tier !== "routine")
470
+ return null;
471
+ // Only Claude reports the model it ran, so only a Claude cohort can earn a rule.
472
+ if (rule.provider !== "claude" || !exactModel(rule.model))
473
+ return null;
474
+ if (configured.spec.provider !== rule.configured.provider || configured.spec.model !== rule.configured.model) {
475
+ return { kind: "skipped", reason: `the earned rule for ${size.size === "small" ? "small" : "routine"} tasks was earned against another configured builder — not applied` };
476
+ }
477
+ const room = (input.headroom ?? []).find(one => one.provider === rule.provider) ?? null;
478
+ const tight = room === null ? null : tightWords(room);
479
+ if (tight !== null)
480
+ return { kind: "skipped", reason: `earned rule skipped for this filing: ${tight} — the configured builder runs it` };
481
+ return { kind: "rule", rule };
482
+ }
483
+ /** The trial this task's build takes (pure, results routing slice 3): null when none could apply (no live trial for
484
+ * its size, or the task isn't eligible: the same facts an earned rule needs), a trial with this task's place, or a
485
+ * trial skipped for this filing with the words why (another configured builder, no place left, no headroom). */
486
+ export function trialFor(input, configured) {
487
+ const trial = input.earned?.trial ?? null;
488
+ const size = input.size ?? null;
489
+ if (trial === null || size === null || size.risky || (size.size !== "small" && size.size !== "medium") || trial.cohortSize !== size.size)
490
+ return null;
491
+ if (input.earned.release || input.earned.protected || input.qualityMode !== "default" || input.publication === "automerge")
492
+ return null;
493
+ if (input.evidence.some(kind => kind === "screenshot" || kind === "manual-review"))
494
+ return null;
495
+ if (demandedTier(input, "build").tier !== "routine")
496
+ return null;
497
+ // Only Claude reports the model it ran, so only a Claude cohort runs a trial.
498
+ if (trial.provider !== "claude" || !exactModel(trial.model) || !Number.isSafeInteger(trial.limit) || trial.limit < 1)
499
+ return null;
500
+ const words = size.size === "small" ? "small" : "routine";
501
+ if (configured.spec.provider !== trial.configured.provider || configured.spec.model !== trial.configured.model) {
502
+ return { kind: "skipped", reason: `the trial for ${words} tasks was started against another configured builder — not applied` };
503
+ }
504
+ const room = (input.headroom ?? []).find(one => one.provider === trial.provider) ?? null;
505
+ const tight = room === null ? null : tightWords(room);
506
+ if (tight !== null)
507
+ return { kind: "skipped", reason: `trial skipped for this filing: ${tight} — the configured builder runs it` };
508
+ if (trial.reservation === null || !Number.isSafeInteger(trial.reservation) || trial.reservation < 1) {
509
+ return { kind: "skipped", reason: `every place in the trial for ${words} tasks is taken — the configured builder runs it` };
510
+ }
511
+ return { kind: "trial", trial: { ...trial, reservation: trial.reservation } };
512
+ }
401
513
  /**
402
514
  * PLAN HEADROOM (v2), at filing:
403
515
  *
@@ -494,6 +606,8 @@ export function canonicalRouteJson(route) {
494
606
  recommended: leg.recommended,
495
607
  reasons: leg.reasons,
496
608
  problem: leg.problem,
609
+ // v3 (results routing): the build leg's selection is in the bytes, so in the digest; v1 and v2 bytes never change.
610
+ ...(route.version === 3 && leg.selection !== undefined ? { selection: leg.selection } : {}),
497
611
  })),
498
612
  overrides: route.overrides.map(one => ({ phase: one.phase, provider: one.provider, model: one.model, by: one.by, at: one.at })),
499
613
  // v2 carries the size it was routed for; a v1 snapshot's bytes never change.
@@ -30,6 +30,10 @@ export type ProjectDeleteResult = {
30
30
  said: string;
31
31
  running?: string[];
32
32
  };
33
+ /** The project's Models rows, counted with its settings and deleted by forgetProjectModels only. */
34
+ export declare const MODEL_SETTINGS: readonly [table: string, column: string][];
35
+ /** The project's settings tables, for the deletion test's inventory. */
36
+ export declare const PROJECT_SETTINGS: readonly [table: string, column: string][];
33
37
  /** What Toolroll holds for a project, counted. */
34
38
  export declare function projectHoldings(store: Store, repo: string): ProjectHoldings;
35
39
  /** What's in it, in words: "12 tasks, 3 versions, 30 runs, …". Empty kinds are left out. */
@@ -15,7 +15,7 @@
15
15
  import { existsSync, lstatSync, rmSync } from "node:fs";
16
16
  import { isAbsolute, relative, resolve } from "node:path";
17
17
  import { teardownGit, refusal } from "./teardown-git.js";
18
- import { BUILT_IN } from "./store.js";
18
+ import { BUILT_IN, forgetProjectModels } from "./store.js";
19
19
  import { BRANCH_PREFIXES, isOwnBranch } from "./names.js";
20
20
  /** The tables whose history is the audit record itself: never touched. */
21
21
  const KEPT = new Set(["action_ledger", "ledger_seal", "ledger_checkpoint"]);
@@ -42,13 +42,19 @@ function doomed(db, repo) {
42
42
  const subagents = ids(db, "SELECT id FROM subagent WHERE repo = ?", repo);
43
43
  return { refs, tasks, runs, artifacts, threads, turns, flows, cards, subagents };
44
44
  }
45
- /** A project's settings: one row per thing someone set for it. */
45
+ /** A project's settings: one row per thing someone set for it. Its Models rows (phase_config, phase_tier_config,
46
+ * phase_tier_alternate) are settings too, but go only through store.ts forgetProjectModels, which moves their
47
+ * generations and takes the project's results-routing rows with them (MODEL_SETTINGS). */
46
48
  const SETTINGS = [
47
49
  ["approval_policy", "repo"], ["verify_command", "repo"], ["worktree_setup", "repo"], ["backend_grant", "repo"], ["publication_grant", "repo"],
48
- ["intake_grant", "repo"], ["capability", "repo"], ["project_tool", "repo"], ["phase_config", "scope"], ["phase_tier_config", "scope"],
50
+ ["intake_grant", "repo"], ["capability", "repo"], ["project_tool", "repo"],
49
51
  ["learning_policy", "repo"], ["operating_mode", "repo"], ["project_knowledge", "repo"], ["live_detail_setting", "repo"],
50
52
  ["project_decision", "repo"], ["flow_script", "repo"], ["workflow_recipe", "repo"], ["team_lead_project", "project"],
51
53
  ];
54
+ /** The project's Models rows, counted with its settings and deleted by forgetProjectModels only. */
55
+ export const MODEL_SETTINGS = Object.freeze([["phase_config", "scope"], ["phase_tier_config", "scope"], ["phase_tier_alternate", "scope"]]);
56
+ /** The project's settings tables, for the deletion test's inventory. */
57
+ export const PROJECT_SETTINGS = Object.freeze(SETTINGS.map(one => [...one]));
52
58
  /** What Toolroll holds for a project, counted. */
53
59
  export function projectHoldings(store, repo) {
54
60
  const db = store.handle;
@@ -61,7 +67,7 @@ export function projectHoldings(store, repo) {
61
67
  checkouts: count("SELECT COUNT(*) AS n FROM worktree WHERE repo = ?", repo),
62
68
  chats: d.threads.length, flows: d.flows.length, cards: d.cards.length, subagents: d.subagents.length,
63
69
  budgets: count(`SELECT COUNT(*) AS n FROM budget WHERE removed_at IS NULL AND ((scope_kind = 'project' AND scope_key = ?) OR (scope_kind = 'subagent' AND scope_key IN (SELECT CAST(value AS TEXT) FROM json_each(?))))`, repo, json(d.subagents)),
64
- settings: SETTINGS.filter(([table]) => tableExists(db, table)).reduce((sum, [table, column]) => sum + count(`SELECT COUNT(*) AS n FROM ${table} WHERE ${column} = ?`, repo), 0),
70
+ settings: [...SETTINGS, ...MODEL_SETTINGS].filter(([table]) => tableExists(db, table)).reduce((sum, [table, column]) => sum + count(`SELECT COUNT(*) AS n FROM ${table} WHERE ${column} = ?`, repo), 0),
65
71
  };
66
72
  }
67
73
  /** What's in it, in words: "12 tasks, 3 versions, 30 runs, …". Empty kinds are left out. */
@@ -315,6 +321,8 @@ function deleteRows(store, repo, d, now) {
315
321
  del("memory_sighting", "gap IN (SELECT id FROM memory_gap WHERE repo = ?) OR session IN (SELECT id FROM memory_session WHERE repo = ?)", repo, repo);
316
322
  for (const [table, column] of SETTINGS)
317
323
  del(table, `${column} = ?`, repo);
324
+ // Its Models rows and results-routing rows: through the Models counter, so whatever depended on them pauses.
325
+ rows += forgetProjectModels(store, repo, now);
318
326
  del("project_mute", "repo = ?", repo);
319
327
  for (const table of ["knowledge_change", "project_identity_carry", "project_skill_change", "memory_gap", "memory_proposal", "memory_rejection", "memory_session", "memory_search", "mode_rail", "side_spend", "watch_episode", "worktree", "coordinator_proposal", "lead_commitment"])
320
328
  del(table, "repo = ?", repo);
@@ -22,18 +22,18 @@ export type KnowledgeView = ReturnType<typeof knowledgeView>;
22
22
  export declare function knowledgeVersion(store: Store, repo: string, actor: string, revision: number): Knowledge;
23
23
  /** Chat reads only an admitted project. Keep the full source in the project UI. */
24
24
  export declare function conversationKnowledge(store: Store, repo: string, actor: string, reference?: string, decision?: number): {
25
- content?: never;
25
+ history?: never;
26
26
  revision: number;
27
27
  stale: boolean;
28
28
  notice: string;
29
29
  decision?: never;
30
30
  title?: never;
31
- history?: never;
31
+ content?: never;
32
32
  instructions?: never;
33
33
  references?: never;
34
34
  decisions?: never;
35
35
  } | {
36
- content?: never;
36
+ history?: never;
37
37
  stale?: never;
38
38
  decision: {
39
39
  repo: string;
@@ -51,27 +51,27 @@ export declare function conversationKnowledge(store: Store, repo: string, actor:
51
51
  };
52
52
  notice: string;
53
53
  title?: never;
54
+ content?: never;
54
55
  revision?: never;
55
- history?: never;
56
56
  instructions?: never;
57
57
  references?: never;
58
58
  decisions?: never;
59
59
  } | {
60
+ history?: never;
60
61
  stale?: never;
61
62
  decision?: never;
62
63
  revision: number;
63
64
  title: string;
64
65
  content: string;
65
66
  notice: string;
66
- history?: never;
67
67
  instructions?: never;
68
68
  references?: never;
69
69
  decisions?: never;
70
70
  } | {
71
- content?: never;
72
71
  stale?: never;
73
72
  decision?: never;
74
73
  title?: never;
74
+ content?: never;
75
75
  revision: number;
76
76
  history: {
77
77
  revision: number;