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.
- package/CHANGELOG.md +16 -0
- package/README.md +55 -80
- package/dist/agentconfig.d.ts +11 -36
- package/dist/agentconfig.js +3 -62
- package/dist/approval-policy.d.ts +1 -1
- package/dist/approval-policy.js +1 -1
- package/dist/board.d.ts +3 -19
- package/dist/board.js +4 -42
- package/dist/browser/workspace.js +18 -18
- package/dist/browser-workspace.d.ts +2 -1
- package/dist/builder.d.ts +9 -40
- package/dist/builder.js +50 -257
- package/dist/chat-actions.js +3 -14
- package/dist/chat-ask.js +8 -8
- package/dist/chat-controls.d.ts +0 -4
- package/dist/chat-controls.js +0 -1
- package/dist/chat-core.d.ts +171 -0
- package/dist/chat-core.js +288 -0
- package/dist/chat-decide.d.ts +10 -8
- package/dist/chat-decide.js +27 -24
- package/dist/chat-delivery-state.d.ts +51 -75
- package/dist/chat-delivery-state.js +140 -170
- package/dist/chat-delivery.d.ts +21 -4
- package/dist/chat-delivery.js +313 -195
- package/dist/chat-flow.d.ts +29 -1
- package/dist/chat-flow.js +24 -50
- package/dist/chat-messages.d.ts +319 -0
- package/dist/chat-messages.js +841 -0
- package/dist/chat-migration.d.ts +30 -0
- package/dist/chat-migration.js +802 -0
- package/dist/chat-quiet.d.ts +3 -0
- package/dist/chat-quiet.js +7 -2
- package/dist/chat-task-actions.d.ts +1 -1
- package/dist/chat-task-actions.js +0 -2
- package/dist/claim.d.ts +79 -244
- package/dist/claim.js +688 -1023
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +10 -2
- package/dist/contracts/chat-content.d.ts +71 -2
- package/dist/contracts/chat-content.js +22 -3
- package/dist/contracts/chat-tables.d.ts +11 -4
- package/dist/contracts/chat-tables.js +165 -87
- package/dist/contracts/cli.d.ts +0 -13
- package/dist/contracts/cli.js +0 -2
- package/dist/contracts/console-api.d.ts +5 -10
- package/dist/contracts/console-api.js +4 -10
- package/dist/contracts/discord-callback.js +2 -1
- package/dist/contracts/flow.d.ts +50 -0
- package/dist/contracts/flow.js +26 -1
- package/dist/contracts/lead-tools.d.ts +0 -13
- package/dist/contracts/lead-tools.js +3 -3
- package/dist/contracts/slack-callback.js +3 -2
- package/dist/contracts/store-json.d.ts +1 -29
- package/dist/contracts/store-json.js +4 -17
- package/dist/contracts/teams-callback.js +2 -1
- package/dist/converse.d.ts +1 -11
- package/dist/converse.js +6 -38
- package/dist/coordinator.d.ts +0 -1
- package/dist/coordinator.js +3 -5
- package/dist/demo.js +16 -26
- package/dist/desktop-update-gate.js +0 -1
- package/dist/desktop-update.js +1 -3
- package/dist/discord-chat.js +19 -38
- package/dist/discord-settings.js +4 -13
- package/dist/discord.js +4 -14
- package/dist/dispatch.d.ts +1 -1
- package/dist/dispatch.js +1 -1
- package/dist/dispose.d.ts +2 -3
- package/dist/dispose.js +17 -92
- package/dist/envelope.js +0 -3
- package/dist/exec.d.ts +0 -64
- package/dist/exec.js +3 -356
- package/dist/exhaustion.d.ts +22 -76
- package/dist/exhaustion.js +14 -140
- package/dist/export.js +8 -8
- package/dist/flow-engine.js +1 -1
- package/dist/flow-schedule.d.ts +154 -0
- package/dist/flow-schedule.js +321 -0
- package/dist/flow-share.js +1 -1
- package/dist/flow-triggers.d.ts +6 -1
- package/dist/flow-triggers.js +65 -10
- package/dist/flows-cli.js +8 -4
- package/dist/flows-ui.js +2 -0
- package/dist/flows.js +1 -1
- package/dist/guides.js +9 -29
- package/dist/invoke.d.ts +1 -24
- package/dist/invoke.js +17 -259
- package/dist/lead-status.js +2 -4
- package/dist/lead-voice.js +1 -1
- package/dist/mate-doors.d.ts +1 -2
- package/dist/mate-doors.js +12 -25
- package/dist/mate-tools.d.ts +2 -2
- package/dist/mate-tools.js +13 -24
- package/dist/modes.d.ts +19 -8
- package/dist/modes.js +24 -14
- package/dist/notification-rows.d.ts +30 -0
- package/dist/notification-rows.js +102 -0
- package/dist/operate-remote-arguments.js +2 -6
- package/dist/operate-remote.js +6 -7
- package/dist/operate.d.ts +0 -6
- package/dist/operate.js +226 -2248
- package/dist/phase-routing.d.ts +13 -23
- package/dist/phase-routing.js +13 -35
- package/dist/plan-auto.js +2 -2
- package/dist/plane-review.js +5 -11
- package/dist/planner-source.js +2 -1
- package/dist/policy.d.ts +1 -4
- package/dist/policy.js +0 -8
- package/dist/process-recovery-eligibility.js +2 -3
- package/dist/project-delete.js +24 -41
- package/dist/proposal.d.ts +4 -24
- package/dist/proposal.js +3 -88
- package/dist/provider-auth.d.ts +3 -3
- package/dist/provider-auth.js +4 -6
- package/dist/provider.d.ts +11 -25
- package/dist/provider.js +4 -36
- package/dist/push.js +1 -1
- package/dist/recipe-ui.js +6 -6
- package/dist/recipes.d.ts +3 -3
- package/dist/recipes.js +27 -23
- package/dist/result-shots.js +2 -2
- package/dist/retention.js +1 -1
- package/dist/run-closeout.d.ts +1 -1
- package/dist/run-closeout.js +1 -4
- package/dist/scope.d.ts +11 -65
- package/dist/scope.js +17 -82
- package/dist/serve.js +15 -24
- package/dist/server/chat.js +7 -30
- package/dist/server/flows.js +11 -10
- package/dist/server/guards.js +0 -1
- package/dist/server/pages-assets.js +1 -1
- package/dist/server/pages.js +0 -2
- package/dist/server/people-tokens.js +0 -4
- package/dist/server/route-table.d.ts +1 -1
- package/dist/server/route-table.js +2 -17
- package/dist/server/settings.js +6 -12
- package/dist/server/shared.d.ts +28 -212
- package/dist/server/shared.js +71 -780
- package/dist/server/task-views.js +9 -76
- package/dist/server/tasks.d.ts +1 -50
- package/dist/server/tasks.js +36 -946
- package/dist/setup-guide.d.ts +2 -2
- package/dist/slack-api.d.ts +5 -1
- package/dist/slack-api.js +2 -2
- package/dist/slack-chat.d.ts +9 -6
- package/dist/slack-chat.js +29 -53
- package/dist/slack-settings.js +6 -15
- package/dist/slack.js +6 -16
- package/dist/store.d.ts +200 -1702
- package/dist/store.js +811 -5439
- package/dist/surface.js +10 -24
- package/dist/task-composer.d.ts +1 -1
- package/dist/task-composer.js +1 -1
- package/dist/task-control.d.ts +3 -10
- package/dist/task-control.js +2 -7
- package/dist/teammate-desk.js +1 -1
- package/dist/teammate-question.js +12 -12
- package/dist/teammates-ui.js +1 -1
- package/dist/teams-chat.js +12 -9
- package/dist/teams-settings.js +2 -2
- package/dist/teams.js +4 -5
- package/dist/telegram-flow.js +5 -5
- package/dist/telegram-mate.d.ts +0 -3
- package/dist/telegram-mate.js +0 -1
- package/dist/telegram.js +4 -4
- package/dist/templates.d.ts +11 -7
- package/dist/templates.js +12 -9
- package/dist/toolroll-update.d.ts +20 -0
- package/dist/toolroll-update.js +104 -5
- package/dist/work-index.js +2 -6
- package/dist/workspace-revision.js +3 -1
- package/dist/workspace-ui.js +1 -1
- package/package.json +1 -1
- package/scripts/recovery-canary.mjs +3 -3
- package/dist/contest.d.ts +0 -318
- package/dist/contest.js +0 -753
- package/dist/held.d.ts +0 -150
- package/dist/held.js +0 -799
- package/dist/liveness.d.ts +0 -30
- package/dist/liveness.js +0 -42
- package/dist/pricing.d.ts +0 -45
- package/dist/pricing.js +0 -77
- package/dist/routine.d.ts +0 -228
- package/dist/routine.js +0 -764
- package/dist/slack-state.d.ts +0 -13
- package/dist/slack-state.js +0 -10
- package/dist/supervisor.mjs +0 -297
package/dist/provider-auth.d.ts
CHANGED
|
@@ -45,7 +45,7 @@ export declare function authPauseOf(store: Store, provider: string): AuthPause |
|
|
|
45
45
|
/**
|
|
46
46
|
* SQL: whether the pause `pause` (a provider_auth_pause alias) is on one of
|
|
47
47
|
* the task_ref `taskRef`'s current providers — its pins, its working profile,
|
|
48
|
-
* route legs
|
|
48
|
+
* and route legs — so a task re-routed to another provider
|
|
49
49
|
* stops saying it waits on the old one. A task that names none yet (an
|
|
50
50
|
* unpinned planner, whose agent comes from configuration) keeps what the gate
|
|
51
51
|
* found.
|
|
@@ -118,8 +118,8 @@ export type SignInGate = {
|
|
|
118
118
|
};
|
|
119
119
|
/**
|
|
120
120
|
* The sign-in gate every road that starts an agent asks before it claims
|
|
121
|
-
* anything: the tick's queue
|
|
122
|
-
*
|
|
121
|
+
* anything: the tick's queue and the roads beside it.
|
|
122
|
+
* Waits when a provider is paused,
|
|
123
123
|
* unless this is the pause's one trial. A waiting task is noted on its
|
|
124
124
|
* task_ref, so the work index says exactly what the gate decided; a task let
|
|
125
125
|
* through is un-noted. A road whose claim fails after a trial calls giveBack.
|
package/dist/provider-auth.js
CHANGED
|
@@ -72,7 +72,7 @@ export function authPauseOf(store, provider) {
|
|
|
72
72
|
/**
|
|
73
73
|
* SQL: whether the pause `pause` (a provider_auth_pause alias) is on one of
|
|
74
74
|
* the task_ref `taskRef`'s current providers — its pins, its working profile,
|
|
75
|
-
* route legs
|
|
75
|
+
* and route legs — so a task re-routed to another provider
|
|
76
76
|
* stops saying it waits on the old one. A task that names none yet (an
|
|
77
77
|
* unpinned planner, whose agent comes from configuration) keeps what the gate
|
|
78
78
|
* found.
|
|
@@ -83,9 +83,7 @@ export function pauseOnTaskProviders(pause, taskRef) {
|
|
|
83
83
|
UNION ALL SELECT json_extract(ts.profile_json, '$.provider') FROM task_ref r JOIN task_scope ts ON ts.task_id = r.external_id
|
|
84
84
|
WHERE r.id = ${taskRef} AND json_valid(ts.profile_json)
|
|
85
85
|
UNION ALL SELECT json_extract(leg.value, '$.provider') FROM task_ref r JOIN task_scope ts ON ts.task_id = r.external_id,
|
|
86
|
-
json_each(CASE WHEN json_valid(ts.proposed_route_json) THEN ts.proposed_route_json ELSE '{}' END, '$.legs') leg WHERE r.id = ${taskRef}
|
|
87
|
-
UNION ALL SELECT json_extract(entry.value, '$.profile.provider') FROM task_ref r JOIN task_scope ts ON ts.task_id = r.external_id,
|
|
88
|
-
json_each(CASE WHEN json_valid(ts.proposed_chain_json) THEN ts.proposed_chain_json ELSE '{}' END, '$.chain') entry WHERE r.id = ${taskRef}`;
|
|
86
|
+
json_each(CASE WHEN json_valid(ts.proposed_route_json) THEN ts.proposed_route_json ELSE '{}' END, '$.legs') leg WHERE r.id = ${taskRef}`;
|
|
89
87
|
return `(${pause}.provider IN (SELECT provider FROM (${providers}) WHERE provider IS NOT NULL)
|
|
90
88
|
OR NOT EXISTS (SELECT 1 FROM (${providers}) WHERE provider IS NOT NULL))`;
|
|
91
89
|
}
|
|
@@ -227,8 +225,8 @@ export function giveBackAuthTrial(store, trial) {
|
|
|
227
225
|
}
|
|
228
226
|
/**
|
|
229
227
|
* The sign-in gate every road that starts an agent asks before it claims
|
|
230
|
-
* anything: the tick's queue
|
|
231
|
-
*
|
|
228
|
+
* anything: the tick's queue and the roads beside it.
|
|
229
|
+
* Waits when a provider is paused,
|
|
232
230
|
* unless this is the pause's one trial. A waiting task is noted on its
|
|
233
231
|
* task_ref, so the work index says exactly what the gate decided; a task let
|
|
234
232
|
* through is un-noted. A road whose claim fails after a trial calls giveBack.
|
package/dist/provider.d.ts
CHANGED
|
@@ -45,7 +45,7 @@ export type Invocation = {
|
|
|
45
45
|
skipPermissions: boolean;
|
|
46
46
|
/** Resume this session (repair). Meaningless across providers. */
|
|
47
47
|
resumeSession: string | null;
|
|
48
|
-
/** Claude's native dollar cap
|
|
48
|
+
/** Claude's native dollar cap — the harness stops
|
|
49
49
|
* itself when spend reaches this. Ignored by providers without one. */
|
|
50
50
|
maxBudgetUsd?: number;
|
|
51
51
|
/** A plane-minted session identity, for providers that can START under a
|
|
@@ -134,9 +134,10 @@ export type ParsedEnvelope = {
|
|
|
134
134
|
diagnostic: string | null;
|
|
135
135
|
/**
|
|
136
136
|
* The structural FAILURE terminal, when the harness emitted a typed one
|
|
137
|
-
* (codex turn.failed, a gemini error terminal) — retained
|
|
138
|
-
*
|
|
139
|
-
* run (
|
|
137
|
+
* (codex turn.failed, a gemini error terminal) — retained to classify
|
|
138
|
+
* how the attempt ended (a sign-in that no longer works) AND to block
|
|
139
|
+
* success ingestion on a failed-but-exit-0 run (C5). null = no structural
|
|
140
|
+
* failure terminal seen.
|
|
140
141
|
*/
|
|
141
142
|
structuralTerminal: import("./exhaustion.js").StructuralTerminal | null;
|
|
142
143
|
};
|
|
@@ -158,15 +159,6 @@ type Adapter = {
|
|
|
158
159
|
clampTimeout(phase: Phase, requestedMs: number): number;
|
|
159
160
|
};
|
|
160
161
|
export declare const OPENROUTER_ENV_KEY = "OPENROUTER_API_KEY";
|
|
161
|
-
/**
|
|
162
|
-
* The held-session argv (Parity II Phase 2): the same family as
|
|
163
|
-
* claudeArgv MINUS the positional prompt — every turn, the brief
|
|
164
|
-
* included, rides stdin as stream-json — plus the input format flag.
|
|
165
|
-
* maxBudgetUsd is the Probe-6 backstop: the CLI's cap is cumulative
|
|
166
|
-
* across the whole process, so the remaining authorization budget rides
|
|
167
|
-
* the argv and later turns cannot spend past it.
|
|
168
|
-
*/
|
|
169
|
-
export declare const claudeHeldArgv: (invocation: Omit<Invocation, "brief">) => string[];
|
|
170
162
|
/** Claude's own tools that only make sense with a person to wake them. */
|
|
171
163
|
export declare const HEADLESS_DISALLOWED_TOOLS: readonly string[];
|
|
172
164
|
/**
|
|
@@ -228,12 +220,10 @@ export type ProviderAudit = {
|
|
|
228
220
|
};
|
|
229
221
|
/** Read-only facts about a provider — safe anywhere, spawns nothing. */
|
|
230
222
|
/**
|
|
231
|
-
* The
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
* end (cumulative across resumed sessions), so no mid-run cap exists to
|
|
236
|
-
* hold — ineligible until their harnesses grow one.
|
|
223
|
+
* The money capability matrix (design v3 finding 14): what each harness
|
|
224
|
+
* can PROVE about money, stated as data. Codex and OpenRouter report
|
|
225
|
+
* billable usage only at turn end (cumulative across resumed sessions),
|
|
226
|
+
* so no mid-run cap exists to hold.
|
|
237
227
|
*/
|
|
238
228
|
export type ProviderMoneyCapabilities = {
|
|
239
229
|
/** Usage events arrive during the run, not only at the end. */
|
|
@@ -241,17 +231,13 @@ export type ProviderMoneyCapabilities = {
|
|
|
241
231
|
/** The harness's own dollar-cap flag, when one exists. */
|
|
242
232
|
nativeDollarCapFlag: string | null;
|
|
243
233
|
usageSemantics: "per-invocation" | "cumulative-session";
|
|
244
|
-
/** Whether this harness may race in a dollar-capped tournament. */
|
|
245
|
-
tournamentEligible: boolean;
|
|
246
|
-
/** Said in words on refusal screens. */
|
|
247
|
-
whyIneligible: string | null;
|
|
248
234
|
};
|
|
249
235
|
export declare const MONEY_CAPABILITIES: Record<ProviderId, ProviderMoneyCapabilities>;
|
|
250
236
|
/**
|
|
251
237
|
* The fail-closed budget-flag probe (finding 24's amendment): resolve
|
|
252
238
|
* the EXACT executable that will spawn, read its version, and prove the
|
|
253
239
|
* flag exists in that binary's own help — presence is a feature check
|
|
254
|
-
* and nothing more
|
|
240
|
+
* and nothing more.
|
|
255
241
|
*/
|
|
256
242
|
export declare function probeBudgetCap(provider: ProviderId, runner: (command: string, argv: readonly string[], options: {
|
|
257
243
|
timeoutMs?: number;
|
|
@@ -277,7 +263,7 @@ export declare function validateSpec(spec: AgentSpec): {
|
|
|
277
263
|
problem: string;
|
|
278
264
|
};
|
|
279
265
|
/**
|
|
280
|
-
* Whether this provider reports dollar cost.
|
|
266
|
+
* Whether this provider reports dollar cost. A schedule's weekly limit interacts:
|
|
281
267
|
* a ceiling against an unmeasured provider fails closed by design, so the
|
|
282
268
|
* approval surfaces refuse the combination outright.
|
|
283
269
|
*/
|
package/dist/provider.js
CHANGED
|
@@ -56,30 +56,6 @@ function toml(value) {
|
|
|
56
56
|
const OPENROUTER_PROVIDER_KEY = "standing-orders_openrouter";
|
|
57
57
|
export const OPENROUTER_ENV_KEY = "OPENROUTER_API_KEY";
|
|
58
58
|
const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
|
|
59
|
-
/**
|
|
60
|
-
* The held-session argv (Parity II Phase 2): the same family as
|
|
61
|
-
* claudeArgv MINUS the positional prompt — every turn, the brief
|
|
62
|
-
* included, rides stdin as stream-json — plus the input format flag.
|
|
63
|
-
* maxBudgetUsd is the Probe-6 backstop: the CLI's cap is cumulative
|
|
64
|
-
* across the whole process, so the remaining authorization budget rides
|
|
65
|
-
* the argv and later turns cannot spend past it.
|
|
66
|
-
*/
|
|
67
|
-
export const claudeHeldArgv = (invocation) => [
|
|
68
|
-
"-p",
|
|
69
|
-
...(invocation.resumeSession === null ? [] : ["--resume", invocation.resumeSession]),
|
|
70
|
-
"--input-format",
|
|
71
|
-
"stream-json",
|
|
72
|
-
"--output-format",
|
|
73
|
-
"stream-json",
|
|
74
|
-
"--verbose",
|
|
75
|
-
"--max-turns",
|
|
76
|
-
String(invocation.maxTurns),
|
|
77
|
-
...(invocation.skipPermissions
|
|
78
|
-
? ["--dangerously-skip-permissions"]
|
|
79
|
-
: ["--permission-mode", invocation.permissionMode]),
|
|
80
|
-
...(invocation.model === null ? [] : ["--model", invocation.model]),
|
|
81
|
-
...(invocation.maxBudgetUsd === undefined ? [] : ["--max-budget-usd", String(invocation.maxBudgetUsd)]),
|
|
82
|
-
];
|
|
83
59
|
/**
|
|
84
60
|
* Reviewer isolation (fail-closed for the phase that must never mutate):
|
|
85
61
|
* the SAME flag family `subscription-chat.ts` already uses for its own
|
|
@@ -499,8 +475,8 @@ function codexParse(stdout) {
|
|
|
499
475
|
}
|
|
500
476
|
}
|
|
501
477
|
else if (type === "turn.failed") {
|
|
502
|
-
// RETAIN the structural failure terminal (Codex
|
|
503
|
-
//
|
|
478
|
+
// RETAIN the structural failure terminal (Codex verify, finding 1):
|
|
479
|
+
// a turn.failed carries the usage-limit or sign-in signal, and
|
|
504
480
|
// dropping it here is where the evidence used to die. The error
|
|
505
481
|
// object's message + typed code ride into the taxonomy; its mere
|
|
506
482
|
// presence blocks success ingestion downstream.
|
|
@@ -859,22 +835,16 @@ export const MONEY_CAPABILITIES = {
|
|
|
859
835
|
incrementalUsage: true,
|
|
860
836
|
nativeDollarCapFlag: "--max-budget-usd",
|
|
861
837
|
usageSemantics: "per-invocation",
|
|
862
|
-
tournamentEligible: true,
|
|
863
|
-
whyIneligible: null,
|
|
864
838
|
},
|
|
865
839
|
codex: {
|
|
866
840
|
incrementalUsage: false,
|
|
867
841
|
nativeDollarCapFlag: null,
|
|
868
842
|
usageSemantics: "cumulative-session",
|
|
869
|
-
tournamentEligible: false,
|
|
870
|
-
whyIneligible: "codex reports billable usage only when a turn completes — no mid-run dollar cap exists to enforce",
|
|
871
843
|
},
|
|
872
844
|
openrouter: {
|
|
873
845
|
incrementalUsage: false,
|
|
874
846
|
nativeDollarCapFlag: null,
|
|
875
847
|
usageSemantics: "cumulative-session",
|
|
876
|
-
tournamentEligible: false,
|
|
877
|
-
whyIneligible: "openrouter rides the codex harness here and shares its turn-end-only usage reporting",
|
|
878
848
|
},
|
|
879
849
|
gemini: {
|
|
880
850
|
incrementalUsage: false,
|
|
@@ -882,15 +852,13 @@ export const MONEY_CAPABILITIES = {
|
|
|
882
852
|
// Stats come from a per-process telemetry service: an invocation's
|
|
883
853
|
// numbers cover that invocation only (conformance fixture j).
|
|
884
854
|
usageSemantics: "per-invocation",
|
|
885
|
-
tournamentEligible: false,
|
|
886
|
-
whyIneligible: "gemini reports tokens, never dollars — no native cap exists to hold",
|
|
887
855
|
},
|
|
888
856
|
};
|
|
889
857
|
/**
|
|
890
858
|
* The fail-closed budget-flag probe (finding 24's amendment): resolve
|
|
891
859
|
* the EXACT executable that will spawn, read its version, and prove the
|
|
892
860
|
* flag exists in that binary's own help — presence is a feature check
|
|
893
|
-
* and nothing more
|
|
861
|
+
* and nothing more.
|
|
894
862
|
*/
|
|
895
863
|
export async function probeBudgetCap(provider, runner) {
|
|
896
864
|
const flag = MONEY_CAPABILITIES[provider].nativeDollarCapFlag;
|
|
@@ -938,7 +906,7 @@ export function validateSpec(spec) {
|
|
|
938
906
|
return { ok: true };
|
|
939
907
|
}
|
|
940
908
|
/**
|
|
941
|
-
* Whether this provider reports dollar cost.
|
|
909
|
+
* Whether this provider reports dollar cost. A schedule's weekly limit interacts:
|
|
942
910
|
* a ceiling against an unmeasured provider fails closed by design, so the
|
|
943
911
|
* approval surfaces refuse the combination outright.
|
|
944
912
|
*/
|
package/dist/push.js
CHANGED
|
@@ -197,7 +197,7 @@ export const PUSH_WORDS = {
|
|
|
197
197
|
attention: { title: "Toolroll", body: "the plane needs attention" },
|
|
198
198
|
progress: { title: "Toolroll", body: "checks in progress" },
|
|
199
199
|
};
|
|
200
|
-
const LINK_SHAPES = [/^\/next$/, /^\/review$/, /^\/system$/, /^\/routines$/, /^\/routines\/[0-9]{1,15}$/, /^\/d\/[0-9]{1,15}$/, /^\/
|
|
200
|
+
const LINK_SHAPES = [/^\/next$/, /^\/review$/, /^\/system$/, /^\/routines$/, /^\/routines\/[0-9]{1,15}$/, /^\/d\/[0-9]{1,15}$/, /^\/r\/[0-9]{1,15}$/];
|
|
201
201
|
const CLASS_FALLBACK = {
|
|
202
202
|
decision: "/next",
|
|
203
203
|
pick: "/next",
|
package/dist/recipe-ui.js
CHANGED
|
@@ -56,7 +56,7 @@ export function recipeFromForm(body) {
|
|
|
56
56
|
export function recipeLibraryHtml(starters, saved, project, csrf, revision, recent) {
|
|
57
57
|
const cards = (recipes, reusable = false) => `<div class="recipe-grid"${reusable ? ' data-saved-recipes' : ''}>${recipes.map(recipe => `<article class="recipe-card" data-recipe-card data-recipe-search="${e(recipe.document.name + " " + recipe.document.description)}"><p class="recipe-kicker">${recipe.document.deliverable === "report" ? "Report" : recipe.document.schedule === null ? "One-time work" : "Recurring care"}${recipe.document.version === 2 ? ` · ${recipe.document.inputs.length} question${recipe.document.inputs.length === 1 ? "" : "s"}` : ""}</p><h3>${e(recipe.document.name)}</h3><p>${e(recipe.document.description)}</p><a class="recipe-card-link" href="/recipes/${reusable ? "run" : "start"}?recipe=${e(recipe.id)}&revision=${recipe.revision}">${reusable ? "Use recipe" : "Customize recipe"} <span aria-hidden="true">→</span></a>${reusable ? `<a class="meta" href="/recipes/start?recipe=${e(recipe.id)}&revision=${recipe.revision}">Edit a copy</a>` : ""}</article>`).join("")}</div>`;
|
|
58
58
|
const savedSection = `<h2>Your project’s recipes</h2>${saved.length >= 100 ? `<p class="meta">Showing 100 recipes, with recently used recipes first.</p>` : ""}${saved.length ? `<p class="hint">Your usual work, ready to go. Recently used recipes come first.</p><label class="recipe-search">Find a recipe<input type="search" data-recipe-search-input placeholder="Search your recipes…"></label>${cards(saved, true)}<p data-recipe-empty hidden>No recipes match. Try another search.</p>` : `<p class="hint">Save work you do often. Teammates with project access can use the same recipe.</p>`}`;
|
|
59
|
-
return `<div class="recipe-heading"><p class="recipe-kicker">Workflows</p><h1>Good work, on repeat.</h1><p class="hint">Save the work you do often. Answer what changes this time. Kick it off.</p><p><a href="/recipes/new" class="new-task">Create a recipe</a> <a href="/
|
|
59
|
+
return `<div class="recipe-heading"><p class="recipe-kicker">Workflows</p><h1>Good work, on repeat.</h1><p class="hint">Save the work you do often. Answer what changes this time. Kick it off.</p><p><a href="/recipes/new" class="new-task">Create a recipe</a> <a href="/flows">Manage scheduled work →</a></p></div>` +
|
|
60
60
|
(project === null ? `<div class="card"><strong>Choose a project to get started.</strong><p>Browse recipes now; <a href="/projects">open a project</a> to customize, save, or run one.</p></div>` : "") +
|
|
61
61
|
(saved.length ? savedSection : "") + `<h2>Start here</h2>${cards(starters)}` + (saved.length ? "" : savedSection) +
|
|
62
62
|
(recent.length ? `<h2>Recent workflows</h2><div class="card recipe-history">${recent.map(one => `<a href="${e(one.href)}"><strong>${e(one.name)}</strong><span>${e(one.state)} →</span></a>`).join("")}</div>` : "") +
|
|
@@ -80,17 +80,17 @@ export function recipeEditorHtml(recipe, repo, csrf, revision, problem = null, v
|
|
|
80
80
|
`<section class="card"><h2>The outcome</h2><label>${creator ? "Recipe name" : "Workflow name"}<input type="text" name="name" value="${e(value("name", d.name))}" maxlength="200" required></label><label>Short description<input type="text" name="description" value="${e(value("description", d.description))}" maxlength="400"></label><label>What should the agent do?<textarea name="goal" rows="5" maxlength="${TASK_TEXT_LIMITS.text}" required>${e(value("goal", d.goal))}</textarea></label><label>What should it leave alone?<textarea name="not" rows="3" maxlength="${TASK_TEXT_LIMITS.text}">${e(value("not", d.outOfScope ?? ""))}</textarea></label><label>Allowed files or folders <span class="meta">optional; one per line</span><textarea name="touches" rows="2" placeholder="src/ tests/">${e(value("touches", d.touches.join("\n")))}</textarea></label><p class="meta">Blank allows work across this project. Add paths to keep the workflow focused.</p></section>` +
|
|
81
81
|
recipeQuestionsHtml(d, values) +
|
|
82
82
|
`<section class="card"><h2>What does success look like?</h2><p class="hint">Give each outcome the evidence you want to see. The workflow checks these before calling the work done.</p><div data-recipe-criteria>${Array.from({ length: count }, (_, i) => criterionHtml(i, d.acceptance[i], values)).join("")}</div><button type="button" data-add-criterion hidden>Add another success check</button><template data-criterion-template>${criterionHtml(11).replaceAll("criterion-11-", "criterion-INDEX-").replaceAll('value="c12"', 'value="cNEXT"')}</template></section>` +
|
|
83
|
-
`<section class="card"><h2>How it should run</h2><div class="recipe-options"><label>Result<select name="deliverable"><option value="branch"${deliverable === "branch" ? " selected" : ""}>Code change with evidence</option><option value="report"${deliverable === "report" ? " selected" : ""}>Report without code changes</option></select></label><label>Planning<select name="planning">${[["auto", "Plan when helpful"], ["required", "Always plan first"], ["skip", "Use this scope directly"]].map(([id, label]) => `<option value="${id}"${planning === id ? " selected" : ""}>${label}</option>`).join("")}</select></label></div><p class="meta">Report work investigates directly. Repeating work
|
|
83
|
+
`<section class="card"><h2>How it should run</h2><div class="recipe-options"><label>Result<select name="deliverable"><option value="branch"${deliverable === "branch" ? " selected" : ""}>Code change with evidence</option><option value="report"${deliverable === "report" ? " selected" : ""}>Report without code changes</option></select></label><label>Planning<select name="planning">${[["auto", "Plan when helpful"], ["required", "Always plan first"], ["skip", "Use this scope directly"]].map(([id, label]) => `<option value="${id}"${planning === id ? " selected" : ""}>${label}</option>`).join("")}</select></label></div><p class="meta">Report work investigates directly. Repeating work files the same scope each time; plan-first workflows run once.</p><label>Frequency<select name="cadence" data-recipe-cadence><option value="once"${cadence === "once" ? " selected" : ""}>Run once</option><option value="repeat"${cadence === "repeat" ? " selected" : ""}>Repeat on a schedule</option></select></label><div data-recipe-recurring>${scheduleEditorHtml(d.schedule, values)}<label>Weekly spending limit <span class="meta">optional, USD</span><input type="number" name="ceiling" min="0.01" step="0.01" value="${e(value("ceiling", d.costCeilingUsd === null ? "" : String(d.costCeilingUsd)))}"></label><p class="meta">One at a time. A spending limit needs an agent that reports dollar costs.</p></div></section>` +
|
|
84
84
|
`<div class="sticky-actions"><button type="submit" name="purpose" value="${creator ? "recipe" : "workflow"}">${creator ? "Preview recipe →" : "Preview workflow →"}</button>${creator ? "" : `<button type="submit" name="purpose" value="recipe" class="quiet">Preview recipe to save</button>`}<p class="meta">${creator ? "Save the instructions once. You can use the recipe whenever this work comes up." : "Previewing runs no agents. You’ll see the approval and scheduling steps next."}</p></div></form>`;
|
|
85
85
|
}
|
|
86
86
|
export function workflowPreviewHtml(preview, csrf, revision, now, readiness, auto) {
|
|
87
87
|
const d = preview.document;
|
|
88
88
|
const common = hidden("csrf", csrf) + hidden("repo", preview.repo) + hidden("projectRevision", String(revision)) + hidden("preview", preview.token);
|
|
89
|
-
const target = preview.taskId === null ? preview.
|
|
89
|
+
const target = preview.taskId === null ? preview.flowId === null ? null : `/flows/${preview.flowId}` : `/t/${encodeURIComponent(preview.taskId)}`;
|
|
90
90
|
return `<a href="/recipes/edit?preview=${preview.token}" class="meta">← Edit this workflow</a><div class="recipe-heading"><p class="recipe-kicker">3 · Preview</p><h1>${e(d.name)}</h1><p class="hint">${e(d.description)}</p></div>` +
|
|
91
91
|
`<div class="recipe-preview-grid"><div><section class="card"><h2>What will happen</h2><ol class="recipe-steps">${workflowSteps(d).map(step => `<li><strong>${e(step.title)}</strong><p>${e(step.detail)}</p></li>`).join("")}</ol></section><section class="card"><h2>The agreed work</h2><p class="recap">${e(d.goal)}</p><h3>Leave alone</h3><p class="recap">${e(d.outOfScope ?? "No additional exclusions.")}</p><h3>Allowed paths</h3><p>${d.touches.length ? d.touches.map(e).join(", ") : "This project"}</p><h3>Success checks</h3><ul>${d.acceptance.map(c => `<li><strong>${e(c.statement)}</strong><p class="meta">${c.evidence.map(kind => e(evidence.find(([id]) => id === kind)?.[1] ?? kind)).join(" · ")}${c.how ? ` · ${e(c.how)}` : ""}</p></li>`).join("")}</ul></section></div>` +
|
|
92
|
-
`<aside><section class="card"><h2>Before it starts</h2>${readiness.map(one => `<p><strong>${e(one.title)}</strong><br><span class="meta">${e(one.detail)}${one.href ? ` <a href="${e(one.href)}">Open settings →</a>` : ""}</span></p>`).join("")}<p class="meta">These checks describe current setup. Agent availability and approval are checked again when work starts.</p></section><section class="card"><h2>Schedule & control</h2><p>${e(recipeScheduleWords(d, now))}</p><p>${d.costCeilingUsd === null ? "Uses existing project and agent budget controls." : `Up to $${d.costCeilingUsd.toFixed(2)} per rolling seven days.`}</p><p>${d.schedule !== null ? "
|
|
93
|
-
`<div class="card recipe-launch"><div>${target ? `<a class="new-task" href="${target}">Open created workflow →</a>` : `<form method="post" action="/recipes/launch">${common}<button type="submit">${d.schedule !== null ? "Create scheduled workflow" : auto ? "Create workflow" : "Create task for approval"}</button></form>`}<p class="meta">${d.schedule !== null ? "Nothing repeats until the
|
|
92
|
+
`<aside><section class="card"><h2>Before it starts</h2>${readiness.map(one => `<p><strong>${e(one.title)}</strong><br><span class="meta">${e(one.detail)}${one.href ? ` <a href="${e(one.href)}">Open settings →</a>` : ""}</span></p>`).join("")}<p class="meta">These checks describe current setup. Agent availability and approval are checked again when work starts.</p></section><section class="card"><h2>Schedule & control</h2><p>${e(recipeScheduleWords(d, now))}</p><p>${d.costCeilingUsd === null ? "Uses existing project and agent budget controls." : `Up to $${d.costCeilingUsd.toFixed(2)} per rolling seven days.`}</p><p>${d.schedule !== null ? "It becomes a flow you can pause or resume any time." : auto ? "Your signed project policy will be evaluated when you create this workflow." : "Review and approve on the task page. You can stop or resume there."}</p></section></aside></div>` +
|
|
93
|
+
`<div class="card recipe-launch"><div>${target ? `<a class="new-task" href="${target}">Open created workflow →</a>` : `<form method="post" action="/recipes/launch">${common}<button type="submit">${d.schedule !== null ? "Create scheduled workflow" : auto ? "Create workflow" : "Create task for approval"}</button></form>`}<p class="meta">${d.schedule !== null ? "Nothing repeats until you turn the schedule on." : "A matching signed policy may start work automatically."} Creating this workflow twice returns the same work.</p></div><div>${preview.savedId ? `<a href="/recipes/start?recipe=${preview.savedId}&revision=${preview.savedRevision}">Recipe saved · reuse it →</a>` : `<form method="post" action="/recipes/save">${common}<button type="submit" class="quiet">Save as a project recipe</button></form>`}<a href="/recipes/export?preview=${preview.token}">Export recipe JSON</a><p class="meta">Save or export without running anything. Sharing includes the work definition, never project access or approvals.</p></div></div>`;
|
|
94
94
|
}
|
|
95
95
|
function inputHtml(index, input, values) {
|
|
96
96
|
const value = (field, fallback) => values?.get(`input-${index}-${field}`) ?? fallback;
|
|
@@ -109,7 +109,7 @@ export function recipeDefinitionPreviewHtml(preview, csrf, revision) {
|
|
|
109
109
|
}
|
|
110
110
|
export function recipeRunHtml(recipe, repo, csrf, revision, problem = null, values, saved = false) {
|
|
111
111
|
const d = recipe.document, inputs = d.version === 2 ? d.inputs : [];
|
|
112
|
-
return `<a class="meta" href="/recipes">← Your recipes</a><div class="recipe-heading"><p class="recipe-kicker">${saved ? "Recipe saved · ready when you are" : "Use a recipe"}</p><h1>${e(d.name)}</h1><p class="hint">${e(d.description || "Your saved instructions, ready to use again.")}</p></div>${problem ? `<p role="alert" class="card recipe-error">${e(problem)}</p>` : ""}<form class="recipe-run card" method="post" action="/recipes/prepare">${hidden("csrf", csrf)}${hidden("repo", repo)}${hidden("projectRevision", String(revision))}${hidden("recipe", recipe.id)}${hidden("recipeRevision", String(recipe.revision))}<h2>${inputs.length ? "What changes this time?" : "Ready to go"}</h2>${inputs.map(input => `<label>${e(input.label)}<textarea name="answer-${e(input.key)}" rows="2" maxlength="500" required>${e(values?.get(`answer-${input.key}`) ?? input.defaultValue ?? "")}</textarea></label>`).join("")}${inputs.length ? "" : `<p>No setup to repeat. Preview the saved scope and create the work.</p>`}<p class="meta">${d.schedule === null ? "Creates one task each time you use this recipe." : "Creates a new
|
|
112
|
+
return `<a class="meta" href="/recipes">← Your recipes</a><div class="recipe-heading"><p class="recipe-kicker">${saved ? "Recipe saved · ready when you are" : "Use a recipe"}</p><h1>${e(d.name)}</h1><p class="hint">${e(d.description || "Your saved instructions, ready to use again.")}</p></div>${problem ? `<p role="alert" class="card recipe-error">${e(problem)}</p>` : ""}<form class="recipe-run card" method="post" action="/recipes/prepare">${hidden("csrf", csrf)}${hidden("repo", repo)}${hidden("projectRevision", String(revision))}${hidden("recipe", recipe.id)}${hidden("recipeRevision", String(recipe.revision))}<h2>${inputs.length ? "What changes this time?" : "Ready to go"}</h2>${inputs.map(input => `<label>${e(input.label)}<textarea name="answer-${e(input.key)}" rows="2" maxlength="500" required>${e(values?.get(`answer-${input.key}`) ?? input.defaultValue ?? "")}</textarea></label>`).join("")}${inputs.length ? "" : `<p>No setup to repeat. Preview the saved scope and create the work.</p>`}<p class="meta">${d.schedule === null ? "Creates one task each time you use this recipe." : "Creates a new scheduled flow with these answers fixed for every run. Manage existing schedules from Flows."}</p><button type="submit">Preview this run →</button><p class="meta">Previewing starts no agents. Existing project approvals apply when you create work.</p></form><details class="card"><summary>Saved instructions and boundaries</summary><p class="recap">${e(d.goal)}</p><p class="recap">Leave alone: ${e(d.outOfScope ?? "No additional exclusions.")}</p><p>Allowed paths: ${d.touches.map(e).join(", ") || "This project"}</p><ul>${d.acceptance.map(one => `<li>${e(one.statement)}</li>`).join("")}</ul><a href="/recipes/start?recipe=${e(recipe.id)}&revision=${recipe.revision}">Edit a copy of this recipe →</a></details>`;
|
|
113
113
|
}
|
|
114
114
|
export function recipeAnswersFromForm(body) {
|
|
115
115
|
const answers = new Map();
|
package/dist/recipes.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Store } from "./store.js";
|
|
2
2
|
import { type RecipeDocument } from "./contracts/recipes.js";
|
|
3
|
-
export declare const RECIPE_SCHEMA = "\nCREATE TABLE IF NOT EXISTS workflow_recipe (\n id TEXT NOT NULL, revision INTEGER NOT NULL CHECK(revision > 0),\n repo TEXT NOT NULL, document TEXT NOT NULL, digest TEXT NOT NULL,\n author TEXT NOT NULL, created_at TEXT NOT NULL,\n PRIMARY KEY(id, revision)\n);\nCREATE INDEX IF NOT EXISTS workflow_recipe_project ON workflow_recipe(repo, id, revision DESC);\nCREATE TRIGGER IF NOT EXISTS workflow_recipe_no_update BEFORE UPDATE ON workflow_recipe\nBEGIN SELECT RAISE(ABORT, 'recipe revisions are immutable'); END;\nCREATE TRIGGER IF NOT EXISTS workflow_recipe_no_delete BEFORE DELETE ON workflow_recipe\nBEGIN SELECT RAISE(ABORT, 'recipe revisions are immutable'); END;\nCREATE TABLE IF NOT EXISTS workflow_preview (\n token TEXT PRIMARY KEY, actor TEXT NOT NULL, repo TEXT NOT NULL,\n document TEXT NOT NULL, digest TEXT NOT NULL, source TEXT NOT NULL,\n created_at TEXT NOT NULL, expires_at TEXT NOT NULL,\n task_id TEXT REFERENCES task(id), routine_id INTEGER
|
|
3
|
+
export declare const RECIPE_SCHEMA = "\nCREATE TABLE IF NOT EXISTS workflow_recipe (\n id TEXT NOT NULL, revision INTEGER NOT NULL CHECK(revision > 0),\n repo TEXT NOT NULL, document TEXT NOT NULL, digest TEXT NOT NULL,\n author TEXT NOT NULL, created_at TEXT NOT NULL,\n PRIMARY KEY(id, revision)\n);\nCREATE INDEX IF NOT EXISTS workflow_recipe_project ON workflow_recipe(repo, id, revision DESC);\nCREATE TRIGGER IF NOT EXISTS workflow_recipe_no_update BEFORE UPDATE ON workflow_recipe\nBEGIN SELECT RAISE(ABORT, 'recipe revisions are immutable'); END;\nCREATE TRIGGER IF NOT EXISTS workflow_recipe_no_delete BEFORE DELETE ON workflow_recipe\nBEGIN SELECT RAISE(ABORT, 'recipe revisions are immutable'); END;\nCREATE TABLE IF NOT EXISTS workflow_preview (\n token TEXT PRIMARY KEY, actor TEXT NOT NULL, repo TEXT NOT NULL,\n document TEXT NOT NULL, digest TEXT NOT NULL, source TEXT NOT NULL,\n created_at TEXT NOT NULL, expires_at TEXT NOT NULL,\n -- routine_id: history (routines became scheduled flows in v115; the flow_id column is added after this runs).\n task_id TEXT REFERENCES task(id), routine_id INTEGER,\n saved_id TEXT, saved_revision INTEGER,\n CHECK(task_id IS NULL OR routine_id IS NULL),\n FOREIGN KEY(saved_id, saved_revision) REFERENCES workflow_recipe(id, revision)\n);\nCREATE INDEX IF NOT EXISTS workflow_preview_project ON workflow_preview(repo, created_at DESC);\nCREATE INDEX IF NOT EXISTS workflow_preview_source ON workflow_preview(repo, source, created_at DESC)\nWHERE task_id IS NOT NULL OR routine_id IS NOT NULL;\n";
|
|
4
4
|
export type { RecipeDocument, RecipeInput } from "./contracts/recipes.js";
|
|
5
5
|
export declare const RECIPE_INPUT_LIMIT: 8;
|
|
6
6
|
export type RecipeAnswers = ReadonlyMap<string, string>;
|
|
@@ -21,7 +21,7 @@ export type WorkflowPreview = {
|
|
|
21
21
|
source: string;
|
|
22
22
|
expiresAt: string;
|
|
23
23
|
taskId: string | null;
|
|
24
|
-
|
|
24
|
+
flowId: number | null;
|
|
25
25
|
savedId: string | null;
|
|
26
26
|
savedRevision: number | null;
|
|
27
27
|
};
|
|
@@ -46,7 +46,7 @@ export declare function createWorkflowPreview(store: Store, actor: string, repo:
|
|
|
46
46
|
export declare function workflowPreview(store: Store, actor: string, repo: string, token: string): WorkflowPreview | null;
|
|
47
47
|
export declare function launchWorkflow(store: Store, actor: string, repo: string, token: string, now: Date, operatorFiling: boolean): {
|
|
48
48
|
taskId: string | null;
|
|
49
|
-
|
|
49
|
+
flowId: number | null;
|
|
50
50
|
};
|
|
51
51
|
export declare function saveWorkflowRecipe(store: Store, actor: string, repo: string, token: string, now: Date): Recipe;
|
|
52
52
|
export declare function workflowSteps(document: RecipeDocument): {
|
package/dist/recipes.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/** Reusable work definitions. Recipes carry scope, never credentials or grants.
|
|
2
2
|
* A preview freezes the exact copy to file; its durable receipt makes a retry
|
|
3
|
-
* after a lost response return the same task
|
|
3
|
+
* after a lost response return the same task or flow, even after a restart.
|
|
4
|
+
* A repeating recipe (v115) makes a scheduled flow whose schedule starts paused. */
|
|
4
5
|
import { createHash, randomBytes } from "node:crypto";
|
|
5
6
|
import { TEMPLATES } from "./templates.js";
|
|
6
7
|
import { hasDisguisedText, hasForbiddenControls } from "./decision.js";
|
|
7
8
|
import { parseAcceptanceCriteria } from "./scope.js";
|
|
8
|
-
import { validateTaskText, fileTaskProposal
|
|
9
|
-
import {
|
|
9
|
+
import { validateTaskText, fileTaskProposal } from "./proposal.js";
|
|
10
|
+
import { createScheduledFlow, describeSchedule, firstFireAt, parseSchedule, standingTermsProblems } from "./flow-schedule.js";
|
|
10
11
|
import { applyModeToNewFiling } from "./plan-auto.js";
|
|
11
12
|
import { canonicalProject } from "./project.js";
|
|
12
13
|
import { RECIPE_FORMAT, RECIPE_LIMITS, readRecipeDocument } from "./contracts/recipes.js";
|
|
@@ -27,7 +28,8 @@ CREATE TABLE IF NOT EXISTS workflow_preview (
|
|
|
27
28
|
token TEXT PRIMARY KEY, actor TEXT NOT NULL, repo TEXT NOT NULL,
|
|
28
29
|
document TEXT NOT NULL, digest TEXT NOT NULL, source TEXT NOT NULL,
|
|
29
30
|
created_at TEXT NOT NULL, expires_at TEXT NOT NULL,
|
|
30
|
-
|
|
31
|
+
-- routine_id: history (routines became scheduled flows in v115; the flow_id column is added after this runs).
|
|
32
|
+
task_id TEXT REFERENCES task(id), routine_id INTEGER,
|
|
31
33
|
saved_id TEXT, saved_revision INTEGER,
|
|
32
34
|
CHECK(task_id IS NULL OR routine_id IS NULL),
|
|
33
35
|
FOREIGN KEY(saved_id, saved_revision) REFERENCES workflow_recipe(id, revision)
|
|
@@ -123,9 +125,9 @@ export function parseRecipe(input) {
|
|
|
123
125
|
if (ordered.version === 2)
|
|
124
126
|
validateInputReferences(ordered);
|
|
125
127
|
if (ordered.schedule !== null) {
|
|
126
|
-
const problems =
|
|
128
|
+
const problems = standingTermsProblems({ ...ordered, requirements: [], budgetPerRunMicrousd: null }, ordered.schedule);
|
|
127
129
|
if (problems.length)
|
|
128
|
-
throw new RecipeError(problems.
|
|
130
|
+
throw new RecipeError(problems.join("; "));
|
|
129
131
|
}
|
|
130
132
|
return ordered;
|
|
131
133
|
}
|
|
@@ -214,11 +216,11 @@ export function prepareRecipeRun(store, actor, repo, id, revision, answers, now)
|
|
|
214
216
|
return createWorkflowPreview(store, actor, repo, resolveRecipe(recipe.document, answers), recipe.id, now);
|
|
215
217
|
});
|
|
216
218
|
}
|
|
217
|
-
const titles = { "
|
|
219
|
+
const titles = { "lint-sweep": "Clean up lint and types" };
|
|
218
220
|
export function starterRecipes() {
|
|
219
221
|
const recipes = TEMPLATES.filter(one => one.kind !== "recipe").map(one => {
|
|
220
|
-
const document = parseRecipe({ format: "standing-orders-recipe", version: 1, name: titles[one.name] ?? one.name, description: one.purpose, goal: one.goal, outOfScope: one.outOfScope, touches: one.touches, acceptance: one.acceptance,
|
|
221
|
-
planning: "skip", deliverable: "branch", schedule: one.kind === "
|
|
222
|
+
const document = parseRecipe({ format: "standing-orders-recipe", version: 1, name: one.kind === "scheduled" ? one.title : titles[one.name] ?? one.name, description: one.purpose, goal: one.goal, outOfScope: one.outOfScope, touches: one.touches, acceptance: one.acceptance,
|
|
223
|
+
planning: "skip", deliverable: "branch", schedule: one.kind === "scheduled" ? one.schedule : null, costCeilingUsd: null });
|
|
222
224
|
return { id: one.name, revision: 1, repo: null, document, digest: recipeDigest(document), author: "Toolroll" };
|
|
223
225
|
});
|
|
224
226
|
for (const [id, document] of [
|
|
@@ -243,7 +245,7 @@ function readRecipe(row) {
|
|
|
243
245
|
export function savedRecipes(store, actor, repo) {
|
|
244
246
|
repo = recipeProject(repo);
|
|
245
247
|
access(store, actor, repo);
|
|
246
|
-
return store.handle.prepare(`SELECT r.* FROM workflow_recipe r WHERE repo=? AND revision=(SELECT MAX(revision) FROM workflow_recipe WHERE id=r.id) ORDER BY (SELECT MAX(p.created_at) FROM workflow_preview p WHERE p.repo=r.repo AND p.source=r.id AND (p.task_id IS NOT NULL OR p.routine_id IS NOT NULL)) DESC, created_at DESC, id LIMIT 100`).all(repo).map(readRecipe);
|
|
248
|
+
return store.handle.prepare(`SELECT r.* FROM workflow_recipe r WHERE repo=? AND revision=(SELECT MAX(revision) FROM workflow_recipe WHERE id=r.id) ORDER BY (SELECT MAX(p.created_at) FROM workflow_preview p WHERE p.repo=r.repo AND p.source=r.id AND (p.task_id IS NOT NULL OR p.routine_id IS NOT NULL OR p.flow_id IS NOT NULL)) DESC, created_at DESC, id LIMIT 100`).all(repo).map(readRecipe);
|
|
247
249
|
}
|
|
248
250
|
export function findRecipe(store, actor, repo, id, revision) {
|
|
249
251
|
const starter = starterRecipes().find(one => one.id === id);
|
|
@@ -264,8 +266,8 @@ export function createWorkflowPreview(store, actor, repo, document, source, now)
|
|
|
264
266
|
throw new RecipeError("Unknown recipe source.");
|
|
265
267
|
return store.transact(() => {
|
|
266
268
|
access(store, actor, repo, true);
|
|
267
|
-
store.handle.prepare("DELETE FROM workflow_preview WHERE actor=? AND expires_at<? AND task_id IS NULL AND routine_id IS NULL AND saved_id IS NULL").run(actor, now.toISOString());
|
|
268
|
-
const count = store.handle.prepare("SELECT COUNT(*) n FROM workflow_preview WHERE actor=? AND expires_at>? AND task_id IS NULL AND routine_id IS NULL AND saved_id IS NULL").get(actor, now.toISOString());
|
|
269
|
+
store.handle.prepare("DELETE FROM workflow_preview WHERE actor=? AND expires_at<? AND task_id IS NULL AND routine_id IS NULL AND flow_id IS NULL AND saved_id IS NULL").run(actor, now.toISOString());
|
|
270
|
+
const count = store.handle.prepare("SELECT COUNT(*) n FROM workflow_preview WHERE actor=? AND expires_at>? AND task_id IS NULL AND routine_id IS NULL AND flow_id IS NULL AND saved_id IS NULL").get(actor, now.toISOString());
|
|
269
271
|
if (Number(count?.n) >= 30)
|
|
270
272
|
throw new RecipeError("You have 30 open previews. Use one or let it expire before making another.", 429);
|
|
271
273
|
const token = randomBytes(16).toString("hex");
|
|
@@ -283,7 +285,7 @@ export function workflowPreview(store, actor, repo, token) {
|
|
|
283
285
|
const document = importRecipe(String(row.document));
|
|
284
286
|
if (recipeDigest(document) !== row.digest)
|
|
285
287
|
throw new RecipeError("This preview has changed. Make a fresh preview.", 409);
|
|
286
|
-
return { token, actor, repo, document, digest: String(row.digest), source: String(row.source), expiresAt: String(row.expires_at), taskId: row.task_id,
|
|
288
|
+
return { token, actor, repo, document, digest: String(row.digest), source: String(row.source), expiresAt: String(row.expires_at), taskId: row.task_id, flowId: row.flow_id == null ? null : Number(row.flow_id), savedId: row.saved_id, savedRevision: row.saved_revision };
|
|
287
289
|
}
|
|
288
290
|
function currentPreview(store, actor, repo, token) {
|
|
289
291
|
access(store, actor, repo, true);
|
|
@@ -296,15 +298,15 @@ export function launchWorkflow(store, actor, repo, token, now, operatorFiling) {
|
|
|
296
298
|
repo = recipeProject(repo);
|
|
297
299
|
return store.transact(() => {
|
|
298
300
|
const preview = currentPreview(store, actor, repo, token);
|
|
299
|
-
if (preview.taskId !== null || preview.
|
|
300
|
-
return { taskId: preview.taskId,
|
|
301
|
+
if (preview.taskId !== null || preview.flowId !== null)
|
|
302
|
+
return { taskId: preview.taskId, flowId: preview.flowId };
|
|
301
303
|
if (preview.expiresAt <= now.toISOString())
|
|
302
304
|
throw new RecipeError("This preview expired. Preview it again before creating work.", 409);
|
|
303
305
|
const d = preview.document;
|
|
304
306
|
if (d.version === 2)
|
|
305
307
|
throw new RecipeError("Answer the recipe questions and preview the filled-in work before creating it.", 409);
|
|
306
308
|
const provenance = `recipe:${preview.savedId ?? preview.source}`;
|
|
307
|
-
let taskId = null,
|
|
309
|
+
let taskId = null, flowId = null;
|
|
308
310
|
if (d.schedule === null) {
|
|
309
311
|
const made = fileTaskProposal(store, { id: `workflow-${token}`, title: d.name, repo, goal: d.goal, outOfScope: d.outOfScope, touches: d.touches, acceptance: d.acceptance, planning: d.planning, deliverable: d.deliverable, filedVia: provenance, filedBy: { name: actor, kind: "person" }, admittedRepos: [repo] }, now);
|
|
310
312
|
if (!made.ok)
|
|
@@ -315,14 +317,16 @@ export function launchWorkflow(store, actor, repo, token, now, operatorFiling) {
|
|
|
315
317
|
}
|
|
316
318
|
else {
|
|
317
319
|
const slug = d.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 32) || "workflow";
|
|
318
|
-
|
|
320
|
+
// A scheduled flow whose schedule starts paused: nothing repeats until a person turns it on.
|
|
321
|
+
const made = createScheduledFlow(store, { repo, name: d.name, stem: `${slug}-${token.slice(0, 8)}`, schedule: d.schedule, by: actor,
|
|
322
|
+
terms: { goal: d.goal, outOfScope: d.outOfScope, touches: d.touches, requirements: [], acceptance: d.acceptance, budgetPerRunMicrousd: null, costCeilingUsd: d.costCeilingUsd } }, now);
|
|
319
323
|
if (!made.ok)
|
|
320
324
|
throw new RecipeError(made.message);
|
|
321
|
-
|
|
325
|
+
flowId = made.flow;
|
|
322
326
|
}
|
|
323
|
-
store.handle.prepare("UPDATE workflow_preview SET task_id=?,
|
|
324
|
-
store.recordAction({ at: now.toISOString(), actor, repo, taskId, runId: null, action: "workflow created", outcome:
|
|
325
|
-
return { taskId,
|
|
327
|
+
store.handle.prepare("UPDATE workflow_preview SET task_id=?,flow_id=? WHERE token=?").run(taskId, flowId, token);
|
|
328
|
+
store.recordAction({ at: now.toISOString(), actor, repo, taskId, runId: null, action: "workflow created", outcome: flowId === null ? "task" : "scheduled flow", source: "work" });
|
|
329
|
+
return { taskId, flowId };
|
|
326
330
|
});
|
|
327
331
|
}
|
|
328
332
|
export function saveWorkflowRecipe(store, actor, repo, token, now) {
|
|
@@ -342,7 +346,7 @@ export function saveWorkflowRecipe(store, actor, repo, token, now) {
|
|
|
342
346
|
}
|
|
343
347
|
export function workflowSteps(document) {
|
|
344
348
|
return [
|
|
345
|
-
{ title: "Authorize", detail: document.schedule === null ? "Your project policy applies. Without a matching policy, review and approve the scope first." : "
|
|
349
|
+
{ title: "Authorize", detail: document.schedule === null ? "Your project policy applies. Without a matching policy, review and approve the scope first." : "Turn the schedule on when you're ready. Each run waits for approval under your project's rules." },
|
|
346
350
|
...(document.deliverable === "report" ? [{ title: "Investigate", detail: "Inspect the project and return a source-backed report." }] : [
|
|
347
351
|
...(document.schedule === null && document.planning !== "skip" ? [{ title: "Plan", detail: document.planning === "required" ? "Inspect the repository and plan before building." : "Plan first when the scope calls for repository discovery." }] : []),
|
|
348
352
|
{ title: "Build", detail: "Work in an isolated branch within the agreed scope." },
|
|
@@ -355,5 +359,5 @@ export function recipeScheduleWords(document, now) {
|
|
|
355
359
|
if (document.schedule === null)
|
|
356
360
|
return "Once, after its approval and readiness gates pass";
|
|
357
361
|
const schedule = parseSchedule(document.schedule);
|
|
358
|
-
return `${describeSchedule(schedule)}
|
|
362
|
+
return `${describeSchedule(schedule)}, one at a time (first ${firstFireAt(schedule, now)} if turned on now). Nothing repeats until you turn the schedule on.`;
|
|
359
363
|
}
|
package/dist/result-shots.js
CHANGED
|
@@ -169,6 +169,6 @@ export function refuseResultShots(state, store, part, content, app, now) {
|
|
|
169
169
|
const title = chatTitle(store, content.image?.taskId ?? content.task ?? "");
|
|
170
170
|
const line = { text: `Screenshots for ${title} weren't sent: the ${app} app isn't allowed to upload files here.`,
|
|
171
171
|
...(content.task === undefined ? {} : { task: content.task }), ...(content.run === undefined ? {} : { run: content.run }) };
|
|
172
|
-
state.prepare("UPDATE chat_part SET payload=?,state='pending',next_at=NULL,problem=NULL,file=NULL,uploaded=0,uncertain=0,created=? WHERE id=?").run(savedChatPart(line), now.toISOString(), part.id);
|
|
173
|
-
state.prepare("UPDATE chat_part SET state='dropped',problem='Uploads are not allowed here' WHERE event=? AND id<>? AND state='pending'").run(part.event, part.id);
|
|
172
|
+
state.prepare("UPDATE chat_part SET payload=?,state='pending',next_at=NULL,problem=NULL,file=NULL,uploaded=0,uncertain=0,created=? WHERE provider=:provider AND id=?").run(savedChatPart(line), now.toISOString(), part.id);
|
|
173
|
+
state.prepare("UPDATE chat_part SET state='dropped',problem='Uploads are not allowed here' WHERE provider=:provider AND event=? AND id<>? AND state='pending'").run(part.event, part.id);
|
|
174
174
|
}
|
package/dist/retention.js
CHANGED
|
@@ -280,7 +280,7 @@ export function sweepRetention(store, evidenceRoot, now, actor = "worker") {
|
|
|
280
280
|
}
|
|
281
281
|
if (periods.notifications !== null) {
|
|
282
282
|
const deliveries = db.prepare("DELETE FROM notification_delivery WHERE notification = ?");
|
|
283
|
-
const outbound = db.prepare("DELETE FROM
|
|
283
|
+
const outbound = db.prepare("DELETE FROM chat_message_ref WHERE notification = ?");
|
|
284
284
|
const pushes = db.prepare("DELETE FROM push_delivery WHERE notification = ?");
|
|
285
285
|
const notification = db.prepare("DELETE FROM notification WHERE id = ?");
|
|
286
286
|
for (const one of plan.items.notifications) {
|
package/dist/run-closeout.d.ts
CHANGED
|
@@ -53,7 +53,7 @@ export type CloseoutOptions = StopOptions & {
|
|
|
53
53
|
};
|
|
54
54
|
/**
|
|
55
55
|
* The worker's pass: every finished run of this host that still has a process witness open is closed out. A run
|
|
56
|
-
* whose transport here still holds a live child
|
|
56
|
+
* whose transport here still holds a live child is left to that road.
|
|
57
57
|
* Returns what each run had stopped.
|
|
58
58
|
*/
|
|
59
59
|
export declare function closeOutRuns(store: Store, clock: () => Date, options?: CloseoutOptions): Promise<{
|
package/dist/run-closeout.js
CHANGED
|
@@ -130,7 +130,7 @@ export function stoppedWords(done, graceMs = STOP_GRACE_MS) {
|
|
|
130
130
|
}
|
|
131
131
|
/**
|
|
132
132
|
* The worker's pass: every finished run of this host that still has a process witness open is closed out. A run
|
|
133
|
-
* whose transport here still holds a live child
|
|
133
|
+
* whose transport here still holds a live child is left to that road.
|
|
134
134
|
* Returns what each run had stopped.
|
|
135
135
|
*/
|
|
136
136
|
export async function closeOutRuns(store, clock, options = {}) {
|
|
@@ -149,9 +149,6 @@ export async function closeOutRuns(store, clock, options = {}) {
|
|
|
149
149
|
store.recordRunProcessExits(runId, clock());
|
|
150
150
|
if (ownedProcessCount(runOwnerTag(store, runId)) > 0)
|
|
151
151
|
continue;
|
|
152
|
-
const held = store.heldSessionOf(runId);
|
|
153
|
-
if (held !== null && held.endedAt === null)
|
|
154
|
-
continue;
|
|
155
152
|
const witnesses = db.prepare(`SELECT pid, process_group, observed_at FROM run_process
|
|
156
153
|
WHERE run = ? AND exited_at IS NULL AND pid IS NOT NULL AND containment IS NULL AND container IS NULL AND host = ?`).all(runId, hostname())
|
|
157
154
|
.map(row => ({ pid: Number(row["pid"]), group: Number(row["process_group"]) === 1, observedAt: Date.parse(String(row["observed_at"])) }))
|