toolroll 0.8.2 → 0.9.0
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 +31 -0
- package/dist/agent-onboard.d.ts +3 -0
- package/dist/agent-onboard.js +3 -0
- package/dist/browser/workspace.js +21 -20
- package/dist/browser-workspace.d.ts +10 -0
- package/dist/chat-delivery.js +51 -3
- package/dist/chat-quiet.d.ts +28 -0
- package/dist/chat-quiet.js +161 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +3 -0
- package/dist/digest-times.d.ts +3 -0
- package/dist/digest-times.js +7 -0
- package/dist/dispose.js +2 -0
- package/dist/export.js +1 -1
- package/dist/integrations-ui.d.ts +13 -0
- package/dist/integrations-ui.js +65 -0
- package/dist/integrations.d.ts +113 -0
- package/dist/integrations.js +526 -0
- package/dist/operate.d.ts +5 -2
- package/dist/operate.js +151 -4
- package/dist/project-delete.js +1 -0
- package/dist/publish.d.ts +11 -0
- package/dist/publish.js +9 -2
- package/dist/pull-request-flow.d.ts +140 -0
- package/dist/pull-request-flow.js +440 -0
- package/dist/pull-request-schema.d.ts +5 -0
- package/dist/pull-request-schema.js +21 -0
- package/dist/pull-request-ui.d.ts +23 -0
- package/dist/pull-request-ui.js +55 -0
- package/dist/pulls.d.ts +5 -0
- package/dist/pulls.js +11 -0
- package/dist/serve.d.ts +6 -0
- package/dist/serve.js +264 -16
- package/dist/setup-guide.d.ts +2 -2
- package/dist/store.d.ts +61 -0
- package/dist/store.js +142 -3
- package/dist/surface.js +2 -0
- package/dist/telegram-progress.d.ts +1 -0
- package/dist/telegram-progress.js +1 -1
- package/dist/telegram.js +95 -15
- package/package.json +1 -1
package/dist/operate.js
CHANGED
|
@@ -13,6 +13,7 @@ import { buildExport, exportSummary, exportZip, writeExportFolder } from "./expo
|
|
|
13
13
|
import { startBudgetAlerts } from "./budget-alerts.js";
|
|
14
14
|
import { liftAuthPause, openAuthPauses, signInGate, signInWords, startSignInProbes } from "./provider-auth.js";
|
|
15
15
|
import { createConnectionChecker } from "./provider-connection.js";
|
|
16
|
+
import { checkIntegrations, integrationsBrokenLine, integrationsNow, renderIntegrations } from "./integrations.js";
|
|
16
17
|
import { buildHandoff, handoffLines, loginAccount, runOnboard } from "./agent-onboard.js";
|
|
17
18
|
import { backupFiles, backupFolderOf, backupOwner, backupNow, restoreDatabase, startBackups } from "./backup.js";
|
|
18
19
|
import { pushLimitSink } from "./provider-limits.js";
|
|
@@ -67,7 +68,7 @@ import { runTaskOutcomeCommand } from "./task-outcome-cli.js";
|
|
|
67
68
|
*/
|
|
68
69
|
import { homedir, hostname, tmpdir } from "node:os";
|
|
69
70
|
import { basename, dirname, join, resolve } from "node:path";
|
|
70
|
-
import { openStore, openStoreNoMigrate, CONCURRENT_WRITER_WAIT_MS, isDatabaseBusy, databasePath, BUILT_IN, DEFAULT_ACTOR, parseCapabilityKey, verifiedAuthor, contestantProfileOf, } from "./store.js";
|
|
71
|
+
import { openStore, openStoreNoMigrate, CONCURRENT_WRITER_WAIT_MS, isDatabaseBusy, databasePath, BUILT_IN, DEFAULT_ACTOR, parseCapabilityKey, verifiedAuthor, contestantProfileOf, isDigestTime, } from "./store.js";
|
|
71
72
|
import { randomBytes, randomInt, randomUUID } from "node:crypto";
|
|
72
73
|
import { authorizePlanUnderMode } from "./plan-auto.js";
|
|
73
74
|
import { ghDispatchAdapter, mirrorTaskId, syncPass } from "./sync.js";
|
|
@@ -136,6 +137,7 @@ import { verifyApproverByPassword } from "./principal.js";
|
|
|
136
137
|
import { authorizedProject, canonicalProject, projectName, resolveCeiling } from "./project.js";
|
|
137
138
|
import { tally, spendLine } from "./summary.js";
|
|
138
139
|
import { bodyHashOf, describePublicationGrant, observeChecks, publicationBody, publishPass, sweepMerges, } from "./publish.js";
|
|
140
|
+
import { checkPublishing, publishingOf, savePublishing, followPullRequests } from "./pull-request-flow.js";
|
|
139
141
|
import { acquire, acquireFallback, acquireIfReady, completeFenced, finalizeFailureFenced, finalizeMalformedFenced, finalizeParkFenced, finalizePlanFenced, finalizePlanFailureFenced, finalizeScoutFenced, finalizeScoutFailureFenced, heartbeat, release, reap, currentClaim, DEFAULT_LEASE_MS, SYNC_MAX_AGE_MS, acquireContinuation, } from "./claim.js";
|
|
140
142
|
import { disposeBuildOutcome, holdStaleApproval, regateTask } from "./dispose.js";
|
|
141
143
|
import { attendedLivenessState } from "./liveness.js";
|
|
@@ -213,11 +215,14 @@ export const OPERATE_HELP = `toolroll — operating the queue
|
|
|
213
215
|
toolroll storage clean [--yes] preview removing finished tasks' clean checkouts (their branches stay); --yes removes them
|
|
214
216
|
toolroll storage cleanup <when> remove a finished task's clean checkout: finished|2d|7d|never (instance operator)
|
|
215
217
|
toolroll storage discard <path> --yes throw away a checkout kept for its changes; its branch stays
|
|
218
|
+
toolroll integrations [--json] which integrations work: Connected, Not set up or Broken, and what to do (--saved: no new checks)
|
|
216
219
|
toolroll monitoring where the audit stream and traces go, and how each destination is doing
|
|
217
220
|
toolroll check-progress <run> current or final approved check progress
|
|
218
221
|
toolroll spend [--month YYYY-MM] [--csv] what agent work cost, by project, person, teammate and model
|
|
219
222
|
toolroll export --out <path> [--zip] everything Toolroll knows, in a folder or .zip (no secrets)
|
|
220
223
|
toolroll budget list|set|remove monthly budgets (--all | --project <p> | --person <name> | --teammate <id>) --usd <n> [--alerts-only]
|
|
224
|
+
toolroll notifications [quiet|all] how chats reach you: only when you're needed (the default), or every step
|
|
225
|
+
toolroll notifications digest <HH:MM>|off one evening message: what finished, what waits, what failed
|
|
221
226
|
toolroll retention show|preview how long evidence, checkout records, chat and notifications are kept; what the daily sweep would remove
|
|
222
227
|
toolroll retention set <kind> <period> evidence|checkouts|chat|notifications, 30d|1y|forever (instance operator)
|
|
223
228
|
toolroll backup now|list back the database up now; list backups and how the last ones went
|
|
@@ -264,6 +269,10 @@ External trackers — build what a tracker nominates, under local approvals
|
|
|
264
269
|
the dispatch grant: its own explicit
|
|
265
270
|
yes, never in any default; writes a
|
|
266
271
|
plane marker label to the repository
|
|
272
|
+
toolroll publish setup [--repo <path>] [--merge-method squash|merge|rebase]
|
|
273
|
+
[--merge-when-green] --yes one-step pull requests: checks gh sign-in
|
|
274
|
+
and push rights, then Complete offers
|
|
275
|
+
"Complete and open a pull request"
|
|
267
276
|
toolroll publish grant --github <owner/name> --allow-merge
|
|
268
277
|
--merge-method squash|merge|rebase [--merge-delete-branch] --yes
|
|
269
278
|
auto-merge this plane's own PRs —
|
|
@@ -532,7 +541,7 @@ export const TASK_ACTIONS = [
|
|
|
532
541
|
"reopen", "scope", "approve", "hold", "unhold", "require", "requeue", "regate", "plan",
|
|
533
542
|
"review", "accept", "repair", "route", "stop", "resume", "complete", "revise",
|
|
534
543
|
];
|
|
535
|
-
export const PUBLISH_ACTIONS = ["grant", "revoke", "status", "unblock", "rearm", "merge", "refire"];
|
|
544
|
+
export const PUBLISH_ACTIONS = ["setup", "grant", "revoke", "status", "unblock", "rearm", "merge", "refire"];
|
|
536
545
|
export const CONFIG_ACTIONS = ["show", "set", "clear"];
|
|
537
546
|
export const APPROVER_ACTIONS = ["list", "add"];
|
|
538
547
|
export const ROUTINE_ACTIONS = ["list", "add", "show", "approve", "refresh", "pause", "resume", "run-now"];
|
|
@@ -577,6 +586,8 @@ export const OPERATE_BOOLEAN_FLAGS = new Set([
|
|
|
577
586
|
"html", "csv", "alerts-only",
|
|
578
587
|
// v105: the full export.
|
|
579
588
|
"zip",
|
|
589
|
+
// Settings → Integrations: the last checks, without checking again.
|
|
590
|
+
"saved",
|
|
580
591
|
]);
|
|
581
592
|
export function parseOperateArgs(argv) {
|
|
582
593
|
const positional = [];
|
|
@@ -720,6 +731,7 @@ export async function runOperate(command, argv, write, options = {}) {
|
|
|
720
731
|
...(options.dispatchAdapter === undefined ? {} : { dispatchAdapter: options.dispatchAdapter }),
|
|
721
732
|
...(options.flowTriggerIo === undefined ? {} : { flowTriggerIo: options.flowTriggerIo }),
|
|
722
733
|
...(options.flowStepIo === undefined ? {} : { flowStepIo: options.flowStepIo }),
|
|
734
|
+
...(options.integrationIo === undefined ? {} : { integrationIo: options.integrationIo }),
|
|
723
735
|
...(options.shouldStop === undefined ? {} : { shouldStop: options.shouldStop }),
|
|
724
736
|
...(options.mateSeams === undefined ? {} : { mateSeams: options.mateSeams }),
|
|
725
737
|
...(options.heldCoordinator === undefined ? {} : { heldCoordinator: options.heldCoordinator }),
|
|
@@ -817,6 +829,8 @@ async function dispatch(command, positional, flags, context) {
|
|
|
817
829
|
return budgetCommand(positional, flags, context);
|
|
818
830
|
case "retention":
|
|
819
831
|
return retentionCommand(positional, flags, context);
|
|
832
|
+
case "notifications":
|
|
833
|
+
return notificationsCommand(positional, flags, context);
|
|
820
834
|
case "backup":
|
|
821
835
|
return backupCommand(positional, flags, context);
|
|
822
836
|
case "export":
|
|
@@ -825,6 +839,8 @@ async function dispatch(command, positional, flags, context) {
|
|
|
825
839
|
return policyCommand(positional, flags, context);
|
|
826
840
|
case "storage":
|
|
827
841
|
return storageCommand(positional, flags, context);
|
|
842
|
+
case "integrations":
|
|
843
|
+
return integrationsCommand(positional, flags, context);
|
|
828
844
|
case "monitoring": {
|
|
829
845
|
// Where the audit stream and traces go, and how each destination is doing. Settings → Monitoring changes it.
|
|
830
846
|
const settings = readMonitoring(dirname(context.databaseFile));
|
|
@@ -7230,6 +7246,14 @@ async function runWatchLoop(args) {
|
|
|
7230
7246
|
});
|
|
7231
7247
|
if (checks.failing > 0)
|
|
7232
7248
|
progress(`watch: CI is red on ${checks.failing} published PR(s) — the outbox has it`);
|
|
7249
|
+
const followed = await followPullRequests(store, checks.seen, {
|
|
7250
|
+
evidenceRoot: context.evidenceRoot,
|
|
7251
|
+
...(context.publishExec === undefined ? {} : { exec: context.publishExec }),
|
|
7252
|
+
});
|
|
7253
|
+
if (followed.revisions > 0)
|
|
7254
|
+
progress(`watch: filed ${followed.revisions} CI revision(s)`);
|
|
7255
|
+
if (followed.merged > 0)
|
|
7256
|
+
progress(`watch: merged ${followed.merged} pull request(s) whose checks passed`);
|
|
7233
7257
|
}
|
|
7234
7258
|
catch (error) {
|
|
7235
7259
|
progress(`watch: CI observation failed — ${describe(error)}; retrying on its next interval`);
|
|
@@ -7710,7 +7734,7 @@ async function onboardCommand(flags, context) {
|
|
|
7710
7734
|
const gitRun = context.gitRunner ?? ((file, args, opts) => run(file, [...args], { ...(opts?.cwd === undefined ? {} : { cwd: opts.cwd }), timeoutMs: 10_000 }));
|
|
7711
7735
|
const registryPath = registryPathOf(context);
|
|
7712
7736
|
const checker = createConnectionChecker({ home, env });
|
|
7713
|
-
|
|
7737
|
+
const code = await runOnboard({
|
|
7714
7738
|
write,
|
|
7715
7739
|
json,
|
|
7716
7740
|
yes: flag(flags, "yes"),
|
|
@@ -7750,7 +7774,23 @@ async function onboardCommand(flags, context) {
|
|
|
7750
7774
|
return { ok: true, added: "error" in before || !before.repos.includes(repo) };
|
|
7751
7775
|
},
|
|
7752
7776
|
checkConnection: seams.checkConnection ?? (agent => checker(agent)),
|
|
7777
|
+
pullRequests: seams.pullRequests ?? (async (repo) => {
|
|
7778
|
+
const publishing = publishingOf(store, repo);
|
|
7779
|
+
if (publishing.on)
|
|
7780
|
+
return `on for ${publishing.githubRepo} into ${publishing.base}`;
|
|
7781
|
+
const checked = await checkPublishing(repo, context.publishExec === undefined ? {} : { exec: context.publishExec });
|
|
7782
|
+
if (!checked.ok)
|
|
7783
|
+
return `off: ${checked.message}`;
|
|
7784
|
+
return `off: turn on with toolroll publish setup --repo ${repo} --yes (asks your password), or in Settings → Projects`;
|
|
7785
|
+
}),
|
|
7753
7786
|
});
|
|
7787
|
+
// One line when an integration is Broken, from the last checks only (never a new check here).
|
|
7788
|
+
if (!json) {
|
|
7789
|
+
const line = integrationsBrokenLine(await savedIntegrations(context).catch(() => []));
|
|
7790
|
+
if (line !== null)
|
|
7791
|
+
write(line);
|
|
7792
|
+
}
|
|
7793
|
+
return code;
|
|
7754
7794
|
}
|
|
7755
7795
|
async function upCommand(flags, context) {
|
|
7756
7796
|
const { store, write, json, clock } = context;
|
|
@@ -8539,6 +8579,42 @@ async function publishCommand(positional, flags, context) {
|
|
|
8539
8579
|
if (action !== undefined && !PUBLISH_ACTIONS.includes(action)) {
|
|
8540
8580
|
return fail(write, json, "publish", "usage", `unknown \`publish ${action}\` — try ${PUBLISH_ACTIONS.join(", ")}, or bare \`publish\` for the publication pass`, EXIT.usage);
|
|
8541
8581
|
}
|
|
8582
|
+
if (action === "setup") {
|
|
8583
|
+
const mergeMethod = text(flags, "merge-method") ?? "squash";
|
|
8584
|
+
if (!["squash", "merge", "rebase"].includes(mergeMethod)) {
|
|
8585
|
+
return fail(write, json, "publish setup", "usage", "--merge-method is squash, merge or rebase", EXIT.usage);
|
|
8586
|
+
}
|
|
8587
|
+
const checked = await checkPublishing(repo, context.publishExec === undefined ? {} : { exec: context.publishExec });
|
|
8588
|
+
if (!checked.ok)
|
|
8589
|
+
return fail(write, json, "publish setup", checked.reason, checked.message, EXIT.refused);
|
|
8590
|
+
const settings = { mergeMethod: mergeMethod, mergeWhenGreen: flags.has("merge-when-green") };
|
|
8591
|
+
const plan = checked.plan;
|
|
8592
|
+
const terms = [
|
|
8593
|
+
`Pull requests for ${plan.githubRepo} into ${plan.base}${plan.account === null ? "" : `, as ${plan.account}`}.`,
|
|
8594
|
+
"Complete will offer \"Complete and open a pull request\"; the pull request opens from the exact completed commit.",
|
|
8595
|
+
`Merges ${settings.mergeMethod} and delete the branch${settings.mergeWhenGreen ? ", by themselves once checks pass" : " when a person selects Merge"}.`,
|
|
8596
|
+
];
|
|
8597
|
+
if (!flags.has("yes")) {
|
|
8598
|
+
if (json) {
|
|
8599
|
+
write(envelopeJson({ ok: false, command: "publish setup", reason: "unconfirmed", proposed: { ...plan, ...settings } }));
|
|
8600
|
+
return EXIT.refused;
|
|
8601
|
+
}
|
|
8602
|
+
for (const line of terms)
|
|
8603
|
+
write(line);
|
|
8604
|
+
write("Nothing is set up yet. Repeat with --yes --as <you> --token <password> to turn it on.");
|
|
8605
|
+
return EXIT.refused;
|
|
8606
|
+
}
|
|
8607
|
+
const acting = await askCredentials(flags, context);
|
|
8608
|
+
if (acting === null) {
|
|
8609
|
+
return fail(write, json, "publish setup", "usage", "turning on pull requests takes --as <you> --token <your password>", EXIT.usage);
|
|
8610
|
+
}
|
|
8611
|
+
const authenticated = authenticateApprover(store, acting.name, acting.token, repo);
|
|
8612
|
+
if (!authenticated.ok) {
|
|
8613
|
+
return fail(write, json, "publish setup", authenticated.reason, describeApproveFailure(authenticated.reason, acting.name), EXIT.refused);
|
|
8614
|
+
}
|
|
8615
|
+
savePublishing(store, plan, acting.name, settings, clock());
|
|
8616
|
+
return succeed(write, json, "publish setup", { publishing: publishingOf(store, repo) }, () => ["Pull requests are on.", ...terms]);
|
|
8617
|
+
}
|
|
8542
8618
|
if (action === "grant") {
|
|
8543
8619
|
const github = text(flags, "github");
|
|
8544
8620
|
if (github === undefined || !/^[\w.-]+\/[\w.-]+$/.test(github)) {
|
|
@@ -9101,7 +9177,49 @@ async function statusCommand(positional, flags, context) {
|
|
|
9101
9177
|
notifySecurityRelease(context.store, release, current, method.updateCommand, context.clock());
|
|
9102
9178
|
const line = updateLine(release, current, method.updateCommand);
|
|
9103
9179
|
const update = release === null ? {} : { update: { current, latest: release.version, security: release.security, updateCommand: method.updateCommand, url: release.url } };
|
|
9104
|
-
|
|
9180
|
+
// One line when an integration is Broken, from the last checks only: status never waits on a check.
|
|
9181
|
+
const brokenIntegrations = await savedIntegrations(context).then(list => list.filter(one => one.state === "broken"), () => []);
|
|
9182
|
+
const brokenLine = integrationsBrokenLine(brokenIntegrations);
|
|
9183
|
+
const integrations = brokenIntegrations.length === 0 ? {} : { integrations: { broken: brokenIntegrations.map(one => ({ key: one.key, name: one.name, fix: one.action.kind === "fix" ? one.action.words : null })) } };
|
|
9184
|
+
return succeed(context.write, context.json, command, { ...status, ...update, ...integrations }, () => [...renderInstallationStatus(status), ...(line === null ? [] : [line]), ...(brokenLine === null ? [] : [brokenLine])]);
|
|
9185
|
+
}
|
|
9186
|
+
/** Where `integrations` looks: the installation's files beside the database, and every project it knows. */
|
|
9187
|
+
async function integrationIoFor(context) {
|
|
9188
|
+
const registered = await loadProjectRegistry(registryPathOf(context)).catch(() => ({ error: "unreadable" }));
|
|
9189
|
+
const repos = [...new Set([...("error" in registered ? [] : registered.repos), ...context.store.listProjects().map(one => one.path)])];
|
|
9190
|
+
const seams = context.integrationIo ?? {};
|
|
9191
|
+
return {
|
|
9192
|
+
store: context.store,
|
|
9193
|
+
dir: dirname(context.databaseFile),
|
|
9194
|
+
telegramTokenFile: context.telegramTokenFile,
|
|
9195
|
+
env: process.env,
|
|
9196
|
+
repos,
|
|
9197
|
+
gh: (file, args, options) => run(file, [...args], options),
|
|
9198
|
+
checkConnection: createConnectionChecker(),
|
|
9199
|
+
clock: context.clock,
|
|
9200
|
+
...seams,
|
|
9201
|
+
};
|
|
9202
|
+
}
|
|
9203
|
+
/** The integrations from files and the last checks: no network, for `status` and `onboard`. */
|
|
9204
|
+
async function savedIntegrations(context) {
|
|
9205
|
+
return integrationsNow(await integrationIoFor(context));
|
|
9206
|
+
}
|
|
9207
|
+
/** `toolroll integrations [--json]`: check every integration now and say which work. `--saved` skips the checks. */
|
|
9208
|
+
async function integrationsCommand(positional, flags, context) {
|
|
9209
|
+
const command = "integrations";
|
|
9210
|
+
const allowed = new Set(["db", "json", "saved"]);
|
|
9211
|
+
for (const name of flags.keys()) {
|
|
9212
|
+
if (!allowed.has(name))
|
|
9213
|
+
return fail(context.write, context.json, command, "usage", `--${name} is not an integrations option.`, EXIT.usage);
|
|
9214
|
+
}
|
|
9215
|
+
if (positional.length > 0)
|
|
9216
|
+
return fail(context.write, context.json, command, "usage", "Use `toolroll integrations [--saved] [--json]`.", EXIT.usage);
|
|
9217
|
+
const io = await integrationIoFor(context);
|
|
9218
|
+
const list = flag(flags, "saved") ? integrationsNow(io) : await checkIntegrations(io);
|
|
9219
|
+
const counts = { connected: 0, "not-set-up": 0, broken: 0 };
|
|
9220
|
+
for (const one of list)
|
|
9221
|
+
counts[one.state] += 1;
|
|
9222
|
+
return succeed(context.write, context.json, command, { integrations: list, counts }, () => renderIntegrations(list));
|
|
9105
9223
|
}
|
|
9106
9224
|
async function waitTask(positional, flags, context) {
|
|
9107
9225
|
const command = "task wait";
|
|
@@ -11270,6 +11388,35 @@ async function budgetCommand(positional, flags, context) {
|
|
|
11270
11388
|
const saved = store.setBudget({ scope: target.scope, key: target.key, limitMicrousd: Math.round(dollars) * 1_000_000, hardStop: !flags.has("alerts-only") }, acting.name, context.clock());
|
|
11271
11389
|
return succeed(context.write, context.json, command, { budget: saved }, () => [`${budgetLabel(saved).replace(/'s$/, "")}: ${spendUsd(saved.limitMicrousd)} a month${saved.hardStop ? ", API work stops at 100%" : ", alerts only"}.`]);
|
|
11272
11390
|
}
|
|
11391
|
+
/** `notifications [quiet | all | digest <HH:MM> | digest off]`: how chats reach this person — only when they are
|
|
11392
|
+
* needed (the default), every step, and an optional evening digest. Each person sets their own. */
|
|
11393
|
+
async function notificationsCommand(positional, flags, context) {
|
|
11394
|
+
const [action, value, ...extra] = positional;
|
|
11395
|
+
const command = `notifications${action === undefined ? "" : ` ${action}`}`;
|
|
11396
|
+
const store = context.store;
|
|
11397
|
+
for (const name of flags.keys())
|
|
11398
|
+
if (!["as", "token", "token-file", "token-env", "db", "json"].includes(name))
|
|
11399
|
+
return fail(context.write, context.json, command, "usage", `--${name} is not a notifications option.`, EXIT.usage);
|
|
11400
|
+
const usage = "Use notifications quiet, notifications all, or notifications digest <HH:MM>|off.";
|
|
11401
|
+
if (extra.length > 0 || (action !== undefined && !["quiet", "all", "digest"].includes(action)) || (action === "digest") !== (value !== undefined)) {
|
|
11402
|
+
return fail(context.write, context.json, command, "usage", usage, EXIT.usage);
|
|
11403
|
+
}
|
|
11404
|
+
if (action === "digest" && value !== "off" && !isDigestTime(value))
|
|
11405
|
+
return fail(context.write, context.json, command, "usage", "The digest time is HH:MM on a 24-hour clock, like 18:30, or off.", EXIT.usage);
|
|
11406
|
+
const acting = await askCredentials(flags, context);
|
|
11407
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
11408
|
+
if (acting === null || verified === null || !verified.ok) {
|
|
11409
|
+
return fail(context.write, context.json, command, "refused", "Notifications are set per person: pass --as and --token (or use the remembered login).", EXIT.refused);
|
|
11410
|
+
}
|
|
11411
|
+
const now = context.clock();
|
|
11412
|
+
const preference = action === "quiet" || action === "all" ? store.setNotificationPreference(acting.name, { mode: action }, acting.name, now)
|
|
11413
|
+
: action === "digest" ? store.setNotificationPreference(acting.name, { digestAt: value === "off" ? null : value }, acting.name, now)
|
|
11414
|
+
: store.notificationPreference(acting.name);
|
|
11415
|
+
return succeed(context.write, context.json, command, { mode: preference.mode, digestAt: preference.digestAt }, () => [
|
|
11416
|
+
preference.mode === "quiet" ? "Only when you're needed: one message per task, updated as it moves, and a new one when something needs you." : "Every step: a message for each update.",
|
|
11417
|
+
preference.digestAt === null ? "No evening digest." : `Evening digest at ${preference.digestAt}: what finished, what waits and what failed.`,
|
|
11418
|
+
]);
|
|
11419
|
+
}
|
|
11273
11420
|
/** `retention show | preview | set <kind> <period>` (v105): how long each kind of data is kept, and what the daily
|
|
11274
11421
|
* sweep would remove now. Setting takes an instance operator. */
|
|
11275
11422
|
async function retentionCommand(positional, flags, context) {
|
package/dist/project-delete.js
CHANGED
|
@@ -231,6 +231,7 @@ function deleteRows(store, repo, d, now) {
|
|
|
231
231
|
del("telegram_conversation_part", `${IN("conversation")} OR ${IN("source_run")} OR ${IN("task_id")}`, conversations, R, K);
|
|
232
232
|
del("telegram_conversation", IN("id"), conversations);
|
|
233
233
|
del("telegram_outbound_message", `project = ? OR ${IN("notification")} OR ${IN("task_ref")} OR ${IN("source_run")}`, repo, notifications, T, R);
|
|
234
|
+
del("chat_card_task", IN("task_ref"), T);
|
|
234
235
|
del("push_delivery", IN("notification"), notifications);
|
|
235
236
|
del("notification_delivery", IN("notification"), notifications);
|
|
236
237
|
del("notification", IN("id"), notifications);
|
package/dist/publish.d.ts
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* @-mention in its conclusion cannot acquire GitHub semantics.
|
|
21
21
|
*/
|
|
22
22
|
import { type ExecResult, type RunOptions } from "./exec.js";
|
|
23
|
+
import { summarizeChecks } from "./pulls.js";
|
|
23
24
|
import type { Publication, PublicationGrant, Store } from "./store.js";
|
|
24
25
|
export declare const BODY_TEMPLATE_VERSION = 2;
|
|
25
26
|
/** After this many failed attempts a publication stops retrying and pages instead. */
|
|
@@ -69,6 +70,16 @@ export type CheckReport = {
|
|
|
69
70
|
failing: number;
|
|
70
71
|
resolved: number;
|
|
71
72
|
problems: string[];
|
|
73
|
+
/** Every observation that won its settle, for the pull-request follower (CI revisions, Ready to merge). */
|
|
74
|
+
seen: ObservedPullRequest[];
|
|
75
|
+
};
|
|
76
|
+
export type ObservedPullRequest = {
|
|
77
|
+
publication: Publication;
|
|
78
|
+
headOid: string;
|
|
79
|
+
state: ReturnType<typeof summarizeChecks>;
|
|
80
|
+
remoteState: "MERGED" | "CLOSED" | null;
|
|
81
|
+
rollup: unknown;
|
|
82
|
+
mergeCommit: string | null;
|
|
72
83
|
};
|
|
73
84
|
/**
|
|
74
85
|
* Read CI for every PR this control plane opened — structured rollup and the
|
package/dist/publish.js
CHANGED
|
@@ -254,7 +254,7 @@ async function openOrAdopt(store, publication, grant, exec, clock) {
|
|
|
254
254
|
export async function observeChecks(store, options) {
|
|
255
255
|
const clock = options.clock ?? (() => new Date());
|
|
256
256
|
const exec = options.exec ?? execRun;
|
|
257
|
-
const report = { observed: 0, failing: 0, resolved: 0, problems: [] };
|
|
257
|
+
const report = { observed: 0, failing: 0, resolved: 0, problems: [], seen: [] };
|
|
258
258
|
for (const publication of store.openedPublications()) {
|
|
259
259
|
if (publication.prNumber === null)
|
|
260
260
|
continue;
|
|
@@ -266,7 +266,7 @@ export async function observeChecks(store, options) {
|
|
|
266
266
|
const viewed = await exec("gh", [
|
|
267
267
|
"pr", "view", String(publication.prNumber),
|
|
268
268
|
"--repo", publication.githubRepo,
|
|
269
|
-
"--json", "statusCheckRollup,headRefOid,state",
|
|
269
|
+
"--json", "statusCheckRollup,headRefOid,state,mergeCommit",
|
|
270
270
|
], { timeoutMs: EXEC_TIMEOUT_MS });
|
|
271
271
|
if (viewed.code !== 0) {
|
|
272
272
|
report.problems.push(`PR #${publication.prNumber}: not read — ${viewed.notFound ? "gh is not on PATH" : firstLine(viewed.stderr) || `exit ${viewed.code}`}`);
|
|
@@ -290,11 +290,15 @@ export async function observeChecks(store, options) {
|
|
|
290
290
|
// remote state is recorded, its episodes resolve, and neither the
|
|
291
291
|
// review queue nor the next observation pass carries it forever.
|
|
292
292
|
const remoteState = typeof payload.state === "string" ? payload.state.toUpperCase() : null;
|
|
293
|
+
const mergeCommit = typeof payload.mergeCommit === "object" && payload.mergeCommit !== null && typeof payload.mergeCommit.oid === "string"
|
|
294
|
+
? payload.mergeCommit.oid : null;
|
|
293
295
|
if (remoteState === "MERGED" || remoteState === "CLOSED") {
|
|
296
|
+
let won = false;
|
|
294
297
|
store.transact(() => {
|
|
295
298
|
const settled = store.settleObservation({ githubRepo: publication.githubRepo, prNumber: publication.prNumber, headSha: headOid, state: "none", generation }, clock());
|
|
296
299
|
if (!settled.won)
|
|
297
300
|
return;
|
|
301
|
+
won = true;
|
|
298
302
|
store.recordPublicationRemoteState(publication.id, remoteState, clock());
|
|
299
303
|
report.resolved += store.resolveCiEpisodes(publication.githubRepo, publication.prNumber, null, clock());
|
|
300
304
|
// A closed PR moots its merge blocker and supersedes its intent —
|
|
@@ -311,6 +315,8 @@ export async function observeChecks(store, options) {
|
|
|
311
315
|
});
|
|
312
316
|
}
|
|
313
317
|
});
|
|
318
|
+
if (won)
|
|
319
|
+
report.seen.push({ publication, headOid, state: "none", remoteState, rollup: payload.statusCheckRollup, mergeCommit });
|
|
314
320
|
continue;
|
|
315
321
|
}
|
|
316
322
|
const state = summarizeChecks(payload.statusCheckRollup);
|
|
@@ -322,6 +328,7 @@ export async function observeChecks(store, options) {
|
|
|
322
328
|
continue;
|
|
323
329
|
}
|
|
324
330
|
store.recordPublicationCheckState(publication.id, state, clock());
|
|
331
|
+
report.seen.push({ publication, headOid, state, remoteState: null, rollup: payload.statusCheckRollup, mergeCommit });
|
|
325
332
|
// The episode identity carries the REPOSITORY (audit C-2): PR #55 in
|
|
326
333
|
// repo A and PR #55 in repo B are different worlds, and one's failure
|
|
327
334
|
// must never light the other's repair button.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Complete → pull request → merge, without a person running git.
|
|
3
|
+
*
|
|
4
|
+
* Setup is one step per project: the project's GitHub remote and default branch are read, `gh` must be signed
|
|
5
|
+
* in with push rights, and a publication grant is saved that publishes only when a person chooses "Complete and
|
|
6
|
+
* open a pull request" (never on every build). The publisher (publish.ts) then pushes the exact accepted commit
|
|
7
|
+
* and opens or adopts the PR, as it always has.
|
|
8
|
+
*
|
|
9
|
+
* The follower reads what observeChecks saw: a red head files ONE revision carrying the failing check's name and
|
|
10
|
+
* a short log excerpt, fenced as untrusted CI output — at most two across one task's revisions, then a person is
|
|
11
|
+
* asked instead. A green head on the exact accepted commit is announced once as Ready to merge.
|
|
12
|
+
*
|
|
13
|
+
* Merging is a person's act behind their password (or the project's opt-in "Merge when checks pass"): a fresh
|
|
14
|
+
* read must show the PR open, on the accepted commit, with checks passing; GitHub's head-commit match guards
|
|
15
|
+
* the race; the branch is deleted; the merge commit and who merged are recorded in the ledger.
|
|
16
|
+
*/
|
|
17
|
+
import { type ObservedPullRequest, type PublishExec } from "./publish.js";
|
|
18
|
+
import type { VerifiedApprover } from "./principal.js";
|
|
19
|
+
import type { Publication, Store } from "./store.js";
|
|
20
|
+
export type MergeMethod = "squash" | "merge" | "rebase";
|
|
21
|
+
export declare const MERGE_METHODS: readonly MergeMethod[];
|
|
22
|
+
/** CI revisions one task may file across its revisions before a person is asked instead. */
|
|
23
|
+
export declare const MAX_CI_REVISIONS = 2;
|
|
24
|
+
export declare const MERGED_ACTION = "pull request merged";
|
|
25
|
+
export declare const REQUESTED_ACTION = "pull request requested";
|
|
26
|
+
type Result<T> = ({
|
|
27
|
+
ok: true;
|
|
28
|
+
} & T) | {
|
|
29
|
+
ok: false;
|
|
30
|
+
reason: string;
|
|
31
|
+
message: string;
|
|
32
|
+
};
|
|
33
|
+
export type PublishingPlan = {
|
|
34
|
+
repo: string;
|
|
35
|
+
githubRepo: string;
|
|
36
|
+
remote: string;
|
|
37
|
+
base: string;
|
|
38
|
+
account: string | null;
|
|
39
|
+
};
|
|
40
|
+
/** owner/name from a GitHub remote URL (https, ssh or scp form), or null when it is not GitHub. */
|
|
41
|
+
export declare function githubRepoOf(remoteUrl: string): string | null;
|
|
42
|
+
/** Everything setup needs, checked in the order a person would fix it — each refusal says what to do next. */
|
|
43
|
+
export declare function checkPublishing(repo: string, options?: {
|
|
44
|
+
exec?: PublishExec;
|
|
45
|
+
remote?: string;
|
|
46
|
+
}): Promise<Result<{
|
|
47
|
+
plan: PublishingPlan;
|
|
48
|
+
}>>;
|
|
49
|
+
export type PublishingSettings = {
|
|
50
|
+
mergeMethod: MergeMethod;
|
|
51
|
+
mergeWhenGreen: boolean;
|
|
52
|
+
};
|
|
53
|
+
export type Publishing = {
|
|
54
|
+
on: false;
|
|
55
|
+
} | ({
|
|
56
|
+
on: true;
|
|
57
|
+
githubRepo: string;
|
|
58
|
+
base: string;
|
|
59
|
+
grantedBy: string;
|
|
60
|
+
legacy: boolean;
|
|
61
|
+
} & PublishingSettings);
|
|
62
|
+
export declare function publishingOf(store: Store, repo: string | null): Publishing;
|
|
63
|
+
/** The grant setup creates: this project's GitHub repo, its default branch, Toolroll's own branches, pull requests
|
|
64
|
+
* only when a person asks at Complete, ready for review, squash by default, branch deleted on merge. */
|
|
65
|
+
export declare function savePublishing(store: Store, plan: PublishingPlan, by: string, settings: Partial<PublishingSettings>, now: Date): void;
|
|
66
|
+
/** Change how merges happen, keeping every other term of the live grant. */
|
|
67
|
+
export declare function saveMergeSettings(store: Store, repo: string, settings: PublishingSettings, by: string, now: Date): Result<object>;
|
|
68
|
+
/** Whether "Complete and open a pull request" may be offered for this result, and if not, why (null = offer it). */
|
|
69
|
+
export declare function pullRequestBlocker(store: Store, runId: number): string | null;
|
|
70
|
+
/**
|
|
71
|
+
* Mark the exact displayed result complete and owe a pull request for its exact commit — one transaction, so
|
|
72
|
+
* "complete" and "this must reach a PR" can't come apart. A result already marked complete may still open its
|
|
73
|
+
* pull request. The watch process pushes and opens it; the task shows its progress.
|
|
74
|
+
*/
|
|
75
|
+
export declare function completeAndOpenPullRequest(store: Store, input: {
|
|
76
|
+
taskId: string;
|
|
77
|
+
digest: string;
|
|
78
|
+
runId: number;
|
|
79
|
+
who: VerifiedApprover;
|
|
80
|
+
root: string;
|
|
81
|
+
}, now: Date): Result<{
|
|
82
|
+
publication: Publication;
|
|
83
|
+
}>;
|
|
84
|
+
export type FollowReport = {
|
|
85
|
+
revisions: number;
|
|
86
|
+
asked: number;
|
|
87
|
+
ready: number;
|
|
88
|
+
merged: number;
|
|
89
|
+
problems: string[];
|
|
90
|
+
};
|
|
91
|
+
/** The revision note: the failing check by name, then its log as fenced, labelled untrusted data. At most 500 characters. */
|
|
92
|
+
export declare function ciRevisionNote(check: string, prNumber: number, head: string, excerpt: string | null): string;
|
|
93
|
+
/**
|
|
94
|
+
* What observeChecks saw, acted on for pull requests opened through Complete: one revision per red head (at most
|
|
95
|
+
* two per task, then a person is asked), Ready to merge once per green head on the accepted commit, and the merge
|
|
96
|
+
* itself when the project merges on green. A remote merge done elsewhere is recorded with its commit.
|
|
97
|
+
*/
|
|
98
|
+
export declare function followPullRequests(store: Store, seen: readonly ObservedPullRequest[], options: {
|
|
99
|
+
evidenceRoot: string;
|
|
100
|
+
exec?: PublishExec;
|
|
101
|
+
clock?: () => Date;
|
|
102
|
+
}): Promise<FollowReport>;
|
|
103
|
+
/** A person's merge: their password first, then the same checked merge. */
|
|
104
|
+
export declare function mergeAsPerson(store: Store, input: {
|
|
105
|
+
runId: number;
|
|
106
|
+
name: string;
|
|
107
|
+
password: string;
|
|
108
|
+
exec?: PublishExec;
|
|
109
|
+
clock?: () => Date;
|
|
110
|
+
}): Promise<Result<{
|
|
111
|
+
commit: string | null;
|
|
112
|
+
}>>;
|
|
113
|
+
/**
|
|
114
|
+
* Merge a followed pull request: callers have already established who is merging. A fresh read must show it open,
|
|
115
|
+
* not a draft, on the accepted commit, with checks passing; the merge names that commit so GitHub refuses a head
|
|
116
|
+
* that moved, uses the project's method, and deletes the branch.
|
|
117
|
+
*/
|
|
118
|
+
export declare function mergePullRequest(store: Store, input: {
|
|
119
|
+
runId: number;
|
|
120
|
+
by: string;
|
|
121
|
+
exec?: PublishExec;
|
|
122
|
+
clock?: () => Date;
|
|
123
|
+
}): Promise<Result<{
|
|
124
|
+
commit: string | null;
|
|
125
|
+
}>>;
|
|
126
|
+
export type PullRequestView = {
|
|
127
|
+
runId: number;
|
|
128
|
+
prNumber: number | null;
|
|
129
|
+
prUrl: string | null;
|
|
130
|
+
state: "opening" | "running" | "waiting" | "ready" | "failing" | "merged" | "closed" | "failed";
|
|
131
|
+
label: string;
|
|
132
|
+
detail: string;
|
|
133
|
+
mergeCommit: string | null;
|
|
134
|
+
mergeMethod: MergeMethod;
|
|
135
|
+
canMerge: boolean;
|
|
136
|
+
revisionTask: string | null;
|
|
137
|
+
};
|
|
138
|
+
/** The pull request a result opened through Complete, in plain words; null when it opened none. */
|
|
139
|
+
export declare function pullRequestViewOf(store: Store, runId: number): PullRequestView | null;
|
|
140
|
+
export {};
|