toolroll 0.9.15 → 0.9.17
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 +36 -0
- package/dist/assignment-brief.d.ts +10 -0
- package/dist/assignment-brief.js +56 -26
- package/dist/assignment.d.ts +8 -7
- package/dist/assignment.js +13 -8
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +22 -22
- package/dist/browser-crew.js +1 -0
- package/dist/browser-workspace.d.ts +5 -0
- package/dist/chat-actions.d.ts +7 -0
- package/dist/chat-actions.js +40 -4
- package/dist/chat-ask.d.ts +28 -0
- package/dist/chat-ask.js +91 -0
- package/dist/chat-channel.js +8 -1
- package/dist/chat-controls.d.ts +8 -0
- package/dist/chat-controls.js +3 -0
- package/dist/chat-delivery-state.d.ts +8 -1
- package/dist/chat-delivery-state.js +15 -2
- package/dist/chat-delivery.d.ts +3 -0
- package/dist/chat-delivery.js +49 -17
- package/dist/chat-quiet.js +4 -2
- package/dist/chat-voice.d.ts +3 -6
- package/dist/chat-voice.js +8 -2
- package/dist/chat-warmth.d.ts +21 -0
- package/dist/chat-warmth.js +36 -0
- package/dist/discord-api.d.ts +1 -1
- package/dist/discord-api.js +1 -1
- package/dist/discord-chat.d.ts +3 -2
- package/dist/discord-chat.js +28 -3
- package/dist/lead-about.d.ts +35 -0
- package/dist/lead-about.js +62 -0
- package/dist/lead-capabilities.d.ts +64 -0
- package/dist/lead-capabilities.js +199 -0
- package/dist/lead-commitments.d.ts +84 -0
- package/dist/lead-commitments.js +214 -0
- package/dist/lead-context.d.ts +34 -3
- package/dist/lead-context.js +155 -19
- package/dist/lead-follow.d.ts +1 -1
- package/dist/lead-follow.js +6 -2
- package/dist/lead-identity.d.ts +23 -0
- package/dist/lead-identity.js +33 -0
- package/dist/lead-people.d.ts +55 -0
- package/dist/lead-people.js +128 -0
- package/dist/lead-voice.d.ts +13 -9
- package/dist/lead-voice.js +26 -16
- package/dist/ledger-view.js +3 -2
- package/dist/mate-cli.d.ts +4 -1
- package/dist/mate-cli.js +23 -16
- package/dist/mate-contract.d.ts +1 -1
- package/dist/mate-contract.js +13 -4
- package/dist/mate-progress.js +7 -0
- package/dist/mate-tools.d.ts +26 -1
- package/dist/mate-tools.js +357 -7
- package/dist/mate.d.ts +35 -1
- package/dist/mate.js +121 -61
- package/dist/operate.js +4 -1
- package/dist/phase-routing.d.ts +1 -1
- package/dist/project-delete.js +3 -1
- package/dist/provider-auth.d.ts +4 -0
- package/dist/provider-auth.js +1 -1
- package/dist/reply-shape.d.ts +96 -0
- package/dist/reply-shape.js +452 -0
- package/dist/result-acts.d.ts +14 -0
- package/dist/result-acts.js +19 -0
- package/dist/retention.js +6 -0
- package/dist/serve.js +162 -55
- package/dist/slack-chat.d.ts +2 -0
- package/dist/slack-chat.js +30 -3
- package/dist/store.d.ts +60 -0
- package/dist/store.js +152 -3
- package/dist/surface.js +2 -2
- package/dist/task-status.d.ts +5 -15
- package/dist/task-status.js +18 -5
- package/dist/team-runtime.js +3 -2
- package/dist/teams-chat.js +16 -2
- package/dist/telegram-mate.d.ts +6 -1
- package/dist/telegram-mate.js +43 -11
- package/dist/telegram-progress.js +3 -2
- package/dist/telegram-team.js +6 -3
- package/dist/telegram.js +43 -3
- package/dist/test-temp.js +1 -1
- package/dist/when-html.d.ts +8 -3
- package/dist/when-html.js +23 -3
- package/dist/work-index.d.ts +2 -0
- package/dist/work-index.js +9 -6
- package/dist/work-summary.d.ts +1 -1
- package/dist/workspace-ui.d.ts +2 -14
- package/dist/workspace-ui.js +4 -22
- package/package.json +1 -1
package/dist/telegram-mate.js
CHANGED
|
@@ -23,10 +23,13 @@ export { channelRepos as telegramConversationRepos, resolveChannelMate as resolv
|
|
|
23
23
|
* the console and CLI state), and the door re-proves everything again.
|
|
24
24
|
*/
|
|
25
25
|
import { createHash, randomBytes } from "node:crypto";
|
|
26
|
-
import { MATE_MESSAGE_MAX_CHARS, runMateTurn } from "./mate.js";
|
|
26
|
+
import { MATE_MESSAGE_MAX_CHARS, mateFailureText, runMateTurn } from "./mate.js";
|
|
27
27
|
import { confirmMateProposal, dismissMateProposal } from "./mate-doors.js";
|
|
28
28
|
import { ceilingDigestOf, verifyApproverStanding } from "./principal.js";
|
|
29
|
+
import { MATE_ASK_OTHER } from "./store.js";
|
|
29
30
|
import { phoneText } from "./telegram-status.js";
|
|
31
|
+
import { shapeReplyParts, telegramReply } from "./reply-shape.js";
|
|
32
|
+
import { WARM_EMOJI, warmTurn } from "./chat-warmth.js";
|
|
30
33
|
import { TeamLeads } from "./team-leads.js";
|
|
31
34
|
import { chatResultHref } from "./chat-controls.js";
|
|
32
35
|
import { resultImageFileName, safeResultImageCaption, verifyResultImage } from "./chat-evidence.js";
|
|
@@ -39,7 +42,7 @@ export const CONVERSATION_MAX_AGE_MS = 10 * 60_000;
|
|
|
39
42
|
export const CARD_TTL_MS = 24 * 3_600_000;
|
|
40
43
|
export const CHALLENGE_TTL_MS = 10 * 60_000;
|
|
41
44
|
/** Telegram's own message ceiling, with room for our part headers (the same bound telegram.ts splits at). */
|
|
42
|
-
const PART_CAP = 3_900;
|
|
45
|
+
export const PART_CAP = 3_900;
|
|
43
46
|
// ---- the ceiling and the principal --------------------------------------------
|
|
44
47
|
/**
|
|
45
48
|
* The phone's project ceiling: the enrolled registry, canonicalized and
|
|
@@ -100,6 +103,17 @@ function keyboardWith(callbacks, link) {
|
|
|
100
103
|
const rows = [...(callbacks ?? []), ...(link === null ? [] : [link])];
|
|
101
104
|
return rows.length === 0 ? undefined : rows;
|
|
102
105
|
}
|
|
106
|
+
// ---- proposal cards on the wire ------------------------------------------------------
|
|
107
|
+
/** One inline button: an opaque callback token (a real in-chat act) or a url (navigation, never authority). */
|
|
108
|
+
/** The buttons under the lead's question: options two to a row, then "Something else". Each names the turn and the
|
|
109
|
+
* option's place, nothing more; a tap re-proves that the question is still open and is the tapper's own. */
|
|
110
|
+
export function askKeyboard(ask) {
|
|
111
|
+
const rows = [];
|
|
112
|
+
for (let at = 0; at < ask.options.length; at += 2)
|
|
113
|
+
rows.push(ask.options.slice(at, at + 2).map((label, offset) => ({ text: label.slice(0, 60), callback_data: `ask:${ask.turn}:${at + offset}` })));
|
|
114
|
+
rows.push([{ text: MATE_ASK_OTHER, callback_data: `ask:${ask.turn}:x` }]);
|
|
115
|
+
return rows;
|
|
116
|
+
}
|
|
103
117
|
/** Mint the card's tokens before the send, so a tap can never name a token that does not exist. */
|
|
104
118
|
export function mintCardTokens(store, binding, proposal, now, messageId, chatId = binding.chatId) {
|
|
105
119
|
const confirm = randomBytes(16).toString("hex");
|
|
@@ -393,7 +407,7 @@ async function runTelegramConversation(row, args) {
|
|
|
393
407
|
const bound = store.getTelegramConversation(row.id)?.session ?? null;
|
|
394
408
|
const session = bound === null ? null : store.getMateSession(bound);
|
|
395
409
|
if (session === null) {
|
|
396
|
-
await notify(
|
|
410
|
+
await notify("This conversation was restarted before my reply went out, so it wasn't sent. Nothing was changed. Send your message again.");
|
|
397
411
|
finish({ state: "failed", outcome: "unsent:no-session" });
|
|
398
412
|
report.refused++;
|
|
399
413
|
return;
|
|
@@ -513,11 +527,14 @@ async function runTelegramConversation(row, args) {
|
|
|
513
527
|
const origin = proposal === null ? null : options.phoneOrigin?.() ?? null;
|
|
514
528
|
const wanted = proposal === null ? null : proposalLink(store, proposal, repos);
|
|
515
529
|
const keyboard = keyboardWith(part.keyboard, phoneLinkButton(origin, wanted));
|
|
516
|
-
|
|
530
|
+
// The lead's own words carry their bold anchors and labelled links as entities, never as Markdown characters.
|
|
531
|
+
const voiced = part.kind === "reply" ? telegramReply(part.text) : null;
|
|
532
|
+
const text = proposal !== null && part.keyboard === null ? handoffCardText(part.text, linkNote(origin, wanted)) : voiced?.text ?? part.text;
|
|
517
533
|
method = "sendMessage";
|
|
518
534
|
send = () => transport("sendMessage", {
|
|
519
535
|
chat_id: chatId,
|
|
520
536
|
text,
|
|
537
|
+
...(voiced === null || voiced.entities.length === 0 ? {} : { entities: voiced.entities }),
|
|
521
538
|
link_preview_options: { is_disabled: true },
|
|
522
539
|
...(part.replyTo === null ? {} : { reply_parameters: { message_id: Number(part.replyTo) } }),
|
|
523
540
|
...(keyboard === undefined ? {} : { reply_markup: { inline_keyboard: keyboard } }),
|
|
@@ -578,7 +595,11 @@ async function runTelegramConversation(row, args) {
|
|
|
578
595
|
try {
|
|
579
596
|
return store.transact(() => {
|
|
580
597
|
const now = clock();
|
|
581
|
-
const parts =
|
|
598
|
+
const parts = shapeReplyParts(reply, PART_CAP, { asked: row.text, appOrigin: options.phoneOrigin?.() ?? null }, shaped => telegramReply(shaped).text.length).map((text, index) => ({ kind: "reply", text, replyTo: index === 0 ? row.messageId : null }));
|
|
599
|
+
// The lead's question to its owner: one tap per option, then "Something else".
|
|
600
|
+
const ask = store.mateAsk(turn);
|
|
601
|
+
if (ask !== null)
|
|
602
|
+
parts.push({ kind: "reply", text: phoneText(ask.question, 1_000), keyboard: askKeyboard(ask) });
|
|
582
603
|
// Only a turn that answered may have its selection sent: a failed or revoked turn's rows were deleted with its drafts, and the state is read again here.
|
|
583
604
|
const selected = store.getMateTurn(turn)?.state === "answered" ? store.listMateTurnEvidence(turn) : [];
|
|
584
605
|
for (const image of selected)
|
|
@@ -617,7 +638,7 @@ async function runTelegramConversation(row, args) {
|
|
|
617
638
|
return;
|
|
618
639
|
}
|
|
619
640
|
if (turn.state !== "answered") {
|
|
620
|
-
await notify(
|
|
641
|
+
await notify(mateFailureText(turn.failureReason));
|
|
621
642
|
finish({ state: "failed", outcome: `replayed:${turn.failureReason ?? "failed"}` });
|
|
622
643
|
report.refused++;
|
|
623
644
|
return;
|
|
@@ -677,7 +698,13 @@ async function runTelegramConversation(row, args) {
|
|
|
677
698
|
const { who, session, thread, config } = resolved;
|
|
678
699
|
const revalidate = async () => {
|
|
679
700
|
const problem = await telegramChannelProblem(store, { botId, bindingId: binding.id, approverGeneration: binding.approverGeneration, repos: who.repos }, readProjects);
|
|
680
|
-
|
|
701
|
+
if (problem === null)
|
|
702
|
+
return { ok: true };
|
|
703
|
+
const reason = problem === UNREADABLE_REGISTRY ? "projects-unreadable"
|
|
704
|
+
: problem === "this chat is no longer paired" ? "unpaired"
|
|
705
|
+
: problem === "the paired account is no longer an approver" ? "not-approver"
|
|
706
|
+
: problem === "the connected projects changed" ? "projects-changed" : "access-changed";
|
|
707
|
+
return { ok: false, reason };
|
|
681
708
|
};
|
|
682
709
|
// Bound BEFORE the dispatch, under the claim: a crash from here on finds
|
|
683
710
|
// the receipt in this session, not in whichever session is live later.
|
|
@@ -688,17 +715,23 @@ async function runTelegramConversation(row, args) {
|
|
|
688
715
|
// crash gets the original turn back, never a second dispatch.
|
|
689
716
|
const heartbeat = setInterval(() => { held(); }, 30_000);
|
|
690
717
|
heartbeat.unref?.();
|
|
718
|
+
// Once the turn reaches a tool step: a 👍 on the owner's message, and "typing…" while the lead works.
|
|
719
|
+
const warm = warmTurn({
|
|
720
|
+
react: () => transport("setMessageReaction", { chat_id: chatId, message_id: Number(row.messageId), reaction: [{ type: "emoji", emoji: WARM_EMOJI }] }),
|
|
721
|
+
typing: () => transport("sendChatAction", { chat_id: chatId, action: "typing" }),
|
|
722
|
+
});
|
|
691
723
|
let outcome;
|
|
692
724
|
try {
|
|
693
725
|
outcome = await runMateTurn({
|
|
694
726
|
store, who, session, thread, config, key: null, message: row.text, requestId: row.request,
|
|
695
727
|
...(row.context === null ? {} : { context: row.context }),
|
|
696
728
|
...(options.subscriptionRunner === undefined ? {} : { subscriptionRunner: options.subscriptionRunner }),
|
|
697
|
-
clock, evidenceRoot: options.evidenceRoot, revalidate, mediaDelivery: "documents",
|
|
729
|
+
clock, evidenceRoot: options.evidenceRoot, revalidate, mediaDelivery: "documents", channel: "telegram", onProgress: warm.onProgress,
|
|
698
730
|
});
|
|
699
731
|
}
|
|
700
732
|
finally {
|
|
701
733
|
clearInterval(heartbeat);
|
|
734
|
+
warm.stop();
|
|
702
735
|
}
|
|
703
736
|
const receipt = store.mateRequestReceipt(session.id, row.request);
|
|
704
737
|
if (receipt !== null)
|
|
@@ -708,7 +741,7 @@ async function runTelegramConversation(row, args) {
|
|
|
708
741
|
await requeue("busy");
|
|
709
742
|
return;
|
|
710
743
|
}
|
|
711
|
-
await notify(phoneText(
|
|
744
|
+
await notify(phoneText(outcome.message, 1_000));
|
|
712
745
|
finish({ state: "failed", outcome: `refused:${outcome.refused}` });
|
|
713
746
|
report.refused++;
|
|
714
747
|
return;
|
|
@@ -716,8 +749,7 @@ async function runTelegramConversation(row, args) {
|
|
|
716
749
|
if (!outcome.ok) {
|
|
717
750
|
// Truthful: what failed, that nothing was kept, and that a new message
|
|
718
751
|
// is a new turn. A failed turn's drafts are already gone.
|
|
719
|
-
|
|
720
|
-
await notify(phoneText(words, 1_000));
|
|
752
|
+
await notify(phoneText(outcome.message, 1_000));
|
|
721
753
|
finish({ state: "failed", outcome: `failed:${outcome.failed}` });
|
|
722
754
|
report.refused++;
|
|
723
755
|
return;
|
|
@@ -9,6 +9,7 @@ import { chatResultHref, chatControlHref } from "./chat-controls.js";
|
|
|
9
9
|
import { phoneText, projectLabel } from "./telegram-status.js";
|
|
10
10
|
import { assignmentStatusFacts, headlineEmoji, pullRequestFactOf, requirementsOf, statusDetailLines, unverifiedWhenRefuted, taskStatusOf } from "./task-status.js";
|
|
11
11
|
import { leadClaimOf } from "./lead-voice.js";
|
|
12
|
+
import { leadNameOf } from "./lead-identity.js";
|
|
12
13
|
import { chatTitle } from "./chat-voice.js";
|
|
13
14
|
import { manualReviewOnly } from "./proof.js";
|
|
14
15
|
import { failedCheckExit } from "./workspace-ui.js";
|
|
@@ -79,11 +80,11 @@ export function telegramProgressCard(store, run, taskId, project, now = new Date
|
|
|
79
80
|
const completer = assignment?.completion?.actor.replace(/^(?:operator|coordinator|lead):/, "") ?? null;
|
|
80
81
|
if (viewer !== undefined && facts.completedBy != null && (completer === viewer || facts.completedBy === viewer))
|
|
81
82
|
facts = { ...facts, completedBy: null };
|
|
82
|
-
// The person's lead took it on: "
|
|
83
|
+
// The person's lead took it on: "<name> is on it" rather than waiting for them (lead-voice.ts).
|
|
83
84
|
if (facts.lead === undefined) {
|
|
84
85
|
const lead = leadClaimOf(store, taskId, now, viewer);
|
|
85
86
|
if (lead !== null)
|
|
86
|
-
facts = { ...facts, lead: lead.state };
|
|
87
|
+
facts = { ...facts, lead: lead.state, leadName: leadNameOf(store, lead.owner) };
|
|
87
88
|
}
|
|
88
89
|
const status = taskStatusOf(facts);
|
|
89
90
|
const checkProgress = store.checkProgress(run.id);
|
package/dist/telegram-team.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { renderReply, shapeReplyParts } from "./reply-shape.js";
|
|
1
2
|
import { TeamLeads } from "./team-leads.js";
|
|
2
3
|
import { proposalPreview, tooLongText } from "./chat-channel.js";
|
|
3
|
-
import { mintCardTokens, phoneLinkButton, splitParts } from "./telegram-mate.js";
|
|
4
|
+
import { PART_CAP, mintCardTokens, phoneLinkButton, splitParts } from "./telegram-mate.js";
|
|
4
5
|
import { MATE_MESSAGE_MAX_CHARS } from "./mate.js";
|
|
5
6
|
import { phoneText } from "./telegram-status.js";
|
|
6
7
|
/** `/team` lists, `/team <n>` chooses, `/team off` (or `private`) leaves. Any other slash text is not ours. */
|
|
@@ -265,7 +266,9 @@ export async function deliverTeamChats(store, botId, transport, clock, report, p
|
|
|
265
266
|
const skip = role === "operator" && (ownMessage || status === "cancelled" || String(message["author"] ?? "").length === 0);
|
|
266
267
|
const prefix = `telegram-team:${chat.id}:`;
|
|
267
268
|
if (!skip) {
|
|
268
|
-
|
|
269
|
+
// The lead's reply is split as written, then each part shaped and measured as Telegram shows it, so no cut lands
|
|
270
|
+
// inside a link and no rendered part passes Telegram's ceiling.
|
|
271
|
+
const text = role === "assistant" ? shapeReplyParts(String(message["text"]), PART_CAP, { appOrigin: phoneOrigin?.() ?? null }, shaped => renderReply(shaped, "telegram").length).map(part => renderReply(part, "telegram")) : `${phoneText(String(message["author"]), 40)}: ${String(message["text"])}`;
|
|
269
272
|
const sent = await sendParts(store, transport, chat.chatId, text, `${prefix}message:${id}`, clock, access, report);
|
|
270
273
|
if (sent !== null) {
|
|
271
274
|
report.problems.push(`team chat ${chat.chatId}: ${sent}`);
|
|
@@ -329,7 +332,7 @@ function cardBindingFor(store, botId, chat, turn) {
|
|
|
329
332
|
return bindings.find(one => members.has(one.approver)) ?? null;
|
|
330
333
|
}
|
|
331
334
|
async function sendParts(store, transport, chatId, text, receipt, clock, access, report, keyboard, tokens = []) {
|
|
332
|
-
const parts = splitParts(text);
|
|
335
|
+
const parts = typeof text === "string" ? splitParts(text) : text;
|
|
333
336
|
const delivered = store.serviceCursor(receipt);
|
|
334
337
|
for (const [index, part] of parts.entries()) {
|
|
335
338
|
if (index < delivered)
|
package/dist/telegram.js
CHANGED
|
@@ -31,7 +31,7 @@ import { isLifecycleNotification, isTelegramProgressNotification, TELEGRAM_HOLD_
|
|
|
31
31
|
import { telegramProgressCard } from "./telegram-progress.js";
|
|
32
32
|
import { enqueueEveningDigests, finishedView, isTaskFact, joinsBatch, needsPerson, quietCardView } from "./chat-quiet.js";
|
|
33
33
|
import { BATCH_MS, chatText, chatTitle, factLinkLabel, mentions, nameTelegramBot } from "./chat-voice.js";
|
|
34
|
-
import { LEAD_SAY_KIND, enqueueLeadLapses, leadSayEarlier, leadSayText } from "./lead-voice.js";
|
|
34
|
+
import { LEAD_SAY_KIND, enqueueLeadLapses, leadSayEarlier, leadSayText, leadSubjectOf } from "./lead-voice.js";
|
|
35
35
|
import { applyTeamInbound, deliverTeamChats, teamCommand } from "./telegram-team.js";
|
|
36
36
|
import { applyFlowReply, applyFlowTap, FLOW_DECIDE_KEY, flowButtons, flowDecisionAt } from "./telegram-flow.js";
|
|
37
37
|
import { connectChannel, FLOW_WORDS, takeChannelMessage, watchedChannel } from "./chat-inbox.js";
|
|
@@ -920,7 +920,7 @@ async function deliverOutboxTo(store, botId, binding, transport, owner, clock, r
|
|
|
920
920
|
};
|
|
921
921
|
/** The lead's words (lead-voice.ts): one message; a later say within two minutes edits it in place. */
|
|
922
922
|
const leadSayOutcome = async (fact) => {
|
|
923
|
-
const words = leadSayText(fact);
|
|
923
|
+
const words = leadSayText(store, fact, binding.approver);
|
|
924
924
|
const button = fact.link === null ? null : factButton(phoneOrigin, fact.link);
|
|
925
925
|
const keyboard = button === null ? [] : [button];
|
|
926
926
|
const earlier = leadSayEarlier(store, fact).map(id => store.telegramMessageOf(id, fact.destination)).find(one => one !== null) ?? null;
|
|
@@ -1064,7 +1064,7 @@ async function deliverOne(store, botId, binding, sender, notification, clock, ph
|
|
|
1064
1064
|
// A short title, never the task's id or a "— revision" suffix (chat-voice.ts).
|
|
1065
1065
|
const title = notification.taskId === null ? undefined : chatTitle(store, notification.taskId);
|
|
1066
1066
|
const task = notification.taskId === null ? [] : [{ id: notification.taskId, ...(title === undefined ? {} : { title }) }];
|
|
1067
|
-
const words = chatText(`${alreadyAccepted ? "Acceptance recorded" : notification.
|
|
1067
|
+
const words = chatText(`${alreadyAccepted ? "Acceptance recorded" : leadSubjectOf(store, notification, binding.approver)}${body === "" ? "" : `\n\n${body}`}`, task);
|
|
1068
1068
|
// The task's title once: in front, unless the words already name it.
|
|
1069
1069
|
const parts = split(`${notificationIdentity(notification, title !== undefined && mentions(words, title) ? undefined : title)}${words}`);
|
|
1070
1070
|
const button = notification.link !== null && !alreadyAccepted && current ? factButton(phoneOrigin, notification.link) : null;
|
|
@@ -1970,6 +1970,46 @@ function applyCallback(context, update, effects) {
|
|
|
1970
1970
|
}
|
|
1971
1971
|
return;
|
|
1972
1972
|
}
|
|
1973
|
+
// The lead's question to its owner: an option is sent as their next message; "Something else" asks them to type it.
|
|
1974
|
+
const askTap = /^ask:([1-9][0-9]{0,14}):([0-3]|x)$/.exec(token);
|
|
1975
|
+
if (askTap !== null) {
|
|
1976
|
+
if (tapChat !== binding.chatId || context.conversation === undefined) {
|
|
1977
|
+
report.ignored++;
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
1980
|
+
const turn = Number(askTap[1]);
|
|
1981
|
+
const found = store.getMateTurn(turn)?.approver === binding.approver ? store.mateAskState(turn, clock()) : { state: "expired" };
|
|
1982
|
+
if (found.state !== "open") {
|
|
1983
|
+
const line = found.state === "answered" ? "That question was already answered." : "That question expired. Send your answer as a message.";
|
|
1984
|
+
ack(line);
|
|
1985
|
+
editText(`${message.text ?? ""}\n\n${line}`.trim().slice(0, 4000));
|
|
1986
|
+
return;
|
|
1987
|
+
}
|
|
1988
|
+
const ask = found.ask;
|
|
1989
|
+
if (store.telegramConversationWaitingOn(binding.id, String(message.message_id))) {
|
|
1990
|
+
ack("Your answer is on its way.");
|
|
1991
|
+
return;
|
|
1992
|
+
}
|
|
1993
|
+
if (askTap[2] === "x") {
|
|
1994
|
+
ack("Type your answer as a message.");
|
|
1995
|
+
return;
|
|
1996
|
+
}
|
|
1997
|
+
const option = ask.options[Number(askTap[2])];
|
|
1998
|
+
if (option === undefined) {
|
|
1999
|
+
ack("That option is no longer there.");
|
|
2000
|
+
return;
|
|
2001
|
+
}
|
|
2002
|
+
const focused = store.chatFocus("telegram", binding.id);
|
|
2003
|
+
store.enqueueTelegramConversation({
|
|
2004
|
+
binding, updateId: update.update_id, messageId: String(message.message_id), replyTo: null,
|
|
2005
|
+
request: telegramRequestId(botId, binding.id, update.update_id), text: option,
|
|
2006
|
+
context: focused === null ? null : focusContextFor(focused), taskId: focused, sourceRun: null,
|
|
2007
|
+
}, clock());
|
|
2008
|
+
report.chatQueued = (report.chatQueued ?? 0) + 1;
|
|
2009
|
+
ack(`Sent: ${option}`.slice(0, 190));
|
|
2010
|
+
editText(`${message.text ?? ask.question}\n\nYou chose: ${option}`.slice(0, 4000));
|
|
2011
|
+
return;
|
|
2012
|
+
}
|
|
1973
2013
|
const action = store.getTelegramAction(token);
|
|
1974
2014
|
if (action === null && context.conversation !== undefined && store.getTelegramProposalAction(token) !== null) {
|
|
1975
2015
|
// A proposal card's button: the shared confirm door, inside this
|
package/dist/test-temp.js
CHANGED
|
@@ -18,7 +18,7 @@ export const TEST_TEMP_PREFIXES = [
|
|
|
18
18
|
"flow-goal-", "flow-scratch", "flows-chat-", "invoke-attest-", "knowledge-test-", "learning-", "peek-", "phone-origin-",
|
|
19
19
|
"proposal-repo-", "race-", "refresh-migration-", "refresh-refusal-", "release-check-", "shared-chat-", "skills-test-",
|
|
20
20
|
"task-status-surfaces-", "template-repo-", "template-test-", "v14-migrate-", "v17-migrate-", "watch-anchor-",
|
|
21
|
-
"needs-you-", "deps-lock-", "deps-state-", "upgrade-fresh-", "upgrade-path-", "deploy-restore-", "result-review-", "one-result-page-", "deploy-before-swap-", "one-vocabulary-", "accept-finishes-",
|
|
21
|
+
"needs-you-", "deps-lock-", "deps-state-", "upgrade-fresh-", "upgrade-path-", "deploy-restore-", "result-review-", "one-result-page-", "deploy-before-swap-", "one-vocabulary-", "accept-finishes-", "lead-promises-", "lead-people-",
|
|
22
22
|
// Playwright's browser profiles and artifacts.
|
|
23
23
|
"playwright_chromiumdev_profile-", "playwright_firefoxdev_profile-", "playwright-artifacts-",
|
|
24
24
|
];
|
package/dist/when-html.d.ts
CHANGED
|
@@ -13,10 +13,15 @@ export declare function deadlineWords(deadline: {
|
|
|
13
13
|
/** The exact minute in `zone` ("2026-09-30 16:39", with " UTC" when it is UTC): a time's title, one hover away. */
|
|
14
14
|
export declare function fullWhen(iso: string, zone?: string): string;
|
|
15
15
|
/** Every server-rendered stamp (`whenHtml`'s `<time data-when>`) inside `root`, reworded by the one formatter in the
|
|
16
|
-
* viewer's zone: the same words on a desk and a phone, the exact minute in the title.
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* viewer's zone: the same words on a desk and a phone, the exact minute in the title. An exact stamp
|
|
17
|
+
* (`exactWhenHtml`'s `data-when="exact"`, the ledger's seconds) keeps its seconds, in the viewer's zone, and its
|
|
18
|
+
* ISO title. The server's UTC words stay only without script. Times React writes and relative ages ("3 min ago")
|
|
19
|
+
* carry no `data-when` and are never touched. */
|
|
19
20
|
export declare function localizeTimes(root: ParentNode, now?: Date, zone?: string): void;
|
|
21
|
+
/** The exact second in `zone` ("2026-09-30 16:39:12", with " UTC" when it is UTC): an audit record's stamp. */
|
|
22
|
+
export declare function exactWhen(iso: string, zone?: string): string;
|
|
23
|
+
/** The `<time>` for an exact stamp: its seconds in UTC words on the server, the viewer's zone in a browser. */
|
|
24
|
+
export declare function exactWhenHtml(iso: string): string;
|
|
20
25
|
/** The `<time>` for a stamp: `full` is the words a desk shows ("2026-09-30 16:39 UTC"). Empty for no stamp. */
|
|
21
26
|
export declare function whenHtml(iso: string | null, full: string, now?: Date): string;
|
|
22
27
|
/** "2026-09-30 16:39 UTC" — the desk's words for a stamp. */
|
package/dist/when-html.js
CHANGED
|
@@ -75,14 +75,21 @@ export function fullWhen(iso, zone = "UTC") {
|
|
|
75
75
|
return `${p.year}-${String(p.month).padStart(2, "0")}-${String(p.day).padStart(2, "0")} ${p.clock}${zone === "UTC" ? " UTC" : ""}`;
|
|
76
76
|
}
|
|
77
77
|
/** Every server-rendered stamp (`whenHtml`'s `<time data-when>`) inside `root`, reworded by the one formatter in the
|
|
78
|
-
* viewer's zone: the same words on a desk and a phone, the exact minute in the title.
|
|
79
|
-
*
|
|
80
|
-
*
|
|
78
|
+
* viewer's zone: the same words on a desk and a phone, the exact minute in the title. An exact stamp
|
|
79
|
+
* (`exactWhenHtml`'s `data-when="exact"`, the ledger's seconds) keeps its seconds, in the viewer's zone, and its
|
|
80
|
+
* ISO title. The server's UTC words stay only without script. Times React writes and relative ages ("3 min ago")
|
|
81
|
+
* carry no `data-when` and are never touched. */
|
|
81
82
|
export function localizeTimes(root, now = new Date(), zone = viewerZone()) {
|
|
82
83
|
root.querySelectorAll("time[data-when][datetime]").forEach(node => {
|
|
83
84
|
const iso = node.getAttribute("datetime") ?? "";
|
|
84
85
|
if (iso === "" || Number.isNaN(new Date(iso).getTime()))
|
|
85
86
|
return;
|
|
87
|
+
if (node.getAttribute("data-when") === "exact") {
|
|
88
|
+
const words = exactWhen(iso, zone);
|
|
89
|
+
if (node.textContent !== words)
|
|
90
|
+
node.textContent = words;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
86
93
|
const words = shortWhen(iso, now, zone), full = fullWhen(iso, zone);
|
|
87
94
|
if (node.title !== full)
|
|
88
95
|
node.title = full;
|
|
@@ -90,6 +97,19 @@ export function localizeTimes(root, now = new Date(), zone = viewerZone()) {
|
|
|
90
97
|
node.textContent = words;
|
|
91
98
|
});
|
|
92
99
|
}
|
|
100
|
+
/** The exact second in `zone` ("2026-09-30 16:39:12", with " UTC" when it is UTC): an audit record's stamp. */
|
|
101
|
+
export function exactWhen(iso, zone = "UTC") {
|
|
102
|
+
const at = new Date(iso);
|
|
103
|
+
if (Number.isNaN(at.getTime()))
|
|
104
|
+
return iso;
|
|
105
|
+
const seconds = String(at.getUTCSeconds()).padStart(2, "0");
|
|
106
|
+
const minute = fullWhen(iso, zone);
|
|
107
|
+
return zone === "UTC" ? minute.replace(/ UTC$/, `:${seconds} UTC`) : `${minute}:${seconds}`;
|
|
108
|
+
}
|
|
109
|
+
/** The `<time>` for an exact stamp: its seconds in UTC words on the server, the viewer's zone in a browser. */
|
|
110
|
+
export function exactWhenHtml(iso) {
|
|
111
|
+
return `<time data-when="exact" datetime="${escape(iso)}" title="${escape(iso)}">${escape(exactWhen(iso))}</time>`;
|
|
112
|
+
}
|
|
93
113
|
/** The `<time>` for a stamp: `full` is the words a desk shows ("2026-09-30 16:39 UTC"). Empty for no stamp. */
|
|
94
114
|
export function whenHtml(iso, full, now = new Date()) {
|
|
95
115
|
if (iso === null || iso === "")
|
package/dist/work-index.d.ts
CHANGED
|
@@ -34,6 +34,8 @@ export type WorkIndexItem = {
|
|
|
34
34
|
digest: string;
|
|
35
35
|
} | null;
|
|
36
36
|
evidence: 'recorded';
|
|
37
|
+
/** The viewer's own lead took it on (lead-voice.ts): "<name> is on it.", by the name they gave it, read now. */
|
|
38
|
+
lead?: string;
|
|
37
39
|
};
|
|
38
40
|
export type WorkIndexOptions = {
|
|
39
41
|
view?: WorkView;
|
package/dist/work-index.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/** Paginated, permission-bound summaries of saved task facts. These reads do
|
|
2
2
|
* not verify artifacts or authorize task operations; a build Toolroll can't
|
|
3
3
|
* yet confirm stopped is read through the store's own read-only probe. */
|
|
4
|
+
import { leadNameOf } from './lead-identity.js';
|
|
4
5
|
import { createHash } from 'node:crypto';
|
|
5
6
|
import { existsSync } from 'node:fs';
|
|
6
7
|
import { join } from 'node:path';
|
|
7
8
|
import { readProjectAccess } from './project-access.js';
|
|
8
9
|
import { pauseOnTaskProviders, providerName, signInReason } from './provider-auth.js';
|
|
9
10
|
import { scopeTermsProblem, stopFactOf } from './store.js';
|
|
10
|
-
import { plainReasonOf, replacedWords, stageOfCode, taskStatusOf, workToneOf } from './task-status.js';
|
|
11
|
+
import { leadOnIt, plainReasonOf, replacedWords, stageOfCode, taskStatusOf, workToneOf } from './task-status.js';
|
|
11
12
|
import { isCheckLevel } from './check-levels.js';
|
|
12
13
|
import { MARKER } from './worktree.js';
|
|
13
14
|
import { withFollowUps } from './result-follow-ups.js';
|
|
@@ -395,7 +396,9 @@ export function workIndexPage(store, now, access, options = {}) {
|
|
|
395
396
|
const ref = store.lookupRef(taskId);
|
|
396
397
|
return ref === null ? null : custody.readings.get(ref.id) ?? null;
|
|
397
398
|
};
|
|
398
|
-
|
|
399
|
+
// A claimed row says the viewer's own name for their lead.
|
|
400
|
+
const leadName = page.some(row => n(row, 'lead_claim') !== 0) && access.principal === 'operator' && access.viewer != null ? leadNameOf(store, access.viewer) : undefined;
|
|
401
|
+
return { items: page.map(row => itemOf(row, access.principal, probe, leadName)), projects, totals: rows[0] === undefined ? { ...EMPTY } : counts(rows[0]),
|
|
399
402
|
groups: rows[0] === undefined ? { ...NO_GROUPS } : groupCounts(rows[0]), nextCursor, limit, view };
|
|
400
403
|
}
|
|
401
404
|
export function workCountsByProject(store, now, access) {
|
|
@@ -441,13 +444,13 @@ function listChecksOf(own, level, follows) {
|
|
|
441
444
|
return null;
|
|
442
445
|
return { status: read.status, exitCode: null, head: null, level: read.level, running: read.running };
|
|
443
446
|
}
|
|
444
|
-
function itemOf(row, principal, probe) {
|
|
447
|
+
function itemOf(row, principal, probe, leadName) {
|
|
445
448
|
const code = String(row['code']), id = String(row['id']);
|
|
446
449
|
const custody = code === 'process-needs-attention' || (code === 'result-needs-attention' && n(row, 'custody_unresolved') === 1);
|
|
447
450
|
const process = custody && probe !== undefined ? processNeedOf(probe(id)) : null;
|
|
448
451
|
const waiting = process !== null && 'wait' in process;
|
|
449
452
|
const need = n(row, 'needs') === 1 && !waiting, running = n(row, 'family_running') === 1;
|
|
450
|
-
// The lead's claim (lead-voice.ts): the row still reads its own stage, worded "
|
|
453
|
+
// The lead's claim (lead-voice.ts): the row still reads its own stage, worded "<name> is on it".
|
|
451
454
|
const lead = n(row, 'lead_claim') === 1 ? 'on-it' : n(row, 'lead_claim') === 2 ? 'lapsed' : null;
|
|
452
455
|
const personNeed = (n(row, 'person_need') === 1 || need) && !waiting;
|
|
453
456
|
const assignmentState = code === 'complete' ? 'complete' : code === 'ready-to-check' ? 'ready-to-check'
|
|
@@ -559,7 +562,7 @@ function itemOf(row, principal, probe) {
|
|
|
559
562
|
reason: finished ? null : plainReasonOf(reading.stage, code, detail || null), report: s(row, 'result_role') === 'scout',
|
|
560
563
|
checks: !finished ? null : listChecksOf(own, s(row, 'check_level'), row['check_follows']),
|
|
561
564
|
completedBy: code !== 'complete' ? null : n(row, 'completed_by_lead') === 1 ? 'the lead' : String(row['checked_actor']).replace(/^(?:operator|coordinator|lead):/, ''),
|
|
562
|
-
...(lead === null ? {} : { lead }) });
|
|
565
|
+
...(lead === null ? {} : { lead, ...(leadName === undefined ? {} : { leadName }) }) });
|
|
563
566
|
label = shared.headline;
|
|
564
567
|
detail = shared.sentence;
|
|
565
568
|
// Every Needs you action wears its need's own words.
|
|
@@ -584,5 +587,5 @@ function itemOf(row, principal, probe) {
|
|
|
584
587
|
ask: need ? ASKS[n(row, 'ask_rank')] ?? 'unblock' : null,
|
|
585
588
|
chip: need ? askChipOf({ headline: shared.headline, need: shared.need?.key ?? null, planChanged, mismatch: verdict === 'refuted' && n(row, 'proof_failed_check') !== 1, ask: ASKS[n(row, 'ask_rank')] ?? 'unblock' }) : null,
|
|
586
589
|
primaryAction, completion: code === 'complete' ? { actor: String(row['checked_actor']), at: String(row['checked_at']), digest: String(row['checked_digest']) } : null,
|
|
587
|
-
evidence: 'recorded' };
|
|
590
|
+
evidence: 'recorded', ...(lead === 'on-it' ? { lead: leadOnIt(leadName) } : {}) };
|
|
588
591
|
}
|
package/dist/work-summary.d.ts
CHANGED
|
@@ -42,7 +42,7 @@ export declare function workDecisionAction(taskId: string, decision: NonNullable
|
|
|
42
42
|
/** Pure counterpart for callers already holding the same facts. */
|
|
43
43
|
export declare function workSummaryOf(facts: WorkFacts, principal: WorkPrincipal, resultDisplay?: DisplayStatus): WorkSummary;
|
|
44
44
|
export type WorkSummaryAccess =
|
|
45
|
-
/** `viewer`: the person reading, whose own lead's claim (lead-voice.ts) a task reads as "
|
|
45
|
+
/** `viewer`: the person reading, whose own lead's claim (lead-voice.ts) a task reads as "<name> is on it". */
|
|
46
46
|
{
|
|
47
47
|
principal: "operator";
|
|
48
48
|
repos: readonly string[] | null;
|
package/dist/workspace-ui.d.ts
CHANGED
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
* claimed without one.
|
|
18
18
|
*/
|
|
19
19
|
import type { DispatchDiagnosis } from "./dispatch.js";
|
|
20
|
-
import { ACCEPT_NEEDS_REASON, type AcceptLabel } from "./result-acts.js";
|
|
21
|
-
export { ACCEPT_NEEDS_REASON };
|
|
20
|
+
import { ACCEPT_NEEDS_REASON, cantAcceptYetOf, evidenceProblemOf, type AcceptLabel, type EvidenceProblem } from "./result-acts.js";
|
|
21
|
+
export { ACCEPT_NEEDS_REASON, cantAcceptYetOf, evidenceProblemOf, type EvidenceProblem };
|
|
22
22
|
import { type ProofVerdict } from "./proof.js";
|
|
23
23
|
import type { ReviewRetryState, TaskState } from "./store.js";
|
|
24
24
|
import type { TaskControlView } from "./task-control.js";
|
|
@@ -113,14 +113,6 @@ export type DisplayStatus = {
|
|
|
113
113
|
tone: StatusTone;
|
|
114
114
|
action: NextAction | null;
|
|
115
115
|
};
|
|
116
|
-
/** What kind of evidence problem a verdict records: a check the machine
|
|
117
|
-
* actually ran and saw fail, evidence that contradicts the sealed record
|
|
118
|
-
* (a mismatched changed-path claim, an altered criterion, a caveat that
|
|
119
|
-
* contradicts a met verdict), or evidence that is simply absent. A generic
|
|
120
|
-
* refuted verdict is NOT a failed test claim — only the verify-command
|
|
121
|
-
* reasons say a check failed. */
|
|
122
|
-
export type EvidenceProblem = "checks-failed" | "mismatched" | "missing" | "none";
|
|
123
|
-
export declare function evidenceProblemOf(verdict: ProofVerdict | null, reasons: readonly string[]): EvidenceProblem;
|
|
124
116
|
/** The exit code of the failed check, when the recorded reason names one. */
|
|
125
117
|
export declare function failedCheckExit(reasons: readonly string[]): number | null;
|
|
126
118
|
/** The observed publication record in words. Each state is a distinct
|
|
@@ -192,10 +184,6 @@ export declare function dispatchActionLabel(dispatch: DispatchDiagnosis | null):
|
|
|
192
184
|
/** A finished result's display status in the shared headline's words: a
|
|
193
185
|
* failed project check is Failed, a result missing its record needs a
|
|
194
186
|
* person, every other saved result is Ready for review. Token and action stay. */
|
|
195
|
-
/** Why a result can't be accepted as it stands, in one plain line, or null
|
|
196
|
-
* when nothing refutes it (a person's acceptance settles it). The result page
|
|
197
|
-
* shows it before Request changes, its one ink act. */
|
|
198
|
-
export declare function cantAcceptYetOf(verdict: ProofVerdict | null, reasons: readonly string[], accepted: boolean): string | null;
|
|
199
187
|
/** The result's headline while its report disagrees with its saved changes. */
|
|
200
188
|
export declare const MISMATCH_HEADLINE = "The report doesn't match the changes";
|
|
201
189
|
/** One way the report disagrees with the saved changes, in plain words: the
|
package/dist/workspace-ui.js
CHANGED
|
@@ -16,10 +16,11 @@
|
|
|
16
16
|
* observed" name observed publication records, and deployment is never
|
|
17
17
|
* claimed without one.
|
|
18
18
|
*/
|
|
19
|
-
import { ACCEPT_NEEDS_REASON } from "./result-acts.js";
|
|
20
|
-
export { ACCEPT_NEEDS_REASON };
|
|
19
|
+
import { ACCEPT_NEEDS_REASON, FAILED_CHECK, cantAcceptYetOf, evidenceProblemOf } from "./result-acts.js";
|
|
20
|
+
export { ACCEPT_NEEDS_REASON, cantAcceptYetOf, evidenceProblemOf };
|
|
21
21
|
import { GOAL_ASSESSMENT_PENDING, manualReviewOnly, plainReasonWords } from "./proof.js";
|
|
22
22
|
import { OPEN_RESULT, plainReasonOf, stageOfCode, taskStatusOf, workToneOf } from "./task-status.js";
|
|
23
|
+
import { whenUtc } from "./when-html.js";
|
|
23
24
|
export const WORK_VIEWS = [
|
|
24
25
|
{ key: "all", label: "All", hint: "Every task in view, most urgent first.", empty: "Nothing is in progress. Describe work in chat or add a task, and it appears here." },
|
|
25
26
|
{ key: "needs-you", label: "Needs you", hint: "Tasks waiting on an answer, approval, or your inspection.", empty: "Nothing needs you right now. Queued and running work continues on its own." },
|
|
@@ -69,14 +70,6 @@ export function reviewStatusOf(review) {
|
|
|
69
70
|
return null;
|
|
70
71
|
return { token: "reviewing", label: "Reviewing", detail: "An automatic review is reading this result before it reaches you.", tone: "live", action: { label: OPEN_RESULT, kind: "open-result" } };
|
|
71
72
|
}
|
|
72
|
-
const FAILED_CHECK = /^the repository's approved verification command exited (-?[0-9]+)/;
|
|
73
|
-
export function evidenceProblemOf(verdict, reasons) {
|
|
74
|
-
if (verdict === "verified" || verdict === "attested")
|
|
75
|
-
return "none";
|
|
76
|
-
if (verdict === "refuted")
|
|
77
|
-
return reasons.some(reason => FAILED_CHECK.test(reason)) ? "checks-failed" : "mismatched";
|
|
78
|
-
return "missing";
|
|
79
|
-
}
|
|
80
73
|
/** The exit code of the failed check, when the recorded reason names one. */
|
|
81
74
|
export function failedCheckExit(reasons) {
|
|
82
75
|
for (const reason of reasons) {
|
|
@@ -314,17 +307,6 @@ const VERIFIED_RESULT = new Set(["ready-to-review", "pr-opened", "merge-observed
|
|
|
314
307
|
/** A finished result's display status in the shared headline's words: a
|
|
315
308
|
* failed project check is Failed, a result missing its record needs a
|
|
316
309
|
* person, every other saved result is Ready for review. Token and action stay. */
|
|
317
|
-
/** Why a result can't be accepted as it stands, in one plain line, or null
|
|
318
|
-
* when nothing refutes it (a person's acceptance settles it). The result page
|
|
319
|
-
* shows it before Request changes, its one ink act. */
|
|
320
|
-
export function cantAcceptYetOf(verdict, reasons, accepted) {
|
|
321
|
-
if (accepted || verdict !== "refuted")
|
|
322
|
-
return null;
|
|
323
|
-
// A mismatch is the status card's own headline; here only what accepting takes.
|
|
324
|
-
return evidenceProblemOf(verdict, reasons) === "checks-failed"
|
|
325
|
-
? "Can't accept yet: the project's check failed on these changes."
|
|
326
|
-
: ACCEPT_NEEDS_REASON;
|
|
327
|
-
}
|
|
328
310
|
/** The result's headline while its report disagrees with its saved changes. */
|
|
329
311
|
export const MISMATCH_HEADLINE = "The report doesn't match the changes";
|
|
330
312
|
const OVERCLAIMED = /^claimed changed paths? not in the sealed diff: ([\s\S]+)$/;
|
|
@@ -548,7 +530,7 @@ export function learningHtml(view, csrf, canManage, source) {
|
|
|
548
530
|
const cards = lessons.map(l => `<article class="card" data-lesson="${l.id}"><p><strong>${e(l.payload.observation)}</strong></p><p>${e(l.payload.action)}</p><p class="meta">${l.payload.kind === 'system' ? 'System suggestion · No change applied' : e(l.status === 'adopted' ? 'Adopted advice' : l.status === 'disabled' ? 'Disabled' : 'Proposed lesson')}</p><details><summary>Source and use</summary><p><a href="/r/${l.source}">Result #${l.source}</a> · <a href="/r/${l.reviewer}">Review #${l.reviewer}</a></p><p>${e(l.payload.paths.join(', '))} · ${e(l.payload.phases.join(', '))} · ${e({ darwin: "macOS", win32: "Windows", linux: "Linux" }[l.payload.platform] ?? l.payload.platform)}</p><ul>${evidence(l.payload.evidence, l.source)}</ul>${l.payload.kind !== 'project' ? '' : `<p class="meta">${l.status === 'proposed' ? 'Save permits advisory reuse when enabled and applicable. It does not prove a remedy works.' : 'Disabling affects future runs. Active snapshots, code and approvals stay unchanged.'}</p>${l.status === 'proposed' ? act('adopt', 'Save lesson', l) : ''}`}</details>${l.payload.kind === 'project' && l.status === 'adopted' ? act('disable', 'Disable lesson', l) : ''}</article>`).join('');
|
|
549
531
|
if (source !== undefined)
|
|
550
532
|
return lessons.length ? `<details class="learning result-learning"><summary>Learned from this task</summary>${cards}<a href="/settings/learning?repo=${encodeURIComponent(view.repo)}">Learning settings</a></details>` : '';
|
|
551
|
-
const history = view.events.map(ev => `<article class="card" data-learning-event="${e(ev.action)}"><p><strong>${e({ assessment: { propose: 'Learning suggested', none: 'No lesson needed', unassessed: 'Learning not assessed', invalid: 'Learning assessment invalid' }[ev.after] ?? 'Learning not assessed', proposal: 'Suggestion recorded', adopt: 'Lesson adopted', disable: 'Lesson disabled', reset: 'Learning reset', enable: 'Reuse enabled', pause: 'Reuse paused', reuse: 'Run context saved', failure: 'Learning issue', capture: 'Capture finished' }[ev.action] ?? ev.action)}</strong>${ev.action === "reuse" ? ` · ${e(ev.after)}` : ""}</p><p class="meta"
|
|
533
|
+
const history = view.events.map(ev => `<article class="card" data-learning-event="${e(ev.action)}"><p><strong>${e({ assessment: { propose: 'Learning suggested', none: 'No lesson needed', unassessed: 'Learning not assessed', invalid: 'Learning assessment invalid' }[ev.after] ?? 'Learning not assessed', proposal: 'Suggestion recorded', adopt: 'Lesson adopted', disable: 'Lesson disabled', reset: 'Learning reset', enable: 'Reuse enabled', pause: 'Reuse paused', reuse: 'Run context saved', failure: 'Learning issue', capture: 'Capture finished' }[ev.action] ?? ev.action)}</strong>${ev.action === "reuse" ? ` · ${e(ev.after)}` : ""}</p><p class="meta">${whenUtc(ev.at)} · ${e(ev.actor)}${ev.run === null ? '' : ` · <a href="/r/${ev.run}">Run #${ev.run}</a>${ev.outcome === null ? "" : ` · ${e(ev.outcome)}`}`}</p><details><summary>Details</summary><p>${e(ev.before)} → ${e(ev.after)}</p><p>${e(ev.reason)}</p>${ev.lesson === null ? '' : `<p>Lesson #${ev.lesson}</p>`}<ul>${evidence(ev.evidence, ev.run)}</ul>${ev.snapshot === null ? "" : `<details><summary>Exact context</summary><pre>${e(ev.snapshot)}</pre></details>`}</details></article>`).join('');
|
|
552
534
|
return `<section class="learning">${view.damaged ? '<p class="problem" role="alert">Some lessons no longer verify and are excluded from reuse.</p>' : ''}<p>${view.enabled ? 'Use adopted lessons: on' : 'Use adopted lessons: off'}</p>${act(view.enabled ? 'pause' : 'enable', view.enabled ? 'Pause reuse' : 'Enable reuse')}${cards || '<p>No lessons yet. Reviews may suggest useful advice here.</p>'}<details><summary>Reset learning</summary><p>Disable all adopted lessons and future reuse. Keep history, code, approvals and active run snapshots.</p>${act('reset', 'Reset learning')}</details><h2>Changes</h2>${history || '<p>No learning changes yet.</p>'}${view.next === null ? '' : `<a class="button-link" href="/settings/learning?repo=${encodeURIComponent(view.repo)}&before=${view.next}">Older changes</a>`}</section>`;
|
|
553
535
|
}
|
|
554
536
|
/** The first signed requirement a finished attempt missed (failed or unanswered; a person's own check is not a miss),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolroll",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.17",
|
|
4
4
|
"description": "A control plane for unattended coding agents — queue tasks, walk away, come back to pull requests. Agents build in leased worktrees, never touch your default branch, and interrupt you only for decisions that need a human.",
|
|
5
5
|
"directories": {
|
|
6
6
|
"doc": "docs"
|