toolroll 0.9.10 → 0.9.12
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 +25 -0
- package/dist/assignment.d.ts +3 -0
- package/dist/assignment.js +3 -1
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +20 -18
- package/dist/browser-workspace.d.ts +61 -0
- package/dist/chat-channel.d.ts +9 -7
- package/dist/chat-channel.js +23 -17
- package/dist/chat-delivery.js +21 -0
- package/dist/chat-quiet.js +1 -0
- package/dist/chat-voice.d.ts +3 -1
- package/dist/chat-voice.js +13 -5
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +1 -0
- package/dist/coding-update.d.ts +13 -0
- package/dist/coding-update.js +68 -0
- package/dist/desktop-update.d.ts +7 -0
- package/dist/desktop-update.js +42 -1
- package/dist/flow-triggers.js +17 -1
- package/dist/guides.js +9 -0
- package/dist/lead-status.d.ts +10 -1
- package/dist/lead-status.js +13 -3
- package/dist/lead-voice.d.ts +81 -0
- package/dist/lead-voice.js +275 -0
- package/dist/needs-you.d.ts +7 -0
- package/dist/needs-you.js +9 -0
- package/dist/operate.d.ts +2 -0
- package/dist/operate.js +186 -38
- package/dist/serve.d.ts +5 -1
- package/dist/serve.js +447 -226
- package/dist/setup-guide.d.ts +2 -2
- package/dist/store.d.ts +15 -1
- package/dist/store.js +38 -1
- package/dist/surface.js +8 -1
- package/dist/task-status.d.ts +6 -0
- package/dist/task-status.js +19 -2
- package/dist/team-cli.js +1 -1
- package/dist/telegram-mate.d.ts +7 -0
- package/dist/telegram-mate.js +50 -21
- package/dist/telegram-progress.js +7 -0
- package/dist/telegram-settings.d.ts +3 -0
- package/dist/telegram-settings.js +11 -0
- package/dist/telegram.d.ts +14 -0
- package/dist/telegram.js +105 -12
- package/dist/test-temp.js +1 -1
- package/dist/toolroll-update.d.ts +3 -0
- package/dist/toolroll-update.js +13 -2
- package/dist/work-index.d.ts +9 -0
- package/dist/work-index.js +57 -20
- package/dist/work-summary.d.ts +4 -1
- package/dist/workspace-ui.d.ts +25 -0
- package/dist/workspace-ui.js +20 -1
- package/package.json +1 -1
package/dist/operate.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { UNSENT_REPLY_MS } from "./telegram-settings.js";
|
|
1
2
|
import { maybeTriggerRepair } from "./dispose.js";
|
|
2
3
|
import { CHECK_LEVEL_HINTS, CHECK_LEVEL_WORDS, isCheckLevel, liveQuickCommand, projectCheckLevel, quickVerifyKey, setProjectCheckLevel, setTaskCheckLevel, suggestQuickCommand } from "./check-levels.js";
|
|
3
4
|
import { fileAddTestsTask, followUpChecksOf, requestFollowUpChecks, runFollowUpCheck, runWaitingChecks } from "./result-follow-ups.js";
|
|
@@ -88,6 +89,7 @@ import { pushPass } from "./push.js";
|
|
|
88
89
|
import { chmodSync, closeSync, constants as fsConstants, existsSync, fstatSync, fsyncSync, openSync, readFileSync, readSync, realpathSync, renameSync, unlinkSync, writeSync, writeFileSync, mkdirSync } from "node:fs";
|
|
89
90
|
import { BRANCH_PREFIX, envTwins, envValue, existingOrFirst, namedPath, taskBranches } from "./names.js";
|
|
90
91
|
import { claimActor, currentActor, parseLeadToken, withActor } from "./actor.js";
|
|
92
|
+
import { leadClaim, leadSay, noteLeadWork } from "./lead-voice.js";
|
|
91
93
|
import { createServer as createNetServer } from "node:net";
|
|
92
94
|
import { spawn as spawnChild } from "node:child_process";
|
|
93
95
|
import { envelopeJson } from "./envelope.js";
|
|
@@ -138,7 +140,7 @@ import { diagnoseTaskDispatch, diagnosisIsDispatchable, scopeApprovedForDispatch
|
|
|
138
140
|
import { createDecisionServer } from "./serve.js";
|
|
139
141
|
import { readTailnetNames } from "./tailnet.js";
|
|
140
142
|
import { daemonLaunchCommand, daemonStatus, installReplacingLegacy, installedLegacyDaemon, planDaemon, uninstallDaemon, awaitFreshHeartbeat, } from "./daemon.js";
|
|
141
|
-
import { bridgePass, clearBotToken, createTransport, followBridge, hashPairingCode, loadBotToken, mintPairingCode, redactToken, saveBotToken, PAIRING_TTL_MS, TOKEN_ENV, telegramHookSecret, telegramPushUrl, } from "./telegram.js";
|
|
143
|
+
import { bridgePass, clearBotToken, createTransport, followBridge, hashPairingCode, loadBotToken, mintPairingCode, networkFence, networkFenceLine, redactToken, saveBotToken, PAIRING_TTL_MS, TOKEN_ENV, telegramHookSecret, telegramPushUrl, } from "./telegram.js";
|
|
142
144
|
import { scanRepo } from "./capscan.js";
|
|
143
145
|
import { computeGaps, describeCapability } from "./gaps.js";
|
|
144
146
|
import { ask, askHidden, confirm, interactive, underAgent } from "./prompt.js";
|
|
@@ -637,6 +639,8 @@ export const OPERATE_VALUE_FLAGS = new Set([
|
|
|
637
639
|
"replaced-by", "replaces",
|
|
638
640
|
// Check levels: a project's level, the quick command, a task's choice.
|
|
639
641
|
"quick", "level", "checks",
|
|
642
|
+
// lead say: the task the lead's words are about.
|
|
643
|
+
"task",
|
|
640
644
|
]);
|
|
641
645
|
export const OPERATE_BOOLEAN_FLAGS = new Set([
|
|
642
646
|
"json", "yes", "all", "local", "history", "latest-watch", "dry-run", "file", "allow-paid-fallback",
|
|
@@ -820,6 +824,7 @@ export async function runOperate(command, argv, write, options = {}) {
|
|
|
820
824
|
...(options.agentRunner === undefined ? {} : { agentRunner: options.agentRunner }),
|
|
821
825
|
...(options.gitRunner === undefined ? {} : { gitRunner: options.gitRunner }),
|
|
822
826
|
...(options.telegramTransport === undefined ? {} : { telegramTransport: options.telegramTransport }),
|
|
827
|
+
...(options.telegramEnv === undefined ? {} : { telegramEnv: options.telegramEnv }),
|
|
823
828
|
...(options.publishExec === undefined ? {} : { publishExec: options.publishExec }),
|
|
824
829
|
...(options.dispatchAdapter === undefined ? {} : { dispatchAdapter: options.dispatchAdapter }),
|
|
825
830
|
...(options.flowTriggerIo === undefined ? {} : { flowTriggerIo: options.flowTriggerIo }),
|
|
@@ -966,6 +971,9 @@ async function dispatch(command, positional, flags, context) {
|
|
|
966
971
|
return projectConcurrencyCommand(positional, flags, context);
|
|
967
972
|
return runProjectCommand(positional, flags, context);
|
|
968
973
|
case "assignment":
|
|
974
|
+
// The person's own lead (its lead token) claims as the lead; coordinator credentials keep their own path.
|
|
975
|
+
if (positional[0] === "claim" && currentActor()?.lead === true && !flags.has("token-env") && !flags.has("token-file"))
|
|
976
|
+
return leadClaimCommand(positional, flags, context);
|
|
969
977
|
return runAssignmentCommand(positional, flags, context);
|
|
970
978
|
case "knowledge":
|
|
971
979
|
return runKnowledgeCommand(positional, flags, { ...context, now: context.clock() });
|
|
@@ -7288,6 +7296,52 @@ async function daemonCommand(positional, flags, context) {
|
|
|
7288
7296
|
const WATCH_LEASE_MS = 90_000;
|
|
7289
7297
|
const WATCH_HEARTBEAT_MS = 30_000;
|
|
7290
7298
|
/** The start repair runs once per process, not once per project loop. */
|
|
7299
|
+
/**
|
|
7300
|
+
* The Telegram follower: one actor on the wire that sends notifications and
|
|
7301
|
+
* replies and reads what arrives, all over the same transport. Null when no
|
|
7302
|
+
* bot token is saved, or when this process's network is fenced (it says so
|
|
7303
|
+
* once instead of failing every send).
|
|
7304
|
+
*/
|
|
7305
|
+
function startTelegramFollower(args) {
|
|
7306
|
+
const { context, progress, label } = args;
|
|
7307
|
+
const source = loadBotToken(process.env, context.telegramTokenFile);
|
|
7308
|
+
if (source === null)
|
|
7309
|
+
return null;
|
|
7310
|
+
const fence = networkFence(context.telegramEnv ?? (context.telegramTransport === undefined ? process.env : {}));
|
|
7311
|
+
if (fence !== null) {
|
|
7312
|
+
progress(networkFenceLine(fence));
|
|
7313
|
+
return null;
|
|
7314
|
+
}
|
|
7315
|
+
const transport = context.telegramTransport ?? createTransport(source.token);
|
|
7316
|
+
// v98: with a public hooks address (Tailscale Funnel covers /hooks), Telegram pushes updates to it instead.
|
|
7317
|
+
const pushUrl = telegramPushUrl(readHooksBase(dirname(context.databaseFile)));
|
|
7318
|
+
return followBridge(context.store, {
|
|
7319
|
+
push: pushUrl === null ? null : { url: pushUrl, secret: telegramHookSecret(dirname(context.databaseFile), true) },
|
|
7320
|
+
readProjects: telegramReadProjects(context),
|
|
7321
|
+
canDeliver: telegramCanDeliver(context, source.token),
|
|
7322
|
+
conversation: telegramConversation(context, args.publicUrl === undefined ? {} : { serverOrigin: args.publicUrl }),
|
|
7323
|
+
botId: source.botId,
|
|
7324
|
+
transport,
|
|
7325
|
+
signal: args.signal,
|
|
7326
|
+
...(args.owner === undefined ? {} : { owner: args.owner }),
|
|
7327
|
+
onCycle: cycle => {
|
|
7328
|
+
// progress(), not write(): under --json this line went to stdout
|
|
7329
|
+
// BESIDE the final envelope — the one stdout contamination in the
|
|
7330
|
+
// watch (round-4 finding 12).
|
|
7331
|
+
progress(`${label} sent ${cycle.sent}, answered ${cycle.answered}, paired ${cycle.paired}` +
|
|
7332
|
+
((cycle.statusReplies ?? 0) > 0 ? `, status replies ${cycle.statusReplies}` : "") +
|
|
7333
|
+
((cycle.chatQueued ?? 0) > 0 ? `, chat received ${cycle.chatQueued}` : "") +
|
|
7334
|
+
((cycle.chatAnswered ?? 0) > 0 ? `, chat replied ${cycle.chatAnswered}` : "") +
|
|
7335
|
+
((cycle.chatConfirmed ?? 0) > 0 ? `, chat confirmed ${cycle.chatConfirmed}` : "") +
|
|
7336
|
+
(cycle.problems.length > 0 ? ` — ${cycle.problems.length} problem(s): ${cycle.problems[0].slice(0, 200)}` : ""));
|
|
7337
|
+
},
|
|
7338
|
+
}).catch(error => {
|
|
7339
|
+
progress(`${args.died.prefix}the telegram follower died — ${describe(error)}; ${args.died.then}`);
|
|
7340
|
+
return null;
|
|
7341
|
+
});
|
|
7342
|
+
}
|
|
7343
|
+
/** How long a stopping service waits for the aborted Telegram follower. */
|
|
7344
|
+
const TELEGRAM_STOP_WAIT_MS = 5_000;
|
|
7291
7345
|
let staleStatusesRepaired = false;
|
|
7292
7346
|
async function runWatchLoop(args) {
|
|
7293
7347
|
const { context, flags, runner, token, repo, progress } = args;
|
|
@@ -7366,38 +7420,13 @@ async function runWatchLoop(args) {
|
|
|
7366
7420
|
// moment they arrive, and answering bumps the wake sequence, so the very
|
|
7367
7421
|
// loop below wakes and resumes the freed task — phone to build, seconds.
|
|
7368
7422
|
// The poll lease keeps this the only live poller; a cron `bridge
|
|
7369
|
-
// telegram` overlapping it loses the lease race and reports busy.
|
|
7370
|
-
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
|
|
7374
|
-
|
|
7375
|
-
|
|
7376
|
-
const pushUrl = telegramPushUrl(readHooksBase(dirname(context.databaseFile)));
|
|
7377
|
-
follower = followBridge(store, {
|
|
7378
|
-
push: pushUrl === null ? null : { url: pushUrl, secret: telegramHookSecret(dirname(context.databaseFile), true) },
|
|
7379
|
-
readProjects: telegramReadProjects(context),
|
|
7380
|
-
canDeliver: telegramCanDeliver(context, followSource.token),
|
|
7381
|
-
conversation: telegramConversation(context, publicUrl === undefined ? {} : { serverOrigin: publicUrl }),
|
|
7382
|
-
botId: followSource.botId,
|
|
7383
|
-
transport,
|
|
7384
|
-
signal: followController.signal,
|
|
7385
|
-
onCycle: cycle => {
|
|
7386
|
-
// progress(), not write(): under --json this line went to stdout
|
|
7387
|
-
// BESIDE the final envelope — the one stdout contamination in the
|
|
7388
|
-
// watch (round-4 finding 12).
|
|
7389
|
-
progress(`watch: bridge sent ${cycle.sent}, answered ${cycle.answered}, paired ${cycle.paired}` +
|
|
7390
|
-
((cycle.statusReplies ?? 0) > 0 ? `, status replies ${cycle.statusReplies}` : "") +
|
|
7391
|
-
((cycle.chatQueued ?? 0) > 0 ? `, chat received ${cycle.chatQueued}` : "") +
|
|
7392
|
-
((cycle.chatAnswered ?? 0) > 0 ? `, chat replied ${cycle.chatAnswered}` : "") +
|
|
7393
|
-
((cycle.chatConfirmed ?? 0) > 0 ? `, chat confirmed ${cycle.chatConfirmed}` : "") +
|
|
7394
|
-
(cycle.problems.length > 0 ? ` — ${cycle.problems.length} problem(s): ${cycle.problems[0].slice(0, 200)}` : ""));
|
|
7395
|
-
},
|
|
7396
|
-
}).catch(error => {
|
|
7397
|
-
progress(`watch: the telegram follower died — ${describe(error)}; taps wait for the next watch`);
|
|
7398
|
-
return null;
|
|
7399
|
-
});
|
|
7400
|
-
}
|
|
7423
|
+
// telegram` overlapping it loses the lease race and reports busy. Under
|
|
7424
|
+
// `up` the service runs the one follower itself and project loops leave
|
|
7425
|
+
// Telegram to it.
|
|
7426
|
+
const serviceBridge = args.serviceBridge === true;
|
|
7427
|
+
const follower = serviceBridge
|
|
7428
|
+
? null
|
|
7429
|
+
: startTelegramFollower({ context, publicUrl: text(flags, "public-url"), signal: followController.signal, progress, label: "watch: bridge", died: { prefix: "watch: ", then: "taps wait for the next watch" } });
|
|
7401
7430
|
const slackFollower = followSlack({ store, dir: dirname(context.databaseFile), signal: followController.signal,
|
|
7402
7431
|
readProjects: telegramReadProjects(context), evidenceRoot: context.evidenceRoot,
|
|
7403
7432
|
...(context.mateSeams?.subscriptionRunner ? { subscriptionRunner: context.mateSeams.subscriptionRunner } : {}),
|
|
@@ -7690,7 +7719,7 @@ async function runWatchLoop(args) {
|
|
|
7690
7719
|
// pass is the fallback shape for a watch started before a token existed.
|
|
7691
7720
|
if (follower === null && now - lastBridge >= bridgeEveryMs) {
|
|
7692
7721
|
lastBridge = now;
|
|
7693
|
-
const source = loadBotToken(process.env, context.telegramTokenFile);
|
|
7722
|
+
const source = serviceBridge ? null : loadBotToken(process.env, context.telegramTokenFile);
|
|
7694
7723
|
const dir = dirname(context.databaseFile);
|
|
7695
7724
|
// Exactly ONE service carries the pages — the chosen primary, or
|
|
7696
7725
|
// the sensible implicit one. Telegram keeps draining taps and
|
|
@@ -8467,6 +8496,10 @@ async function upCommand(flags, context) {
|
|
|
8467
8496
|
resolveStopped();
|
|
8468
8497
|
for (const controller of followControllers)
|
|
8469
8498
|
controller.abort();
|
|
8499
|
+
// The coding owner record is released first, whatever else is still
|
|
8500
|
+
// finishing: launchd kills a service that outlasts its exit window, and a
|
|
8501
|
+
// killed process cannot release it. Its outcome is read again at close.
|
|
8502
|
+
console_.server.closeCoding().catch(() => { });
|
|
8470
8503
|
const building = store.liveClaimCount(runnerName, clock());
|
|
8471
8504
|
if (building > 0) {
|
|
8472
8505
|
progress(`Stopping: ${building} build(s) running. Waiting up to ${Math.round(stopGraceMs / 1000)}s for them to finish; any still running then is saved and resumes when the builder starts again.`);
|
|
@@ -8538,6 +8571,7 @@ async function upCommand(flags, context) {
|
|
|
8538
8571
|
onFollowController: controller => followControllers.push(controller),
|
|
8539
8572
|
containmentLines,
|
|
8540
8573
|
passes: projectPasses,
|
|
8574
|
+
serviceBridge: true,
|
|
8541
8575
|
onReady: () => {
|
|
8542
8576
|
markReady();
|
|
8543
8577
|
if (bind)
|
|
@@ -8557,6 +8591,37 @@ async function upCommand(flags, context) {
|
|
|
8557
8591
|
await ready;
|
|
8558
8592
|
};
|
|
8559
8593
|
await Promise.all(repos.map(repo => launchRepo(repo, false)));
|
|
8594
|
+
// Telegram: one follower for the whole service, never one per project
|
|
8595
|
+
// loop. Replies and notifications go out from it, over the same transport
|
|
8596
|
+
// in the same cycle (Oct 2: a reply sent by a project loop's own follower
|
|
8597
|
+
// failed "fetch failed" while notifications went out). A token saved
|
|
8598
|
+
// while the service runs connects within seconds; a follower that dies
|
|
8599
|
+
// is started again.
|
|
8600
|
+
const telegramController = new AbortController();
|
|
8601
|
+
followControllers.push(telegramController);
|
|
8602
|
+
const telegramSupervisor = (async () => {
|
|
8603
|
+
const pause = (ms) => new Promise(resolvePause => {
|
|
8604
|
+
const timer = setTimeout(done, ms);
|
|
8605
|
+
function done() { clearTimeout(timer); telegramController.signal.removeEventListener("abort", done); resolvePause(); }
|
|
8606
|
+
// An aborted signal never fires again: without this a stop waited the whole pause.
|
|
8607
|
+
if (telegramController.signal.aborted)
|
|
8608
|
+
done();
|
|
8609
|
+
else
|
|
8610
|
+
telegramController.signal.addEventListener("abort", done, { once: true });
|
|
8611
|
+
});
|
|
8612
|
+
while (!stopping && !telegramController.signal.aborted) {
|
|
8613
|
+
const follower = startTelegramFollower({
|
|
8614
|
+
context, publicUrl: text(flags, "public-url"), signal: telegramController.signal, progress, label: "telegram:",
|
|
8615
|
+
died: { prefix: "", then: "starting it again" }, owner: `service-${randomUUID()}`,
|
|
8616
|
+
});
|
|
8617
|
+
if (follower !== null)
|
|
8618
|
+
await follower;
|
|
8619
|
+
// Without a token (or on a fenced network) look again shortly; the fence line is said once.
|
|
8620
|
+
if (follower === null && loadBotToken(process.env, context.telegramTokenFile) !== null)
|
|
8621
|
+
break;
|
|
8622
|
+
await pause(follower === null ? 2_000 : 5_000);
|
|
8623
|
+
}
|
|
8624
|
+
})();
|
|
8560
8625
|
const dynamicCeiling = resolveCeiling(repos, projectRoots).ceiling;
|
|
8561
8626
|
const rejected = new Set();
|
|
8562
8627
|
const registrySupervisor = (async () => {
|
|
@@ -8691,6 +8756,14 @@ async function upCommand(flags, context) {
|
|
|
8691
8756
|
await stopped;
|
|
8692
8757
|
await registrySupervisor;
|
|
8693
8758
|
await Promise.all([...loopResults.values()]);
|
|
8759
|
+
// The follower was aborted with the stop. A send or a chat turn it is still
|
|
8760
|
+
// finishing keeps its own durable record; the service does not wait past
|
|
8761
|
+
// this for it.
|
|
8762
|
+
await new Promise(done => {
|
|
8763
|
+
const timer = setTimeout(done, TELEGRAM_STOP_WAIT_MS);
|
|
8764
|
+
const finish = () => { clearTimeout(timer); done(); };
|
|
8765
|
+
void telegramSupervisor.then(finish, finish);
|
|
8766
|
+
});
|
|
8694
8767
|
clearInterval(runnerHeartbeat);
|
|
8695
8768
|
if (runTimer !== undefined)
|
|
8696
8769
|
clearTimeout(runTimer);
|
|
@@ -8738,6 +8811,7 @@ async function upCommand(flags, context) {
|
|
|
8738
8811
|
* bridge telegram token [<bot-token>|--clear] set the credential file
|
|
8739
8812
|
* bridge telegram status
|
|
8740
8813
|
* bridge telegram digest [--every 30m|2h|24h | --off] away mode: routine facts batch, decisions still page
|
|
8814
|
+
* bridge telegram retry send unsent assistant replies again now
|
|
8741
8815
|
*
|
|
8742
8816
|
* The bot token comes from ${TOKEN_ENV} or the credential file this command
|
|
8743
8817
|
* writes (0600, beside the database). Cron the pass right after tick; a
|
|
@@ -8754,7 +8828,14 @@ async function bridgeCommand(positional, flags, context) {
|
|
|
8754
8828
|
return demoFence;
|
|
8755
8829
|
const [channel, action] = positional;
|
|
8756
8830
|
if (channel !== "telegram") {
|
|
8757
|
-
return fail(write, json, "bridge", "usage", "`toolroll bridge telegram [pair|unpair|token|status|digest]`", EXIT.usage);
|
|
8831
|
+
return fail(write, json, "bridge", "usage", "`toolroll bridge telegram [pair|unpair|token|status|digest|retry]`", EXIT.usage);
|
|
8832
|
+
}
|
|
8833
|
+
if (action === "retry") {
|
|
8834
|
+
// Replies already written and fenced on the pairing like every send: this only moves their next attempt to now.
|
|
8835
|
+
const retried = store.retryTelegramReplies(null, clock());
|
|
8836
|
+
return succeed(write, json, "bridge retry", { retried }, () => [
|
|
8837
|
+
retried === 0 ? "No unsent Telegram replies." : `Sending ${retried} Telegram ${retried === 1 ? "reply" : "replies"} again on the next bridge pass.`,
|
|
8838
|
+
]);
|
|
8758
8839
|
}
|
|
8759
8840
|
if (action === "token") {
|
|
8760
8841
|
const value = positional[2];
|
|
@@ -8871,6 +8952,9 @@ async function bridgeCommand(positional, flags, context) {
|
|
|
8871
8952
|
if (source === null) {
|
|
8872
8953
|
return fail(write, json, "bridge", "no-token", `no bot token — set ${TOKEN_ENV}, run \`toolroll bridge telegram token <t>\`, or use the serve settings card`, EXIT.refused);
|
|
8873
8954
|
}
|
|
8955
|
+
const fenced = networkFence(context.telegramEnv ?? (context.telegramTransport === undefined ? process.env : {}));
|
|
8956
|
+
if (fenced !== null)
|
|
8957
|
+
return fail(write, json, "bridge", "network-fenced", networkFenceLine(fenced), EXIT.refused);
|
|
8874
8958
|
const transport = context.telegramTransport ?? createTransport(source.token);
|
|
8875
8959
|
// --follow: stay on the wire. One long-poll actor holds the poll lease;
|
|
8876
8960
|
// an answer tapped on a phone lands in seconds instead of at the next
|
|
@@ -9569,7 +9653,9 @@ async function statusCommand(positional, flags, context) {
|
|
|
9569
9653
|
}
|
|
9570
9654
|
if (positional.length > 0)
|
|
9571
9655
|
return fail(context.write, context.json, command, "usage", "Use `toolroll status [--json]`.", EXIT.usage);
|
|
9572
|
-
|
|
9656
|
+
// Whose lead the status line follows: the lead's own person, else the remembered login; nobody shows no lead.
|
|
9657
|
+
const viewer = currentActor()?.account ?? readLoginFile(join(dirname(context.databaseFile), UP_LOGIN_FILE))?.name ?? null;
|
|
9658
|
+
const status = installationStatus(context.store, context.clock(), viewer);
|
|
9573
9659
|
// One line, only when a newer Toolroll exists; offline or switched off says nothing.
|
|
9574
9660
|
const current = PACKAGE_VERSION;
|
|
9575
9661
|
const method = installMethod(context.installBin);
|
|
@@ -9588,7 +9674,18 @@ async function statusCommand(positional, flags, context) {
|
|
|
9588
9674
|
const projects = projectBuildsStatus(context);
|
|
9589
9675
|
const projectsLine = projects.length === 0 ? null
|
|
9590
9676
|
: `Builds by project: ${projects.slice(0, 8).map(one => `${one.name} ${one.running} of ${one.limit}`).join(", ")}${projects.length > 8 ? ", …" : ""}`;
|
|
9591
|
-
|
|
9677
|
+
// A Telegram reply written but still not sent after two minutes, with its Retry.
|
|
9678
|
+
const unsent = context.store.unsentTelegramReplies(null, null, new Date(context.clock().getTime() - UNSENT_REPLY_MS));
|
|
9679
|
+
const unsentLine = unsentRepliesLine(unsent);
|
|
9680
|
+
const unsentReplies = unsent.length === 0 ? {} : { unsentReplies: unsent.map(one => ({ ...one, retry: "toolroll bridge telegram retry" })) };
|
|
9681
|
+
return succeed(context.write, context.json, command, { ...status, projects, ...update, ...integrations, ...updateWaiting, ...unsentReplies }, () => [...(waiting === null ? [] : [waiting.words]), ...(unsentLine === null ? [] : [unsentLine]), ...renderInstallationStatus(status), ...(projectsLine === null ? [] : [projectsLine]), ...(line === null ? [] : [line]), ...(brokenLine === null ? [] : [brokenLine])]);
|
|
9682
|
+
}
|
|
9683
|
+
function unsentRepliesLine(unsent) {
|
|
9684
|
+
if (unsent.length === 0)
|
|
9685
|
+
return null;
|
|
9686
|
+
const oldest = unsent[0];
|
|
9687
|
+
const what = unsent.length === 1 ? "A Telegram reply hasn't" : `${unsent.length} Telegram replies haven't`;
|
|
9688
|
+
return `${what} been sent since ${oldest.since.slice(11, 16)} UTC${oldest.error === null ? "" : ` (${oldest.error})`}. Retry: toolroll bridge telegram retry`;
|
|
9592
9689
|
}
|
|
9593
9690
|
/** Each known project's running builds against its limit: its own number, never past its workers' capacity. */
|
|
9594
9691
|
function projectBuildsStatus(context) {
|
|
@@ -9698,6 +9795,19 @@ function taskCommand(positional, flags, context) {
|
|
|
9698
9795
|
context.write(OPERATE_HELP);
|
|
9699
9796
|
return EXIT.ok;
|
|
9700
9797
|
}
|
|
9798
|
+
// The lead's work on a task it took on counts as acting on it (lead-voice.ts): its claim stays fresh.
|
|
9799
|
+
const actor = currentActor();
|
|
9800
|
+
if (actor?.lead === true && rest[0] !== undefined && LEAD_WORK_VERBS.has(action)) {
|
|
9801
|
+
const noted = (code) => { if (code === EXIT.ok)
|
|
9802
|
+
noteLeadWork(context.store, actor, rest[0], action, context.clock()); return code; };
|
|
9803
|
+
const done = taskAction(action, rest, flags, context);
|
|
9804
|
+
return typeof done === "number" ? noted(done) : done.then(noted);
|
|
9805
|
+
}
|
|
9806
|
+
return taskAction(action, rest, flags, context);
|
|
9807
|
+
}
|
|
9808
|
+
/** Task verbs that are the lead working on a task, not reading it. */
|
|
9809
|
+
const LEAD_WORK_VERBS = new Set(["state", "steer", "reopen", "requeue", "regate", "repair", "resume", "stop", "scope", "approve", "plan", "checks", "revise", "hold", "unhold", "route", "require"]);
|
|
9810
|
+
function taskAction(action, rest, flags, context) {
|
|
9701
9811
|
switch (action) {
|
|
9702
9812
|
case "complete":
|
|
9703
9813
|
case "revise":
|
|
@@ -10526,9 +10636,11 @@ function askTask(positional, flags, context) {
|
|
|
10526
10636
|
async function leadCommand(positional, flags, context) {
|
|
10527
10637
|
const { store, write, json } = context;
|
|
10528
10638
|
const [action, ...extra] = positional;
|
|
10639
|
+
if (action === "say")
|
|
10640
|
+
return leadSayCommand(extra, flags, context);
|
|
10529
10641
|
const command = "lead token";
|
|
10530
10642
|
if (action !== "token" || extra.length > 0)
|
|
10531
|
-
return fail(write, json, `lead ${action ?? ""}`.trim(), "usage", "`toolroll lead token [--revoke] --as <you> --token <password>`", EXIT.usage);
|
|
10643
|
+
return fail(write, json, `lead ${action ?? ""}`.trim(), "usage", "`toolroll lead token [--revoke] --as <you> --token <password>` · `toolroll lead say \"<text>\" [--task <id>]` with the lead token", EXIT.usage);
|
|
10532
10644
|
for (const name of flags.keys())
|
|
10533
10645
|
if (!["as", "token", "revoke", "db", "json"].includes(name))
|
|
10534
10646
|
return fail(write, json, command, "usage", `--${name} is not a lead token option.`, EXIT.usage);
|
|
@@ -10552,6 +10664,42 @@ async function leadCommand(positional, flags, context) {
|
|
|
10552
10664
|
"it reaches you when the lead asks you (task ask), when it fails with nothing left to try, or for a security alert.",
|
|
10553
10665
|
]);
|
|
10554
10666
|
}
|
|
10667
|
+
/** `lead say "<text>" [--task <id>]` with the lead token: one short message in its person's chat from "Your lead".
|
|
10668
|
+
* Several within two minutes are one message. */
|
|
10669
|
+
function leadSayCommand(positional, flags, context) {
|
|
10670
|
+
const { store, write, json } = context;
|
|
10671
|
+
const command = "lead say";
|
|
10672
|
+
for (const name of flags.keys())
|
|
10673
|
+
if (!["task", "db", "json"].includes(name))
|
|
10674
|
+
return fail(write, json, command, "usage", `--${name} is not a lead say option.`, EXIT.usage);
|
|
10675
|
+
const task = flags.get("task");
|
|
10676
|
+
if (positional.length === 0 || task === true)
|
|
10677
|
+
return fail(write, json, command, "usage", "`toolroll lead say \"<what you're doing>\" [--task <id>]` with the lead token", EXIT.usage);
|
|
10678
|
+
const actor = currentActor();
|
|
10679
|
+
if (actor?.lead !== true)
|
|
10680
|
+
return fail(write, json, command, "unauthenticated", "Only the lead speaks as the lead: pass its lead token (--token or TOOLROLL_LEAD_TOKEN).", EXIT.refused);
|
|
10681
|
+
const said = leadSay(store, actor, positional.join(" "), task ?? null, context.clock());
|
|
10682
|
+
if (!said.ok)
|
|
10683
|
+
return fail(write, json, command, said.reason, said.message, said.reason === "usage" ? EXIT.usage : EXIT.refused);
|
|
10684
|
+
return succeed(write, json, command, { owner: actor.account, task: task ?? null, joined: said.joined, notification: said.notification }, () => [said.joined ? `Added to your last message to ${actor.account}.` : `Told ${actor.account}.`]);
|
|
10685
|
+
}
|
|
10686
|
+
/** `assignment claim <task>` with the lead token: the task reads "Your lead is on it" and leaves Needs you until the
|
|
10687
|
+
* lead completes it, hands it on (task ask), or two hours pass with no lead act on it. */
|
|
10688
|
+
function leadClaimCommand(positional, flags, context) {
|
|
10689
|
+
const { store, write, json } = context;
|
|
10690
|
+
const command = "assignment claim";
|
|
10691
|
+
for (const name of flags.keys())
|
|
10692
|
+
if (!["db", "json"].includes(name))
|
|
10693
|
+
return fail(write, json, command, "usage", `--${name} is not an assignment claim option.`, EXIT.usage);
|
|
10694
|
+
const id = positional[1];
|
|
10695
|
+
if (id === undefined || positional.length !== 2)
|
|
10696
|
+
return fail(write, json, command, "usage", "assignment claim takes exactly one task id.", EXIT.usage);
|
|
10697
|
+
const actor = currentActor();
|
|
10698
|
+
const claimed = leadClaim(store, actor, id, context.clock());
|
|
10699
|
+
if (!claimed.ok)
|
|
10700
|
+
return fail(write, json, command, claimed.reason, claimed.message, claimed.reason === "usage" ? EXIT.usage : EXIT.refused);
|
|
10701
|
+
return succeed(write, json, command, { task: id, root: claimed.root, lead: claimed.claim }, () => [`${id} is yours: it reads "Your lead is on it" until you complete it, hand it on with task ask, or go two hours without acting on it.`]);
|
|
10702
|
+
}
|
|
10555
10703
|
function blockTask(positional, flags, context) {
|
|
10556
10704
|
const { store, write, json } = context;
|
|
10557
10705
|
const id = positional[0];
|
package/dist/serve.d.ts
CHANGED
|
@@ -189,10 +189,14 @@ export type ServeOptions = {
|
|
|
189
189
|
/** Where `up`'s one-time sign-in link points, and how long it works. */
|
|
190
190
|
export declare const SIGN_IN_LINK_PATH = "/login/once/";
|
|
191
191
|
export declare const SIGN_IN_LINK_MS: number;
|
|
192
|
-
/** The console's server, and the one-time sign-in link `up` opens: a path on this server, or null for no such approver.
|
|
192
|
+
/** The console's server, and the one-time sign-in link `up` opens: a path on this server, or null for no such approver.
|
|
193
|
+
* closeCoding starts the coding shutdown on its own, ahead of the rest of a stop; close() awaits the same promise. */
|
|
193
194
|
export type DecisionServer = Server & {
|
|
194
195
|
mintSignInLink(account: string): string | null;
|
|
196
|
+
closeCoding(): Promise<void>;
|
|
195
197
|
};
|
|
198
|
+
/** How long a stop waits for teammates and the lead's follow pass before closing anyway. */
|
|
199
|
+
export declare const SHUTDOWN_WAIT_MS = 5000;
|
|
196
200
|
export declare function createDecisionServer(options: ServeOptions): DecisionServer;
|
|
197
201
|
/**
|
|
198
202
|
* A vscode://file href, or null (arc 6, finding 2): the link exists only
|