toolroll 0.9.94 → 0.9.96
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 +17 -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 +43 -0
- package/dist/card-render.js +260 -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 +75 -19
- 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
package/dist/chat-actions.js
CHANGED
|
@@ -12,12 +12,24 @@ import { withActor } from "./actor.js";
|
|
|
12
12
|
import { TEXT_LIMITS } from "./text-limits.js";
|
|
13
13
|
import { publicChatText } from "./chat-display.js";
|
|
14
14
|
import { gateWords } from "./approval-policy.js";
|
|
15
|
+
import { checkLeadIdentity } from "./lead-identity.js";
|
|
16
|
+
import { hasForbiddenControls } from "./decision.js";
|
|
17
|
+
import { activeUnattendedGrant, candidatesDigestOf, FLOW_ACCEPTANCE, parseUnattendedAsk, revokeUnattended, sealUnattended, unattendedCandidates, unattendedSignProblem, untilWords, workReposOf } from "./flow-unattended.js";
|
|
18
|
+
import { UNATTENDED_RULE_WORDS, unattendedCoverWords, unattendedHeadWords, unattendedLimitWords } from "./flow-unattended-words.js";
|
|
19
|
+
import { CHECK_ACTION, checkFindingAt, cleanFindingText, familyInReach, FINDING_EXCERPT_LIMIT, FINDING_ROUND_LIMIT, FINDING_TEXT_LIMIT, findingLine, findingStandingWords, getFinding, resendFinding, REVIEW_ACTION, reviewFindingAt, sendCheckFinding, sendFinding, sendReviewFinding } from "./findings.js";
|
|
20
|
+
import { lessonNoteOf } from "./lesson-notes.js";
|
|
21
|
+
import { decideProposal, listProposals } from "./memory-pass.js";
|
|
22
|
+
import { authPauseOf, liftAuthPause } from "./provider-auth.js";
|
|
23
|
+
import { isProviderId } from "./provider.js";
|
|
24
|
+
import { approverStandsNow, familyRepoOf } from "./tidy.js";
|
|
25
|
+
import { chatApproveMode, effectiveChatApproval } from "./chat-approval.js";
|
|
26
|
+
import { cardTermsDigest, chatLimitProblem, scopeTermsText } from "./chat-terms.js";
|
|
15
27
|
import { createHash, randomBytes } from "node:crypto";
|
|
16
|
-
import { leadTurnKeepsProposals, verifiedAuthor } from "./store.js";
|
|
28
|
+
import { archiveTaskFamily, leadTurnKeepsProposals, restoreTaskFamily, verifiedAuthor } from "./store.js";
|
|
17
29
|
import { isVerifiedApprover, reproveApprover, } from "./principal.js";
|
|
18
30
|
import { changeSkills, importSkill, skillsView, skillsVersion, testSkill, validateSkill, } from "./project-skills.js";
|
|
19
31
|
import { changeKnowledge, knowledgeVersion, knowledgeView, } from "./project-knowledge.js";
|
|
20
|
-
import { passwordlessSessionFor, approve, authenticateApprover, describeScope, scopeAuthorityOf, freshnessFor, localApprovalEligible, spendingOnlyOnSuccess, ssoNowAvailable, withFreshness, } from "./scope.js";
|
|
32
|
+
import { passwordlessSessionFor, approve, authenticateApprover, describeScope, scopeAuthorityOf, machineActing, freshnessFor, localApprovalEligible, spendingOnlyOnSuccess, ssoNowAvailable, withFreshness, } from "./scope.js";
|
|
21
33
|
import { readVerifiedArtifact, scanForSecrets } from "./evidence.js";
|
|
22
34
|
import { readAuthModeStrict } from "./keys.js";
|
|
23
35
|
import { acceptAndCompleteAsOperator, assignmentOf } from "./assignment.js";
|
|
@@ -32,7 +44,7 @@ import { assignFlowCard, commentOnFlowCard, flowPeople, mentionsIn, watchFlowCar
|
|
|
32
44
|
import { browserViewOf, liveBrowserGrant, revokeFlowBrowser, signFlowBrowser } from "./flow-browser.js";
|
|
33
45
|
import { addFlowTriggerTo, describeTrigger, readLinearKey, removeFlowTrigger, takesDeliveries, triggerConfigOf, validateTriggerConfig } from "./flow-triggers.js";
|
|
34
46
|
import { describeSchedule, parseSchedule } from "./flow-schedule.js";
|
|
35
|
-
import { dirname } from "node:path";
|
|
47
|
+
import { basename, dirname } from "node:path";
|
|
36
48
|
import { handleOf, parseSoul, SOUL_CHARS, SUBAGENT_TEMPLATES, subagentLabel } from "./subagents.js";
|
|
37
49
|
import { createSubagentFrom, labelOf, nameOf, renamedSoul, saveSoul, setSubagentState } from "./subagent-admin.js";
|
|
38
50
|
import { cleanMemory, editMemory, forgetMemory, tellSubagent } from "./subagent-memory.js";
|
|
@@ -96,7 +108,7 @@ export const CHAT_ACTIONS = {
|
|
|
96
108
|
flow_starter: { label: "Switch on", protected: false, password: false },
|
|
97
109
|
// Lead fan-out (docs/plans/lead-fan-out.md): files N tasks under one brief and approves each one's scope — the
|
|
98
110
|
// person's ceremony (protected, with the password), N ordinary approvals of exactly the scopes shown.
|
|
99
|
-
sweep_file: { label: "Start sweep", protected: true, password:
|
|
111
|
+
sweep_file: { label: "Start sweep", protected: true, password: false },
|
|
100
112
|
// Stops every item of a sweep not yet ready; finished results stay. An ordinary act, like cancelling a task.
|
|
101
113
|
sweep_cancel_rest: { label: "Cancel the rest", protected: false, password: false },
|
|
102
114
|
flow_edit: { label: "Save flow", protected: false, password: false },
|
|
@@ -114,9 +126,9 @@ export const CHAT_ACTIONS = {
|
|
|
114
126
|
flow_trigger_pause: { label: "Pause trigger", protected: false, password: false },
|
|
115
127
|
flow_trigger_resume: { label: "Turn trigger on", protected: false, password: false },
|
|
116
128
|
flow_trigger_remove: { label: "Remove trigger", protected: false, password: false },
|
|
117
|
-
// Browser use widens a flow's sandbox (flow-browser.ts): the lead may only propose it; a person allows it with their
|
|
118
|
-
//
|
|
119
|
-
flow_browser: { label: "Allow browser", protected: true, password:
|
|
129
|
+
// Browser use widens a flow's sandbox (flow-browser.ts): the lead may only propose it; a person allows it with their own
|
|
130
|
+
// yes on a card that shows every term, in a chat they are signed in to or linked from. Turning it off lowers authority.
|
|
131
|
+
flow_browser: { label: "Allow browser", protected: true, password: false },
|
|
120
132
|
flow_browser_off: { label: "Turn off browser", protected: false, password: false },
|
|
121
133
|
subagent_create: { label: "Add subagent", protected: false, password: false },
|
|
122
134
|
subagent_soul: { label: "Update soul file", protected: false, password: false },
|
|
@@ -138,22 +150,47 @@ export const CHAT_ACTIONS = {
|
|
|
138
150
|
// The code map's switch and holdout (docs/plans/code-map.md §5): a person's yes on the card, re-proved in the write.
|
|
139
151
|
code_map_switch: { label: "Change code map", protected: false, password: false },
|
|
140
152
|
code_map_holdout_end: { label: "End holdout", protected: false, password: false },
|
|
153
|
+
// Chat and console parity (docs/plans/toolroll-cards.md): plain acts that lower authority or change no one's reach,
|
|
154
|
+
// each the console's own write. Ending a mode and turning a flow off its own only ever lower authority.
|
|
155
|
+
mode_revoke: { label: "End automatic approvals", protected: false, password: false },
|
|
156
|
+
flow_unattended_off: { label: "Stop running on its own", protected: false, password: false },
|
|
157
|
+
flow_archive: { label: "Archive flow", protected: false, password: false },
|
|
158
|
+
provider_resume: { label: "Resume agent", protected: false, password: false },
|
|
159
|
+
worker_note: { label: "Note on worker", protected: false, password: false },
|
|
160
|
+
lead_identity: { label: "Name your lead", protected: false, password: false },
|
|
161
|
+
task_archive: { label: "Put away task", protected: false, password: false },
|
|
162
|
+
task_restore: { label: "Restore task", protected: false, password: false },
|
|
163
|
+
incident_resolve: { label: "Resolve incident", protected: false, password: false },
|
|
164
|
+
memory_decide: { label: "Decide memory proposal", protected: false, password: false },
|
|
165
|
+
// Follow-through (findings.ts): not an approval. A person's yes to words shown on the card sends them to the agent.
|
|
166
|
+
finding_send: { label: "Send finding", protected: false, password: false },
|
|
167
|
+
finding_resend: { label: "Send finding back", protected: false, password: false },
|
|
168
|
+
// The rest of the approvals: a person's own yes on a card that shows every term.
|
|
169
|
+
task_reopen: { label: "Reopen task", protected: true, password: false },
|
|
170
|
+
revision_accept: { label: "Accept plan revision", protected: true, password: false },
|
|
171
|
+
revision_reject: { label: "Reject plan revision", protected: true, password: false },
|
|
172
|
+
flow_unattended: { label: "Let flow run on its own", protected: true, password: false },
|
|
141
173
|
// What the owner's own lead knows about them: theirs alone, in no project (lead-about.ts).
|
|
142
174
|
lead_about_you: { label: "Remember", protected: false, password: false },
|
|
143
|
-
scope_approve: { label: "Approve work", protected: true, password:
|
|
175
|
+
scope_approve: { label: "Approve work", protected: true, password: false },
|
|
144
176
|
result_accept: { label: "Accept", protected: true, password: false },
|
|
145
177
|
task_cancel: { label: "Cancel task", protected: true, password: false },
|
|
146
|
-
task_resume: { label: "Resume task", protected: true, password:
|
|
178
|
+
task_resume: { label: "Resume task", protected: true, password: false },
|
|
147
179
|
};
|
|
148
180
|
// Every action has a schema (contracts/chat-actions.ts) and every schema an action: a mismatch fails to compile.
|
|
149
181
|
const SAME_ACTIONS = true;
|
|
150
182
|
void SAME_ACTIONS;
|
|
151
|
-
/**
|
|
152
|
-
*
|
|
153
|
-
|
|
154
|
-
|
|
183
|
+
/** Approvals (owner's decision, docs/plans/toolroll-cards.md): a person's own yes from their chat stands without a password.
|
|
184
|
+
* Only a person's tap in a linked chat, or their click in a signed-in browser, ever runs one. */
|
|
185
|
+
export const APPROVAL_ACTIONS = new Set(["scope_approve", "task_resume", "sweep_file", "flow_browser", "task_reopen", "revision_accept", "revision_reject", "flow_unattended"]);
|
|
186
|
+
/** Protected actions a linked chat may confirm behind its own explicit yes/cancel challenge instead of the review screen.
|
|
187
|
+
* Password actions and long or redacted terms never qualify: the card must show the exact terms. */
|
|
188
|
+
export const APPROVAL_TERM_CHARS = 3000;
|
|
189
|
+
export const CHALLENGE_ACTIONS = new Set(["result_accept", ...APPROVAL_ACTIONS]);
|
|
155
190
|
/** Actions about the person themselves, in no project: their card carries no project (repo ""). */
|
|
156
|
-
export const OWNER_ACTIONS = new Set(["lead_about_you"]);
|
|
191
|
+
export const OWNER_ACTIONS = new Set(["lead_about_you", "provider_resume", "worker_note", "lead_identity"]);
|
|
192
|
+
/** Actions the console refuses a project-limited account (route table: limited "deny"): the same here. */
|
|
193
|
+
const INSTANCE_ACTIONS = new Set(["mode_revoke", "flow_unattended_off", "flow_unattended", "provider_resume", "worker_note", "lead_identity", "incident_resolve", "memory_decide"]);
|
|
157
194
|
export function isChatAction(value) {
|
|
158
195
|
return typeof value === "string" && Object.hasOwn(CHAT_ACTIONS, value);
|
|
159
196
|
}
|
|
@@ -267,6 +304,14 @@ export function prepareSharedAction(store, who, operation, input, root, now = ne
|
|
|
267
304
|
const read = readChatActionRequest(operation, input);
|
|
268
305
|
if (!read.ok)
|
|
269
306
|
throw Error(`This action can't be read — ${read.issues.map(issue => issue.line).join("; ")}.`);
|
|
307
|
+
if (INSTANCE_ACTIONS.has(operation) && store.accountOf(who.name)?.projects !== null)
|
|
308
|
+
throw Error("That needs an account with access to every project, as in the console.");
|
|
309
|
+
if (operation === "provider_resume" || operation === "worker_note" || operation === "lead_identity") {
|
|
310
|
+
requireActor(store, who, "", operation);
|
|
311
|
+
const { request, terms, state, title } = instanceAction(store, who, operation, input);
|
|
312
|
+
const stamp = hash({ operation, request, repo: "", state, terms, actor: who.name, generation: who.generation, ceiling: who.ceilingDigest });
|
|
313
|
+
return { version: SHARED_ACTION_VERSION, operation, request, repo: "", title, terms, stamp, state };
|
|
314
|
+
}
|
|
270
315
|
if (operation === "lead_about_you") {
|
|
271
316
|
// The owner's own note: the card shows the line, and the line it replaces beside it.
|
|
272
317
|
const checked = checkAboutYouLine(input["line"]);
|
|
@@ -289,7 +334,7 @@ export function prepareSharedAction(store, who, operation, input, root, now = ne
|
|
|
289
334
|
const stamp = hash({ operation, request, repo: "", state, terms, actor: who.name, generation: who.generation, ceiling: who.ceilingDigest });
|
|
290
335
|
return { version: SHARED_ACTION_VERSION, operation, request, repo: "", title, terms, stamp, state };
|
|
291
336
|
}
|
|
292
|
-
const task = operation.startsWith("code_map_") || operation.startsWith("skill_") || operation.startsWith("knowledge_") || operation.startsWith("decision_") || operation.startsWith("tool_") || operation.startsWith("flow_") || operation.startsWith("subagent_") || operation.startsWith("kit_") || operation.startsWith("sweep_") || operation.startsWith("suggestion_") || operation.startsWith("trial_")
|
|
337
|
+
const task = operation === "mode_revoke" || operation === "memory_decide" || operation === "incident_resolve" || operation === "finding_resend" || operation.startsWith("code_map_") || operation.startsWith("skill_") || operation.startsWith("knowledge_") || operation.startsWith("decision_") || operation.startsWith("tool_") || operation.startsWith("flow_") || operation.startsWith("subagent_") || operation.startsWith("kit_") || operation.startsWith("sweep_") || operation.startsWith("suggestion_") || operation.startsWith("trial_")
|
|
293
338
|
? null
|
|
294
339
|
: text(input, "task", 64);
|
|
295
340
|
const flowTarget = operation.startsWith("flow_") && operation !== "flow_create" && operation !== "flow_script_save" && operation !== "flow_starter" ? flowTargetOf(store, input) : null;
|
|
@@ -302,13 +347,21 @@ export function prepareSharedAction(store, who, operation, input, root, now = ne
|
|
|
302
347
|
if (operation === "subagent_answer" && (questionTarget === null || questionFlow === null))
|
|
303
348
|
throw Error("That question isn't open any more.");
|
|
304
349
|
const repo = operation === "sweep_cancel_rest" ? store.getSweep(integer(input, "sweep"))?.repo
|
|
305
|
-
:
|
|
350
|
+
: operation === "incident_resolve" ? incidentRepoOf(store, integer(input, "incident"))
|
|
351
|
+
: operation === "finding_resend" ? getFinding(store, integer(input, "finding"))?.repo
|
|
352
|
+
: flowTarget !== null ? flowTarget.flow.repo : subagentTarget !== null ? subagentTarget.repo : questionFlow !== null ? questionFlow.repo : task === null ? text(input, "repo", 4096) : store.lookupRef(task)?.repo;
|
|
306
353
|
if (!repo)
|
|
307
354
|
throw Error("Choose an available project.");
|
|
308
355
|
requireActor(store, who, repo);
|
|
309
356
|
const request = structuredClone(input), terms = [];
|
|
310
357
|
let state, title = CHAT_ACTIONS[operation].label;
|
|
311
|
-
if (
|
|
358
|
+
if (PARITY_ACTIONS.has(operation)) {
|
|
359
|
+
const made = parityAction(store, who, operation, input, repo, flowTarget?.flow ?? null, now, request, root);
|
|
360
|
+
state = made.state;
|
|
361
|
+
title = made.title;
|
|
362
|
+
terms.push(...made.terms);
|
|
363
|
+
}
|
|
364
|
+
else if (operation.startsWith("skill_")) {
|
|
312
365
|
const view = skillsView(store, repo, who.name);
|
|
313
366
|
state = {
|
|
314
367
|
identity: view.identity,
|
|
@@ -625,7 +678,9 @@ export function prepareSharedAction(store, who, operation, input, root, now = ne
|
|
|
625
678
|
const children = sweepChildScopes(store, who, repo, brief, cleaned.map(({ title, item }) => ({ title, item })), input["acceptance"], now);
|
|
626
679
|
state = { children: children.map(one => one.fingerprint) };
|
|
627
680
|
const tests = acceptance.map((one, index) => `Success test ${index + 1}${typeof one["id"] === "string" ? ` (${one["id"]})` : ""}: ${String(one["statement"] ?? "")}${Array.isArray(one["evidence"]) ? ` — shown by ${one["evidence"].map(String).join(", ")}` : ""}`);
|
|
628
|
-
terms.push(`${cleaned.length} tasks, each in its own worktree on its own branch, at most ${maxParallel} running at once, built directly (no separate planning step).`, `Shared rules for every item: ${brief}`, ...cleaned.map(one => `Item ${one.index + 1} — ${one.title}: ${one.item}`), `Each item's goal is the shared rules, then its own item, then: ${VERDICT_RULE}`, ...tests.map(line => `Every item — ${line}`), ...(children.every(one => one.runtime === children[0].runtime) ? [`Every item — ${children[0].runtime}`] : children.map((one, index) => `Item ${index + 1} — ${one.runtime}`)),
|
|
681
|
+
terms.push(`${cleaned.length} tasks, each in its own worktree on its own branch, at most ${maxParallel} running at once, built directly (no separate planning step).`, `Shared rules for every item: ${brief}`, ...cleaned.map(one => `Item ${one.index + 1} — ${one.title}: ${one.item}`), `Each item's goal is the shared rules, then its own item, then: ${VERDICT_RULE}`, ...tests.map(line => `Every item — ${line}`), ...(children.every(one => one.runtime === children[0].runtime) ? [`Every item — ${children[0].runtime}`] : children.map((one, index) => `Item ${index + 1} — ${one.runtime}`)),
|
|
682
|
+
// Each item's own terms, word for word: its yes is bound to this text.
|
|
683
|
+
...children.map((one, index) => `Item ${index + 1} in full:\n${one.terms}`), "Confirming files all of them and approves each one's scope exactly as shown. An item that needs a second person, or that the project's rules keep from its requester, is filed and waits for its own approval.", "Each ends with a verdict (fixed, not reproducible, blocked or done) and the sweep's table shows them as they land.");
|
|
629
684
|
}
|
|
630
685
|
else if (operation.startsWith("flow_")) {
|
|
631
686
|
const project = repo.split(/[\\/]/).filter(Boolean).at(-1) ?? repo;
|
|
@@ -749,7 +804,9 @@ export function prepareSharedAction(store, who, operation, input, root, now = ne
|
|
|
749
804
|
// The approval the person reads and the one their password signs: the digest is re-derived when they confirm.
|
|
750
805
|
state = { flow: flow.id, revision: flow.revision, digest: view.digest };
|
|
751
806
|
title = `Allow browser use in ${flow.name}`;
|
|
752
|
-
terms.push(`Flow: ${flow.name} · project ${project}`,
|
|
807
|
+
terms.push(`Flow: ${flow.name} · project ${project}`,
|
|
808
|
+
// Each script by its short digest: the whole approval, every script's full digest included, is bound in the state.
|
|
809
|
+
...view.zones.map(zone => `${zone.title} runs ${zone.script} v${zone.version}\nScript ${zone.digest.slice(0, 12)}`), "Each run of these zones starts a headless Chromium.", ...view.terms);
|
|
753
810
|
}
|
|
754
811
|
}
|
|
755
812
|
else if (operation === "flow_trigger_pause" || operation === "flow_trigger_resume" || operation === "flow_trigger_remove") {
|
|
@@ -988,8 +1045,9 @@ export function prepareSharedAction(store, who, operation, input, root, now = ne
|
|
|
988
1045
|
if (!authority.ok)
|
|
989
1046
|
throw Error(authority.problem);
|
|
990
1047
|
state["authMode"] = authority.authMode;
|
|
991
|
-
terms
|
|
992
|
-
|
|
1048
|
+
// The whole terms word for word (the yes is bound to this text: chat-terms.ts cardShowsScope), then everything else.
|
|
1049
|
+
terms.push(scopeTermsText(scope), ...describeScope(scope, undefined, taskEvidenceWords(store, scope.taskId, now))
|
|
1050
|
+
.filter((line) => !/^\s*(reference|approved|goal|not this|touches)\s/.test(line))
|
|
993
1051
|
.map((line) => line
|
|
994
1052
|
.trimStart()
|
|
995
1053
|
.replace(/^why model\s+/, "Why this model: ")
|
|
@@ -1051,6 +1109,9 @@ export function prepareSharedAction(store, who, operation, input, root, now = ne
|
|
|
1051
1109
|
terms.push(`Limitation: ${caveat}`);
|
|
1052
1110
|
}
|
|
1053
1111
|
}
|
|
1112
|
+
// An approval keeps the digest of the exact words its card shows, from now on: the yes is to these (scope.ts approve).
|
|
1113
|
+
if (APPROVAL_ACTIONS.has(operation))
|
|
1114
|
+
state["shown"] = cardTermsDigest(terms.join("\n"));
|
|
1054
1115
|
const stamp = hash({
|
|
1055
1116
|
operation,
|
|
1056
1117
|
request,
|
|
@@ -1071,10 +1132,13 @@ export function sharedActionPayload(value) {
|
|
|
1071
1132
|
/** A shortened or redacted preview is not complete consent. This rule lives
|
|
1072
1133
|
* in the shared door, so a forged transport callback cannot bypass it. */
|
|
1073
1134
|
function sharedActionContentNeedsReview(action) {
|
|
1135
|
+
// An approval's card has room for longer terms. Its words are compared exactly: anything the card would hide or
|
|
1136
|
+
// rewrite (a long hex value, a path, a secret) sends the person to the review page. Toolroll's own digest is bound in
|
|
1137
|
+
// the state, never in the terms, and a commit is written short, so an ordinary approval shows every word as stored.
|
|
1138
|
+
const approval = APPROVAL_ACTIONS.has(action.operation), cap = approval ? APPROVAL_TERM_CHARS : 1200;
|
|
1074
1139
|
const content = [action.title, ...action.terms];
|
|
1075
|
-
return (action.terms.join("\n").length >
|
|
1076
|
-
content.some((value, index) => publicChatText(value, index === 0 ? 200 :
|
|
1077
|
-
value.replace(/\s+/g, " ").trim()));
|
|
1140
|
+
return (action.terms.join("\n").length > cap ||
|
|
1141
|
+
content.some((value, index) => publicChatText(value, index === 0 ? 200 : cap) !== value.replace(/\s+/g, " ").trim()));
|
|
1078
1142
|
}
|
|
1079
1143
|
export function sharedActionNeedsReview(action) {
|
|
1080
1144
|
return (CHAT_ACTIONS[action.operation].protected ||
|
|
@@ -1161,8 +1225,10 @@ export function executeSharedAction(store, who, id, payload, now, options) {
|
|
|
1161
1225
|
if ((options.authority === "local-owner" || currentLocalOwner() !== null) && (!isChatAction(payload.operation) || CHAT_ACTION_LOCAL_ACTS[payload.operation] === null))
|
|
1162
1226
|
return refuse("refused", "Set a password to do this.");
|
|
1163
1227
|
let live;
|
|
1228
|
+
let drafted;
|
|
1164
1229
|
try {
|
|
1165
1230
|
const saved = savedActionContext(store, who, id, "confirming");
|
|
1231
|
+
drafted = saved;
|
|
1166
1232
|
if (hash(saved) !== hash(payload))
|
|
1167
1233
|
return refuse("refused", "This action does not match the saved proposal.");
|
|
1168
1234
|
live = prepareSharedAction(store, who, saved.operation, saved.request, options.root, now);
|
|
@@ -1176,31 +1242,24 @@ export function executeSharedAction(store, who, id, payload, now, options) {
|
|
|
1176
1242
|
}
|
|
1177
1243
|
// The review nonce was minted against the saved proposal's stamp: it is checked against that, whoever reviews.
|
|
1178
1244
|
const savedStamp = payload.stamp;
|
|
1179
|
-
let personProved = null;
|
|
1180
1245
|
payload = live;
|
|
1181
1246
|
const config = CHAT_ACTIONS[live.operation], req = live.request;
|
|
1182
1247
|
const challenged = options.via !== "web" &&
|
|
1183
1248
|
options.via !== "cli" &&
|
|
1184
1249
|
options.confirm === true &&
|
|
1185
1250
|
sharedActionAllowsChallenge(payload);
|
|
1251
|
+
if (APPROVAL_ACTIONS.has(live.operation) && machineActing())
|
|
1252
|
+
return refuse("needs-confirm", "Only a person approves this.");
|
|
1253
|
+
// A lead's or remote token reaches the same web door: an approval needs a person's own session (or tap, above).
|
|
1254
|
+
if (APPROVAL_ACTIONS.has(live.operation) && options.via === "web" && options.authority !== "local-owner" && options.person !== true)
|
|
1255
|
+
return refuse("needs-confirm", "Approve this yourself, in your signed-in chat.");
|
|
1186
1256
|
if (sharedActionNeedsReview(payload) && !challenged) {
|
|
1187
1257
|
if (options.via !== "web" || !options.review || options.confirm !== true)
|
|
1188
1258
|
return refuse("needs-confirm", "Review the complete action in the secure confirmation screen.");
|
|
1189
1259
|
if (!store.consumeCeremonyNonce(nonceHash(options.review.nonce), who.name, "chat-action", id, savedStamp, now))
|
|
1190
1260
|
return refuse("needs-confirm", "This confirmation expired or was already used. Open the review again.");
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
const emptyApproval = payload.operation === "scope_approve" && options.review.password === "";
|
|
1194
|
-
const proved = config.password && !emptyApproval ? authenticateApprover(store, who.name, options.review.password, payload.repo) : null;
|
|
1195
|
-
// How the person was proved, for an approval that records it (browser use: flow-browser.ts refuses any but a person's).
|
|
1196
|
-
if (proved?.ok === true)
|
|
1197
|
-
personProved = proved.method;
|
|
1198
|
-
if (config.password &&
|
|
1199
|
-
(emptyApproval
|
|
1200
|
-
? !passwordlessSessionFor(store, who.name) && !ssoNowAvailable(who.name) && withFreshness("approve-standard", () => freshnessFor(who.name)) === null
|
|
1201
|
-
// Easy-start C5: a local owner's eligible approval takes no password (approve() proves it again in the seal).
|
|
1202
|
-
&& !(options.authority === "local-owner" && currentLocalOwner()?.act === "approve-plan" && typeof req["task"] === "string" && localApprovalEligible(store, who.name, req["task"]).ok)
|
|
1203
|
-
: proved?.ok !== true))
|
|
1261
|
+
const proved = config.password ? authenticateApprover(store, who.name, options.review.password, payload.repo) : null;
|
|
1262
|
+
if (config.password && proved?.ok !== true)
|
|
1204
1263
|
return refuse("needs-confirm", "Confirm with your password in the secure screen.");
|
|
1205
1264
|
}
|
|
1206
1265
|
try {
|
|
@@ -1209,6 +1268,10 @@ export function executeSharedAction(store, who, id, payload, now, options) {
|
|
|
1209
1268
|
const act = () => store.savepoint(() => {
|
|
1210
1269
|
const task = typeof req["task"] === "string" ? req["task"] : null, repo = payload.repo, actor = who.name;
|
|
1211
1270
|
let taskId = task;
|
|
1271
|
+
if (LATE_APPROVALS.has(payload.operation))
|
|
1272
|
+
return runLateApproval(store, who, payload, repo => approvalBy(store, who, repo, now, options, drafted), approvalRoad(options), now);
|
|
1273
|
+
if (PARITY_ACTIONS.has(payload.operation) || OWNER_PARITY.has(payload.operation))
|
|
1274
|
+
return runParityAction(store, who, payload, now, options.root);
|
|
1212
1275
|
if (payload.operation === "lead_about_you") {
|
|
1213
1276
|
const next = withAboutYouLine(aboutYouOf(store, actor), String(req["line"]), Number(req["replaces"] ?? 0));
|
|
1214
1277
|
if (!next.ok)
|
|
@@ -1245,7 +1308,10 @@ export function executeSharedAction(store, who, id, payload, now, options) {
|
|
|
1245
1308
|
// re-reads the flow, its zones and scripts in this transaction and refuses unless they are what the card showed.
|
|
1246
1309
|
const flow = Number(payload.state["flow"]);
|
|
1247
1310
|
// signFlowBrowser re-reads the account's generation and reach in its transaction (it may have narrowed since).
|
|
1248
|
-
const
|
|
1311
|
+
const by = approvalBy(store, who, repo, now, options, drafted);
|
|
1312
|
+
if (by.ok === false)
|
|
1313
|
+
throw Error(by.why);
|
|
1314
|
+
const signed = signFlowBrowser(store, { flow, signer: actor, proof: options.via === "web" ? "session" : "chat", sawDigest: String(payload.state["digest"] ?? ""), console: { generation: who.generation } }, now);
|
|
1249
1315
|
if (!signed.ok)
|
|
1250
1316
|
throw Error(signed.message);
|
|
1251
1317
|
return { ok: true, taskId: null, said: "Browser allowed. Its browser zones can start Chromium now.", href: `/flows/${flow}` };
|
|
@@ -1265,7 +1331,7 @@ export function executeSharedAction(store, who, id, payload, now, options) {
|
|
|
1265
1331
|
return { ok: true, taskId: null, said, href: `/sweeps/${Number(req["sweep"])}` };
|
|
1266
1332
|
}
|
|
1267
1333
|
else if (payload.operation === "sweep_file") {
|
|
1268
|
-
const done = fileSweep(store, who, id, payload,
|
|
1334
|
+
const done = fileSweep(store, who, id, payload, approvalBy(store, who, payload.repo, now, options, drafted), now);
|
|
1269
1335
|
return { ok: true, taskId: null, said: done.said, href: done.href };
|
|
1270
1336
|
}
|
|
1271
1337
|
else if (payload.operation === "kit_setup") {
|
|
@@ -1336,7 +1402,16 @@ export function executeSharedAction(store, who, id, payload, now, options) {
|
|
|
1336
1402
|
else if (payload.operation === "scope_approve") {
|
|
1337
1403
|
const scope = store.getScope(task);
|
|
1338
1404
|
// Cockpit B2: classified by the act, not the road — the same standard-approval freshness as /t/:id/approve.
|
|
1339
|
-
const
|
|
1405
|
+
const by = approvalBy(store, who, repo, now, options, drafted);
|
|
1406
|
+
if (by.ok === false)
|
|
1407
|
+
throw Error(by.why);
|
|
1408
|
+
const limited = chatLimitProblem(scope, by.limits, "your chat approval setting");
|
|
1409
|
+
if (limited !== null)
|
|
1410
|
+
throw Error(limited);
|
|
1411
|
+
// A local owner keeps its own road (approve proves it again in the seal); everyone else, their own yes from chat,
|
|
1412
|
+
// to the scope and the words the card was made with.
|
|
1413
|
+
const sawDigest = String(drafted.state["scope"]?.digest ?? "");
|
|
1414
|
+
const result = withFreshness("approve-standard", () => approve(store, task, actor, now, sawDigest, options.review?.password ?? "", {}, undefined, by.chat));
|
|
1340
1415
|
if (!result.ok) {
|
|
1341
1416
|
// A first yes on protected work is kept, with how its person was proved: committed, never rolled back.
|
|
1342
1417
|
// The card stays open (`awaiting`) for the second approver; the same person again changes nothing.
|
|
@@ -1363,11 +1438,14 @@ export function executeSharedAction(store, who, id, payload, now, options) {
|
|
|
1363
1438
|
throw Error(`Task was not cancelled: ${result.reason}.`);
|
|
1364
1439
|
}
|
|
1365
1440
|
else if (payload.operation === "task_resume") {
|
|
1441
|
+
const by = approvalBy(store, who, repo, now, options, drafted);
|
|
1442
|
+
if (by.ok === false)
|
|
1443
|
+
throw Error(by.why);
|
|
1366
1444
|
const result = resumeTaskStop(store, {
|
|
1367
1445
|
taskId: task,
|
|
1368
1446
|
runId: Number(req["run"]),
|
|
1369
1447
|
by: verifiedAuthor(actor),
|
|
1370
|
-
via:
|
|
1448
|
+
via: options.via,
|
|
1371
1449
|
}, now);
|
|
1372
1450
|
if (!result.ok)
|
|
1373
1451
|
throw Error(result.detail);
|
|
@@ -1446,7 +1524,7 @@ function sweepChildScopeOf(store, taskId) {
|
|
|
1446
1524
|
// The fingerprint covers the scope's every field, not only what is rendered (a profile setting no sentence names, such
|
|
1447
1525
|
// as an agent's idle timeout, still makes the card stale): its exact terms with only identity left out.
|
|
1448
1526
|
const { taskId: _task, digest: _digest, approvedDigest: _approved, approvedBy: _by, approvedAt: _at, proposedAt: _proposed, approvalBasis: _basis, modeDigest: _mode, approvedProfile: _approvedProfile, approvedProfileJson: _approvedProfileJson, approvedChainJson: _approvedChain, ...terms } = scope;
|
|
1449
|
-
return { fingerprint: hash({ words, runtime, authMode: authority.authMode, profile: authority.profile, terms }), runtime, words };
|
|
1527
|
+
return { fingerprint: hash({ words, runtime, authMode: authority.authMode, profile: authority.profile, terms }), runtime, words, terms: scopeTermsText(scope) };
|
|
1450
1528
|
}
|
|
1451
1529
|
/** Every child as filing would make it, found in a savepoint that is always rolled back: prepare never writes. */
|
|
1452
1530
|
function sweepChildScopes(store, who, repo, brief, items, acceptance, now) {
|
|
@@ -1472,7 +1550,24 @@ function sweepChildScopes(store, who, repo, brief, items, acceptance, now) {
|
|
|
1472
1550
|
});
|
|
1473
1551
|
return found;
|
|
1474
1552
|
}
|
|
1475
|
-
|
|
1553
|
+
/** How this yes stands, and under whose limits: a person's own from chat, by their own chat setting, with the words and
|
|
1554
|
+
* digest the card was made with. A local owner's road has none: approve() proves it itself. */
|
|
1555
|
+
function approvalBy(store, who, repo, now, options, drafted) {
|
|
1556
|
+
if (options.authority === "local-owner")
|
|
1557
|
+
return { ok: true, limits: { fullAccess: true, capMicrousd: null } };
|
|
1558
|
+
if (options.via === "cli")
|
|
1559
|
+
return { ok: false, why: "Approve this in chat or in Toolroll." };
|
|
1560
|
+
const shown = { expect: String(drafted.state["shown"] ?? ""), terms: drafted.terms.join("\n") };
|
|
1561
|
+
if (options.via === "web") {
|
|
1562
|
+
const allowed = effectiveChatApproval(store, repo, who.name);
|
|
1563
|
+
return allowed.ok ? { ok: true, limits: allowed.limits, chat: { modeDigest: allowed.setting.digest, shown } } : { ok: false, why: allowed.why };
|
|
1564
|
+
}
|
|
1565
|
+
const allowed = chatApproveMode(store, repo, who.name, now, options.via);
|
|
1566
|
+
return allowed.ok ? { ok: true, limits: allowed.limits, chat: { modeDigest: allowed.digest, shown } } : allowed;
|
|
1567
|
+
}
|
|
1568
|
+
function fileSweep(store, who, proposal, payload, by, now) {
|
|
1569
|
+
if (by.ok === false)
|
|
1570
|
+
throw Error(by.why);
|
|
1476
1571
|
const req = payload.request;
|
|
1477
1572
|
const already = store.sweepOfProposal(proposal);
|
|
1478
1573
|
if (already !== null)
|
|
@@ -1495,7 +1590,9 @@ function fileSweep(store, who, proposal, payload, password, now) {
|
|
|
1495
1590
|
const waiting = [];
|
|
1496
1591
|
for (const one of filed) {
|
|
1497
1592
|
const scope = store.getScope(one.id);
|
|
1498
|
-
|
|
1593
|
+
// A person's own limits hold for each item: one past them is filed and waits for its own approval.
|
|
1594
|
+
const approved = scope === null || chatLimitProblem(scope, by.limits, "your chat approval setting") !== null ? null
|
|
1595
|
+
: withFreshness("approve-standard", () => approve(store, one.id, who.name, now, scope.digest, "", {}, undefined, by.chat));
|
|
1499
1596
|
if (approved === null || !approved.ok)
|
|
1500
1597
|
waiting.push(one.title);
|
|
1501
1598
|
}
|
|
@@ -1677,3 +1774,310 @@ function runFlowAction(store, payload, actor, repos, root, now) {
|
|
|
1677
1774
|
throw Error(acted.message);
|
|
1678
1775
|
return settle(card.flow, acted.said, card.id);
|
|
1679
1776
|
}
|
|
1777
|
+
// ---- chat and console parity (docs/plans/toolroll-cards.md) ------------------------------------------------------------
|
|
1778
|
+
const LATE_APPROVALS = new Set(["task_reopen", "revision_accept", "revision_reject", "flow_unattended"]);
|
|
1779
|
+
const PARITY_ACTIONS = new Set(["mode_revoke", "flow_unattended_off", "flow_archive", "task_archive", "task_restore", "incident_resolve", "memory_decide", "finding_send", "finding_resend", ...LATE_APPROVALS]);
|
|
1780
|
+
const OWNER_PARITY = new Set(["provider_resume", "worker_note", "lead_identity"]);
|
|
1781
|
+
function incidentRepoOf(store, id) {
|
|
1782
|
+
const open = store.openIncidents().find(one => one.id === id);
|
|
1783
|
+
return open === undefined ? null : store.lookupRef(open.taskId)?.repo ?? null;
|
|
1784
|
+
}
|
|
1785
|
+
function instanceAction(store, who, operation, input) {
|
|
1786
|
+
if (operation === "provider_resume") {
|
|
1787
|
+
const provider = text(input, "provider", 40);
|
|
1788
|
+
if (!isProviderId(provider))
|
|
1789
|
+
throw Error("Choose an agent provider.");
|
|
1790
|
+
const pause = authPauseOf(store, provider);
|
|
1791
|
+
if (pause === null)
|
|
1792
|
+
throw Error("That agent isn't paused.");
|
|
1793
|
+
// The pause is the whole installation's: every project with work waiting on it is named, and must be in reach here.
|
|
1794
|
+
const repos = [...new Set(store.handle.prepare(`SELECT t.repo AS repo FROM run r JOIN task_ref t ON t.id = r.task_ref WHERE r.provider = ? AND r.reason = 'auth-expired' AND r.finished_at >= ?
|
|
1795
|
+
UNION SELECT repo FROM task_ref WHERE auth_wait_pause = ?`).all(provider, pause.openedAt, pause.id).flatMap(row => row["repo"] === null ? [] : [String(row["repo"])]))].sort();
|
|
1796
|
+
if (repos.some(one => !who.repos.includes(one)))
|
|
1797
|
+
throw Error("That agent has work waiting in a project outside your access here.");
|
|
1798
|
+
return { request: { provider }, title: `Resume ${provider}`, state: { pause: pause.id, repos }, terms: [`Lifts the sign-in pause on ${provider} and sends its waiting tasks back to the queue. Existing approvals and holds still apply.`, ...(repos.length === 0 ? [] : [`Waiting work in: ${repos.map(one => basename(one)).join(", ")}`])] };
|
|
1799
|
+
}
|
|
1800
|
+
if (operation === "worker_note") {
|
|
1801
|
+
const runner = text(input, "runner", 120), note = String(input["note"] ?? "").trim();
|
|
1802
|
+
if (note.length > 200 || hasForbiddenControls(note))
|
|
1803
|
+
throw Error("A worker's note is at most 200 plain characters.");
|
|
1804
|
+
const worker = store.listRunners().find(one => one.name === runner && one.retiredAt === null);
|
|
1805
|
+
if (worker === undefined)
|
|
1806
|
+
throw Error("No such worker.");
|
|
1807
|
+
return { request: { runner, note }, title: `Note on ${runner}`, state: { runner }, terms: [note === "" ? "Clears this worker's note." : `Sets this worker's note to: ${note}`] };
|
|
1808
|
+
}
|
|
1809
|
+
const checked = checkLeadIdentity(String(input["name"] ?? ""), String(input["persona"] ?? ""));
|
|
1810
|
+
if (!checked.ok)
|
|
1811
|
+
throw Error(checked.message);
|
|
1812
|
+
return { request: { name: checked.identity.name, persona: checked.identity.persona }, title: `Call your lead ${checked.identity.name}`, state: {}, terms: [`Name: ${checked.identity.name}`, `Persona: ${checked.identity.persona}`] };
|
|
1813
|
+
}
|
|
1814
|
+
const REOPEN_REFUSED = {
|
|
1815
|
+
"unknown-task": "This task is not external work.", "not-latched": "The tracker never closed this: there is nothing to reopen.",
|
|
1816
|
+
"not-seen-open": "The tracker hasn't been seen open again since the close. Reopen it there first; the next sync notices.",
|
|
1817
|
+
claimed: "This task is being built right now.", held: "A hold stands: lift it first.", "question-open": "An unanswered question stands: answer or close it first.",
|
|
1818
|
+
"incident-open": "An unresolved incident stands: resolve it first.", "bad-state": "This task is not in a state reopen can take.",
|
|
1819
|
+
};
|
|
1820
|
+
/** The one plan revision waiting on a decision for this task, by its id or as the only one. */
|
|
1821
|
+
function waitingRevision(store, task, asked) {
|
|
1822
|
+
const ref = store.lookupRef(task);
|
|
1823
|
+
const waiting = ref === null ? [] : store.listPlanRevisions(ref.id).filter(one => one.status === "blocked");
|
|
1824
|
+
const found = asked === undefined ? (waiting.length === 1 ? waiting[0] : null) : waiting.find(one => one.id === asked) ?? null;
|
|
1825
|
+
if (found === null)
|
|
1826
|
+
throw Error("That plan revision isn't waiting on a decision.");
|
|
1827
|
+
return found;
|
|
1828
|
+
}
|
|
1829
|
+
/** A finding follows its task's family, so every project the family reaches must be in reach (findings.ts, one gate). */
|
|
1830
|
+
function familyWithin(store, who, task) {
|
|
1831
|
+
if (familyInReach(store, who, task) === null)
|
|
1832
|
+
throw Error("This task's work reaches a project outside your access here.");
|
|
1833
|
+
}
|
|
1834
|
+
function parityAction(store, who, operation, input, repo, flow, now, request, root) {
|
|
1835
|
+
switch (operation) {
|
|
1836
|
+
case "finding_send": {
|
|
1837
|
+
const task = String(input["task"]), row = store.getTask(task);
|
|
1838
|
+
if (row === null)
|
|
1839
|
+
throw Error("No such task.");
|
|
1840
|
+
familyWithin(store, who, task);
|
|
1841
|
+
const given = ["summary", "pointer", "run"].filter(key => input[key] !== undefined);
|
|
1842
|
+
if (given.length !== 1)
|
|
1843
|
+
throw Error("Send one thing: words in summary, a review finding's pointer, or a failed check's run.");
|
|
1844
|
+
const head = `Sends this to the agent on ${row.title}. It reaches a running build now, or the next attempt's brief, as data to check. The agent must accept it or reject it with a reason; a repeat counts as another round.`;
|
|
1845
|
+
if (input["pointer"] !== undefined) {
|
|
1846
|
+
const read = reviewFindingAt(store, task, text(input, "pointer", 40));
|
|
1847
|
+
if (!read.ok)
|
|
1848
|
+
throw Error(read.message);
|
|
1849
|
+
const place = read.finding.file === null ? [] : [`Where: ${read.finding.file}${read.finding.line === null ? "" : `:${read.finding.line}`}`];
|
|
1850
|
+
return { state: { task, pointer: input["pointer"], finding: read.finding }, title: "Send a review finding", terms: [head, `Finding: ${cleanFindingText(read.finding.scenario, FINDING_TEXT_LIMIT)}`, ...place, `Severity: ${read.finding.severity}`, `Asks: ${REVIEW_ACTION}`] };
|
|
1851
|
+
}
|
|
1852
|
+
if (input["run"] !== undefined) {
|
|
1853
|
+
if (root === undefined)
|
|
1854
|
+
throw Error("The check's log can't be read here.");
|
|
1855
|
+
const read = checkFindingAt(store, root, task, integer(input, "run"));
|
|
1856
|
+
if (!read.ok)
|
|
1857
|
+
throw Error(read.message);
|
|
1858
|
+
return { state: { task, run: input["run"], excerpt: read.excerpt, head: read.head }, title: "Send a failed check", terms: [head, `Finding: ${read.summary}`, `Log: ${cleanFindingText(read.excerpt, FINDING_EXCERPT_LIMIT)}`, `Asks: ${CHECK_ACTION}`] };
|
|
1859
|
+
}
|
|
1860
|
+
const summary = cleanFindingText(text(input, "summary", FINDING_TEXT_LIMIT), FINDING_TEXT_LIMIT);
|
|
1861
|
+
if (summary === "")
|
|
1862
|
+
throw Error("Say what the finding is.");
|
|
1863
|
+
const file = input["file"] === undefined ? null : cleanFindingText(text(input, "file", 300), 300) || null;
|
|
1864
|
+
const line = file === null || input["line"] === undefined ? null : integer(input, "line");
|
|
1865
|
+
for (const key of ["file", "line"])
|
|
1866
|
+
delete request[key];
|
|
1867
|
+
request["summary"] = summary;
|
|
1868
|
+
if (file !== null)
|
|
1869
|
+
request["file"] = file;
|
|
1870
|
+
if (line !== null)
|
|
1871
|
+
request["line"] = line;
|
|
1872
|
+
return { state: { task, updatedAt: row.updatedAt }, title: "Send a finding", terms: [head, `Finding: ${summary}`, ...(file === null ? [] : [`Where: ${file}${line === null ? "" : `:${line}`}`]), `Asks: ${REVIEW_ACTION}`] };
|
|
1873
|
+
}
|
|
1874
|
+
case "finding_resend": {
|
|
1875
|
+
const finding = getFinding(store, integer(input, "finding"));
|
|
1876
|
+
if (finding === null)
|
|
1877
|
+
throw Error("No such finding.");
|
|
1878
|
+
familyWithin(store, who, finding.taskId);
|
|
1879
|
+
if (finding.state === "open")
|
|
1880
|
+
throw Error("That finding is already with the agent.");
|
|
1881
|
+
if (finding.state === "closed")
|
|
1882
|
+
throw Error("That finding is closed.");
|
|
1883
|
+
if (finding.state === "with-person")
|
|
1884
|
+
throw Error("That finding has used its rounds: it needs your decision on the task.");
|
|
1885
|
+
const next = finding.rounds >= FINDING_ROUND_LIMIT ? "Its rounds are used, so this hands it to a person on the task." : `This is round ${finding.rounds + 1} of ${FINDING_ROUND_LIMIT}.`;
|
|
1886
|
+
return { state: { finding: finding.id, state: finding.state, rounds: finding.rounds, answer: finding.answer?.detail ?? null }, title: `Send finding #${finding.id} back`, terms: [`Finding: ${findingLine(finding)}`, `How it stands: ${findingStandingWords(finding)}`, next] };
|
|
1887
|
+
}
|
|
1888
|
+
case "task_reopen": {
|
|
1889
|
+
const task = String(input["task"]), row = store.getTask(task);
|
|
1890
|
+
if (row === null)
|
|
1891
|
+
throw Error("No such task.");
|
|
1892
|
+
if (store.mirrorByTask(task) === null)
|
|
1893
|
+
throw Error(REOPEN_REFUSED["unknown-task"]);
|
|
1894
|
+
return { state: { task, updatedAt: row.updatedAt, taskState: row.state }, title: `Reopen ${row.title}`, terms: ["Takes this outside work back up: the tracker shows it open again, and Toolroll may build it. Existing approvals and holds still apply."] };
|
|
1895
|
+
}
|
|
1896
|
+
case "revision_accept":
|
|
1897
|
+
case "revision_reject": {
|
|
1898
|
+
const task = String(input["task"]), revision = waitingRevision(store, task, input["revision"]);
|
|
1899
|
+
request["revision"] = revision.id;
|
|
1900
|
+
const artifact = store.getArtifact(revision.artifact);
|
|
1901
|
+
if (artifact === null || root === undefined)
|
|
1902
|
+
throw Error("The revised plan can't be read here.");
|
|
1903
|
+
const read = readVerifiedArtifact(root, artifact);
|
|
1904
|
+
if (!read.ok || artifact.truncated || artifact.redacted)
|
|
1905
|
+
throw Error("The revised plan couldn't be verified in full.");
|
|
1906
|
+
return { state: { task, revision: revision.id, artifact: artifact.sha256, authority: revision.authorityDigest }, title: `${operation === "revision_accept" ? "Accept" : "Reject"} revision ${revision.revision} of this plan`,
|
|
1907
|
+
terms: [`Why the builder asked: ${revision.reason}`, `Changes: ${revision.changedFields.join(", ") || "the plan only"}${revision.authorityKind === "authority-change" ? ". This changes what was approved." : "."}`, `Plan:\n${read.content.toString("utf8")}`] };
|
|
1908
|
+
}
|
|
1909
|
+
case "flow_unattended": {
|
|
1910
|
+
const asked = parseUnattendedAsk({ days: input["days"], budget: input["budget"], runs: input["runs"], include: input["include"] });
|
|
1911
|
+
if (!asked.ok)
|
|
1912
|
+
throw Error(asked.message);
|
|
1913
|
+
const problem = unattendedSignProblem(store, flow, who.name);
|
|
1914
|
+
if (problem !== null)
|
|
1915
|
+
throw Error(problem);
|
|
1916
|
+
const candidates = unattendedCandidates(store, flow.id), include = asked.ask.include ?? [];
|
|
1917
|
+
const covered = candidates.filter(one => one.optional === null || include.includes(one.id));
|
|
1918
|
+
if (include.some(id => !candidates.some(one => one.id === id)))
|
|
1919
|
+
throw Error("Name schedules from this flow's own list.");
|
|
1920
|
+
if (covered.length === 0)
|
|
1921
|
+
throw Error("Choose a schedule to include: this flow's schedules bring in outside content, so none is covered unless you pick it.");
|
|
1922
|
+
// Every project its work files in: the person must reach each, and each is named and checked at the yes.
|
|
1923
|
+
const repos = [...new Set([flow.repo, ...workReposOf(flow)])];
|
|
1924
|
+
if (repos.some(one => !who.repos.includes(one) || !store.accountCanAccess(who.name, one)))
|
|
1925
|
+
throw Error("This flow files work in a project outside your access here.");
|
|
1926
|
+
return { state: { flow: flow.id, revision: flow.revision, schedules: candidatesDigestOf(candidates), ask: asked.ask, repos }, title: `Let ${flow.name} run on its own`,
|
|
1927
|
+
terms: [...unattendedHeadWords(who.name, FLOW_ACCEPTANCE), `Works in: ${repos.map(one => basename(one)).join(", ")}`, unattendedCoverWords(covered), ...covered.flatMap(one => one.optional === null ? [] : [`${one.words}: ${one.optional}`]), ...unattendedLimitWords(asked.ask.budgetMicrousd, asked.ask.dailyRunCap), ...UNATTENDED_RULE_WORDS, `It ends after ${asked.ask.days} ${asked.ask.days === 1 ? "day" : "days"}.`] };
|
|
1928
|
+
}
|
|
1929
|
+
case "mode_revoke": {
|
|
1930
|
+
const mode = store.activeMode(repo, now);
|
|
1931
|
+
if (mode === null)
|
|
1932
|
+
throw Error("No automatic approvals are on for this project.");
|
|
1933
|
+
return { state: { mode: mode.id, digest: mode.digest }, title: "End automatic approvals", terms: [`Ends the ${mode.name} mode signed by ${mode.signedBy}. Every act goes back to its own approval.`] };
|
|
1934
|
+
}
|
|
1935
|
+
case "flow_unattended_off": {
|
|
1936
|
+
const grant = activeUnattendedGrant(store, flow.id, now);
|
|
1937
|
+
if (grant === null)
|
|
1938
|
+
throw Error(`${flow.name} isn't running on its own.`);
|
|
1939
|
+
return { state: { flow: flow.id, grant: grant.id }, title: `Stop ${flow.name} running on its own`, terms: ["New cards wait for a person again."] };
|
|
1940
|
+
}
|
|
1941
|
+
case "flow_archive":
|
|
1942
|
+
return { state: { flow: flow.id, revision: flow.revision }, title: `Archive ${flow.name}`, terms: ["Takes this flow off the list. Its triggers stop starting cards."] };
|
|
1943
|
+
case "task_archive": {
|
|
1944
|
+
const task = String(input["task"]), row = store.getTask(task), reason = text(input, "reason", 500, true).trim();
|
|
1945
|
+
if (row === null)
|
|
1946
|
+
throw Error("No such task.");
|
|
1947
|
+
if (reason === "")
|
|
1948
|
+
delete request["reason"];
|
|
1949
|
+
else
|
|
1950
|
+
request["reason"] = reason;
|
|
1951
|
+
return { state: { task, updatedAt: row.updatedAt, state: row.state }, title: `Put away ${row.title}`, terms: ["Takes this task out of every list. It stays saved and can be restored.", ...(reason === "" ? [] : [`Reason: ${reason}`])] };
|
|
1952
|
+
}
|
|
1953
|
+
case "task_restore": {
|
|
1954
|
+
const task = String(input["task"]), row = store.getTask(task);
|
|
1955
|
+
if (row === null)
|
|
1956
|
+
throw Error("No such task.");
|
|
1957
|
+
return { state: { task, updatedAt: row.updatedAt }, title: `Restore ${row.title}`, terms: ["Puts this task back in the lists."] };
|
|
1958
|
+
}
|
|
1959
|
+
case "incident_resolve": {
|
|
1960
|
+
const id = integer(input, "incident"), open = store.openIncidents().find(one => one.id === id);
|
|
1961
|
+
return { state: { incident: id }, title: `Resolve the incident on ${open.taskId}`, terms: ["Marks this incident resolved. Nothing else about the task changes."] };
|
|
1962
|
+
}
|
|
1963
|
+
case "memory_decide": {
|
|
1964
|
+
const id = integer(input, "proposal"), decision = input["decision"] === "accept" ? "accept" : "reject";
|
|
1965
|
+
const proposal = listProposals(store, repo).find(one => one.id === id);
|
|
1966
|
+
if (proposal === undefined)
|
|
1967
|
+
throw Error("That proposal isn't waiting any more.");
|
|
1968
|
+
// A lesson note is accepted in the person's own words (memory-pass.ts): never a model's, so never from a card.
|
|
1969
|
+
if (decision === "accept" && lessonNoteOf(store, id) !== null)
|
|
1970
|
+
throw Error("A lesson note is accepted in your own words: write it on the Knowledge page.");
|
|
1971
|
+
return { state: { proposal: id }, title: `${decision === "accept" ? "Accept" : "Reject"} a memory proposal`, terms: [proposal.title, ...(proposal.beforeText === null ? [] : [`Was: ${proposal.beforeText}`]), `Becomes: ${proposal.afterText}`, `Why: ${proposal.rationale}`, decision === "accept" ? "Later agent sessions in this project read this." : "Nothing is added to project memory."] };
|
|
1972
|
+
}
|
|
1973
|
+
default:
|
|
1974
|
+
throw Error("Choose an available action.");
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
/** Where a yes came from, in the words the other chat approvals' ledger lines use. */
|
|
1978
|
+
const approvalRoad = (options) => `via ${options.via}${options.binding === undefined ? "" : ` · chat binding #${options.binding}`}`;
|
|
1979
|
+
/** The approvals added last: the console's own write, after the same question every approval asks. */
|
|
1980
|
+
function runLateApproval(store, who, payload, byFor, road, now) {
|
|
1981
|
+
const req = payload.request, actor = who.name, by = byFor(payload.repo);
|
|
1982
|
+
// Rejecting lowers: it takes a person's yes, not their approving setting.
|
|
1983
|
+
if (by.ok === false && payload.operation !== "revision_reject")
|
|
1984
|
+
throw Error(by.why);
|
|
1985
|
+
if (payload.operation === "task_reopen") {
|
|
1986
|
+
const task = String(req["task"]), reopened = store.reopenMirror(task, actor, now);
|
|
1987
|
+
if (!reopened.ok)
|
|
1988
|
+
throw Error(REOPEN_REFUSED[reopened.reason] ?? "The task could not be reopened.");
|
|
1989
|
+
return { ok: true, taskId: task, said: "Reopened." };
|
|
1990
|
+
}
|
|
1991
|
+
if (payload.operation === "flow_unattended") {
|
|
1992
|
+
const ask = payload.state["ask"];
|
|
1993
|
+
// The person's own setting holds in every project this flow's work files in, not only its own.
|
|
1994
|
+
for (const repo of payload.state["repos"]) {
|
|
1995
|
+
const here = byFor(repo);
|
|
1996
|
+
if (here.ok === false)
|
|
1997
|
+
throw Error(here.why);
|
|
1998
|
+
if (here.limits.capMicrousd !== null && (ask.budgetMicrousd === null || ask.budgetMicrousd > here.limits.capMicrousd))
|
|
1999
|
+
throw Error("This has a higher attempt limit than your chat approval setting allows.");
|
|
2000
|
+
}
|
|
2001
|
+
const signed = sealUnattended(store, { flow: Number(req["flow"]), signer: actor, ask, sawRevision: Number(payload.state["revision"]), sawSchedules: String(payload.state["schedules"]), road }, now);
|
|
2002
|
+
if (!signed.ok)
|
|
2003
|
+
throw Error(signed.message);
|
|
2004
|
+
return { ok: true, taskId: null, said: `Runs on its own until ${untilWords(signed.grant.absoluteExpiry)}.`, href: `/flows/${Number(req["flow"])}` };
|
|
2005
|
+
}
|
|
2006
|
+
const task = String(req["task"]), revision = waitingRevision(store, task, req["revision"]), accept = payload.operation === "revision_accept";
|
|
2007
|
+
// A saved limit can't be read against a change of authority: that one opens in Toolroll.
|
|
2008
|
+
if (accept && by.ok && revision.authorityKind === "authority-change" && (!by.limits.fullAccess || by.limits.capMicrousd !== null))
|
|
2009
|
+
throw Error("This revision changes what was approved, beyond your chat approval limits. Open it in Toolroll.");
|
|
2010
|
+
if (!store.resolvePlanRevision(revision.id, accept ? "applied" : "rejected", actor, now))
|
|
2011
|
+
throw Error("That plan revision is no longer waiting on a decision.");
|
|
2012
|
+
store.releaseOwnedHold("revision", String(revision.id));
|
|
2013
|
+
return { ok: true, taskId: task, said: accept ? "Revision accepted." : "Revision rejected." };
|
|
2014
|
+
}
|
|
2015
|
+
/** Each runs the write the console's own handler calls, as this person, inside the confirm door's transaction. */
|
|
2016
|
+
function runParityAction(store, who, payload, now, root) {
|
|
2017
|
+
const req = payload.request, actor = who.name, repo = payload.repo;
|
|
2018
|
+
const done = (said, href, taskId = null) => ({ ok: true, taskId, said, ...(href === undefined ? {} : { href }) });
|
|
2019
|
+
switch (payload.operation) {
|
|
2020
|
+
case "finding_send":
|
|
2021
|
+
case "finding_resend": {
|
|
2022
|
+
const task = payload.operation === "finding_send" ? String(req["task"]) : getFinding(store, Number(req["finding"]))?.taskId ?? null;
|
|
2023
|
+
const sent = payload.operation === "finding_resend" ? resendFinding(store, Number(req["finding"]), now)
|
|
2024
|
+
: req["pointer"] !== undefined ? sendReviewFinding(store, task, String(req["pointer"]), actor, now)
|
|
2025
|
+
: req["run"] !== undefined ? sendCheckFinding(store, root ?? "", task, Number(req["run"]), actor, now)
|
|
2026
|
+
// The lead drafted these words and the person confirmed them: recorded as the lead's, sent by the person.
|
|
2027
|
+
: sendFinding(store, { taskId: task, source: "lead", by: actor, summary: String(req["summary"]), file: typeof req["file"] === "string" ? req["file"] : null, line: typeof req["line"] === "number" ? req["line"] : null }, now);
|
|
2028
|
+
if (!sent.ok)
|
|
2029
|
+
throw Error(sent.message);
|
|
2030
|
+
const said = sent.standing === "with-person" ? `Finding #${sent.id} needs a person's decision on the task.` : sent.standing === "already-sent" ? `Finding #${sent.id} was already sent.`
|
|
2031
|
+
: sent.standing === "running" ? `Finding #${sent.id} sent to the running build.` : `Finding #${sent.id} goes to the next attempt.`;
|
|
2032
|
+
return done(said, task === null ? undefined : `/t/${encodeURIComponent(task)}#findings`, task);
|
|
2033
|
+
}
|
|
2034
|
+
case "mode_revoke":
|
|
2035
|
+
if (!store.revokeMode(repo, actor, "operator", now))
|
|
2036
|
+
throw Error("That mode already ended.");
|
|
2037
|
+
return done("Automatic approvals are off. Every act goes back to its own approval.", "/mode");
|
|
2038
|
+
case "flow_unattended_off":
|
|
2039
|
+
if (!revokeUnattended(store, Number(req["flow"]), actor, now))
|
|
2040
|
+
throw Error("It wasn't running on its own.");
|
|
2041
|
+
return done("It no longer runs on its own. New cards wait for a person.", `/flows/${Number(req["flow"])}`);
|
|
2042
|
+
case "flow_archive":
|
|
2043
|
+
store.archiveFlow(Number(req["flow"]), actor, now);
|
|
2044
|
+
return done("Flow archived.", "/flows");
|
|
2045
|
+
case "task_archive":
|
|
2046
|
+
case "task_restore": {
|
|
2047
|
+
const task = String(req["task"]);
|
|
2048
|
+
const standing = (refs) => approverStandsNow(store, actor, familyRepoOf(store, refs));
|
|
2049
|
+
const made = payload.operation === "task_archive"
|
|
2050
|
+
? archiveTaskFamily(store, task, { by: actor, byKind: "person", rule: null, reason: String(req["reason"] ?? "").trim() || "Put away from chat" }, now, { guard: standing })
|
|
2051
|
+
: restoreTaskFamily(store, task, actor, now, standing);
|
|
2052
|
+
if (!made.ok)
|
|
2053
|
+
throw Error(made.message);
|
|
2054
|
+
return done(payload.operation === "task_archive" ? "Put away." : "Restored.", undefined, task);
|
|
2055
|
+
}
|
|
2056
|
+
case "incident_resolve":
|
|
2057
|
+
if (!store.resolveIncident(Number(req["incident"]), actor, now))
|
|
2058
|
+
throw Error("That incident was already resolved.");
|
|
2059
|
+
return done("Incident resolved.");
|
|
2060
|
+
case "memory_decide": {
|
|
2061
|
+
const decision = req["decision"] === "accept" ? "accept" : "reject";
|
|
2062
|
+
decideProposal(store, { repo, actor, id: Number(req["proposal"]), decision }, now);
|
|
2063
|
+
return done(decision === "accept" ? "Added to project memory." : "Rejected.", `/settings/knowledge?repo=${encodeURIComponent(repo)}`);
|
|
2064
|
+
}
|
|
2065
|
+
case "provider_resume": {
|
|
2066
|
+
const lifted = liftAuthPause(store, String(req["provider"]), "person", actor, now);
|
|
2067
|
+
if (lifted === null)
|
|
2068
|
+
throw Error("That agent isn't paused any more.");
|
|
2069
|
+
return done(`Resumed. ${lifted.resumed} ${lifted.resumed === 1 ? "task went" : "tasks went"} back to the queue.`);
|
|
2070
|
+
}
|
|
2071
|
+
case "worker_note": {
|
|
2072
|
+
const note = String(req["note"]);
|
|
2073
|
+
if (!store.setRunnerQueueNote(String(req["runner"]), note === "" ? null : note).ok)
|
|
2074
|
+
throw Error("No such worker.");
|
|
2075
|
+
return done("Note saved.", "/fleet");
|
|
2076
|
+
}
|
|
2077
|
+
case "lead_identity":
|
|
2078
|
+
store.setLeadConfig(actor, String(req["name"]), String(req["persona"]), now);
|
|
2079
|
+
return done(`Your lead is called ${String(req["name"])} now.`, "/settings/lead");
|
|
2080
|
+
default:
|
|
2081
|
+
throw Error("Choose an available action.");
|
|
2082
|
+
}
|
|
2083
|
+
}
|