toolroll 0.9.52 → 0.9.54

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 (187) hide show
  1. package/CHANGELOG.md +16 -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-ask.js +8 -8
  15. package/dist/chat-controls.d.ts +0 -4
  16. package/dist/chat-controls.js +0 -1
  17. package/dist/chat-core.d.ts +171 -0
  18. package/dist/chat-core.js +288 -0
  19. package/dist/chat-decide.d.ts +10 -8
  20. package/dist/chat-decide.js +27 -24
  21. package/dist/chat-delivery-state.d.ts +51 -75
  22. package/dist/chat-delivery-state.js +140 -170
  23. package/dist/chat-delivery.d.ts +21 -4
  24. package/dist/chat-delivery.js +313 -195
  25. package/dist/chat-flow.d.ts +29 -1
  26. package/dist/chat-flow.js +24 -50
  27. package/dist/chat-messages.d.ts +319 -0
  28. package/dist/chat-messages.js +841 -0
  29. package/dist/chat-migration.d.ts +30 -0
  30. package/dist/chat-migration.js +802 -0
  31. package/dist/chat-quiet.d.ts +3 -0
  32. package/dist/chat-quiet.js +7 -2
  33. package/dist/chat-task-actions.d.ts +1 -1
  34. package/dist/chat-task-actions.js +0 -2
  35. package/dist/claim.d.ts +79 -244
  36. package/dist/claim.js +688 -1023
  37. package/dist/cli.d.ts +2 -0
  38. package/dist/cli.js +10 -2
  39. package/dist/contracts/chat-content.d.ts +71 -2
  40. package/dist/contracts/chat-content.js +22 -3
  41. package/dist/contracts/chat-tables.d.ts +11 -4
  42. package/dist/contracts/chat-tables.js +165 -87
  43. package/dist/contracts/cli.d.ts +0 -13
  44. package/dist/contracts/cli.js +0 -2
  45. package/dist/contracts/console-api.d.ts +5 -10
  46. package/dist/contracts/console-api.js +4 -10
  47. package/dist/contracts/discord-callback.js +2 -1
  48. package/dist/contracts/flow.d.ts +50 -0
  49. package/dist/contracts/flow.js +26 -1
  50. package/dist/contracts/lead-tools.d.ts +0 -13
  51. package/dist/contracts/lead-tools.js +3 -3
  52. package/dist/contracts/slack-callback.js +3 -2
  53. package/dist/contracts/store-json.d.ts +1 -29
  54. package/dist/contracts/store-json.js +4 -17
  55. package/dist/contracts/teams-callback.js +2 -1
  56. package/dist/converse.d.ts +1 -11
  57. package/dist/converse.js +6 -38
  58. package/dist/coordinator.d.ts +0 -1
  59. package/dist/coordinator.js +3 -5
  60. package/dist/demo.js +16 -26
  61. package/dist/desktop-update-gate.js +0 -1
  62. package/dist/desktop-update.js +1 -3
  63. package/dist/discord-chat.js +19 -38
  64. package/dist/discord-settings.js +4 -13
  65. package/dist/discord.js +4 -14
  66. package/dist/dispatch.d.ts +1 -1
  67. package/dist/dispatch.js +1 -1
  68. package/dist/dispose.d.ts +2 -3
  69. package/dist/dispose.js +17 -92
  70. package/dist/envelope.js +0 -3
  71. package/dist/exec.d.ts +0 -64
  72. package/dist/exec.js +3 -356
  73. package/dist/exhaustion.d.ts +22 -76
  74. package/dist/exhaustion.js +14 -140
  75. package/dist/export.js +8 -8
  76. package/dist/flow-engine.js +1 -1
  77. package/dist/flow-schedule.d.ts +154 -0
  78. package/dist/flow-schedule.js +321 -0
  79. package/dist/flow-share.js +1 -1
  80. package/dist/flow-triggers.d.ts +6 -1
  81. package/dist/flow-triggers.js +65 -10
  82. package/dist/flows-cli.js +8 -4
  83. package/dist/flows-ui.js +2 -0
  84. package/dist/flows.js +1 -1
  85. package/dist/guides.js +9 -29
  86. package/dist/invoke.d.ts +1 -24
  87. package/dist/invoke.js +17 -259
  88. package/dist/lead-status.js +2 -4
  89. package/dist/lead-voice.js +1 -1
  90. package/dist/mate-doors.d.ts +1 -2
  91. package/dist/mate-doors.js +12 -25
  92. package/dist/mate-tools.d.ts +2 -2
  93. package/dist/mate-tools.js +13 -24
  94. package/dist/modes.d.ts +19 -8
  95. package/dist/modes.js +24 -14
  96. package/dist/notification-rows.d.ts +30 -0
  97. package/dist/notification-rows.js +102 -0
  98. package/dist/operate-remote-arguments.js +2 -6
  99. package/dist/operate-remote.js +6 -7
  100. package/dist/operate.d.ts +0 -6
  101. package/dist/operate.js +226 -2248
  102. package/dist/phase-routing.d.ts +13 -23
  103. package/dist/phase-routing.js +13 -35
  104. package/dist/plan-auto.js +2 -2
  105. package/dist/plane-review.js +5 -11
  106. package/dist/planner-source.js +2 -1
  107. package/dist/policy.d.ts +1 -4
  108. package/dist/policy.js +0 -8
  109. package/dist/process-recovery-eligibility.js +2 -3
  110. package/dist/project-delete.js +24 -41
  111. package/dist/proposal.d.ts +4 -24
  112. package/dist/proposal.js +3 -88
  113. package/dist/provider-auth.d.ts +3 -3
  114. package/dist/provider-auth.js +4 -6
  115. package/dist/provider.d.ts +11 -25
  116. package/dist/provider.js +4 -36
  117. package/dist/push.js +1 -1
  118. package/dist/recipe-ui.js +6 -6
  119. package/dist/recipes.d.ts +3 -3
  120. package/dist/recipes.js +27 -23
  121. package/dist/result-shots.js +2 -2
  122. package/dist/retention.js +1 -1
  123. package/dist/run-closeout.d.ts +1 -1
  124. package/dist/run-closeout.js +1 -4
  125. package/dist/scope.d.ts +11 -65
  126. package/dist/scope.js +17 -82
  127. package/dist/serve.js +15 -24
  128. package/dist/server/chat.js +7 -30
  129. package/dist/server/flows.js +11 -10
  130. package/dist/server/guards.js +0 -1
  131. package/dist/server/pages-assets.js +1 -1
  132. package/dist/server/pages.js +0 -2
  133. package/dist/server/people-tokens.js +0 -4
  134. package/dist/server/route-table.d.ts +1 -1
  135. package/dist/server/route-table.js +2 -17
  136. package/dist/server/settings.js +6 -12
  137. package/dist/server/shared.d.ts +28 -212
  138. package/dist/server/shared.js +71 -780
  139. package/dist/server/task-views.js +9 -76
  140. package/dist/server/tasks.d.ts +1 -50
  141. package/dist/server/tasks.js +36 -946
  142. package/dist/setup-guide.d.ts +2 -2
  143. package/dist/slack-api.d.ts +5 -1
  144. package/dist/slack-api.js +2 -2
  145. package/dist/slack-chat.d.ts +9 -6
  146. package/dist/slack-chat.js +29 -53
  147. package/dist/slack-settings.js +6 -15
  148. package/dist/slack.js +6 -16
  149. package/dist/store.d.ts +200 -1702
  150. package/dist/store.js +811 -5439
  151. package/dist/surface.js +10 -24
  152. package/dist/task-composer.d.ts +1 -1
  153. package/dist/task-composer.js +1 -1
  154. package/dist/task-control.d.ts +3 -10
  155. package/dist/task-control.js +2 -7
  156. package/dist/teammate-desk.js +1 -1
  157. package/dist/teammate-question.js +12 -12
  158. package/dist/teammates-ui.js +1 -1
  159. package/dist/teams-chat.js +12 -9
  160. package/dist/teams-settings.js +2 -2
  161. package/dist/teams.js +4 -5
  162. package/dist/telegram-flow.js +5 -5
  163. package/dist/telegram-mate.d.ts +0 -3
  164. package/dist/telegram-mate.js +0 -1
  165. package/dist/telegram.js +4 -4
  166. package/dist/templates.d.ts +11 -7
  167. package/dist/templates.js +12 -9
  168. package/dist/toolroll-update.d.ts +20 -0
  169. package/dist/toolroll-update.js +104 -5
  170. package/dist/work-index.js +2 -6
  171. package/dist/workspace-revision.js +3 -1
  172. package/dist/workspace-ui.js +1 -1
  173. package/package.json +1 -1
  174. package/scripts/recovery-canary.mjs +3 -3
  175. package/dist/contest.d.ts +0 -318
  176. package/dist/contest.js +0 -753
  177. package/dist/held.d.ts +0 -150
  178. package/dist/held.js +0 -799
  179. package/dist/liveness.d.ts +0 -30
  180. package/dist/liveness.js +0 -42
  181. package/dist/pricing.d.ts +0 -45
  182. package/dist/pricing.js +0 -77
  183. package/dist/routine.d.ts +0 -228
  184. package/dist/routine.js +0 -764
  185. package/dist/slack-state.d.ts +0 -13
  186. package/dist/slack-state.js +0 -10
  187. package/dist/supervisor.mjs +0 -297
@@ -1,12 +1,12 @@
1
1
  /**
2
- * Explainable, risk-aware phase routing (v47).
2
+ * Explainable phase routing (v47), sized since v2.
3
3
  *
4
4
  * WHICH agent plans, builds, repairs, and reviews a task is decided here,
5
5
  * once, from signed facts — never guessed from a model's name and never
6
6
  * re-decided behind an approval's back. The policy is a small table over
7
7
  * explicit inputs:
8
8
  *
9
- * risk — the task's declared risk (routine / elevated / high)
9
+ * size — how big a change the task is (small / medium / large, risky or not)
10
10
  * quality — the evidence policy the scope signs (default / strict)
11
11
  * evidence — what the acceptance rubric demands (screenshots, …)
12
12
  * publication — how far the plane may carry the result unattended
@@ -35,8 +35,11 @@
35
35
  * is what actually runs. Provider readiness is a runner's observation,
36
36
  * not a term — it rides beside the route (ready / unavailable / unknown),
37
37
  * it is stated wherever the route is shown, and an unavailable provider
38
- * is never substituted: admission halts with the reason, and only an
39
- * already-approved explicit fallback entry may ever run instead.
38
+ * is never substituted: admission halts with the reason.
39
+ *
40
+ * Every route carries `risk: "routine"`: the declared risk level was
41
+ * removed in v115 (size does that job), and older sealed routes keep
42
+ * whatever risk they were recommended for, read back exactly.
40
43
  *
41
44
  * Approval seals the route (`approvedRouteJson`); its digest folds into
42
45
  * the scope digest of EVERY route filed since v47 — routine-shaped routes
@@ -69,8 +72,8 @@ export declare const ROUTE_ERA = 1;
69
72
  export declare const PHASES: readonly Phase[];
70
73
  /** The review leg remains readable in signed history but is no longer scheduled. */
71
74
  export declare const ACTIVE_PHASES: readonly Phase[];
75
+ /** The risk words a route sealed before v115 may carry (history); every route since says `routine`. */
72
76
  export type RiskLevel = (typeof RISKS)[number];
73
- export declare const RISK_LEVELS: readonly RiskLevel[];
74
77
  export declare function isRiskLevel(value: unknown): value is RiskLevel;
75
78
  /** How big a change the task is, sized once at filing (by the classifier,
76
79
  * by the description when it cannot answer, or by a person). */
@@ -78,26 +81,14 @@ export type TaskSize = TaskSizing["size"];
78
81
  export declare const TASK_SIZES: readonly TaskSize[];
79
82
  export type SizeSource = TaskSizing["source"];
80
83
  export declare function isTaskSize(value: unknown): value is TaskSize;
81
- /** Whether a task makes no plan: a small change that is not risky, at
82
- * routine risk. Elevated or high risk always plans, whatever the size. */
84
+ /** Whether a task makes no plan: a small change that is not risky (on an
85
+ * older route sealed at elevated or high risk, it always planned). */
83
86
  export declare function makesNoPlan(size: TaskSizing | null | undefined, risk: RiskLevel): boolean;
84
87
  /** Where a size came from, in words. */
85
88
  export declare function sizeSourceWords(source: SizeSource): string;
86
89
  /** Strict read of a stored sizing (the route contract's): exact keys, known words, bounded reason. */
87
90
  export declare function parseSizing(raw: unknown): TaskSizing | null;
88
91
  export declare function riskTitle(risk: RiskLevel): string;
89
- /**
90
- * What declaring each risk level DOES, in one plain sentence — derived
91
- * from the tiering table below, so the explanation a form or a chat gives
92
- * can never drift from the policy that acts on it. The same words on
93
- * every surface: the task page's risk control, the chat's answer, the CLI.
94
- */
95
- export declare function riskConsequence(risk: RiskLevel): string;
96
- export declare const RISK_CHOICES: readonly {
97
- risk: RiskLevel;
98
- title: string;
99
- consequence: string;
100
- }[];
101
92
  /** A runner's non-spending observation of one provider. `unknown` is a
102
93
  * real answer (claude has no login probe that does not spend) and is never
103
94
  * upgraded to ready. */
@@ -184,7 +175,6 @@ export declare function headroomFrom(limits: readonly {
184
175
  observedAt: string;
185
176
  }[], now: Date): ProviderRoom[];
186
177
  export type RouteInput = {
187
- risk: RiskLevel;
188
178
  qualityMode: QualityMode;
189
179
  evidence: readonly RouteEvidenceKind[];
190
180
  publication: PublicationAuthority;
@@ -308,9 +298,9 @@ export declare function routeWords(projection: RouteProjection, indent?: string)
308
298
  /** The empty readiness lookup: every provider unknown. */
309
299
  export declare const NO_READINESS: ReadinessLookup;
310
300
  /** How a run's provenance names the leg it spent as: the frozen leg's own
311
- * word, `legacy` for a run governed by a pre-routing approval, a contest
312
- * lane, or an attended session, and `fallback` for an approved fallback
313
- * entry (and every repair of one). */
301
+ * word, and `legacy` for a run governed by a pre-routing approval. Before
302
+ * v115, `legacy` also named a watched session's run, and `fallback` an
303
+ * approved fallback entry's; those rows still read back. */
314
304
  export type RouteChosen = RouteLeg["chosen"] | "legacy" | "fallback";
315
305
  export declare const ROUTE_CHOSEN: readonly RouteChosen[];
316
306
  /** One run's route provenance: the exact provider and model that ran,
@@ -1,12 +1,12 @@
1
1
  /**
2
- * Explainable, risk-aware phase routing (v47).
2
+ * Explainable phase routing (v47), sized since v2.
3
3
  *
4
4
  * WHICH agent plans, builds, repairs, and reviews a task is decided here,
5
5
  * once, from signed facts — never guessed from a model's name and never
6
6
  * re-decided behind an approval's back. The policy is a small table over
7
7
  * explicit inputs:
8
8
  *
9
- * risk — the task's declared risk (routine / elevated / high)
9
+ * size — how big a change the task is (small / medium / large, risky or not)
10
10
  * quality — the evidence policy the scope signs (default / strict)
11
11
  * evidence — what the acceptance rubric demands (screenshots, …)
12
12
  * publication — how far the plane may carry the result unattended
@@ -35,8 +35,11 @@
35
35
  * is what actually runs. Provider readiness is a runner's observation,
36
36
  * not a term — it rides beside the route (ready / unavailable / unknown),
37
37
  * it is stated wherever the route is shown, and an unavailable provider
38
- * is never substituted: admission halts with the reason, and only an
39
- * already-approved explicit fallback entry may ever run instead.
38
+ * is never substituted: admission halts with the reason.
39
+ *
40
+ * Every route carries `risk: "routine"`: the declared risk level was
41
+ * removed in v115 (size does that job), and older sealed routes keep
42
+ * whatever risk they were recommended for, read back exactly.
40
43
  *
41
44
  * Approval seals the route (`approvedRouteJson`); its digest folds into
42
45
  * the scope digest of EVERY route filed since v47 — routine-shaped routes
@@ -77,7 +80,6 @@ export const ROUTE_ERA = 1;
77
80
  export const PHASES = ROUTE_PHASES;
78
81
  /** The review leg remains readable in signed history but is no longer scheduled. */
79
82
  export const ACTIVE_PHASES = ["plan", "build", "repair"];
80
- export const RISK_LEVELS = RISKS;
81
83
  export function isRiskLevel(value) {
82
84
  return value === "routine" || value === "elevated" || value === "high";
83
85
  }
@@ -85,8 +87,8 @@ export const TASK_SIZES = TASK_SIZE_WORDS;
85
87
  export function isTaskSize(value) {
86
88
  return value === "small" || value === "medium" || value === "large";
87
89
  }
88
- /** Whether a task makes no plan: a small change that is not risky, at
89
- * routine risk. Elevated or high risk always plans, whatever the size. */
90
+ /** Whether a task makes no plan: a small change that is not risky (on an
91
+ * older route sealed at elevated or high risk, it always planned). */
90
92
  export function makesNoPlan(size, risk) {
91
93
  return size != null && size.size === "small" && !size.risky && risk === "routine";
92
94
  }
@@ -102,20 +104,6 @@ export function parseSizing(raw) {
102
104
  export function riskTitle(risk) {
103
105
  return risk === "high" ? "High risk" : risk === "elevated" ? "Elevated risk" : "Routine";
104
106
  }
105
- /**
106
- * What declaring each risk level DOES, in one plain sentence — derived
107
- * from the tiering table below, so the explanation a form or a chat gives
108
- * can never drift from the policy that acts on it. The same words on
109
- * every surface: the task page's risk control, the chat's answer, the CLI.
110
- */
111
- export function riskConsequence(risk) {
112
- if (risk === "high")
113
- return "every active role — planner, builder, and repair — uses the strongest agent you have configured";
114
- if (risk === "elevated")
115
- return "planning and building use the strongest agent you have configured";
116
- return "every role uses the everyday configured agent unless the work itself asks for more (strict quality, screenshots, or a self-merging mode)";
117
- }
118
- export const RISK_CHOICES = RISK_LEVELS.map(risk => ({ risk, title: riskTitle(risk), consequence: riskConsequence(risk) }));
119
107
  const TIER_RANK = { light: 0, routine: 1, strong: 2 };
120
108
  /** Above these a provider has no headroom: even a candidate one tier
121
109
  * stronger on another provider takes the phase. Below them, a tier's
@@ -171,16 +159,6 @@ export function specWords(spec) {
171
159
  return `${spec.provider} · ${spec.model}`;
172
160
  }
173
161
  const DEMANDS = [
174
- {
175
- when: input => input.risk === "high",
176
- phases: ["plan", "build", "repair", "review"],
177
- reason: "risk is high — every role uses the strongest configured agent",
178
- },
179
- {
180
- when: input => input.risk === "elevated",
181
- phases: ["plan", "build", "repair", "review"],
182
- reason: "risk is elevated — planning and building use the strongest configured agent",
183
- },
184
162
  {
185
163
  when: input => input.size?.size === "large",
186
164
  phases: ["plan", "build", "repair"],
@@ -218,7 +196,8 @@ function demandedTier(input, phase) {
218
196
  }
219
197
  function economyReason(input, phase) {
220
198
  const facts = [
221
- `risk is ${input.risk}`,
199
+ // Said as it always was, so a route's words (and its digest) stay the same.
200
+ "risk is routine",
222
201
  `quality is ${input.qualityMode === "strict" ? "strict" : "default"}`,
223
202
  ...(input.publication === "notify" ? ["publication waits for a person"] : []),
224
203
  ];
@@ -242,7 +221,6 @@ export function recommendRoute(input) {
242
221
  .sort((a, b) => PHASES.indexOf(a.phase) - PHASES.indexOf(b.phase));
243
222
  const overrideFor = (phase) => overrides.find(one => one.phase === phase) ?? null;
244
223
  const legs = [];
245
- // Elevated or high risk lifts both legs through the table, so it never reaches the small branches.
246
224
  const small = input.size?.size === "small";
247
225
  const pick = (phase) => {
248
226
  const chosen = pickTier(phase);
@@ -405,7 +383,7 @@ export function recommendRoute(input) {
405
383
  }
406
384
  return {
407
385
  version: ROUTE_VERSION,
408
- risk: input.risk,
386
+ risk: "routine",
409
387
  qualityMode: input.qualityMode,
410
388
  publication: input.publication,
411
389
  evidence: [...new Set(input.evidence)].sort(),
@@ -616,7 +594,7 @@ export function sizeWords(route) {
616
594
  const model = build?.tier === "light" ? "fast model" : build?.tier === "strong" ? "strongest model" : "everyday model";
617
595
  if (makesNoPlan(route.size, route.risk))
618
596
  return `Small change: ${model}, no plan`;
619
- // Elevated or high risk plans a small change too.
597
+ // (A route sealed at elevated or high risk planned a small change too.)
620
598
  const label = route.size.size === "large" ? "Large change" : route.size.size === "small" ? (route.size.risky ? "Small but risky change" : "Small change") : route.size.risky ? "Risky change" : "Medium change";
621
599
  if (route.size.size !== "large" && !route.size.risky && route.risk === "routine")
622
600
  return build?.tier === "strong" ? "Medium change: strongest agents" : "Medium change: everyday agents";
package/dist/plan-auto.js CHANGED
@@ -12,7 +12,7 @@ export const PLAN_AUTO_SCHEMA = `CREATE TABLE IF NOT EXISTS plan_authorization (
12
12
  function plainPlan(store, taskId) {
13
13
  const ref = store.lookupRef(taskId);
14
14
  return ref !== null && ref.deliverable === "branch" && ref.coordinatorCid === null &&
15
- store.activeTournamentTerms(ref.id) === null && store.openAuthorizationFor(ref.id) === null && store.mirrorByTask(taskId) === null;
15
+ store.mirrorByTask(taskId) === null;
16
16
  }
17
17
  /** Join the console's authority-free proposal to the authenticated filing
18
18
  * road in one transaction. Planning defers the seal until verified ingestion. */
@@ -24,7 +24,7 @@ export function applyModeToNewFiling(store, taskId, actor, now) {
24
24
  return;
25
25
  const filed = propose(store, { taskId, goal: scope.goal, outOfScope: scope.outOfScope, touches: scope.touches, acceptance: scope.acceptance,
26
26
  budgetMicrousd: scope.budgetMicrousd ?? coverage.defaultBudgetMicrousd,
27
- qualityMode: scope.qualityMode ?? "default", riskLevel: scope.riskLevel ?? "routine",
27
+ qualityMode: scope.qualityMode ?? "default",
28
28
  ...(coverage.escalated ? { posture: "escalated" } : {}), now });
29
29
  if (ref.plan === "requested")
30
30
  authorizePlanUnderMode(store, taskId, actor, now);
@@ -175,17 +175,11 @@ export function reviewPlane(store, now, canSee = () => true) {
175
175
  }
176
176
  }
177
177
  // Chat replies that couldn't be delivered.
178
- for (const app of ["slack", "discord", "teams"]) {
179
- if (!tableExists(store, `${app}_part`))
180
- continue;
181
- const name = { slack: "Slack", discord: "Discord", teams: "Teams" }[app];
182
- for (const row of store.handle.prepare(`SELECT id, problem, attempts FROM ${app}_part WHERE state = 'dropped' AND created >= ? ORDER BY id`).all(since)) {
183
- add(`chat/${app}`, `${name} replies weren't delivered`, ["reply", "replies"], { evidence: `After ${plural(Number(row["attempts"] ?? 0), "try", "tries")}: ${excerpt(str(row["problem"])) || "dropped"}` });
184
- }
185
- }
186
- if (tableExists(store, "telegram_conversation_part")) {
187
- for (const row of store.handle.prepare("SELECT last_error, attempts FROM telegram_conversation_part WHERE state = 'dropped' AND created_at >= ?").all(since)) {
188
- add("chat/telegram", "Telegram replies weren't delivered", ["reply", "replies"], { evidence: `After ${plural(Number(row["attempts"] ?? 0), "try", "tries")}: ${excerpt(str(row["last_error"])) || "dropped"}` });
178
+ if (tableExists(store, "chat_part")) {
179
+ const names = { telegram: "Telegram", slack: "Slack", discord: "Discord", teams: "Teams" };
180
+ for (const row of store.handle.prepare("SELECT provider, problem, attempts FROM chat_part WHERE state = 'dropped' AND created >= ? ORDER BY provider = 'telegram' DESC, provider, id").all(since)) {
181
+ const app = String(row["provider"]);
182
+ add(`chat/${app}`, `${names[app] ?? app} replies weren't delivered`, ["reply", "replies"], { evidence: `After ${plural(Number(row["attempts"] ?? 0), "try", "tries")}: ${excerpt(str(row["problem"])) || "dropped"}` });
189
183
  }
190
184
  }
191
185
  if (tableExists(store, "notification_delivery")) {
@@ -128,7 +128,8 @@ export function plannerContractOf(store, taskId) {
128
128
  agentPin: { provider: ref.agentProvider, model: ref.agentModel },
129
129
  planPin: { provider: ref.planProvider, model: ref.planModel },
130
130
  deliverable: ref.deliverable,
131
- riskLevel: ref.riskLevel ?? null,
131
+ // The declared risk level was removed in v115; the field stays in the recorded shape.
132
+ riskLevel: null,
132
133
  qualityMode: ref.qualityMode ?? null,
133
134
  permissionMode: ref.permissionMode ?? null,
134
135
  },
package/dist/policy.d.ts CHANGED
@@ -15,8 +15,7 @@
15
15
  * (lowered), or, when there is none, doesn't run (refused); the words say which.
16
16
  *
17
17
  * Every road that admits work asks here: scope approval, the tick before a
18
- * run starts (and build() as the last look), fallback entries, race lanes,
19
- * attended sessions, the lead and project chats, teammates and flow steps.
18
+ * run starts (and build() as the last look), the lead and project chats, teammates and flow steps.
20
19
  */
21
20
  import type { Database } from "./store.js";
22
21
  import type { ExecutionProfile } from "./scope.js";
@@ -87,8 +86,6 @@ export declare function underCeiling(policy: OrgPolicy, profile: ExecutionProfil
87
86
  export declare function sessionCeilingRefusal(policy: OrgPolicy, provider: string, what: string): string | null;
88
87
  /** Everything the policy says about a profile before it runs: refused (words), or what runs and any lowering. */
89
88
  export declare function profileVerdict(policy: OrgPolicy, profile: ExecutionProfile): CeilingVerdict;
90
- /** An attended session runs at exactly what the person signed, so nothing is lowered: it runs as signed, or is refused. */
91
- export declare function attendedRefusal(policy: OrgPolicy, profile: ExecutionProfile): string | null;
92
89
  /** At approval, a person signs exact terms: anything above the ceiling is refused with words (a new filing is lowered). */
93
90
  export declare function approvalRefusal(policy: OrgPolicy, profiles: readonly ExecutionProfile[], legs?: readonly {
94
91
  provider: string;
package/dist/policy.js CHANGED
@@ -166,14 +166,6 @@ export function profileVerdict(policy, profile) {
166
166
  return { ok: false, message: `Its repairs: ${repair.charAt(0).toLowerCase()}${repair.slice(1)}` };
167
167
  return underCeiling(policy, profile);
168
168
  }
169
- /** An attended session runs at exactly what the person signed, so nothing is lowered: it runs as signed, or is refused. */
170
- export function attendedRefusal(policy, profile) {
171
- const verdict = profileVerdict(policy, profile);
172
- if (!verdict.ok)
173
- return verdict.message;
174
- return verdict.lowered === null ? null
175
- : `This session asks for ${levelWords(profile)}, above the organisation policy's permission ceiling (${LEVEL_NAMES[policy.ceiling]}). Authorise it again with a lower permission, or ${WHERE.charAt(0).toLowerCase()}${WHERE.slice(1)}`;
176
- }
177
169
  /** At approval, a person signs exact terms: anything above the ceiling is refused with words (a new filing is lowered). */
178
170
  export function approvalRefusal(policy, profiles, legs = []) {
179
171
  for (const profile of profiles) {
@@ -46,7 +46,7 @@ export function preparedCandidateObserverGapEligibility(store, input) {
46
46
  !instant(run.startedAt) || !instant(run.finishedAt) || Date.parse(run.finishedAt) < Date.parse(run.startedAt) ||
47
47
  Date.parse(run.finishedAt) > now.getTime())
48
48
  return refuse("not-terminal-prepared-builder");
49
- if (run.providerStartedAt !== null || run.sessionId !== null || run.attendedAuthorization != null || run.contestant !== null ||
49
+ if (run.providerStartedAt !== null || run.sessionId !== null ||
50
50
  !commit(run.baseRevision) || !commit(run.headRevision) || !run.branch || !run.worktree)
51
51
  return refuse("provider-or-run-identity-unproven");
52
52
  const ref = store.refById(run.taskRef);
@@ -79,8 +79,7 @@ export function preparedCandidateObserverGapEligibility(store, input) {
79
79
  if (!one || one.outcome === null || !instant(one.startedAt) || !instant(one.finishedAt) ||
80
80
  Date.parse(one.finishedAt) < Date.parse(one.startedAt) || Date.parse(one.finishedAt) > Date.parse(run.finishedAt) ||
81
81
  one.providerStartedAt !== null || one.sessionId !== null ||
82
- store.applicableStopFor(id) !== null || ownedProcessCount(runOwnerTag(store, id)) > 0 ||
83
- store.heldSessionOf(id)?.endedAt === null)
82
+ store.applicableStopFor(id) !== null || ownedProcessCount(runOwnerTag(store, id)) > 0)
84
83
  return refuse("owned-run-not-settled");
85
84
  ownedRuns.push({ id, taskRef: one.taskRef, leaseId: one.leaseId, parentRun: one.parentRun, role: one.role,
86
85
  outcome: one.outcome, startedAt: one.startedAt, finishedAt: one.finishedAt, providerStartedAt: one.providerStartedAt, sessionId: one.sessionId });
@@ -48,7 +48,7 @@ function doomed(db, repo) {
48
48
  const SETTINGS = [
49
49
  ["approval_policy", "repo"], ["verify_command", "repo"], ["worktree_setup", "repo"], ["backend_grant", "repo"], ["publication_grant", "repo"],
50
50
  ["intake_grant", "repo"], ["capability", "repo"], ["project_tool", "repo"], ["phase_config", "scope"], ["phase_tier_config", "scope"],
51
- ["fallback_config", "scope"], ["learning_policy", "repo"], ["operating_mode", "repo"], ["routine", "repo"], ["project_knowledge", "repo"],
51
+ ["learning_policy", "repo"], ["operating_mode", "repo"], ["project_knowledge", "repo"],
52
52
  ["project_decision", "repo"], ["flow_script", "repo"], ["workflow_recipe", "repo"], ["team_lead_project", "project"],
53
53
  ];
54
54
  /** What Toolroll holds for a project, counted. */
@@ -84,7 +84,6 @@ export function projectRunning(store, repo, now) {
84
84
  const at = now.toISOString();
85
85
  const count = (sql, ...params) => Number(db.prepare(sql).get(...params)?.["n"] ?? 0);
86
86
  const building = count(`SELECT COUNT(DISTINCT task_ref) AS n FROM claim WHERE released_at IS NULL AND expires_at > ? AND task_ref IN (SELECT value FROM json_each(?))`, at, json(d.refs));
87
- const sessions = count("SELECT COUNT(*) AS n FROM held_session WHERE ended_at IS NULL AND run IN (SELECT value FROM json_each(?))", json(d.runs));
88
87
  const slots = count("SELECT COUNT(*) AS n FROM execution_slot WHERE state IN ('reserved','running') AND run IN (SELECT value FROM json_each(?))", json(d.runs));
89
88
  const chats = count("SELECT COUNT(*) AS n FROM mate_turn WHERE state IN ('queued','running') AND id IN (SELECT value FROM json_each(?))", json(d.turns));
90
89
  const steps = count("SELECT COUNT(*) AS n FROM flow_step_run WHERE state = 'running' AND card IN (SELECT value FROM json_each(?))", json(d.cards));
@@ -93,7 +92,7 @@ export function projectRunning(store, repo, now) {
93
92
  const checkouts = count("SELECT COUNT(*) AS n FROM worktree WHERE repo = ? AND runner IS NOT NULL AND released_at IS NULL", repo);
94
93
  return [
95
94
  ...(building > 0 ? [`${plural(building, "task")} ${building === 1 ? "is" : "are"} being built`] : []),
96
- ...(sessions > 0 || slots > 0 ? ["an agent is working"] : []),
95
+ ...(slots > 0 ? ["an agent is working"] : []),
97
96
  ...(chats > 0 ? ["a chat is answering"] : []),
98
97
  ...(steps > 0 ? ["a flow step is running"] : []),
99
98
  ...(calls > 0 ? ["a teammate is working"] : []),
@@ -117,7 +116,6 @@ async function removeCheckoutsAndBranches(store, repo, d, git, poolRoot) {
117
116
  const named = [
118
117
  ...d.tasks.flatMap(id => OWN_BRANCHES.map(prefix => `${prefix}${id}`)), ...recorded.map(row => row.branch),
119
118
  ...list(db, "SELECT DISTINCT branch FROM run WHERE branch IS NOT NULL AND id IN (SELECT value FROM json_each(?))", json(d.runs)),
120
- ...list(db, "SELECT DISTINCT c.branch FROM contestant c JOIN contest k ON k.id = c.contest WHERE c.branch IS NOT NULL AND k.task_ref IN (SELECT value FROM json_each(?))", json(d.refs)),
121
119
  ...list(db, "SELECT DISTINCT head FROM publication WHERE head IS NOT NULL AND task_ref IN (SELECT value FROM json_each(?))", json(d.refs)),
122
120
  ].map(String).filter(isOwnBranch);
123
121
  const ours = new Set(named);
@@ -209,35 +207,30 @@ function deleteRows(store, repo, d, now) {
209
207
  const notifications = json(ids(db, `SELECT id FROM notification WHERE project = ? OR ${IN("task_ref")} OR ${IN("source_run")}`, repo, T, R));
210
208
  const questions = json(ids(db, `SELECT id FROM teammate_question WHERE ${IN("teammate")} OR ${IN("card")}`, M, C));
211
209
  const proposals = json(ids(db, `SELECT id FROM mate_proposal WHERE ${IN("thread")}`, H));
212
- const conversations = json(ids(db, `SELECT id FROM telegram_conversation WHERE ${IN("task_id")}`, K));
213
- // Chats about the project's work, on every surface.
214
- for (const surface of ["discord", "slack", "teams"]) {
215
- del(`${surface}_progress`, IN("run"), R);
216
- del(`${surface}_action`, IN("proposal"), proposals);
217
- del(`${surface}_flow_action`, IN("card"), C);
218
- del(`${surface}_flow_prompt`, IN("card"), C);
219
- del(`${surface}_flow_choice`, IN("card"), C);
220
- del(`${surface}_flow_note`, IN("card"), C);
221
- del(`${surface}_question_action`, IN("question"), questions);
222
- del(`${surface}_question_prompt`, IN("question"), questions);
223
- del(`${surface}_ask_action`, IN("turn"), U);
224
- }
225
- del("telegram_action", IN("decision"), decisions);
226
- del("telegram_decision_message", IN("decision"), decisions);
227
- del("telegram_note_draft", IN("decision"), decisions);
228
- del("telegram_proposal_action", IN("proposal"), proposals);
229
- del("telegram_flow_action", IN("card"), C);
230
- del("telegram_flow_prompt", IN("card"), C);
231
- del("telegram_flow_choice", IN("card"), C);
210
+ // A chat message about one of the project's tasks (its turn named it), with its reply. Event ids are text ('m123'),
211
+ // and the same id can be another app's: each is kept whole, as provider:id.
212
+ const conversations = json(list(db, `SELECT provider || ':' || id FROM chat_event WHERE kind = 'message' AND json_extract(payload, '$.about.task') IN (SELECT value FROM json_each(?))`, K));
213
+ // Every app's planned parts about the project's work: a result screenshot, or a message naming one of its tasks or runs.
214
+ const parts = `SELECT provider || ':' || id FROM chat_part WHERE provider || ':' || event IN (SELECT value FROM json_each(?))
215
+ OR json_extract(payload, '$.image.taskId') IN (SELECT value FROM json_each(?)) OR json_extract(payload, '$.image.run') IN (SELECT value FROM json_each(?))`;
216
+ const doomedParts = `provider || ':' || part IN (${parts})`;
217
+ // Chats about the project's work, on every app.
218
+ del("chat_progress", IN("run"), R);
219
+ del("chat_action", `${IN("proposal")} OR ${IN("decision")} OR ${doomedParts}`, proposals, decisions, conversations, K, R);
220
+ del("chat_flow_action", `${IN("card")} OR ${doomedParts}`, C, conversations, K, R);
221
+ del("chat_flow_prompt", IN("card"), C);
222
+ del("chat_flow_choice", `${IN("card")} OR ${doomedParts}`, C, conversations, K, R);
223
+ del("chat_flow_note", IN("card"), C);
224
+ del("chat_question_action", `${IN("question")} OR ${doomedParts}`, questions, conversations, K, R);
225
+ del("chat_question_prompt", IN("question"), questions);
226
+ del("chat_ask_action", `${IN("turn")} OR ${doomedParts}`, U, conversations, K, R);
227
+ del("chat_note_draft", IN("decision"), decisions);
232
228
  del("flow_send", IN("card"), C);
233
- del("telegram_question_action", IN("question"), questions);
234
- del("telegram_question_prompt", IN("question"), questions);
235
229
  del("chat_decide_action", IN("task_id"), K);
236
230
  del("chat_decide_prompt", IN("task_id"), K);
237
- del("telegram_task_message", `${IN("task_id")} OR ${IN("source_run")}`, K, R);
238
- del("telegram_conversation_part", `${IN("conversation")} OR ${IN("source_run")} OR ${IN("task_id")}`, conversations, R, K);
239
- del("telegram_conversation", IN("id"), conversations);
240
- del("telegram_outbound_message", `project = ? OR ${IN("notification")} OR ${IN("task_ref")} OR ${IN("source_run")}`, repo, notifications, T, R);
231
+ del("chat_message_ref", `${IN("decision")} OR ${IN("task_id")} OR ${IN("run")} OR project = ? OR ${IN("notification")} OR ${IN("task_ref")}`, decisions, K, R, repo, notifications, T);
232
+ del("chat_part", `provider || ':' || id IN (${parts})`, conversations, K, R);
233
+ del("chat_event", `provider || ':' || id IN (SELECT value FROM json_each(?))`, conversations);
241
234
  del("chat_card_task", IN("task_ref"), T);
242
235
  del("chat_batch_item", IN("task_ref"), T);
243
236
  del("result_shot_sent", IN("run"), R);
@@ -263,7 +256,7 @@ function deleteRows(store, repo, d, now) {
263
256
  del("diff_comment", `${IN("run")} OR ${IN("reviewer_run")} OR ${IN("artifact")}`, R, R, A);
264
257
  del("review_request", `${IN("run")} OR ${IN("reviewer_run")}`, R, R);
265
258
  del("repair_chain", IN("source_run"), R);
266
- for (const table of ["incident", "held_session", "proof_acceptance", "proof_verdict", "run_process", "run_route", "run_spend", "run_tool", "run_note", "run_stop", "run_checkpoint", "session_turn", "execution_slot"])
259
+ for (const table of ["incident", "proof_acceptance", "proof_verdict", "run_process", "run_route", "run_spend", "run_tool", "run_note", "run_stop", "run_checkpoint", "execution_slot"])
267
260
  del(table, IN("run"), R);
268
261
  del("knowledge_snapshot", `repo = ? OR ${IN("run")}`, repo, R);
269
262
  del("learning_snapshot", `repo = ? OR ${IN("run")}`, repo, R);
@@ -277,19 +270,10 @@ function deleteRows(store, repo, d, now) {
277
270
  del("publication", IN("id"), publications);
278
271
  del("artifact", IN("id"), A);
279
272
  // Tasks and their versions.
280
- const cycles = json(ids(db, `SELECT id FROM fallback_cycle WHERE ${IN("task_ref")}`, T));
281
- del("fallback_transition", `${IN("cycle")} OR ${IN("predecessor_run")} OR ${IN("consumed_by")}`, cycles, R, R);
282
- del("fallback_cycle", IN("id"), cycles);
283
- const contests = json(ids(db, `SELECT id FROM contest WHERE ${IN("task_ref")}`, T));
284
- del("contestant", IN("contest"), contests);
285
- del("contest", IN("id"), contests);
286
- del("tournament_terms", IN("task_ref"), T);
287
- del("attended_authorization", IN("task_ref"), T);
288
273
  del("run", IN("id"), R);
289
274
  del("plan_revision", IN("task_ref"), T);
290
275
  for (const table of ["claim", "hold", "task_steer"])
291
276
  del(table, IN("task_ref"), T);
292
- del("routine_fire", IN("instance_task_ref"), T);
293
277
  const mirrors = json(list(db, `SELECT local_task_id FROM external_mirror WHERE ${IN("local_task_id")}`, K));
294
278
  del("external_intent", IN("mirror"), mirrors);
295
279
  del("external_mirror", IN("local_task_id"), mirrors);
@@ -323,7 +307,6 @@ function deleteRows(store, repo, d, now) {
323
307
  del("operating_mode_event", "mode IN (SELECT id FROM operating_mode WHERE repo = ?)", repo);
324
308
  del("decision_change", "decision IN (SELECT id FROM project_decision WHERE repo = ?)", repo);
325
309
  del("memory_sighting", "gap IN (SELECT id FROM memory_gap WHERE repo = ?) OR session IN (SELECT id FROM memory_session WHERE repo = ?)", repo, repo);
326
- del("routine_fire", "routine_id IN (SELECT id FROM routine WHERE repo = ?)", repo);
327
310
  for (const [table, column] of SETTINGS)
328
311
  del(table, `${column} = ?`, repo);
329
312
  del("project_mute", "repo = ?", repo);
@@ -1,7 +1,7 @@
1
1
  import type { Filer } from "./approval-policy.js";
2
2
  export { validateTaskText } from "./task-text.js";
3
3
  import type { Store } from "./store.js";
4
- import { type UnattendedPermissionMode } from "./scope.js";
4
+ import type { UnattendedPermissionMode } from "./scope.js";
5
5
  import type { QualityMode } from "./quality.js";
6
6
  import type { TaskSizing } from "./phase-routing.js";
7
7
  export type ProposalRefusal = {
@@ -48,6 +48,9 @@ export type TaskProposalInput = {
48
48
  * which is the fail-closed reading of an empty ceiling (finding 4).
49
49
  */
50
50
  admittedRepos?: readonly string[];
51
+ /** A per-task dollar cap in micro-USD, bound into the scope's digest (a
52
+ * scheduled flow's standing order carries one, v115). */
53
+ budgetMicrousd?: number | null;
51
54
  /** v2 routing: the task's size when the caller already knows it (a
52
55
  * person's choice). Absent, filing sizes it from the description at
53
56
  * once and the process's classifier refines it within five seconds. */
@@ -59,32 +62,9 @@ export type TaskProposalInput = {
59
62
  * size (v2 routing), a small change is not planned and a large or risky
60
63
  * one is; a medium one keeps the description's signals. */
61
64
  export declare function shouldPlanTask(input: TaskProposalInput, sizing?: TaskSizing): boolean;
62
- export type RoutineProposalInput = {
63
- name: string;
64
- /** v102: who is making it (its instances are filed as theirs). */
65
- createdBy?: string | null;
66
- repo: string;
67
- goal: string;
68
- outOfScope: string | null;
69
- touches: string[];
70
- /** v39: the signed rubric every instance's scope copies forward. */
71
- acceptance: unknown;
72
- requirements: string[];
73
- schedule: string;
74
- costCeilingUsd: number | null;
75
- /** Per-instance dollar cap in micro-USD (v16); optional, digest-bound. */
76
- budgetPerRunMicrousd?: number | null;
77
- filedVia: string;
78
- admittedRepos?: readonly string[];
79
- };
80
65
  export declare function fileTaskProposal(store: Store, input: TaskProposalInput, now: Date): {
81
66
  ok: true;
82
67
  id: string;
83
68
  planning: boolean;
84
69
  sizing: TaskSizing;
85
70
  } | ProposalRefusal;
86
- export declare function fileRoutineProposal(store: Store, input: RoutineProposalInput, now: Date): {
87
- ok: true;
88
- id: number;
89
- digest: string;
90
- } | ProposalRefusal;
package/dist/proposal.js CHANGED
@@ -5,8 +5,8 @@ export { validateTaskText } from "./task-text.js";
5
5
  *
6
6
  * Before this module, each filing surface — CLI, console, intake — validated
7
7
  * what it happened to think of: createConsoleTask checked title and goal but
8
- * not disguised text, the routine command checked terms but nothing checked a
9
- * repo against the caller's ceiling, and nothing stamped who filed what. The
8
+ * not disguised text, nothing checked a repo against the caller's ceiling,
9
+ * and nothing stamped who filed what. The
10
10
  * review's ruling: one canonical service that every filer calls, with an
11
11
  * exact field allowlist, so a careless caller CANNOT spread a template or
12
12
  * model object into scope rows and smuggle approval metadata.
@@ -27,12 +27,8 @@ export { validateTaskText } from "./task-text.js";
27
27
  * Filing carries NO authority. Everything this module creates is unapproved
28
28
  * and stays inert until the operator's own ceremony says otherwise.
29
29
  */
30
- import { resolveRoutineAuthority } from "./agentconfig.js";
31
30
  import { resolve } from "node:path";
32
- import { hasForbiddenControls, hasDisguisedText } from "./decision.js";
33
31
  import { canonicalProject } from "./project.js";
34
- import { ROUTINE_NAME, routineDigestOf, validateRoutineTerms, } from "./routine.js";
35
- import { parseAcceptanceCriteria } from "./scope.js";
36
32
  import { heuristicSizing, planningSignals, refineFiledSizing } from "./task-sizing.js";
37
33
  /** Provenance tokens are part of the audit surface: lowercase, bounded,
38
34
  * nothing that could render as anything but itself. */
@@ -65,18 +61,6 @@ export function shouldPlanTask(input, sizing) {
65
61
  function refuse(reason, message) {
66
62
  return { ok: false, reason, message };
67
63
  }
68
- /** The fields an approver reads must BE what they appear to be: no control
69
- * characters anywhere, no invisible or direction-override text either. */
70
- function dishonest(text) {
71
- return hasForbiddenControls(text) || hasDisguisedText(text);
72
- }
73
- /** Byte caps beside the character caps (Codex v3 review, change 8): a
74
- * 2000-character goal of astral-plane text is 8000 bytes — model-authored
75
- * fields must bound STORAGE, not just what a screen shows. */
76
- const BYTE_CAPS = { title: 800, name: 200, text: 8_000, path: 800, requirement: 400 };
77
- function overBytes(text, cap) {
78
- return Buffer.byteLength(text, "utf8") > cap;
79
- }
80
64
  function checkRepo(given, admitted) {
81
65
  if (given === undefined || given === "") {
82
66
  // A ceiling names the ONLY repos this surface may file into. Filing
@@ -119,6 +103,7 @@ export function fileTaskProposal(store, input, now) {
119
103
  ...(input.goal === undefined ? {} : { goal: input.goal }),
120
104
  ...(input.permissionMode === undefined ? {} : { permissionMode: input.permissionMode }),
121
105
  ...(input.qualityMode === undefined ? {} : { qualityMode: input.qualityMode }),
106
+ ...(input.budgetMicrousd == null ? {} : { budgetMicrousd: input.budgetMicrousd }),
122
107
  outOfScope,
123
108
  touches,
124
109
  acceptance: input.acceptance,
@@ -153,73 +138,3 @@ export function fileTaskProposal(store, input, now) {
153
138
  }
154
139
  return { ok: true, id: made.id, planning, sizing };
155
140
  }
156
- export function fileRoutineProposal(store, input, now) {
157
- if (!FILED_VIA.test(input.filedVia)) {
158
- return refuse("bad-provenance", "filedVia is an audit token: lowercase letters, digits, dashes, colons");
159
- }
160
- const repo = checkRepo(input.repo, input.admittedRepos);
161
- if (!repo.ok)
162
- return repo;
163
- if (repo.repo === undefined)
164
- return refuse("bad-repo", "a routine needs the repository it runs in");
165
- const acceptanceParse = parseAcceptanceCriteria(input.acceptance);
166
- const terms = {
167
- repo: repo.repo,
168
- goal: input.goal,
169
- outOfScope: input.outOfScope,
170
- touches: input.touches,
171
- acceptance: acceptanceParse.criteria,
172
- requirements: input.requirements,
173
- schedule: input.schedule,
174
- // v1 routines run one instance at a time, period — hardcoded, not accepted.
175
- singleFlight: true,
176
- costCeilingUsd: input.costCeilingUsd,
177
- ...(input.budgetPerRunMicrousd == null ? {} : { budgetPerRunMicrousd: input.budgetPerRunMicrousd }),
178
- };
179
- // EVERY problem at once — an operator fixing a form deserves the whole
180
- // list, not one complaint per submission.
181
- const problems = validateRoutineTerms(terms);
182
- if (acceptanceParse.problems.length > 0) {
183
- problems.push({ field: "acceptance", problem: acceptanceParse.problems.map(p => p.message).join("; ") });
184
- }
185
- if (!ROUTINE_NAME.test(input.name)) {
186
- problems.unshift({ field: "name", problem: "lowercase letters, digits, and dashes — it becomes each instance's id" });
187
- }
188
- if (dishonest(input.goal) ||
189
- overBytes(input.goal, BYTE_CAPS.text) ||
190
- (input.outOfScope !== null && (dishonest(input.outOfScope) || overBytes(input.outOfScope, BYTE_CAPS.text)))) {
191
- problems.push({ field: "goal", problem: "no control or disguised text, bounded bytes" });
192
- }
193
- // The v3 review, change 8: touches and requirements are approver-read
194
- // text too — the door holds them to the same honesty everywhere.
195
- if (input.touches.some(one => dishonest(one) || overBytes(one, BYTE_CAPS.path))) {
196
- problems.push({ field: "touches", problem: "no control or disguised text, bounded bytes" });
197
- }
198
- if (input.requirements.some(one => dishonest(one) || overBytes(one, BYTE_CAPS.requirement))) {
199
- problems.push({ field: "requirements", problem: "no control or disguised text, bounded bytes" });
200
- }
201
- if (problems.length > 0) {
202
- const named = problems.map(one => `${one.field}: ${one.problem}`).join("; ");
203
- return refuse(problems.some(one => one.field === "name") ? "bad-name" : "bad-terms", named);
204
- }
205
- // v24 filing invariant, routine flavor — since v48 the whole four-role
206
- // ROUTE: resolve which agents plan, build, repair, and review a firing
207
- // once, here, from the configuration of this moment, and bind route and
208
- // profile into the digest a person will sign. Unresolved saves too
209
- // (finding 19) — approval then refuses until the routine is restated.
210
- const authority = resolveRoutineAuthority(store, repo.repo, terms.acceptance, now);
211
- const routineProfile = authority.ok ? authority.profile : null;
212
- const routineRoute = authority.ok ? authority.route : null;
213
- const created = store.createRoutine({
214
- name: input.name,
215
- ...terms,
216
- digest: routineDigestOf(terms, routineProfile, routineRoute),
217
- filedVia: input.filedVia,
218
- createdBy: input.createdBy ?? null,
219
- ...(routineProfile === null ? {} : { profile: routineProfile }),
220
- ...(routineRoute === null ? {} : { route: routineRoute }),
221
- }, now);
222
- if (!created.ok)
223
- return refuse("duplicate", `a routine named ${input.name} already exists`);
224
- return { ok: true, id: created.id, digest: routineDigestOf(terms, routineProfile, routineRoute) };
225
- }