toolroll 0.9.52 → 0.9.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -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-controls.d.ts +0 -4
- package/dist/chat-controls.js +0 -1
- package/dist/chat-decide.js +1 -3
- package/dist/chat-delivery.d.ts +0 -2
- package/dist/chat-delivery.js +0 -1
- package/dist/chat-quiet.js +2 -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/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/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/store-json.d.ts +1 -25
- package/dist/contracts/store-json.js +3 -13
- 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/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 +4 -4
- 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 +10 -22
- package/dist/modes.d.ts +6 -7
- package/dist/modes.js +7 -13
- 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 +222 -2247
- package/dist/phase-routing.d.ts +13 -23
- package/dist/phase-routing.js +13 -35
- package/dist/plan-auto.js +2 -2
- 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 +3 -15
- 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/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 +1 -8
- 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-chat.d.ts +0 -2
- package/dist/slack-chat.js +0 -1
- package/dist/store.d.ts +105 -1340
- package/dist/store.js +706 -4010
- 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/teammates-ui.js +1 -1
- package/dist/telegram-mate.d.ts +0 -3
- package/dist/telegram-mate.js +0 -1
- package/dist/templates.d.ts +11 -7
- package/dist/templates.js +12 -9
- package/dist/toolroll-update.d.ts +10 -0
- package/dist/toolroll-update.js +56 -4
- package/dist/work-index.js +2 -6
- 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/supervisor.mjs +0 -297
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/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"])) }))
|
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
|
|
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
|
|
285
|
-
*
|
|
286
|
-
* `
|
|
287
|
-
*
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
580
|
-
* the
|
|
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;
|