toolroll 0.9.63 → 0.9.67
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 +108 -0
- package/README.md +33 -5
- package/dist/acp-agents.d.ts +43 -0
- package/dist/acp-agents.js +62 -0
- package/dist/acp-attest.d.ts +44 -0
- package/dist/acp-attest.js +269 -0
- package/dist/acp-check.d.ts +28 -0
- package/dist/acp-check.js +410 -0
- package/dist/acp-client.d.ts +163 -0
- package/dist/acp-client.js +738 -0
- package/dist/acp-inspect.d.ts +23 -0
- package/dist/acp-inspect.js +109 -0
- package/dist/agent-board.d.ts +19 -6
- package/dist/agent-board.js +30 -9
- package/dist/agent-fence.d.ts +117 -2
- package/dist/agent-fence.js +439 -12
- package/dist/agent-pulse.js +9 -2
- package/dist/agent-state-words.d.ts +19 -0
- package/dist/agent-state-words.js +20 -0
- package/dist/agent-state.d.ts +65 -0
- package/dist/agent-state.js +229 -0
- package/dist/agentconfig.js +39 -13
- package/dist/agents-board.d.ts +34 -2
- package/dist/agents-board.js +110 -1
- package/dist/approval-rules-ui.js +2 -2
- package/dist/assignment-delivery.d.ts +14 -0
- package/dist/assignment-delivery.js +13 -0
- package/dist/assignment.js +13 -0
- package/dist/attest.js +7 -0
- package/dist/backup-ui.js +2 -2
- package/dist/batch-checks.js +3 -2
- package/dist/browser/THIRD_PARTY_NOTICES.txt +94 -0
- package/dist/browser/terminal.css +37 -0
- package/dist/browser/terminal.js +228 -0
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +42 -38
- package/dist/browser-shell.js +6 -3
- package/dist/browser-workspace.d.ts +235 -1
- package/dist/browser-workspace.js +60 -0
- package/dist/builder.d.ts +10 -1
- package/dist/builder.js +87 -24
- package/dist/chat-actions.d.ts +13 -1
- package/dist/chat-actions.js +197 -15
- package/dist/chat-ask.d.ts +4 -0
- package/dist/chat-ask.js +14 -0
- package/dist/chat-channel.js +20 -0
- package/dist/chat-continuity.js +5 -4
- package/dist/chat-decide.d.ts +0 -14
- package/dist/chat-decide.js +4 -1
- package/dist/chat-delivery.d.ts +9 -0
- package/dist/chat-delivery.js +45 -6
- package/dist/chat-messages.d.ts +8 -0
- package/dist/chat-messages.js +36 -4
- package/dist/check-levels-ui.js +2 -2
- package/dist/checkout-proof.d.ts +28 -0
- package/dist/checkout-proof.js +62 -0
- package/dist/claim.d.ts +12 -0
- package/dist/claim.js +19 -0
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +4 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +21 -2
- package/dist/coding-ui.js +6 -4
- package/dist/console-instance.d.ts +124 -0
- package/dist/console-instance.js +337 -0
- package/dist/contracts/chat-actions.d.ts +16 -0
- package/dist/contracts/chat-actions.js +4 -0
- package/dist/contracts/chat-content.d.ts +19 -0
- package/dist/contracts/chat-content.js +6 -0
- package/dist/contracts/console-api.d.ts +13 -4
- package/dist/contracts/console-api.js +13 -4
- package/dist/contracts/flow-card.d.ts +4 -0
- package/dist/contracts/flow-card.js +3 -0
- package/dist/contracts/flow.d.ts +46 -0
- package/dist/contracts/flow.js +11 -2
- package/dist/contracts/lead-tools.d.ts +92 -0
- package/dist/contracts/lead-tools.js +13 -0
- package/dist/contracts/provider.d.ts +11 -1
- package/dist/contracts/provider.js +1 -1
- package/dist/contracts/route.d.ts +76 -1
- package/dist/contracts/route.js +1 -1
- package/dist/contracts/store-json.d.ts +2 -0
- package/dist/control-setup.d.ts +1 -1
- package/dist/control-ui.js +2 -2
- package/dist/converse.d.ts +16 -0
- package/dist/converse.js +32 -4
- package/dist/demo-chat.js +4 -4
- package/dist/demo.js +2 -2
- package/dist/desktop-update-gate.js +6 -0
- package/dist/desktop-update.d.ts +1 -1
- package/dist/discord-chat.js +4 -1
- package/dist/doctor.js +1 -1
- package/dist/egress-bridge.d.ts +17 -0
- package/dist/egress-bridge.js +111 -0
- package/dist/egress.d.ts +119 -0
- package/dist/egress.js +423 -0
- package/dist/exec.d.ts +78 -0
- package/dist/exec.js +367 -9
- package/dist/export-ui.js +2 -2
- package/dist/flow-engine.js +24 -5
- package/dist/flow-gallery-ui.js +2 -2
- package/dist/flow-gallery.js +27 -8
- package/dist/flow-share.js +16 -5
- package/dist/flow-triggers.d.ts +77 -8
- package/dist/flow-triggers.js +517 -70
- package/dist/flows-cli.js +8 -2
- package/dist/flows-ui.d.ts +0 -2
- package/dist/flows-ui.js +52 -4
- package/dist/flows.d.ts +4 -0
- package/dist/flows.js +9 -1
- package/dist/html.d.ts +9 -0
- package/dist/html.js +11 -0
- package/dist/invoke.d.ts +35 -0
- package/dist/invoke.js +347 -28
- package/dist/keys.d.ts +18 -14
- package/dist/keys.js +5 -0
- package/dist/lead-capabilities.js +3 -2
- package/dist/lead-contract.js +2 -0
- package/dist/lead-doors.d.ts +2 -0
- package/dist/lead-doors.js +7 -0
- package/dist/lead-follow.d.ts +2 -0
- package/dist/lead-follow.js +58 -2
- package/dist/lead-progress.js +1 -0
- package/dist/lead-queue-runtime.d.ts +46 -0
- package/dist/lead-queue-runtime.js +191 -0
- package/dist/lead-queue.d.ts +104 -0
- package/dist/lead-queue.js +187 -0
- package/dist/lead-sweep.d.ts +67 -0
- package/dist/lead-sweep.js +99 -0
- package/dist/lead-tools.d.ts +22 -0
- package/dist/lead-tools.js +152 -1
- package/dist/lead.d.ts +13 -5
- package/dist/lead.js +89 -29
- package/dist/limits-ui.d.ts +8 -1
- package/dist/limits-ui.js +26 -0
- package/dist/live-bus.d.ts +8 -0
- package/dist/live-bus.js +40 -4
- package/dist/live-detail.d.ts +27 -0
- package/dist/live-detail.js +57 -0
- package/dist/live-notes.d.ts +20 -0
- package/dist/live-notes.js +47 -0
- package/dist/live-records.d.ts +140 -0
- package/dist/live-records.js +935 -0
- package/dist/live-terminal.d.ts +269 -0
- package/dist/live-terminal.js +686 -0
- package/dist/live.d.ts +38 -27
- package/dist/live.js +142 -124
- package/dist/mcp-oauth.d.ts +6 -3
- package/dist/mcp-oauth.js +8 -4
- package/dist/memory-pass.js +2 -2
- package/dist/mobile-viewport.d.ts +5 -1
- package/dist/mobile-viewport.js +18 -2
- package/dist/model-catalog.js +4 -0
- package/dist/monitoring-ui.js +2 -2
- package/dist/needs-you.d.ts +1 -1
- package/dist/needs-you.js +11 -4
- package/dist/operate.d.ts +4 -0
- package/dist/operate.js +335 -19
- package/dist/passwordless.d.ts +23 -0
- package/dist/passwordless.js +26 -0
- package/dist/peek-cli.d.ts +12 -4
- package/dist/peek-cli.js +19 -11
- package/dist/phase-routing.js +10 -3
- package/dist/planner-source.d.ts +3 -0
- package/dist/planner-source.js +14 -0
- package/dist/planner.d.ts +2 -1
- package/dist/planner.js +7 -1
- package/dist/policy-ui.js +2 -2
- package/dist/policy.d.ts +1 -20
- package/dist/policy.js +29 -2
- package/dist/project-delete-ui.d.ts +18 -0
- package/dist/project-delete-ui.js +60 -9
- package/dist/project-delete.js +2 -2
- package/dist/project-memory.d.ts +3 -1
- package/dist/project-memory.js +57 -25
- package/dist/project-tools.d.ts +5 -3
- package/dist/project-tools.js +12 -9
- package/dist/provider-auth.d.ts +4 -3
- package/dist/provider-auth.js +5 -4
- package/dist/provider-connection.d.ts +2 -1
- package/dist/provider-connection.js +5 -0
- package/dist/provider.d.ts +25 -2
- package/dist/provider.js +63 -9
- package/dist/pull-request-ui.js +2 -2
- package/dist/push.js +1 -1
- package/dist/quick-start.d.ts +24 -0
- package/dist/quick-start.js +127 -0
- package/dist/release-adapters.d.ts +6 -0
- package/dist/release-adapters.js +70 -3
- package/dist/release.d.ts +10 -1
- package/dist/release.js +34 -15
- package/dist/request-budget-ui.js +2 -2
- package/dist/result-follow-ups.js +3 -2
- package/dist/result-review.d.ts +1 -1
- package/dist/result-review.js +9 -3
- package/dist/retention-ui.js +2 -2
- package/dist/retention.js +10 -1
- package/dist/run-efficiency.d.ts +73 -0
- package/dist/run-efficiency.js +228 -0
- package/dist/runner.d.ts +5 -1
- package/dist/runner.js +22 -1
- package/dist/scope.d.ts +183 -5
- package/dist/scope.js +256 -31
- package/dist/scout-net.d.ts +4 -1
- package/dist/scout-net.js +37 -12
- package/dist/scout.d.ts +20 -6
- package/dist/scout.js +27 -3
- package/dist/serve.js +248 -40
- package/dist/server/chat.d.ts +1 -1
- package/dist/server/chat.js +122 -31
- package/dist/server/chrome.d.ts +16 -2
- package/dist/server/chrome.js +63 -11
- package/dist/server/console.css +107 -42
- package/dist/server/flows.js +38 -8
- package/dist/server/guards.d.ts +1 -0
- package/dist/server/guards.js +23 -9
- package/dist/server/http.d.ts +7 -0
- package/dist/server/http.js +3 -2
- package/dist/server/live.d.ts +34 -1
- package/dist/server/live.js +39 -17
- package/dist/server/pages-assets.js +5 -0
- package/dist/server/pages.js +90 -13
- package/dist/server/people-tokens.d.ts +3 -0
- package/dist/server/people-tokens.js +55 -17
- package/dist/server/remote-hooks.js +12 -6
- package/dist/server/remote.d.ts +2 -1
- package/dist/server/remote.js +14 -5
- package/dist/server/render-chat.d.ts +12 -3
- package/dist/server/render-chat.js +47 -16
- package/dist/server/render-pages.d.ts +32 -4
- package/dist/server/render-pages.js +83 -13
- package/dist/server/render-people.d.ts +3 -2
- package/dist/server/render-people.js +3 -2
- package/dist/server/render-results.d.ts +18 -2
- package/dist/server/render-results.js +37 -28
- package/dist/server/render-settings.js +6 -5
- package/dist/server/render-tasks.d.ts +35 -10
- package/dist/server/render-tasks.js +111 -27
- package/dist/server/request-context.d.ts +2 -0
- package/dist/server/request-context.js +2 -1
- package/dist/server/route-table.d.ts +3 -0
- package/dist/server/route-table.js +47 -5
- package/dist/server/runtime.d.ts +33 -2
- package/dist/server/session.d.ts +25 -1
- package/dist/server/session.js +18 -4
- package/dist/server/settings.js +142 -10
- package/dist/server/sweeps.d.ts +4 -0
- package/dist/server/sweeps.js +58 -0
- package/dist/server/task-views.d.ts +2 -1
- package/dist/server/task-views.js +96 -6
- package/dist/server/tasks.js +327 -78
- package/dist/server/terminals.d.ts +31 -0
- package/dist/server/terminals.js +365 -0
- package/dist/setup-guide.js +3 -2
- package/dist/slack-chat.js +4 -1
- package/dist/spend-ui.js +3 -3
- package/dist/spend.js +2 -1
- package/dist/steer-capabilities.d.ts +32 -0
- package/dist/steer-capabilities.js +60 -0
- package/dist/steer-controls.d.ts +14 -0
- package/dist/steer-controls.js +36 -0
- package/dist/steer-status.js +2 -2
- package/dist/storage-ui.js +4 -4
- package/dist/store-baseline.d.ts +1 -1
- package/dist/store-baseline.js +1 -1
- package/dist/store.d.ts +440 -15
- package/dist/store.js +1072 -52
- package/dist/subagent-desk.js +1 -1
- package/dist/summary.js +3 -0
- package/dist/task-source.d.ts +58 -0
- package/dist/task-source.js +93 -0
- package/dist/team-leads.js +1 -1
- package/dist/teams-chat.js +4 -1
- package/dist/teardown-git.d.ts +22 -0
- package/dist/teardown-git.js +136 -2
- package/dist/telegram-lead.d.ts +3 -0
- package/dist/telegram-lead.js +44 -7
- package/dist/telegram.d.ts +11 -2
- package/dist/telegram.js +51 -17
- package/dist/terminal-catalog.d.ts +11 -0
- package/dist/terminal-catalog.js +129 -0
- package/dist/text-limits.d.ts +2 -0
- package/dist/text-limits.js +2 -0
- package/dist/tool-broker.d.ts +14 -0
- package/dist/tool-broker.js +67 -2
- package/dist/toolroll-update-ui.js +2 -2
- package/dist/toolroll-update.js +13 -3
- package/dist/tools-ui.js +2 -2
- package/dist/unfenced-test-support.d.ts +2 -0
- package/dist/unfenced-test-support.js +11 -0
- package/dist/webhook-limits.d.ts +23 -0
- package/dist/webhook-limits.js +30 -0
- package/dist/webhook-readers.d.ts +56 -0
- package/dist/webhook-readers.js +138 -0
- package/dist/webhook-signing.d.ts +27 -0
- package/dist/webhook-signing.js +119 -0
- package/dist/work-index.d.ts +29 -0
- package/dist/work-index.js +16 -2
- package/dist/workspace-revision.js +3 -1
- package/dist/writes-mode.d.ts +9 -0
- package/dist/writes-mode.js +8 -0
- package/package.json +15 -2
package/dist/chat-actions.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
/** Shared, exact-state actions. Models may prepare; only the existing human
|
|
2
2
|
* confirmation door executes. Protected actions use a one-use review receipt. */
|
|
3
|
+
import { fileTaskProposal } from "./proposal.js";
|
|
4
|
+
import { heuristicSizing } from "./task-sizing.js";
|
|
5
|
+
import { cancelSweepRest, sweepOpen, sweepTable, VERDICT_RULE } from "./lead-sweep.js";
|
|
6
|
+
import { isAcpProfile } from "./acp-agents.js";
|
|
3
7
|
import { withActor } from "./actor.js";
|
|
4
8
|
import { TEXT_LIMITS } from "./text-limits.js";
|
|
5
9
|
import { publicChatText } from "./chat-display.js";
|
|
@@ -9,7 +13,7 @@ import { leadTurnKeepsProposals, verifiedAuthor } from "./store.js";
|
|
|
9
13
|
import { isVerifiedApprover, reproveApprover, } from "./principal.js";
|
|
10
14
|
import { changeSkills, importSkill, skillsView, skillsVersion, testSkill, validateSkill, } from "./project-skills.js";
|
|
11
15
|
import { changeKnowledge, knowledgeVersion, knowledgeView, } from "./project-knowledge.js";
|
|
12
|
-
import { approve, authenticateApprover, describeScope, scopeAuthorityOf, } from "./scope.js";
|
|
16
|
+
import { passwordlessSessionFor, approve, authenticateApprover, describeScope, scopeAuthorityOf, freshnessFor, spendingOnlyOnSuccess, ssoNowAvailable, withFreshness, } from "./scope.js";
|
|
13
17
|
import { readVerifiedArtifact, scanForSecrets } from "./evidence.js";
|
|
14
18
|
import { readAuthModeStrict } from "./keys.js";
|
|
15
19
|
import { parseProof } from "./proof.js";
|
|
@@ -86,6 +90,11 @@ export const CHAT_ACTIONS = {
|
|
|
86
90
|
flow_create: { label: "Create flow", protected: false, password: false },
|
|
87
91
|
// A starter flow: its zones and its trigger, switched on with one yes (flow-starters.ts).
|
|
88
92
|
flow_starter: { label: "Switch on", protected: false, password: false },
|
|
93
|
+
// Lead fan-out (docs/plans/lead-fan-out.md): files N tasks under one brief and approves each one's scope — the
|
|
94
|
+
// person's ceremony (protected, with the password), N ordinary approvals of exactly the scopes shown.
|
|
95
|
+
sweep_file: { label: "Start sweep", protected: true, password: true },
|
|
96
|
+
// Stops every item of a sweep not yet ready; finished results stay. An ordinary act, like cancelling a task.
|
|
97
|
+
sweep_cancel_rest: { label: "Cancel the rest", protected: false, password: false },
|
|
89
98
|
flow_edit: { label: "Save flow", protected: false, password: false },
|
|
90
99
|
flow_card_add: { label: "Add card", protected: false, password: false },
|
|
91
100
|
flow_card_move: { label: "Move card", protected: false, password: false },
|
|
@@ -175,10 +184,12 @@ function runtimeTerms(profile) {
|
|
|
175
184
|
? profile.approvalArgv === "yolo"
|
|
176
185
|
? "Full access: every tool is approved automatically (--approval-mode yolo)."
|
|
177
186
|
: "Edits are approved automatically; other tools are refused."
|
|
178
|
-
: profile
|
|
179
|
-
? "Full access:
|
|
180
|
-
:
|
|
181
|
-
|
|
187
|
+
: isAcpProfile(profile)
|
|
188
|
+
? "Full access inside the OS fence: the agent runs any tool it chooses, and nothing outside the fence is reachable."
|
|
189
|
+
: profile.sandboxMode === "danger-full-access"
|
|
190
|
+
? "Full access: approvals and sandbox are bypassed (--dangerously-bypass-approvals-and-sandbox)."
|
|
191
|
+
: "Commands use the workspace-write sandbox.";
|
|
192
|
+
return `${profile.provider} · ${profile.model}\n${permission}\n${isAcpProfile(profile) ? `${profile.timeoutSeconds} seconds per attempt, or ${profile.idleTimeoutSeconds} seconds silent` : `${profile.timeoutSeconds} seconds ${profile.timeoutKind === "idle" ? "without progress" : "per attempt"}`}${profile.provider === "claude" ? `; at most ${profile.maxTurns} turns` : ""}. Repairs: ${profile.repairModel}, ${profile.repairTimeoutSeconds} seconds${profile.provider === "claude" ? `, at most ${profile.repairMaxTurns} turns` : ""}.`;
|
|
182
193
|
}
|
|
183
194
|
function resultOf(store, task, run, repo) {
|
|
184
195
|
const ref = store.lookupRef(task);
|
|
@@ -263,7 +274,7 @@ export function prepareSharedAction(store, who, operation, input, root, now = ne
|
|
|
263
274
|
const stamp = hash({ operation, request, repo: "", state, terms, actor: who.name, generation: who.generation, ceiling: who.ceilingDigest });
|
|
264
275
|
return { version: SHARED_ACTION_VERSION, operation, request, repo: "", title, terms, stamp, state };
|
|
265
276
|
}
|
|
266
|
-
const task = operation.startsWith("skill_") || operation.startsWith("knowledge_") || operation.startsWith("decision_") || operation.startsWith("tool_") || operation.startsWith("flow_") || operation.startsWith("subagent_") || operation.startsWith("kit_")
|
|
277
|
+
const task = operation.startsWith("skill_") || operation.startsWith("knowledge_") || operation.startsWith("decision_") || operation.startsWith("tool_") || operation.startsWith("flow_") || operation.startsWith("subagent_") || operation.startsWith("kit_") || operation.startsWith("sweep_")
|
|
267
278
|
? null
|
|
268
279
|
: text(input, "task", 64);
|
|
269
280
|
const flowTarget = operation.startsWith("flow_") && operation !== "flow_create" && operation !== "flow_script_save" && operation !== "flow_starter" ? flowTargetOf(store, input) : null;
|
|
@@ -275,7 +286,8 @@ export function prepareSharedAction(store, who, operation, input, root, now = ne
|
|
|
275
286
|
const questionFlow = questionTarget === null ? null : store.getFlow(store.getFlowCard(questionTarget.card)?.flow ?? -1);
|
|
276
287
|
if (operation === "subagent_answer" && (questionTarget === null || questionFlow === null))
|
|
277
288
|
throw Error("That question isn't open any more.");
|
|
278
|
-
const repo =
|
|
289
|
+
const repo = operation === "sweep_cancel_rest" ? store.getSweep(integer(input, "sweep"))?.repo
|
|
290
|
+
: flowTarget !== null ? flowTarget.flow.repo : subagentTarget !== null ? subagentTarget.repo : questionFlow !== null ? questionFlow.repo : task === null ? text(input, "repo", 4096) : store.lookupRef(task)?.repo;
|
|
279
291
|
if (!repo)
|
|
280
292
|
throw Error("Choose an available project.");
|
|
281
293
|
requireActor(store, who, repo);
|
|
@@ -554,6 +566,50 @@ export function prepareSharedAction(store, who, operation, input, root, now = ne
|
|
|
554
566
|
terms.push(question.question, `Your answer: ${[choice?.label, said].filter(Boolean).join(" — ")}`);
|
|
555
567
|
}
|
|
556
568
|
}
|
|
569
|
+
else if (operation === "sweep_cancel_rest") {
|
|
570
|
+
// Exactly the items open now: if any settles or starts in between, the card is stale and a fresh one is needed.
|
|
571
|
+
const table = sweepTable(store, integer(input, "sweep"), now, who.repos);
|
|
572
|
+
if (table === null || table.repo !== repo)
|
|
573
|
+
throw Error("That sweep isn't one of yours.");
|
|
574
|
+
// Exactly the items as they stand now — each one's active task (a revision, after one), state, running attempt and
|
|
575
|
+
// last change: any change makes this card stale, so nothing is acted on that wasn't shown.
|
|
576
|
+
const open = sweepOpen(store, table, now);
|
|
577
|
+
if (open.length === 0)
|
|
578
|
+
throw Error("Nothing in that sweep is still unfinished.");
|
|
579
|
+
state = { open };
|
|
580
|
+
title = `Cancel the rest of ${table.title}`;
|
|
581
|
+
const running = open.filter(one => one.run !== null);
|
|
582
|
+
terms.push(`Cancels ${open.length - running.length} unfinished item${open.length - running.length === 1 ? "" : "s"}${running.length === 0 ? "" : ` and stops ${running.length} running build${running.length === 1 ? "" : "s"}`}: ${open.map(one => `${one.label} (${one.task})`).join(", ")}.`, "Finished results stay.");
|
|
583
|
+
}
|
|
584
|
+
else if (operation === "sweep_file") {
|
|
585
|
+
// Everything the person approves is on this screen: the shared rules, every item, the one success test, the
|
|
586
|
+
// limit. Confirming files exactly these and approves exactly these scopes; anything changed is a new card.
|
|
587
|
+
const sweepTitle = text(input, "title", 200).trim(), brief = text(input, "brief", 8000).trim();
|
|
588
|
+
const items = Array.isArray(input["items"]) ? input["items"] : [];
|
|
589
|
+
if (items.length < 2 || items.length > 20)
|
|
590
|
+
throw Error("A sweep is 2 to 20 items. For one, propose a task.");
|
|
591
|
+
const cleaned = items.map((one, index) => ({ title: text(one, "title", 200).trim(), item: text(one, "item", 4000).trim(), index }));
|
|
592
|
+
const maxParallel = integer(input, "maxParallel");
|
|
593
|
+
if (maxParallel < 1 || maxParallel > 8)
|
|
594
|
+
throw Error("Run 1 to 8 at once.");
|
|
595
|
+
request["title"] = sweepTitle;
|
|
596
|
+
request["brief"] = brief;
|
|
597
|
+
request["items"] = cleaned.map(({ title, item }) => ({ title, item }));
|
|
598
|
+
const project = repo.split(/[\\/]/).filter(Boolean).at(-1) ?? repo;
|
|
599
|
+
title = `Start a sweep in ${project}: ${sweepTitle}`;
|
|
600
|
+
// Every word each child's scope will hold is shown here (the security review: nothing approved off-screen): its
|
|
601
|
+
// goal is the shared rules, then its item, then the verdict line; its success test is the shared acceptance.
|
|
602
|
+
const acceptance = Array.isArray(input["acceptance"]) ? input["acceptance"] : [];
|
|
603
|
+
if (acceptance.length === 0)
|
|
604
|
+
throw Error("A sweep needs a success test every item shares.");
|
|
605
|
+
// Every child's scope exactly as filing would make it — its words AND its agent's permissions, limits and route —
|
|
606
|
+
// found by filing them in a savepoint that is always rolled back (prepare writes nothing). They are on this card and
|
|
607
|
+
// in its state: if a project default changes before confirm, the card is stale and nothing is filed or approved.
|
|
608
|
+
const children = sweepChildScopes(store, who, repo, brief, cleaned.map(({ title, item }) => ({ title, item })), input["acceptance"], now);
|
|
609
|
+
state = { children: children.map(one => one.fingerprint) };
|
|
610
|
+
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(", ")}` : ""}`);
|
|
611
|
+
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}`)), "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.");
|
|
612
|
+
}
|
|
557
613
|
else if (operation.startsWith("flow_")) {
|
|
558
614
|
const project = repo.split(/[\\/]/).filter(Boolean).at(-1) ?? repo;
|
|
559
615
|
if (operation === "flow_create") {
|
|
@@ -985,10 +1041,28 @@ function savedActionContext(store, who, id, state) {
|
|
|
985
1041
|
requireActor(store, who, payload.repo, payload.operation);
|
|
986
1042
|
return payload;
|
|
987
1043
|
}
|
|
1044
|
+
/** An action's terms alone, without whose proposal it was (actor, generation, ceiling). */
|
|
1045
|
+
const termsStampOf = (action) => hash({ operation: action.operation, request: action.request, repo: action.repo, state: action.state, terms: action.terms });
|
|
1046
|
+
/**
|
|
1047
|
+
* Whether the live action is the saved one: the exact stamp; or, for a scope approval already holding another
|
|
1048
|
+
* approver's yes on these exact terms (awaiting the second), the same terms — each reviewer's own nonce and authority
|
|
1049
|
+
* are still bound to them individually.
|
|
1050
|
+
*/
|
|
1051
|
+
function sameSharedAction(store, who, live, saved) {
|
|
1052
|
+
if (live.stamp === saved.stamp)
|
|
1053
|
+
return true;
|
|
1054
|
+
if (saved.operation !== "scope_approve" || termsStampOf(live) !== termsStampOf(saved))
|
|
1055
|
+
return false;
|
|
1056
|
+
const task = typeof saved.request["task"] === "string" ? saved.request["task"] : null;
|
|
1057
|
+
const scope = task === null ? null : store.getScope(task);
|
|
1058
|
+
if (scope === null)
|
|
1059
|
+
return false;
|
|
1060
|
+
return store.handle.prepare("SELECT 1 FROM scope_approval_vote WHERE task_id = ? AND digest = ? AND approver <> ? LIMIT 1").get(task, scope.digest, who.name) !== undefined;
|
|
1061
|
+
}
|
|
988
1062
|
export function mintSharedActionReview(store, who, id, root, now) {
|
|
989
1063
|
const payload = savedActionContext(store, who, id, "pending");
|
|
990
1064
|
const live = prepareSharedAction(store, who, payload.operation, payload.request, root, now);
|
|
991
|
-
if (live
|
|
1065
|
+
if (!sameSharedAction(store, who, live, payload))
|
|
992
1066
|
throw Error("This action changed. Ask for a fresh proposal.");
|
|
993
1067
|
const nonce = randomBytes(24).toString("hex");
|
|
994
1068
|
if (!store.mintCeremonyNonce({
|
|
@@ -1014,7 +1088,7 @@ export function executeSharedAction(store, who, id, payload, now, options) {
|
|
|
1014
1088
|
if (hash(saved) !== hash(payload))
|
|
1015
1089
|
return refuse("refused", "This action does not match the saved proposal.");
|
|
1016
1090
|
live = prepareSharedAction(store, who, saved.operation, saved.request, options.root, now);
|
|
1017
|
-
if (live
|
|
1091
|
+
if (!sameSharedAction(store, who, live, saved))
|
|
1018
1092
|
return refuse("stale", "This action changed. Ask for a fresh proposal.");
|
|
1019
1093
|
}
|
|
1020
1094
|
catch (error) {
|
|
@@ -1022,6 +1096,8 @@ export function executeSharedAction(store, who, id, payload, now, options) {
|
|
|
1022
1096
|
? error.message
|
|
1023
1097
|
: "This action is no longer available.");
|
|
1024
1098
|
}
|
|
1099
|
+
// The review nonce was minted against the saved proposal's stamp: it is checked against that, whoever reviews.
|
|
1100
|
+
const savedStamp = payload.stamp;
|
|
1025
1101
|
payload = live;
|
|
1026
1102
|
const config = CHAT_ACTIONS[live.operation], req = live.request;
|
|
1027
1103
|
const challenged = options.via !== "web" &&
|
|
@@ -1031,14 +1107,21 @@ export function executeSharedAction(store, who, id, payload, now, options) {
|
|
|
1031
1107
|
if (sharedActionNeedsReview(payload) && !challenged) {
|
|
1032
1108
|
if (options.via !== "web" || !options.review || options.confirm !== true)
|
|
1033
1109
|
return refuse("needs-confirm", "Review the complete action in the secure confirmation screen.");
|
|
1034
|
-
if (!store.consumeCeremonyNonce(nonceHash(options.review.nonce), who.name, "chat-action", id,
|
|
1110
|
+
if (!store.consumeCeremonyNonce(nonceHash(options.review.nonce), who.name, "chat-action", id, savedStamp, now))
|
|
1035
1111
|
return refuse("needs-confirm", "This confirmation expired or was already used. Open the review again.");
|
|
1112
|
+
// A scope approval proves its person inside approve() (one transaction with the seal, under the same freshness
|
|
1113
|
+
// rule as the task page); here an empty field is only looked at, never spent (an explicit re-check is one-use).
|
|
1114
|
+
const emptyApproval = payload.operation === "scope_approve" && options.review.password === "";
|
|
1036
1115
|
if (config.password &&
|
|
1037
|
-
|
|
1116
|
+
(emptyApproval
|
|
1117
|
+
? !passwordlessSessionFor(store, who.name) && !ssoNowAvailable(who.name) && withFreshness("approve-standard", () => freshnessFor(who.name)) === null
|
|
1118
|
+
: !authenticateApprover(store, who.name, options.review.password, payload.repo).ok))
|
|
1038
1119
|
return refuse("needs-confirm", "Confirm with your password in the secure screen.");
|
|
1039
1120
|
}
|
|
1040
1121
|
try {
|
|
1041
|
-
|
|
1122
|
+
// A scope approval's re-check (sso-now) is spent only if it happens: a refusal or a rolled-back savepoint puts it
|
|
1123
|
+
// back (its every effect is in this transaction). Other actions spend it whatever comes of them.
|
|
1124
|
+
const act = () => store.savepoint(() => {
|
|
1042
1125
|
const task = typeof req["task"] === "string" ? req["task"] : null, repo = payload.repo, actor = who.name;
|
|
1043
1126
|
let taskId = task;
|
|
1044
1127
|
if (payload.operation === "lead_about_you") {
|
|
@@ -1076,6 +1159,14 @@ export function executeSharedAction(store, who, id, payload, now, options) {
|
|
|
1076
1159
|
const done = runFlowAction(store, payload, actor, who.repos, options.root, now);
|
|
1077
1160
|
return { ok: true, taskId: null, said: done.said, href: done.href };
|
|
1078
1161
|
}
|
|
1162
|
+
else if (payload.operation === "sweep_cancel_rest") {
|
|
1163
|
+
const said = cancelSweepRest(store, who.name, payload.state["open"], now, "web");
|
|
1164
|
+
return { ok: true, taskId: null, said, href: `/sweeps/${Number(req["sweep"])}` };
|
|
1165
|
+
}
|
|
1166
|
+
else if (payload.operation === "sweep_file") {
|
|
1167
|
+
const done = fileSweep(store, who, id, payload, options.review?.password ?? "", now);
|
|
1168
|
+
return { ok: true, taskId: null, said: done.said, href: done.href };
|
|
1169
|
+
}
|
|
1079
1170
|
else if (payload.operation === "kit_setup") {
|
|
1080
1171
|
const kit = kitOf(String(req["kit"]));
|
|
1081
1172
|
const made = setUpKitNow(store, kit, repo, actor, now, null, true);
|
|
@@ -1117,11 +1208,14 @@ export function executeSharedAction(store, who, id, payload, now, options) {
|
|
|
1117
1208
|
}, now);
|
|
1118
1209
|
else if (payload.operation === "scope_approve") {
|
|
1119
1210
|
const scope = store.getScope(task);
|
|
1120
|
-
|
|
1211
|
+
// Cockpit B2: classified by the act, not the road — the same standard-approval freshness as /t/:id/approve.
|
|
1212
|
+
const result = withFreshness("approve-standard", () => approve(store, task, actor, now, scope.digest, options.review.password));
|
|
1121
1213
|
if (!result.ok) {
|
|
1122
|
-
//
|
|
1214
|
+
// A first yes on protected work is kept, with how its person was proved: committed, never rolled back.
|
|
1215
|
+
// The card stays open (`awaiting`) for the second approver; the same person again changes nothing.
|
|
1123
1216
|
if (result.reason === "second-approver")
|
|
1124
|
-
|
|
1217
|
+
return { ok: true, taskId, awaiting: true, ...(result.already ? { already: true } : {}), said: gateWords({ verdict: "vote", have: result.have, need: 2, already: result.already }) };
|
|
1218
|
+
// v102: the project's approval rules speak for themselves.
|
|
1125
1219
|
if (result.reason === "requester" || result.reason === "person-required")
|
|
1126
1220
|
throw Error(gateWords({ verdict: "refuse", reason: result.reason }));
|
|
1127
1221
|
if (result.reason === "policy")
|
|
@@ -1187,6 +1281,7 @@ export function executeSharedAction(store, who, id, payload, now, options) {
|
|
|
1187
1281
|
: "Project knowledge saved.";
|
|
1188
1282
|
return { ok: true, taskId, said };
|
|
1189
1283
|
});
|
|
1284
|
+
return payload.operation === "scope_approve" ? spendingOnlyOnSuccess(act, result => !result.ok || ("already" in result && result.already === true)) : act();
|
|
1190
1285
|
}
|
|
1191
1286
|
catch (error) {
|
|
1192
1287
|
return refuse("refused", error instanceof Error
|
|
@@ -1195,6 +1290,93 @@ export function executeSharedAction(store, who, id, payload, now, options) {
|
|
|
1195
1290
|
}
|
|
1196
1291
|
}
|
|
1197
1292
|
/** A confirmed subagent action (v92), through the same helpers the Subagents pages use. */
|
|
1293
|
+
/**
|
|
1294
|
+
* Lead fan-out: file every item (all of them or none — any refusal throws and the savepoint takes the whole sweep
|
|
1295
|
+
* back), link them to one sweep record, then approve each one's scope through the ordinary approval (its own digest,
|
|
1296
|
+
* its own rules). A child the approval refuses stays filed and unapproved; it is named, never retried.
|
|
1297
|
+
*/
|
|
1298
|
+
/** One child as filing makes it (the shared brief, its item, the verdict rule; the shared acceptance; built directly). */
|
|
1299
|
+
function fileSweepChild(store, who, repo, brief, one, acceptance, now) {
|
|
1300
|
+
const goal = `${brief}\n\nThis item:\n${one.item}\n\n${VERDICT_RULE}`;
|
|
1301
|
+
return fileTaskProposal(store, {
|
|
1302
|
+
title: one.title, repo, goal, acceptance, planning: "skip",
|
|
1303
|
+
// Sized now, by the same heuristic filing would use: no background classifier is started — the card's dry run must
|
|
1304
|
+
// leave nothing running after its rollback, and the approved scope must be the one shown (Codex sweep r2).
|
|
1305
|
+
sizing: heuristicSizing({ title: one.title, goal, outOfScope: null }),
|
|
1306
|
+
filedVia: "mate", filedBy: { name: who.name, kind: "person" }, proposedVia: "mate", admittedRepos: [...who.repos],
|
|
1307
|
+
}, now);
|
|
1308
|
+
}
|
|
1309
|
+
/** A filed child's scope in the words an approval agrees to (no identity lines), its runtime terms, and their digest. */
|
|
1310
|
+
function sweepChildScopeOf(store, taskId) {
|
|
1311
|
+
const scope = store.getScope(taskId);
|
|
1312
|
+
if (scope === null)
|
|
1313
|
+
throw Error("A sweep item has no scope to show.");
|
|
1314
|
+
const authority = scopeAuthorityOf(scope, { authMode: readAuthModeStrict });
|
|
1315
|
+
if (!authority.ok)
|
|
1316
|
+
throw Error(authority.problem);
|
|
1317
|
+
const words = describeScope(scope).filter(line => !/^\s*(reference|approved)\s/.test(line)).map(line => line.trim());
|
|
1318
|
+
const runtime = `Agent permissions and limits: ${runtimeTerms(authority.profile)} ${authority.authMode === "api-key" ? "Uses your API key; spend is charged to that account." : authority.authMode === "subscription" ? "Uses your subscription login." : ""}`.trim();
|
|
1319
|
+
// The fingerprint covers the scope's every field, not only what is rendered (a profile setting no sentence names, such
|
|
1320
|
+
// as an agent's idle timeout, still makes the card stale): its exact terms with only identity left out.
|
|
1321
|
+
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;
|
|
1322
|
+
return { fingerprint: hash({ words, runtime, authMode: authority.authMode, profile: authority.profile, terms }), runtime, words };
|
|
1323
|
+
}
|
|
1324
|
+
/** Every child as filing would make it, found in a savepoint that is always rolled back: prepare never writes. */
|
|
1325
|
+
function sweepChildScopes(store, who, repo, brief, items, acceptance, now) {
|
|
1326
|
+
const dryRun = new Error("sweep dry run");
|
|
1327
|
+
let found = [];
|
|
1328
|
+
// Inside one transaction (filing opens its own; transact flattens), a savepoint that always rolls back.
|
|
1329
|
+
store.transact(() => {
|
|
1330
|
+
try {
|
|
1331
|
+
store.savepoint(() => {
|
|
1332
|
+
found = items.map((one, index) => {
|
|
1333
|
+
const made = fileSweepChild(store, who, repo, brief, one, acceptance, now);
|
|
1334
|
+
if (!made.ok)
|
|
1335
|
+
throw Error(`Item ${index + 1} (${one.title}) can't be filed: ${made.message}`);
|
|
1336
|
+
return sweepChildScopeOf(store, made.id);
|
|
1337
|
+
});
|
|
1338
|
+
throw dryRun;
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
catch (error) {
|
|
1342
|
+
if (error !== dryRun)
|
|
1343
|
+
throw error;
|
|
1344
|
+
}
|
|
1345
|
+
});
|
|
1346
|
+
return found;
|
|
1347
|
+
}
|
|
1348
|
+
function fileSweep(store, who, proposal, payload, password, now) {
|
|
1349
|
+
const req = payload.request;
|
|
1350
|
+
const already = store.sweepOfProposal(proposal);
|
|
1351
|
+
if (already !== null)
|
|
1352
|
+
return { said: "This sweep was already started.", href: `/sweeps/${already}` };
|
|
1353
|
+
const items = req["items"];
|
|
1354
|
+
const brief = String(req["brief"]);
|
|
1355
|
+
const sweep = store.createSweep({ owner: who.name, thread: store.getLeadProposal(proposal)?.thread ?? null, repo: payload.repo, title: String(req["title"]), brief,
|
|
1356
|
+
maxParallel: Number(req["maxParallel"]), proposal }, now);
|
|
1357
|
+
const filed = [];
|
|
1358
|
+
for (const [index, one] of items.entries()) {
|
|
1359
|
+
const made = fileSweepChild(store, who, payload.repo, brief, one, req["acceptance"], now);
|
|
1360
|
+
if (!made.ok)
|
|
1361
|
+
throw Error(`Item ${index + 1} (${one.title}) couldn't be filed: ${made.message}. Nothing was filed.`);
|
|
1362
|
+
// Each filed scope must be exactly the one the card showed (words, permissions, limits, route); else nothing is filed.
|
|
1363
|
+
if (sweepChildScopeOf(store, made.id).fingerprint !== payload.state["children"]?.[index])
|
|
1364
|
+
throw Error(`Item ${index + 1} (${one.title}) no longer matches what you reviewed (a project default changed). Nothing was filed; ask for a fresh card.`);
|
|
1365
|
+
store.addSweepTask(sweep, made.id, index, one.title);
|
|
1366
|
+
filed.push({ id: made.id, title: one.title });
|
|
1367
|
+
}
|
|
1368
|
+
const waiting = [];
|
|
1369
|
+
for (const one of filed) {
|
|
1370
|
+
const scope = store.getScope(one.id);
|
|
1371
|
+
const approved = scope === null ? null : withFreshness("approve-standard", () => approve(store, one.id, who.name, now, scope.digest, password));
|
|
1372
|
+
if (approved === null || !approved.ok)
|
|
1373
|
+
waiting.push(one.title);
|
|
1374
|
+
}
|
|
1375
|
+
const said = waiting.length === 0
|
|
1376
|
+
? `Started: ${filed.length} tasks, at most ${Number(req["maxParallel"])} at once.`
|
|
1377
|
+
: `Started: ${filed.length - waiting.length} of ${filed.length} approved; ${waiting.length} wait for their own approval (${waiting.join(", ")}).`;
|
|
1378
|
+
return { said, href: `/sweeps/${sweep}` };
|
|
1379
|
+
}
|
|
1198
1380
|
function runSubagentAction(store, payload, actor, now) {
|
|
1199
1381
|
const req = payload.request;
|
|
1200
1382
|
if (payload.operation === "subagent_create") {
|
package/dist/chat-ask.d.ts
CHANGED
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import type { ChatBinding, ChatEvent, ChatState } from "./chat-delivery-state.js";
|
|
14
14
|
import { type LeadAsk, type Store } from "./store.js";
|
|
15
|
+
/** Whether a task (the execution shown, never only its family's root) moved past a lead's question asked at `since`:
|
|
16
|
+
* its plan was rewritten or approved, or a new attempt of it started. The options answered a state that is gone. A run
|
|
17
|
+
* that merely finishes, or a review beside it, answers nothing, so it never takes the question's buttons. */
|
|
18
|
+
export declare function taskMovedSince(store: Store, taskId: string, since: string): boolean;
|
|
15
19
|
/** The question as text with its options, where no buttons are drawn. */
|
|
16
20
|
export declare function chatAskText(ask: Pick<LeadAsk, "question" | "options">): string;
|
|
17
21
|
/** The live buttons on one part, in the order they were minted: the options, then "Something else". None when the part
|
package/dist/chat-ask.js
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { chatHash, partContent, savedChatPart } from "./chat-delivery-state.js";
|
|
2
2
|
import { LEAD_ASK_OTHER } from "./store.js";
|
|
3
|
+
/** Whether a task (the execution shown, never only its family's root) moved past a lead's question asked at `since`:
|
|
4
|
+
* its plan was rewritten or approved, or a new attempt of it started. The options answered a state that is gone. A run
|
|
5
|
+
* that merely finishes, or a review beside it, answers nothing, so it never takes the question's buttons. */
|
|
6
|
+
export function taskMovedSince(store, taskId, since) {
|
|
7
|
+
const at = new Date(since).getTime();
|
|
8
|
+
if (Number.isNaN(at))
|
|
9
|
+
return false;
|
|
10
|
+
const after = (iso) => iso != null && new Date(iso).getTime() > at;
|
|
11
|
+
const scope = store.getScope(taskId);
|
|
12
|
+
if (scope !== null && (after(scope.proposedAt) || after(scope.approvedAt)))
|
|
13
|
+
return true;
|
|
14
|
+
const ref = store.lookupRef(taskId);
|
|
15
|
+
return ref !== null && store.runsFor(ref.id).some(run => run.role !== "reviewer" && after(run.startedAt));
|
|
16
|
+
}
|
|
3
17
|
/** The question as text with its options, where no buttons are drawn. */
|
|
4
18
|
export function chatAskText(ask) {
|
|
5
19
|
return [ask.question, ...ask.options.map(one => `• ${one}`), `• ${LEAD_ASK_OTHER}: say it in your own words`].join("\n");
|
package/dist/chat-channel.js
CHANGED
|
@@ -509,6 +509,11 @@ export const CHAT_ACTION_PARITY = {
|
|
|
509
509
|
how: "Read by the model during a phone turn over the enrolled ceiling.",
|
|
510
510
|
gap: null,
|
|
511
511
|
},
|
|
512
|
+
find_tools: {
|
|
513
|
+
support: "direct",
|
|
514
|
+
how: "Searches the lead's own other tools by what it wants to do; what it finds is offered to it from its next step in the same reply. Same on every channel: the shared engine runs it.",
|
|
515
|
+
gap: null,
|
|
516
|
+
},
|
|
512
517
|
list_repos: {
|
|
513
518
|
support: "direct",
|
|
514
519
|
how: "Read during a turn; projects are r1..rN in enrollment order, as on the console.",
|
|
@@ -583,6 +588,21 @@ export const CHAT_ACTION_PARITY = {
|
|
|
583
588
|
how: "Card with Confirm/Dismiss through confirmLeadProposal (filed as a lead proposal, via telegram). The confirmed card links Review & start for the filed task while its scope waits; under a signed automatic mode it says the scope is approved and links the task.",
|
|
584
589
|
gap: "Under manual approval the password step happens in the console, reached from the card's button.",
|
|
585
590
|
},
|
|
591
|
+
propose_sweep: {
|
|
592
|
+
support: "direct",
|
|
593
|
+
how: "A protected shared action (sweep_file): the card lists every item, the shared rules and the success test; confirming files them all and approves each scope.",
|
|
594
|
+
gap: "Starting a sweep approves work, so it opens the secure review in the console (the password, or Passwordless).",
|
|
595
|
+
},
|
|
596
|
+
get_sweep: {
|
|
597
|
+
support: "direct",
|
|
598
|
+
how: "Reads a sweep's table (each item's state, verdict and result) from the local database through the shared engine.",
|
|
599
|
+
gap: null,
|
|
600
|
+
},
|
|
601
|
+
propose_sweep_cancel: {
|
|
602
|
+
support: "direct",
|
|
603
|
+
how: "An ordinary shared action (sweep_cancel_rest): Confirm cancels every item not yet ready; finished results stay.",
|
|
604
|
+
gap: null,
|
|
605
|
+
},
|
|
586
606
|
propose_scope: {
|
|
587
607
|
support: "direct",
|
|
588
608
|
how: "Confirm rewrites the scope through the shared door; the confirmed card links Review & start for the exact task.",
|
package/dist/chat-continuity.js
CHANGED
|
@@ -63,7 +63,7 @@ export const CHAT_CONTINUITY_SCRIPT = String.raw `
|
|
|
63
63
|
var carry=JSON.parse(sessionStorage.getItem(carryKey)||'null');sessionStorage.removeItem(carryKey);
|
|
64
64
|
if(!draft&&carry&&user&&carry.owner===user&&carry.task===task&&typeof carry.text==='string'&&carry.text.length<=2000&&typeof carry.at==='number'&&carry.at>Date.now()-86400000){box.value=carry.text;draft={text:carry.text,request:request.value,submitted:false,at:Date.now()};save();}
|
|
65
65
|
}catch(e){storage=false;}
|
|
66
|
-
function buttons(){document.querySelectorAll('form[action="/chat"] button[type="submit"]').forEach(function(button){button.disabled=
|
|
66
|
+
function buttons(){document.querySelectorAll('form[action="/chat"] button[type="submit"]').forEach(function(button){button.disabled=sending||stale||offline;});}
|
|
67
67
|
function showReconnect(on){if(reconnect)reconnect.hidden=!on;}
|
|
68
68
|
function lostAuthority(){stale=true;buttons();showReconnect(true);say('Sign in again to reconnect. Your draft stays in this tab.');}
|
|
69
69
|
function changed(){stale=true;buttons();showReconnect(true);say('This conversation changed or ended. Your draft stays in this tab; reconnect to continue.');}
|
|
@@ -97,7 +97,8 @@ export const CHAT_CONTINUITY_SCRIPT = String.raw `
|
|
|
97
97
|
if(!(one instanceof HTMLFormElement))return;
|
|
98
98
|
var action=one.getAttribute('action')||'';
|
|
99
99
|
if(action==='/chat'){
|
|
100
|
-
|
|
100
|
+
// A reply in progress no longer holds the composer: a message sent now waits its turn (the lead queue).
|
|
101
|
+
if(sending||stale||offline){event.preventDefault();return;}
|
|
101
102
|
bind(one);
|
|
102
103
|
if(one===form){
|
|
103
104
|
if(!box.value.trim()){event.preventDefault();return;}
|
|
@@ -125,7 +126,7 @@ export const CHAT_CONTINUITY_SCRIPT = String.raw `
|
|
|
125
126
|
var data=got.data||{};
|
|
126
127
|
if(data.ok===true){
|
|
127
128
|
if(String(data.session)!==session){changed();return;}
|
|
128
|
-
say('Sent. Waiting for the reply…');later(300);return;
|
|
129
|
+
say(data.queued===true?'Queued. It goes to the lead after the reply in progress.':'Sent. Waiting for the reply…');later(300);return;
|
|
129
130
|
}
|
|
130
131
|
// Refused, so nothing was received: the draft stays, editable and
|
|
131
132
|
// resendable under its own key; the words are the server's.
|
|
@@ -302,7 +303,7 @@ export const CHAT_CONTINUITY_SCRIPT = String.raw `
|
|
|
302
303
|
}
|
|
303
304
|
settleLive();
|
|
304
305
|
busy=data.pending;buttons();
|
|
305
|
-
say(busy?'Reply in progress.
|
|
306
|
+
say(busy?'Reply in progress. A message you send now waits its turn.':sent?'Message not confirmed. Check the conversation before retrying.':'');
|
|
306
307
|
later(busy?2500:5000);
|
|
307
308
|
}catch(e){say('Connection lost. Reconnecting… Your existing work is not cancelled.');later(10000);}
|
|
308
309
|
finally{polling=false;}
|
package/dist/chat-decide.d.ts
CHANGED
|
@@ -1,17 +1,3 @@
|
|
|
1
|
-
/** Decisions finish in the chat app. A pushed card about a result, a failed task, a plan waiting for approval or a
|
|
2
|
-
* pull request ready to merge carries buttons that act in place: Accept and finish, Request changes, Retry,
|
|
3
|
-
* Approve & start, Merge. Every act that changes something asks twice ("Accept and finish 'X'? [Yes] [Cancel]"),
|
|
4
|
-
* and each button is one opaque token bound to the exact result, receipt, plan or commit the card showed: a card a
|
|
5
|
-
* newer result, a finished task or a changed plan has overtaken says so and acts on nothing.
|
|
6
|
-
*
|
|
7
|
-
* Approving a plan or merging needs the password, which never goes in chat, so they act in chat only where the owner
|
|
8
|
-
* turned approving from chat on (their lasting setting, chat-approval.ts, or a signed mode's chatApprove term), for
|
|
9
|
-
* their own chat; otherwise, and for a plan that widens permissions, exceeds the limit or touches protected paths, the
|
|
10
|
-
* card keeps its link.
|
|
11
|
-
*
|
|
12
|
-
* The same in every chat app: Telegram's bridge and the shared engine Slack, Discord and Teams run on (chat-delivery.ts)
|
|
13
|
-
* offer, mint and answer these through the same tokens (the `channel` column). Every act runs through the door the
|
|
14
|
-
* console uses. */
|
|
15
1
|
import { DECIDE_ACTS } from "./contracts/chat-callback-rows.js";
|
|
16
2
|
import { type ChatApprovalLimits } from "./chat-approval.js";
|
|
17
3
|
import { type VerifiedApprover } from "./principal.js";
|
package/dist/chat-decide.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* The same in every chat app: Telegram's bridge and the shared engine Slack, Discord and Teams run on (chat-delivery.ts)
|
|
13
13
|
* offer, mint and answer these through the same tokens (the `channel` column). Every act runs through the door the
|
|
14
14
|
* console uses. */
|
|
15
|
+
import { isAcpProfile } from "./acp-agents.js";
|
|
15
16
|
import { DECIDE_ACTS, readDecideActionRow } from "./contracts/chat-callback-rows.js";
|
|
16
17
|
import { createHash, randomBytes } from "node:crypto";
|
|
17
18
|
import { acceptAndCompleteAsOperator, assignmentOf, owedAcceptance } from "./assignment.js";
|
|
@@ -81,13 +82,15 @@ export function permissionPlainWords(profile) {
|
|
|
81
82
|
}
|
|
82
83
|
if (profile.provider === "gemini")
|
|
83
84
|
return profile.approvalArgv === "yolo" ? full : "file edits only; other tools are refused";
|
|
85
|
+
if (isAcpProfile(profile))
|
|
86
|
+
return "full access inside the OS fence, nothing asks first";
|
|
84
87
|
return profile.sandboxMode === "danger-full-access" ? full : "file edits and commands inside the project only";
|
|
85
88
|
}
|
|
86
89
|
function fullAccess(profile) {
|
|
87
90
|
if (profile === null || profile === undefined)
|
|
88
91
|
return false;
|
|
89
92
|
return profile.provider === "claude" ? profile.permissionArgv === "bypassPermissions"
|
|
90
|
-
: profile.provider === "gemini" ? profile.approvalArgv === "yolo" : profile.sandboxMode === "danger-full-access";
|
|
93
|
+
: profile.provider === "gemini" ? profile.approvalArgv === "yolo" : isAcpProfile(profile) ? true : profile.sandboxMode === "danger-full-access";
|
|
91
94
|
}
|
|
92
95
|
const money = (micro) => `$${(micro / 1_000_000).toFixed(2)}`;
|
|
93
96
|
/** A plan this person's chat may approve now: the term, the plan's exact bytes, and none of the reasons it must open
|
package/dist/chat-delivery.d.ts
CHANGED
|
@@ -3,6 +3,15 @@ import { ChatState, type ChatBinding, type ChatContent, type ChatEvent, type Cha
|
|
|
3
3
|
import { type Store } from "./store.js";
|
|
4
4
|
import type { SubscriptionLeadRunner } from "./subscription-chat.js";
|
|
5
5
|
import { type ChatMerge } from "./chat-decide.js";
|
|
6
|
+
/**
|
|
7
|
+
* The watch at a chat app's final fence (cockpit §3, R11), right before the network: a notice about an agent its
|
|
8
|
+
* person has open now waits, as a held part, until the watch ends; then it is delivered as usual. The subject is the
|
|
9
|
+
* part's own run, or its task's live build. Only a part planned with `watch` (never a security alert or the digest)
|
|
10
|
+
* can be held. True: held, so nothing is sent now.
|
|
11
|
+
*/
|
|
12
|
+
export declare function heldForWatch(state: ChatState, store: Store, part: number, event: {
|
|
13
|
+
kind: string;
|
|
14
|
+
}, approver: string, content: ChatContent, now: Date): boolean;
|
|
6
15
|
export declare const chatObject: (v: unknown) => Record<string, unknown>;
|
|
7
16
|
export type ChatDeliveryOptions = {
|
|
8
17
|
store: Store;
|
package/dist/chat-delivery.js
CHANGED
|
@@ -26,7 +26,33 @@ import { isShotsKind, resultShotsFor } from "./result-shots.js";
|
|
|
26
26
|
import { CHAT_APP_NAMES } from "./flow-triggers.js";
|
|
27
27
|
import { answersPrompt, applyDecideFeedback, applyDecideTap, decideFallbackLink, decideOffer, decideTargetOf, hasLiveDecideTokens, linksFor, mergedText, mintDecideButtons, offerFingerprint, openPromptFor, recordChatMerge } from "./chat-decide.js";
|
|
28
28
|
import { mergePullRequest } from "./pull-request-flow.js";
|
|
29
|
-
import { watchedPing } from "./agent-board.js";
|
|
29
|
+
import { attentionSubjectOf, neverHeld, watchedPing } from "./agent-board.js";
|
|
30
|
+
/**
|
|
31
|
+
* The watch at a chat app's final fence (cockpit §3, R11), right before the network: a notice about an agent its
|
|
32
|
+
* person has open now waits, as a held part, until the watch ends; then it is delivered as usual. The subject is the
|
|
33
|
+
* part's own run, or its task's live build. Only a part planned with `watch` (never a security alert or the digest)
|
|
34
|
+
* can be held. True: held, so nothing is sent now.
|
|
35
|
+
*/
|
|
36
|
+
export function heldForWatch(state, store, part, event, approver, content, now) {
|
|
37
|
+
// Fail closed: only a notice planned as holdable (a kind a watch may hold) is ever held here.
|
|
38
|
+
if (event.kind !== "notice" || content.watch !== true)
|
|
39
|
+
return false;
|
|
40
|
+
// One part may cover several tasks (a grouped card, a batch): it waits only while every one of them is watched,
|
|
41
|
+
// until the first of those watches ends.
|
|
42
|
+
const subjects = content.subjects ?? [{ ref: content.task ? store.lookupRef(content.task)?.id ?? null : null, run: content.run ?? null }];
|
|
43
|
+
if (subjects.length === 0)
|
|
44
|
+
return false;
|
|
45
|
+
let until = null;
|
|
46
|
+
for (const subject of subjects) {
|
|
47
|
+
const decided = watchedPing(store, { kind: "notice", run: subject.run, taskRef: subject.ref }, approver, now);
|
|
48
|
+
if (decided === "send" || decided === "skip")
|
|
49
|
+
return false;
|
|
50
|
+
if (until === null || decided.hold < until)
|
|
51
|
+
until = decided.hold;
|
|
52
|
+
}
|
|
53
|
+
state.holdPart(part, until);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
30
56
|
export const chatObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v)
|
|
31
57
|
? v
|
|
32
58
|
: {};
|
|
@@ -397,8 +423,9 @@ export async function processChatEvent(options) {
|
|
|
397
423
|
return false;
|
|
398
424
|
if (!outcome.ok &&
|
|
399
425
|
"refused" in outcome &&
|
|
400
|
-
outcome.refused === "concurrent") {
|
|
401
|
-
|
|
426
|
+
(outcome.refused === "concurrent" || outcome.refused === "backlog")) {
|
|
427
|
+
// The lead is answering, or messages sent earlier wait in the person's lead queue: this one waits its turn.
|
|
428
|
+
state.defer(event.id, outcome.refused === "backlog" ? "Waiting behind earlier messages" : "Assistant is answering another message", new Date(nowOf(options).getTime() + 5000));
|
|
402
429
|
return true;
|
|
403
430
|
}
|
|
404
431
|
receipt = store.leadRequestReceipt(resolved.session.id, request);
|
|
@@ -422,7 +449,7 @@ export async function processChatEvent(options) {
|
|
|
422
449
|
if (turn?.state !== "answered") {
|
|
423
450
|
state.plan(event.id, [
|
|
424
451
|
{
|
|
425
|
-
text: leadFailureText(turn?.failureReason ?? null),
|
|
452
|
+
text: leadFailureText(turn?.failureReason ?? null, turn != null && store.leadTurnCostUnconfirmed(turn.id)),
|
|
426
453
|
},
|
|
427
454
|
], nowOf(options));
|
|
428
455
|
return true;
|
|
@@ -734,7 +761,12 @@ function planQuietCard(options, binding, notification, now) {
|
|
|
734
761
|
const view = quietCardView(store, card.tasks, now, options.evidenceRoot, binding.approver);
|
|
735
762
|
if (view === null)
|
|
736
763
|
return;
|
|
737
|
-
|
|
764
|
+
// A card about one task is that task's: its person's watch may hold it at the final fence (never a security kind).
|
|
765
|
+
// A card about its tasks is theirs: its person's watch may hold it at the final fence while every one of them is
|
|
766
|
+
// watched (never for a security kind).
|
|
767
|
+
const content = { text: view.text, link: view.link, ...(card.tasks.length === 1 ? { task: notification.taskId } : {}),
|
|
768
|
+
...(card.tasks.length > 0 && !neverHeld(notification.kind) ? { watch: true } : {}),
|
|
769
|
+
...(card.tasks.length > 1 && !neverHeld(notification.kind) ? { subjects: card.tasks.slice(0, 100).map(ref => ({ ref, run: null })) } : {}) };
|
|
738
770
|
const shown = chatHash(JSON.stringify(content));
|
|
739
771
|
if (card.message !== null) {
|
|
740
772
|
if (card.digest === shown)
|
|
@@ -763,7 +795,12 @@ function planFinished(options, binding, taskRef, run, now, notification = null,
|
|
|
763
795
|
// A single update keeps its task and run, so a reply to it names that work.
|
|
764
796
|
const single = batch.items.length === 1 ? batch.items[0] : null;
|
|
765
797
|
const task = single === null ? null : store.refById(single.taskRef)?.externalId ?? null;
|
|
766
|
-
|
|
798
|
+
// Holdable when its fact is a kind a watch may hold (the batch line for a finished run, or the notification joining
|
|
799
|
+
// it); a batch of several holds only while every one of them is watched.
|
|
800
|
+
const joining = notification === null ? null : options.store.notificationById(notification);
|
|
801
|
+
const holdable = batch.items.length > 0 && (joining === null || !neverHeld(joining.kind));
|
|
802
|
+
const about = { ...(task === null ? {} : { task }), ...(single?.run == null ? {} : { run: single.run }), ...(holdable ? { watch: true } : {}),
|
|
803
|
+
...(holdable && batch.items.length > 1 ? { subjects: batch.items.slice(0, 100).map(one => ({ ref: one.taskRef, run: one.run })) } : {}) };
|
|
767
804
|
// A message about one result, plan, failure or pull request acts in place (chat-decide.ts), as on Telegram: its
|
|
768
805
|
// buttons are minted only when what it offers changed, or its own were spent (Not now), so an unchanged repaint keeps them.
|
|
769
806
|
const offer = decideOfferFor(options, binding, view.target, repos, now);
|
|
@@ -962,6 +999,7 @@ function offerNotice(options, notification, binding, registry) {
|
|
|
962
999
|
task: notification.taskId,
|
|
963
1000
|
run: run.id,
|
|
964
1001
|
link: card.link,
|
|
1002
|
+
...(attentionSubjectOf(store, notification) !== null ? { watch: true } : {}),
|
|
965
1003
|
};
|
|
966
1004
|
const digest = chatHash(JSON.stringify(content));
|
|
967
1005
|
const prior = state
|
|
@@ -1044,6 +1082,7 @@ function offerNotice(options, notification, binding, registry) {
|
|
|
1044
1082
|
text: offer?.text ?? chatText(phoneText(notification.body === "" ? leadSubjectOf(store, notification, binding.approver) : `${leadSubjectOf(store, notification, binding.approver)}\n\n${notification.body}`, 2500), notification.taskId === null ? [] : [{ id: notification.taskId, title: chatTitle(store, notification.taskId) }]),
|
|
1045
1083
|
...(notification.taskId ? { task: notification.taskId } : {}),
|
|
1046
1084
|
...(run ? { run: run.id } : {}),
|
|
1085
|
+
...(attentionSubjectOf(store, notification) !== null ? { watch: true } : {}),
|
|
1047
1086
|
...(offer !== null ? { decide: mintDecide(options, binding, target, offer, now) }
|
|
1048
1087
|
: fallback !== null ? { link: fallback }
|
|
1049
1088
|
: notification.link
|
package/dist/chat-messages.d.ts
CHANGED
|
@@ -174,6 +174,8 @@ export declare class ChatMessages extends ChatCore {
|
|
|
174
174
|
taskId: string | null;
|
|
175
175
|
sourceRun: number | null;
|
|
176
176
|
}, now: Date): number;
|
|
177
|
+
/** How many of this chat's messages wait for, or are being answered by, the lead (the lead queue's bound per chat). */
|
|
178
|
+
messagesWaiting(binding: number): number;
|
|
177
179
|
/** Whether a message from this chat is already waiting on, or being answered from, this bot message (a tapped question). */
|
|
178
180
|
messageWaitingOn(binding: number, messageId: string): boolean;
|
|
179
181
|
getMessage(id: number): TelegramConversation | null;
|
|
@@ -294,6 +296,12 @@ export declare class ChatMessages extends ChatCore {
|
|
|
294
296
|
claimDeliveries(binding: TelegramBinding, owner: string, ttlMs: number, now: Date, only?: "all" | "urgent"): TelegramDelivery[];
|
|
295
297
|
/** Synchronous final fence, called AFTER each asynchronous enrollment read. */
|
|
296
298
|
deliveryProblem(row: TelegramDelivery, binding: TelegramBinding, owner: string, projects: readonly string[], now: Date, batch?: readonly number[]): string | null;
|
|
299
|
+
/**
|
|
300
|
+
* The watch at the final fence (cockpit §3, R11): right before the network, a person who has since opened the
|
|
301
|
+
* agent this ping is about holds it — the claim is released with the watch's end as its next attempt, no attempt
|
|
302
|
+
* spent — and news about a finished run they have now seen settles quietly. "send": nothing changed.
|
|
303
|
+
*/
|
|
304
|
+
watchFence(row: TelegramDelivery, binding: TelegramBinding, owner: string, now: Date): "send" | "held" | "skipped";
|
|
297
305
|
claimHeld(row: TelegramDelivery, owner: string, now: Date): boolean;
|
|
298
306
|
/** A confirmed network message remains history even if authority changed
|
|
299
307
|
* while it was in flight. It only binds the OLD destination, never new work. */
|