toolroll 0.9.63 → 0.9.65
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 +51 -0
- package/README.md +15 -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 +50 -2
- package/dist/agent-fence.js +157 -10
- 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 +36 -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/browser/THIRD_PARTY_NOTICES.txt +48 -0
- package/dist/browser/terminal.css +37 -0
- package/dist/browser/terminal.js +117 -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 +6 -1
- package/dist/builder.js +34 -13
- 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 +12 -4
- package/dist/contracts/console-api.js +12 -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 +17 -0
- package/dist/invoke.js +217 -21
- 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 +258 -0
- package/dist/live-terminal.js +649 -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 +2 -0
- package/dist/operate.js +275 -17
- 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 +12 -0
- package/dist/project-delete-ui.js +41 -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.d.ts +3 -0
- package/dist/release.js +14 -8
- package/dist/request-budget-ui.js +2 -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 +170 -5
- package/dist/scope.js +251 -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 +247 -40
- package/dist/server/chat.d.ts +1 -1
- package/dist/server/chat.js +122 -31
- package/dist/server/chrome.d.ts +6 -1
- package/dist/server/chrome.js +18 -7
- package/dist/server/console.css +52 -38
- 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 +4 -1
- package/dist/server/render-pages.js +5 -4
- 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 +110 -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 +46 -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 +121 -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 +320 -74
- 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 +430 -15
- package/dist/store.js +1041 -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/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 +2 -0
- package/dist/tool-broker.js +1 -0
- package/dist/toolroll-update-ui.js +2 -2
- package/dist/toolroll-update.js +13 -3
- package/dist/tools-ui.js +2 -2
- 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/package.json +11 -2
package/dist/exec.js
CHANGED
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
* command. And nothing here rejects: a failed command is a value. Discovery
|
|
7
7
|
* walks repos it has never seen, and one broken repo must not end the scan.
|
|
8
8
|
*/
|
|
9
|
+
import { ACP_LINE_CAP, createAcpConversation } from "./acp-client.js";
|
|
9
10
|
import { linuxFenceAvailable, linuxFenced, macosFenceAvailable, macosFenced } from "./agent-fence.js";
|
|
10
|
-
import { execFile, spawn } from "node:child_process";
|
|
11
|
-
import { realpathSync } from "node:fs";
|
|
11
|
+
import { execFile, execFileSync, spawn } from "node:child_process";
|
|
12
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
12
14
|
import { observeProcessTree, stopProcessTree } from "./process-tree.js";
|
|
13
15
|
import { jsonlDiscriminants } from "./jsonl-discriminants.js";
|
|
14
16
|
import { createContainer, currentContainment } from "./containment.js";
|
|
@@ -45,6 +47,24 @@ const containers = new WeakMap();
|
|
|
45
47
|
export class ContainmentRefusal extends Error {
|
|
46
48
|
name = "ContainmentRefusal";
|
|
47
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* The command as it runs inside the agent fence (sandbox-exec or bubblewrap wrapping it), or as it is when it has no
|
|
52
|
+
* fence. A spawn whose confinement is required and that no backend here can wrap is refused (ContainmentRefusal):
|
|
53
|
+
* nothing runs, and nothing falls back to running it unwrapped.
|
|
54
|
+
*/
|
|
55
|
+
export function confined(file, args, bag) {
|
|
56
|
+
const fence = bag.fence ?? [];
|
|
57
|
+
if (fence.length > 0 && macosFenceAvailable())
|
|
58
|
+
return macosFenced(file, args, fence, bag.fenceGrants);
|
|
59
|
+
if (fence.length > 0 && linuxFenceAvailable())
|
|
60
|
+
return linuxFenced(file, args, fence, bag.fenceGrants);
|
|
61
|
+
if (bag.confinement === "required") {
|
|
62
|
+
throw new ContainmentRefusal(fence.length === 0
|
|
63
|
+
? "This process must run inside the agent fence, and it was given none."
|
|
64
|
+
: "This machine can't confine a build (no macOS sandbox, no working bubblewrap), so nothing was started.");
|
|
65
|
+
}
|
|
66
|
+
return { file, args: [...args] };
|
|
67
|
+
}
|
|
48
68
|
/** A spawn callback refused custody AFTER the child existed: the caller
|
|
49
69
|
* must reap exactly this child before answering. */
|
|
50
70
|
class SpawnCustodyFailure extends Error {
|
|
@@ -72,10 +92,7 @@ function rejectedChild(error, child) {
|
|
|
72
92
|
*/
|
|
73
93
|
function spawnContained(file, args, spawnOptions, label, bag, contain = true) {
|
|
74
94
|
// The agent fence wraps the target itself (sandbox-exec then execs it, same pid).
|
|
75
|
-
|
|
76
|
-
({ file, args } = macosFenced(file, args, bag.fence, bag.fenceGrants));
|
|
77
|
-
else if (bag.fence !== undefined && bag.fence.length > 0 && linuxFenceAvailable())
|
|
78
|
-
({ file, args } = linuxFenced(file, args, bag.fence, bag.fenceGrants));
|
|
95
|
+
({ file, args } = confined(file, args, bag));
|
|
79
96
|
// The policy refusal comes first: nothing is reserved, recorded or
|
|
80
97
|
// spawned for a spawn that cannot be contained as required.
|
|
81
98
|
const effective = contain ? currentContainment() : null;
|
|
@@ -224,7 +241,7 @@ function containmentOf(container, empty, attachFailure) {
|
|
|
224
241
|
}
|
|
225
242
|
/** The streaming transports' spawn: contained when the spawn is a provider (process group), plain otherwise. */
|
|
226
243
|
function spawnStream(file, args, options, stdio, childEnv) {
|
|
227
|
-
return spawnContained(file, args, { cwd: options.cwd, env: childEnv, stdio, detached: options.processGroup === true && process.platform !== "win32" }, options.owner ?? "stream", { beforeSpawn: options.beforeSpawn, onSpawn: options.onSpawn, onSpawnFailed: options.onSpawnFailed, onContainer: options.onContainer, onContainerEmpty: options.onContainerEmpty, onUnknown: options.onUnknown, fence: options.fence, fenceGrants: options.fenceGrants }, options.processGroup === true);
|
|
244
|
+
return spawnContained(file, args, { cwd: options.cwd, env: childEnv, stdio, detached: options.processGroup === true && process.platform !== "win32" }, options.owner ?? "stream", { beforeSpawn: options.beforeSpawn, onSpawn: options.onSpawn, onSpawnFailed: options.onSpawnFailed, onContainer: options.onContainer, onContainerEmpty: options.onContainerEmpty, onUnknown: options.onUnknown, fence: options.fence, fenceGrants: options.fenceGrants, confinement: options.confinement }, options.processGroup === true);
|
|
228
245
|
}
|
|
229
246
|
/** The refusal-before-spawn shape shared by every containable transport. */
|
|
230
247
|
function refusedResult(error) {
|
|
@@ -522,6 +539,7 @@ export function run(file, args, options = {}) {
|
|
|
522
539
|
...(options.onStderr === undefined ? {} : { onStderr: options.onStderr }),
|
|
523
540
|
...(options.fence === undefined ? {} : { fence: options.fence }),
|
|
524
541
|
...(options.fenceGrants === undefined ? {} : { fenceGrants: options.fenceGrants }),
|
|
542
|
+
...(options.confinement === undefined ? {} : { confinement: options.confinement }),
|
|
525
543
|
...(options.onDescendant === undefined ? {} : { onDescendant: options.onDescendant }),
|
|
526
544
|
...(options.onDescendantWriteFailure === undefined ? {} : { onDescendantWriteFailure: options.onDescendantWriteFailure }),
|
|
527
545
|
...(options.onDescendantExit === undefined ? {} : { onDescendantExit: options.onDescendantExit }),
|
|
@@ -531,8 +549,19 @@ export function run(file, args, options = {}) {
|
|
|
531
549
|
...(options.onContainerEmpty === undefined ? {} : { onContainerEmpty: options.onContainerEmpty }),
|
|
532
550
|
}));
|
|
533
551
|
}
|
|
552
|
+
// The plain road wraps exactly as the process-group road does: a fence given here is never dropped, and a spawn
|
|
553
|
+
// whose confinement is required and can't be wrapped refuses before anything runs.
|
|
554
|
+
let wrapped;
|
|
555
|
+
try {
|
|
556
|
+
wrapped = confined(file, args, options);
|
|
557
|
+
}
|
|
558
|
+
catch (error) {
|
|
559
|
+
if (!(error instanceof ContainmentRefusal))
|
|
560
|
+
throw error;
|
|
561
|
+
return Promise.resolve({ code: CONTAINMENT_REFUSED_CODE, stdout: "", stderr: error.message, timedOut: false, notFound: false, containment: { refused: error.message } });
|
|
562
|
+
}
|
|
534
563
|
return retryTransientSpawn(() => new Promise(resolve => {
|
|
535
|
-
execFile(file,
|
|
564
|
+
execFile(wrapped.file, wrapped.args, {
|
|
536
565
|
cwd,
|
|
537
566
|
timeout: timeoutMs,
|
|
538
567
|
maxBuffer,
|
|
@@ -564,7 +593,7 @@ function runBufferedGroup(file, args, bag) {
|
|
|
564
593
|
let container = null;
|
|
565
594
|
let attached;
|
|
566
595
|
try {
|
|
567
|
-
const spawned = spawnContained(file, args, { cwd: bag.cwd, env: bag.childEnv, stdio: [bag.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"], detached: process.platform !== "win32" }, bag.owner ?? "buffered", { beforeSpawn: bag.beforeSpawn, onSpawn: bag.onSpawn, onSpawnFailed: bag.onSpawnFailed, onContainer: bag.onContainer, onContainerEmpty: bag.onContainerEmpty, onUnknown: bag.onUnknown, fence: bag.fence, fenceGrants: bag.fenceGrants });
|
|
596
|
+
const spawned = spawnContained(file, args, { cwd: bag.cwd, env: bag.childEnv, stdio: [bag.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"], detached: process.platform !== "win32" }, bag.owner ?? "buffered", { beforeSpawn: bag.beforeSpawn, onSpawn: bag.onSpawn, onSpawnFailed: bag.onSpawnFailed, onContainer: bag.onContainer, onContainerEmpty: bag.onContainerEmpty, onUnknown: bag.onUnknown, fence: bag.fence, fenceGrants: bag.fenceGrants, confinement: bag.confinement });
|
|
568
597
|
child = spawned.child;
|
|
569
598
|
container = spawned.container;
|
|
570
599
|
attached = spawned.attached;
|
|
@@ -1403,6 +1432,335 @@ export function runGeminiStreamJsonl(file, args, options = {}) {
|
|
|
1403
1432
|
child.on("close", code => finish(code));
|
|
1404
1433
|
});
|
|
1405
1434
|
}
|
|
1435
|
+
/** One process listing: ids, group, state and command name only (never arguments — they can carry a brief). */
|
|
1436
|
+
function processRows() {
|
|
1437
|
+
try {
|
|
1438
|
+
return execFileSync("/bin/ps", ["-axo", "pid=,ppid=,pgid=,stat=,comm="], { encoding: "utf8", timeout: 2_000, maxBuffer: 8 * 1024 * 1024 })
|
|
1439
|
+
.split("\n").flatMap(line => {
|
|
1440
|
+
const parts = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/.exec(line);
|
|
1441
|
+
return parts === null ? [] : [{ pid: Number(parts[1]), parent: Number(parts[2]), group: Number(parts[3]), state: parts[4], name: parts[5].trim() }];
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
catch {
|
|
1445
|
+
return null;
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
/**
|
|
1449
|
+
* What the agent started and is still running: from `root` (the spawned process — the agent itself, or a fence
|
|
1450
|
+
* wrapper around it), down to the agent's own process (the first whose command is the agent's launch, `names`), then
|
|
1451
|
+
* every live, non-zombie process beneath it, with its group. The agent itself and any wrapper are never counted.
|
|
1452
|
+
*/
|
|
1453
|
+
function liveUnderAgent(root, names) {
|
|
1454
|
+
const rows = processRows();
|
|
1455
|
+
if (rows === null)
|
|
1456
|
+
return null;
|
|
1457
|
+
const children = new Map();
|
|
1458
|
+
for (const row of rows)
|
|
1459
|
+
children.set(row.parent, [...(children.get(row.parent) ?? []), row]);
|
|
1460
|
+
const self = rows.find(row => row.pid === root);
|
|
1461
|
+
const named = (row) => names.some(name => row.name === name || row.name.endsWith(`/${name}`));
|
|
1462
|
+
// The agent: the root when it is the agent (macOS sandbox-exec execs in place), else the first named descendant.
|
|
1463
|
+
let agent = self !== undefined && named(self) ? root : null;
|
|
1464
|
+
const pending = [...(children.get(root) ?? [])];
|
|
1465
|
+
while (agent === null && pending.length > 0) {
|
|
1466
|
+
const row = pending.shift();
|
|
1467
|
+
if (named(row))
|
|
1468
|
+
agent = row.pid;
|
|
1469
|
+
else
|
|
1470
|
+
pending.push(...(children.get(row.pid) ?? []));
|
|
1471
|
+
}
|
|
1472
|
+
if (agent === null)
|
|
1473
|
+
return [];
|
|
1474
|
+
const found = [];
|
|
1475
|
+
const below = [...(children.get(agent) ?? [])];
|
|
1476
|
+
while (below.length > 0) {
|
|
1477
|
+
const row = below.pop();
|
|
1478
|
+
if (!row.state.startsWith("Z"))
|
|
1479
|
+
found.push({ pid: row.pid, group: row.group });
|
|
1480
|
+
below.push(...(children.get(row.pid) ?? []));
|
|
1481
|
+
}
|
|
1482
|
+
return found;
|
|
1483
|
+
}
|
|
1484
|
+
/** How long each shutdown step waits: session/close's answer, then exit after stdin closes, then exit after SIGTERM. */
|
|
1485
|
+
export const ACP_SHUTDOWN_MS = { close: 2_000, eof: 5_000, term: 5_000 };
|
|
1486
|
+
/** How long each handshake step (initialize, session/new, choosing the model) may take before the agent is stopped. */
|
|
1487
|
+
export const ACP_HANDSHAKE_MS = 30_000;
|
|
1488
|
+
/** After the agent itself has exited, how long its pipes may stay open (a descendant holding them) before they are closed. */
|
|
1489
|
+
const ACP_PIPES_AFTER_EXIT_MS = 5_000;
|
|
1490
|
+
/** SIGTERM the child's process group (POSIX) or the child alone; a container's own stop is the forced one, later. */
|
|
1491
|
+
function termGroup(child) {
|
|
1492
|
+
const pid = child.pid;
|
|
1493
|
+
try {
|
|
1494
|
+
if (pid !== undefined && process.platform !== "win32")
|
|
1495
|
+
process.kill(-pid, "SIGTERM");
|
|
1496
|
+
else
|
|
1497
|
+
child.kill("SIGTERM");
|
|
1498
|
+
}
|
|
1499
|
+
catch {
|
|
1500
|
+
// Already gone.
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
/**
|
|
1504
|
+
* One ACP build conversation over the agent's stdio (docs/plans/acp-adapter.md § Gateway, runner, custody). The
|
|
1505
|
+
* conversation itself is acp-client.ts; this owns the process: the same contained spawn, ownership, descendant
|
|
1506
|
+
* observation, container settlement and watchdog every other runner uses, with both clocks at once.
|
|
1507
|
+
*
|
|
1508
|
+
* After the prompt is answered it shuts the agent down in bounded steps — session/close (when offered), stdin EOF,
|
|
1509
|
+
* SIGTERM to the group, then the forced kill — and returns only when the process has settled, so the gateway's
|
|
1510
|
+
* witness records the real exit. A protocol failure kills at once. A stop, from the gateway's stop watch, kills at
|
|
1511
|
+
* any step. The result's stdout is the conversation's retained record, never the agent's stream; its code is 0 only
|
|
1512
|
+
* when the prompt was answered end_turn and the agent then exited cleanly or on our own signal.
|
|
1513
|
+
*/
|
|
1514
|
+
export function runAcpSession(file, args, options = {}) {
|
|
1515
|
+
const acp = options.acp;
|
|
1516
|
+
if (acp === undefined)
|
|
1517
|
+
return Promise.resolve({ code: CONTAINMENT_REFUSED_CODE, stdout: "", stderr: "an ACP run needs its conversation terms", timedOut: false, notFound: false });
|
|
1518
|
+
const childEnv = resolveChildEnv(options);
|
|
1519
|
+
return new Promise(resolve => {
|
|
1520
|
+
let child;
|
|
1521
|
+
let container = null;
|
|
1522
|
+
let attachFailure = null;
|
|
1523
|
+
try {
|
|
1524
|
+
const spawned = spawnStream(file, args, options, ["pipe", "pipe", "pipe"], childEnv);
|
|
1525
|
+
child = spawned.child;
|
|
1526
|
+
container = spawned.container;
|
|
1527
|
+
void spawned.attached.then(outcome => { if (!outcome.ok)
|
|
1528
|
+
attachFailure = outcome.detail; });
|
|
1529
|
+
}
|
|
1530
|
+
catch (error) {
|
|
1531
|
+
void reapRejectedSpawn(rejectedChild(error, child), options.processGroup === true).then(() => resolve(refusedResult(error)));
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
if (options.processGroup === true)
|
|
1535
|
+
liveProviders.add(child);
|
|
1536
|
+
registerOwned(options.owner, child);
|
|
1537
|
+
if (options.processGroup === true && container === null)
|
|
1538
|
+
observeProcessTree(child, options);
|
|
1539
|
+
const conversation = createAcpConversation({ cwd: options.cwd ?? process.cwd(), prompt: acp.prompt, model: acp.model, expect: acp.expect });
|
|
1540
|
+
// The agent's own process, as a listing names it: the launched file, or the script a pinned interpreter runs.
|
|
1541
|
+
const agentNames = [...new Set([file, ...args.slice(0, 2)].filter(one => one.startsWith("/")).map(one => one.split("/").pop()).filter(one => one !== ""))];
|
|
1542
|
+
let stderr = "";
|
|
1543
|
+
let timedOut = false;
|
|
1544
|
+
let notFound = false;
|
|
1545
|
+
let settled = false;
|
|
1546
|
+
let shuttingDown = false;
|
|
1547
|
+
/** The signals the plane itself sent after the prompt was answered, by its own shutdown ladder: an exit by one of
|
|
1548
|
+
* those is clean. A stop or a timeout kills through other roads and never sets these. */
|
|
1549
|
+
let termAfterTerminal = false;
|
|
1550
|
+
let killAfterTerminal = false;
|
|
1551
|
+
/** How the process came to end, for the record: on its own (before or after stdin closed), or by our signal. */
|
|
1552
|
+
let endedBy = "self";
|
|
1553
|
+
let announced = null;
|
|
1554
|
+
const timers = [];
|
|
1555
|
+
const later = (ms, fn) => { const timer = setTimeout(fn, ms); timer.unref?.(); timers.push(timer); };
|
|
1556
|
+
const write = (lines) => {
|
|
1557
|
+
for (const one of lines) {
|
|
1558
|
+
if (child.stdin === null || child.stdin.destroyed || child.stdin.writableEnded)
|
|
1559
|
+
return;
|
|
1560
|
+
try {
|
|
1561
|
+
child.stdin.write(`${one}\n`);
|
|
1562
|
+
}
|
|
1563
|
+
catch { /* the agent closed its stdin: its exit decides */ }
|
|
1564
|
+
}
|
|
1565
|
+
};
|
|
1566
|
+
const forceKill = () => {
|
|
1567
|
+
endedBy = "kill";
|
|
1568
|
+
if (options.processGroup === true)
|
|
1569
|
+
killGroup(child);
|
|
1570
|
+
else
|
|
1571
|
+
child.kill("SIGKILL");
|
|
1572
|
+
};
|
|
1573
|
+
/** Each handshake step has its own deadline, beside the run's clocks. */
|
|
1574
|
+
let handshakePhase = null;
|
|
1575
|
+
const armHandshake = () => {
|
|
1576
|
+
const phase = conversation.phase();
|
|
1577
|
+
if (phase === handshakePhase || (phase !== "initialize" && phase !== "session" && phase !== "model"))
|
|
1578
|
+
return;
|
|
1579
|
+
handshakePhase = phase;
|
|
1580
|
+
const limit = acp.handshakeMs ?? ACP_HANDSHAKE_MS;
|
|
1581
|
+
later(limit, () => {
|
|
1582
|
+
if (settled || conversation.phase() !== phase)
|
|
1583
|
+
return;
|
|
1584
|
+
conversation.refuse(`the agent took more than ${limit / 1000} s to answer ${phase === "initialize" ? "initialize" : phase === "session" ? "session/new" : "the model choice"}`);
|
|
1585
|
+
forceKill();
|
|
1586
|
+
});
|
|
1587
|
+
};
|
|
1588
|
+
const exited = () => child.exitCode !== null || child.signalCode !== null;
|
|
1589
|
+
const shutdown = () => {
|
|
1590
|
+
if (shuttingDown)
|
|
1591
|
+
return;
|
|
1592
|
+
shuttingDown = true;
|
|
1593
|
+
if (conversation.phase() === "failed") {
|
|
1594
|
+
forceKill();
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
1597
|
+
const endInput = () => {
|
|
1598
|
+
if (exited())
|
|
1599
|
+
return;
|
|
1600
|
+
try {
|
|
1601
|
+
child.stdin?.end();
|
|
1602
|
+
}
|
|
1603
|
+
catch { /* gone */ }
|
|
1604
|
+
endedBy = "eof";
|
|
1605
|
+
later(ACP_SHUTDOWN_MS.eof, () => {
|
|
1606
|
+
if (exited())
|
|
1607
|
+
return;
|
|
1608
|
+
termAfterTerminal = conversation.phase() === "ended";
|
|
1609
|
+
endedBy = "term";
|
|
1610
|
+
termGroup(child);
|
|
1611
|
+
later(ACP_SHUTDOWN_MS.term, () => {
|
|
1612
|
+
if (exited())
|
|
1613
|
+
return;
|
|
1614
|
+
killAfterTerminal = conversation.phase() === "ended";
|
|
1615
|
+
forceKill();
|
|
1616
|
+
});
|
|
1617
|
+
});
|
|
1618
|
+
};
|
|
1619
|
+
const close = conversation.close();
|
|
1620
|
+
if (close.length === 0) {
|
|
1621
|
+
endInput();
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
write(close);
|
|
1625
|
+
const started = Date.now();
|
|
1626
|
+
const waitClose = () => {
|
|
1627
|
+
if (conversation.closeAnswered() || Date.now() - started >= ACP_SHUTDOWN_MS.close || exited())
|
|
1628
|
+
endInput();
|
|
1629
|
+
else
|
|
1630
|
+
later(50, waitClose);
|
|
1631
|
+
};
|
|
1632
|
+
waitClose();
|
|
1633
|
+
};
|
|
1634
|
+
const onLine = (framed) => {
|
|
1635
|
+
if (settled)
|
|
1636
|
+
return;
|
|
1637
|
+
armHandshake();
|
|
1638
|
+
const reaction = framed.overflowed
|
|
1639
|
+
? (conversation.refuse("the agent wrote a line too long to read"), { send: [], live: [] })
|
|
1640
|
+
: conversation.receive(framed.prefix.toString("utf8"));
|
|
1641
|
+
write(reaction.send);
|
|
1642
|
+
const id = conversation.sessionId();
|
|
1643
|
+
if (id !== null && announced === null) {
|
|
1644
|
+
announced = id;
|
|
1645
|
+
try {
|
|
1646
|
+
options.onSessionId?.(id);
|
|
1647
|
+
}
|
|
1648
|
+
catch { /* a registry that cannot be written must not kill the turn */ }
|
|
1649
|
+
}
|
|
1650
|
+
for (const update of reaction.live) {
|
|
1651
|
+
try {
|
|
1652
|
+
options.onStreamEvent?.({ acp: update });
|
|
1653
|
+
}
|
|
1654
|
+
catch { /* observational only */ }
|
|
1655
|
+
}
|
|
1656
|
+
const phase = conversation.phase();
|
|
1657
|
+
if (phase === "failed" || phase === "ended")
|
|
1658
|
+
shutdown();
|
|
1659
|
+
else
|
|
1660
|
+
armHandshake();
|
|
1661
|
+
};
|
|
1662
|
+
const framing = boundedJsonlFramer(ACP_LINE_CAP, onLine);
|
|
1663
|
+
const watchdog = streamWatchdog(options, () => {
|
|
1664
|
+
timedOut = true;
|
|
1665
|
+
forceKill();
|
|
1666
|
+
});
|
|
1667
|
+
child.stdout?.on("data", (chunk) => {
|
|
1668
|
+
watchdog.touch();
|
|
1669
|
+
framing.push(chunk);
|
|
1670
|
+
});
|
|
1671
|
+
child.stderr?.setEncoding("utf8");
|
|
1672
|
+
child.stderr?.on("data", (chunk) => {
|
|
1673
|
+
watchdog.touch();
|
|
1674
|
+
if (stderr.length < JSONL_STDERR_CAP)
|
|
1675
|
+
stderr += chunk.slice(0, JSONL_STDERR_CAP - stderr.length);
|
|
1676
|
+
});
|
|
1677
|
+
child.stdin?.on("error", () => { });
|
|
1678
|
+
write(conversation.start());
|
|
1679
|
+
armHandshake();
|
|
1680
|
+
try {
|
|
1681
|
+
acp.onControl?.({
|
|
1682
|
+
cancel: (deadlineMs) => {
|
|
1683
|
+
write(conversation.cancel());
|
|
1684
|
+
if (deadlineMs !== undefined)
|
|
1685
|
+
later(deadlineMs, () => {
|
|
1686
|
+
if (settled || conversation.terminal() !== null)
|
|
1687
|
+
return;
|
|
1688
|
+
conversation.refuse(`the agent didn't answer the cancel within ${Math.round(deadlineMs / 1000)} s`);
|
|
1689
|
+
forceKill();
|
|
1690
|
+
});
|
|
1691
|
+
},
|
|
1692
|
+
liveDescendants: () => process.platform === "win32" ? null : exited() || child.pid === undefined ? [] : liveUnderAgent(child.pid, agentNames),
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
catch { /* the caller's own failure */ }
|
|
1696
|
+
// The moment the agent itself exits, what it left in its own group ends with it — not at "close", which a
|
|
1697
|
+
// descendant holding the agent's pipes can postpone forever; and if those pipes are still open shortly after, they
|
|
1698
|
+
// are closed, so settlement never waits on a stray. Escaped groups are the custody observer's, as for every runner.
|
|
1699
|
+
let leftover = false;
|
|
1700
|
+
child.on("exit", () => {
|
|
1701
|
+
if (options.processGroup === true && container === null && child.pid !== undefined && process.platform !== "win32") {
|
|
1702
|
+
try {
|
|
1703
|
+
process.kill(-child.pid, 0);
|
|
1704
|
+
leftover = true;
|
|
1705
|
+
}
|
|
1706
|
+
catch {
|
|
1707
|
+
leftover = false;
|
|
1708
|
+
}
|
|
1709
|
+
try {
|
|
1710
|
+
process.kill(-child.pid, "SIGKILL");
|
|
1711
|
+
}
|
|
1712
|
+
catch { /* none left */ }
|
|
1713
|
+
}
|
|
1714
|
+
later(ACP_PIPES_AFTER_EXIT_MS, () => {
|
|
1715
|
+
if (settled)
|
|
1716
|
+
return;
|
|
1717
|
+
for (const pipe of [child.stdout, child.stderr, child.stdin]) {
|
|
1718
|
+
try {
|
|
1719
|
+
pipe?.destroy();
|
|
1720
|
+
}
|
|
1721
|
+
catch { /* gone */ }
|
|
1722
|
+
}
|
|
1723
|
+
});
|
|
1724
|
+
});
|
|
1725
|
+
const finish = (code, signal) => {
|
|
1726
|
+
if (settled)
|
|
1727
|
+
return;
|
|
1728
|
+
// The last record, even one with no newline after it, is read before anything is decided.
|
|
1729
|
+
framing.finish();
|
|
1730
|
+
settled = true;
|
|
1731
|
+
for (const timer of timers)
|
|
1732
|
+
clearTimeout(timer);
|
|
1733
|
+
watchdog.stop();
|
|
1734
|
+
liveProviders.delete(child);
|
|
1735
|
+
// Whatever the agent started in its own group and left running ends with it (a group id can't be reused while any
|
|
1736
|
+
// member lives, so this reaches only what it left). Escaped groups are the custody observer's, as for every runner.
|
|
1737
|
+
const terminal = conversation.terminal();
|
|
1738
|
+
const answered = terminal !== null && "stopReason" in terminal && terminal.stopReason === "end_turn" && conversation.failure() === null;
|
|
1739
|
+
// A clean exit after the answer, or an exit by the plane's own post-answer signal (its shutdown ladder's SIGTERM,
|
|
1740
|
+
// then SIGKILL), is success; anything else — a stop, a timeout, another signal — is its code.
|
|
1741
|
+
const ownSignal = (termAfterTerminal && (signal === "SIGTERM" || code === 143)) || (killAfterTerminal && (signal === "SIGKILL" || code === 137));
|
|
1742
|
+
const exitCode = answered && !timedOut && (code === 0 || ownSignal) ? 0 : (code ?? 1);
|
|
1743
|
+
const settlement = container === null ? Promise.resolve(null) : settleContainer(container, options);
|
|
1744
|
+
void settlement.then(empty => resolve({
|
|
1745
|
+
code: notFound ? NOT_FOUND_CODE : timedOut ? TIMEOUT_CODE : attachFailure !== null || empty === false ? CONTAINMENT_REFUSED_CODE : exitCode,
|
|
1746
|
+
// The runner's own account of the ending rides last: a type no agent can emit.
|
|
1747
|
+
stdout: `${conversation.retained()}${JSON.stringify({ type: "acp.exit", how: endedBy, leftover: leftover || empty === false, timedOut })}\n`,
|
|
1748
|
+
stderr: empty === false ? `${stderr}\nNative process containment could not prove all descendants exited.`.trim() : attachFailure !== null && stderr === "" ? attachFailure : stderr,
|
|
1749
|
+
timedOut,
|
|
1750
|
+
notFound,
|
|
1751
|
+
...containmentOf(container, empty, attachFailure),
|
|
1752
|
+
}));
|
|
1753
|
+
};
|
|
1754
|
+
child.on("error", error => {
|
|
1755
|
+
endedBy = "error";
|
|
1756
|
+
notFound = error.code === "ENOENT";
|
|
1757
|
+
if (stderr === "")
|
|
1758
|
+
stderr = String(error);
|
|
1759
|
+
finish(null, null);
|
|
1760
|
+
});
|
|
1761
|
+
child.on("close", (code, signal) => finish(code, signal));
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1406
1764
|
const TRUNCATION_MARKER = "…[truncated]";
|
|
1407
1765
|
/**
|
|
1408
1766
|
* One serialized synthetic line, guaranteed ≤ JSONL_LINE_CAP in UTF-8
|
package/dist/export-ui.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* Settings → Data (v105): an instance operator downloads everything Standing
|
|
3
3
|
* Orders knows as one .zip. One form: the password, typed again, and a button.
|
|
4
4
|
*/
|
|
5
|
-
import { html, postForm } from "./html.js";
|
|
5
|
+
import { html, postForm, reauth } from "./html.js";
|
|
6
6
|
export const EXPORT_CSS = `.data-export{max-width:720px;min-width:0}.data-export label{display:grid;gap:4px;margin:14px 0 0;font-size:.875rem}` +
|
|
7
7
|
`.data-export input[type=password]{width:100%;max-width:360px;box-sizing:border-box}.data-export button[type=submit]{margin-top:14px;min-height:44px;white-space:nowrap}`;
|
|
8
8
|
export function dataExportHtml(notice) {
|
|
9
9
|
const note = notice.problem ? html `<p class="problem" role="alert">${notice.problem}</p>` : "";
|
|
10
|
-
return html `<section class="data-export">${note}<p>Everything Toolroll knows, as one .zip: projects, tasks, runs and cost, the action ledger, evidence packs, chats, flows, subagents and settings. Passwords, keys and tokens are never included.</p>${postForm("/settings/data", html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label
|
|
10
|
+
return html `<section class="data-export">${note}<p>Everything Toolroll knows, as one .zip: projects, tasks, runs and cost, the action ledger, evidence packs, chats, flows, subagents and settings. Passwords, keys and tokens are never included.</p>${postForm("/settings/data", html `${reauth(html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label>`)}<button type="submit">Download export</button>`)}<p class="meta">The same export from the command line: <code>toolroll export --out <path> [--zip]</code></p></section>`;
|
|
11
11
|
}
|
package/dist/flow-engine.js
CHANGED
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
* waits for people. See flows.ts for what each zone does.
|
|
7
7
|
*/
|
|
8
8
|
import { assignmentOf } from "./assignment.js";
|
|
9
|
-
import { filerFor } from "./approval-policy.js";
|
|
10
9
|
import { diagnoseTaskDispatch } from "./dispatch.js";
|
|
11
10
|
import { passOn, TEXT_LIMITS } from "./text-limits.js";
|
|
12
11
|
import { deciderOf, durationWords, fillFlowText, flowGoal, flowValueLabel, flowWorkTemplate, validateFlowDefinition, withinHours } from "./flows.js";
|
|
12
|
+
import { readSnapshot, referenceWords, snapshotOf } from "./task-source.js";
|
|
13
13
|
import { reportFillIns, reportSummaryFor } from "./report-summary.js";
|
|
14
14
|
import { fileTaskProposal } from "./proposal.js";
|
|
15
15
|
import { FLOW_ACCEPTANCE, unattendedCoverageFor } from "./flow-unattended.js";
|
|
@@ -273,13 +273,20 @@ function workStage(store, flow, definition, stage, card, now, options, outcome,
|
|
|
273
273
|
title: (report ? `${stage.title}: ${card.title}` : card.title).slice(0, 200),
|
|
274
274
|
repo,
|
|
275
275
|
goal,
|
|
276
|
-
filedVia: `flow:${flow.id}`,
|
|
276
|
+
filedVia: `flow:${flow.id}`,
|
|
277
|
+
// Who made the card asked for the work (an owner can be reassigned by anyone). v126: an automation's card (a
|
|
278
|
+
// delivery, a poll, a schedule, a form) files as automation, by the kind kept on the card, never by its name.
|
|
279
|
+
filedBy: { name: card.createdBy, kind: card.createdKind },
|
|
277
280
|
deliverable: report ? "report" : "branch",
|
|
278
281
|
planning: report ? "skip" : stage.planning ?? "auto",
|
|
279
282
|
acceptance: grant === null ? ACCEPTANCE[kind] : grant.terms.acceptance[kind],
|
|
280
283
|
...(grant?.terms.perAttemptBudgetMicrousd == null ? {} : { budgetMicrousd: grant.terms.perAttemptBudgetMicrousd }),
|
|
281
284
|
admittedRepos: [repo],
|
|
282
285
|
}, now);
|
|
286
|
+
// v126: a card an outside event's delivery made gives its task a snapshot of where it came from, in this transaction.
|
|
287
|
+
const snapshot = made.ok ? snapshotOf(fresh.source) : null;
|
|
288
|
+
if (made.ok && snapshot !== null)
|
|
289
|
+
store.recordTaskSource(made.id, { flow: flow.id, card: fresh.id, entry: fresh.entry }, JSON.stringify(snapshot), now);
|
|
283
290
|
const ranOnItsOwn = made.ok && grant !== null && sealFlowFilingUnderGrant(store, made.id, grant, fresh, now);
|
|
284
291
|
if (made.ok)
|
|
285
292
|
store.updateFlowCard(card.id, { task: made.id, ...(report || fresh.primaryTask !== null ? {} : { primaryTask: made.id }), waiting: ranOnItsOwn ? "Filed as a task, approved: this flow runs on its own" : "Filed as a task" }, now);
|
|
@@ -359,12 +366,24 @@ function workStage(store, flow, definition, stage, card, now, options, outcome,
|
|
|
359
366
|
* 64 KiB), so in practice an attachment is never shortened. */
|
|
360
367
|
const FLOW_ATTACHED_LIMIT = 128_000;
|
|
361
368
|
export function flowGoalCuts(store, taskId, goal) {
|
|
369
|
+
let cuts = [];
|
|
362
370
|
try {
|
|
363
|
-
|
|
371
|
+
cuts = goalCuts(store, taskId, goal);
|
|
364
372
|
}
|
|
365
373
|
catch {
|
|
366
|
-
|
|
374
|
+
cuts = [];
|
|
367
375
|
} // context, never a reason a build can't start
|
|
376
|
+
// v126: where the work came from, from the task's own kept source (or the task it revises) — never from the card's
|
|
377
|
+
// current task link or zone, which move on. Given whole whenever the goal itself couldn't carry it.
|
|
378
|
+
try {
|
|
379
|
+
const kept = store.taskSourceOf(taskId);
|
|
380
|
+
const snapshot = kept === null ? null : readSnapshot(kept.sourceJson);
|
|
381
|
+
const words = snapshot === null ? "" : referenceWords(snapshot);
|
|
382
|
+
if (words !== "" && !goal.includes(words) && !cuts.some(one => one.text === words))
|
|
383
|
+
cuts.push({ label: flowValueLabel("card.source"), text: words });
|
|
384
|
+
}
|
|
385
|
+
catch { /* context, never a reason a build can't start */ }
|
|
386
|
+
return cuts;
|
|
368
387
|
}
|
|
369
388
|
function goalCuts(store, taskId, goal) {
|
|
370
389
|
const root = store.taskFamilyOf(taskId, null, true)?.root.id ?? taskId;
|
|
@@ -418,7 +437,7 @@ export function addCardToFlow(store, flow, input, actor, now) {
|
|
|
418
437
|
if ("problem" in text)
|
|
419
438
|
return { ok: false, message: text.problem };
|
|
420
439
|
const stage = definition.stages.find(one => one.id === input.stage) ?? definition.stages.find(one => one.id === definition.start);
|
|
421
|
-
const card = store.addFlowCard({ flow: flow.id, title: text.title, description: text.description, stage: stage.id, by: actor }, now);
|
|
440
|
+
const card = store.addFlowCard({ flow: flow.id, title: text.title, description: text.description, stage: stage.id, by: actor, byKind: "person" }, now);
|
|
422
441
|
// Whoever adds a card follows it; owning it is a separate, deliberate step.
|
|
423
442
|
store.setFlowCardWatcher(card, actor, true, now);
|
|
424
443
|
return { ok: true, said: `Card added to ${zoneWords(stage.title)}`, card };
|
package/dist/flow-gallery-ui.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { brandMarkHtml } from "./brand-mark.js";
|
|
8
8
|
import { BLANK, GALLERY, GALLERY_GROUPS, galleryDiagram, galleryToolsOf, OUTDATED_COMMANDS, SEND_RESULT, sendsAlready } from "./flow-gallery.js";
|
|
9
9
|
import { choiceTargets } from "./flows.js";
|
|
10
|
-
import { html, joinHtml, postForm } from "./html.js";
|
|
10
|
+
import { html, joinHtml, postForm, reauth } from "./html.js";
|
|
11
11
|
export const GALLERY_CSS = `.gallery{max-width:1120px;min-width:0}.gallery h2{margin:28px 0 10px;font-size:.9375rem}.gallery h2:first-of-type{margin-top:8px}` +
|
|
12
12
|
`.gallery-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:12px}` +
|
|
13
13
|
`.gallery-card{display:flex;flex-direction:column;gap:8px;padding:16px;border:1px solid var(--so-line);border-radius:10px;background:var(--so-paper);min-width:0}` +
|
|
@@ -89,7 +89,7 @@ export function galleryUseHtml(input) {
|
|
|
89
89
|
if (tool.state === "connected" || tool.state === "taken" || input.csrf === "")
|
|
90
90
|
return html `<div class="gallery-connect" data-tool="${tool.id}" data-state="${String(tool.state)}">${head}</div>`;
|
|
91
91
|
const how = tool.id === "figma-desktop" ? "Open the Figma desktop app and turn on its Dev Mode MCP server in Preferences." : `You sign in on ${tool.label}, then come back here.`;
|
|
92
|
-
return html `<div class="gallery-connect" data-tool="${tool.id}" data-state="open">${head}${postForm("/settings/tools/connect", html `<p class="meta">${how}</p
|
|
92
|
+
return html `<div class="gallery-connect" data-tool="${tool.id}" data-state="open">${head}${postForm("/settings/tools/connect", html `<p class="meta">${how}</p>${reauth(html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label>`)}<button name="service" value="${tool.id}">Connect ${tool.label}</button>`, { hidden: { repo: input.repo, shown: input.repo, template: template.id } })}</div>`;
|
|
93
93
|
};
|
|
94
94
|
const toolsBlock = tools.length === 0 ? "" : html `<section class="gallery-tools-use" aria-label="Tools">${tools.map(connect)}</section>`;
|
|
95
95
|
// Made without a tool, the preview names the zone that needs it.
|
package/dist/flow-gallery.js
CHANGED
|
@@ -266,10 +266,26 @@ export const GALLERY = [
|
|
|
266
266
|
does: ["Checks for outdated {{ask.outdated}} packages {{ask.schedule-words}}, with no AI.", "When some are out of date, updates them as a task, under your usual approvals.", "What you approve opens a pull request."],
|
|
267
267
|
never: NEVER_MERGES,
|
|
268
268
|
},
|
|
269
|
+
{
|
|
270
|
+
// v126: the direct road from a signed outside request to a build that waits for your approval.
|
|
271
|
+
id: "webhook-to-build", name: "Webhook to build", group: "health",
|
|
272
|
+
promise: "A signed request from any tool becomes a build, under your usual approvals.",
|
|
273
|
+
needs: ["A tool that can send signed webhooks", "GitHub"], asks: [],
|
|
274
|
+
steps: [
|
|
275
|
+
{ id: "build", title: "Build it", kind: "task", instructions: work("{{card.title}}\n\n{{card.description}}") },
|
|
276
|
+
owner("review", "Review the build"),
|
|
277
|
+
{ ...pr, next: "Done" },
|
|
278
|
+
{ id: "done", title: "Done", kind: "done" },
|
|
279
|
+
],
|
|
280
|
+
triggers: [{ kind: "webhook", sender: "standard", title: "Request" }],
|
|
281
|
+
does: ["Adds a webhook signed the Standard Webhooks way. Once the flow is made, open its Triggers and choose Set up under Signing: it shows the address and the secret together, once; give both to the sender.",
|
|
282
|
+
"Each signed request becomes a card and is built as a task. The task waits for your approval, like any other.", "You review the build; what you approve opens a pull request."],
|
|
283
|
+
never: NEVER_MERGES,
|
|
284
|
+
},
|
|
269
285
|
{
|
|
270
286
|
id: "error-to-fix", name: "Error to fix", group: "health", tools: ["sentry"],
|
|
271
|
-
promise: "New
|
|
272
|
-
needs: ["A
|
|
287
|
+
promise: "New Sentry errors are sorted, researched and fixed.",
|
|
288
|
+
needs: ["A Sentry Internal Integration", SORTS], asks: [],
|
|
273
289
|
steps: [
|
|
274
290
|
{ id: "worth-fixing", title: "Worth fixing?", kind: "sort", question: "Is this error in our own code, and new or frequent enough to fix now?",
|
|
275
291
|
answers: [
|
|
@@ -284,8 +300,9 @@ export const GALLERY = [
|
|
|
284
300
|
{ id: "done", title: "Done", kind: "done" },
|
|
285
301
|
{ id: "ignored", title: "Ignored", kind: "done" },
|
|
286
302
|
],
|
|
287
|
-
|
|
288
|
-
|
|
303
|
+
// v126: signed by Sentry and read by Toolroll's Sentry reader: one card per issue, linked to it.
|
|
304
|
+
triggers: [{ kind: "webhook", sender: "sentry", title: "Error" }],
|
|
305
|
+
does: ["Adds a Sentry webhook. Once the flow is made, open its Triggers and choose New address; in Sentry create an Internal Integration (Settings → Developer Settings), paste that address as its Webhook URL, turn on Alert Rule Action and Issue events, then paste its Client Secret under Signing on the flow's Triggers.", "One card per Sentry issue, however often it alerts, linked to the issue.", "Jev keeps errors in your code that are new or frequent, and drops noise; ones it isn't sure about wait for you.",
|
|
289
306
|
"Each kept error is researched, then fixed as a task, under your usual approvals.", "What you approve opens a pull request."],
|
|
290
307
|
never: NEVER_MERGES,
|
|
291
308
|
},
|
|
@@ -300,7 +317,7 @@ export const GALLERY = [
|
|
|
300
317
|
pr,
|
|
301
318
|
],
|
|
302
319
|
triggers: [{ kind: "webhook", title: "Incident", titleField: "data.attributes.name" }],
|
|
303
|
-
does: ["Adds a webhook. Once the flow is made, open its Triggers and choose New address, then paste that into Better Stack.", "Reads the incident, its monitor's history and your recent commits and deploys, and sends you the likely cause.", "You choose Fix or Ignore; a fix is built as a task, under your usual approvals, and opens a pull request."],
|
|
320
|
+
does: ["Adds a webhook. Once the flow is made, open its Triggers and choose New address, then paste that into Better Stack. Better Stack doesn't sign its webhooks, so anyone with the address could add cards: keep it private.", "Reads the incident, its monitor's history and your recent commits and deploys, and sends you the likely cause.", "You choose Fix or Ignore; a fix is built as a task, under your usual approvals, and opens a pull request."],
|
|
304
321
|
never: NEVER_MERGES,
|
|
305
322
|
},
|
|
306
323
|
{
|
|
@@ -335,8 +352,9 @@ export const GALLERY = [
|
|
|
335
352
|
pr,
|
|
336
353
|
send("Send the fix"),
|
|
337
354
|
],
|
|
338
|
-
|
|
339
|
-
|
|
355
|
+
// v126: signed by Vercel and read by Toolroll's Vercel reader: failed deployments only, one card each.
|
|
356
|
+
triggers: [{ kind: "webhook", sender: "vercel", title: "Failed deployment" }],
|
|
357
|
+
does: ["Adds a Vercel webhook. Once the flow is made, open its Triggers and choose New address; in Vercel (Settings → Webhooks) paste that address and choose Deployment Error, then paste the secret Vercel shows under Signing on the flow's Triggers. Put your Vercel project ID in Signing too, so only its deployments add cards.", "Reads the build log and finds the cause.", "Builds a fix as a task, under your usual approvals, opens a pull request and sends it to you."],
|
|
340
358
|
never: NEVER_MERGES,
|
|
341
359
|
},
|
|
342
360
|
// ---------------------------------------------------------- Hear from users
|
|
@@ -637,7 +655,8 @@ export function useGalleryTemplate(store, template, repo, given, input) {
|
|
|
637
655
|
const id = store.createFlow({ repo, name, definitionJson: JSON.stringify(built.definition), by: input.by }, input.now);
|
|
638
656
|
const made = store.getFlow(id);
|
|
639
657
|
for (const trigger of built.triggers) {
|
|
640
|
-
|
|
658
|
+
// No one is shown the gallery's addresses: a webhook is made with none until its panel makes one (v126).
|
|
659
|
+
const added = addFlowTriggerTo(store, made, { ...trigger, zone: trigger["zone"] ?? flowDefinitionOf(made).start }, input.by, input.now, input.dir, { shown: false });
|
|
641
660
|
if (!added.ok)
|
|
642
661
|
throw new Error(added.message);
|
|
643
662
|
if (added.reveal !== null)
|