toolroll 0.9.40 → 0.9.42

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 (68) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/cli.js +3 -1
  3. package/dist/coding-types.d.ts +2 -24
  4. package/dist/coding-workspace.js +12 -11
  5. package/dist/contracts/cli.d.ts +736 -0
  6. package/dist/contracts/cli.js +223 -0
  7. package/dist/contracts/coding-activity.d.ts +64 -0
  8. package/dist/contracts/coding-activity.js +58 -0
  9. package/dist/contracts/console-api.d.ts +483 -0
  10. package/dist/contracts/console-api.js +273 -0
  11. package/dist/contracts/diff-stat.d.ts +58 -0
  12. package/dist/contracts/diff-stat.js +59 -0
  13. package/dist/contracts/integration-metadata.d.ts +99 -0
  14. package/dist/contracts/integration-metadata.js +101 -0
  15. package/dist/contracts/model-catalog.d.ts +21 -0
  16. package/dist/contracts/model-catalog.js +14 -0
  17. package/dist/contracts/native-census.d.ts +42 -0
  18. package/dist/contracts/native-census.js +36 -0
  19. package/dist/contracts/observation-request.d.ts +37 -0
  20. package/dist/contracts/observation-request.js +25 -0
  21. package/dist/contracts/provider.d.ts +30 -0
  22. package/dist/contracts/provider.js +15 -0
  23. package/dist/contracts/recipes.d.ts +166 -0
  24. package/dist/contracts/recipes.js +52 -0
  25. package/dist/contracts/retention.d.ts +42 -0
  26. package/dist/contracts/retention.js +24 -0
  27. package/dist/contracts/storage.d.ts +23 -0
  28. package/dist/contracts/storage.js +10 -0
  29. package/dist/contracts/store-json.d.ts +315 -0
  30. package/dist/contracts/store-json.js +196 -0
  31. package/dist/contracts/trigger-payloads.d.ts +369 -0
  32. package/dist/contracts/trigger-payloads.js +144 -0
  33. package/dist/contracts/update-journal.d.ts +254 -0
  34. package/dist/contracts/update-journal.js +155 -0
  35. package/dist/contracts/verification-view.d.ts +93 -0
  36. package/dist/contracts/verification-view.js +43 -0
  37. package/dist/control-setup.d.ts +1 -1
  38. package/dist/desktop-update.d.ts +7 -40
  39. package/dist/desktop-update.js +36 -8
  40. package/dist/evidence.d.ts +3 -19
  41. package/dist/flow-triggers.js +61 -60
  42. package/dist/mcp-connect.d.ts +0 -1
  43. package/dist/mcp-connect.js +41 -23
  44. package/dist/model-catalog.js +8 -6
  45. package/dist/observations.d.ts +2 -6
  46. package/dist/observations.js +13 -9
  47. package/dist/operate.js +2 -1
  48. package/dist/process-recovery-native.d.ts +2 -13
  49. package/dist/process-recovery-native.js +8 -5
  50. package/dist/project-tools.d.ts +6 -24
  51. package/dist/project-tools.js +22 -18
  52. package/dist/provider.d.ts +3 -8
  53. package/dist/provider.js +15 -11
  54. package/dist/recipes.d.ts +3 -28
  55. package/dist/recipes.js +79 -52
  56. package/dist/retention.d.ts +4 -5
  57. package/dist/retention.js +5 -4
  58. package/dist/serve.js +142 -28
  59. package/dist/storage.d.ts +2 -1
  60. package/dist/storage.js +12 -9
  61. package/dist/store.d.ts +3 -15
  62. package/dist/store.js +80 -158
  63. package/dist/surface.d.ts +6 -54
  64. package/dist/surface.js +6 -1
  65. package/dist/toolroll-update.d.ts +8 -99
  66. package/dist/toolroll-update.js +28 -18
  67. package/dist/verification-evidence.js +27 -12
  68. package/package.json +1 -1
package/dist/serve.js CHANGED
@@ -56,6 +56,7 @@ import { addCardToFlow, advanceFlows, cancelFlowCard, crossProjectProblem, decid
56
56
  import { chooseFlowCard } from "./flow-send.js";
57
57
  import { FlowContractError, FLOW_TEMPLATES, validateFlowDefinition, withZoneNames } from "./flows.js";
58
58
  import { stageReferenceProblems } from "./contracts/stage-output.js";
59
+ import { CONSOLE_FORMS, checkResponse, readForm } from "./contracts/console-api.js";
59
60
  import { TOOL_CATALOG, addToolTo, catalogTool, discoverTools, localAppOf, projectToolsOf, removeToolFrom, secretsSetFor, setToolSecret, splitCommandLine, testToolOf } from "./project-tools.js";
60
61
  import { changeLearning, learningView } from "./project-learning.js";
61
62
  import { applySavedKnowledge, changeKnowledge, knowledgeView, knowledgeVersion, readKnowledgeSnapshot } from "./project-knowledge.js";
@@ -1412,7 +1413,7 @@ export function createDecisionServer(options) {
1412
1413
  if (setupAttemptsLeft <= 0) {
1413
1414
  return page(response, 403, signupPage("too many wrong codes — restart the server to get a fresh code", 0));
1414
1415
  }
1415
- const body = await form(request);
1416
+ const body = readForm(await form(request), CONSOLE_FORMS.signup);
1416
1417
  const code = (body.get("code") ?? "").trim();
1417
1418
  const name = (body.get("name") ?? "").trim();
1418
1419
  const password = body.get("password") ?? "";
@@ -1451,7 +1452,7 @@ export function createDecisionServer(options) {
1451
1452
  return redirect(response, "/");
1452
1453
  }
1453
1454
  if (url.pathname === "/login" && method === "POST") {
1454
- const body = await form(request);
1455
+ const body = readForm(await form(request), CONSOLE_FORMS.login);
1455
1456
  const name = body.get("name");
1456
1457
  const token = body.get("token");
1457
1458
  // The destination a deep link asked for rides the form as a same-site
@@ -1528,7 +1529,7 @@ export function createDecisionServer(options) {
1528
1529
  }
1529
1530
  let body;
1530
1531
  try {
1531
- body = await form(request);
1532
+ body = readForm(await form(request), CONSOLE_FORMS.join);
1532
1533
  }
1533
1534
  catch {
1534
1535
  return respond(response, 413, "text/plain; charset=utf-8", "body too large");
@@ -1607,7 +1608,7 @@ export function createDecisionServer(options) {
1607
1608
  return freshIdentitySignIn.run({ actor: requestFacts.sso?.fresh === true ? who.name : null }, () => requestContext.run(requestFacts, async () => {
1608
1609
  const taskTextForm = url.pathname === "/tasks/add" || /^\/t\/[^/]+\/scope$/.test(url.pathname);
1609
1610
  const body = method === "POST" ? await form(request, url.pathname === "/settings/skills/import" ? 2 * 1024 * 1024 : url.pathname === "/flows/import" ? 1024 * 1024 : taskTextForm ? TASK_FORM_BODY_CAP : BODY_CAP) : null;
1610
- const target = actionTarget(url, who, request, body);
1611
+ const target = actionTarget(url, who, request, body === null ? null : readForm(body, CONSOLE_FORMS.ledgerTarget));
1611
1612
  const execute = async () => {
1612
1613
  if (!projectRequestAllowed(url, who, request, response))
1613
1614
  return;
@@ -2633,7 +2634,7 @@ export function createDecisionServer(options) {
2633
2634
  latest: operator && latest !== null ? latest : null, csrf: operator ? who.session.csrf : null, today: now.toISOString().slice(0, 10) };
2634
2635
  const rows = store.actionLedger({ ...query, ...(before === null ? {} : { before: Number(before) }), limit: 51 });
2635
2636
  if (url.searchParams.get("format") === "json")
2636
- return respond(response, 200, "application/json; charset=utf-8", JSON.stringify({ chain: { ok: report.ok, entries: report.entries, through: report.through, head: report.head, problem: report.problem }, entries: rows.slice(0, 50), nextBefore: rows.length > 50 ? rows[49].id : null }));
2637
+ return respond(response, 200, "application/json; charset=utf-8", JSON.stringify(checkResponse("ledgerPage", { chain: { ok: report.ok, entries: report.entries, through: report.through, head: report.head, problem: report.problem }, entries: rows.slice(0, 50), nextBefore: rows.length > 50 ? rows[49].id : null })));
2637
2638
  return sendScreen(response, 200, screen("Action ledger", ledgerBody(rows, projects, url.searchParams, chainView), { chrome: chromeFor(chosen === "" ? null : chosen, "ledger", undefined, chosen === "" ? "all" : "project") }));
2638
2639
  }
2639
2640
  if (url.pathname === "/activity") {
@@ -3010,7 +3011,7 @@ export function createDecisionServer(options) {
3010
3011
  if (url.searchParams.get("format") === "json") {
3011
3012
  response.writeHead(200, { "content-type": "application/json; charset=utf-8", "content-disposition": `attachment; filename="evidence-${family.root.id.replace(/[^A-Za-z0-9._-]/g, "_")}.json"`,
3012
3013
  "cache-control": "no-store", "x-content-type-options": "nosniff" });
3013
- return void response.end(JSON.stringify(pack, null, 2));
3014
+ return void response.end(JSON.stringify(checkResponse("evidencePack", pack), null, 2));
3014
3015
  }
3015
3016
  return sendScreen(response, 200, screen(`Evidence pack · ${family.root.id}`, evidencePackHtml(pack), { chrome: chromeFor(family.root.repo, "tasks") }));
3016
3017
  }
@@ -3358,7 +3359,7 @@ export function createDecisionServer(options) {
3358
3359
  const view = flowView(store, flow, { name: who.name, approver: who.via === "cookie" && who.role === "approver" }, Number.isSafeInteger(selected) && selected > 0 ? selected : null, { dir: options.configDir ?? null, repos: consoleProjects(), startTrigger: Number.isSafeInteger(start) && start > 0 ? start : null,
3359
3360
  sortReady: keyStatus("openrouter", providerHome).set, toolHome });
3360
3361
  if (url.searchParams.get("format") === "json")
3361
- return respond(response, 200, "application/json; charset=utf-8", JSON.stringify(view));
3362
+ return respond(response, 200, "application/json; charset=utf-8", JSON.stringify(checkResponse("flowView", view)));
3362
3363
  return sendScreen(response, 200, screen(flow.name, `<p><a href="/flows">Flows</a></p><h1>${escape(flow.name)}</h1>${flowFallbackHtml(view)}`, { chrome: chromeFor(flow.repo, "flows"), workspace: { view } }));
3363
3364
  }
3364
3365
  if (url.pathname === "/recipes" || url.pathname.startsWith("/recipes/")) {
@@ -6611,7 +6612,7 @@ export function createDecisionServer(options) {
6611
6612
  }
6612
6613
  return respond(response, 200, "application/json; charset=utf-8", JSON.stringify({ ok: true, beaten }));
6613
6614
  }
6614
- async function handlePost(url, who, request, response, body) {
6615
+ async function handlePost(url, who, request, response, posted) {
6615
6616
  // The attended beat answers BEFORE the shared mutation guard (v28): it
6616
6617
  // carries no parameters, so there is no csrf token to check — its OWN
6617
6618
  // guard is complete and STRICTER for the browsers this console
@@ -6623,12 +6624,13 @@ export function createDecisionServer(options) {
6623
6624
  if (url.pathname === "/session/attended-beats") {
6624
6625
  return attendedBeats(who, request, response, clock());
6625
6626
  }
6626
- const denied = authorizeMutation(request, who, body);
6627
+ const denied = authorizeMutation(request, who, readForm(posted, CONSOLE_FORMS.mutationGuard));
6627
6628
  if (denied !== null)
6628
6629
  return refuse(response, who, denied.status, denied.message);
6629
6630
  // The scripted demo lead's acts: ask, approve, change a plan, complete or send back a result.
6630
6631
  const demoAct = /^\/chat\/demo\/(?:ask|([0-9]{1,9})\/(approve|change|revise|complete))$/.exec(url.pathname);
6631
6632
  if (demoAct !== null) {
6633
+ const body = readForm(posted, CONSOLE_FORMS.demo);
6632
6634
  const lead = demoLeadHere();
6633
6635
  if (lead === null || who.via !== "cookie")
6634
6636
  return refuse(response, who, 404, "There's no page at this address.", "/chat");
@@ -6677,6 +6679,7 @@ export function createDecisionServer(options) {
6677
6679
  }
6678
6680
  // The update notice is dismissed per browser and per version: a newer release shows it again.
6679
6681
  if (url.pathname === "/settings/updates/dismiss") {
6682
+ const body = readForm(posted, CONSOLE_FORMS.updatesDismiss);
6680
6683
  const version = body.get("version") ?? "";
6681
6684
  if (!/^[0-9A-Za-z.-]{1,60}$/.test(version))
6682
6685
  return refuse(response, who, 400, "Choose a version to dismiss.", "/settings#updates");
@@ -6690,6 +6693,7 @@ export function createDecisionServer(options) {
6690
6693
  }
6691
6694
  // The daily check's switch: an installation setting, so an operator's. (Its own address: POST /settings/updates starts an update.)
6692
6695
  if (url.pathname === "/settings/updates/checks") {
6696
+ const body = readForm(posted, CONSOLE_FORMS.updatesChecks);
6693
6697
  if (options.configDir === undefined)
6694
6698
  return refuse(response, who, 404, "Updates are not set up on this console.", "/settings");
6695
6699
  if (!store.isInstanceOperator(who.name))
@@ -6699,6 +6703,7 @@ export function createDecisionServer(options) {
6699
6703
  return redirect(response, `/settings?said=${encodeURIComponent(on ? "Toolroll looks for a newer version once a day." : "Update checks are off.")}#updates`);
6700
6704
  }
6701
6705
  if (url.pathname === '/code' || url.pathname.startsWith('/code/')) {
6706
+ const body = readForm(posted, CONSOLE_FORMS.code);
6702
6707
  const wantsJson = request.headers.accept?.includes('application/json') === true;
6703
6708
  const fail = (status, message, delivery = 'rejected', sessionId) => wantsJson
6704
6709
  ? respond(response, status, 'application/json', JSON.stringify({ ok: false, error: message, delivery, ...(sessionId ? { sessionId } : {}) }))
@@ -6707,7 +6712,7 @@ export function createDecisionServer(options) {
6707
6712
  return fail(403, 'Coding sessions require installation-wide operator access. You can manage project tasks in Work.');
6708
6713
  if (!coding)
6709
6714
  return fail(409, codingProblem || 'Open Toolroll on the machine with your installed coding agent.');
6710
- if ([...new Set(body.keys())].some(key => body.getAll(key).length !== 1))
6715
+ if ([...new Set(body.keys())].some(key => body.sent.getAll(key).length !== 1))
6711
6716
  return fail(400, 'Submit one value for each field.');
6712
6717
  const actor = { name: who.name, generation: who.session.generation };
6713
6718
  const permitted = (repo) => visible(repo) && codingProjectAllowed(repo);
@@ -6779,7 +6784,7 @@ export function createDecisionServer(options) {
6779
6784
  if (ship && !wantsJson) {
6780
6785
  try {
6781
6786
  const preview = codingHandoffPreview(store, { sessionId: ship[1], actor: who.name });
6782
- return sendScreen(response, 409, screen('Review for shipping', codingShippingHtml(preview, who.session.csrf, body, message), { chrome: chromeFor(preview.repo, 'code') }));
6787
+ return sendScreen(response, 409, screen('Review for shipping', codingShippingHtml(preview, who.session.csrf, body.sent, message), { chrome: chromeFor(preview.repo, 'code') }));
6783
6788
  }
6784
6789
  catch { }
6785
6790
  }
@@ -6789,7 +6794,7 @@ export function createDecisionServer(options) {
6789
6794
  if (!projectRequestAllowed(url, who, request, response))
6790
6795
  return;
6791
6796
  if (restricted()) {
6792
- const repos = body.getAll("repo");
6797
+ const repos = readForm(posted, CONSOLE_FORMS.projectCeiling).getAll("repo");
6793
6798
  if (repos.length > 1 || repos.some(repo => repo.trim() !== "" && !visible(repo.trim())))
6794
6799
  return refuse(response, who, 403, "That project is outside your access.", "/projects");
6795
6800
  }
@@ -6809,6 +6814,7 @@ export function createDecisionServer(options) {
6809
6814
  // re-counts within five seconds either way, this just makes it exact.
6810
6815
  bustBadge();
6811
6816
  if (url.pathname === "/settings/skills/revise") {
6817
+ const body = readForm(posted, CONSOLE_FORMS.skillsRevise);
6812
6818
  if (who.via !== 'cookie' || who.role !== 'approver')
6813
6819
  return refuse(response, who, 403, 'Sign in to revise a test.', '/settings/skills');
6814
6820
  try {
@@ -6828,12 +6834,13 @@ export function createDecisionServer(options) {
6828
6834
  }
6829
6835
  }
6830
6836
  if (url.pathname === "/settings/skills/import" || url.pathname === "/settings/skills/change") {
6837
+ const body = readForm(posted, CONSOLE_FORMS.skillsChange);
6831
6838
  if (who.via !== 'cookie' || who.role !== 'approver')
6832
6839
  return refuse(response, who, 403, 'Sign in as an approver to manage skills.', '/settings/skills');
6833
6840
  const repo = body.get('repo') ?? '', back = `/settings/skills?repo=${encodeURIComponent(repo)}`;
6834
6841
  if (!visible(repo) || !consoleProjects().includes(repo))
6835
6842
  return refuse(response, who, 403, 'That project is outside your access.', '/projects');
6836
- if ([...new Set(body.keys())].some(k => body.getAll(k).length !== 1))
6843
+ if ([...new Set(body.keys())].some(k => body.sent.getAll(k).length !== 1))
6837
6844
  return refuse(response, who, 400, 'Invalid skills form.', back);
6838
6845
  try {
6839
6846
  const view = skillsView(store, repo, who.name);
@@ -6883,6 +6890,7 @@ export function createDecisionServer(options) {
6883
6890
  // setting a starter kit up, trying its sample card, and bringing GitHub issues in.
6884
6891
  const kitPost = /^\/kits\/([a-z-]{1,40})\/(setup|sample|github)$/.exec(url.pathname);
6885
6892
  if (kitPost !== null) {
6893
+ const body = readForm(posted, CONSOLE_FORMS.kit);
6886
6894
  const now = clock();
6887
6895
  if (who.via !== "cookie" || who.role !== "approver")
6888
6896
  return refuse(response, who, 403, "Sign in as an approver to set kits up.", "/kits");
@@ -6914,6 +6922,7 @@ export function createDecisionServer(options) {
6914
6922
  const teammatePost = url.pathname === "/teammates/new" ? ["", "0", "new"] : /^\/teammates\/([1-9][0-9]{0,9})\/(soul|state|note|settings|summary|tools|memory|routines|week)$/.exec(url.pathname);
6915
6923
  const questionPost = /^\/teammates\/questions\/([1-9][0-9]{0,9})\/answer$/.exec(url.pathname);
6916
6924
  if (teammatePost !== null || questionPost !== null) {
6925
+ const body = readForm(posted, CONSOLE_FORMS.teammates);
6917
6926
  const now = clock();
6918
6927
  const wantsJson = (request.headers.accept ?? "").includes("application/json");
6919
6928
  if (who.via !== "cookie")
@@ -6989,7 +6998,7 @@ export function createDecisionServer(options) {
6989
6998
  const grant = store.teammateGrant(mate.id, tool);
6990
6999
  const changed = op === "grant" ? await grantTool(store, mate, tool, who.name, now, { toolHome })
6991
7000
  : op === "revoke" ? revokeTool(store, mate, tool, who.name, now)
6992
- : grant === null ? { ok: false, said: `${nameOf(mate)} doesn't use ${tool}.` } : setToolRules(store, mate, tool, rulesFromForm(grant, key => body.get(key)), who.name, now);
7001
+ : grant === null ? { ok: false, said: `${nameOf(mate)} doesn't use ${tool}.` } : setToolRules(store, mate, tool, rulesFromForm(grant, key => body.sent.get(key)), who.name, now);
6993
7002
  return redirect(response, `${back}?${changed.ok ? "said" : "problem"}=${encodeURIComponent(changed.said)}#tools`);
6994
7003
  }
6995
7004
  const done = action === "state" ? setTeammateState(store, mate, body.get("state") === "removed" ? "removed" : body.get("state") === "paused" ? "paused" : "active", who.name, now)
@@ -7003,6 +7012,7 @@ export function createDecisionServer(options) {
7003
7012
  // Flows → New → a template: preview these answers, or create exactly what was previewed.
7004
7013
  const galleryPost = /^\/flows\/new\/([a-z-]{1,40})$/.exec(url.pathname);
7005
7014
  if (galleryPost !== null) {
7015
+ const body = readForm(posted, CONSOLE_FORMS.galleryUse);
7006
7016
  const template = galleryTemplateOf(galleryPost[1]);
7007
7017
  const projects = consoleProjects();
7008
7018
  if (template === null)
@@ -7034,6 +7044,7 @@ export function createDecisionServer(options) {
7034
7044
  const flowPost = /^\/flows\/([1-9][0-9]{0,9})\/(save|cards|archive|triggers|linear-key|hooks-address|scripts|secrets)$/.exec(url.pathname) ?? /^\/flows\/([1-9][0-9]{0,9})\/cards\/([1-9][0-9]{0,9})\/(move|decide|choose|cancel|comment|assign|watch)$/.exec(url.pathname);
7035
7045
  const triggerPost = /^\/flows\/([1-9][0-9]{0,9})\/triggers\/([1-9][0-9]{0,9})\/(pause|resume|remove|check|press|renew|secret|share|unshare)$/.exec(url.pathname);
7036
7046
  if (url.pathname === "/flows/new" || url.pathname === "/flows/example" || url.pathname === "/flows/import" || flowPost !== null || triggerPost !== null) {
7047
+ const body = readForm(posted, CONSOLE_FORMS.flows);
7037
7048
  const now = clock();
7038
7049
  const answer = (status, payload) => respond(response, status, "application/json; charset=utf-8", JSON.stringify(payload));
7039
7050
  const projects = consoleProjects();
@@ -7305,6 +7316,7 @@ export function createDecisionServer(options) {
7305
7316
  // v103: an instance operator records a checkpoint of the ledger chain (its head, to copy off the machine).
7306
7317
  // v105: set or remove a monthly budget. An instance operator, with a step-up; the ledger keeps before → after.
7307
7318
  if (url.pathname === "/spend/budget") {
7319
+ const body = readForm(posted, CONSOLE_FORMS.spendBudget);
7308
7320
  if (who.via !== "cookie" || !store.isInstanceOperator(who.name))
7309
7321
  return refuse(response, who, 403, "An instance operator sets budgets.", "/spend");
7310
7322
  const back = (key, words) => redirect(response, `/spend?${key}=${encodeURIComponent(words)}`);
@@ -7336,6 +7348,7 @@ export function createDecisionServer(options) {
7336
7348
  // Delete a project, in two steps: its name, then what goes and the password. An instance operator; the ledger keeps who and what.
7337
7349
  // Settings → Project → Builds at once: an approver for the project sets it; the ledger keeps before → after.
7338
7350
  if (url.pathname === "/settings/project/concurrency") {
7351
+ const body = readForm(posted, CONSOLE_FORMS.projectConcurrency);
7339
7352
  const repo = body.get("repo") ?? "";
7340
7353
  const known = consoleProjects();
7341
7354
  if (!known.includes(repo))
@@ -7355,6 +7368,7 @@ export function createDecisionServer(options) {
7355
7368
  return back("said", changed.before === changed.after ? "Nothing changed." : `Saved. Up to ${n} at once.`);
7356
7369
  }
7357
7370
  if (url.pathname === "/settings/project/delete") {
7371
+ const body = readForm(posted, CONSOLE_FORMS.projectDelete);
7358
7372
  if (who.via !== "cookie" || !store.isInstanceOperator(who.name))
7359
7373
  return refuse(response, who, 403, "An instance operator deletes projects.", "/settings/project");
7360
7374
  const repo = body.get("repo") ?? "";
@@ -7395,6 +7409,7 @@ export function createDecisionServer(options) {
7395
7409
  }
7396
7410
  // Sprint 8: an instance operator saves the organisation policy, with a step-up; the ledger keeps each rule's before → after.
7397
7411
  if (url.pathname === "/settings/policy") {
7412
+ const body = readForm(posted, CONSOLE_FORMS.policy);
7398
7413
  if (who.via !== "cookie" || !store.isInstanceOperator(who.name))
7399
7414
  return refuse(response, who, 403, "An instance operator sets the policy.", "/settings/policy");
7400
7415
  const back = (key, words) => redirect(response, `/settings/policy?${key}=${encodeURIComponent(words)}`);
@@ -7411,6 +7426,7 @@ export function createDecisionServer(options) {
7411
7426
  }
7412
7427
  // v102: an instance operator changes a project's approval rules, with a step-up; the ledger keeps before → after.
7413
7428
  if (url.pathname === "/settings/approval") {
7429
+ const body = readForm(posted, CONSOLE_FORMS.approval);
7414
7430
  if (who.via !== "cookie" || !store.isInstanceOperator(who.name))
7415
7431
  return refuse(response, who, 403, "An instance operator sets approval rules.", "/settings/approval");
7416
7432
  const repo = body.get("repo") ?? "";
@@ -7431,6 +7447,7 @@ export function createDecisionServer(options) {
7431
7447
  return back("said", `Saved. ${rulesSummary(next)}`);
7432
7448
  }
7433
7449
  if (url.pathname === "/settings/sessions") {
7450
+ const body = readForm(posted, CONSOLE_FORMS.sessions);
7434
7451
  if (who.via !== "cookie")
7435
7452
  return refuse(response, who, 403, "Sign in in a browser to manage sessions and tokens.", "/settings");
7436
7453
  const operator = store.isInstanceOperator(who.name);
@@ -7489,6 +7506,7 @@ export function createDecisionServer(options) {
7489
7506
  return back("problem", "Choose an action.");
7490
7507
  }
7491
7508
  if (url.pathname === "/settings/sign-in") {
7509
+ const body = readForm(posted, CONSOLE_FORMS.signIn);
7492
7510
  if (who.via !== "cookie" || !store.isInstanceOperator(who.name) || !options.configDir)
7493
7511
  return refuse(response, who, 403, "An instance operator sets up sign-in.", "/settings");
7494
7512
  const back = (key, words) => redirect(response, `/settings/sign-in?${key}=${encodeURIComponent(words)}`);
@@ -7526,6 +7544,7 @@ export function createDecisionServer(options) {
7526
7544
  }
7527
7545
  // Settings → Updates: start the `toolroll update` job. A step-up; the job writes the ledger entries.
7528
7546
  if (url.pathname === "/settings/updates" || url.pathname === "/settings/updates/seen" || url.pathname === "/settings/updates/cancel") {
7547
+ const body = readForm(posted, CONSOLE_FORMS.updates);
7529
7548
  const databaseFile = store.databaseFile();
7530
7549
  if (who.via !== "cookie" || !store.isInstanceOperator(who.name) || databaseFile === null)
7531
7550
  return refuse(response, who, 403, "An instance operator updates Toolroll.", "/settings");
@@ -7572,6 +7591,7 @@ export function createDecisionServer(options) {
7572
7591
  }
7573
7592
  // v105: change how long each kind of data is kept. A step-up; the ledger keeps before → after for each kind.
7574
7593
  if (url.pathname === "/settings/retention") {
7594
+ const body = readForm(posted, CONSOLE_FORMS.retention);
7575
7595
  if (who.via !== "cookie" || !store.isInstanceOperator(who.name))
7576
7596
  return refuse(response, who, 403, "An instance operator sets retention.", "/settings");
7577
7597
  const back = (key, words) => redirect(response, `/settings/retention?${key}=${encodeURIComponent(words)}`);
@@ -7591,6 +7611,7 @@ export function createDecisionServer(options) {
7591
7611
  // Settings → Storage: when a finished task's clean checkout goes, cleaning up what the preview showed, and throwing
7592
7612
  // away a checkout kept for its changes. Each takes the password; the ledger keeps every change and removal.
7593
7613
  if (url.pathname === "/settings/storage" || url.pathname === "/settings/storage/clean" || url.pathname === "/settings/storage/discard") {
7614
+ const body = readForm(posted, CONSOLE_FORMS.storage);
7594
7615
  const databaseFile = store.databaseFile();
7595
7616
  if (who.via !== "cookie" || !store.isInstanceOperator(who.name) || databaseFile === null)
7596
7617
  return refuse(response, who, 403, "An instance operator looks after storage.", "/settings");
@@ -7624,6 +7645,7 @@ export function createDecisionServer(options) {
7624
7645
  // Pull requests for one project: turn on (the checks run again and must match what the page showed), merge
7625
7646
  // settings, or turn off. Each takes the password and lands in the ledger.
7626
7647
  if (url.pathname === "/settings/pull-requests") {
7648
+ const body = readForm(posted, CONSOLE_FORMS.pullRequests);
7627
7649
  const repo = body.get("repo") ?? "";
7628
7650
  const known = [...new Set([...managedRepos(), ...store.knownRepos(), ...store.listProjects().map(one => one.path)])];
7629
7651
  if (who.via !== "cookie" || !known.includes(repo) || !visible(repo))
@@ -7661,6 +7683,7 @@ export function createDecisionServer(options) {
7661
7683
  // Checks for one project: the level, or the quick command (approved like `verify set --quick`). Each takes
7662
7684
  // the password and lands in the ledger.
7663
7685
  if (url.pathname === "/settings/checks") {
7686
+ const body = readForm(posted, CONSOLE_FORMS.checks);
7664
7687
  const repo = body.get("repo") ?? "";
7665
7688
  const known = [...new Set([...managedRepos(), ...store.knownRepos(), ...store.listProjects().map(one => one.path)])];
7666
7689
  if (who.via !== "cookie" || !known.includes(repo) || !visible(repo))
@@ -7722,6 +7745,7 @@ export function createDecisionServer(options) {
7722
7745
  }
7723
7746
  // Sprint 8: change the backup schedule. A step-up; the ledger keeps before → after.
7724
7747
  if (url.pathname === "/settings/backups") {
7748
+ const body = readForm(posted, CONSOLE_FORMS.backups);
7725
7749
  const databaseFile = store.databaseFile();
7726
7750
  if (who.via !== "cookie" || !store.isInstanceOperator(who.name) || databaseFile === null)
7727
7751
  return refuse(response, who, 403, "An instance operator looks after backups.", "/settings");
@@ -7748,6 +7772,7 @@ export function createDecisionServer(options) {
7748
7772
  }
7749
7773
  // v105: download everything as a .zip. An instance operator, with a step-up; the ledger records it.
7750
7774
  if (url.pathname === "/settings/data") {
7775
+ const body = readForm(posted, CONSOLE_FORMS.data);
7751
7776
  if (who.via !== "cookie" || !store.isInstanceOperator(who.name))
7752
7777
  return refuse(response, who, 403, "An instance operator exports data.", "/settings");
7753
7778
  if (!authenticateApprover(store, who.name, body.get("password") ?? "").ok) {
@@ -7762,6 +7787,7 @@ export function createDecisionServer(options) {
7762
7787
  }
7763
7788
  // Settings → Flows → Switch on: a starter flow's zones and trigger, made together.
7764
7789
  if (url.pathname === "/settings/flows/on") {
7790
+ const body = readForm(posted, CONSOLE_FORMS.flowsOn);
7765
7791
  const repo = body.get("repo") ?? "";
7766
7792
  const back = (key, words) => redirect(response, `/settings/flows?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}`);
7767
7793
  const known = [...new Set([...(admissionList() ?? []), ...managedRepos(), ...store.knownRepos()])];
@@ -7777,6 +7803,7 @@ export function createDecisionServer(options) {
7777
7803
  }
7778
7804
  // Settings → Integrations → Send test: one harmless read-only check of one integration, now.
7779
7805
  if (url.pathname === "/settings/integrations/test") {
7806
+ const body = readForm(posted, CONSOLE_FORMS.integrationsTest);
7780
7807
  if (who.via !== "cookie" || who.role !== "approver" || restricted() || !options.configDir)
7781
7808
  return refuse(response, who, 403, "An installation approver tests integrations.", "/settings");
7782
7809
  const key = body.get("key") ?? "";
@@ -7789,6 +7816,7 @@ export function createDecisionServer(options) {
7789
7816
  }
7790
7817
  // v104: change where monitoring goes. A step-up; the ledger keeps before → after (addresses, never secrets).
7791
7818
  if (url.pathname === "/settings/monitoring") {
7819
+ const body = readForm(posted, CONSOLE_FORMS.monitoring);
7792
7820
  if (who.via !== "cookie" || !store.isInstanceOperator(who.name) || !options.configDir)
7793
7821
  return refuse(response, who, 403, "An instance operator sets up monitoring.", "/settings");
7794
7822
  const back = (key, words) => redirect(response, `/settings/monitoring?${key}=${encodeURIComponent(words)}`);
@@ -7812,6 +7840,7 @@ export function createDecisionServer(options) {
7812
7840
  return back("said", "Saved. Deliveries start within a few seconds.");
7813
7841
  }
7814
7842
  if (url.pathname === "/settings/tools/connect") {
7843
+ const body = readForm(posted, CONSOLE_FORMS.toolsConnect);
7815
7844
  if (who.via !== "cookie" || who.role !== "approver")
7816
7845
  return refuse(response, who, 403, "Sign in as an approver to connect tools.", "/settings/tools");
7817
7846
  const repo = body.get("repo") ?? "";
@@ -7887,6 +7916,7 @@ export function createDecisionServer(options) {
7887
7916
  return goOutside(response, started.go, `Going to ${service.label} to sign in…`, { state: started.state, path: CONNECT_CALLBACK, secure: origin.startsWith("https:") });
7888
7917
  }
7889
7918
  if (url.pathname === "/settings/tools/change") {
7919
+ const body = readForm(posted, CONSOLE_FORMS.toolsChange);
7890
7920
  if (who.via !== "cookie" || who.role !== "approver")
7891
7921
  return refuse(response, who, 403, "Sign in as an approver to manage tools.", "/settings/tools");
7892
7922
  const repo = body.get("repo") ?? "";
@@ -7974,6 +8004,7 @@ export function createDecisionServer(options) {
7974
8004
  }
7975
8005
  }
7976
8006
  if (url.pathname === "/settings/appearance") {
8007
+ const body = readForm(posted, CONSOLE_FORMS.appearance);
7977
8008
  // A per-browser preference, not a shared setting: it lives in this
7978
8009
  // person's own cookie and never in the database.
7979
8010
  const accentField = body.get("accent");
@@ -8002,6 +8033,7 @@ export function createDecisionServer(options) {
8002
8033
  return redirect(response, back);
8003
8034
  }
8004
8035
  if (url.pathname.startsWith("/settings/models/")) {
8036
+ const body = readForm(posted, CONSOLE_FORMS.models);
8005
8037
  if (who.via !== "cookie" || who.role !== "approver")
8006
8038
  return refuse(response, who, 403, "Sign in as an approver to change models.", "/settings/models");
8007
8039
  const back = (said, bad = false) => redirect(response, `/settings/models?${bad ? "problem" : "said"}=${encodeURIComponent(said)}`);
@@ -8056,6 +8088,7 @@ export function createDecisionServer(options) {
8056
8088
  return refuse(response, who, 404, "No such models action.", "/settings/models");
8057
8089
  }
8058
8090
  if (url.pathname === "/settings/knowledge/refresh") {
8091
+ const body = readForm(posted, CONSOLE_FORMS.knowledgeRefresh);
8059
8092
  if (who.via !== 'cookie' || who.role !== 'approver')
8060
8093
  return refuse(response, who, 403, 'Sign in as an approver to refresh project context.', '/settings/knowledge');
8061
8094
  const repo = body.get('repo') ?? '';
@@ -8065,6 +8098,7 @@ export function createDecisionServer(options) {
8065
8098
  return sendScreen(response, refreshed.index.status === 'ready' ? 200 : 503, screen('Project context', `<h1>Project context</h1><p>${refreshed.index.status === 'ready' ? 'Code index refreshed.' : 'The index is unavailable. Source search still works.'}</p><a class="button-link" href="/settings/knowledge?repo=${encodeURIComponent(repo)}">Open knowledge</a>`, { chrome: chromeFor(repo, 'settings') }));
8066
8099
  }
8067
8100
  if (url.pathname === "/settings/knowledge/proposal") {
8101
+ const body = readForm(posted, CONSOLE_FORMS.knowledgeProposal);
8068
8102
  if (who.via !== 'cookie' || who.role !== 'approver')
8069
8103
  return refuse(response, who, 403, 'Sign in as an approver to decide memory proposals.', '/settings/knowledge');
8070
8104
  const repo = body.get('repo') ?? '', decision = body.get('decision') ?? '';
@@ -8081,6 +8115,7 @@ export function createDecisionServer(options) {
8081
8115
  return redirect(response, `/settings/knowledge?repo=${encodeURIComponent(repo)}&saved=1`);
8082
8116
  }
8083
8117
  if (url.pathname === "/settings/knowledge/decision") {
8118
+ const body = readForm(posted, CONSOLE_FORMS.knowledgeDecision);
8084
8119
  if (who.via !== 'cookie' || who.role !== 'approver')
8085
8120
  return refuse(response, who, 403, 'Sign in as an approver to change project decisions.', '/settings/knowledge');
8086
8121
  const repo = body.get('repo') ?? '', action = body.get('action') ?? '';
@@ -8108,6 +8143,7 @@ export function createDecisionServer(options) {
8108
8143
  return redirect(response, `/settings/knowledge?repo=${encodeURIComponent(repo)}&saved=1`);
8109
8144
  }
8110
8145
  if (url.pathname === "/settings/knowledge/change") {
8146
+ const body = readForm(posted, CONSOLE_FORMS.knowledgeChange);
8111
8147
  if (who.via !== 'cookie' || who.role !== 'approver')
8112
8148
  return refuse(response, who, 403, 'Sign in as an approver to change project knowledge.', '/settings/knowledge');
8113
8149
  const repo = body.get('repo') ?? '', action = body.get('action') ?? '';
@@ -8133,6 +8169,7 @@ export function createDecisionServer(options) {
8133
8169
  return redirect(response, `/settings/knowledge?repo=${encodeURIComponent(repo)}&saved=1`);
8134
8170
  }
8135
8171
  if (url.pathname === "/settings/learning/change") {
8172
+ const body = readForm(posted, CONSOLE_FORMS.learningChange);
8136
8173
  if (who.via !== "cookie")
8137
8174
  return refuse(response, who, 403, "Sign in to change learning.", "/settings/learning");
8138
8175
  const repo = body.get("repo") ?? "", action = body.get("action") ?? "";
@@ -8149,6 +8186,7 @@ export function createDecisionServer(options) {
8149
8186
  return redirect(response, `/settings/learning?repo=${encodeURIComponent(repo)}`);
8150
8187
  }
8151
8188
  if (["/control/setup-preview", "/control/setup-approve", "/control/instructions-preview", "/control/instructions-approve"].includes(url.pathname)) {
8189
+ const body = readForm(posted, CONSOLE_FORMS.projectSetup);
8152
8190
  const project = projectOf(who, request);
8153
8191
  if (project == null || body.get("repo") !== project || !visible(project))
8154
8192
  return refuse(response, who, 409, "The selected project changed. Open project setup again.", "/control");
@@ -8188,6 +8226,7 @@ export function createDecisionServer(options) {
8188
8226
  return redirect(response, "/control");
8189
8227
  }
8190
8228
  if (["connect", "pair", "unpair", "disconnect", "alerts"].some(action => url.pathname === `/settings/slack/${action}`)) {
8229
+ const body = readForm(posted, CONSOLE_FORMS.slack);
8191
8230
  if (who.via !== "cookie" || who.role !== "approver" || restricted() || !options.configDir)
8192
8231
  return refuse(response, who, 403, "An installation approver can connect Slack.", "/settings");
8193
8232
  const dir = options.configDir, state = new SlackState(store), action = url.pathname.split("/").at(-1);
@@ -8249,6 +8288,7 @@ export function createDecisionServer(options) {
8249
8288
  return redirect(response, "/settings/telegram");
8250
8289
  }
8251
8290
  if (url.pathname === "/settings/telegram/pair" || url.pathname === "/settings/telegram/unpair") {
8291
+ const body = readForm(posted, CONSOLE_FORMS.telegramPair);
8252
8292
  // The person's own pairing, under their password: a code minted for
8253
8293
  // them alone, or their own chats revoked. Teammates' pairings are
8254
8294
  // never touched from here.
@@ -8274,6 +8314,7 @@ export function createDecisionServer(options) {
8274
8314
  return sendScreen(response, 200, screen("Pair Telegram", telegramSettingsHtml(store, botId, who.name, who.session.csrf, { code }), { chrome: chromeFor(projectOf(who, request) ?? null, "settings"), forceSensitive: true }));
8275
8315
  }
8276
8316
  if (["connect", "pair", "unpair", "disconnect", "alerts"].some(action => url.pathname === `/settings/teams/${action}`)) {
8317
+ const body = readForm(posted, CONSOLE_FORMS.teams);
8277
8318
  if (who.via !== "cookie" || who.role !== "approver" || restricted() || !options.configDir)
8278
8319
  return refuse(response, who, 403, "An installation approver can connect Teams.", "/settings");
8279
8320
  const dir = options.configDir, state = new ChatState(store, "teams"), action = url.pathname.split("/").at(-1);
@@ -8328,6 +8369,7 @@ export function createDecisionServer(options) {
8328
8369
  return redirect(response, "/settings/teams");
8329
8370
  }
8330
8371
  if (["connect", "pair", "unpair", "disconnect", "alerts"].some(action => url.pathname === `/settings/discord/${action}`)) {
8372
+ const body = readForm(posted, CONSOLE_FORMS.discord);
8331
8373
  if (who.via !== "cookie" || who.role !== "approver" || restricted() || !options.configDir)
8332
8374
  return refuse(response, who, 403, "An installation approver can connect Discord.", "/settings");
8333
8375
  const dir = options.configDir, state = new ChatState(store, "discord"), action = url.pathname.split("/").at(-1);
@@ -8382,6 +8424,7 @@ export function createDecisionServer(options) {
8382
8424
  return redirect(response, "/settings/discord");
8383
8425
  }
8384
8426
  if (url.pathname === "/settings/messaging" && options.configDir !== undefined && options.telegramTokenFile !== undefined) {
8427
+ const body = readForm(posted, CONSOLE_FORMS.messaging);
8385
8428
  const wanted = (body.get("primary") ?? "").trim();
8386
8429
  const facts = effectivePrimary(process.env, options.configDir, loadBotToken(process.env, options.telegramTokenFile) !== null);
8387
8430
  // Only a CONFIGURED service may page — choosing silence is not a
@@ -8393,6 +8436,7 @@ export function createDecisionServer(options) {
8393
8436
  return redirect(response, "/settings");
8394
8437
  }
8395
8438
  if (url.pathname === "/settings/permission-default" && options.telegramTokenFile !== undefined) {
8439
+ const body = readForm(posted, CONSOLE_FORMS.permissionDefault);
8396
8440
  const wanted = body.get("permission-mode");
8397
8441
  if (wanted !== "auto" && wanted !== "bypassPermissions") {
8398
8442
  return refuse(response, who, 400, "permissions must be Auto or Full access", "/settings");
@@ -8403,6 +8447,7 @@ export function createDecisionServer(options) {
8403
8447
  : "New tasks now start with Auto. Approved tasks keep their setting.")}`);
8404
8448
  }
8405
8449
  if (url.pathname === "/settings/quality-default" && options.telegramTokenFile !== undefined) {
8450
+ const body = readForm(posted, CONSOLE_FORMS.qualityDefault);
8406
8451
  const wanted = body.get("quality-mode");
8407
8452
  if (!isQualityMode(wanted)) {
8408
8453
  return refuse(response, who, 400, "quality must be Default or Strict / release", "/settings");
@@ -8413,6 +8458,7 @@ export function createDecisionServer(options) {
8413
8458
  : "New tasks now use Default quality. Approved tasks keep their setting.")}`);
8414
8459
  }
8415
8460
  if (url.pathname === "/settings/notifications") {
8461
+ const body = readForm(posted, CONSOLE_FORMS.notifications);
8416
8462
  // Each person's own choice: a closed list of modes and a 24-hour HH:MM, or off.
8417
8463
  const mode = body.get("mode"), time = (body.get("digest") ?? "").trim(), shots = body.get("screenshots");
8418
8464
  if (mode !== null && mode !== "quiet" && mode !== "all")
@@ -8432,6 +8478,7 @@ export function createDecisionServer(options) {
8432
8478
  return redirect(response, `/settings?said=${encodeURIComponent(said)}`);
8433
8479
  }
8434
8480
  if (url.pathname === "/settings/notifications/mute") {
8481
+ const body = readForm(posted, CONSOLE_FORMS.notificationsMute);
8435
8482
  // Each person's own pings: a muted project stays in Tasks and the evening digest.
8436
8483
  const repo = body.get("repo") ?? "";
8437
8484
  const project = notificationProjects(store, who.name).find(one => one.repo === repo);
@@ -8442,6 +8489,7 @@ export function createDecisionServer(options) {
8442
8489
  return redirect(response, `/settings?said=${encodeURIComponent(muted ? `${project.name} muted. It still shows in Tasks and your evening digest.` : `${project.name} pings you again.`)}#notifications`);
8443
8490
  }
8444
8491
  if (url.pathname === "/settings/telegram-digest" && options.telegramTokenFile !== undefined) {
8492
+ const body = readForm(posted, CONSOLE_FORMS.telegramDigest);
8445
8493
  // The cadence is a closed list of minutes — never a free number from
8446
8494
  // a form; "off" clears it. Any approver session may set it.
8447
8495
  const wanted = (body.get("every") ?? "").trim();
@@ -8453,6 +8501,7 @@ export function createDecisionServer(options) {
8453
8501
  return redirect(response, `/settings?said=${encodeURIComponent(minutes === null ? "Digest off: each update arrives as it happens." : `Digest every ${minutes >= 60 ? `${minutes / 60}h` : `${minutes}m`}. Anything that needs you still arrives at once.`)}`);
8454
8502
  }
8455
8503
  if (url.pathname === "/settings/provider-key" || url.pathname === "/settings/provider-key-clear") {
8504
+ const body = readForm(posted, CONSOLE_FORMS.providerKey);
8456
8505
  // The central gate already required an ACTIVE approver; the value is
8457
8506
  // write-only from here — status pages say set/not-set, never bytes.
8458
8507
  const provider = body.get("provider") ?? "";
@@ -8505,6 +8554,7 @@ export function createDecisionServer(options) {
8505
8554
  return redirect(response, `/settings?said=${encodeURIComponent((verdict.ok ? `${stored} and verified — it works` : `${stored}. ${verdictWords(provider, verdict)}`) + modeNote)}`);
8506
8555
  }
8507
8556
  if (url.pathname === "/settings/telegram-token" && options.telegramTokenFile !== undefined) {
8557
+ const body = readForm(posted, CONSOLE_FORMS.telegramToken);
8508
8558
  const value = body.get("token") ?? "";
8509
8559
  const saved = saveBotToken(options.telegramTokenFile, value);
8510
8560
  if (!saved.ok) {
@@ -8520,6 +8570,7 @@ export function createDecisionServer(options) {
8520
8570
  }
8521
8571
  // v87: the mail server Send email steps use, and a test email to its own address.
8522
8572
  if ((url.pathname === "/settings/email" || url.pathname === "/settings/email-test" || url.pathname === "/settings/email-read-test") && who.role === "approver" && options.configDir !== undefined) {
8573
+ const body = readForm(posted, CONSOLE_FORMS.email);
8523
8574
  const said = (words) => redirect(response, `/settings?said=${encodeURIComponent(words)}#email`);
8524
8575
  if (url.pathname === "/settings/email") {
8525
8576
  const saved = saveEmailSettings(options.configDir, { host: body.get("host"), port: body.get("port"), secure: body.get("secure"), user: body.get("user"), from: body.get("from"), password: body.get("password"), imapHost: body.get("imapHost"), imapPort: body.get("imapPort") });
@@ -8542,6 +8593,7 @@ export function createDecisionServer(options) {
8542
8593
  }
8543
8594
  // v89: a Google account for email: save the OAuth client and go to Google's consent screen, or disconnect.
8544
8595
  if ((url.pathname === "/settings/google" || url.pathname === "/settings/google/disconnect") && who.role === "approver" && options.configDir !== undefined) {
8596
+ const body = readForm(posted, CONSOLE_FORMS.google);
8545
8597
  const said = (words) => redirect(response, `/settings?said=${encodeURIComponent(words)}#email`);
8546
8598
  if (url.pathname === "/settings/google/disconnect") {
8547
8599
  await disconnectGoogle(options.configDir, options.googleFetch ?? fetch);
@@ -8561,6 +8613,7 @@ export function createDecisionServer(options) {
8561
8613
  return goOutside(response, consent.url, "Going to Google to sign in…", { state: consent.visit.state, path: GOOGLE_CALLBACK, secure: origin.startsWith("https:") });
8562
8614
  }
8563
8615
  if (url.pathname === "/projects/select") {
8616
+ const body = readForm(posted, CONSOLE_FORMS.projectsSelect);
8564
8617
  // Session-only project switching (E2): reads the registry, writes
8565
8618
  // ONLY session.project — the durable upsert lives in /projects/open,
8566
8619
  // which stays an approver's act. This is the one project verb a
@@ -8579,6 +8632,7 @@ export function createDecisionServer(options) {
8579
8632
  return redirect(response, safeReturn(body.get("return")));
8580
8633
  }
8581
8634
  if (url.pathname === "/projects/remove") {
8635
+ const body = readForm(posted, CONSOLE_FORMS.projectsRemove);
8582
8636
  // The reversible remove: off every list and the builder; tasks, results and settings stay. Deleting is
8583
8637
  // Settings → Project → Delete project. The same admission code as `repos remove` (project-admission.ts).
8584
8638
  if (who.via !== "cookie" || !store.isInstanceOperator(who.name))
@@ -8598,6 +8652,7 @@ export function createDecisionServer(options) {
8598
8652
  return redirect(response, `/settings/project?said=${encodeURIComponent(`Removed ${projectName(repo)}. Its tasks and results stay saved.`)}`);
8599
8653
  }
8600
8654
  if (url.pathname === "/projects/open") {
8655
+ const body = readForm(posted, CONSOLE_FORMS.projectsOpen);
8601
8656
  // Sessions only: a bearer caller names its project per request and has
8602
8657
  // no session to mutate — refusing here keeps that boundary legible.
8603
8658
  if (who.via !== "cookie") {
@@ -8637,6 +8692,7 @@ export function createDecisionServer(options) {
8637
8692
  return redirect(response, safeReturn(body.get("return")));
8638
8693
  }
8639
8694
  if (url.pathname === "/tasks/add") {
8695
+ const body = readForm(posted, CONSOLE_FORMS.tasksAdd);
8640
8696
  const project = projectOf(who, request);
8641
8697
  if (project === undefined) {
8642
8698
  return refuse(response, who, 403, "that project is outside what this server was configured to show");
@@ -8734,7 +8790,7 @@ export function createDecisionServer(options) {
8734
8790
  });
8735
8791
  if (!made.ok) {
8736
8792
  const csrf = who.via === "cookie" ? who.session.csrf : "";
8737
- return sendScreen(response, made.reason === "backlog-full" ? 429 : 400, tasksPage(chromeFor(project, "tasks"), familyTasksInView(project).slice(0, 200), null, csrf, made.message, project, { title, goal, not: notThis, touches: body.get("touches") ?? "", acceptance: body.get("acceptance") ?? "", values: body }, store.permissionDefault().mode, store.qualityDefault().mode, store.replacements()));
8793
+ return sendScreen(response, made.reason === "backlog-full" ? 429 : 400, tasksPage(chromeFor(project, "tasks"), familyTasksInView(project).slice(0, 200), null, csrf, made.message, project, { title, goal, not: notThis, touches: body.get("touches") ?? "", acceptance: body.get("acceptance") ?? "", values: body.sent }, store.permissionDefault().mode, store.qualityDefault().mode, store.replacements()));
8738
8794
  }
8739
8795
  const actionContext = requestContext.getStore();
8740
8796
  if (actionContext !== undefined)
@@ -8748,6 +8804,7 @@ export function createDecisionServer(options) {
8748
8804
  return redirect(response, taskHref(made.id));
8749
8805
  }
8750
8806
  if (url.pathname === "/queue/move" || url.pathname === "/queue/note") {
8807
+ const body = readForm(posted, CONSOLE_FORMS.queue);
8751
8808
  const project = projectOf(who, request);
8752
8809
  if (project === undefined) {
8753
8810
  return refuse(response, who, 403, "that project is outside what this server was configured to show");
@@ -8862,6 +8919,7 @@ export function createDecisionServer(options) {
8862
8919
  // hash — the page that shows it renders no self-refreshing script, so
8863
8920
  // the token is never re-rendered.
8864
8921
  if (url.pathname === "/fleet/runner/register") {
8922
+ const body = readForm(posted, CONSOLE_FORMS.runnerRegister);
8865
8923
  if (who.via !== "cookie")
8866
8924
  return refuse(response, who, 403, "runner registration is a browser surface");
8867
8925
  const token = body.get("token") ?? "";
@@ -8891,6 +8949,7 @@ export function createDecisionServer(options) {
8891
8949
  return sendScreen(response, 200, tokenScreen);
8892
8950
  }
8893
8951
  if (url.pathname === "/mode/confirm" || url.pathname === "/mode/sign") {
8952
+ const body = readForm(posted, CONSOLE_FORMS.mode);
8894
8953
  if (who.via !== "cookie")
8895
8954
  return refuse(response, who, 403, "signing a mode is a browser ceremony");
8896
8955
  const project = projectOf(who, request);
@@ -8991,6 +9050,7 @@ export function createDecisionServer(options) {
8991
9050
  return redirect(response, ended ? "/mode?said=the%20mode%20is%20ended%20—%20every%20act%20falls%20back%20to%20its%20own%20ceremony" : "/mode");
8992
9051
  }
8993
9052
  if (url.pathname === "/people/projects") {
9053
+ const body = readForm(posted, CONSOLE_FORMS.peopleProjects);
8994
9054
  if (who.via !== "cookie" || !store.isInstanceOperator(who.name))
8995
9055
  return refuse(response, who, 403, "An instance operator manages project access.", "/people");
8996
9056
  if (!authenticateApprover(store, who.name, body.get("token") ?? "", null).ok)
@@ -9005,6 +9065,7 @@ export function createDecisionServer(options) {
9005
9065
  return redirect(response, "/people?said=Project%20access%20saved");
9006
9066
  }
9007
9067
  if (url.pathname === "/people/invite") {
9068
+ const body = readForm(posted, CONSOLE_FORMS.peopleInvite);
9008
9069
  if (who.via !== "cookie")
9009
9070
  return refuse(response, who, 403, "inviting is a browser surface");
9010
9071
  const token = body.get("token") ?? "";
@@ -9035,6 +9096,7 @@ export function createDecisionServer(options) {
9035
9096
  return sendScreen(response, 200, linkScreen);
9036
9097
  }
9037
9098
  if (url.pathname === "/people/invite-revoke") {
9099
+ const body = readForm(posted, CONSOLE_FORMS.peopleInviteRevoke);
9038
9100
  if (who.via !== "cookie")
9039
9101
  return refuse(response, who, 403, "inviting is a browser surface");
9040
9102
  const token = body.get("token") ?? "";
@@ -9046,6 +9108,7 @@ export function createDecisionServer(options) {
9046
9108
  return redirect(response, `/people?said=${encodeURIComponent(revoked ? "the invite is cancelled — its link is dead" : "that invite was already gone")}`);
9047
9109
  }
9048
9110
  if (url.pathname === "/people/revoke") {
9111
+ const body = readForm(posted, CONSOLE_FORMS.peopleRevoke);
9049
9112
  if (who.via !== "cookie")
9050
9113
  return refuse(response, who, 403, "removing a person is a browser surface");
9051
9114
  const token = body.get("token") ?? "";
@@ -9065,6 +9128,7 @@ export function createDecisionServer(options) {
9065
9128
  return redirect(response, `/people?said=${encodeURIComponent(`${name} can no longer sign in — their sessions, invites, and signed modes ended with them; history stays`)}`);
9066
9129
  }
9067
9130
  if (url.pathname === "/fleet/runner/retire") {
9131
+ const body = readForm(posted, CONSOLE_FORMS.runnerRetire);
9068
9132
  if (who.via !== "cookie")
9069
9133
  return refuse(response, who, 403, "runner retirement is a browser surface");
9070
9134
  const token = body.get("token") ?? "";
@@ -9079,6 +9143,7 @@ export function createDecisionServer(options) {
9079
9143
  }
9080
9144
  const answer = /^\/d\/([0-9]{1,15})\/answer$/.exec(url.pathname);
9081
9145
  if (answer !== null) {
9146
+ const body = readForm(posted, CONSOLE_FORMS.decisionAnswer);
9082
9147
  const id = Number(answer[1]);
9083
9148
  const requestedReturn = body.get("return");
9084
9149
  const decisionBack = requestedReturn === "next"
@@ -9118,6 +9183,7 @@ export function createDecisionServer(options) {
9118
9183
  }
9119
9184
  const contestAct = /^\/contest\/([0-9]{1,15})\/(arm|pick|abandon)$/.exec(url.pathname);
9120
9185
  if (contestAct !== null) {
9186
+ const body = readForm(posted, CONSOLE_FORMS.contest);
9121
9187
  const contestId = Number(contestAct[1]);
9122
9188
  const verb = contestAct[2];
9123
9189
  const data = contestData(contestId);
@@ -9192,10 +9258,11 @@ export function createDecisionServer(options) {
9192
9258
  }
9193
9259
  const attendAct = matchTaskPath(url.pathname, "/(attend-preview|attend|attend-revoke)$");
9194
9260
  if (attendAct !== null) {
9195
- return attendMutation(response, who, attendAct.taskId, attendAct.verb, body, now);
9261
+ return attendMutation(response, who, attendAct.taskId, attendAct.verb, readForm(posted, CONSOLE_FORMS.attend), now);
9196
9262
  }
9197
9263
  const act = matchTaskPath(url.pathname, "/(hold|unhold|requeue|cancel|scope|approve|plan|plan-edit|block|unblock|repair-dependency|next|reopen|steer|follow-up|accept-proof|accept-revision|reject-revision|route|retry-review|complete|merge|confirm-stopped|stop|resume-arm|resume)$");
9198
9264
  if (act !== null && act.verb === "confirm-stopped") {
9265
+ const body = readForm(posted, CONSOLE_FORMS.confirmStopped);
9199
9266
  // Confirming a finished build stopped is an approver's act behind the password, typed again — the same
9200
9267
  // record as `toolroll run settle`, refused while anything of the run may still be running.
9201
9268
  const back = taskHref(act.taskId);
@@ -9227,6 +9294,7 @@ export function createDecisionServer(options) {
9227
9294
  return redirect(response, body.get("return") ? to : back);
9228
9295
  }
9229
9296
  if (act !== null && act.verb === "merge") {
9297
+ const body = readForm(posted, CONSOLE_FORMS.merge);
9230
9298
  // Merging is a person's act behind their password, typed again: a browser session, an approver, the exact
9231
9299
  // result's pull request. The flow re-reads GitHub before merging and records who merged in the ledger.
9232
9300
  if (who.via !== "cookie" || who.role !== "approver")
@@ -9248,9 +9316,10 @@ export function createDecisionServer(options) {
9248
9316
  return redirect(response, `${taskHref(act.taskId)}#merge`);
9249
9317
  }
9250
9318
  if (act !== null) {
9251
- return taskMutation(response, who, act.taskId, act.verb, body, now);
9319
+ return taskMutation(response, who, act.taskId, act.verb, posted, now);
9252
9320
  }
9253
9321
  if (url.pathname === "/projects/onboard-preview" || url.pathname === "/projects/onboard-confirm") {
9322
+ const body = readForm(posted, CONSOLE_FORMS.onboard);
9254
9323
  // Repo onboarding (findings 1-39): bearer and demo refuse BEFORE any
9255
9324
  // gh call; the console flow exists only on root-configured serves.
9256
9325
  if (who.via !== "cookie")
@@ -9375,6 +9444,7 @@ export function createDecisionServer(options) {
9375
9444
  : `${admitted} is ready and open`, 200);
9376
9445
  }
9377
9446
  if (url.pathname === "/push/subscribe") {
9447
+ const body = readForm(posted, CONSOLE_FORMS.pushSubscribe);
9378
9448
  // Enrolling a durable notification sink is a CEREMONY (arc 3 finding
9379
9449
  // 6): browser session + CSRF + the password typed again; bearer
9380
9450
  // identities are machines and are refused outright.
@@ -9414,6 +9484,7 @@ export function createDecisionServer(options) {
9414
9484
  return redirect(response, `/settings?said=${encodeURIComponent("this device now gets a buzz when the plane needs a person")}`);
9415
9485
  }
9416
9486
  if (url.pathname === "/push/remove") {
9487
+ const body = readForm(posted, CONSOLE_FORMS.pushRemove);
9417
9488
  if (who.via !== "cookie")
9418
9489
  return refuse(response, who, 403, "push removal is a browser session's act");
9419
9490
  const id = Number(body.get("id") ?? "");
@@ -9424,6 +9495,7 @@ export function createDecisionServer(options) {
9424
9495
  return redirect(response, `/settings?said=${encodeURIComponent("removed — that device stops receiving pushes")}`);
9425
9496
  }
9426
9497
  if (url.pathname === "/onboarding/phone/dismiss") {
9498
+ const body = readForm(posted, CONSOLE_FORMS.phoneDismiss);
9427
9499
  if (who.via !== "cookie" || who.role !== "approver")
9428
9500
  return refuse(response, who, 403, "An approver puts this away.", "/chat");
9429
9501
  store.recordInstallationFact(PHONE_CARD_FACT, who.name, now);
@@ -9433,6 +9505,7 @@ export function createDecisionServer(options) {
9433
9505
  // conversation it needs no second password; the full form (Advanced) still asks for one.
9434
9506
  // Settings → Lead: the name and persona this person's lead speaks with.
9435
9507
  if (url.pathname === "/settings/lead/identity") {
9508
+ const body = readForm(posted, CONSOLE_FORMS.leadIdentity);
9436
9509
  if (who.via !== "cookie" || who.role !== "approver")
9437
9510
  return refuse(response, who, 403, "An approver names their lead.", "/settings/lead");
9438
9511
  const checked = checkLeadIdentity(body.get("name") ?? "", body.get("persona") ?? "");
@@ -9443,6 +9516,7 @@ export function createDecisionServer(options) {
9443
9516
  }
9444
9517
  // Settings → Lead: what this person's lead knows about them, one line each.
9445
9518
  if (url.pathname === "/settings/lead/about") {
9519
+ const body = readForm(posted, CONSOLE_FORMS.leadAbout);
9446
9520
  if (who.via !== "cookie" || who.role !== "approver")
9447
9521
  return refuse(response, who, 403, "An approver edits what their lead knows about them.", "/settings/lead");
9448
9522
  const checked = checkAboutYou(body.get("about") ?? "");
@@ -9453,6 +9527,7 @@ export function createDecisionServer(options) {
9453
9527
  }
9454
9528
  // Settings → Lead: stop the lead following up on one of your promises.
9455
9529
  if (url.pathname === "/settings/lead/promise/cancel") {
9530
+ const body = readForm(posted, CONSOLE_FORMS.leadPromiseCancel);
9456
9531
  if (who.via !== "cookie" || who.role !== "approver")
9457
9532
  return refuse(response, who, 403, "An approver manages the lead's promises.", "/settings/lead");
9458
9533
  const id = Number(body.get("promise"));
@@ -9471,6 +9546,7 @@ export function createDecisionServer(options) {
9471
9546
  return redirect(response, "/chat");
9472
9547
  }
9473
9548
  if (url.pathname === "/chat/config") {
9549
+ const body = readForm(posted, CONSOLE_FORMS.chatConfig);
9474
9550
  // The console's own door into `config set chat` (operator request:
9475
9551
  // chat lives mainly in the web UI). Same ceremony weight as the CLI
9476
9552
  // verb: the password typed again authenticates the approver, the
@@ -9549,6 +9625,7 @@ export function createDecisionServer(options) {
9549
9625
  }
9550
9626
  // ---- the mate (mate arc §5) ------------------------------------------
9551
9627
  if (url.pathname === "/chat/mate/mint") {
9628
+ const body = readForm(posted, CONSOLE_FORMS.mateMint);
9552
9629
  if (who.via !== "cookie")
9553
9630
  return refuse(response, who, 403, "the mate is a browser surface");
9554
9631
  const back = safeChatReturn(body.get("return"));
@@ -9573,6 +9650,7 @@ export function createDecisionServer(options) {
9573
9650
  return redirect(response, back);
9574
9651
  }
9575
9652
  if (url.pathname === "/chat/mate/follow") {
9653
+ const body = readForm(posted, CONSOLE_FORMS.mateFollow);
9576
9654
  if (who.via !== "cookie")
9577
9655
  return refuse(response, who, 403, "Open the conversation to change automatic updates.");
9578
9656
  const principal = matePrincipal(who), session = store.activeMateSession(who.name), thread = store.liveMateThreadFor(who.name);
@@ -9581,6 +9659,7 @@ export function createDecisionServer(options) {
9581
9659
  return redirect(response, safeChatReturn(body.get("return")));
9582
9660
  }
9583
9661
  if (url.pathname === "/chat/mate/end") {
9662
+ const body = readForm(posted, CONSOLE_FORMS.mateEnd);
9584
9663
  if (who.via !== "cookie")
9585
9664
  return refuse(response, who, 403, "the mate is a browser surface");
9586
9665
  const back = safeChatReturn(body.get("return"));
@@ -9593,6 +9672,7 @@ export function createDecisionServer(options) {
9593
9672
  return redirect(response, back);
9594
9673
  }
9595
9674
  if (url.pathname === "/chat/mate/stop") {
9675
+ const body = readForm(posted, CONSOLE_FORMS.mateStop);
9596
9676
  if (who.via !== "cookie")
9597
9677
  return refuse(response, who, 403, "the mate is a browser surface");
9598
9678
  const back = safeChatReturn(body.get("return"));
@@ -9611,6 +9691,7 @@ export function createDecisionServer(options) {
9611
9691
  }
9612
9692
  const mateProposal = /^\/chat\/proposal\/([0-9]{1,15})\/(confirm|dismiss)$/.exec(url.pathname);
9613
9693
  if (mateProposal !== null) {
9694
+ const body = readForm(posted, CONSOLE_FORMS.mateProposal);
9614
9695
  if (who.via !== "cookie")
9615
9696
  return refuse(response, who, 403, "the mate is a browser surface");
9616
9697
  const back = safeChatReturn(body.get("return"));
@@ -9696,6 +9777,7 @@ export function createDecisionServer(options) {
9696
9777
  // ceiling admits the repo; the card lives on /chat and on the task.
9697
9778
  const coordinatorProposal = /^\/proposals\/([0-9]{1,15})\/(confirm|dismiss)$/.exec(url.pathname);
9698
9779
  if (coordinatorProposal !== null) {
9780
+ const body = readForm(posted, CONSOLE_FORMS.coordinatorProposal);
9699
9781
  if (who.via !== "cookie")
9700
9782
  return refuse(response, who, 403, "proposals are confirmed from the browser or the CLI");
9701
9783
  const principal = matePrincipal(who);
@@ -9715,6 +9797,7 @@ export function createDecisionServer(options) {
9715
9797
  return redirect(response, outcome.ok || outcome.reason !== "needs-confirm" ? back : `${back}${back.includes("?") ? "&" : "?"}said=${encodeURIComponent(outcome.said)}`);
9716
9798
  }
9717
9799
  if (url.pathname === "/chat") {
9800
+ const body = readForm(posted, CONSOLE_FORMS.chat);
9718
9801
  if (who.via !== "cookie")
9719
9802
  return refuse(response, who, 403, "chat is a browser surface");
9720
9803
  const requestedTask = body.get("task");
@@ -9860,6 +9943,7 @@ export function createDecisionServer(options) {
9860
9943
  }
9861
9944
  const chatFile = /^\/chat\/file\/([0-9a-f]{32})$/.exec(url.pathname);
9862
9945
  if (chatFile !== null) {
9946
+ const body = readForm(posted, CONSOLE_FORMS.chatFile);
9863
9947
  if (who.via !== "cookie")
9864
9948
  return refuse(response, who, 403, "chat is a browser surface");
9865
9949
  const key = chatFile[1];
@@ -9933,6 +10017,7 @@ export function createDecisionServer(options) {
9933
10017
  }
9934
10018
  const chatAckPost = /^\/chat\/ack\/([0-9]{1,15})$/.exec(url.pathname);
9935
10019
  if (chatAckPost !== null) {
10020
+ const body = readForm(posted, CONSOLE_FORMS.chatAck);
9936
10021
  if (who.via !== "cookie")
9937
10022
  return refuse(response, who, 403, "acknowledgement is a browser ceremony");
9938
10023
  const turn = store.getChatTurn(Number(chatAckPost[1]));
@@ -9952,6 +10037,7 @@ export function createDecisionServer(options) {
9952
10037
  return redirect(response, "/chat");
9953
10038
  }
9954
10039
  if (["/recipes/prepare", "/recipes/preview", "/recipes/import", "/recipes/launch", "/recipes/save"].includes(url.pathname)) {
10040
+ const body = readForm(posted, CONSOLE_FORMS.recipes);
9955
10041
  const project = projectOf(who, request);
9956
10042
  try {
9957
10043
  if (project === null || project === undefined || !visible(project))
@@ -9964,7 +10050,7 @@ export function createDecisionServer(options) {
9964
10050
  const rootMode = !unscopedMode && ceiling.roots.length > 0;
9965
10051
  if (rootMode && !(await authorizedProject(liveCeiling(), project)))
9966
10052
  throw new RecipeError("The project is outside this server's access.", 403);
9967
- const rechecked = authorizeMutation(request, who, body);
10053
+ const rechecked = authorizeMutation(request, who, readForm(posted, CONSOLE_FORMS.mutationGuard));
9968
10054
  if (rechecked !== null)
9969
10055
  throw new RecipeError(rechecked.message, rechecked.status);
9970
10056
  if (projectOf(who, request) !== project)
@@ -9979,13 +10065,13 @@ export function createDecisionServer(options) {
9979
10065
  if (recipe === null || recipe.repo === null)
9980
10066
  throw new RecipeError("This recipe is not available in this project.", 404);
9981
10067
  try {
9982
- const preview = prepareRecipeRun(store, who.name, project, id, Number(version), recipeAnswersFromForm(body), now);
10068
+ const preview = prepareRecipeRun(store, who.name, project, id, Number(version), recipeAnswersFromForm(body.sent), now);
9983
10069
  return redirect(response, `/recipes/preview?preview=${preview.token}`);
9984
10070
  }
9985
10071
  catch (error) {
9986
10072
  if (!(error instanceof RecipeError))
9987
10073
  throw error;
9988
- return sendScreen(response, error.status, screen("Use recipe", recipeRunHtml(recipe, project, csrf, revision, error.message, body), { chrome: chromeFor(project, "recipes"), functional: { script: recipeScript() } }));
10074
+ return sendScreen(response, error.status, screen("Use recipe", recipeRunHtml(recipe, project, csrf, revision, error.message, body.sent), { chrome: chromeFor(project, "recipes"), functional: { script: recipeScript() } }));
9989
10075
  }
9990
10076
  }
9991
10077
  if (url.pathname === "/recipes/import") {
@@ -10005,13 +10091,13 @@ export function createDecisionServer(options) {
10005
10091
  throw new RecipeError("That recipe version is not available in this project.", 404);
10006
10092
  let document;
10007
10093
  try {
10008
- document = recipeFromForm(body);
10094
+ document = recipeFromForm(body.sent);
10009
10095
  }
10010
10096
  catch (error) {
10011
10097
  if (!(error instanceof RecipeError))
10012
10098
  throw error;
10013
10099
  const fallback = original ?? { ...starterRecipes().find(one => one.id === "small-feature"), id: source };
10014
- return sendScreen(response, error.status, screen("Customize recipe", recipeEditorHtml(fallback, project, csrf, revision, error.message, body, purpose === "recipe"), { chrome: chromeFor(project, "recipes"), functional: { script: recipeScript() } }));
10100
+ return sendScreen(response, error.status, screen("Customize recipe", recipeEditorHtml(fallback, project, csrf, revision, error.message, body.sent, purpose === "recipe"), { chrome: chromeFor(project, "recipes"), functional: { script: recipeScript() } }));
10015
10101
  }
10016
10102
  const preview = createWorkflowPreview(store, who.name, project, document, source, now);
10017
10103
  return redirect(response, `/recipes/preview?preview=${preview.token}${purpose === "recipe" || document.version === 2 ? "&purpose=recipe" : ""}`);
@@ -10040,6 +10126,7 @@ export function createDecisionServer(options) {
10040
10126
  }
10041
10127
  }
10042
10128
  if (url.pathname === "/routines/add") {
10129
+ const body = readForm(posted, CONSOLE_FORMS.routinesAdd);
10043
10130
  // A standing order files into the OPEN project — never a typed path,
10044
10131
  // so the ceiling question never even arises — and lands on its own
10045
10132
  // screen where the approval step-up already lives: filing is cheap,
@@ -10075,11 +10162,11 @@ export function createDecisionServer(options) {
10075
10162
  // 7): the service validates, canonicalizes, digests, and stamps
10076
10163
  // provenance; the admission list makes the ceiling explicit even
10077
10164
  // though `project` was already proved inside it.
10078
- const scheduled = body.has("repeat") ? composerSchedule(body) : { ok: true, schedule: (body.get("schedule") ?? "").trim() };
10165
+ const scheduled = body.has("repeat") ? composerSchedule(body.sent) : { ok: true, schedule: (body.get("schedule") ?? "").trim() };
10079
10166
  if (!scheduled.ok || scheduled.schedule === null) {
10080
10167
  return sendScreen(response, 400, routinesPage(chromeFor(project, "routines"), store.routineTracks(project, now).filter(track => visible(track.routine.repo)), {
10081
10168
  csrf: who.via === "cookie" ? who.session.csrf : "", revision: who.via === "cookie" ? who.session.projectRevision : 0,
10082
- problem: scheduled.ok ? "Choose a recurring schedule." : scheduled.message, values: body,
10169
+ problem: scheduled.ok ? "Choose a recurring schedule." : scheduled.message, values: body.sent,
10083
10170
  }));
10084
10171
  }
10085
10172
  const created = fileRoutineProposal(store, {
@@ -10102,17 +10189,18 @@ export function createDecisionServer(options) {
10102
10189
  return sendScreen(response, created.reason === "duplicate" ? 409 : 400, routinesPage(chromeFor(project, "routines"), tracks, {
10103
10190
  csrf: who.via === "cookie" ? who.session.csrf : "",
10104
10191
  revision: who.via === "cookie" ? who.session.projectRevision : 0,
10105
- problem: created.message, values: body,
10192
+ problem: created.message, values: body.sent,
10106
10193
  }));
10107
10194
  }
10108
10195
  return redirect(response, `/routines/${created.id}`);
10109
10196
  }
10110
10197
  const routineAct = /^\/routines\/([0-9]{1,15})\/(approve|refresh|pause|resume|run-now)$/.exec(url.pathname);
10111
10198
  if (routineAct !== null) {
10112
- return routineMutation(response, who, Number(routineAct[1]), routineAct[2], body, now);
10199
+ return routineMutation(response, who, Number(routineAct[1]), routineAct[2], readForm(posted, CONSOLE_FORMS.routine), now);
10113
10200
  }
10114
10201
  const runNote = /^\/r\/([0-9]{1,15})\/note$/.exec(url.pathname);
10115
10202
  if (runNote !== null) {
10203
+ const body = readForm(posted, CONSOLE_FORMS.runNote);
10116
10204
  // An operator's verdict beside the machine's record (M6): immutable,
10117
10205
  // bounded by the same validator as decision notes, ceiling-checked
10118
10206
  // like every run resource. Ordinary authenticated mutation — no
@@ -10130,6 +10218,7 @@ export function createDecisionServer(options) {
10130
10218
  }
10131
10219
  const diffComment = /^\/r\/([0-9]{1,15})\/comment$/.exec(url.pathname);
10132
10220
  if (diffComment !== null) {
10221
+ const body = readForm(posted, CONSOLE_FORMS.diffComment);
10133
10222
  // A review comment on the IMMUTABLE terminal diff (M6.8): bound to
10134
10223
  // the exact artifact and its hash. Ordinary authenticated mutation —
10135
10224
  // the nonce belongs to the approval screen that later restates the
@@ -10216,6 +10305,7 @@ export function createDecisionServer(options) {
10216
10305
  return redirect(response, `${back}${back.includes("?") ? "&" : "?"}noted=${receipt}#request-changes`);
10217
10306
  }
10218
10307
  if (url.pathname === "/session/editor-links") {
10308
+ const body = readForm(posted, CONSOLE_FORMS.editorLinks);
10219
10309
  // The session half of the editor-link activation (arc 6, finding 1):
10220
10310
  // only the person at the browser can say "this device holds the
10221
10311
  // worktrees". Per-session, dies with the session, grants nothing —
@@ -10230,6 +10320,7 @@ export function createDecisionServer(options) {
10230
10320
  }
10231
10321
  const turnAct = /^\/r\/([0-9]{1,15})\/turn$/.exec(url.pathname);
10232
10322
  if (turnAct !== null) {
10323
+ const body = readForm(posted, CONSOLE_FORMS.turn);
10233
10324
  // The operator's turn (Phase 2E, v2 S1g): cookie-only — a watching
10234
10325
  // person, never a bearer machine — and every hard gate (custody,
10235
10326
  // lease, cap, budget, open decision, single flight) re-proves
@@ -10266,6 +10357,7 @@ export function createDecisionServer(options) {
10266
10357
  }
10267
10358
  const revise = /^\/r\/([0-9]{1,15})\/revise$/.exec(url.pathname);
10268
10359
  if (revise !== null) {
10360
+ const body = readForm(posted, CONSOLE_FORMS.revise);
10269
10361
  // Seal the live comment batch into ONE unapproved revision task with
10270
10362
  // an immutable brief (M6.8). Deterministic — no model reads anything
10271
10363
  // here — and every revision takes its own approval: comments can
@@ -10288,6 +10380,7 @@ export function createDecisionServer(options) {
10288
10380
  // approved command once and seals the log), or file a task to add tests.
10289
10381
  const followUp = /^\/r\/([0-9]{1,15})\/(checks|add-tests)$/.exec(url.pathname);
10290
10382
  if (followUp !== null) {
10383
+ const body = readForm(posted, CONSOLE_FORMS.followUp);
10291
10384
  const id = Number(followUp[1]);
10292
10385
  const found = store.getRun(id);
10293
10386
  if (found === null || !visible(taskRepoOf(found.taskRef)))
@@ -10385,6 +10478,7 @@ export function createDecisionServer(options) {
10385
10478
  }
10386
10479
  const resolve = /^\/i\/([0-9]{1,15})\/resolve$/.exec(url.pathname);
10387
10480
  if (resolve !== null) {
10481
+ const body = readForm(posted, CONSOLE_FORMS.resolveIncident);
10388
10482
  const id = Number(resolve[1]);
10389
10483
  // The ceiling applies to incident mutation exactly as to every other
10390
10484
  // resource (v3 review, finding 3): resolve incident → run → task, and
@@ -10668,7 +10762,7 @@ export function createDecisionServer(options) {
10668
10762
  store.beatAuthorization(minted.authorization.id, now);
10669
10763
  return redirect(response, taskHref(taskId));
10670
10764
  }
10671
- function taskMutation(response, who, taskId, verb, body, now) {
10765
+ function taskMutation(response, who, taskId, verb, posted, now) {
10672
10766
  const ref = store.lookupRef(taskId);
10673
10767
  if (ref === null || store.getTask(taskId) === null) {
10674
10768
  return refuse(response, who, 404, "no such task", "/tasks");
@@ -10678,6 +10772,7 @@ export function createDecisionServer(options) {
10678
10772
  }
10679
10773
  switch (verb) {
10680
10774
  case "steer": {
10775
+ const body = readForm(posted, CONSOLE_FORMS.taskSteer);
10681
10776
  // Steering is a browser session's act, explicitly (arc 1 v2 §3):
10682
10777
  // identify() accepts bearer credentials generically, and those are
10683
10778
  // for machines — a person watching steers, cookie + CSRF only.
@@ -10699,6 +10794,7 @@ export function createDecisionServer(options) {
10699
10794
  return redirect(response, taskHref(taskId));
10700
10795
  }
10701
10796
  case "hold": {
10797
+ const body = readForm(posted, CONSOLE_FORMS.taskHold);
10702
10798
  const reason = (body.get("reason") ?? "").trim() || "held from the console";
10703
10799
  if (reason.length > 200 || hasForbiddenControls(reason)) {
10704
10800
  return taskScreen(response, who, taskId, "that reason will not render, so it will not store", 400);
@@ -10710,6 +10806,7 @@ export function createDecisionServer(options) {
10710
10806
  return redirect(response, taskHref(taskId));
10711
10807
  }
10712
10808
  case "block": {
10809
+ const body = readForm(posted, CONSOLE_FORMS.taskBlock);
10713
10810
  // Chains are scheduling, not authority (chains-and-next review,
10714
10811
  // finding 2): the edge decides WHEN the ready set admits the task;
10715
10812
  // approval still decides WHAT may build. Both ends are re-proved
@@ -10731,6 +10828,7 @@ export function createDecisionServer(options) {
10731
10828
  return redirect(response, taskHref(taskId));
10732
10829
  }
10733
10830
  case "unblock": {
10831
+ const body = readForm(posted, CONSOLE_FORMS.taskBlock);
10734
10832
  const on = (body.get("on") ?? "").trim();
10735
10833
  if (store.openContestFor(ref.id) !== null) {
10736
10834
  return taskScreen(response, who, taskId, "a tournament is running on this task — let it finish, then pick or abandon it", 409);
@@ -10742,6 +10840,7 @@ export function createDecisionServer(options) {
10742
10840
  return redirect(response, taskHref(taskId));
10743
10841
  }
10744
10842
  case "repair-dependency": {
10843
+ const body = readForm(posted, CONSOLE_FORMS.taskRepairDependency);
10745
10844
  const blocker = (body.get("blocker") ?? "").trim();
10746
10845
  const operation = (body.get("operation") ?? "").trim();
10747
10846
  const blockerTask = blocker === "" || !store.blockers(taskId).includes(blocker) ? null : store.getTask(blocker);
@@ -10785,6 +10884,7 @@ export function createDecisionServer(options) {
10785
10884
  return taskScreen(response, who, taskId, "choose whether to try that task again, wait for a different task, or continue without it", 400);
10786
10885
  }
10787
10886
  case "next": {
10887
+ const body = readForm(posted, CONSOLE_FORMS.taskNext);
10788
10888
  if (body.get("undo") !== null) {
10789
10889
  const cleared = store.clearTaskPriority(taskId);
10790
10890
  if (!cleared.ok)
@@ -10805,6 +10905,7 @@ export function createDecisionServer(options) {
10805
10905
  return redirect(response, taskHref(taskId));
10806
10906
  }
10807
10907
  case "reopen": {
10908
+ const body = readForm(posted, CONSOLE_FORMS.taskReopen);
10808
10909
  // Authenticated like every approving act: the session alone may
10809
10910
  // read; resuming external work takes the password, typed again.
10810
10911
  const token = (body.get("token") ?? "").trim();
@@ -10834,6 +10935,7 @@ export function createDecisionServer(options) {
10834
10935
  return redirect(response, taskHref(taskId));
10835
10936
  }
10836
10937
  case "requeue": {
10938
+ const body = readForm(posted, CONSOLE_FORMS.taskRequeue);
10837
10939
  // Retry may carry a note for the next attempt: the same steering note, checked before anything changes.
10838
10940
  const note = (body.get("note") ?? "").trim();
10839
10941
  if (note !== "" && who.via !== "cookie")
@@ -10853,6 +10955,7 @@ export function createDecisionServer(options) {
10853
10955
  return redirect(response, body.get("return") === "inbox" ? "/inbox" : body.get("return") === "next" ? "/next" : taskHref(taskId));
10854
10956
  }
10855
10957
  case "follow-up": {
10958
+ const body = readForm(posted, CONSOLE_FORMS.taskFollowUp);
10856
10959
  // A scout's proposed follow-up, filed by the operator's tap (mate
10857
10960
  // arc §10): the ONE filing door, this task's repository, the scope
10858
10961
  // text stamped as the scout's — mode coverage never seals it.
@@ -10910,6 +11013,7 @@ export function createDecisionServer(options) {
10910
11013
  return redirect(response, taskHref(taskId));
10911
11014
  }
10912
11015
  case "plan-edit": {
11016
+ const body = readForm(posted, CONSOLE_FORMS.taskPlanEdit);
10913
11017
  if (who.via !== "cookie") {
10914
11018
  return refuse(response, who, 403, "editing a plan is a browser session's act");
10915
11019
  }
@@ -10967,6 +11071,7 @@ export function createDecisionServer(options) {
10967
11071
  }
10968
11072
  case "accept-revision":
10969
11073
  case "reject-revision": {
11074
+ const body = readForm(posted, CONSOLE_FORMS.taskRevision);
10970
11075
  // Adaptive execution plans (v44): resolving a revision the builder
10971
11076
  // filed while its authority snapshot no longer matched the run it
10972
11077
  // started under — the ONLY road a 'blocked' plan_revision reaches,
@@ -10997,6 +11102,7 @@ export function createDecisionServer(options) {
10997
11102
  return redirect(response, taskHref(taskId));
10998
11103
  }
10999
11104
  case "cancel": {
11105
+ const body = readForm(posted, CONSOLE_FORMS.taskCancel);
11000
11106
  const reason = body.get("reason") ?? undefined;
11001
11107
  const cancelled = withActor({ account: who.name, lead: false }, () => store.cancelTask(taskId, now, reason));
11002
11108
  if (!cancelled.ok) {
@@ -11010,6 +11116,7 @@ export function createDecisionServer(options) {
11010
11116
  return redirect(response, taskHref(taskId));
11011
11117
  }
11012
11118
  case "route": {
11119
+ const body = readForm(posted, CONSOLE_FORMS.taskRoute);
11013
11120
  // THE AGENTS EDIT (v47): an approver's session declares the risk,
11014
11121
  // overrides one role to an exact agent, or clears an override — ONE
11015
11122
  // authenticated transaction in the store that CAS-checks the scope
@@ -11095,6 +11202,7 @@ export function createDecisionServer(options) {
11095
11202
  return redirect(response, toChat ? target : `${target}#agents`);
11096
11203
  }
11097
11204
  case "scope": {
11205
+ const body = readForm(posted, CONSOLE_FORMS.taskScope);
11098
11206
  const sawDigest = body.get("sawDigest");
11099
11207
  const permissionGiven = body.get("permission-mode");
11100
11208
  if (permissionGiven !== null && permissionGiven !== "" && permissionGiven !== "auto" && permissionGiven !== "bypassPermissions") {
@@ -11290,7 +11398,7 @@ export function createDecisionServer(options) {
11290
11398
  if (!saved.ok) {
11291
11399
  // Refused because the plan moved on: the draft comes back with the version now on file, so saving it
11292
11400
  // again (having read the reason) can succeed instead of being refused the same way.
11293
- const draft = new URLSearchParams(body);
11401
+ const draft = new URLSearchParams(body.sent);
11294
11402
  if (saved.status === 409)
11295
11403
  draft.set("sawDigest", store.getScope(taskId)?.digest ?? "");
11296
11404
  return taskScreen(response, who, taskId, saved.message, saved.status, draft);
@@ -11298,6 +11406,7 @@ export function createDecisionServer(options) {
11298
11406
  return redirect(response, taskHref(taskId));
11299
11407
  }
11300
11408
  case "approve": {
11409
+ const body = readForm(posted, CONSOLE_FORMS.taskApprove);
11301
11410
  const requestedReturn = body.get("return");
11302
11411
  const approvalBack = requestedReturn === "next"
11303
11412
  ? "/next"
@@ -11384,6 +11493,7 @@ export function createDecisionServer(options) {
11384
11493
  return redirect(response, approvalBack);
11385
11494
  }
11386
11495
  case "accept-proof": {
11496
+ const body = readForm(posted, CONSOLE_FORMS.taskAcceptProof);
11387
11497
  // Accepting is a person's act, like approving a scope (Priority
11388
11498
  // 2): a cookie session only, never a bearer credential.
11389
11499
  if (who.via !== "cookie") {
@@ -11420,6 +11530,7 @@ export function createDecisionServer(options) {
11420
11530
  return redirect(response, body.get("return") === null ? taskHref(taskId) : safeReturn(body.get("return")));
11421
11531
  }
11422
11532
  case "complete": {
11533
+ const body = readForm(posted, CONSOLE_FORMS.taskComplete);
11423
11534
  if (who.via !== "cookie" || who.role !== "approver")
11424
11535
  return refuse(response, who, 403, "Only an approver can mark a result complete.");
11425
11536
  const principal = matePrincipal(who);
@@ -11465,6 +11576,7 @@ export function createDecisionServer(options) {
11465
11576
  return taskScreen(response, who, taskId, "Separate agent reviews have retired. Open the saved result to mark it complete or request a revision.", 410);
11466
11577
  }
11467
11578
  case "stop": {
11579
+ const body = readForm(posted, CONSOLE_FORMS.taskStop);
11468
11580
  // The exact-run stop (v52): a browser session's act, an approver's
11469
11581
  // act, naming ONE run. The store's transaction proves the run is
11470
11582
  // this task's current live attempt and records who asked before
@@ -11496,9 +11608,11 @@ export function createDecisionServer(options) {
11496
11608
  return redirect(response, back === "chat" ? `${taskChatHref(taskId)}#task-control` : `${taskHref(taskId)}#task-control`);
11497
11609
  }
11498
11610
  case "resume-arm": {
11611
+ const body = readForm(posted, CONSOLE_FORMS.taskResume);
11499
11612
  return armTaskResume(response, who, taskId, (body.get("run") ?? "").trim(), body.get("return") === "chat" ? "chat" : "task", now);
11500
11613
  }
11501
11614
  case "resume": {
11615
+ const body = readForm(posted, CONSOLE_FORMS.taskResume);
11502
11616
  // The resume itself (v52): password typed again + the durable nonce
11503
11617
  // consumed INSIDE the store's resume transaction against the digest
11504
11618
  // re-derived from live state. A replay finds the nonce spent; a