toolroll 0.9.52 → 0.9.53

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 (146) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +55 -80
  3. package/dist/agentconfig.d.ts +11 -36
  4. package/dist/agentconfig.js +3 -62
  5. package/dist/approval-policy.d.ts +1 -1
  6. package/dist/approval-policy.js +1 -1
  7. package/dist/board.d.ts +3 -19
  8. package/dist/board.js +4 -42
  9. package/dist/browser/workspace.js +18 -18
  10. package/dist/browser-workspace.d.ts +2 -1
  11. package/dist/builder.d.ts +9 -40
  12. package/dist/builder.js +50 -257
  13. package/dist/chat-actions.js +3 -14
  14. package/dist/chat-controls.d.ts +0 -4
  15. package/dist/chat-controls.js +0 -1
  16. package/dist/chat-decide.js +1 -3
  17. package/dist/chat-delivery.d.ts +0 -2
  18. package/dist/chat-delivery.js +0 -1
  19. package/dist/chat-quiet.js +2 -2
  20. package/dist/chat-task-actions.d.ts +1 -1
  21. package/dist/chat-task-actions.js +0 -2
  22. package/dist/claim.d.ts +79 -244
  23. package/dist/claim.js +688 -1023
  24. package/dist/cli.d.ts +2 -0
  25. package/dist/cli.js +10 -2
  26. package/dist/contracts/cli.d.ts +0 -13
  27. package/dist/contracts/cli.js +0 -2
  28. package/dist/contracts/console-api.d.ts +5 -10
  29. package/dist/contracts/console-api.js +4 -10
  30. package/dist/contracts/flow.d.ts +50 -0
  31. package/dist/contracts/flow.js +26 -1
  32. package/dist/contracts/lead-tools.d.ts +0 -13
  33. package/dist/contracts/lead-tools.js +3 -3
  34. package/dist/contracts/store-json.d.ts +1 -25
  35. package/dist/contracts/store-json.js +3 -13
  36. package/dist/converse.d.ts +1 -11
  37. package/dist/converse.js +6 -38
  38. package/dist/coordinator.d.ts +0 -1
  39. package/dist/coordinator.js +3 -5
  40. package/dist/demo.js +16 -26
  41. package/dist/desktop-update-gate.js +0 -1
  42. package/dist/desktop-update.js +1 -3
  43. package/dist/dispatch.d.ts +1 -1
  44. package/dist/dispatch.js +1 -1
  45. package/dist/dispose.d.ts +2 -3
  46. package/dist/dispose.js +17 -92
  47. package/dist/envelope.js +0 -3
  48. package/dist/exec.d.ts +0 -64
  49. package/dist/exec.js +3 -356
  50. package/dist/exhaustion.d.ts +22 -76
  51. package/dist/exhaustion.js +14 -140
  52. package/dist/export.js +4 -4
  53. package/dist/flow-engine.js +1 -1
  54. package/dist/flow-schedule.d.ts +154 -0
  55. package/dist/flow-schedule.js +321 -0
  56. package/dist/flow-share.js +1 -1
  57. package/dist/flow-triggers.d.ts +6 -1
  58. package/dist/flow-triggers.js +65 -10
  59. package/dist/flows-cli.js +8 -4
  60. package/dist/flows-ui.js +2 -0
  61. package/dist/flows.js +1 -1
  62. package/dist/guides.js +9 -29
  63. package/dist/invoke.d.ts +1 -24
  64. package/dist/invoke.js +17 -259
  65. package/dist/lead-status.js +2 -4
  66. package/dist/lead-voice.js +1 -1
  67. package/dist/mate-doors.d.ts +1 -2
  68. package/dist/mate-doors.js +12 -25
  69. package/dist/mate-tools.d.ts +2 -2
  70. package/dist/mate-tools.js +10 -22
  71. package/dist/modes.d.ts +6 -7
  72. package/dist/modes.js +7 -13
  73. package/dist/operate-remote-arguments.js +2 -6
  74. package/dist/operate-remote.js +6 -7
  75. package/dist/operate.d.ts +0 -6
  76. package/dist/operate.js +222 -2247
  77. package/dist/phase-routing.d.ts +13 -23
  78. package/dist/phase-routing.js +13 -35
  79. package/dist/plan-auto.js +2 -2
  80. package/dist/planner-source.js +2 -1
  81. package/dist/policy.d.ts +1 -4
  82. package/dist/policy.js +0 -8
  83. package/dist/process-recovery-eligibility.js +2 -3
  84. package/dist/project-delete.js +3 -15
  85. package/dist/proposal.d.ts +4 -24
  86. package/dist/proposal.js +3 -88
  87. package/dist/provider-auth.d.ts +3 -3
  88. package/dist/provider-auth.js +4 -6
  89. package/dist/provider.d.ts +11 -25
  90. package/dist/provider.js +4 -36
  91. package/dist/push.js +1 -1
  92. package/dist/recipe-ui.js +6 -6
  93. package/dist/recipes.d.ts +3 -3
  94. package/dist/recipes.js +27 -23
  95. package/dist/run-closeout.d.ts +1 -1
  96. package/dist/run-closeout.js +1 -4
  97. package/dist/scope.d.ts +11 -65
  98. package/dist/scope.js +17 -82
  99. package/dist/serve.js +15 -24
  100. package/dist/server/chat.js +7 -30
  101. package/dist/server/flows.js +11 -10
  102. package/dist/server/guards.js +0 -1
  103. package/dist/server/pages-assets.js +1 -1
  104. package/dist/server/pages.js +0 -2
  105. package/dist/server/people-tokens.js +0 -4
  106. package/dist/server/route-table.d.ts +1 -1
  107. package/dist/server/route-table.js +2 -17
  108. package/dist/server/settings.js +1 -8
  109. package/dist/server/shared.d.ts +28 -212
  110. package/dist/server/shared.js +71 -780
  111. package/dist/server/task-views.js +9 -76
  112. package/dist/server/tasks.d.ts +1 -50
  113. package/dist/server/tasks.js +36 -946
  114. package/dist/setup-guide.d.ts +2 -2
  115. package/dist/slack-chat.d.ts +0 -2
  116. package/dist/slack-chat.js +0 -1
  117. package/dist/store.d.ts +105 -1340
  118. package/dist/store.js +706 -4010
  119. package/dist/surface.js +10 -24
  120. package/dist/task-composer.d.ts +1 -1
  121. package/dist/task-composer.js +1 -1
  122. package/dist/task-control.d.ts +3 -10
  123. package/dist/task-control.js +2 -7
  124. package/dist/teammate-desk.js +1 -1
  125. package/dist/teammates-ui.js +1 -1
  126. package/dist/telegram-mate.d.ts +0 -3
  127. package/dist/telegram-mate.js +0 -1
  128. package/dist/templates.d.ts +11 -7
  129. package/dist/templates.js +12 -9
  130. package/dist/toolroll-update.d.ts +10 -0
  131. package/dist/toolroll-update.js +56 -4
  132. package/dist/work-index.js +2 -6
  133. package/dist/workspace-ui.js +1 -1
  134. package/package.json +1 -1
  135. package/scripts/recovery-canary.mjs +3 -3
  136. package/dist/contest.d.ts +0 -318
  137. package/dist/contest.js +0 -753
  138. package/dist/held.d.ts +0 -150
  139. package/dist/held.js +0 -799
  140. package/dist/liveness.d.ts +0 -30
  141. package/dist/liveness.js +0 -42
  142. package/dist/pricing.d.ts +0 -45
  143. package/dist/pricing.js +0 -77
  144. package/dist/routine.d.ts +0 -228
  145. package/dist/routine.js +0 -764
  146. package/dist/supervisor.mjs +0 -297
package/dist/scope.js CHANGED
@@ -36,7 +36,7 @@ import { z } from "zod";
36
36
  import {} from "./contracts/acceptance-terms.js";
37
37
  import { parseContract } from "./contracts/contract.js";
38
38
  import { readAcceptance, readScopeTerms, storedRubricSchema } from "./contracts/scope.js";
39
- import { NO_READINESS, exactModelId, isRiskLevel, legOf, projectRoute, routeDigestOf, routeFromJson, routeJsonProblem, routeWords, } from "./phase-routing.js";
39
+ import { NO_READINESS, exactModelId, legOf, projectRoute, routeDigestOf, routeFromJson, routeJsonProblem, routeWords, } from "./phase-routing.js";
40
40
  /** Same shape as a runner's credential, for the same reasons. */
41
41
  function mintToken() {
42
42
  return randomBytes(32).toString("base64url");
@@ -129,9 +129,10 @@ export function profileDigestOf(profile) {
129
129
  .digest("hex")
130
130
  .slice(0, 32);
131
131
  }
132
- // ---- fallback chains (v30) ------------------------------------------------
133
- // The version this build stamps into a chain snapshot AND folds into the
134
- // chain digest. Bumping it is a re-attestation, never a silent change.
132
+ // ---- fallback chains (v30-v114; removed in v115) --------------------------
133
+ // History only: an approval sealed over a fallback chain still decodes and
134
+ // its digest still re-derives, so old tasks verify and render. Nothing
135
+ // files, seals, or dispatches a chain any more.
135
136
  export const CHAIN_DIGEST_VERSION = 1;
136
137
  /** The stored snapshot bytes for a chain: the version rides IN the
137
138
  * snapshot, exactly like a single profile's. */
@@ -148,19 +149,6 @@ export function chainDigestOf(entries) {
148
149
  .digest("hex")
149
150
  .slice(0, 32);
150
151
  }
151
- /**
152
- * One chain entry's BINDING digest (E3d): the profile digest + auth mode
153
- * under their own domain. A run row pins this at admission (or base-cycle
154
- * open), and the dispatch proof re-derives it from the approved chain at
155
- * the run's index — so nothing downstream can swap WHICH entry a run
156
- * spends as, in model or in credential.
157
- */
158
- export function entryDigestOf(entry) {
159
- return createHash("sha256")
160
- .update(`standing-orders:chain-entry:v${CHAIN_DIGEST_VERSION}:${profileDigestOf(entry.profile)}:${entry.authMode}`, "utf8")
161
- .digest("hex")
162
- .slice(0, 32);
163
- }
164
152
  /** Strict re-hydration of a stored chain — every entry proved through the
165
153
  * single-profile rehydrator; anything unexpected is null. */
166
154
  export function chainFromJson(json) {
@@ -409,7 +397,7 @@ export function profileFromJson(json) {
409
397
  }
410
398
  /**
411
399
  * Parse a rubric from already-JSON-parsed input (the planner's handoff, a
412
- * console form, a routine or template definition): fail closed, every
400
+ * console form, a template or a schedule's terms): fail closed, every
413
401
  * problem reported at once, each naming its path — the acceptance contract
414
402
  * (contracts/scope.ts) reads it. An absent or empty `value` parses to `[]`
415
403
  * with no problems: whether that is ALLOWED is a question for the caller
@@ -569,7 +557,7 @@ route) {
569
557
  .slice(0, 32);
570
558
  }
571
559
  export function propose(store, input) {
572
- const { taskId, goal, outOfScope = null, touches = [], budgetMicrousd = null, acceptance = [], qualityMode = "default", now, mutation = {}, profile, permissionMode, posture, proposedVia = null, riskLevel } = input;
560
+ const { taskId, goal, outOfScope = null, touches = [], budgetMicrousd = null, acceptance = [], qualityMode = "default", now, mutation = {}, profile, permissionMode, posture, proposedVia = null } = input;
573
561
  const draft = { goal, outOfScope, touches: [...touches], budgetMicrousd, acceptance: [...acceptance], qualityMode, candidate: input.candidate ?? null };
574
562
  const previous = store.getScope(taskId);
575
563
  const scope = {
@@ -590,7 +578,6 @@ export function propose(store, input) {
590
578
  ...(profile === undefined ? {} : { profile }),
591
579
  ...(permissionMode === undefined ? {} : { permissionMode }),
592
580
  ...(input.qualityMode === undefined ? {} : { qualityMode: input.qualityMode }),
593
- ...(riskLevel === undefined || !isRiskLevel(riskLevel) ? {} : { riskLevel }),
594
581
  ...(posture === undefined ? {} : { posture }),
595
582
  proposedVia,
596
583
  });
@@ -755,7 +742,7 @@ export function authenticateAccount(store, by, secret) {
755
742
  * v100: someone who signed in with the identity provider moments ago has no
756
743
  * password to type again. The console marks such a request with this, for
757
744
  * that person only, and an EMPTY password then stands for the fresh sign-in
758
- * at every step-up that reaches `authenticateApprover` (scope and routine
745
+ * at every step-up that reaches `authenticateApprover` (scope
759
746
  * approval, chat-action review, the console's own ceremonies). A typed
760
747
  * password is always checked, and nothing else stands in.
761
748
  */
@@ -990,16 +977,10 @@ export function scopeAuthorityOf(scope, env = {}) {
990
977
  if (profile === null) {
991
978
  return { ok: false, reason: "profile", problem: profileJson === null ? "the scope carries no agent profile" : "the scope's agent profile cannot be read exactly (unknown keys, an unsafe number, or a clock no timer can hold)" };
992
979
  }
993
- const chainJson = scope.proposedChainJson ?? null;
994
- const chain = chainJson === null ? null : chainFromJson(chainJson);
995
- if (chainJson !== null && chain === null) {
996
- return { ok: false, reason: "chain", problem: "the scope's fallback chain cannot be read exactly (unknown keys, a malformed auth mode, or an entry that is not a whole profile)" };
997
- }
998
- if (chain !== null) {
999
- const base = chain[0];
1000
- if (base === undefined || profileDigestOf(base.profile) !== profileDigestOf(profile)) {
1001
- return { ok: false, reason: "parity", problem: "the scope's agent profile is not its fallback chain's first entry" };
1002
- }
980
+ // A scope filed with fallback agents (v30-v114) holds no authority now
981
+ // that fallback chains are gone: saving it again chooses its agents.
982
+ if (scope.proposedChainJson != null) {
983
+ return { ok: false, reason: "chain", problem: "this scope named fallback agents, which Toolroll no longer uses — save the scope again to choose its agents" };
1003
984
  }
1004
985
  const routeJson = scope.proposedRouteJson ?? null;
1005
986
  const route = routeFromJson(routeJson);
@@ -1009,7 +990,7 @@ export function scopeAuthorityOf(scope, env = {}) {
1009
990
  const parity = routeParityProblem(route, profile, { riskLevel: scope.riskLevel ?? "routine", qualityMode: scope.qualityMode ?? "default" });
1010
991
  if (parity !== null)
1011
992
  return { ok: false, reason: "parity", problem: parity };
1012
- const digest = digestOf({ goal: scope.goal, outOfScope: scope.outOfScope, touches: scope.touches, budgetMicrousd: scope.budgetMicrousd, acceptance: scope.acceptance, candidate: scope.candidate ?? null, qualityMode: scope.qualityMode ?? "default" }, chain !== null ? { chain } : profile, route);
993
+ const digest = digestOf({ goal: scope.goal, outOfScope: scope.outOfScope, touches: scope.touches, budgetMicrousd: scope.budgetMicrousd, acceptance: scope.acceptance, candidate: scope.candidate ?? null, qualityMode: scope.qualityMode ?? "default" }, profile, route);
1013
994
  if (digest !== scope.digest) {
1014
995
  return { ok: false, reason: "digest", problem: "the scope's reference does not re-derive from its own terms, agents, and route — file it again" };
1015
996
  }
@@ -1018,31 +999,15 @@ export function scopeAuthorityOf(scope, env = {}) {
1018
999
  // strict reader — a present mode file that says neither word closes
1019
1000
  // the door in its words, never the default a lenient read would coerce
1020
1001
  // it to (the spawn reads the same way, so nothing sealed here spends
1021
- // on a credential the operator never named). A chain filing carries
1022
- // its base entry's pinned mode and must agree with the live file too.
1002
+ // on a credential the operator never named).
1023
1003
  let authMode = null;
1024
1004
  if (env.authMode !== undefined) {
1025
1005
  const read = env.authMode(profile.provider);
1026
1006
  if (!read.ok)
1027
1007
  return { ok: false, reason: "auth-mode", problem: read.problem };
1028
1008
  authMode = read.mode;
1029
- // PINNED AND LIVE AUTH AGREE, or nothing (final authority closure): a
1030
- // chain filed under one credential for its base entry, read beside a
1031
- // live mode file that now says the other, is a scope whose signed
1032
- // words no longer describe what would spend — a subscription chain
1033
- // approved after the operator moved to an API key would bill the
1034
- // key. It holds no authority until it is filed again under today's
1035
- // mode.
1036
- const base = chain === null ? null : chain[0] ?? null;
1037
- if (base !== null && base.authMode !== authMode) {
1038
- return {
1039
- ok: false,
1040
- reason: "auth-mode",
1041
- problem: `the scope's fallback chain pins its base entry to ${base.authMode === "subscription" ? "your subscription" : "your API key"} for ${profile.provider}, but the live auth mode is now ${authMode === "subscription" ? "your subscription" : "your API key"} — re-file the scope under today's mode`,
1042
- };
1043
- }
1044
1009
  }
1045
- return { ok: true, profile, chain, route, digest, authMode };
1010
+ return { ok: true, profile, route, digest, authMode };
1046
1011
  }
1047
1012
  /** The rubric's approval-card lines (v39): one per criterion, the id in
1048
1013
  * front (the exact string a proof must answer by) and its required
@@ -1075,32 +1040,13 @@ export function describeScope(scope, readiness = NO_READINESS) {
1075
1040
  ...(scope.budgetMicrousd === null
1076
1041
  ? []
1077
1042
  : [` budget $${(scope.budgetMicrousd / 1_000_000).toFixed(2)} per build attempt — the agent is stopped at this figure`]),
1078
- // The FALLBACK CHAIN, in the words the yes agrees to (Layer F): when
1079
- // this scope files under configured fallbacks, the digest above binds
1080
- // the WHOLE ordered chain — so the card says every entry, credential
1081
- // included, before anyone signs.
1082
- ...chainWords(chainFromJson(scope.proposedChainJson ?? null)),
1083
- // The ROUTE, in the words the yes agrees to (v47): risk, posture, and
1084
- // every leg with its reason and readiness — before anyone signs.
1085
- ...(scope.riskLevel === undefined || scope.riskLevel === "routine" ? [] : [` risk ${scope.riskLevel}`]),
1043
+ // The ROUTE, in the words the yes agrees to (v47): posture and every
1044
+ // leg with its reason and readiness — before anyone signs.
1086
1045
  ...scopeRouteWords(scope, readiness),
1087
1046
  ` reference ${scope.digest}`,
1088
1047
  ` approved ${describeApproval(approval)}`,
1089
1048
  ];
1090
1049
  }
1091
- /** The chain's approval-card lines; empty for a single-profile scope. */
1092
- export function chainWords(chain) {
1093
- if (chain === null || chain.length < 2)
1094
- return [];
1095
- const credential = (mode) => mode === "subscription" ? "your subscription" : "your API key";
1096
- const lines = [];
1097
- const base = chain[0];
1098
- lines.push(` runs on ${base.profile.provider} (${base.profile.model}) — ${credential(base.authMode)}`);
1099
- for (const entry of chain.slice(1)) {
1100
- lines.push(` if that runs out falls back to ${entry.profile.provider} (${entry.profile.model}) — ${credential(entry.authMode)}${entry.authMode === "api-key" ? "; spend moves to that account" : ""}`);
1101
- }
1102
- return lines;
1103
- }
1104
1050
  function describeApproval(approval) {
1105
1051
  if (approval.approved)
1106
1052
  return `yes, by ${approval.by} at ${approval.at}`;
@@ -1109,14 +1055,3 @@ function describeApproval(approval) {
1109
1055
  }
1110
1056
  return "no — nothing will build this until somebody agrees to it";
1111
1057
  }
1112
- /** Deterministic bytes: sorted keys, undefined dropped — the signed text. */
1113
- export function attendedTermsJson(terms) {
1114
- return canonicalJson(terms);
1115
- }
1116
- /** The composite digest one password signs (ruling 12), domain-separated. */
1117
- export function attendedDigestOf(terms) {
1118
- return createHash("sha256")
1119
- .update(`standing-orders:attended:${attendedTermsJson(terms)}`)
1120
- .digest("hex")
1121
- .slice(0, 32);
1122
- }
package/dist/serve.js CHANGED
@@ -115,7 +115,7 @@ import { createFlowsHandlers } from './server/flows.js';
115
115
  import { createPagesHandlers } from './server/pages.js';
116
116
  import { createPeopleHandlers } from './server/people-tokens.js';
117
117
  import { createSettingsHandlers } from './server/settings.js';
118
- import { beatScript, BODY_CAP, chromeScript, decisionsFor, DEMO_BANNER, DEMO_BANNER_SHORT, escape, focusDocument, form, KBD_HELP, LEAD_BY_DEFAULT_FACT, loginHref, matchTaskPath, mateBrowserMessages, mateChatVersion, NO_PROJECT, NO_TOUCH_FRAGMENTS, NONCE_CAP, NONCE_TTL_MS, page, PersistentSessions, pinnedTheme, projectChatHref, QUEUE_VIEW, redactedPath, redirect, refuse, requestContext, respond, safeReturn, screen, SENSITIVE_INPUT, SESSION_ABSOLUTE_MS, SESSION_IDLE_MS, shell, SHUTDOWN_WAIT_MS, sidebarScript, SIGN_IN_LINK_MS, SIGN_IN_LINK_PATH, ssoStepUps, TASK_FORM_BODY_CAP, taskChatHref, teamProposalCardParts, wrongHostPage } from "./server/shared.js";
118
+ import { BODY_CAP, chromeScript, decisionsFor, DEMO_BANNER, DEMO_BANNER_SHORT, escape, focusDocument, form, KBD_HELP, LEAD_BY_DEFAULT_FACT, loginHref, matchTaskPath, mateBrowserMessages, mateChatVersion, NO_PROJECT, NO_TOUCH_FRAGMENTS, NONCE_CAP, NONCE_TTL_MS, page, PersistentSessions, pinnedTheme, projectChatHref, QUEUE_VIEW, redactedPath, redirect, refuse, requestContext, respond, safeReturn, screen, SENSITIVE_INPUT, SESSION_ABSOLUTE_MS, SESSION_IDLE_MS, shell, SHUTDOWN_WAIT_MS, sidebarScript, SIGN_IN_LINK_MS, SIGN_IN_LINK_PATH, ssoStepUps, TASK_FORM_BODY_CAP, taskChatHref, teamProposalCardParts, wrongHostPage } from "./server/shared.js";
119
119
  import { createTasksHandlers } from './server/tasks.js';
120
120
  import { DEFAULT_GUARD_POLICY, passwordGuardOf, provenPasswordAccount, SourceBudget, withPasswordSource } from "./sign-in-guard.js";
121
121
  import { sourceKey } from "./source-key.js";
@@ -738,7 +738,7 @@ export function createDecisionServer(options) {
738
738
  }
739
739
  return handlePost(route, url, who, request, response, body);
740
740
  };
741
- if (method === "GET" || url.pathname === "/session/attended-beats")
741
+ if (method === "GET")
742
742
  return projectAuthority.run({ actor: who.name, repo: target.repo }, execute);
743
743
  // Request acceptance is separate from durable work completion. Never
744
744
  // persist a body, query string, token, password, or arbitrary URL.
@@ -797,18 +797,16 @@ export function createDecisionServer(options) {
797
797
  const task = matchTaskPath(url.pathname, "(?:/([a-z-]+))?$");
798
798
  if (task !== null)
799
799
  return { repo: store.lookupRef(task.taskId)?.repo ?? null, taskId: task.taskId, runId: null, action: `task ${task.verb || "view"}` };
800
- const resource = /^\/(r|d|i|routines)\/([0-9]{1,15})(?:\/([a-z-]+)(?:\/[0-9]+)?)?$/.exec(url.pathname);
800
+ const resource = /^\/(r|d|i)\/([0-9]{1,15})(?:\/([a-z-]+)(?:\/[0-9]+)?)?$/.exec(url.pathname);
801
801
  if (resource !== null) {
802
802
  const id = Number(resource[2]);
803
- if (resource[1] === "routines")
804
- return { repo: store.getRoutine(id)?.repo ?? null, taskId: null, runId: null, action: `routine ${resource[3] ?? "view"}` };
805
803
  const runId = resource[1] === "r" ? id : resource[1] === "d" ? store.getDecision(id)?.run ?? null : store.openIncidents().find(one => one.id === id)?.run ?? null;
806
804
  const run = runId === null ? null : store.getRun(runId);
807
805
  const ref = run === null ? null : store.refForId(run.taskRef);
808
806
  return { repo: ref?.repo ?? null, taskId: ref?.externalId ?? null, runId, action: `${resource[1] === "d" ? "decision" : resource[1] === "i" ? "incident" : "run"} ${resource[3] ?? "view"}` };
809
807
  }
810
- const known = new Set(["/recipes/prepare", "/recipes/preview", "/recipes/import", "/recipes/save", "/recipes/launch", "/tasks/add", "/queue/move", "/queue/note", "/routines/add", "/people/invite", "/people/invite-revoke", "/people/revoke", "/people/projects", "/projects/select", "/projects/open", "/mode/confirm", "/mode/sign", "/mode/revoke", "/settings/chat-approval/save", "/settings/chat-approval/off"]);
811
- const placed = ["/tasks/add", "/routines/add"].includes(url.pathname) ? body?.get("repo")?.trim() : null;
808
+ const known = new Set(["/recipes/prepare", "/recipes/preview", "/recipes/import", "/recipes/save", "/recipes/launch", "/tasks/add", "/queue/move", "/queue/note", "/people/invite", "/people/invite-revoke", "/people/revoke", "/people/projects", "/projects/select", "/projects/open", "/mode/confirm", "/mode/sign", "/mode/revoke", "/settings/chat-approval/save", "/settings/chat-approval/off"]);
809
+ const placed = url.pathname === "/tasks/add" ? body?.get("repo")?.trim() : null;
812
810
  return { repo: url.pathname.startsWith("/people/") ? null : placed ? canonicalProject(placed) ?? placed : projectOf(who, request) ?? null, taskId: null, runId: null,
813
811
  action: known.has(url.pathname) ? url.pathname.slice(1).replaceAll("/", " ") : "console request" };
814
812
  }
@@ -1170,7 +1168,7 @@ export function createDecisionServer(options) {
1170
1168
  { label: "board", href: "/board?scope=all" },
1171
1169
  { label: "queue", href: QUEUE_VIEW },
1172
1170
  { label: "workbench", href: "/workbench" },
1173
- { label: "routines", href: "/routines" },
1171
+ { label: "flows", href: "/flows" },
1174
1172
  { label: "done", href: "/done" },
1175
1173
  { label: "review cockpit", href: "/review" },
1176
1174
  { label: "task list", href: "/tasks" },
@@ -1188,7 +1186,7 @@ export function createDecisionServer(options) {
1188
1186
  entries.push({ label: `${one.id} — ${one.title}`, href: `/t/${encodeURIComponent(one.id)}` });
1189
1187
  }
1190
1188
  const saturated = open.length > 200;
1191
- const safeEntries = restricted() ? entries.filter(one => ["/work", "/work?view=needs-you", "/", "/board", "/routines", "/done", "/review", "/tasks", "/runs", "/projects", "/ledger"].includes(one.href) || one.href.startsWith("/t/")) : entries;
1189
+ const safeEntries = restricted() ? entries.filter(one => ["/work", "/work?view=needs-you", "/", "/board", "/flows", "/done", "/review", "/tasks", "/runs", "/projects", "/ledger"].includes(one.href) || one.href.startsWith("/t/")) : entries;
1192
1190
  const json = JSON.stringify(saturated ? [...safeEntries, { label: "… more in the task list", href: "/tasks" }] : safeEntries)
1193
1191
  .replace(/</g, "\\u003c");
1194
1192
  return `<script type="application/json" id="palette-index">${json}</script>`;
@@ -1343,7 +1341,7 @@ export function createDecisionServer(options) {
1343
1341
  // A page with its own view keeps its script; the chat page runs the result script.
1344
1342
  const functional = conversation === null || extras.view ? (s.functional?.script ?? '') : RESULT_REVIEW_SCRIPT;
1345
1343
  const document = shell(s.title, s.body, { chrome: s.chrome, sensitive });
1346
- return page(response, status, browserWorkspaceDocument(document, workspace, nonce, functional + beatScript(!restricted()) + MOBILE_VIEWPORT_SCRIPT), nonce, true);
1344
+ return page(response, status, browserWorkspaceDocument(document, workspace, nonce, functional + MOBILE_VIEWPORT_SCRIPT), nonce, true);
1347
1345
  }
1348
1346
  // A one-time secret on screen (a token, an invite link, a pairing code): the
1349
1347
  // page carries no script of any kind, so it leaves the workspace for a
@@ -1352,13 +1350,9 @@ export function createDecisionServer(options) {
1352
1350
  return page(response, status, focusDocument(s.title, s.body));
1353
1351
  const chromeLayer = !sensitive && s.chrome !== undefined;
1354
1352
  const functional = s.functional?.script ?? "";
1355
- // Sensitive pages strip the palette and keys but keep the MINIMAL beat
1356
- // (round-1 finding 3): reading a ceremony for a minute must not lapse
1357
- // every other session. The beat reads no DOM and posts no parameters.
1358
- const sensitiveChrome = sensitive && s.chrome !== undefined
1359
- ? beatScript(!restricted()) + sidebarScript()
1360
- : "";
1361
- const script = functional + (chromeLayer ? chromeScript(!restricted()) + WORKSPACE_MOTION_SCRIPT : sensitiveChrome);
1353
+ // Sensitive pages strip the palette and keys but keep the sidebar toggle.
1354
+ const sensitiveChrome = sensitive && s.chrome !== undefined ? sidebarScript() : "";
1355
+ const script = functional + (chromeLayer ? chromeScript() + WORKSPACE_MOTION_SCRIPT : sensitiveChrome);
1362
1356
  const nonce = script === "" ? undefined : randomBytes(16).toString("base64");
1363
1357
  const body = chromeLayer
1364
1358
  ? `${s.body}\n${paletteTagCached(s.chrome?.project ?? null)}\n${KBD_HELP}`
@@ -1370,8 +1364,8 @@ export function createDecisionServer(options) {
1370
1364
  ...(s.refreshSeconds === undefined ? {} : { refreshSeconds: s.refreshSeconds }),
1371
1365
  ...(nonce === undefined ? {} : { live: { nonce, script, fallbackRefresh: s.functional?.fetches === true } }),
1372
1366
  });
1373
- // v28: the chrome layer itself fetches (the attended beat), so any
1374
- // page that ships it needs connect-src — not only pages whose own
1367
+ // Pages that ship the chrome layer keep connect-src (v28 granted it for
1368
+ // the since-removed watched-session beat), beside pages whose own
1375
1369
  // functional script polls.
1376
1370
  return page(response, status, html, nonce, s.functional?.fetches === true || chromeLayer || sensitiveChrome !== "");
1377
1371
  }
@@ -1462,7 +1456,7 @@ export function createDecisionServer(options) {
1462
1456
  : {
1463
1457
  modeBanner: {
1464
1458
  name: liveMode.name,
1465
- words: `${liveMode.name} mode until ${liveMode.absoluteExpiry.slice(0, 16).replace("T", " ")} UTC${liveModeTerms.autoApproveFiling ? ` — every scope ${liveMode.signedBy} files builds without further ceremony` : ""}${liveModeTerms.permissionDefault === "escalated" ? " — FULL permissions by default" : ""}${liveModeTerms.quickMint ? ` — ${liveMode.signedBy} starts watched sessions without a password` : ""}${liveModeTerms.publication === "automerge" ? " — merges fire themselves on green" : ""}`,
1459
+ words: `${liveMode.name} mode until ${liveMode.absoluteExpiry.slice(0, 16).replace("T", " ")} UTC${liveModeTerms.autoApproveFiling ? ` — every scope ${liveMode.signedBy} files builds without further ceremony` : ""}${liveModeTerms.permissionDefault === "escalated" ? " — FULL permissions by default" : ""}${liveModeTerms.publication === "automerge" ? " — merges fire themselves on green" : ""}`,
1466
1460
  },
1467
1461
  }),
1468
1462
  ...(!restricted() && !unscopedMode && managedRepos().length > 0 ? { chat: true } : {}),
@@ -1535,8 +1529,6 @@ export function createDecisionServer(options) {
1535
1529
  };
1536
1530
  async function handlePost(route, url, who, request, response, posted) {
1537
1531
  const early = () => dispatchConsole(route, { url, who, request, response, route, now: clock(), project: projectOf(who, request) ?? null, chosenProject: projectOf(who, request) ?? null, posted });
1538
- if (route.id === 'session.attended-beats')
1539
- return early();
1540
1532
  const denied = authorizeMutation(request, who, readForm(posted, CONSOLE_FORMS.mutationGuard));
1541
1533
  if (denied !== null)
1542
1534
  return refuse(response, who, denied.status, denied.message);
@@ -1550,8 +1542,7 @@ export function createDecisionServer(options) {
1550
1542
  // THE CENTRAL VIEWER GATE (modes chain, D2/E2): consequential POSTs
1551
1543
  // require ACTIVE approver standing — cookie and bearer alike — with
1552
1544
  // an EXACT allowlist of session-local acts, never a prefix. /login,
1553
- // /logout and /join answer before this handler; the attended beat
1554
- // guards itself (approver-only) above.
1545
+ // /logout and /join answer before this handler.
1555
1546
  if (who.role === "viewer") {
1556
1547
  if (route.viewer !== true) {
1557
1548
  return refuse(response, who, 403, "your login can watch, not act — ask an approver to upgrade you");
@@ -23,7 +23,7 @@ import { MATE_MESSAGE_MAX_CHARS, runMateTurn } from "../mate.js";
23
23
  import { livePin, modelOptions } from "../model-catalog.js";
24
24
  import { verifyApproverByPassword, verifyApproverStanding } from "../principal.js";
25
25
  import { projectName } from "../project.js";
26
- import { fileRoutineProposal, fileTaskProposal } from "../proposal.js";
26
+ import { fileTaskProposal } from "../proposal.js";
27
27
  import { providerName } from "../provider-auth.js";
28
28
  import { validModelId } from "../provider.js";
29
29
  import { loadOrCreateVapidKeys, validatePushEndpoint } from "../push.js";
@@ -763,7 +763,7 @@ export function createChatHandlers(runtime) {
763
763
  noteMate(who.session.csrf, null, "that proposal was already acted on");
764
764
  return redirect(response, chatReturnWithLatest(back));
765
765
  }
766
- const outcome = confirmMateProposal(store, principal, id, now, { chatProvider: teamChatProvider, confirm: body.get("confirm") === "yes", via: "web", evidenceRoot, held: options.attended?.coordinator, ...(body.has("nonce") ? { actionReview: { nonce: body.get("nonce") ?? "", password: body.get("token") ?? "" } } : {}) });
766
+ const outcome = confirmMateProposal(store, principal, id, now, { chatProvider: teamChatProvider, confirm: body.get("confirm") === "yes", via: "web", evidenceRoot, ...(body.has("nonce") ? { actionReview: { nonce: body.get("nonce") ?? "", password: body.get("token") ?? "" } } : {}) });
767
767
  if (cardJson) {
768
768
  if (!outcome.ok)
769
769
  return cardAnswer(outcome.reason === "standing" ? 403 : outcome.reason === "not-yours" ? 404 : 409, false, outcome.said);
@@ -1005,43 +1005,20 @@ export function createChatHandlers(runtime) {
1005
1005
  // Re-validated at the act: the door runs every field check again, and
1006
1006
  // the fields are scanned for secrets before they become durable.
1007
1007
  const acceptanceFields = candidate.draft.acceptance.flatMap(c => [c.statement, ...(c.how === null ? [] : [c.how])]);
1008
- const fields = candidate.draft.kind === "task"
1009
- ? [candidate.draft.title, candidate.draft.goal, candidate.draft.outOfScope ?? "", ...candidate.draft.touches, ...acceptanceFields]
1010
- : [candidate.draft.name, candidate.draft.goal, candidate.draft.outOfScope ?? "", ...candidate.draft.touches, ...acceptanceFields];
1008
+ const fields = [candidate.draft.title, candidate.draft.goal, candidate.draft.outOfScope ?? "", ...candidate.draft.touches, ...acceptanceFields];
1011
1009
  if (scanForSecrets(fields.join("\n")).length > 0) {
1012
1010
  chat.candidates.delete(key);
1013
1011
  return refuse(response, who, 400, "that draft contains something credential-shaped — discarded", "/chat");
1014
1012
  }
1015
1013
  const filedVia = `chat:${enabled.config.provider}`;
1016
- if (candidate.draft.kind === "task") {
1017
- const made = fileTaskProposal(store, {
1018
- title: candidate.draft.title,
1019
- repo: candidate.repoPath,
1020
- goal: candidate.draft.goal,
1021
- outOfScope: candidate.draft.outOfScope,
1022
- touches: candidate.draft.touches,
1023
- acceptance: candidate.draft.acceptance,
1024
- filedVia, filedBy: { name: who.name, kind: "person" },
1025
- admittedRepos: managedRepos(),
1026
- }, now);
1027
- if (!made.ok) {
1028
- candidate.state = "pending";
1029
- return refuse(response, who, 400, `the door refused it: ${made.message}`, "/chat");
1030
- }
1031
- chat.candidates.delete(key);
1032
- return redirect(response, taskHref(made.id));
1033
- }
1034
- const made = fileRoutineProposal(store, {
1035
- name: candidate.draft.name,
1014
+ const made = fileTaskProposal(store, {
1015
+ title: candidate.draft.title,
1036
1016
  repo: candidate.repoPath,
1037
1017
  goal: candidate.draft.goal,
1038
1018
  outOfScope: candidate.draft.outOfScope,
1039
1019
  touches: candidate.draft.touches,
1040
1020
  acceptance: candidate.draft.acceptance,
1041
- requirements: [],
1042
- schedule: candidate.draft.schedule,
1043
- costCeilingUsd: null,
1044
- filedVia, createdBy: who.name,
1021
+ filedVia, filedBy: { name: who.name, kind: "person" },
1045
1022
  admittedRepos: managedRepos(),
1046
1023
  }, now);
1047
1024
  if (!made.ok) {
@@ -1049,7 +1026,7 @@ export function createChatHandlers(runtime) {
1049
1026
  return refuse(response, who, 400, `the door refused it: ${made.message}`, "/chat");
1050
1027
  }
1051
1028
  chat.candidates.delete(key);
1052
- return redirect(response, `/routines/${made.id}`);
1029
+ return redirect(response, taskHref(made.id));
1053
1030
  }
1054
1031
  const chatAckPost = /^\/chat\/ack\/([0-9]{1,15})$/.exec(url.pathname);
1055
1032
  if (chatAckPost !== null) {
@@ -3,7 +3,7 @@ import { createHash } from "node:crypto";
3
3
  import {} from "node:http";
4
4
  import { tmpdir } from "node:os";
5
5
  import { join } from "node:path";
6
- import { resolveRoutineAuthority } from "../agentconfig.js";
6
+ import { resolveAgentAuthority } from "../agentconfig.js";
7
7
  import { checkResponse, CONSOLE_FORMS, readForm } from "../contracts/console-api.js";
8
8
  import { stageReferenceProblems } from "../contracts/stage-output.js";
9
9
  import { LIMITS } from "../decision.js";
@@ -17,7 +17,7 @@ import { assignFlowCard, commentOnFlowCard, watchFlowCard } from "../flow-people
17
17
  import { saveScript } from "../flow-scripts.js";
18
18
  import { chooseFlowCard } from "../flow-send.js";
19
19
  import { exportFlow, fetchFlowFile, FlowFileError, importFlow, parseFlowFile, planFlowImport } from "../flow-share.js";
20
- import { addFlowTriggerTo, checkFlowTriggerNow, pressFlowButton, removeFlowTrigger, removeLinearKey, renewFlowHook, saveHooksBase, saveLinearKey, saveLinearSigningSecret, shareFlowButton, stopSharingFlowButton } from "../flow-triggers.js";
20
+ import { addFlowTriggerTo, checkFlowTriggerNow, pressFlowButton, removeFlowTrigger, removeLinearKey, setFlowTriggerOn, renewFlowHook, saveHooksBase, saveLinearKey, saveLinearSigningSecret, shareFlowButton, stopSharingFlowButton } from "../flow-triggers.js";
21
21
  import { FLOW_IMPORT_SCRIPT, flowFallbackHtml, flowImportHtml, flowsListHtml, flowView } from "../flows-ui.js";
22
22
  import { FLOW_TEMPLATES, FlowContractError, validateFlowDefinition, withZoneNames } from "../flows.js";
23
23
  import { keyStatus } from "../keys.js";
@@ -169,11 +169,12 @@ export function createFlowsHandlers(runtime) {
169
169
  const revision = who.via === "cookie" ? who.session.projectRevision : 0;
170
170
  const render = (html) => sendScreen(response, 200, screen("Workflow recipes", html, { chrome: chromeFor(project, "recipes"), functional: { script: recipeScript() } }));
171
171
  if (url.pathname === "/recipes") {
172
- const recent = project === null ? [] : store.handle.prepare("SELECT document,task_id,routine_id FROM workflow_preview WHERE repo=? AND (task_id IS NOT NULL OR routine_id IS NOT NULL) ORDER BY created_at DESC LIMIT 12").all(project).map(row => {
172
+ const recent = project === null ? [] : store.handle.prepare("SELECT document,task_id,flow_id FROM workflow_preview WHERE repo=? AND (task_id IS NOT NULL OR flow_id IS NOT NULL) ORDER BY created_at DESC LIMIT 12").all(project).map(row => {
173
173
  const d = importRecipe(String(row["document"]));
174
174
  const task = row["task_id"] === null ? null : store.getTask(String(row["task_id"]));
175
- const routine = row["routine_id"] === null ? null : store.getRoutine(Number(row["routine_id"]));
176
- return { name: d.name, href: task !== null ? taskHref(task.id) : `/routines/${routine?.id}`, state: task?.state ?? (routine?.approvedAt === null ? "Needs approval" : routine?.paused ? "Paused" : "Scheduled") };
175
+ const flow = row["flow_id"] === null ? null : store.getFlow(Number(row["flow_id"]));
176
+ const on = flow !== null && flow.state === "active" && store.flowTriggers(flow.id).some(one => one.kind === "schedule" && one.state === "active");
177
+ return { name: d.name, href: task !== null ? taskHref(task.id) : `/flows/${Number(row["flow_id"])}`, state: task?.state ?? (flow === null || flow.state !== "active" ? "Archived" : on ? "Scheduled" : "Paused") };
177
178
  });
178
179
  return render(recipeLibraryHtml(starterRecipes(), project === null ? [] : savedRecipes(store, who.name, project), project, csrf, revision, recent));
179
180
  }
@@ -215,7 +216,7 @@ export function createFlowsHandlers(runtime) {
215
216
  if (url.pathname === "/recipes/preview" && preview !== null) {
216
217
  if (preview.document.version === 2 || url.searchParams.get("purpose") === "recipe")
217
218
  return render(recipeDefinitionPreviewHtml(preview, csrf, revision));
218
- const agents = resolveRoutineAuthority(store, project, preview.document.acceptance, now);
219
+ const agents = resolveAgentAuthority(store, project, preview.document.acceptance, now);
219
220
  const workers = store.listRunners().filter(one => one.retiredAt === null && one.repos.includes(project) && runnerAlive(one, now));
220
221
  const mode = store.activeMode(project, now);
221
222
  const readiness = [
@@ -498,7 +499,7 @@ export function createFlowsHandlers(runtime) {
498
499
  return answer(404, { ok: false, said: "That trigger isn't on this flow." });
499
500
  const verb = triggerPost[3];
500
501
  if (verb === "pause" || verb === "resume") {
501
- store.updateFlowTrigger(trigger.id, { state: verb === "pause" ? "paused" : "active" }, now);
502
+ setFlowTriggerOn(store, trigger, verb === "resume", now);
502
503
  return settle(verb === "pause" ? "Trigger paused." : "Trigger on again.");
503
504
  }
504
505
  if (verb === "remove") {
@@ -722,8 +723,8 @@ export function createFlowsHandlers(runtime) {
722
723
  const sourceVersion = body.get("sourceRevision") ?? "1";
723
724
  if (!/^[1-9][0-9]{0,8}$/.test(sourceVersion))
724
725
  throw new RecipeError("Choose a valid recipe version.");
725
- const original = ["custom", "imported", "task-copy", "routine-copy"].includes(source) ? null : findRecipe(store, who.name, project, source, Number(sourceVersion));
726
- if (original === null && !["custom", "imported", "task-copy", "routine-copy"].includes(source))
726
+ const original = ["custom", "imported", "task-copy"].includes(source) ? null : findRecipe(store, who.name, project, source, Number(sourceVersion));
727
+ if (original === null && !["custom", "imported", "task-copy"].includes(source))
727
728
  throw new RecipeError("That recipe version is not available in this project.", 404);
728
729
  let document;
729
730
  try {
@@ -753,7 +754,7 @@ export function createFlowsHandlers(runtime) {
753
754
  if (context !== undefined)
754
755
  context.createdTask = made.taskId;
755
756
  }
756
- return redirect(response, made.taskId === null ? `/routines/${made.routineId}` : taskHref(made.taskId));
757
+ return redirect(response, made.taskId === null ? `/flows/${made.flowId}` : taskHref(made.taskId));
757
758
  }
758
759
  catch (error) {
759
760
  if (error instanceof RecipeError)
@@ -152,7 +152,6 @@ export function createSharedGuards(runtime) {
152
152
  case 'run': return runRepo(id);
153
153
  case 'decision': return runRepo(runtime.store.getDecision(id)?.run);
154
154
  case 'incident': return runRepo(runtime.store.openIncidents().find(one => one.id === id)?.run);
155
- case 'routine': return runtime.store.getRoutine(id)?.repo ?? null;
156
155
  case 'flow': return runtime.store.getFlow(id)?.repo ?? null;
157
156
  case 'proposal':
158
157
  case 'coding': return runtime.actionTarget(url, who, request, body === null ? null : readForm(body, CONSOLE_FORMS.ledgerTarget)).repo;
@@ -36,7 +36,7 @@ const PWA_MANIFEST = JSON.stringify({
36
36
  // paths — the payload URL is data, revalidated, never handed raw to the
37
37
  // browser (arc 3 finding 5).
38
38
  const PWA_WORKER = `// Toolroll — push only; deliberately NO fetch handler (no offline cache of an authenticated console).
39
- const SHAPES = [/^\\/next$/, /^\\/review$/, /^\\/system$/, /^\\/routines$/, /^\\/routines\\/[0-9]+$/, /^\\/d\\/[0-9]+$/, /^\\/contest\\/[0-9]+$/, /^\\/r\\/[0-9]+$/];
39
+ const SHAPES = [/^\\/next$/, /^\\/review$/, /^\\/system$/, /^\\/routines$/, /^\\/routines\\/[0-9]+$/, /^\\/d\\/[0-9]+$/, /^\\/r\\/[0-9]+$/];
40
40
  self.addEventListener("push", function (event) {
41
41
  var data = {};
42
42
  try { data = event.data ? event.data.json() : {}; } catch (e) {}
@@ -320,7 +320,6 @@ export function createPagesHandlers(runtime) {
320
320
  settings: options.telegramTokenFile !== undefined,
321
321
  building: store.liveClaims(project, now),
322
322
  runners: store.listRunners(),
323
- heldSessions: new Map(store.openHeldSessions().reduce((by, one) => by.set(one.runner, (by.get(one.runner) ?? 0) + 1), new Map())),
324
323
  worktrees: store
325
324
  .listWorktrees()
326
325
  .filter(one => project === null || sameRepo(one.repo, project)),
@@ -346,7 +345,6 @@ export function createPagesHandlers(runtime) {
346
345
  }),
347
346
  building: store.liveClaims(project, now),
348
347
  runners: store.listRunners(),
349
- heldSessions: new Map(store.openHeldSessions().reduce((by, one) => by.set(one.runner, (by.get(one.runner) ?? 0) + 1), new Map())),
350
348
  worktrees: store.listWorktrees().filter(one => project === null || sameRepo(one.repo, project)),
351
349
  episode: project === null ? null : store.latestWatchEpisode(project),
352
350
  outboxPending: store.pendingForAttention().length,
@@ -73,7 +73,6 @@ export function createPeopleHandlers(runtime) {
73
73
  `<p class="meta">Operators with all-project access also manage the instance.</p><fieldset><legend>Projects</legend>${projectChoices.length === 0 ? `<p class="meta">Add a project before granting selected access.</p>` : projectChoices.map(repo => `<label class="row"><input type="checkbox" name="projects" value="${escape(repo)}"${selected?.includes(repo) ? " checked" : ""}>${escape(projectName(repo))}</label>`).join("")}</fieldset>`;
74
74
  const cards = accounts.map(one => {
75
75
  const seen = lastSeenOf(one.name);
76
- const attended = store.openAttendedOf(one.name);
77
76
  const acts = store.recentActsOf(one.name);
78
77
  const standing = one.revokedAt !== null
79
78
  ? `revoked ${escape(one.revokedAt.slice(0, 16).replace("T", " "))} by ${personChip(one.revokedBy ?? "?")}`
@@ -87,9 +86,6 @@ export function createPeopleHandlers(runtime) {
87
86
  approverView && one.revokedAt === null ? `<details><summary>Edit project access</summary><form method="post" action="/people/projects">${hiddenFields({ csrf, name: one.name })}${accessFields(one.projects)}<label>Your password<input type="password" name="token" autocomplete="current-password" required></label><p class="meta">Changing access signs this person out and ends their derived sessions and modes. Previously approved work stays recorded.</p><button type="submit">Save project access</button></form></details>` : "",
88
87
  `<p class="meta">${seen === null ? "not signed in right now" : `signed in \u2014 active ${escape(new Date(seen).toISOString().slice(11, 16))} UTC`} \u00b7 joined ${escape(one.addedAt.slice(0, 10))}</p>`,
89
88
  `<p><a href="${escape(personHref(one.name))}">Tokens and remote activity</a></p>`,
90
- attended.length === 0
91
- ? ""
92
- : `<p class="row">watching now: ${attended.map(session => `<a href="/t/${escape(session.taskId)}">${escape(session.taskId)}</a>`).join(", ")}</p>`,
93
89
  acts.length === 0
94
90
  ? `<p class="meta">No recorded acts yet</p>`
95
91
  : acts.map(act => `<p class="row meta">${escape(act.kind)} ${escape(act.subject)} \u00b7 ${escape(act.at.slice(0, 16).replace("T", " "))}</p>`).join("\n"),
@@ -17,7 +17,7 @@ export type Caller = "anonymous" | "service" | "cookie" | "bearer" | "oauth" | "
17
17
  /** What the caller must hold: nothing, read, act (an approver or an `act` token), or a fresh password/provider check on top. */
18
18
  export type RouteScope = "none" | "read" | "act" | "step-up";
19
19
  /** Where the request's project comes from. */
20
- export type ProjectResolver = "none" | "session" | "form" | "form-path" | "task" | "run" | "decision" | "incident" | "routine" | "flow" | "proposal" | "conversation" | "coding" | "adapter";
20
+ export type ProjectResolver = "none" | "session" | "form" | "form-path" | "task" | "run" | "decision" | "incident" | "flow" | "proposal" | "conversation" | "coding" | "adapter";
21
21
  /**
22
22
  * What a project-limited account may do here:
23
23
  * - `deny`: instance access only.
@@ -120,7 +120,6 @@ get("tasks", "runs", "/runs", { limited: "collection" });
120
120
  get("pages", "menu", "/menu", { limited: "collection", ...free });
121
121
  get("tasks", "run.page", `^\\/r\\/${ID}$`, { project: "run", limited: "resource", ...free, sample: "/r/1" });
122
122
  get("tasks", "run.evidence", `^\\/r\\/${ID}\\/evidence\\/${ID}$`, { project: "run", limited: "resource", ...free, sample: "/r/1/evidence/2" });
123
- get("tasks", "contest.page", `^\\/contest\\/${ID}$`, { ...free, sample: "/contest/1" });
124
123
  get("pages", "caps", "/caps");
125
124
  get("flows", "flows.new", "/flows/new", { limited: "resource", ...free });
126
125
  get("flows", "flows.gallery", "^\\/flows\\/new\\/[a-z-]{1,40}$", { limited: "resource", ...free, sample: "/flows/new/triage" });
@@ -139,13 +138,14 @@ for (const one of ["run", "start", "new", "edit", "from-task", "preview", "expor
139
138
  get("flows", `recipes.${one}`, `/recipes/${one}`, { limited: "collection" });
140
139
  // Any other recipe screen answers the recipe area's own refusal.
141
140
  get("flows", "recipes.other", "^\\/recipes\\/.*$", { sample: "/recipes/unknown" });
141
+ // v115: routines became scheduled flows; old links to them land on the flows page.
142
142
  get("tasks", "routines", "/routines", { limited: "collection" });
143
143
  get("chat", "chat.stream", "/chat/stream", free);
144
144
  get("chat", "chat.mate-status", "/chat/mate/status", free);
145
145
  get("chat", "chat.demo-live", "/chat/demo/live", free);
146
146
  get("chat", "chat.page", "/chat", { project: "conversation", limited: "conversation", ...free });
147
147
  get("chat", "chat.ack", `^\\/chat\\/ack\\/${ID}$`, { sample: "/chat/ack/1" });
148
- get("tasks", "routine.page", `^\\/routines\\/${ID}$`, { project: "routine", limited: "resource", sample: "/routines/1" });
148
+ get("tasks", "routine.page", `^\\/routines\\/${ID}$`, { limited: "resource", sample: "/routines/1" });
149
149
  get("settings", "control", "/control");
150
150
  get("settings", "control.connection", "/control/connection");
151
151
  get("chat", "chat.action", `^\\/chat\\/action\\/${ID}$`, { project: "proposal", limited: "proposal", ...free, sample: "/chat/action/1" });
@@ -166,8 +166,6 @@ get("chat", "lead.status", "/lead/status", free);
166
166
  get("chat", "chat.task-status", "/chat/task-status", free);
167
167
  get("tasks", "decision.evidence", `^\\/d\\/${ID}\\/evidence\\/${ID}$`, { project: "decision", limited: "resource", ...free, sample: "/d/1/evidence/2" });
168
168
  // ---- console actions -------------------------------------------------------------------------------------------
169
- // The attended beat guards itself (cookie, form type, Sec-Fetch-Site) before the shared mutation guard.
170
- post("tasks", "session.attended-beats", "/session/attended-beats", { callers: ["cookie"], scopeRefusal: "your login can watch, not keep sessions alive", callerRefusal: { status: 403, type: "text/plain; charset=utf-8", body: "watching is a browser session's act" } });
171
169
  // The scripted demo: the handler answers a non-browser caller "no page here".
172
170
  post("chat", "chat.demo", "^\\/chat\\/demo\\/(ask|[0-9]{1,9}\\/(approve|change|revise|complete))$", { sample: "/chat/demo/ask" });
173
171
  post("settings", "provider.resume", "^\\/providers\\/[a-z]+\\/resume$", { sample: "/providers/codex/resume" });
@@ -243,12 +241,6 @@ post("people", "people.revoke", "/people/revoke", { scope: "step-up" });
243
241
  post("pages", "fleet.retire", "/fleet/runner/retire", { scope: "step-up" });
244
242
  // Answering a parked decision needs act access and irreversible-choice confirmation, not password step-up.
245
243
  post("tasks", "decision.answer", `^\\/d\\/${ID}\\/answer$`, { project: "decision", limited: "resource", scope: "act", sample: "/d/1/answer" });
246
- post("tasks", "contest.act.arm", `^\\/contest\\/${ID}\\/arm$`, { sample: "/contest/1/arm" });
247
- post("tasks", "contest.act.pick", `^\\/contest\\/${ID}\\/pick$`, { scope: "step-up", sample: "/contest/1/pick" });
248
- post("tasks", "contest.act.abandon", `^\\/contest\\/${ID}\\/abandon$`, { scope: "step-up", sample: "/contest/1/abandon" });
249
- post("tasks", "task.attend.attend-preview", `^\\/t\\/${TASK}\\/attend-preview$`, { callers: ["cookie"], project: "task", sample: "/t/one/attend-preview" });
250
- post("tasks", "task.attend.attend", `^\\/t\\/${TASK}\\/attend$`, { scope: "step-up", project: "task", sample: "/t/one/attend" });
251
- post("tasks", "task.attend.attend-revoke", `^\\/t\\/${TASK}\\/attend-revoke$`, { callers: ["cookie"], project: "task", sample: "/t/one/attend-revoke" });
252
244
  post("tasks", "task.act.hold", `^\\/t\\/${TASK}\\/hold$`, { project: "task", limited: "resource", sample: "/t/one/hold" });
253
245
  post("tasks", "task.act.unhold", `^\\/t\\/${TASK}\\/unhold$`, { project: "task", limited: "resource", sample: "/t/one/unhold" });
254
246
  post("tasks", "task.act.requeue", `^\\/t\\/${TASK}\\/requeue$`, { project: "task", limited: "resource", sample: "/t/one/requeue" });
@@ -291,15 +283,8 @@ post("chat", "chat.ack-send", `^\\/chat\\/ack\\/${ID}$`, { scope: "step-up", sam
291
283
  for (const one of ["prepare", "preview", "import", "save"])
292
284
  post("flows", `recipes.${one}-send`, `/recipes/${one}`, { limited: "collection" });
293
285
  post("flows", "recipes.launch-send", "/recipes/launch", { limited: "collection" });
294
- post("tasks", "routines.add", "/routines/add", { project: "form", limited: "collection" });
295
- post("tasks", "routine.act.approve", `^\\/routines\\/${ID}\\/approve$`, { scope: "step-up", project: "routine", limited: "resource", sample: "/routines/1/approve" });
296
- post("tasks", "routine.act.refresh", `^\\/routines\\/${ID}\\/refresh$`, { project: "routine", limited: "resource", sample: "/routines/1/refresh" });
297
- post("tasks", "routine.act.pause", `^\\/routines\\/${ID}\\/pause$`, { project: "routine", limited: "resource", sample: "/routines/1/pause" });
298
- post("tasks", "routine.act.resume", `^\\/routines\\/${ID}\\/resume$`, { project: "routine", limited: "resource", sample: "/routines/1/resume" });
299
- post("tasks", "routine.act.run-now", `^\\/routines\\/${ID}\\/run-now$`, { scope: "step-up", project: "routine", limited: "resource", sample: "/routines/1/run-now" });
300
286
  post("tasks", "run.act", `^\\/r\\/${ID}\\/(note|comment|revise|draft-repair|checks|add-tests)$`, { project: "run", limited: "resource", sample: "/r/1/note" });
301
287
  post("tasks", "session.editor-links", "/session/editor-links", { viewer: true });
302
- post("tasks", "run.turn", `^\\/r\\/${ID}\\/turn$`, { project: "run", sample: "/r/1/turn" });
303
288
  post("tasks", "incident.resolve", `^\\/i\\/${ID}\\/resolve$`, { project: "incident", sample: "/i/1/resolve" });
304
289
  /** The sole route table, in match order. */
305
290
  export const ROUTES = Object.freeze(rows.map(one => Object.freeze(one)));