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
package/dist/scope.d.ts CHANGED
@@ -123,14 +123,6 @@ export declare function canonicalChainJson(entries: readonly ChainEntry[]): stri
123
123
  * ":profile:"), so a chain digest can never collide with a profile
124
124
  * digest, truncated to the same 128 bits. */
125
125
  export declare function chainDigestOf(entries: readonly ChainEntry[]): string;
126
- /**
127
- * One chain entry's BINDING digest (E3d): the profile digest + auth mode
128
- * under their own domain. A run row pins this at admission (or base-cycle
129
- * open), and the dispatch proof re-derives it from the approved chain at
130
- * the run's index — so nothing downstream can swap WHICH entry a run
131
- * spends as, in model or in credential.
132
- */
133
- export declare function entryDigestOf(entry: ChainEntry): string;
134
126
  /** Strict re-hydration of a stored chain — every entry proved through the
135
127
  * single-profile rehydrator; anything unexpected is null. */
136
128
  export declare function chainFromJson(json: string | null): ChainEntry[] | null;
@@ -182,7 +174,7 @@ export declare function profileFromJson(json: string | null): ExecutionProfile |
182
174
  export type { AcceptanceCriterion, AcceptanceProblem };
183
175
  /**
184
176
  * Parse a rubric from already-JSON-parsed input (the planner's handoff, a
185
- * console form, a routine or template definition): fail closed, every
177
+ * console form, a template or a schedule's terms): fail closed, every
186
178
  * problem reported at once, each naming its path — the acceptance contract
187
179
  * (contracts/scope.ts) reads it. An absent or empty `value` parses to `[]`
188
180
  * with no problems: whether that is ALLOWED is a question for the caller
@@ -281,13 +273,10 @@ export type Scope = SavedScopeTerms & {
281
273
  profileJson?: string | null;
282
274
  approvedProfileJson?: string | null;
283
275
  digestVersion?: number;
284
- /** v30 fallback chains. `proposedChainJson` is the WORKING chain snapshot
285
- * the digest bound (present only when the repo has configured fallbacks);
286
- * `approvedChainJson` is the immutable snapshot the seal COPIED from it;
287
- * `approvalKind` names which the approval sealed. All undefined/null on a
288
- * legacy single-profile scope — every scope until an operator configures a
289
- * fallback chain. The runtime re-derives the active entry from
290
- * `approvedChainJson`, never from mutable config. */
276
+ /** v30-v114 fallback chains (history): the working and sealed chain
277
+ * snapshots and which kind the approval sealed. Never written since v115
278
+ * (a re-approval writes `profile` and clears the sealed chain); an old
279
+ * chain approval still decodes and verifies. */
291
280
  proposedChainJson?: string | null;
292
281
  approvedChainJson?: string | null;
293
282
  approvalKind?: "profile" | "chain";
@@ -336,8 +325,8 @@ export type ScopeInput = {
336
325
  goal: string;
337
326
  outOfScope?: string | null;
338
327
  touches?: readonly string[];
339
- /** v24: an EXPLICIT profile skips resolution in the store (routine
340
- * firings and the demo's illustrative scopes use this road). */
328
+ /** v24: an EXPLICIT profile skips resolution in the store (a schedule's
329
+ * approved firings and the demo's illustrative scopes use this road). */
341
330
  profile?: ExecutionProfile;
342
331
  /** A task-level permission choice. When absent, the task's stored choice
343
332
  * (if any), then the installation default, decides the concrete profile. */
@@ -347,9 +336,6 @@ export type ScopeInput = {
347
336
  qualityMode?: QualityMode;
348
337
  /** v69: a prepared commit to install as this attempt — no agent runs. */
349
338
  candidate?: string | null;
350
- /** v47: the task's declared risk. When absent, the task's stored choice
351
- * (if any), else routine. A durable TASK choice, like qualityMode. */
352
- riskLevel?: RiskLevel;
353
339
  /** Integer micro-dollars per build attempt; digest-bound when present. */
354
340
  budgetMicrousd?: number | null;
355
341
  /** The mode road's escalated filing default (C7): the resolved profile
@@ -504,7 +490,7 @@ export declare function authenticateAccount(store: Store, by: string, secret: st
504
490
  * v100: someone who signed in with the identity provider moments ago has no
505
491
  * password to type again. The console marks such a request with this, for
506
492
  * that person only, and an EMPTY password then stands for the fresh sign-in
507
- * at every step-up that reaches `authenticateApprover` (scope and routine
493
+ * at every step-up that reaches `authenticateApprover` (scope
508
494
  * approval, chat-action review, the console's own ceremonies). A typed
509
495
  * password is always checked, and nothing else stands in.
510
496
  */
@@ -574,12 +560,10 @@ export declare function approvalOf(scope: Scope | null): Approval;
574
560
  /**
575
561
  * THE STRICT STORED-SCOPE PROJECTION (v48 integrity): the ONE reading of
576
562
  * a filed scope that a seal and every consent surface believe. Nothing
577
- * lenient stands in for it — the working profile, chain, and route are
563
+ * lenient stands in for it — the working profile and route are
578
564
  * re-parsed from their raw bytes with exact keys, safe integers, and
579
- * timer-safe clocks; the chain's entries carry a well-formed auth mode;
580
- * the route's build and repair legs ARE the profile's exact pairs; on a
581
- * chain filing the profile IS the chain's entry zero; and the row's
582
- * digest re-derives, complete, from these very values. A legacy row (no
565
+ * timer-safe clocks; the route's build and repair legs ARE the profile's
566
+ * exact pairs; and the row's digest re-derives, complete, from these very values. A legacy row (no
583
567
  * route era) has no authority a person can newly agree to. One
584
568
  * disagreement is the words, and nothing — no nonce, no password field,
585
569
  * no approve action, no seal — is exposed on it.
@@ -587,7 +571,6 @@ export declare function approvalOf(scope: Scope | null): Approval;
587
571
  export type ScopeAuthority = {
588
572
  ok: true;
589
573
  profile: ExecutionProfile;
590
- chain: ChainEntry[] | null;
591
574
  route: PhaseRoute;
592
575
  digest: string;
593
576
  authMode: AuthMode | null;
@@ -637,40 +620,3 @@ export declare function acceptanceWords(criteria: readonly AcceptanceCriterion[]
637
620
  export declare function scopeRouteWords(scope: Pick<Scope, "proposedRouteJson">, readiness?: ReadinessLookup): string[];
638
621
  /** The scope, in the words an operator has to be able to agree or disagree with. */
639
622
  export declare function describeScope(scope: Scope, readiness?: ReadinessLookup): string[];
640
- /** The chain's approval-card lines; empty for a single-profile scope. */
641
- export declare function chainWords(chain: ChainEntry[] | null): string[];
642
- /**
643
- * EVERY rendered term of one watched attempt — what the form shows is what
644
- * the password signs, byte for byte. The subset the dispatch proof
645
- * re-derives (scopeDigest, profileDigest, profileJson, repo, head) is read
646
- * back by the builder and the coordinator; the rest are the product terms
647
- * the ceremony renders in words: the budget as a STOP THRESHOLD, the turn
648
- * cap, the per-turn clock whose expiry is SESSION-FATAL, and the absolute
649
- * expiry. Continuation carries the parent attempt and the follow-up text
650
- * INSIDE the signed terms (v3 R7).
651
- */
652
- export type AttendedTerms = {
653
- taskId: string;
654
- /** v28: HOW liveness is renewed — a SIGNED term, because liveness is an
655
- * admission predicate, not decoration. "console-visible" = any open
656
- * console page of this server renews use. Legacy terms lack the field
657
- * and beat-all refuses them (they were signed as page-bound). */
658
- attentionMode: "console-visible";
659
- scopeDigest: string;
660
- profileDigest: string;
661
- profileJson: string;
662
- repo: string;
663
- runner: string;
664
- runnerGeneration: number;
665
- head: string;
666
- maxSessionTurns: number;
667
- budgetMicrousd: number;
668
- turnTimeoutSeconds: number;
669
- absoluteExpiry: string;
670
- parentRun?: number | null;
671
- followup?: string | null;
672
- };
673
- /** Deterministic bytes: sorted keys, undefined dropped — the signed text. */
674
- export declare function attendedTermsJson(terms: AttendedTerms): string;
675
- /** The composite digest one password signs (ruling 12), domain-separated. */
676
- export declare function attendedDigestOf(terms: AttendedTerms): string;
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) {}