toolroll 0.9.84 → 0.9.86
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 +66 -0
- package/README.md +94 -1193
- package/dist/agent-fence.d.ts +9 -5
- package/dist/agent-fence.js +6 -5
- package/dist/agent-onboard.d.ts +2 -0
- package/dist/agent-onboard.js +3 -0
- package/dist/assignment.js +4 -3
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +33 -31
- package/dist/browser-workspace.d.ts +45 -0
- package/dist/chat-actions.d.ts +11 -1
- package/dist/chat-actions.js +53 -1
- package/dist/chat-channel.d.ts +10 -1
- package/dist/chat-channel.js +36 -5
- package/dist/chat-decide.d.ts +16 -4
- package/dist/chat-decide.js +239 -31
- package/dist/chat-messages.d.ts +5 -0
- package/dist/chat-messages.js +1 -1
- package/dist/chat-quiet.js +6 -4
- package/dist/chat-review.d.ts +4 -0
- package/dist/chat-review.js +13 -4
- package/dist/chat-terms.d.ts +9 -0
- package/dist/chat-terms.js +46 -0
- package/dist/claim.d.ts +3 -0
- package/dist/claim.js +4 -1
- package/dist/contracts/chat-actions.d.ts +9 -0
- package/dist/contracts/chat-actions.js +3 -0
- package/dist/contracts/console-api.d.ts +6 -2
- package/dist/contracts/console-api.js +6 -2
- package/dist/contracts/flow-propose.d.ts +4 -1
- package/dist/contracts/flow-propose.js +1 -1
- package/dist/contracts/flow.d.ts +41 -0
- package/dist/contracts/flow.js +4 -2
- package/dist/contracts/lead-tools.d.ts +51 -0
- package/dist/contracts/lead-tools.js +5 -0
- package/dist/contracts/store-json.d.ts +2 -0
- package/dist/contracts/trigger-payloads.d.ts +20 -0
- package/dist/contracts/trigger-payloads.js +4 -0
- package/dist/contracts/youiyoux-request.d.ts +248 -0
- package/dist/contracts/youiyoux-request.js +152 -0
- package/dist/coordinator.js +1 -0
- package/dist/deploy-gate.js +4 -0
- package/dist/dispatch.d.ts +3 -0
- package/dist/dispatch.js +4 -1
- package/dist/flow-browser.d.ts +103 -0
- package/dist/flow-browser.js +204 -0
- package/dist/flow-cadence.d.ts +8 -2
- package/dist/flow-cadence.js +16 -3
- package/dist/flow-card-task.d.ts +9 -0
- package/dist/flow-card-task.js +15 -6
- package/dist/flow-code.d.ts +5 -1
- package/dist/flow-code.js +4 -1
- package/dist/flow-engine.js +16 -0
- package/dist/flow-live.js +9 -1
- package/dist/flow-pull-request.js +15 -7
- package/dist/flow-share.js +3 -1
- package/dist/flow-steps.d.ts +9 -1
- package/dist/flow-steps.js +55 -5
- package/dist/flow-triggers.js +55 -0
- package/dist/flows-cli.d.ts +19 -0
- package/dist/flows-cli.js +47 -2
- package/dist/flows-ui.js +11 -2
- package/dist/flows.js +5 -2
- package/dist/integrations-ui.js +9 -4
- package/dist/integrations.d.ts +9 -1
- package/dist/integrations.js +43 -2
- package/dist/landed-cleanup.d.ts +4 -0
- package/dist/landed-cleanup.js +3 -2
- package/dist/lead-capabilities.js +2 -1
- package/dist/lead-cli.js +3 -1
- package/dist/lead-tools.d.ts +2 -0
- package/dist/lead-tools.js +65 -10
- package/dist/lead-voice.js +16 -5
- package/dist/lead.d.ts +2 -0
- package/dist/lead.js +3 -3
- package/dist/operate.d.ts +2 -3
- package/dist/operate.js +226 -21
- package/dist/project-activity.d.ts +20 -0
- package/dist/project-activity.js +23 -0
- package/dist/project-cli.d.ts +1 -0
- package/dist/project-cli.js +36 -19
- package/dist/project-delete.js +7 -3
- package/dist/project-knowledge.d.ts +3 -3
- package/dist/project-outcomes.d.ts +119 -0
- package/dist/project-outcomes.js +79 -0
- package/dist/pull-request-flow.js +35 -4
- package/dist/result-actions.d.ts +4 -0
- package/dist/result-actions.js +41 -9
- package/dist/result-follow-ups.js +4 -0
- package/dist/result-review.d.ts +9 -0
- package/dist/result-review.js +45 -5
- package/dist/scope.d.ts +5 -0
- package/dist/scope.js +7 -0
- package/dist/server/chat.js +4 -2
- package/dist/server/console.css +41 -0
- package/dist/server/flows.js +15 -0
- package/dist/server/pages.js +11 -1
- package/dist/server/render-chat.js +3 -2
- package/dist/server/render-pages.d.ts +15 -2
- package/dist/server/render-pages.js +52 -4
- package/dist/server/render-results.d.ts +2 -0
- package/dist/server/render-results.js +20 -7
- package/dist/server/render-tasks.d.ts +9 -0
- package/dist/server/render-tasks.js +32 -11
- package/dist/server/route-table.js +12 -0
- package/dist/server/settings.js +18 -1
- package/dist/server/task-views.js +11 -0
- package/dist/server/tasks.d.ts +10 -0
- package/dist/server/tasks.js +95 -7
- package/dist/settings-proofs.js +1 -0
- package/dist/store.d.ts +116 -5
- package/dist/store.js +385 -24
- package/dist/surface.js +18 -2
- package/dist/task-archive.d.ts +10 -0
- package/dist/task-archive.js +16 -0
- package/dist/task-control.d.ts +1 -1
- package/dist/task-source.d.ts +13 -1
- package/dist/task-source.js +17 -1
- package/dist/telegram-lead.js +7 -5
- package/dist/telegram.js +1 -1
- package/dist/tidy.d.ts +130 -0
- package/dist/tidy.js +503 -0
- package/dist/toolroll-update.js +2 -2
- package/dist/webhooks.d.ts +6 -0
- package/dist/webhooks.js +25 -0
- package/dist/work-index.d.ts +28 -0
- package/dist/work-index.js +82 -28
- package/dist/workspace-ui.d.ts +1 -1
- package/dist/workspace-ui.js +3 -2
- package/dist/youiyoux-intake.d.ts +234 -0
- package/dist/youiyoux-intake.js +908 -0
- package/package.json +11 -8
package/dist/flow-steps.js
CHANGED
|
@@ -36,7 +36,9 @@ import { claudeDraftRunner, DRAFT_CHARS, DRAFT_TIMEOUT, draftPrompt, wholeDraft
|
|
|
36
36
|
import { overruns, passOn, TEXT_LIMITS, writeWithin } from "./text-limits.js";
|
|
37
37
|
import { readFlowSecrets, sendingReady, runRequest, sendEmail, toolWaiting, useTool } from "./flow-actions.js";
|
|
38
38
|
import { cleanFolder, flowLaunch, runCode } from "./flow-code.js";
|
|
39
|
+
import { browserStanding } from "./flow-browser.js";
|
|
39
40
|
import { readProviderKey } from "./keys.js";
|
|
41
|
+
import { cardTaskArchived } from "./task-archive.js";
|
|
40
42
|
import { replyInChannel } from "./chat-inbox.js";
|
|
41
43
|
import { recordSent, threadOf } from "./flow-replies.js";
|
|
42
44
|
import { ASKED, subagentReady, subagentTurn } from "./subagent-work.js";
|
|
@@ -70,6 +72,9 @@ export async function runFlowSteps(store, repo, now, io) {
|
|
|
70
72
|
const stage = known.definition?.stages.find(one => one.id === card.stage);
|
|
71
73
|
if (stage === undefined)
|
|
72
74
|
continue;
|
|
75
|
+
// A card whose task is put away (v145) runs nothing — no step, subagent turn or pull request — until it's restored.
|
|
76
|
+
if (cardTaskArchived(store, card.id))
|
|
77
|
+
continue;
|
|
73
78
|
// v92: a subagent's turn — a zone it handles, or a decision it staffs (a person decides when it's paused or gone).
|
|
74
79
|
if (stage.kind === "subagent" || (stage.kind === "approval" && stage.subagent !== undefined)) {
|
|
75
80
|
await subagentStep(store, known.flow, known.definition, stage, card, now, io, pass);
|
|
@@ -145,6 +150,14 @@ export async function runFlowSteps(store, repo, now, io) {
|
|
|
145
150
|
store.updateFlowCard(card.id, { waiting }, now);
|
|
146
151
|
continue;
|
|
147
152
|
}
|
|
153
|
+
// A script that starts a browser waits, saying so, until a person approves browser use for the flow (flow-browser.ts),
|
|
154
|
+
// rather than run and crash inside the sandbox; it runs on its own once approved.
|
|
155
|
+
const browser = stage.kind === "check" && stage.browser === true ? (script === null || known.definition === null ? { allowed: false, said: "This zone's script can't be read." } : browserStanding(store, known.flow.id, stage.id, { definition: known.definition, script })) : null;
|
|
156
|
+
if (browser !== null && !browser.allowed) {
|
|
157
|
+
if (card.waiting !== browser.said)
|
|
158
|
+
store.updateFlowCard(card.id, { waiting: browser.said }, now);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
148
161
|
if (!claimStep(store, card, { stage: stage.id, kind: stage.kind, script: script?.name ?? null, scriptVersion: script?.version ?? null }, now))
|
|
149
162
|
continue;
|
|
150
163
|
pass.ran++;
|
|
@@ -174,12 +187,21 @@ export async function runFlowSteps(store, repo, now, io) {
|
|
|
174
187
|
}
|
|
175
188
|
/** Claim this card's visit to its step zone, only while the card is still there on that visit: a pass that read the card
|
|
176
189
|
* before another pass (or a person) moved it never runs the step for a visit that's over. */
|
|
177
|
-
function claimStep(store, card, step, now) {
|
|
190
|
+
export function claimStep(store, card, step, now) {
|
|
178
191
|
return store.transact(() => {
|
|
179
192
|
const fresh = store.getFlowCard(card.id);
|
|
180
193
|
if (fresh === null || fresh.state !== "active" || fresh.entry !== card.entry || fresh.stage !== step.stage)
|
|
181
194
|
return false;
|
|
182
|
-
|
|
195
|
+
// A card whose task is put away (v145) runs no step until the task is restored (both of its tasks, re-read here).
|
|
196
|
+
if (cardTaskArchived(store, card.id))
|
|
197
|
+
return false;
|
|
198
|
+
if (!store.claimFlowStep({ card: card.id, entry: card.entry, ...step }, now))
|
|
199
|
+
return false;
|
|
200
|
+
// The tasks this step was admitted for stay held while it runs, wherever the card moves meanwhile.
|
|
201
|
+
for (const task of [fresh.task, fresh.primaryTask])
|
|
202
|
+
if (task !== null)
|
|
203
|
+
store.handle.prepare("INSERT OR IGNORE INTO flow_step_task (card, entry, task_id) VALUES (?, ?, ?)").run(card.id, card.entry, task);
|
|
204
|
+
return true;
|
|
183
205
|
});
|
|
184
206
|
}
|
|
185
207
|
/** One subagent turn, when one is due for this card: claimed like any step, retried on failure, and waiting while its question is open. */
|
|
@@ -361,16 +383,31 @@ async function runCheck(store, flow, stage, script, card, now, io) {
|
|
|
361
383
|
if (added.code !== 0)
|
|
362
384
|
return { state: "retry", said: `Couldn't make a copy of${where}: ${added.stderr.trim().split("\n")[0]?.slice(0, 160) ?? "git refused"}.` };
|
|
363
385
|
}
|
|
364
|
-
|
|
386
|
+
// Browser use, proved at the moment of launch: a zone that says it starts a browser runs only with a person's standing
|
|
387
|
+
// approval (flow-browser.ts); then its script, and only its script, gets Chromium's one service.
|
|
388
|
+
const wantsBrowser = stage.browser === true;
|
|
389
|
+
// Read once: the drawing and the script this launch runs are the very objects the browser decision is about.
|
|
390
|
+
const running = flowDefinitionOf(flow);
|
|
391
|
+
const standing = !wantsBrowser ? null : running === null || running.stages.find(one => one.id === stage.id) === undefined || JSON.stringify(running.stages.find(one => one.id === stage.id)) !== JSON.stringify(stage)
|
|
392
|
+
? { allowed: false, said: "This zone changed while it was starting, so it didn't run." } : browserStanding(store, flow.id, stage.id, { definition: running, script });
|
|
393
|
+
if (standing !== null && !standing.allowed) {
|
|
394
|
+
if (copy)
|
|
395
|
+
await io.git("git", ["-C", flow.repo, "worktree", "remove", "--force", path], { timeoutMs: 60_000 }).catch(() => undefined);
|
|
396
|
+
rmSync(path, { recursive: true, force: true });
|
|
397
|
+
return { state: "failed", said: standing.said };
|
|
398
|
+
}
|
|
399
|
+
const launch = flowLaunch(store, { repo: flow.repo, folder: path, scratch: io.scratch, copy, policy: store.flowFencePolicy(flow.id), ...(standing?.allowed === true ? { browser: true } : {}), ...(io.fenceAvailable === undefined ? {} : { available: io.fenceAvailable }), ...(io.home === undefined ? {} : { home: io.home }) });
|
|
365
400
|
try {
|
|
366
401
|
if (launch !== null && "problem" in launch)
|
|
367
402
|
return { state: "failed", said: launch.problem };
|
|
403
|
+
// The project's setup never gets the browser rule: only the zone's script asked for it.
|
|
404
|
+
const setupFence = launch?.fence === undefined ? undefined : { ...launch.fence, fenceGrants: withoutBrowser(launch.fence.fenceGrants) };
|
|
368
405
|
let log = "";
|
|
369
406
|
const setup = copy ? store.liveWorktreeSetup(flow.repo) : null;
|
|
370
407
|
if (setup !== null) {
|
|
371
408
|
const shell = approvedCommandShell(setup.command);
|
|
372
409
|
const prepared = await io.shell(shell.file, shell.args, { cwd: path, timeoutMs: setup.timeoutMs, envAllowlist: SETUP_ENV_ALLOWLIST, omitEnv: SETUP_ENV_DENYLIST, processGroup: true,
|
|
373
|
-
...(launch === null ? {} : { env: launch.env, ...(
|
|
410
|
+
...(launch === null ? {} : { env: launch.env, ...(setupFence ?? {}) }) });
|
|
374
411
|
log += `$ ${setup.command}\n${prepared.stdout}${prepared.stderr}\n`;
|
|
375
412
|
if (prepared.code !== 0)
|
|
376
413
|
return { state: "failed", said: `The project's setup failed before ${script.name} ran (exit ${prepared.code}).\n${tail(`${prepared.stdout}\n${prepared.stderr}`)}`, log, exitCode: prepared.code };
|
|
@@ -385,7 +422,15 @@ async function runCheck(store, flow, stage, script, card, now, io) {
|
|
|
385
422
|
answers: (stage.routes ?? []).map(one => one.answer),
|
|
386
423
|
};
|
|
387
424
|
const env = { FLOW_NAME: flow.name, FLOW_CARD_ID: String(card.id), FLOW_CARD_TITLE: card.title, FLOW_COMMIT: commit ?? "", FLOW_SCRIPT: script.name, FLOW_PROJECT: flow.repo };
|
|
388
|
-
|
|
425
|
+
// Checked again right before the script: the flow or its script may have changed while the setup ran.
|
|
426
|
+
if (wantsBrowser) {
|
|
427
|
+
const still = running === null ? { allowed: false, said: "This flow's drawing can't be read." } : browserStanding(store, flow.id, stage.id, { definition: running, script });
|
|
428
|
+
if (!still.allowed)
|
|
429
|
+
return { state: "failed", said: still.said, log };
|
|
430
|
+
}
|
|
431
|
+
// And at every spawn attempt (a retry after a failed spawn too): revoked or changed meanwhile, nothing starts.
|
|
432
|
+
const stillApproved = !wantsBrowser || running === null ? undefined : () => browserStanding(store, flow.id, stage.id, { definition: running, script }).allowed;
|
|
433
|
+
const ran = await runCode({ script, cwd: path, root: copy ? path : flow.repo, input, env, secrets, scratch: io.scratch, shell: io.shell, launch, ...(stillApproved === undefined ? {} : { beforeSpawn: stillApproved }) });
|
|
389
434
|
log += ran.log;
|
|
390
435
|
const output = ran.output === "" ? undefined : ran.output;
|
|
391
436
|
// Where it ran goes at the end of the first line: "run-tests failed (exit 1) on main."
|
|
@@ -407,6 +452,11 @@ async function runCheck(store, flow, stage, script, card, now, io) {
|
|
|
407
452
|
rmSync(path, { recursive: true, force: true });
|
|
408
453
|
}
|
|
409
454
|
}
|
|
455
|
+
/** A fence's grants without Chromium's service (a flow's setup, beside a script zone that has it). */
|
|
456
|
+
function withoutBrowser(grants) {
|
|
457
|
+
const { browser: _scriptOnly, ...rest } = grants;
|
|
458
|
+
return rest;
|
|
459
|
+
}
|
|
410
460
|
/** An update: a comment on the issue the card came from, and closing it if the zone says so. */
|
|
411
461
|
async function updateSource(store, stage, card, io, now) {
|
|
412
462
|
const source = card.source;
|
package/dist/flow-triggers.js
CHANGED
|
@@ -49,6 +49,7 @@ import { readFlowSecrets } from "./flow-secrets.js";
|
|
|
49
49
|
import { prepareUnattendedCandidates, grantSignsScriptOf, scriptProvenanceOf, scriptRanDigestOf } from "./flow-unattended.js";
|
|
50
50
|
import { tmpdir } from "node:os";
|
|
51
51
|
import { envValue } from "./names.js";
|
|
52
|
+
import { recordBranchCheckRun, unprovenNoticeRuns } from "./tidy.js";
|
|
52
53
|
import { linearLabelNames, readFormSubmission, readGithubEvent, readGithubIssue, readGithubIssueEventList, readGithubIssueList, readGithubRun, readGithubRunList, readInboundMail, readLinearAnswer, readLinearEvent, readLinearIssue, readWebhookPayload } from "./contracts/trigger-payloads.js";
|
|
53
54
|
import { isRecord } from "./is-record.js";
|
|
54
55
|
export { FLOW_TRIGGER_KINDS };
|
|
@@ -925,6 +926,9 @@ async function checkTrigger(store, trigger, config, now, io) {
|
|
|
925
926
|
store.updateFlowTrigger(trigger.id, { failures, lastAt: now.toISOString(), lastOutcome: fetched.problem, nextAt: new Date(now.getTime() + BACKOFF_MS[Math.min(failures, BACKOFF_MS.length) - 1]).toISOString() }, now);
|
|
926
927
|
return { ok: false, said: fetched.problem, added: 0 };
|
|
927
928
|
}
|
|
929
|
+
// Tidy's CI evidence (task-tidy.md): the watched branch's completed runs, green and red, beside the failures it carded.
|
|
930
|
+
if (config.kind === "github" && config.watch === "checks")
|
|
931
|
+
await observeBranchRuns(store, trigger, config, io.gh, now);
|
|
928
932
|
const by = config.kind === "github" ? "GitHub" : config.kind === "linear" ? "Linear" : "Email";
|
|
929
933
|
// Known items are left out before the batch is taken (v126): a page of things already received can't keep the unseen
|
|
930
934
|
// ones behind it waiting.
|
|
@@ -1206,6 +1210,14 @@ export function receiveFlowHook(store, token, delivery, dir, now) {
|
|
|
1206
1210
|
: config.watch === "pulls" && event === "pull_request" && ["opened", "ready_for_review", "labeled", "reopened"].includes(action) ? githubItem(config, body.pull_request, "pull")
|
|
1207
1211
|
: config.watch === "checks" && event === "workflow_run" && action === "completed" ? githubRunItem(config, body.workflow_run) : null;
|
|
1208
1212
|
items = found === null ? [] : [found];
|
|
1213
|
+
// A signed workflow run on the watched branch is tidy's CI evidence too (task-tidy.md), green or red: recorded only
|
|
1214
|
+
// inside `written`, which re-proves the trigger is still active with the same config, credential and address.
|
|
1215
|
+
if (config.watch === "checks" && config.branch !== null && event === "workflow_run" && action === "completed") {
|
|
1216
|
+
const branch = config.branch;
|
|
1217
|
+
const recorded = written(() => { recordBranchCheckRun(store, { repo: config.repo, branch }, readGithubRun(body.workflow_run), now); return { status: 200, said: "Recorded." }; });
|
|
1218
|
+
if (recorded.status !== 200)
|
|
1219
|
+
return recorded;
|
|
1220
|
+
}
|
|
1209
1221
|
}
|
|
1210
1222
|
else if (config.kind === "linear") {
|
|
1211
1223
|
const body = readLinearEvent(payload), sent = Number(body.webhookTimestamp);
|
|
@@ -1648,3 +1660,46 @@ export function receiveFlowForm(store, token, fields, now) {
|
|
|
1648
1660
|
store.updateFlowTrigger(trigger.id, { lastAt: now.toISOString(), lastOutcome: "Someone sent the form." }, now);
|
|
1649
1661
|
return { status: 200, html: thanks };
|
|
1650
1662
|
}
|
|
1663
|
+
/** Tidy's CI evidence (docs/plans/task-tidy.md): the watched branch's recent completed workflow runs, read through the
|
|
1664
|
+
* person's own gh login. Recorded only while the trigger still watches exactly that repository and branch once GitHub
|
|
1665
|
+
* answered; any failure records nothing (tidy then keeps the notices). */
|
|
1666
|
+
async function observeBranchRuns(store, trigger, config, gh, now) {
|
|
1667
|
+
if (config.branch === null)
|
|
1668
|
+
return;
|
|
1669
|
+
const branch = config.branch;
|
|
1670
|
+
let result;
|
|
1671
|
+
try {
|
|
1672
|
+
result = await gh("gh", ["api", "-H", "Accept: application/vnd.github+json", `repos/${config.repo}/actions/runs?branch=${encodeURIComponent(branch)}&status=completed&per_page=50`], { timeoutMs: 20_000 });
|
|
1673
|
+
}
|
|
1674
|
+
catch {
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
if (result.code !== 0)
|
|
1678
|
+
return;
|
|
1679
|
+
let body;
|
|
1680
|
+
try {
|
|
1681
|
+
body = JSON.parse(result.stdout);
|
|
1682
|
+
}
|
|
1683
|
+
catch {
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1686
|
+
const runs = [...readGithubRunList(body).workflow_runs ?? []];
|
|
1687
|
+
// A few older failure cards' own runs, so tidy can tell whether each was a push to this branch (task-tidy.md).
|
|
1688
|
+
for (const id of unprovenNoticeRuns(store, trigger.id, config.repo)) {
|
|
1689
|
+
try {
|
|
1690
|
+
const one = await gh("gh", ["api", "-H", "Accept: application/vnd.github+json", `repos/${config.repo}/actions/runs/${id}`], { timeoutMs: 20_000 });
|
|
1691
|
+
if (one.code === 0)
|
|
1692
|
+
runs.push(readGithubRun(JSON.parse(one.stdout)));
|
|
1693
|
+
}
|
|
1694
|
+
catch { /* read again next time */ }
|
|
1695
|
+
}
|
|
1696
|
+
// Re-proved and recorded in one transaction: a trigger paused or changed meanwhile records nothing.
|
|
1697
|
+
store.transact(() => {
|
|
1698
|
+
const current = store.getFlowTrigger(trigger.id);
|
|
1699
|
+
const still = current === null ? null : triggerConfigOf(current);
|
|
1700
|
+
if (current === null || current.state !== "active" || current.configJson !== trigger.configJson || still === null || still.kind !== "github" || still.watch !== "checks" || still.repo !== config.repo || still.branch !== branch)
|
|
1701
|
+
return;
|
|
1702
|
+
for (const run of runs)
|
|
1703
|
+
recordBranchCheckRun(store, { repo: config.repo, branch }, run, now);
|
|
1704
|
+
});
|
|
1705
|
+
}
|
package/dist/flows-cli.d.ts
CHANGED
|
@@ -296,6 +296,25 @@ export declare const FLOWS_DESCRIPTORS: readonly [{
|
|
|
296
296
|
readonly takesValue: true;
|
|
297
297
|
readonly meaning: 'also cover these script or plane-review schedules (trigger ids, like 12,14); plain schedules are covered already';
|
|
298
298
|
}];
|
|
299
|
+
}, {
|
|
300
|
+
readonly action: 'browser';
|
|
301
|
+
readonly synopsis: "let a flow's script zones marked browser: true start Chromium in their sandbox (on: a person approves with their own password, previews until --yes; off: any approver revokes)";
|
|
302
|
+
readonly mutation: 'identity-idempotent';
|
|
303
|
+
readonly audience: 'operator';
|
|
304
|
+
readonly positionals: readonly ['flow', 'on|off'];
|
|
305
|
+
readonly flags: readonly [...{
|
|
306
|
+
name: string;
|
|
307
|
+
takesValue: boolean;
|
|
308
|
+
meaning: string;
|
|
309
|
+
}[], {
|
|
310
|
+
name: string;
|
|
311
|
+
takesValue: boolean;
|
|
312
|
+
meaning: string;
|
|
313
|
+
}, {
|
|
314
|
+
readonly name: 'digest';
|
|
315
|
+
readonly takesValue: true;
|
|
316
|
+
readonly meaning: "required with --yes: the approval digest the preview showed; refuses if the flow or its scripts changed since";
|
|
317
|
+
}];
|
|
299
318
|
}, {
|
|
300
319
|
readonly action: 'archive';
|
|
301
320
|
readonly synopsis: 'archive a flow: its cards stop moving and its triggers stop; previews until --yes';
|
package/dist/flows-cli.js
CHANGED
|
@@ -19,6 +19,7 @@ import { STARTER_FLOWS, starterFlowOf, starterOf, startersFor, starterTerms, swi
|
|
|
19
19
|
import { addFlowTriggerTo, checkFlowTriggerNow, describeTrigger, removeFlowTrigger, setFlowTriggerOn, triggerConfigOf, validateTriggerConfig, rawTakesDeliveries } from './flow-triggers.js';
|
|
20
20
|
import { deciderOf, FLOW_KIND_WORDS, FLOW_TEMPLATES, flowDigest, flowFromSteps, flowTerms, stepsFor } from './flows.js';
|
|
21
21
|
import { prepareUnattendedCandidates, activeUnattendedGrant, parseUnattendedAsk, revokeUnattended, signUnattended, unattendedViewOf, unattendedWords, untilWords, UNATTENDED_MAX_RUNS } from './flow-unattended.js';
|
|
22
|
+
import { browserApprovalDigest, browserTerms, browserTermsOf, browserViewOf, revokeFlowBrowser, signFlowBrowser } from './flow-browser.js';
|
|
22
23
|
import { MODE_MAX_DAYS } from './modes.js';
|
|
23
24
|
import { verifyApproverByPassword } from './principal.js';
|
|
24
25
|
import { authenticateApprover, proveCredential } from './scope.js';
|
|
@@ -71,6 +72,8 @@ export const FLOWS_DESCRIPTORS = [
|
|
|
71
72
|
{ name: 'budget', takesValue: true, meaning: 'optional dollar cap per attempt, like 5 or 2.50' },
|
|
72
73
|
{ name: 'runs', takesValue: true, meaning: `optional limit on runs a day (UTC), 1 to ${UNATTENDED_MAX_RUNS}` },
|
|
73
74
|
{ name: 'include', takesValue: true, meaning: 'also cover these script or plane-review schedules (trigger ids, like 12,14); plain schedules are covered already' }] },
|
|
75
|
+
{ action: 'browser', synopsis: "let a flow's script zones marked browser: true start Chromium in their sandbox (on: a person approves with their own password, previews until --yes; off: any approver revokes)", mutation: 'identity-idempotent', audience: 'operator', positionals: ['flow', 'on|off'], flags: [...write, yes,
|
|
76
|
+
{ name: 'digest', takesValue: true, meaning: "required with --yes: the approval digest the preview showed; refuses if the flow or its scripts changed since" }] },
|
|
74
77
|
{ action: 'archive', synopsis: 'archive a flow: its cards stop moving and its triggers stop; previews until --yes', mutation: 'identity-idempotent', positionals: ['flow'], flags: [...write, yes] },
|
|
75
78
|
];
|
|
76
79
|
/** `--file` is a value here (a path in the project), where elsewhere it is a switch; so are unattended's `--budget`, `--runs` and `--include`. */
|
|
@@ -160,10 +163,12 @@ export async function runFlowsCommand(positional, flags, context) {
|
|
|
160
163
|
const refuse = (project, reason, message, code = EXIT.refused) => { record(project, 'refused', reason); return fail(reason, message, code); };
|
|
161
164
|
// A trigger from anyone widens what comes in (chat-management.md §0a.3): only the person's own proof counts, never a
|
|
162
165
|
// machine credential standing in for them (a remote run's API token, a lead's token), though both pass the check above.
|
|
163
|
-
|
|
166
|
+
// The same proof approves browser use in a flow's script zones (flow-browser.ts): the method is recorded with it.
|
|
167
|
+
const personMethod = (project) => {
|
|
164
168
|
const proved = authenticateApprover(store, acting.name, acting.token, project);
|
|
165
|
-
return proved.ok && personProof(proved.method) ?
|
|
169
|
+
return proved.ok && personProof(proved.method) ? proved.method : null;
|
|
166
170
|
};
|
|
171
|
+
const widening = (project) => personMethod(project) === null ? {} : { widening: 'proved' };
|
|
167
172
|
const settle = (project) => { try {
|
|
168
173
|
advanceFlows(store, project, context.clock(), { evidenceRoot: context.evidenceRoot });
|
|
169
174
|
}
|
|
@@ -396,6 +401,42 @@ export async function runFlowsCommand(positional, flags, context) {
|
|
|
396
401
|
`Revoke: toolroll flows unattended ${flow.id} off`,
|
|
397
402
|
]);
|
|
398
403
|
}
|
|
404
|
+
if (action === 'browser') {
|
|
405
|
+
const flow = flowOf(args[0], reachable);
|
|
406
|
+
if (flow === null)
|
|
407
|
+
return refuse(null, 'unknown-flow', 'No such flow in your projects. toolroll flows list names them.');
|
|
408
|
+
const turn = args[1];
|
|
409
|
+
if (turn !== 'on' && turn !== 'off')
|
|
410
|
+
return fail('usage', `Use flows browser ${flow.id} on, or flows browser ${flow.id} off.`, EXIT.usage);
|
|
411
|
+
if (turn === 'off') {
|
|
412
|
+
// Lowering authority is one step for any approver on the project.
|
|
413
|
+
if (!revokeFlowBrowser(store, flow.id, who, now))
|
|
414
|
+
return refuse(flow.repo, 'not-on', `${flow.name} has no approved browser use. Nothing was changed.`);
|
|
415
|
+
record(flow.repo, 'accepted', `flow #${flow.id} browser off`);
|
|
416
|
+
return ok({ applied: true, browser: null }, [`${flow.name}'s scripts can no longer start a browser.`]);
|
|
417
|
+
}
|
|
418
|
+
if (flowDefinitionOf(flow) === null)
|
|
419
|
+
return refuse(flow.repo, 'unreadable', 'This flow\'s drawing can\'t be read. Save it again from the editor.');
|
|
420
|
+
const terms = browserTermsOf(store, flow);
|
|
421
|
+
if (terms === null || terms.zones.length === 0)
|
|
422
|
+
return refuse(flow.repo, 'refused', terms === null ? 'A zone that starts a browser has no script it can approve: missing, waiting for approval, or one that runs a project file (write the script on the zone instead). Nothing was changed.'
|
|
423
|
+
: `No zone in ${flow.name} says it starts a browser. Mark its script zone with browser: true (flows edit), then approve. Nothing was changed.`);
|
|
424
|
+
const digest = browserApprovalDigest(terms);
|
|
425
|
+
if (!flags.has('yes'))
|
|
426
|
+
return preview(flow.repo, `Approve browser use in the ${flow.name} flow`, [...browserTerms(terms.zones), `Approval digest ${digest}: approve exactly this with --yes --digest ${digest}.`], { flowId: flow.id, digest, zones: terms.zones });
|
|
427
|
+
// A person's own proof only: a lead's or remote run's token passes the approver check above but never approves this.
|
|
428
|
+
// The digest the person read is required: approving with --yes alone could approve something they never saw.
|
|
429
|
+
const given = text('digest');
|
|
430
|
+
if (given === null)
|
|
431
|
+
return fail('usage', `Approve exactly what you read: flows browser ${flow.id} on --yes --digest <the digest the preview showed>.`, EXIT.usage);
|
|
432
|
+
const signed = signFlowBrowser(store, { flow: flow.id, signer: who, proof: personMethod(flow.repo), sawDigest: given }, now);
|
|
433
|
+
if (!signed.ok)
|
|
434
|
+
return refuse(flow.repo, 'refused', `${signed.message} Nothing was changed.`);
|
|
435
|
+
record(flow.repo, 'accepted', `flow #${flow.id} browser on`);
|
|
436
|
+
return ok({ applied: true, browser: { digest: signed.grant.digest, zones: signed.grant.zones, signedBy: signed.grant.signedBy } }, [
|
|
437
|
+
`${flow.name} may start a browser. Approved by ${who}:`, ...browserTerms(signed.grant.zones).map(line => ` ${line}`), `Revoke: toolroll flows browser ${flow.id} off`,
|
|
438
|
+
]);
|
|
439
|
+
}
|
|
399
440
|
if (action === 'archive') {
|
|
400
441
|
const cards = store.flowCards(flow.id, false).length, triggers = store.flowTriggers(flow.id).filter(one => one.state === 'active').length;
|
|
401
442
|
if (!flags.has('yes'))
|
|
@@ -566,6 +607,7 @@ function zoneOf(stage, flow) {
|
|
|
566
607
|
...(stage.limit === undefined ? {} : { remindAfterMinutes: stage.limit.minutes, thenMoveTo: stage.limit.to }),
|
|
567
608
|
...(stage.kind === 'approval' ? { decider: deciderOf(stage, flow) } : {}),
|
|
568
609
|
...(stage.script === null ? {} : { script: stage.script }),
|
|
610
|
+
...(stage.browser === true ? { browser: true } : {}),
|
|
569
611
|
...(stage.merge === undefined ? {} : { merge: stage.merge }),
|
|
570
612
|
...(stage.subagent === undefined ? {} : { subagent: stage.subagent }),
|
|
571
613
|
...(stage.options === undefined ? {} : { options: stage.options.map(one => ({ label: one.label, to: one.to })) }),
|
|
@@ -589,6 +631,8 @@ function describeFlow(store, flow, now) {
|
|
|
589
631
|
}),
|
|
590
632
|
cards: store.flowCards(flow.id, true).slice(0, 10).map(card => cardOf(definition, card)),
|
|
591
633
|
unattended: unattendedJson(store, flow, now),
|
|
634
|
+
// Browser use, only for a flow with a zone that starts one: the same status line the flow page shows.
|
|
635
|
+
...(() => { const view = browserViewOf(store, flow); return view === null ? {} : { browser: { status: view.status, words: view.words, signedBy: view.signedBy, signedAt: view.signedAt } }; })(),
|
|
592
636
|
};
|
|
593
637
|
}
|
|
594
638
|
/** Whether a flow runs on its own, and until when: null when it doesn't. */
|
|
@@ -606,6 +650,7 @@ function showLines(flow) {
|
|
|
606
650
|
? `Runs on its own: no · turn on: toolroll flows unattended ${flow.id} on --days N`
|
|
607
651
|
: `Runs on its own until ${untilWords(flow.unattended.until)} · revoke: toolroll flows unattended ${flow.id} off`,
|
|
608
652
|
...(flow.unattended === null ? [] : [` Covers: ${flow.unattended.covers.join('; ')}`]),
|
|
653
|
+
...(flow.browser === undefined ? [] : [`${flow.browser.words} · ${flow.browser.status === 'allowed' ? `turn off: toolroll flows browser ${flow.id} off` : `allow: toolroll flows browser ${flow.id} on`}`]),
|
|
609
654
|
'', 'Zones',
|
|
610
655
|
...flow.zones.flatMap((zone, index) => [
|
|
611
656
|
` ${index + 1}. ${zone.title} (${zone.id}) — ${zone.does}${zone.id === flow.start ? ' · start' : ''}`,
|
package/dist/flows-ui.js
CHANGED
|
@@ -24,6 +24,7 @@ import { callWords, receiptWords } from "./subagent-tools.js";
|
|
|
24
24
|
import { undoFor } from "./subagent-week.js";
|
|
25
25
|
import { flowCardTaskLine } from "./flow-card-task.js";
|
|
26
26
|
import { unattendedViewOf } from "./flow-unattended.js";
|
|
27
|
+
import { browserViewOf } from "./flow-browser.js";
|
|
27
28
|
import { readFlowSecrets } from "./flow-secrets.js";
|
|
28
29
|
import { html, joinHtml, postForm } from "./html.js";
|
|
29
30
|
const projectName = (repo) => repo.split(/[\\/]/).filter(Boolean).pop() ?? repo;
|
|
@@ -114,6 +115,8 @@ const historyText = (event, title, sorts = new Set()) => {
|
|
|
114
115
|
export function flowView(store, flow, viewer, selectedCard, setup = { dir: null, repos: [] }) {
|
|
115
116
|
const definition = flowDefinitionOf(flow);
|
|
116
117
|
const stages = definition?.stages ?? [];
|
|
118
|
+
const browserNow = browserViewOf(store, flow);
|
|
119
|
+
const browser = browserNow === null ? null : { ...browserNow, canApprove: viewer.approver && store.accountOf(viewer.name)?.projects === null };
|
|
117
120
|
const title = (id) => stages.find(one => one.id === id)?.title ?? id;
|
|
118
121
|
const sortZones = new Set(stages.filter(one => one.kind === "sort").map(one => one.id));
|
|
119
122
|
const decisions = sortZones.size === 0 ? new Map() : store.flowSortDecisions(flow.id);
|
|
@@ -179,10 +182,15 @@ export function flowView(store, flow, viewer, selectedCard, setup = { dir: null,
|
|
|
179
182
|
// After research: the same numbered items the message carried, each beside its screenshot.
|
|
180
183
|
...(content.items === undefined ? {} : { items: content.items.map((item, index) => ({ number: index + 1, title: item.title, lines: whyLines(item.why), source: item.source, url: item.url,
|
|
181
184
|
image: item.shot === null || content.shots === null ? null : { src: `/r/${content.shots.run}/evidence/${item.shot}`, caption: itemCaption(index + 1, item) } })) }) });
|
|
185
|
+
const browserWait = card.state === "active" && stage?.kind === "check" && stage.browser === true && browser !== null && browser.status !== "allowed"
|
|
186
|
+
&& card.waiting !== null && card.waiting.startsWith(`${stage.title} starts a browser`);
|
|
182
187
|
return {
|
|
183
188
|
id: card.id, title: card.title, description: card.description, stage: card.stage, state: card.state,
|
|
184
189
|
waiting: card.state === "active" && card.task !== null && (stage?.kind === "task" || stage?.kind === "report")
|
|
185
|
-
? canReadTask ? flowCardTaskLine(store, card.task, taskRepo, now, secretsFor(taskRepo)) : "Task unavailable"
|
|
190
|
+
? canReadTask ? flowCardTaskLine(store, card.task, taskRepo, now, secretsFor(taskRepo)) : "Task unavailable"
|
|
191
|
+
// Waiting for browser use: the page offers Allow browser itself, so the command line the CLI shows is left off.
|
|
192
|
+
: browserWait ? card.waiting.replace(/ Approve it with: toolroll flows browser \d+ on$/, "") : card.waiting,
|
|
193
|
+
...(browserWait ? { browserWait: true } : {}),
|
|
186
194
|
task: task === null ? null : { id: task, href: `/t/${encodeURIComponent(task)}`,
|
|
187
195
|
// Its running attempt's composer, for an approver who may read the task (steer-controls.ts).
|
|
188
196
|
...(() => {
|
|
@@ -262,6 +270,7 @@ export function flowView(store, flow, viewer, selectedCard, setup = { dir: null,
|
|
|
262
270
|
live: flowFingerprint(store, flow.id),
|
|
263
271
|
canEdit: viewer.approver,
|
|
264
272
|
unattended: unattendedViewOf(store, flow, viewer, setup.now ?? now),
|
|
273
|
+
browser,
|
|
265
274
|
approvers: store.listApprovers().map(one => one.name).filter(name => store.accountCanAccess(name, flow.repo)),
|
|
266
275
|
projects: [flow.repo, ...setup.repos.filter(one => one !== flow.repo)].map(path => ({ path, name: projectName(path) })),
|
|
267
276
|
kinds: FLOW_STAGE_KINDS.map(kind => ({ kind, label: FLOW_KIND_WORDS[kind].label, about: FLOW_KIND_WORDS[kind].about })),
|
|
@@ -271,7 +280,7 @@ export function flowView(store, flow, viewer, selectedCard, setup = { dir: null,
|
|
|
271
280
|
/** The page the canvas replaces when scripts can't run: each zone and its cards, in order. */
|
|
272
281
|
export function flowFallbackHtml(view) {
|
|
273
282
|
const own = view.unattended.active;
|
|
274
|
-
return html `<section class="flows flow-fallback"><p class="meta">${view.flow.project}</p>${own !== null && html `<p data-flow-unattended>Runs on its own until ${own.untilWords}</p>`}<ol>${view.stages.map(stage => {
|
|
283
|
+
return html `<section class="flows flow-fallback"><p class="meta">${view.flow.project}</p>${own !== null && html `<p data-flow-unattended>Runs on its own until ${own.untilWords}</p>`}${view.browser !== null && html `<p data-flow-browser="${view.browser.status}">${view.browser.words}</p>`}<ol>${view.stages.map(stage => {
|
|
275
284
|
const cards = view.cards.filter(card => card.stage === stage.id && card.state === "active");
|
|
276
285
|
return html `<li><strong>${stage.title}</strong> · ${FLOW_KIND_WORDS[stage.kind].label}${cards.length > 0 && html `<ul>${cards.map(card => html `<li>${card.title}${card.waiting !== null && ` — ${card.waiting}`}</li>`)}</ul>`}</li>`;
|
|
277
286
|
})}</ol></section>`;
|
package/dist/flows.js
CHANGED
|
@@ -224,7 +224,9 @@ function canonicalStage(input, index) {
|
|
|
224
224
|
close: kind === "update" ? stage["close"] !== false : null,
|
|
225
225
|
script: kind === "check" ? said(stage["script"]) : null,
|
|
226
226
|
// Left out, a script runs where every script ran before v90: in a copy of the card's work.
|
|
227
|
-
...(kind === "check" ? { ...(runIn === undefined ? {} : { runIn }), ...(routes === undefined ? {} : { routes }), ...(secrets === undefined ? {} : { secrets })
|
|
227
|
+
...(kind === "check" ? { ...(runIn === undefined ? {} : { runIn }), ...(routes === undefined ? {} : { routes }), ...(secrets === undefined ? {} : { secrets }),
|
|
228
|
+
// v144: a script that starts a browser says so; false or none is a script that doesn't (and keeps its digest).
|
|
229
|
+
...(!given(stage["browser"]) || stage["browser"] === false ? {} : { browser: stage["browser"] }) } : {}),
|
|
228
230
|
sort: kind === "sort" ? canonicalSort(stage["sort"]) : null,
|
|
229
231
|
...(kind === "request" ? { request: { method, url: said(request["url"]), headers: Object.fromEntries(Object.entries(isRecord(request["headers"]) ? request["headers"] : {}).flatMap(([name, value]) => { const one = said(value); return one === null ? [] : [[name, one]]; })), body: method === "GET" || method === "DELETE" ? null : said(request["body"]) } } : {}),
|
|
230
232
|
...(kind === "email" ? { email: { to: said(email["to"]), subject: said(email["subject"]), body: said(email["body"]) } } : {}),
|
|
@@ -732,6 +734,7 @@ export function flowFromSteps(input, previous = null) {
|
|
|
732
734
|
...(step.runIn !== undefined ? { runIn: step.runIn } : old?.runIn === undefined ? {} : { runIn: old.runIn }),
|
|
733
735
|
...(step.routes !== undefined ? routesOf(step.routes) : old?.routes === undefined ? {} : { routes: old.routes }),
|
|
734
736
|
...(step.secrets !== undefined ? step.secrets.length === 0 ? {} : { secrets: step.secrets } : old?.secrets === undefined ? {} : { secrets: old.secrets }),
|
|
737
|
+
...((step.browser ?? old?.browser) === true ? { browser: true } : {}),
|
|
735
738
|
} : {}),
|
|
736
739
|
sort: kind === "sort" ? sortFromStep(step, old?.sort ?? null, find) : null,
|
|
737
740
|
...(kind === "request" ? { request: { method: step.method ?? old?.request?.method ?? "POST", url: step.url ?? old?.request?.url ?? "", headers: step.headers ?? old?.request?.headers ?? {}, body: step.body ?? old?.request?.body ?? null } } : {}),
|
|
@@ -912,7 +915,7 @@ export function flowTerms(definition, previous) {
|
|
|
912
915
|
lines.push("Opens a pull request for the card's built result, then waits for CI.", `Checks pass → ${stage.merge === undefined ? to(stage.next) : `merges it (${stage.merge}) and deletes its branch, only when a person approved it${approval === undefined ? "" : ` at ${approval.title}`} since it was built. Then → ${to(stage.next)}`}`, `Checks fail → ${stage.onFail === null ? "waits there, naming the failing check." : `${to(stage.onFail)} The failing check is named on the card.`}`);
|
|
913
916
|
}
|
|
914
917
|
else if (stage.kind === "check")
|
|
915
|
-
lines.push(`Runs the project's script “${stage.script}” with no AI.`, `Passes → ${to(stage.next)}${stage.onFail === null ? " Fails → waits there." : ` Fails → ${to(stage.onFail)}`}`);
|
|
918
|
+
lines.push(`Runs the project's script “${stage.script}” with no AI.`, ...(stage.browser === true ? ["Starts a browser (Playwright's Chromium): it runs only once a person approves browser use for this flow."] : []), `Passes → ${to(stage.next)}${stage.onFail === null ? " Fails → waits there." : ` Fails → ${to(stage.onFail)}`}`);
|
|
916
919
|
else if (stage.kind === "wait" && stage.wait !== undefined)
|
|
917
920
|
lines.push(stage.wait.for === "hours" ? `Waits until it's between ${stage.wait.from} and ${stage.wait.to}${stage.wait.timeZone === undefined ? "" : ` (${stage.wait.timeZone})`}; cards that arrive then go straight on. Then → ${to(stage.next)}`
|
|
918
921
|
: stage.wait.for === "time" ? `Waits ${durationWords(stage.wait.minutes)}. Then → ${to(stage.next)}`
|
package/dist/integrations-ui.js
CHANGED
|
@@ -14,7 +14,7 @@ export const INTEGRATIONS_CSS = `.integrations{max-width:760px;min-width:0}.inte
|
|
|
14
14
|
`.integrations .integration>:not(.integration-action),.integrations .integration-body>*{grid-column:1}.integrations .integration-body{display:contents}.integrations .integration:first-of-type{border-top:0}.integrations .integration-head{margin:0;display:flex;flex-wrap:wrap;gap:6px 10px;align-items:center;min-width:0}` +
|
|
15
15
|
`.integrations .integration .integration-body>*{margin-left:calc(var(--brand-mark) + 10px)}.integrations .integration-head strong{overflow-wrap:anywhere}.integrations .integration p{margin:2px 0 0;min-width:0;overflow-wrap:anywhere}` +
|
|
16
16
|
`.integrations .integration-action{grid-column:2;grid-row:1 / span 3;align-self:center;margin:0}.integrations .integration-action button,.integrations .integration-action .button-link{min-height:44px;white-space:nowrap}` +
|
|
17
|
-
`.integrations .integration .integration-action button{width:auto}.integrations .integration .integration-action a.integration-quiet{background:var(--so-paper);color:var(--so-ink);border:1px solid var(--so-input-line)}` +
|
|
17
|
+
`.integrations .integration .integration-action button{width:auto}.integrations .integration .integration-action a.integration-quiet,.integrations .integration .integration-action button.integration-quiet{background:var(--so-paper);color:var(--so-ink);border:1px solid var(--so-input-line)}` +
|
|
18
18
|
`.integrations .integration-fix{color:var(--so-danger)}.integrations code{overflow-wrap:anywhere}.integrations details{margin-top:4px;font-size:.8125rem}.integrations summary{cursor:pointer;padding:4px 0}` +
|
|
19
19
|
`.integration-state{display:inline-flex;align-items:center;gap:6px;font-size:.75rem;font-weight:500;padding:0 8px;border-radius:5px;line-height:20px;white-space:nowrap}` +
|
|
20
20
|
`.integration-state i{width:6px;height:6px;border-radius:50%;background:currentColor}` +
|
|
@@ -33,6 +33,10 @@ function action(one, csrf) {
|
|
|
33
33
|
if (a.kind === "test") {
|
|
34
34
|
return csrf === "" ? "" : postForm("/settings/integrations/test", html `<button type="submit">Send test</button>`, { attrs: { class: "integration-action" }, hidden: { key: one.key } });
|
|
35
35
|
}
|
|
36
|
+
// A project's youiyoux requests: one switch for that project.
|
|
37
|
+
if (a.kind === "switch") {
|
|
38
|
+
return csrf === "" ? "" : postForm("/settings/integrations/youiyoux", html `<button type="submit"${a.on ? "" : html ` class="integration-quiet"`}>${a.label}</button>`, { attrs: { class: "integration-action" }, hidden: { repo: a.repo, on: a.on ? "1" : "0" } });
|
|
39
|
+
}
|
|
36
40
|
// A command is run where Toolroll runs; a page is linked. Fix and Set up both say which.
|
|
37
41
|
// Fix is the one filled action on the page; Set up stays quiet so a list of unused services doesn't shout.
|
|
38
42
|
if (a.href !== null)
|
|
@@ -50,12 +54,13 @@ function row(one, csrf) {
|
|
|
50
54
|
? html `<span class="integration-state integration-state--checking"><i aria-hidden="true"></i>Checking</span>`
|
|
51
55
|
: html `<span class="integration-state integration-state--${one.state}"><i aria-hidden="true"></i>${STATE_WORDS[one.state]}</span>`;
|
|
52
56
|
const facts = [one.account, one.detail, one.credentials].filter(Boolean).join(" · ");
|
|
53
|
-
const command = one.action.kind
|
|
54
|
-
const
|
|
57
|
+
const command = one.action.kind === "setup" || one.action.kind === "fix" ? one.action.command : null;
|
|
58
|
+
const words = one.action.kind === "fix" || one.action.kind === "switch" ? one.action.words : null;
|
|
59
|
+
const fix = words !== null ? html `<p class="integration-fix" role="status">${words}</p>` : "";
|
|
55
60
|
const run = command === null || (one.action.kind === "fix" && one.action.words.includes(command)) ? "" : html `<p class="meta">Run <code>${command}</code> on the computer running Toolroll.</p>`;
|
|
56
61
|
const seen = [one.lastSuccessAt === null ? null : html `Last success ${when(one.lastSuccessAt)}`, one.usedBy.length === 0 ? null : html `Used by ${one.usedBy.join(", ")}`].filter(Boolean);
|
|
57
62
|
const history = one.state === "not-set-up" || seen.length === 0 ? "" : html `<p class="meta">${joinHtml(seen, " · ")}</p>`;
|
|
58
|
-
const lastError = one.lastError === null || one.state === "not-set-up" ? "" : one.state === "broken" &&
|
|
63
|
+
const lastError = one.lastError === null || one.state === "not-set-up" ? "" : one.state === "broken" && words === one.lastError
|
|
59
64
|
? html `<p class="meta">Failed ${when(one.lastErrorAt ?? "")}</p>`
|
|
60
65
|
: html `<details><summary>Last error ${one.lastErrorAt === null ? "" : when(one.lastErrorAt)}</summary><p class="meta">${one.lastError}</p></details>`;
|
|
61
66
|
return html `<div class="integration" data-integration="${one.key}" data-state="${one.state}"><p class="integration-head">${brandMarkHtml(markFor(one), one.name, one.state === "connected")}<strong>${one.name}</strong> ${badge}</p>${action(one, csrf)}<div class="integration-body">${facts === "" ? "" : html `<p class="meta">${facts}</p>`}${fix}${run}${history}${lastError}</div></div>`;
|
package/dist/integrations.d.ts
CHANGED
|
@@ -18,7 +18,15 @@ export type IntegrationAction = ({
|
|
|
18
18
|
kind: "fix";
|
|
19
19
|
label: "Fix";
|
|
20
20
|
words: string;
|
|
21
|
-
} & IntegrationPlace)
|
|
21
|
+
} & IntegrationPlace)
|
|
22
|
+
/** A project's youiyoux requests: one switch, and the problem to fix when it is Broken. */
|
|
23
|
+
| {
|
|
24
|
+
kind: "switch";
|
|
25
|
+
label: "Turn off" | "Turn on";
|
|
26
|
+
repo: string;
|
|
27
|
+
on: boolean;
|
|
28
|
+
words: string | null;
|
|
29
|
+
};
|
|
22
30
|
export type Integration = {
|
|
23
31
|
key: string;
|
|
24
32
|
group: IntegrationGroup;
|
package/dist/integrations.js
CHANGED
|
@@ -31,6 +31,7 @@ import { LINEAR_URL, readHooksBase, readLinearKey } from "./flow-triggers.js";
|
|
|
31
31
|
import { credentialModeOf, projectToolsOf, readToolSecrets, missingSecrets, secretsSetFor, testToolOf } from "./project-tools.js";
|
|
32
32
|
import { origin, readMonitoring } from "./monitoring-settings.js";
|
|
33
33
|
import { targetOf } from "./monitoring.js";
|
|
34
|
+
import { youiyouxState } from "./youiyoux-intake.js";
|
|
34
35
|
export const STATE_WORDS = { connected: "Connected", "not-set-up": "Not set up", broken: "Broken" };
|
|
35
36
|
const NO_ACTIVITY = { okAt: null, error: null, errorAt: null, failing: false };
|
|
36
37
|
const PROBE_MS = 8_000;
|
|
@@ -459,10 +460,46 @@ function combine(target, row) {
|
|
|
459
460
|
}
|
|
460
461
|
return { ...base, ...common, state: "connected", action: { kind: "test", label: "Send test" } };
|
|
461
462
|
}
|
|
463
|
+
/**
|
|
464
|
+
* youiyoux change requests, one row per project that uses youiyoux (youiyoux-intake.ts): Connected with how many of
|
|
465
|
+
* its requests are queued, Broken when the last pass couldn't read its folder or some of its files, or Not set up when
|
|
466
|
+
* a person turned it off; one row Not set up when no project uses it. Read from the store and the folders' presence.
|
|
467
|
+
*/
|
|
468
|
+
function youiyouxIntegrations(io) {
|
|
469
|
+
// In the order every other row's keys come (combine), so `--json` reads alike.
|
|
470
|
+
const row = (key, account, facts, state, action) => ({
|
|
471
|
+
key, group: "code", name: "youiyoux", usedBy: facts.usedBy ?? [], account, detail: facts.detail, checked: true, checkedAt: facts.checkedAt ?? null,
|
|
472
|
+
lastSuccessAt: facts.lastSuccessAt ?? null, lastError: facts.lastError ?? null, lastErrorAt: facts.lastErrorAt ?? null, state, action,
|
|
473
|
+
});
|
|
474
|
+
const rows = io.repos.flatMap((repo) => {
|
|
475
|
+
let state;
|
|
476
|
+
try {
|
|
477
|
+
state = youiyouxState(io.store, repo);
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
return [];
|
|
481
|
+
}
|
|
482
|
+
if (!state.uses)
|
|
483
|
+
return [];
|
|
484
|
+
const key = `youiyoux:${repo}`, account = basename(repo);
|
|
485
|
+
const turn = (on, words) => ({ kind: "switch", label: on ? "Turn on" : "Turn off", repo, on, words });
|
|
486
|
+
if (state.off)
|
|
487
|
+
return [row(key, account, { detail: "Turned off" }, "not-set-up", turn(true, null))];
|
|
488
|
+
const facts = { usedBy: [`Tasks in ${account}`], detail: `${plural(state.queued, "request")} queued`, checkedAt: state.checkedAt };
|
|
489
|
+
if (state.problem === null)
|
|
490
|
+
return [row(key, account, { ...facts, lastSuccessAt: state.checkedAt }, "connected", turn(false, null))];
|
|
491
|
+
const words = scrubIntegrationText(state.problem);
|
|
492
|
+
return [row(key, account, { ...facts, lastError: words, lastErrorAt: state.checkedAt }, "broken", turn(false, words))];
|
|
493
|
+
});
|
|
494
|
+
return rows.length > 0 ? rows : [row("youiyoux", null, { detail: "Starts by itself in a project with a youi.project.json" }, "not-set-up", { kind: "setup", label: "Set up", href: null, command: null })];
|
|
495
|
+
}
|
|
462
496
|
/** Every integration as it stands now, from files and saved history only: no network, no process, never slow. */
|
|
463
497
|
export function integrationsNow(io) {
|
|
464
498
|
const rows = readIntegrationChecks(io.store);
|
|
465
|
-
|
|
499
|
+
const list = targets(io).map(target => combine(target, rows.get(target.key)));
|
|
500
|
+
const linear = list.findIndex(one => one.key === "linear");
|
|
501
|
+
list.splice(linear + 1, 0, ...youiyouxIntegrations(io));
|
|
502
|
+
return list;
|
|
466
503
|
}
|
|
467
504
|
/** Run the checks (all, or the named ones), record each answer, and return the list. */
|
|
468
505
|
export async function checkIntegrations(io, keys) {
|
|
@@ -512,7 +549,11 @@ export function renderIntegrations(list, secrets = null) {
|
|
|
512
549
|
lines.push(`${"".padEnd(width)} last success ${whenText(one.lastSuccessAt)}${one.lastError === null ? "" : ` · last error ${whenText(one.lastErrorAt)}: ${one.lastError}`}`);
|
|
513
550
|
if (one.action.kind === "fix")
|
|
514
551
|
lines.push(`${"".padEnd(width)} Fix: ${one.action.words}${one.action.command === null || one.action.words.includes(one.action.command) ? "" : ` (run ${one.action.command})`}`);
|
|
515
|
-
if (one.action.kind === "
|
|
552
|
+
if (one.action.kind === "switch" && one.action.words !== null)
|
|
553
|
+
lines.push(`${"".padEnd(width)} Fix: ${one.action.words}`);
|
|
554
|
+
if (one.action.kind === "switch" && one.action.on)
|
|
555
|
+
lines.push(`${"".padEnd(width)} Turn on: Settings → Integrations`);
|
|
556
|
+
if (one.action.kind === "setup" && (one.action.command !== null || one.action.href !== null))
|
|
516
557
|
lines.push(`${"".padEnd(width)} Set up: ${one.action.command !== null ? `run ${one.action.command}` : `Settings (${one.action.href ?? "/settings"})`}`);
|
|
517
558
|
return lines;
|
|
518
559
|
}), ...note];
|
package/dist/landed-cleanup.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type AssignmentSnapshot } from "./assignment.js";
|
|
1
2
|
import type { RemoteMainRead, RemoteMains } from "./remote-main.js";
|
|
2
3
|
import type { Store } from "./store.js";
|
|
3
4
|
import { type Git } from "./teardown-git.js";
|
|
@@ -70,6 +71,9 @@ export type LandedCandidate = {
|
|
|
70
71
|
targets: LandedTarget[];
|
|
71
72
|
keeps: LandedKeep[];
|
|
72
73
|
};
|
|
74
|
+
/** Use the current manual completion guards. Only a recorded merge of this exact result can settle them. Also why a
|
|
75
|
+
* youiyoux options task a person decided on in the hub stays for review (youiyoux-intake.ts). */
|
|
76
|
+
export declare function completionProblem(store: Store, assignment: AssignmentSnapshot, root: string): string | null;
|
|
73
77
|
/**
|
|
74
78
|
* Results in a project whose work landed, with what their teardown would remove and what stays. Reads only.
|
|
75
79
|
* `limit` caps how many results are read against git (reconcile's bound; results read before and unchanged since
|
package/dist/landed-cleanup.js
CHANGED
|
@@ -237,8 +237,9 @@ function resultProblem(store, runId, head) {
|
|
|
237
237
|
return "its result couldn't be read";
|
|
238
238
|
return run.outcome === "no-change" || run.baseRevision === head ? "its result has no new work" : null;
|
|
239
239
|
}
|
|
240
|
-
/** Use the current manual completion guards. Only a recorded merge of this exact result can settle them.
|
|
241
|
-
|
|
240
|
+
/** Use the current manual completion guards. Only a recorded merge of this exact result can settle them. Also why a
|
|
241
|
+
* youiyoux options task a person decided on in the hub stays for review (youiyoux-intake.ts). */
|
|
242
|
+
export function completionProblem(store, assignment, root) {
|
|
242
243
|
if (assignment.state !== "ready-to-check" && assignment.state !== "complete")
|
|
243
244
|
return "it needs a decision first";
|
|
244
245
|
if (assignment.state === "complete")
|
|
@@ -181,7 +181,8 @@ export function integrationCapabilities(list) {
|
|
|
181
181
|
return list.map(one => {
|
|
182
182
|
const link = one.group === "agents" ? "providers" : one.group === "tools" ? "tools" : INTEGRATION_LINK[one.key.replace(/^.*:/, "").toLowerCase()] ?? INTEGRATION_LINK[one.name.toLowerCase()] ?? "integrations";
|
|
183
183
|
const lets = one.usedBy.length > 0 ? `use ${one.name} for ${one.usedBy.join(", ")}` : `use ${one.name}`;
|
|
184
|
-
const next = one.action.kind === "setup" ? `Not set up: set up ${one.name} in Settings` : one.action.kind === "fix" ? `Fix: ${one.action.words}`
|
|
184
|
+
const next = one.action.kind === "setup" ? `Not set up: set up ${one.name} in Settings` : one.action.kind === "fix" ? `Fix: ${one.action.words}`
|
|
185
|
+
: one.action.kind === "switch" ? (one.action.words !== null ? `Fix: ${one.action.words}` : one.action.on ? `Turned off: turn ${one.name} on in Settings → Integrations` : null) : null;
|
|
185
186
|
return { name: one.name, kind: one.group, state: STATE_WORDS[one.state], ok: one.state === "connected", account: one.account, usedBy: one.usedBy, lastError: one.lastError, lets, next, link: next === null ? null : link };
|
|
186
187
|
});
|
|
187
188
|
}
|
package/dist/lead-cli.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseFeedbackImage } from "./result-review.js";
|
|
1
2
|
import { configureLeadFollow, leadFollowStatus } from './lead-follow.js';
|
|
2
3
|
import { closedQuestionWords } from "./decision.js";
|
|
3
4
|
/**
|
|
@@ -104,7 +105,8 @@ export function proposalBlock(store, proposal, repos, number) {
|
|
|
104
105
|
const snapshot = proposal.payload["snapshot"];
|
|
105
106
|
const selected = Array.isArray(proposal.payload["notes"]) ? proposal.payload["notes"] : [];
|
|
106
107
|
const notes = snapshot?.notes.filter(one => selected.includes(one.id)) ?? [];
|
|
107
|
-
|
|
108
|
+
const image = parseFeedbackImage(proposal.payload["image"]);
|
|
109
|
+
return [line, ...(image ? [` Screenshot ${image.artifact} · result #${image.run}`] : []), ...notes.map(one => ` ${one.image ? `Screenshot ${one.image.artifact} · result #${one.image.run}: ` : ""}${one.path === null ? "" : `${one.path}${one.line === null ? "" : `:${one.line}`}: `}${one.note}`),
|
|
108
110
|
proposal.payload["operation"] === "revise" ? " Creates a revision of the same task; approval is separate." : " Saves feedback; no work starts."];
|
|
109
111
|
}
|
|
110
112
|
return proposal.kind === "answer" ? [line, ...answerContextLines(store, proposal.payload)] : [line];
|
package/dist/lead-tools.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type FeedbackImage } from "./result-review.js";
|
|
1
2
|
import { type CommitmentChannel } from "./lead-commitments.js";
|
|
2
3
|
import { type Integration } from "./integrations.js";
|
|
3
4
|
import type { AuthMode } from "./keys.js";
|
|
@@ -23,6 +24,7 @@ export type LeadToolContext = {
|
|
|
23
24
|
step: number;
|
|
24
25
|
snapshot: ReviewSnapshot;
|
|
25
26
|
}>;
|
|
27
|
+
replyImage?: FeedbackImage;
|
|
26
28
|
/** Where evidence lives, when the surface knows — a scout's report reads from here. */
|
|
27
29
|
evidenceRoot?: string;
|
|
28
30
|
/** Records the screenshots this turn selected, under the turn, for a channel that delivers files; returns how many are recorded. Absent: nothing durable is kept. */
|