toolroll 0.9.94 → 0.9.95
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 +12 -1
- package/dist/agent-fence.d.ts +6 -0
- package/dist/agent-fence.js +2 -2
- package/dist/assignment-status.d.ts +1 -1
- package/dist/assignment-ui.js +2 -0
- package/dist/attest.d.ts +3 -0
- package/dist/attest.js +10 -0
- package/dist/browser/terminal.js +1 -1
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +24 -24
- package/dist/browser-crew.js +2 -0
- package/dist/browser-workspace.d.ts +5 -0
- package/dist/build-review.js +2 -0
- package/dist/builder.d.ts +7 -2
- package/dist/builder.js +183 -79
- package/dist/card-catalog.d.ts +209 -0
- package/dist/card-catalog.js +133 -0
- package/dist/card-cli.d.ts +43 -0
- package/dist/card-cli.js +44 -0
- package/dist/card-render.d.ts +26 -0
- package/dist/card-render.js +223 -0
- package/dist/card-sources.d.ts +68 -0
- package/dist/card-sources.js +234 -0
- package/dist/chat-actions.d.ts +95 -8
- package/dist/chat-actions.js +449 -45
- package/dist/chat-approval.d.ts +13 -0
- package/dist/chat-approval.js +39 -9
- package/dist/chat-card-part.d.ts +11 -0
- package/dist/chat-card-part.js +18 -0
- package/dist/chat-card.d.ts +46 -0
- package/dist/chat-card.js +99 -0
- package/dist/chat-channel.d.ts +2 -0
- package/dist/chat-channel.js +26 -14
- package/dist/chat-controls.d.ts +4 -0
- package/dist/chat-controls.js +4 -0
- package/dist/chat-decide.d.ts +3 -14
- package/dist/chat-decide.js +43 -54
- package/dist/chat-delivery.js +13 -4
- package/dist/chat-flow.d.ts +0 -27
- package/dist/chat-flow.js +31 -2
- package/dist/chat-quiet.js +1 -1
- package/dist/chat-task-actions.d.ts +3 -2
- package/dist/chat-task-actions.js +19 -5
- package/dist/chat-terms.d.ts +14 -2
- package/dist/chat-terms.js +35 -0
- package/dist/check-failure.d.ts +2 -0
- package/dist/check-failure.js +34 -2
- package/dist/check-repair.d.ts +47 -0
- package/dist/check-repair.js +240 -0
- package/dist/claim.d.ts +6 -0
- package/dist/claim.js +13 -2
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +2 -0
- package/dist/cli.js +4 -0
- package/dist/coding-provider.d.ts +1 -1
- package/dist/coding-provider.js +9 -5
- package/dist/coding-workspace.d.ts +1 -1
- package/dist/coding-workspace.js +4 -4
- package/dist/contracts/chat-actions.d.ts +83 -0
- package/dist/contracts/chat-actions.js +18 -0
- package/dist/contracts/chat-card.d.ts +91 -0
- package/dist/contracts/chat-card.js +54 -0
- package/dist/contracts/chat-content.d.ts +191 -0
- package/dist/contracts/chat-content.js +4 -0
- package/dist/contracts/console-api.d.ts +2 -0
- package/dist/contracts/console-api.js +2 -0
- package/dist/contracts/flow-propose.d.ts +8 -1
- package/dist/contracts/flow-propose.js +3 -1
- package/dist/contracts/flow-send.d.ts +126 -0
- package/dist/contracts/flow-send.js +3 -0
- package/dist/contracts/lead-tools.d.ts +173 -0
- package/dist/contracts/lead-tools.js +15 -0
- package/dist/contracts/store-json.d.ts +16 -0
- package/dist/discord-chat.js +14 -0
- package/dist/dispatch.d.ts +1 -1
- package/dist/dispatch.js +7 -1
- package/dist/dispose.d.ts +19 -0
- package/dist/dispose.js +47 -7
- package/dist/eval-oracle.js +4 -1
- package/dist/evidence-pair.d.ts +53 -0
- package/dist/evidence-pair.js +128 -0
- package/dist/exhaustion.d.ts +17 -4
- package/dist/exhaustion.js +44 -4
- package/dist/export.js +1 -1
- package/dist/failing-first.d.ts +31 -0
- package/dist/failing-first.js +44 -0
- package/dist/fallback-schema.d.ts +8 -0
- package/dist/fallback-schema.js +40 -0
- package/dist/fallback.d.ts +138 -0
- package/dist/fallback.js +605 -0
- package/dist/findings.d.ts +224 -0
- package/dist/findings.js +449 -0
- package/dist/fleet-schema.d.ts +17 -0
- package/dist/fleet-schema.js +135 -0
- package/dist/flow-browser.js +2 -2
- package/dist/flow-code.d.ts +2 -2
- package/dist/flow-code.js +9 -5
- package/dist/flow-result-card.d.ts +22 -0
- package/dist/flow-result-card.js +55 -0
- package/dist/flow-send.js +12 -9
- package/dist/flow-steps.js +1 -1
- package/dist/flow-triggers.js +1 -1
- package/dist/flow-unattended.d.ts +26 -0
- package/dist/flow-unattended.js +12 -3
- package/dist/flows-ui.js +10 -2
- package/dist/follow-through-brief.d.ts +12 -0
- package/dist/follow-through-brief.js +31 -0
- package/dist/follow-through-cli.d.ts +23 -0
- package/dist/follow-through-cli.js +153 -0
- package/dist/git-incident.d.ts +16 -0
- package/dist/git-incident.js +69 -6
- package/dist/handover-note.d.ts +33 -0
- package/dist/handover-note.js +106 -0
- package/dist/invoke.d.ts +20 -1
- package/dist/invoke.js +108 -44
- package/dist/lead-cli.js +26 -24
- package/dist/lead-contract.js +1 -0
- package/dist/lead-doors.d.ts +4 -0
- package/dist/lead-doors.js +2 -2
- package/dist/lead-progress.js +2 -0
- package/dist/lead-tools.js +111 -32
- package/dist/lead.js +1 -1
- package/dist/local-owner.js +5 -0
- package/dist/modes.d.ts +6 -6
- package/dist/modes.js +3 -1
- package/dist/needs-you.d.ts +5 -2
- package/dist/needs-you.js +14 -2
- package/dist/operate-remote-arguments.js +1 -0
- package/dist/operate-remote.js +1 -1
- package/dist/operate.d.ts +1 -1
- package/dist/operate.js +165 -14
- package/dist/plan-login.d.ts +14 -0
- package/dist/plan-login.js +58 -0
- package/dist/planner.d.ts +1 -1
- package/dist/planner.js +2 -1
- package/dist/project-delete-ui.d.ts +12 -0
- package/dist/project-delete-ui.js +18 -1
- package/dist/project-delete.js +6 -0
- package/dist/project-knowledge.d.ts +6 -6
- package/dist/project-tools.d.ts +4 -4
- package/dist/project-tools.js +23 -7
- package/dist/protected-storage-look.d.ts +1 -0
- package/dist/protected-storage-look.js +13 -0
- package/dist/protected-storage.d.ts +70 -39
- package/dist/protected-storage.js +196 -147
- package/dist/provider-capture.d.ts +34 -0
- package/dist/provider-capture.js +53 -0
- package/dist/release-carry.js +10 -1
- package/dist/release.js +9 -1
- package/dist/scope.d.ts +22 -3
- package/dist/scope.js +71 -20
- package/dist/scout.d.ts +1 -1
- package/dist/scout.js +2 -1
- package/dist/serve.js +2 -0
- package/dist/server/chat.js +2 -2
- package/dist/server/console.css +50 -0
- package/dist/server/render-chat.js +14 -1
- package/dist/server/render-follow-through.d.ts +11 -0
- package/dist/server/render-follow-through.js +68 -0
- package/dist/server/render-results.js +1 -1
- package/dist/server/render-tasks.d.ts +3 -0
- package/dist/server/render-tasks.js +24 -5
- package/dist/server/route-table.js +3 -0
- package/dist/server/settings.js +37 -12
- package/dist/server/task-views.js +5 -0
- package/dist/server/tasks.js +76 -0
- package/dist/settings-groups-ui.d.ts +1 -0
- package/dist/settings-groups-ui.js +3 -1
- package/dist/settings-kit.js +3 -2
- package/dist/settings-proofs.js +3 -1
- package/dist/setup-guide.d.ts +2 -2
- package/dist/slack-chat.js +14 -1
- package/dist/steer-mailbox.d.ts +8 -1
- package/dist/steer-mailbox.js +164 -6
- package/dist/store.d.ts +11 -4
- package/dist/store.js +93 -30
- package/dist/surface.js +24 -4
- package/dist/task-control.d.ts +2 -1
- package/dist/task-status.js +5 -0
- package/dist/teardown-git.d.ts +2 -0
- package/dist/teardown-git.js +42 -16
- package/dist/telegram-lead.js +23 -6
- package/dist/telegram.js +12 -1
- package/dist/toolroll-update.js +2 -0
- package/dist/work-index.js +22 -7
- package/dist/work-summary.d.ts +1 -1
- package/dist/work-summary.js +5 -2
- package/dist/workspace-ui.d.ts +3 -1
- package/dist/workspace-ui.js +10 -1
- package/dist/worktree.js +23 -1
- package/package.json +2 -1
|
@@ -3,7 +3,8 @@ import { archivedWords } from "../tidy.js";
|
|
|
3
3
|
import { btn } from "./kit.js";
|
|
4
4
|
import { whenUtc } from "../when-html.js";
|
|
5
5
|
import { networkWords } from "../egress.js";
|
|
6
|
-
import { releaseAsksAgainWords, releaseTermsSentence } from "../scope.js";
|
|
6
|
+
import { chainTermsLines, liveChainFromJson, releaseAsksAgainWords, releaseTermsSentence } from "../scope.js";
|
|
7
|
+
import { SUBSCRIPTION_CAPABLE } from "../keys.js";
|
|
7
8
|
import { RELEASE_CI_RUNNING_WORDS } from "../release-ci-hold.js";
|
|
8
9
|
import { writesWords } from "../writes-mode.js";
|
|
9
10
|
import { checkAccessWords } from "../check-access.js";
|
|
@@ -28,7 +29,7 @@ import { taskRoad } from "../first-run.js";
|
|
|
28
29
|
import { starterForWork } from "../flow-starters.js";
|
|
29
30
|
import {} from "../gaps.js";
|
|
30
31
|
import { readAuthModeStrict } from "../keys.js";
|
|
31
|
-
import { failedAttemptSentence, retryNoteOf, START_BUILDER_COMMAND, startBuilderHref } from "../needs-you.js";
|
|
32
|
+
import { failedAttemptSentence, retryNoteOf, START_BUILDER_COMMAND, startBuilderHref, STORAGE_HOLD_PREFIX } from "../needs-you.js";
|
|
32
33
|
import { planVersionWords } from "../plain-words.js";
|
|
33
34
|
import { chosenWords, makesNoPlan, PHASES as ROUTE_PHASES, TASK_SIZES } from "../phase-routing.js";
|
|
34
35
|
import { parseExecutionPlanDocument } from "../plan.js";
|
|
@@ -315,6 +316,14 @@ export function approvalSheetParts(input) {
|
|
|
315
316
|
if (allowed !== null)
|
|
316
317
|
allowing.push(allowed);
|
|
317
318
|
allowing.push(scope.budgetMicrousd === null ? "no attempt limit" : `up to ${money(scope.budgetMicrousd)} per attempt`);
|
|
319
|
+
// Fallback agents are terms: who takes over when an agent runs out, with any that spend money said before the yes.
|
|
320
|
+
const chain = chainTermsLines(scope);
|
|
321
|
+
const fallbacks = liveChainFromJson(scope.proposedChainJson ?? null)?.slice(1) ?? [];
|
|
322
|
+
if (fallbacks.length > 0) {
|
|
323
|
+
const paid = fallbacks.filter(one => one.authMode === "api-key" || SUBSCRIPTION_CAPABLE[one.profile.provider] !== true);
|
|
324
|
+
// Each agent named once, the paid one said where it stands in the order.
|
|
325
|
+
allowing.push(`if it runs out of its plan, the work moves to ${fallbacks.map(one => `${agentNameWords(one.profile.provider, one.profile.model)}${paid.includes(one) ? " on an API key, which spends money" : ""}`).join(", then ")}`);
|
|
326
|
+
}
|
|
318
327
|
const earlier = earlierVersionsWords(input.earlier.active, input.earlier.running);
|
|
319
328
|
if (earlier !== null)
|
|
320
329
|
allowing.push(earlier);
|
|
@@ -441,6 +450,7 @@ export function approvalSheetParts(input) {
|
|
|
441
450
|
row("Changes", changes),
|
|
442
451
|
row("Won’t touch", html `<p>${scope.outOfScope === null ? "Nothing is ruled out." : scope.outOfScope}</p>`),
|
|
443
452
|
row("Done when", doneWhen),
|
|
453
|
+
chain.length === 0 ? "" : row("If it runs out", html `<p>${chain[0]}</p><ol class="approval-steps" data-approval-fallback>${chain.slice(1).map(one => html `<li>${one.replace(/^\d+\. /, "")}</li>`)}</ol>`),
|
|
444
454
|
html `</dl>`,
|
|
445
455
|
amendment,
|
|
446
456
|
// The size, said once and plainly beside who builds: "Small change: fast model, no plan".
|
|
@@ -1044,6 +1054,7 @@ export function statusActionHref(status, taskId, runId, prUrl, repo) {
|
|
|
1044
1054
|
case "open-review": return repo === undefined ? reviewHref(taskId) : reviewHref(taskId, runId);
|
|
1045
1055
|
case "open-run": return runId === null ? taskHref(taskId) : `/r/${runId}`;
|
|
1046
1056
|
case "open-pr": return safePrUrl(prUrl) ?? (repo === undefined ? reviewHref(taskId) : reviewHref(taskId, runId));
|
|
1057
|
+
case "open-findings": return `${taskHref(taskId)}#findings`;
|
|
1047
1058
|
case "open-task":
|
|
1048
1059
|
default: return taskHref(taskId);
|
|
1049
1060
|
}
|
|
@@ -1364,7 +1375,7 @@ export function taskBodyParts(data) {
|
|
|
1364
1375
|
case "select-agent": return html ` <a href="#scope">Choose an available provider and model</a>.`;
|
|
1365
1376
|
case "approve-scope": return html ` <a href="#approve">Review the plan and approve it</a>.`;
|
|
1366
1377
|
case "answer-decision": return html ` <a href="#decisions">Answer the waiting question</a>.`;
|
|
1367
|
-
case "unhold": return html ` Use <strong>Remove hold</strong> when it can continue.`;
|
|
1378
|
+
case "unhold": return diagnosis.code === "storage-blocked" ? html ` Use <strong>Try again</strong> once it's allowed.` : html ` Use <strong>Remove hold</strong> when it can continue.`;
|
|
1368
1379
|
// The reason is the detail itself; Retry is the status card's own act, said once.
|
|
1369
1380
|
case "retry-task": return "";
|
|
1370
1381
|
case "repair-capability": return html ` <a href="/caps">Repair the requirement</a>.`;
|
|
@@ -1536,7 +1547,9 @@ export function taskBodyParts(data) {
|
|
|
1536
1547
|
? ""
|
|
1537
1548
|
: joinHtml([
|
|
1538
1549
|
html `<h2>Holds</h2>`,
|
|
1539
|
-
joinHtml(data.holds.map(
|
|
1550
|
+
joinHtml(data.holds.map(
|
|
1551
|
+
// The storage check's hold (dispose.ts holdForStorage) says what it waits for, in the check's own words.
|
|
1552
|
+
hold => html `<p class="row">${hold.ownerKind === "backoff" && hold.reason.startsWith(STORAGE_HOLD_PREFIX) ? `waiting for macOS permission — ${hold.reason.slice(STORAGE_HOLD_PREFIX.length)}` : `${holdOwnerWords(hold.ownerKind)} — ${hold.reason}`}${hold.until === null ? "" : html ` <span class="meta">until ${whenTime(hold.until)}</span>`}</p>`), "\n"),
|
|
1540
1553
|
html `<p class="meta">Only your hold can be lifted here. Waits for a question, repeated failures or a retry clear on their own.</p>`,
|
|
1541
1554
|
]);
|
|
1542
1555
|
const approval = approvalOf(scope);
|
|
@@ -2012,7 +2025,9 @@ export function taskBodyParts(data) {
|
|
|
2012
2025
|
// to "plan first" adds a second, unnecessary decision at the exact
|
|
2013
2026
|
// moment the page should have one obvious action.
|
|
2014
2027
|
stopControlsActive || canPlan || !canHold ? "" : holdAct,
|
|
2015
|
-
data.holds.some(hold => hold.ownerKind === "operator") ? act("unhold", "Remove hold")
|
|
2028
|
+
data.holds.some(hold => hold.ownerKind === "operator") ? act("unhold", "Remove hold")
|
|
2029
|
+
// The storage check couldn't finish (dispose.ts holdForStorage): one tap tries again now.
|
|
2030
|
+
: data.holds.some(hold => hold.ownerKind === "backoff" && hold.ownerId === `storage:${hold.taskRef}`) ? act("unhold", "Try again") : "",
|
|
2016
2031
|
html `</div>`,
|
|
2017
2032
|
primaryAct === null ? "" : html `<p class="meta acts-why acts-why-${primaryAct.whyClass}">${primaryAct.why}</p>`,
|
|
2018
2033
|
data.claimed && !stopControlsActive
|
|
@@ -2159,6 +2174,10 @@ export function taskBodyParts(data) {
|
|
|
2159
2174
|
data.assignment == null ? section("attempts", runs, true, data.runs.length) : section("Build activity", data.runs.length === 0 ? "" : runRows, false, data.runs.length, "attempts"),
|
|
2160
2175
|
section("usage", spendCard, false),
|
|
2161
2176
|
section("steering", steeringCard, (data.steering ?? []).length > 0, (data.steering ?? []).length),
|
|
2177
|
+
data.followThrough == null ? "" : section("findings", data.followThrough.findings, data.followThrough.openFindings > 0, data.followThrough.findingCount),
|
|
2178
|
+
data.followThrough == null ? "" : section("failed checks", data.followThrough.repairs, true),
|
|
2179
|
+
data.followThrough == null ? "" : section("before and after", data.followThrough.evidence, false),
|
|
2180
|
+
data.followThrough == null ? "" : section("handover note", data.followThrough.handover, false),
|
|
2162
2181
|
section("plan",
|
|
2163
2182
|
// The recipe road rides with the scope it reuses (UI polish
|
|
2164
2183
|
// 2026-09-13), off the title-to-action path.
|
|
@@ -461,6 +461,7 @@ post("settings", "spend.budget", "/spend/budget", { scope: "step-up", ...operato
|
|
|
461
461
|
post("settings", "settings.project-concurrency", "/settings/project/concurrency");
|
|
462
462
|
post("settings", "settings.project-live-view", "/settings/project/live-view", { scope: "step-up", callers: ["cookie"] });
|
|
463
463
|
post("settings", "settings.project-network", "/settings/project/network", { scope: "step-up", callers: ["cookie"] });
|
|
464
|
+
post("settings", "settings.project-fallback", "/settings/project/fallback", { scope: "step-up", callers: ["cookie"] });
|
|
464
465
|
post("settings", "settings.project-writes", "/settings/project/writes", { scope: "step-up", callers: ["cookie"] });
|
|
465
466
|
post("settings", "settings.project-check-access", "/settings/project/check-access", { scope: "step-up", callers: ["cookie"] });
|
|
466
467
|
post("settings", "settings.project-sandbox", "/settings/project/sandbox", { scope: "step-up", callers: ["cookie"] });
|
|
@@ -572,6 +573,8 @@ post("tasks", "decision.answer", `^\\/d\\/${ID}\\/answer$`, { project: "decision
|
|
|
572
573
|
// A held automatic release goes on: a person, with their password typed now (release-auto.ts allowAutoPublish).
|
|
573
574
|
post("tasks", "task.release.go-on", `^\\/t\\/${TASK}\\/release\\/go-on$`, { project: "task", limited: "resource", scope: "step-up", sample: "/t/one/release/go-on", ...approver("An approver lets a release go on.", "/tasks") });
|
|
574
575
|
post("tasks", "task.act.keep-grown", `^\\/t\\/${TASK}\\/keep-grown$`, { project: "task", limited: "resource", sample: "/t/one/keep-grown", callers: ["cookie"], ...approver("An approver keeps a change.", "/tasks") });
|
|
576
|
+
// Findings, a failed check's repair, before-and-after evidence: a person's acts from a browser, none of them an approval.
|
|
577
|
+
post("tasks", "task.act.follow-through", `^\\/t\\/${TASK}\\/follow-through$`, { project: "task", limited: "resource", sample: "/t/one/follow-through", callers: ["cookie"], ...approver("An approver acts on a finding.", "/tasks") });
|
|
575
578
|
post("tasks", "task.act.hold", `^\\/t\\/${TASK}\\/hold$`, { project: "task", limited: "resource", sample: "/t/one/hold" });
|
|
576
579
|
post("tasks", "task.act.unhold", `^\\/t\\/${TASK}\\/unhold$`, { project: "task", limited: "resource", sample: "/t/one/unhold" });
|
|
577
580
|
post("tasks", "task.act.requeue", `^\\/t\\/${TASK}\\/requeue$`, { project: "task", limited: "resource", sample: "/t/one/requeue" });
|
package/dist/server/settings.js
CHANGED
|
@@ -2,6 +2,7 @@ import { PASSWORDLESS_TRADE, passwordlessSetting, setPasswordless } from "../pas
|
|
|
2
2
|
import { ACP_AGENTS, isKeyedProvider } from "../acp-agents.js";
|
|
3
3
|
import { LIVE_VIEW_CONSENT, liveViewConsentDigest, setLiveCommands } from "../live-detail.js";
|
|
4
4
|
import { CONSOLE_FORMS, readForm } from "../contracts/console-api.js";
|
|
5
|
+
import { clearFallbackChain, parseEntry, setFallbackChain, setPaidGrant } from "../fallback.js";
|
|
5
6
|
import { galleryTemplateOf } from "../flow-gallery.js";
|
|
6
7
|
import { finishGoogleConsent, GOOGLE_CALLBACK } from "../google-mail.js";
|
|
7
8
|
import { kitInstalled, kitOf } from "../kits.js";
|
|
@@ -84,7 +85,7 @@ import { acceptSuggestion, dismissSuggestion, grantWords, keepRule, modelName, r
|
|
|
84
85
|
import { ALL_SLOTS, applyModelChoices, browserWords, checkScope, DEFAULT_PRESET, defaultAgentOf, fillDefaultRouting, isPresetName, newerModels, PRESETS, presetChange, presetInUse, previewModelChange, recentRuns, rowAt, slotKey, SLOTS, slotWords, stateDigest, switchChange } from "../model-choices.js";
|
|
85
86
|
import { HEADROOM_FIVE_HOUR_LIMIT, HEADROOM_WEEKLY_LIMIT, exactModelId } from "../phase-routing.js";
|
|
86
87
|
import { RISKY_MEANS, SIZE_MEANS, SIZING_CLAUDE_MODEL } from "../task-sizing.js";
|
|
87
|
-
import { CHAT_APPROVE_ALL, MODE_MAX_DAYS, modeDigestOf, modeTermsJson, modeWords, presetTerms } from "../modes.js";
|
|
88
|
+
import { CHAT_APPROVE_ALL, CHECK_REPAIR_LIMIT, MODE_MAX_DAYS, modeDigestOf, modeTermsJson, modeWords, presetTerms } from "../modes.js";
|
|
88
89
|
import { monitoringChange, readMonitoring, saveMonitoring } from "../monitoring-settings.js";
|
|
89
90
|
import { monitoringHtml, signingSecretHtml } from "../monitoring-ui.js";
|
|
90
91
|
import { openRouterPickerScript } from "../openrouter-models.js";
|
|
@@ -2461,19 +2462,17 @@ ${postForm("/settings/chat-approval/save", html `\
|
|
|
2461
2462
|
planAuto: body.get("plan-auto") === "1",
|
|
2462
2463
|
reviewAuto: false,
|
|
2463
2464
|
reviewRetryAuto: false,
|
|
2464
|
-
//
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
repairAuto: false,
|
|
2468
|
-
repairMaxAttempts: 0,
|
|
2465
|
+
// Repairing failed checks: only the explicit choice grants it, with the limit it signs (check-repair.ts).
|
|
2466
|
+
repairAuto: body.get("repair-auto") === "1",
|
|
2467
|
+
repairMaxAttempts: body.get("repair-auto") === "1" ? CHECK_REPAIR_LIMIT : 0,
|
|
2469
2468
|
// Approving from the paired chat: only the explicit choice grants it, naming every chat app it covers (a grant
|
|
2470
2469
|
// signed before it named them stays Telegram only).
|
|
2471
2470
|
chatApprove: body.get("chat-approve") === "1",
|
|
2472
2471
|
...(body.get("chat-approve") === "1" ? { chatApproveChats: CHAT_APPROVE_ALL } : {}),
|
|
2473
2472
|
publication: body.get("publication") === "automerge" ? "automerge" : "notify",
|
|
2474
2473
|
};
|
|
2475
|
-
if (["review-auto", "review-retry-auto"
|
|
2476
|
-
return refuse(response, who, 400, "Review scheduling
|
|
2474
|
+
if (["review-auto", "review-retry-auto"].some(field => body.get(field) === "1")) {
|
|
2475
|
+
return refuse(response, who, 400, "Review scheduling is no longer available. Reload the mode form.", "/mode");
|
|
2477
2476
|
}
|
|
2478
2477
|
if (terms.planAuto && !terms.autoApproveFiling) {
|
|
2479
2478
|
return refuse(response, who, 400, "Automatic planner approval requires automatic filing approval.", "/mode");
|
|
@@ -2486,9 +2485,9 @@ ${postForm("/settings/chat-approval/save", html `\
|
|
|
2486
2485
|
// THE CEREMONY (M1): every resolved term in words, and the password
|
|
2487
2486
|
// signs exactly this digest — a drifted form refuses at /mode/sign.
|
|
2488
2487
|
const nonce = mintApprovalNonce(who.name, "mode-sign", `${project}:${digest}`);
|
|
2489
|
-
const signed = Object.fromEntries(["name", "days", "publication", "auto-approve", "plan-auto", "chat-approve"]
|
|
2488
|
+
const signed = Object.fromEntries(["name", "days", "publication", "auto-approve", "plan-auto", "chat-approve", "repair-auto"]
|
|
2490
2489
|
.map(field => [field, body.get(field) ?? ""]));
|
|
2491
|
-
const bodyHtml = modeCeremonyHtml({ name, project: projectName(project), words: modeWords(terms), chatApprove: signed["chat-approve"] === "1", hidden: { nonce, digest, ...signed, expiry } });
|
|
2490
|
+
const bodyHtml = modeCeremonyHtml({ name, project: projectName(project), words: modeWords(terms), chatApprove: signed["chat-approve"] === "1", repairAuto: signed["repair-auto"] === "1", hidden: { nonce, digest, ...signed, expiry } });
|
|
2492
2491
|
return sendScreen(response, 200, screen("Sign the mode", bodyHtml, { chrome: chromeFor(project, "mode") }));
|
|
2493
2492
|
}
|
|
2494
2493
|
// /mode/sign: the fixed expiry rides the form; the digest is
|
|
@@ -2504,8 +2503,8 @@ ${postForm("/settings/chat-approval/save", html `\
|
|
|
2504
2503
|
return refuse(response, who, 409, "the terms moved while you were reading — read them again", "/mode");
|
|
2505
2504
|
}
|
|
2506
2505
|
const token = body.get("token") ?? "";
|
|
2507
|
-
// A mode that lets a chat app approve opens a way to act: a credential act. Any other mode is an ordinary one.
|
|
2508
|
-
if (!(body.get("chat-approve") === "1" ? authenticateCredential(who, token) : authenticateApprover(who, token)).ok) {
|
|
2506
|
+
// A mode that lets a chat app approve, or repairs start by themselves, opens a way to act: a credential act. Any other mode is an ordinary one.
|
|
2507
|
+
if (!(body.get("chat-approve") === "1" || body.get("repair-auto") === "1" ? authenticateCredential(who, token) : authenticateApprover(who, token)).ok) {
|
|
2509
2508
|
return refuse(response, who, 403, "signing a mode takes your password, typed again", "/mode");
|
|
2510
2509
|
}
|
|
2511
2510
|
store.signMode({
|
|
@@ -2814,6 +2813,7 @@ ${postForm("/settings/chat-approval/save", html `\
|
|
|
2814
2813
|
"settings.project-concurrency": projectConcurrency,
|
|
2815
2814
|
"settings.project-live-view": projectLiveView,
|
|
2816
2815
|
"settings.project-network": projectNetwork,
|
|
2816
|
+
"settings.project-fallback": projectFallback,
|
|
2817
2817
|
"settings.project-writes": projectWrites,
|
|
2818
2818
|
"settings.project-check-access": projectCheckAccess,
|
|
2819
2819
|
"settings.project-sandbox": projectSandbox,
|
|
@@ -3198,5 +3198,30 @@ ${postForm("/settings/chat-approval/save", html `\
|
|
|
3198
3198
|
: kind === "suggest-on" ? "Suggestions are on for this project." : "Suggestions are off for this project.");
|
|
3199
3199
|
};
|
|
3200
3200
|
}
|
|
3201
|
+
// Settings → Project → Fallback agents (fallback.ts): who takes over when an agent runs out of its plan. An approver for
|
|
3202
|
+
// the project, with their password typed now; the ledger keeps every change. New tasks seal it; approved ones keep theirs.
|
|
3203
|
+
async function projectFallback(ctx) {
|
|
3204
|
+
const { who, response, now, posted } = ctx;
|
|
3205
|
+
const body = readForm(posted, CONSOLE_FORMS.projectFallback);
|
|
3206
|
+
const repo = body.get("repo") ?? "";
|
|
3207
|
+
if (!consoleProjects().includes(repo) || !visible(repo))
|
|
3208
|
+
return refuse(response, who, 403, "That project is outside your access.", "/settings/project");
|
|
3209
|
+
if (who.via !== "cookie" || who.role !== "approver" || store.isDemo() || !store.accountCanAccess(who.name, repo))
|
|
3210
|
+
return refuse(response, who, 403, "An approver for this project changes its fallback agents.", "/settings/project");
|
|
3211
|
+
const back = sayTo(response, `/settings/project?repo=${encodeURIComponent(repo)}`, "#fallback");
|
|
3212
|
+
const parsed = (body.get("agents") ?? "").split(/[\n,]+/).map(one => one.trim()).filter(Boolean).map(parseEntry);
|
|
3213
|
+
const bad = parsed.find((one) => typeof one === "string");
|
|
3214
|
+
if (bad !== undefined)
|
|
3215
|
+
return back("problem", `Nothing changed: ${bad}.`);
|
|
3216
|
+
if (!authenticateCredential(who, body.get("password") ?? "").ok)
|
|
3217
|
+
return back("problem", "Enter your Toolroll password to change fallback agents.");
|
|
3218
|
+
const done = store.transact(() => {
|
|
3219
|
+
const set = parsed.length === 0 ? clearFallbackChain(store, repo, who.name, now) : setFallbackChain(store, repo, parsed, who.name, now);
|
|
3220
|
+
if (!set.ok || parsed.length === 0)
|
|
3221
|
+
return set;
|
|
3222
|
+
return setPaidGrant(store, repo, body.get("paid") === "on", who.name, now);
|
|
3223
|
+
});
|
|
3224
|
+
return done.ok ? back("said", "Saved. Tasks scoped from now on use it; tasks already approved keep the chain they were approved with.") : back("problem", `Nothing changed: ${done.problem}`);
|
|
3225
|
+
}
|
|
3201
3226
|
return { registrations, modelsView };
|
|
3202
3227
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { heldFindingCount } from "../findings.js";
|
|
2
|
+
import { familyInWebReach } from "../findings.js";
|
|
1
3
|
import { existsSync } from "node:fs";
|
|
2
4
|
import { resultRecordWordsOf } from "../route-results.js";
|
|
3
5
|
import { selectionEvidenceWords } from "../route-rules.js";
|
|
@@ -28,6 +30,7 @@ import { readSkillsSnapshot, skillTestResult } from "../project-skills.js";
|
|
|
28
30
|
import { newestPullRequestOf, publishingOf, pullRequestBlocker } from '../pull-request-flow.js';
|
|
29
31
|
import { structuredHandoffView, terminalDiffView } from "../result-evidence-readers.js";
|
|
30
32
|
import { resultCheckLevel } from '../result-follow-ups.js';
|
|
33
|
+
import { followThroughView } from "./render-follow-through.js";
|
|
31
34
|
import { skillsSnapshotHtml, skillTestFeedbackHtml } from "../skills-ui.js";
|
|
32
35
|
import { archiveOf } from "../store.js";
|
|
33
36
|
import { runActivityOf } from "../task-activity.js";
|
|
@@ -143,6 +146,7 @@ export function createTaskViews(runtime) {
|
|
|
143
146
|
const live = runs.find(one => runIsLive(one)) ?? null;
|
|
144
147
|
const facts = {
|
|
145
148
|
id: task.id,
|
|
149
|
+
heldFindings: heldFindingCount(runtime.store, task.id),
|
|
146
150
|
title: task.title,
|
|
147
151
|
repo: task.repo,
|
|
148
152
|
state: task.state,
|
|
@@ -541,6 +545,7 @@ export function createTaskViews(runtime) {
|
|
|
541
545
|
: field.field === "local-refused" ? { eligible: false, message: field.message, href: field.href, action: field.action } : null };
|
|
542
546
|
})(),
|
|
543
547
|
routeChange: ref === null ? null : routeChangeOf(runtime.store, ref.externalId),
|
|
548
|
+
followThrough: who === undefined || familyInWebReach(runtime.store, who, taskId, runtime.visible) === null ? null : followThroughView(runtime.store, runtime.evidenceRoot, taskId, taskHref(taskId), who.role === "approver" && who.via === "cookie" && !runtime.store.isDemo(), runtime.clock()),
|
|
544
549
|
fastGrown: (() => {
|
|
545
550
|
if (ref === null || runtime.store.getTask(ref.externalId)?.state !== "done" || !fastStarted(runtime.store, ref.externalId))
|
|
546
551
|
return null;
|
package/dist/server/tasks.js
CHANGED
|
@@ -36,6 +36,10 @@ import { firstTaskSuggestions, withBuilderStep } from "../first-run.js";
|
|
|
36
36
|
import { fastStartPage, fastTermsRows, importPage, newTaskPage } from "./render-new-task.js";
|
|
37
37
|
import { fileGithubItem, readGithubItem } from "../github-import.js";
|
|
38
38
|
import { allowAutoPublish, autoPublishHeld } from "../release-auto.js";
|
|
39
|
+
import { startCheckRepair } from "../check-repair.js";
|
|
40
|
+
import { askFailingFirst, dropFailingFirst, rerunEvidence, saveEvidencePair } from "../evidence-pair.js";
|
|
41
|
+
import { closeFinding, familyInWebReach, familyRootOf, getFinding, NOT_ON_TASK, resendFinding, runOnTask, sendFinding, sendReviewFinding } from "../findings.js";
|
|
42
|
+
import { labelProviderCapture, providerCapture } from "../provider-capture.js";
|
|
39
43
|
import { fastStartBuild, keepGrownChange, prepareFastStart } from "../fast-start.js";
|
|
40
44
|
import { evidencePack } from "../evidence-pack.js";
|
|
41
45
|
import { decisionPage, evidenceRecordPage, runPage } from "./render-history.js";
|
|
@@ -2962,6 +2966,7 @@ export function createTasksHandlers(runtime) {
|
|
|
2962
2966
|
"tasks.add": addTask,
|
|
2963
2967
|
"tasks.fast": buildNow,
|
|
2964
2968
|
"task.act.keep-grown": keepGrown,
|
|
2969
|
+
"task.act.follow-through": followThroughAct,
|
|
2965
2970
|
"queue.move": queueMove,
|
|
2966
2971
|
"queue.note": queueNote,
|
|
2967
2972
|
"decision.answer": answerDecision,
|
|
@@ -3062,6 +3067,77 @@ export function createTasksHandlers(runtime) {
|
|
|
3062
3067
|
return taskScreen(response, who, act.taskId, kept.message, 409);
|
|
3063
3068
|
return redirect(response, taskHref(act.taskId));
|
|
3064
3069
|
}
|
|
3070
|
+
/** Findings, a failed check's repair and evidence (render-follow-through.ts). Nothing here is an approval: a repair is a
|
|
3071
|
+
* draft, and its task page asks for the plan's approval as any other. */
|
|
3072
|
+
async function followThroughAct(ctx) {
|
|
3073
|
+
const { url, who, response, now, posted } = ctx;
|
|
3074
|
+
const body = readForm(posted, CONSOLE_FORMS.followThrough);
|
|
3075
|
+
const act = matchTaskPath(url.pathname, "/follow-through$");
|
|
3076
|
+
if (act === null)
|
|
3077
|
+
return noPage(who, response);
|
|
3078
|
+
const { taskId } = act;
|
|
3079
|
+
// Chat's gate, here too (findings.ts familyInReach): every project in this task's family, not only this page's.
|
|
3080
|
+
if (familyInWebReach(store, who, taskId, visible) === null)
|
|
3081
|
+
return refuse(response, who, 403, "That task is outside your access.", "/tasks");
|
|
3082
|
+
const number = (name) => Number(body.get(name) ?? "");
|
|
3083
|
+
const back = () => redirect(response, `${taskHref(taskId)}#findings`);
|
|
3084
|
+
const refused = (message) => taskScreen(response, who, taskId, message, 409);
|
|
3085
|
+
/** A finding or a run named by a form must belong to this task's family: this page's project was the one admitted. */
|
|
3086
|
+
const ownFinding = () => { const found = getFinding(store, number("finding")); return found !== null && found.familyRoot === familyRootOf(store, taskId) ? found : null; };
|
|
3087
|
+
const ownRun = () => runOnTask(store, number("run"), taskId);
|
|
3088
|
+
switch (body.get("act")) {
|
|
3089
|
+
case "send": {
|
|
3090
|
+
const line = body.get("line") ?? "";
|
|
3091
|
+
const pointer = body.get("pointer") ?? "";
|
|
3092
|
+
const sent = pointer !== "" ? sendReviewFinding(store, taskId, pointer, who.name, now)
|
|
3093
|
+
: sendFinding(store, { taskId, source: "person", by: who.name, summary: body.get("note") ?? "", file: body.get("file") || null, line: line === "" ? null : Number(line) }, now);
|
|
3094
|
+
return sent.ok ? back() : refused(sent.message);
|
|
3095
|
+
}
|
|
3096
|
+
case "resend": {
|
|
3097
|
+
if (ownFinding() === null)
|
|
3098
|
+
return refused("That finding isn't on this task.");
|
|
3099
|
+
const sent = resendFinding(store, number("finding"), now);
|
|
3100
|
+
return sent.ok ? back() : refused(sent.message);
|
|
3101
|
+
}
|
|
3102
|
+
case "close": {
|
|
3103
|
+
if (ownFinding() === null)
|
|
3104
|
+
return refused("That finding isn't on this task.");
|
|
3105
|
+
const closed = closeFinding(store, number("finding"), who.name, body.get("note") ?? "", now);
|
|
3106
|
+
return closed.ok ? back() : refused(closed.message);
|
|
3107
|
+
}
|
|
3108
|
+
case "repair": {
|
|
3109
|
+
if (!ownRun())
|
|
3110
|
+
return refused(NOT_ON_TASK);
|
|
3111
|
+
const started = startCheckRepair(store, evidenceRoot, number("run"), now);
|
|
3112
|
+
return started.ok ? redirect(response, taskHref(started.draft)) : refused(started.message);
|
|
3113
|
+
}
|
|
3114
|
+
case "save-evidence": {
|
|
3115
|
+
if (!ownRun())
|
|
3116
|
+
return refused(NOT_ON_TASK);
|
|
3117
|
+
const saved = saveEvidencePair(store, evidenceRoot, { taskId, runId: number("run"), by: who.name }, now);
|
|
3118
|
+
return saved.ok ? back() : refused(saved.message);
|
|
3119
|
+
}
|
|
3120
|
+
case "rerun-evidence": {
|
|
3121
|
+
const asked = rerunEvidence(store, taskId, who.name, now);
|
|
3122
|
+
return asked.ok ? back() : refused(asked.message);
|
|
3123
|
+
}
|
|
3124
|
+
case "failing-first-on":
|
|
3125
|
+
askFailingFirst(store, taskId, who.name, now);
|
|
3126
|
+
return back();
|
|
3127
|
+
case "failing-first-off":
|
|
3128
|
+
dropFailingFirst(store, taskId);
|
|
3129
|
+
return back();
|
|
3130
|
+
case "mark": {
|
|
3131
|
+
const capture = providerCapture(store, number("capture"));
|
|
3132
|
+
const label = body.get("label");
|
|
3133
|
+
if (capture === null || !runOnTask(store, capture.run, taskId) || (label !== "exhausted" && label !== "not-exhausted"))
|
|
3134
|
+
return refused("That capture isn't on this task.");
|
|
3135
|
+
labelProviderCapture(store, capture.id, label, who.name);
|
|
3136
|
+
return back();
|
|
3137
|
+
}
|
|
3138
|
+
default: return refused("Reload the page and try again.");
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3065
3141
|
return { registrations, runListPane, peekFragment, taskMutation, queueRegionFor, ciFailingFor, completedRowFor, attemptRowFor, resultRowOf, reviewCockpitViewOf, decisionEvidence, runEvidence, sendArtifact, peekGuards, liveRunIds };
|
|
3066
3142
|
}
|
|
3067
3143
|
/** Put away and Restore one task, and Put away old tasks (docs/plans/task-tidy.md): the preview reads only (GET-quiet);
|
|
@@ -527,6 +527,8 @@ export function modePageHtml(view) {
|
|
|
527
527
|
items: [["0", "Wait for a person"], ["1", "Approve plans that keep what I filed"]] })),
|
|
528
528
|
formRow("Approving from a chat app", "Plans that widen permissions, go past the attempt limit or touch protected paths still open Toolroll.", select({ name: "chat-approve", label: "Approving from a chat app", current: "",
|
|
529
529
|
items: [["", "Here, with my password"], ["1", "In my paired chat app, two taps"]] })),
|
|
530
|
+
formRow("Failed checks", "A repair stays inside the plan you approved. When it can't, or the tries run out, it comes to you.", select({ name: "repair-auto", label: "Failed checks", current: "",
|
|
531
|
+
items: [["", "Wait for me to start a repair"], ["1", "Repair them, up to 2 tries"]] })),
|
|
530
532
|
html `<div class="k-acts">${btn("Review the terms", { variant: "primary", type: "submit" })}</div>`,
|
|
531
533
|
]))
|
|
532
534
|
});
|
|
@@ -538,7 +540,7 @@ export function modeCeremonyHtml(view) {
|
|
|
538
540
|
return settingsSubPage("approval", `Sign the ${view.name} mode`, joinHtml([
|
|
539
541
|
settingsSection({ id: "mode-terms", title: `What you sign for ${view.project}`,
|
|
540
542
|
body: html `<ul class="k-ssec-lead">${view.words.map(words => html `<li>${words}</li>`)}</ul>` }),
|
|
541
|
-
kitSectionForm({ proof: view.chatApprove ? "mode-sign-chat-approve" : "mode-sign", action: "/mode/sign", id: "mode-sign-form", native: true, hidden: view.hidden,
|
|
543
|
+
kitSectionForm({ proof: view.repairAuto ? "mode-sign-repair" : view.chatApprove ? "mode-sign-chat-approve" : "mode-sign", action: "/mode/sign", id: "mode-sign-form", native: true, hidden: view.hidden,
|
|
542
544
|
save: { label: "Sign it" }, confirm: `Sign the ${view.name} mode for ${view.project}`, body: html `` }),
|
|
543
545
|
note(html `<a class="k-link" href="/mode">Back, and sign nothing</a>`),
|
|
544
546
|
]));
|
package/dist/settings-kit.js
CHANGED
|
@@ -151,11 +151,12 @@ function rowForForm(proof, action, hidden) {
|
|
|
151
151
|
for (const name of Object.keys(hidden))
|
|
152
152
|
if (row.field === name || row.secrets.includes(name))
|
|
153
153
|
throw new Error(`${row.id}: ${name} is never a hidden value`);
|
|
154
|
-
// The exact variant: its own selector must be in the hidden terms, and no other variant's may be
|
|
154
|
+
// The exact variant: its own selector must be in the hidden terms, and no other variant's may be, unless both are
|
|
155
|
+
// credential acts: two of those on one form ask the strongest proof there is, so nothing gets weaker.
|
|
155
156
|
if (row.select !== undefined && String(hidden[row.select.field] ?? "") !== row.select.value)
|
|
156
157
|
throw new Error(`${row.id} posts ${row.select.field}=${row.select.value}`);
|
|
157
158
|
for (const other of variantsOf(row))
|
|
158
|
-
if (other.select !== undefined && String(hidden[other.select.field] ?? "") === other.select.value)
|
|
159
|
+
if (other.select !== undefined && !(other.proof === "credential" && row.proof === "credential") && String(hidden[other.select.field] ?? "") === other.select.value)
|
|
159
160
|
throw new Error(`${action} with ${other.select.field}=${other.select.value} is ${other.id}, not ${row.id}`);
|
|
160
161
|
return row;
|
|
161
162
|
}
|
package/dist/settings-proofs.js
CHANGED
|
@@ -20,7 +20,7 @@ const CREDENTIAL = { passwordless: "refused", sso: "accepted" };
|
|
|
20
20
|
const TYPED = { passwordless: "refused", sso: "refused" };
|
|
21
21
|
const NONE = { passwordless: "n/a", sso: "n/a" };
|
|
22
22
|
/** /mode/sign re-derives the digest from the posted terms, so every one rides back. */
|
|
23
|
-
const MODE_TERMS = ["nonce", "digest", "expiry", "name", "days", "publication", "auto-approve", "plan-auto", "chat-approve"];
|
|
23
|
+
const MODE_TERMS = ["nonce", "digest", "expiry", "name", "days", "publication", "auto-approve", "plan-auto", "chat-approve", "repair-auto"];
|
|
24
24
|
const CHAT_APPS = [["slack", "Slack", "slack", ["app-token", "bot-token"]], ["teams", "Teams", "teams", ["secret"]], ["discord", "Discord", "discord", ["bot-token"]]];
|
|
25
25
|
export const PROOF_INVENTORY = [
|
|
26
26
|
// CSRF only.
|
|
@@ -72,11 +72,13 @@ export const PROOF_INVENTORY = [
|
|
|
72
72
|
{ id: "local-mode-off", action: "Turn off local mode", routes: ["/settings/local-mode/off"], form: "localModeOff", field: "password", proof: "step-up", ...ACCEPT, role: "operator", handlerRole: "a full session (never the local owner)", secrets: [], terms: [] },
|
|
73
73
|
{ id: "mode-sign", action: "Sign an operating mode", routes: ["/mode/sign"], form: "mode", field: "token", proof: "step-up", ...ACCEPT, role: "any", handlerRole: "an approver of the project", secrets: [], terms: MODE_TERMS },
|
|
74
74
|
{ id: "mode-sign-chat-approve", action: "Sign an operating mode that lets your chat app approve", routes: ["/mode/sign"], select: { field: "chat-approve", value: "1" }, form: "mode", field: "token", proof: "credential", ...CREDENTIAL, role: "any", handlerRole: "an approver of the project", secrets: [], terms: MODE_TERMS },
|
|
75
|
+
{ id: "mode-sign-repair", action: "Sign an operating mode that repairs failed checks by itself", routes: ["/mode/sign"], select: { field: "repair-auto", value: "1" }, form: "mode", field: "token", proof: "credential", ...CREDENTIAL, role: "any", handlerRole: "an approver of the project", secrets: [], terms: MODE_TERMS },
|
|
75
76
|
// Credential acts: a password typed now or a one-use re-check; never Passwordless.
|
|
76
77
|
{ id: "passwordless-on", action: "Turn Passwordless on", routes: ["/settings/passwordless"], form: "passwordless", field: "password", proof: "credential", when: "only to turn it on (off needs nothing)", ...CREDENTIAL, role: "operator", secrets: [], terms: [] },
|
|
77
78
|
{ id: "sign-in", action: "Change sign-in (SSO)", routes: ["/settings/sign-in"], form: "signIn", field: "password", proof: "credential", when: "every action except Test", ...CREDENTIAL, role: "operator", secrets: ["client-secret"], terms: [] },
|
|
78
79
|
{ id: "release-approval", action: "Approve releases automatically", routes: ["/settings/release-approval"], form: "releaseApproval", field: "password", proof: "credential", when: "only to approve automatically (Ask me needs nothing)", ...CREDENTIAL, role: "approver", handlerRole: "an approver of the project", secrets: [], terms: ["repo"] },
|
|
79
80
|
{ id: "project-network", action: "Change what builds may reach", routes: ["/settings/project/network"], form: "projectNetwork", field: "password", proof: "credential", ...CREDENTIAL, role: "any", handlerRole: "an approver of the project", secrets: [], terms: ["repo"] },
|
|
81
|
+
{ id: "project-fallback", action: "Change a project's fallback agents", routes: ["/settings/project/fallback"], form: "projectFallback", field: "password", proof: "credential", ...CREDENTIAL, role: "any", handlerRole: "an approver of the project", secrets: [], terms: [] },
|
|
80
82
|
{ id: "project-writes", action: "Change where builds may write", routes: ["/settings/project/writes"], form: "projectWrites", field: "password", proof: "credential", ...CREDENTIAL, role: "any", handlerRole: "an approver of the project", secrets: [], terms: ["repo"] },
|
|
81
83
|
{ id: "project-sandbox", action: "Change a project's sandbox", routes: ["/settings/project/sandbox"], form: "projectSandbox", field: "password", proof: "credential", ...CREDENTIAL, role: "any", handlerRole: "an approver of the project", secrets: [], terms: ["repo"] },
|
|
82
84
|
{ id: "project-check-access", action: "Narrow how far checks reach", routes: ["/settings/project/check-access"], form: "projectCheckAccess", field: "password", proof: "credential", ...CREDENTIAL, role: "any", handlerRole: "an approver of the project", secrets: [], terms: ["repo"], note: "for a form that can only narrow; one that offers a wider level uses project-check-access-widen" },
|
package/dist/setup-guide.d.ts
CHANGED
|
@@ -18,9 +18,9 @@ export declare function previewProjectInstructions(repo: string): {
|
|
|
18
18
|
ok: false;
|
|
19
19
|
message: string;
|
|
20
20
|
plan?: never;
|
|
21
|
+
content?: never;
|
|
21
22
|
fingerprint?: never;
|
|
22
23
|
installed?: never;
|
|
23
|
-
content?: never;
|
|
24
24
|
} | {
|
|
25
25
|
message?: never;
|
|
26
26
|
ok: true;
|
|
@@ -33,7 +33,7 @@ export declare function addProjectInstructions(repo: string, fingerprint: string
|
|
|
33
33
|
ok: false;
|
|
34
34
|
message: string;
|
|
35
35
|
plan?: never;
|
|
36
|
+
content?: never;
|
|
36
37
|
fingerprint?: never;
|
|
37
38
|
installed?: never;
|
|
38
|
-
content?: never;
|
|
39
39
|
} | import("./skills.js").InstallResult;
|
package/dist/slack-chat.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { slackCardBlocks } from "./card-render.js";
|
|
2
|
+
import { withoutCard } from "./chat-card-part.js";
|
|
3
|
+
import { cardWithin } from "./chat-card.js";
|
|
1
4
|
import { chatFlowButtons } from "./chat-flow.js";
|
|
2
5
|
import { refuseResultShots, resultShotsPruned, shotWaitsUntil } from "./result-shots.js";
|
|
3
6
|
import { chatQuestionButtons } from "./subagent-question.js";
|
|
@@ -14,6 +17,7 @@ import { readSlackBlockActions, readSlackButton, SLACK_LINK_ACTION } from "./con
|
|
|
14
17
|
import { object, slackId, slackTs, slackMember, SlackError, uploadSlackBytes, } from "./slack-api.js";
|
|
15
18
|
import { resultImageFileName, safeResultImageCaption, verifyResultImage, } from "./chat-evidence.js";
|
|
16
19
|
import {} from "./store.js";
|
|
20
|
+
const SLACK_REFUSED_BLOCKS = new Set(["invalid_blocks", "invalid_blocks_format", "invalid_arguments", "msg_too_long", "msg_blocks_too_long"]);
|
|
17
21
|
const nowOf = (options) => options.clock?.() ?? new Date();
|
|
18
22
|
const split = (text, size = 2800) => {
|
|
19
23
|
const parts = [];
|
|
@@ -214,6 +218,10 @@ export async function deliverSlackPart(options) {
|
|
|
214
218
|
if (content.task &&
|
|
215
219
|
!repos.includes(store.lookupRef(content.task)?.repo ?? ""))
|
|
216
220
|
throw new SlackError("Connected projects changed");
|
|
221
|
+
if (content.rich && !cardWithin(content.rich, repos)) {
|
|
222
|
+
state.dropPart(row.id, "Connected projects changed");
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
217
225
|
// A screenshot sent with a result goes in that result's thread once its message is placed, and waits until then.
|
|
218
226
|
const result = content.shot?.follows == null ? null : state.part(content.shot.follows);
|
|
219
227
|
if (result !== null && !slackTs(result.message) && result.state === "pending") {
|
|
@@ -385,7 +393,7 @@ export async function deliverSlackPart(options) {
|
|
|
385
393
|
.replaceAll("&", "&")
|
|
386
394
|
.replaceAll("<", "<")
|
|
387
395
|
.replaceAll(">", ">"),
|
|
388
|
-
blocks: voiced === null ? slackBlocks(text, buttons) : slackVoiceBlocks(voiced, buttons),
|
|
396
|
+
blocks: content.rich ? slackCardBlocks(content.rich, buttons) : voiced === null ? slackBlocks(text, buttons) : slackVoiceBlocks(voiced, buttons),
|
|
389
397
|
mrkdwn: voiced !== null,
|
|
390
398
|
parse: "none",
|
|
391
399
|
unfurl_links: false,
|
|
@@ -415,6 +423,11 @@ export async function deliverSlackPart(options) {
|
|
|
415
423
|
}
|
|
416
424
|
// No permission to upload files here: one plain line instead of the result's screenshots.
|
|
417
425
|
const saved = readChatPart(row.payload), content = saved.ok ? saved.value : null;
|
|
426
|
+
// Slack refused the card's blocks: the part's own plain text goes instead, so the message is never lost.
|
|
427
|
+
if (content?.rich && error instanceof SlackError && SLACK_REFUSED_BLOCKS.has(error.code)) {
|
|
428
|
+
state.repaint(row.id, withoutCard(content));
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
418
431
|
if (content !== null && content.image && content.shot && error instanceof SlackError && ["missing_scope", "no_permission"].includes(error.code)) {
|
|
419
432
|
refuseResultShots(state, store, row, content, "Slack", nowOf(options));
|
|
420
433
|
return true;
|
package/dist/steer-mailbox.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { Store } from "./store.js";
|
|
2
2
|
export declare const STEER_HANDLED = "handled";
|
|
3
|
+
/** Findings have their own folder: a finding is not operator steering, and must never read as it (findings.ts). */
|
|
4
|
+
export declare const STEER_FINDINGS = "findings";
|
|
5
|
+
/** What the agent writes back: one file per finding it answers. */
|
|
6
|
+
export declare const STEER_ANSWERS = "answers";
|
|
7
|
+
export declare const findingNoteName: (id: number) => string;
|
|
3
8
|
export type SteerMailbox = {
|
|
4
9
|
runId: number;
|
|
5
10
|
taskRef: number;
|
|
@@ -14,11 +19,13 @@ export type SteerMailbox = {
|
|
|
14
19
|
last: number;
|
|
15
20
|
/** Mailed and not yet moved to handled/: number → note id. */
|
|
16
21
|
waiting: Map<number, number>;
|
|
22
|
+
/** Findings mailed to this run and not yet moved to handled/. */
|
|
23
|
+
findingsWaiting: Set<number>;
|
|
17
24
|
};
|
|
18
25
|
export declare const steerNoteName: (number: number) => string;
|
|
19
26
|
/** Make the folder and its handled/ inside the worktree; null when it cannot be made. */
|
|
20
27
|
export declare function openSteerMailbox(worktree: string, folder: string, runId: number, taskRef: number, key: string): SteerMailbox | null;
|
|
21
|
-
/** One beat: record what the agent read, then mail what is new while the agent runs. */
|
|
28
|
+
/** One beat: record what the agent read and answered, then mail what is new while the agent runs. */
|
|
22
29
|
export declare function pulseSteerMailbox(store: Store, box: SteerMailbox, now: Date, agentRunning: boolean): void;
|
|
23
30
|
/** The attempt is over: one last read, then the folder goes. Unread notes stay pending for the next attempt. */
|
|
24
31
|
export declare function closeSteerMailbox(store: Store, box: SteerMailbox, now: Date): void;
|