toolroll 0.9.16 → 0.9.18
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 +28 -0
- package/dist/assignment-brief.js +11 -4
- package/dist/browser/workspace.js +14 -14
- package/dist/browser-workspace.d.ts +2 -0
- package/dist/chat-actions.d.ts +7 -0
- package/dist/chat-actions.js +40 -4
- package/dist/chat-ask.js +29 -15
- package/dist/chat-channel.js +3 -1
- package/dist/chat-decide.d.ts +203 -0
- package/dist/chat-decide.js +554 -0
- package/dist/chat-delivery-state.d.ts +7 -0
- package/dist/chat-delivery.d.ts +3 -0
- package/dist/chat-delivery.js +63 -17
- package/dist/chat-quiet.d.ts +4 -1
- package/dist/chat-quiet.js +6 -2
- package/dist/chat-warmth.d.ts +21 -0
- package/dist/chat-warmth.js +36 -0
- package/dist/desktop-update.d.ts +1 -1
- package/dist/discord-api.d.ts +3 -1
- package/dist/discord-api.js +4 -2
- package/dist/discord-chat.d.ts +3 -2
- package/dist/discord-chat.js +35 -6
- 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.js +13 -7
- package/dist/lead-context.d.ts +5 -3
- package/dist/lead-context.js +29 -7
- package/dist/lead-follow.d.ts +1 -1
- package/dist/lead-follow.js +3 -2
- package/dist/lead-identity.js +1 -1
- package/dist/lead-people.d.ts +55 -0
- package/dist/lead-people.js +128 -0
- package/dist/lead-voice.d.ts +3 -2
- package/dist/lead-voice.js +4 -3
- package/dist/mate-cli.d.ts +3 -0
- package/dist/mate-cli.js +15 -14
- package/dist/mate-contract.d.ts +1 -1
- package/dist/mate-contract.js +6 -4
- package/dist/mate-progress.js +2 -0
- package/dist/mate-tools.d.ts +8 -0
- package/dist/mate-tools.js +112 -6
- package/dist/mate.d.ts +26 -1
- package/dist/mate.js +89 -25
- package/dist/modes.d.ts +6 -0
- package/dist/modes.js +6 -0
- package/dist/operate.js +20 -8
- package/dist/phase-routing.d.ts +1 -1
- package/dist/project-delete.js +3 -0
- 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-shots.d.ts +51 -0
- package/dist/result-shots.js +119 -0
- package/dist/serve.js +76 -49
- package/dist/slack-chat.d.ts +2 -0
- package/dist/slack-chat.js +38 -7
- package/dist/store.d.ts +36 -3
- package/dist/store.js +163 -16
- package/dist/team-runtime.js +1 -1
- package/dist/teams-chat.js +17 -1
- package/dist/telegram-mate.d.ts +12 -0
- package/dist/telegram-mate.js +27 -11
- package/dist/telegram-team.js +6 -3
- package/dist/telegram.d.ts +5 -3
- package/dist/telegram.js +227 -21
- package/dist/test-temp.js +1 -1
- package/package.json +1 -1
package/dist/serve.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { checkLeadIdentity, leadIdentityOf, leadNameOf, LEAD_NAME_MAX, LEAD_PERSONA_MAX } from "./lead-identity.js";
|
|
2
|
+
import { aboutYouOf, checkAboutYou, saveAboutYou, ABOUT_YOU_LINE_MAX, ABOUT_YOU_MAX_LINES } from "./lead-about.js";
|
|
2
3
|
import { withActor } from "./actor.js";
|
|
3
4
|
import { repositoryContext, repositoryContextRead } from './repository-context.js';
|
|
4
5
|
import { ADD_TESTS_ACTION, followUpChecksOf, requestFollowUpChecks, fileAddTestsTask, resultCheckLevel } from './result-follow-ups.js';
|
|
@@ -20,7 +21,7 @@ import { ChatState } from "./chat-delivery-state.js";
|
|
|
20
21
|
import { slackSettingsHtml } from "./slack-settings.js";
|
|
21
22
|
import { checkSlackCredentials, loadSlackCredentials, saveSlackCredentials, clearSlackCredentials, SLACK_MANIFEST, SlackError } from "./slack-api.js";
|
|
22
23
|
import { SlackState } from "./slack-state.js";
|
|
23
|
-
import { CHAT_ACTIONS, sharedActionPayload, sharedActionNeedsReview, sharedActionReviewPath, mintSharedActionReview } from './chat-actions.js';
|
|
24
|
+
import { CHAT_ACTIONS, OWNER_ACTIONS, sharedActionPayload, sharedActionNeedsReview, sharedActionReviewPath, mintSharedActionReview } from './chat-actions.js';
|
|
24
25
|
import { changeSkills, githubSkill, importSkill, readSkillsSnapshot, reviseSkillTest, skillTestResult, skillsView, testSkill } from "./project-skills.js";
|
|
25
26
|
import { skillsHtml, skillsScript, skillsSnapshotHtml, skillTestFeedbackHtml, SKILLS_CSS } from "./skills-ui.js";
|
|
26
27
|
import { toolsHtml, TOOLS_CSS } from "./tools-ui.js";
|
|
@@ -92,6 +93,7 @@ import { DISCLOSURE_CSS } from "./disclosure.js";
|
|
|
92
93
|
import { styleAsset } from "./style-asset.js";
|
|
93
94
|
import { MOBILE_VIEWPORT_SCRIPT } from "./mobile-viewport.js";
|
|
94
95
|
import { authorizePlanUnderMode, applyModeToNewFiling, planAutoPending } from "./plan-auto.js";
|
|
96
|
+
import { permissionPlainWords } from "./chat-decide.js";
|
|
95
97
|
import { LEDGER_CSV_HEADER, ledgerCsvRows } from "./ledger-csv.js";
|
|
96
98
|
import { Readable } from "node:stream";
|
|
97
99
|
import { pipeline } from "node:stream/promises";
|
|
@@ -187,7 +189,8 @@ import { dirname } from "node:path";
|
|
|
187
189
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
188
190
|
import { loadOrCreateVapidKeys, validatePushEndpoint } from "./push.js";
|
|
189
191
|
import { parseGithubRepo, previewGithubRepo, cloneGithubRepo, listGithubRepos, isLargeRepo } from "./onboard.js";
|
|
190
|
-
import { verifiedAuthor, LEAD_THREAD, isDigestTime, MATE_ASK_OTHER } from "./store.js";
|
|
192
|
+
import { verifiedAuthor, LEAD_THREAD, isDigestTime, RESULT_SCREENSHOTS, MATE_ASK_OTHER } from "./store.js";
|
|
193
|
+
import { RESULT_SHOT_CHOICES } from "./result-shots.js";
|
|
191
194
|
import { digestTimes } from "./digest-times.js";
|
|
192
195
|
import { updateRepos, addRepos, removeRepos } from "./repos.js";
|
|
193
196
|
import { run as execRun } from "./exec.js";
|
|
@@ -212,7 +215,7 @@ import { authorizedProject, canonicalProject, isGitRepo, projectName, sameRepo,
|
|
|
212
215
|
import { tally, spendLine, runCostWords } from "./summary.js";
|
|
213
216
|
import { classify, holdOwnerWords, attentionCardForUnverifiedDone } from "./board.js";
|
|
214
217
|
import { approveRoutine, describeSchedule, fireRoutine, parseSchedule, refreshRoutineAgents, routineAgentsState, routineDigestOf, validateRoutineTerms, ROUTINE_NAME } from "./routine.js";
|
|
215
|
-
import { effectivePrimary, isMessagingChannel, savePrimary } from "./webhooks.js";
|
|
218
|
+
import { effectivePrimary, isMessagingChannel, loadConsoleUrl, savePrimary } from "./webhooks.js";
|
|
216
219
|
import { resolvePhaseAgent, resolveRoutineAuthority, INSTALLATION_SCOPE, routeOfTask, agentChoicesFor } from "./agentconfig.js";
|
|
217
220
|
import { isRiskLevel, projectRoute, riskTitle, riskConsequence, chosenWords, agentsSummary, postureWords, RISK_CHOICES, RISK_LEVELS, PHASES as ROUTE_PHASES } from "./phase-routing.js";
|
|
218
221
|
import { ALL_CREDENTIAL_ENV, isProviderId, reportsCost, PROVIDER_IDS, validModelId, validateSpec } from "./provider.js";
|
|
@@ -260,6 +263,7 @@ import { handleTeamsHttp } from "./teams.js";
|
|
|
260
263
|
import { checkTeamsCredentials, clearTeamsCredentials, loadTeamsCredentials, saveTeamsCredentials, TeamsError } from "./teams-api.js";
|
|
261
264
|
import { verifyApproverByPassword, verifyApproverStanding } from "./principal.js";
|
|
262
265
|
import { runMateTurn, MATE_MESSAGE_MAX_CHARS } from "./mate.js";
|
|
266
|
+
import { replyHtmlInline, shapeReply } from "./reply-shape.js";
|
|
263
267
|
import { confirmCoordinatorProposal, confirmMateProposal, dismissCoordinatorProposal, dismissMateProposal } from "./mate-doors.js";
|
|
264
268
|
import { envValue } from "./names.js";
|
|
265
269
|
import { cachedRelease, isNewer, runnerVersions, setUpdateChecks, updateChecksOff } from "./releases.js";
|
|
@@ -1529,6 +1533,8 @@ export function createDecisionServer(options) {
|
|
|
1529
1533
|
actor: who.name,
|
|
1530
1534
|
csrf: who.via === "cookie" ? who.session.csrf : "",
|
|
1531
1535
|
returnTo: safeReturn(url.pathname + url.search),
|
|
1536
|
+
// This console's own addresses: a lead reply's links there read as "the task", "the result", "Settings → Lead".
|
|
1537
|
+
appOrigins: [consoleOrigin(request.headers.host), publicOrigin?.origin ?? null, options.configDir === undefined ? null : loadConsoleUrl(process.env, options.configDir)],
|
|
1532
1538
|
// Every signed-in page shares the workspace shell; pages showing a one-time secret opt out (forceSensitive).
|
|
1533
1539
|
browser: who.via === 'cookie',
|
|
1534
1540
|
// v100: signed in with the identity provider: its label, and whether it checked them recently enough to stand in for a password.
|
|
@@ -1634,7 +1640,7 @@ export function createDecisionServer(options) {
|
|
|
1634
1640
|
}
|
|
1635
1641
|
if (admitted && proposal) {
|
|
1636
1642
|
const taskId = typeof action?.request['task'] === 'string' ? action.request['task'] : typeof proposal.payload['task'] === 'string' ? proposal.payload['task'] : null;
|
|
1637
|
-
const repo = action?.repo ?? (taskId ? store.lookupRef(taskId)?.repo : null) ?? (typeof proposal.payload['repo'] === 'string' ? proposal.payload['repo'] : null);
|
|
1643
|
+
const repo = (action?.repo || null) ?? (taskId ? store.lookupRef(taskId)?.repo : null) ?? (typeof proposal.payload['repo'] === 'string' ? proposal.payload['repo'] : null);
|
|
1638
1644
|
return { repo, taskId, runId: typeof action?.request['run'] === 'number' ? action.request['run'] : null, action: action?.operation ?? `chat ${proposal.kind}` };
|
|
1639
1645
|
}
|
|
1640
1646
|
}
|
|
@@ -1699,6 +1705,9 @@ export function createDecisionServer(options) {
|
|
|
1699
1705
|
}
|
|
1700
1706
|
if (action && proposal && store.getMateThread(proposal.thread)?.approver === who.name && visible(action.repo) && store.accountCanAccess(who.name, action.repo))
|
|
1701
1707
|
return true;
|
|
1708
|
+
// A card about the person themselves (what their lead knows about them) is in no project.
|
|
1709
|
+
if (action && proposal && action.repo === '' && OWNER_ACTIONS.has(action.operation) && store.getMateThread(proposal.thread)?.approver === who.name)
|
|
1710
|
+
return true;
|
|
1702
1711
|
refuse(response, who, 404, 'No such action in your projects.', '/projects');
|
|
1703
1712
|
return false;
|
|
1704
1713
|
}
|
|
@@ -2641,6 +2650,10 @@ export function createDecisionServer(options) {
|
|
|
2641
2650
|
`<option value="0">wait for my approval</option>` +
|
|
2642
2651
|
`<option value="1">auto-approve plans that preserve my filed contract</option>` +
|
|
2643
2652
|
`</select><span class="meta">Requires automatic filing approval. File the goal, paths, and acceptance criteria upfront. Changed scope and unanswered questions still pause.</span></label>`,
|
|
2653
|
+
`<label>From my chat app<select name="chat-approve">` +
|
|
2654
|
+
`<option value="">approve plans and merges here, with my password (the default)</option>` +
|
|
2655
|
+
`<option value="1">approve plans and merge ready pull requests from my paired chat, two taps each</option>` +
|
|
2656
|
+
`</select><span class="meta">Plans that widen permissions, exceed the attempt cap or touch protected paths still open Toolroll.</span></label>`,
|
|
2644
2657
|
`<label>If a subscription runs out<select name="allow-paid-fallback">` +
|
|
2645
2658
|
`<option value="">never switch to a paid API key on its own (the default, every preset)</option>` +
|
|
2646
2659
|
`<option value="1">allow the approved fallback — spend moves to that account</option>` +
|
|
@@ -4138,7 +4151,8 @@ export function createDecisionServer(options) {
|
|
|
4138
4151
|
const signedIn = localSignIn?.states.claude === "connected" ? "Claude Code" : localSignIn?.states.codex === "connected" ? "Codex" : null;
|
|
4139
4152
|
const promises = openCommitments(store, who.name, 50).map(one => ({ id: one.id, what: one.what, when: conditionWords(store, one.condition), until: one.expiresAt }));
|
|
4140
4153
|
return sendScreen(response, 200, screen("Lead", leadSettingsHtml({ config, facts, words: leadWords(), signedIn, command: agentSignInCommand(),
|
|
4141
|
-
said: url.searchParams.get("said"), saved: url.searchParams.get("saved") === "1", identity: leadIdentityOf(store, who.name), promises
|
|
4154
|
+
said: url.searchParams.get("said"), saved: url.searchParams.get("saved") === "1", identity: leadIdentityOf(store, who.name), promises,
|
|
4155
|
+
about: aboutYouOf(store, who.name), aboutSaved: url.searchParams.get("saved") === "about" }), { chrome: chromeFor(project, "settings") }));
|
|
4142
4156
|
}
|
|
4143
4157
|
if (url.pathname === "/settings/telegram") {
|
|
4144
4158
|
// Any approver pairs their OWN phone here; the bot token stays on /settings.
|
|
@@ -4223,7 +4237,7 @@ export function createDecisionServer(options) {
|
|
|
4223
4237
|
return at === null || since === null ? null : firstResultWords(since, at);
|
|
4224
4238
|
})(), (() => {
|
|
4225
4239
|
const chosen = store.notificationPreference(who.name);
|
|
4226
|
-
return { mode: chosen.mode, digestAt: chosen.digestAt, projects: notificationProjects(store, who.name) };
|
|
4240
|
+
return { mode: chosen.mode, digestAt: chosen.digestAt, screenshots: chosen.screenshots, projects: notificationProjects(store, who.name) };
|
|
4227
4241
|
})()));
|
|
4228
4242
|
}
|
|
4229
4243
|
if (url.pathname === "/push/key") {
|
|
@@ -4818,20 +4832,20 @@ export function createDecisionServer(options) {
|
|
|
4818
4832
|
if (result.problem.startsWith("status-")) {
|
|
4819
4833
|
// The provider ANSWERED with an error: nothing billed.
|
|
4820
4834
|
finish({ state: "failed", failureReason: "provider-error", settledMicrousd: 0 });
|
|
4821
|
-
tell(null, "
|
|
4835
|
+
tell(null, "The chat provider turned the request down. Nothing was kept or charged. Try again in a minute.");
|
|
4822
4836
|
}
|
|
4823
4837
|
else if (result.problem === "timeout") {
|
|
4824
4838
|
finish({ state: "failed", failureReason: "timeout", settledMicrousd: null, unknownSpend: true });
|
|
4825
|
-
tell(null, "
|
|
4839
|
+
tell(null, "The reply took too long and was stopped, and its cost isn't known yet, so chat is paused. Confirm the cost on this page to turn chat back on.");
|
|
4826
4840
|
}
|
|
4827
4841
|
else if (result.problem === "network") {
|
|
4828
4842
|
finish({ state: "failed", failureReason: "provider-error", settledMicrousd: null, unknownSpend: true });
|
|
4829
|
-
tell(null, "
|
|
4843
|
+
tell(null, "The chat provider stopped responding partway through, and the cost isn't known, so chat is paused. Confirm the cost on this page to turn chat back on.");
|
|
4830
4844
|
}
|
|
4831
4845
|
else {
|
|
4832
4846
|
// 200 with an unusable wrapper: billed, amount unproven.
|
|
4833
4847
|
finish({ state: "failed", failureReason: "malformed-reply", settledMicrousd: null, unknownSpend: true });
|
|
4834
|
-
tell(null, "
|
|
4848
|
+
tell(null, "The chat provider sent back an answer that couldn't be read, and its cost isn't known, so chat is paused. Confirm the cost on this page to turn chat back on.");
|
|
4835
4849
|
}
|
|
4836
4850
|
return;
|
|
4837
4851
|
}
|
|
@@ -8228,16 +8242,21 @@ export function createDecisionServer(options) {
|
|
|
8228
8242
|
}
|
|
8229
8243
|
if (url.pathname === "/settings/notifications") {
|
|
8230
8244
|
// Each person's own choice: a closed list of modes and a 24-hour HH:MM, or off.
|
|
8231
|
-
const mode = body.get("mode"), time = (body.get("digest") ?? "").trim();
|
|
8245
|
+
const mode = body.get("mode"), time = (body.get("digest") ?? "").trim(), shots = body.get("screenshots");
|
|
8232
8246
|
if (mode !== null && mode !== "quiet" && mode !== "all")
|
|
8233
8247
|
return refuse(response, who, 400, "choose Only when I'm needed or Every step", "/settings");
|
|
8234
8248
|
if (time !== "" && time !== "off" && !isDigestTime(time))
|
|
8235
8249
|
return refuse(response, who, 400, "the evening digest time is HH:MM, or off", "/settings");
|
|
8250
|
+
if (shots !== null && !RESULT_SCREENSHOTS.includes(shots))
|
|
8251
|
+
return refuse(response, who, 400, "choose Off, First one or Up to 4", "/settings");
|
|
8236
8252
|
const before = store.notificationPreference(who.name);
|
|
8237
|
-
const after = store.setNotificationPreference(who.name, { ...(mode === null ? {} : { mode }), ...(time === "" ? {} : { digestAt: time === "off" ? null : time })
|
|
8253
|
+
const after = store.setNotificationPreference(who.name, { ...(mode === null ? {} : { mode }), ...(time === "" ? {} : { digestAt: time === "off" ? null : time }),
|
|
8254
|
+
...(shots === null ? {} : { screenshots: shots }) }, who.name, now);
|
|
8238
8255
|
const said = after.mode !== before.mode
|
|
8239
8256
|
? after.mode === "quiet" ? "Chats now message you only when you're needed." : "Chats now message you at every step."
|
|
8240
|
-
: after.
|
|
8257
|
+
: after.screenshots !== before.screenshots
|
|
8258
|
+
? after.screenshots === "off" ? "Results arrive without screenshots." : `Results arrive with ${after.screenshots === "first" ? "their first screenshot" : "up to 4 screenshots"}.`
|
|
8259
|
+
: after.digestAt === null ? "Evening digest off." : `Evening digest at ${after.digestAt}.`;
|
|
8241
8260
|
return redirect(response, `/settings?said=${encodeURIComponent(said)}`);
|
|
8242
8261
|
}
|
|
8243
8262
|
if (url.pathname === "/settings/notifications/mute") {
|
|
@@ -8704,6 +8723,8 @@ export function createDecisionServer(options) {
|
|
|
8704
8723
|
// attempt cap it carries is meaningless without it.
|
|
8705
8724
|
repairAuto: false,
|
|
8706
8725
|
repairMaxAttempts: 0,
|
|
8726
|
+
// Approving from the paired chat: only the explicit choice grants it.
|
|
8727
|
+
chatApprove: body.get("chat-approve") === "1",
|
|
8707
8728
|
publication: body.get("publication") === "automerge" ? "automerge" : "notify",
|
|
8708
8729
|
};
|
|
8709
8730
|
if (["review-auto", "review-retry-auto", "repair-auto"].some(field => body.get(field) === "1")) {
|
|
@@ -8725,7 +8746,7 @@ export function createDecisionServer(options) {
|
|
|
8725
8746
|
`<input type="hidden" name="csrf" value="${escape(who.session.csrf)}">` +
|
|
8726
8747
|
`<input type="hidden" name="nonce" value="${escape(nonce)}">` +
|
|
8727
8748
|
`<input type="hidden" name="digest" value="${escape(digest)}">` +
|
|
8728
|
-
["name", "days", "publication", "auto-approve", "plan-auto", "allow-paid-fallback"]
|
|
8749
|
+
["name", "days", "publication", "auto-approve", "plan-auto", "chat-approve", "allow-paid-fallback"]
|
|
8729
8750
|
.map(field => `<input type="hidden" name="${field}" value="${escape(body.get(field) ?? "")}">`)
|
|
8730
8751
|
.join("") +
|
|
8731
8752
|
`<input type="hidden" name="expiry" value="${escape(expiry)}">` +
|
|
@@ -9231,6 +9252,16 @@ export function createDecisionServer(options) {
|
|
|
9231
9252
|
store.setLeadConfig(who.name, checked.identity.name, checked.identity.persona, now);
|
|
9232
9253
|
return redirect(response, "/settings/lead?saved=1");
|
|
9233
9254
|
}
|
|
9255
|
+
// Settings → Lead: what this person's lead knows about them, one line each.
|
|
9256
|
+
if (url.pathname === "/settings/lead/about") {
|
|
9257
|
+
if (who.via !== "cookie" || who.role !== "approver")
|
|
9258
|
+
return refuse(response, who, 403, "An approver edits what their lead knows about them.", "/settings/lead");
|
|
9259
|
+
const checked = checkAboutYou(body.get("about") ?? "");
|
|
9260
|
+
if (!checked.ok)
|
|
9261
|
+
return redirect(response, chatReturnWithSaid("/settings/lead", checked.message));
|
|
9262
|
+
saveAboutYou(store, who.name, checked.lines, now);
|
|
9263
|
+
return redirect(response, "/settings/lead?saved=about");
|
|
9264
|
+
}
|
|
9234
9265
|
// Settings → Lead: stop the lead following up on one of your promises.
|
|
9235
9266
|
if (url.pathname === "/settings/lead/promise/cancel") {
|
|
9236
9267
|
if (who.via !== "cookie" || who.role !== "approver")
|
|
@@ -12525,7 +12556,7 @@ function approvalSheetHtml(input) {
|
|
|
12525
12556
|
// What the yes allows, in plain words, right above it: the agent's
|
|
12526
12557
|
// permissions, the per-attempt limit, and an earlier version still running.
|
|
12527
12558
|
const allowing = [];
|
|
12528
|
-
const allowed =
|
|
12559
|
+
const allowed = permissionPlainWords(scope.profile);
|
|
12529
12560
|
if (allowed !== null)
|
|
12530
12561
|
allowing.push(allowed);
|
|
12531
12562
|
if (raceTerms === null)
|
|
@@ -12676,20 +12707,6 @@ class PermissionsWouldChange extends Error {
|
|
|
12676
12707
|
}
|
|
12677
12708
|
/** Evidence kinds in plain words, for the signed criteria in Details. */
|
|
12678
12709
|
const EVIDENCE_PLAIN = { check: "the project check", screenshot: "screenshots", "changed-path": "changed files", "manual-review": "your own check" };
|
|
12679
|
-
/** What an agent may do on its own, in a few plain words. */
|
|
12680
|
-
function permissionPlainWordsOf(profile) {
|
|
12681
|
-
if (profile === null || profile === undefined)
|
|
12682
|
-
return null;
|
|
12683
|
-
const full = "full access, nothing asks first";
|
|
12684
|
-
if (profile.provider === "claude") {
|
|
12685
|
-
return profile.permissionArgv === "bypassPermissions" ? full
|
|
12686
|
-
: profile.permissionArgv === "auto" ? "file edits and routine commands; anything risky stops"
|
|
12687
|
-
: "file edits only; commands are refused";
|
|
12688
|
-
}
|
|
12689
|
-
if (profile.provider === "gemini")
|
|
12690
|
-
return profile.approvalArgv === "yolo" ? full : "file edits only; other tools are refused";
|
|
12691
|
-
return profile.sandboxMode === "danger-full-access" ? full : "file edits and commands inside the project only";
|
|
12692
|
-
}
|
|
12693
12710
|
/** The permission choice a sealed profile carries, as the scope form names it. */
|
|
12694
12711
|
function permissionModeOfProfile(profile) {
|
|
12695
12712
|
if (profile === null || profile === undefined)
|
|
@@ -17477,13 +17494,13 @@ function chatHeading(copy, projectCount, showProjectToggle = true) {
|
|
|
17477
17494
|
`${strokeIcon(`<path d="M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2Z"/>`)}<span>projects</span><span class="badge">${projectCount}</span></button>`
|
|
17478
17495
|
: "") + `</div></div>`);
|
|
17479
17496
|
}
|
|
17480
|
-
/** A deliberately small rich-text grammar for model copy.
|
|
17481
|
-
*
|
|
17482
|
-
*
|
|
17483
|
-
|
|
17484
|
-
|
|
17485
|
-
|
|
17486
|
-
|
|
17497
|
+
/** A deliberately small rich-text grammar for model copy. The reply is shaped first (the lead's voice, enforced:
|
|
17498
|
+
* no headers, at most three bold anchors, labelled links, no internal ids unless `asked` wanted them); input is
|
|
17499
|
+
* escaped before tags are introduced: bullets, numbered steps, bold, inline code and labelled http(s) links are
|
|
17500
|
+
* presentation only—never executable HTML. */
|
|
17501
|
+
function renderChatText(raw, asked) {
|
|
17502
|
+
const inline = replyHtmlInline;
|
|
17503
|
+
const text = shapeReply(raw, { appOrigin: requestContext.getStore()?.appOrigins ?? null, ...(asked === undefined ? {} : { asked }) });
|
|
17487
17504
|
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
|
17488
17505
|
const out = [];
|
|
17489
17506
|
let paragraph = [];
|
|
@@ -17501,15 +17518,9 @@ function renderChatText(text) {
|
|
|
17501
17518
|
list = null;
|
|
17502
17519
|
};
|
|
17503
17520
|
for (const line of lines) {
|
|
17504
|
-
const heading = /^(?:#{1,3}\s+)(.+)$/.exec(line);
|
|
17505
17521
|
const bullet = /^\s*[-*]\s+(.+)$/.exec(line);
|
|
17506
17522
|
const numbered = /^\s*(\d{1,9})[.)]\s+(.+)$/.exec(line);
|
|
17507
|
-
if (
|
|
17508
|
-
flushParagraph();
|
|
17509
|
-
closeList();
|
|
17510
|
-
out.push(`<h3>${inline(heading[1] ?? "")}</h3>`);
|
|
17511
|
-
}
|
|
17512
|
-
else if (bullet !== null || numbered !== null) {
|
|
17523
|
+
if (bullet !== null || numbered !== null) {
|
|
17513
17524
|
flushParagraph();
|
|
17514
17525
|
const wanted = bullet !== null ? "ul" : "ol";
|
|
17515
17526
|
if (list !== wanted) {
|
|
@@ -17598,6 +17609,12 @@ function leadSettingsHtml(data) {
|
|
|
17598
17609
|
`<label>Name your lead<input name="name" value="${escape(data.identity.name)}" maxlength="${LEAD_NAME_MAX}" autocomplete="off" required></label>` +
|
|
17599
17610
|
`<label>Persona<textarea name="persona" rows="4" maxlength="${LEAD_PERSONA_MAX}">${escape(data.identity.persona)}</textarea></label>` +
|
|
17600
17611
|
`<button type="submit">Save</button>${data.saved === true ? ` <span class="meta" role="status">Saved.</span>` : ""}</form>`,
|
|
17612
|
+
// What the lead knows about this person: lines they confirmed in chat or wrote here. Every project, every chat.
|
|
17613
|
+
data.about === undefined ? "" : `<form method="post" action="/settings/lead/about" class="card lead-about" data-lead-about>` +
|
|
17614
|
+
`<input type="hidden" name="csrf" value="${escape(facts.csrf)}">` +
|
|
17615
|
+
`<label>What your lead knows about you<textarea name="about" rows="${Math.min(10, Math.max(4, data.about.length + 1))}" maxlength="${ABOUT_YOU_MAX_LINES * (ABOUT_YOU_LINE_MAX + 1)}" placeholder="Keep copy terse. I test changes myself.">${escape(data.about.join("\n"))}</textarea></label>` +
|
|
17616
|
+
`<p class="meta">One per line, up to ${ABOUT_YOU_MAX_LINES}.</p>` +
|
|
17617
|
+
`<button type="submit">Save</button>${data.aboutSaved === true ? ` <span class="meta" role="status">Saved.</span>` : ""}</form>`,
|
|
17601
17618
|
// What the lead promised to follow up on; it reports each once, here in chat, and drops it after 7 days.
|
|
17602
17619
|
(data.promises ?? []).length === 0 ? "" : `<section class="card lead-promises" data-lead-promises><h2>Promises</h2><ul class="lead-promise-list">${data.promises.map(one => `<li data-promise="${one.id}"><p>${escape(one.what)}</p><p class="meta">${escape(one.when.charAt(0).toUpperCase() + one.when.slice(1))} · <span class="nowrap">until ${escape(new Date(one.until).toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" }))}</span></p>` +
|
|
17603
17620
|
`<form method="post" action="/settings/lead/promise/cancel" class="inline">${hidden}<input type="hidden" name="promise" value="${one.id}"><button type="submit" class="secondary">Cancel</button></form></li>`).join("")}</ul></section>`,
|
|
@@ -17699,8 +17716,8 @@ function chatPage(chrome, data) {
|
|
|
17699
17716
|
parts.push(data.coordinatorProposals);
|
|
17700
17717
|
parts.push(data.focusTask === null && !data.catchUp ? `<details class="chat-fleet-context">${chatOverviewSummaryHtml(data.projects, data.fleetSnapshot?.attentionCount)}${chatFleetOverview(data.fleetSnapshot, data.projects, data.csrf, false)}</details>` : "", chatLimitsHtml({ provider: config.provider, model: config.model, turnsToday: data.turnsToday, dailyTurns: config.dailyTurns, subscription, weekly: subscription ? null : { spent: data.weeklySpent, ceiling: config.weeklyCeilingMicrousd } }));
|
|
17701
17718
|
for (const turn of data.latched) {
|
|
17702
|
-
parts.push(`<div class="problem"><strong>
|
|
17703
|
-
`<a href="/chat/ack/${turn.id}">
|
|
17719
|
+
parts.push(`<div class="problem"><strong>Chat is paused.</strong> An earlier reply stopped before its cost was known; it may have cost up to ${chatMoney(turn.reservedMicrousd)}. ` +
|
|
17720
|
+
`<a href="/chat/ack/${turn.id}">Confirm that cost</a> to turn chat back on.</div>`);
|
|
17704
17721
|
}
|
|
17705
17722
|
if (data.pending !== null) {
|
|
17706
17723
|
parts.push(chatWorkingHtml({ details: `Turn #${data.pending.id}${subscription ? " · subscription" : ` · up to ${chatMoney(data.pending.reservedMicrousd)} reserved`}` }));
|
|
@@ -18115,9 +18132,14 @@ function mateBrowserMessages(rows, csrf, back, target = { task: null, project: n
|
|
|
18115
18132
|
const ask = turn === null ? undefined : rows.asks?.get(turn);
|
|
18116
18133
|
return ask === undefined ? "" : mateAskHtml(ask, csrf, target, target.task === null ? "lead-message" : "task-message", ask.turn === open);
|
|
18117
18134
|
};
|
|
18135
|
+
// The owner's message each reply answers: when it asked for ids, the reply keeps them.
|
|
18136
|
+
const askedBy = new Map();
|
|
18137
|
+
rows.messages.reduce((last, message) => { if (message.role === 'operator')
|
|
18138
|
+
return message.text; if (last !== undefined)
|
|
18139
|
+
askedBy.set(message.id, last); return last; }, undefined);
|
|
18118
18140
|
return rows.messages.map(message => ({
|
|
18119
18141
|
id: message.id, role: message.role, text: message.text,
|
|
18120
|
-
html: message.role === 'operator' ? `<p>${escape(message.text)}</p>` : renderChatText(message.text) + asked(message.turn),
|
|
18142
|
+
html: message.role === 'operator' ? `<p>${escape(message.text)}</p>` : renderChatText(message.text, askedBy.get(message.id)) + asked(message.turn),
|
|
18121
18143
|
activity: message.activity, createdAt: message.createdAt,
|
|
18122
18144
|
...(() => {
|
|
18123
18145
|
const parts = message.turn === null ? [] : rows.proposals.filter(one => one.turn === message.turn)
|
|
@@ -18188,14 +18210,16 @@ function mateThreadHtml(data) {
|
|
|
18188
18210
|
parts.push(`<div class="chat-empty" data-key="empty"><strong>${data.focusTask === null ? "What do you want to get done?" : "What do you want to understand or change?"}</strong>` +
|
|
18189
18211
|
`<p class="meta">${data.focusTask === null ? "Describe a task or ask about your projects." : "Ask about progress, review results, or adjust the plan."}</p></div>`);
|
|
18190
18212
|
}
|
|
18213
|
+
let asked;
|
|
18191
18214
|
for (const message of data.messages) {
|
|
18192
18215
|
if (message.role === "operator") {
|
|
18216
|
+
asked = message.text;
|
|
18193
18217
|
parts.push(`<div class="msg op" data-message-role="operator" data-key="m${message.id}"><p style="white-space:pre-wrap">${escape(message.text)}</p></div>`);
|
|
18194
18218
|
continue;
|
|
18195
18219
|
}
|
|
18196
18220
|
const cards = message.turn === null ? [] : (byTurn.get(message.turn) ?? []);
|
|
18197
18221
|
parts.push(`<div class="msg mate" data-message-role="assistant" data-key="m${message.id}"${message.id === latestReply ? ' id="latest"' : ""}>` +
|
|
18198
|
-
renderChatText(message.text) +
|
|
18222
|
+
renderChatText(message.text, asked) +
|
|
18199
18223
|
(() => {
|
|
18200
18224
|
const ask = message.turn === null ? undefined : data.asks?.get(message.turn);
|
|
18201
18225
|
return ask === undefined ? "" : mateAskHtml(ask, data.csrf, { task: data.focusTask?.id ?? null, project: data.chatProject ?? null }, "chat-message", data.pending === null && data.ask?.turn === ask.turn);
|
|
@@ -18255,8 +18279,8 @@ function matePage(chrome, data) {
|
|
|
18255
18279
|
if (data.problem !== null)
|
|
18256
18280
|
conversation.push(`<div class="problem">${escape(data.problem)}</div>`);
|
|
18257
18281
|
for (const turn of data.latched) {
|
|
18258
|
-
conversation.push(`<div class="problem"><strong>
|
|
18259
|
-
`<a href="/chat/ack/${turn.id}">
|
|
18282
|
+
conversation.push(`<div class="problem"><strong>Chat is paused.</strong> An earlier reply stopped before its cost was known; it may have cost up to ${chatMoney(turn.reservedMicrousd)}. ` +
|
|
18283
|
+
`<a href="/chat/ack/${turn.id}">Confirm that cost</a> to turn chat back on.</div>`);
|
|
18260
18284
|
}
|
|
18261
18285
|
const lastMessage = data.messages.at(-1);
|
|
18262
18286
|
const latestReply = data.pending === null && lastMessage?.role === "assistant" ? lastMessage.id : null;
|
|
@@ -23916,6 +23940,9 @@ function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null,
|
|
|
23916
23940
|
digestTimes(chatNotices.digestAt).map(([value, label]) => `<option value="${value}"${value === (chatNotices.digestAt ?? "off") ? " selected" : ""}>${label}</option>`).join("") +
|
|
23917
23941
|
`</select></label>`,
|
|
23918
23942
|
`<p class="meta">One message: what finished, what waits, what failed.</p>`,
|
|
23943
|
+
`<label>Screenshots with results<select name="screenshots">` +
|
|
23944
|
+
RESULT_SHOT_CHOICES.map(([value, label]) => `<option value="${value}"${value === (chatNotices.screenshots ?? "off") ? " selected" : ""}>${label}</option>`).join("") +
|
|
23945
|
+
`</select></label>`,
|
|
23919
23946
|
`<button type="submit">Save</button>`,
|
|
23920
23947
|
`</form>`,
|
|
23921
23948
|
...((chatNotices.projects ?? []).length === 0 ? [] : [
|
package/dist/slack-chat.d.ts
CHANGED
|
@@ -25,4 +25,6 @@ export declare const applySlackAction: (options: SlackChatOptions, event: SlackE
|
|
|
25
25
|
export declare const planSlackNotifications: (options: SlackChatOptions) => Promise<void>;
|
|
26
26
|
export declare const planSlackRooms: (options: SlackChatOptions) => Promise<void>;
|
|
27
27
|
export declare function slackBlocks(text: string, buttons?: Record<string, unknown>[]): Record<string, unknown>[];
|
|
28
|
+
/** The lead's reply as mrkdwn sections: no header block, so its first line stays an ordinary line. */
|
|
29
|
+
export declare function slackVoiceBlocks(mrkdwn: string, buttons?: Record<string, unknown>[]): Record<string, unknown>[];
|
|
28
30
|
export declare function deliverSlackPart(options: SlackChatOptions): Promise<boolean>;
|
package/dist/slack-chat.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { chatFlowButtons } from "./chat-flow.js";
|
|
2
|
+
import { refuseResultShots, resultShotsPruned } from "./result-shots.js";
|
|
2
3
|
import { chatQuestionButtons } from "./teammate-question.js";
|
|
3
4
|
import { chatAskButtons } from "./chat-ask.js";
|
|
4
5
|
import { channelInbox } from "./chat-inbox.js";
|
|
@@ -7,6 +8,7 @@ import { processChatEvent, applyChatAction, planChatNotifications, channelAccess
|
|
|
7
8
|
/** Slack is a transport for the same saved assistant and confirmation doors. */
|
|
8
9
|
import { proposalPreview, proposalLink, proposalOutcomeText, armedCardText, armedYesLabel, } from "./chat-channel.js";
|
|
9
10
|
import { MATE_MESSAGE_MAX_CHARS } from "./mate.js";
|
|
11
|
+
import { renderReply } from "./reply-shape.js";
|
|
10
12
|
import {} from "./mate-doors.js";
|
|
11
13
|
import { SlackState, slackHash, } from "./slack-state.js";
|
|
12
14
|
import { object, slackId, slackTs, slackMember, SlackError, uploadSlackBytes, } from "./slack-api.js";
|
|
@@ -131,6 +133,10 @@ const delivery = (options) => ({
|
|
|
131
133
|
state: new SlackState(options.store),
|
|
132
134
|
label: "Slack",
|
|
133
135
|
member: (member, channel) => slackMember(options.api, options.identity, member, channel),
|
|
136
|
+
// A 👍 on the owner's message where the app has reactions:write; Slack shows bots no typing indicator.
|
|
137
|
+
warm: (event) => ({
|
|
138
|
+
react: () => options.api("reactions.add", { channel: event.channel, timestamp: event.ts, name: "thumbsup" }),
|
|
139
|
+
}),
|
|
134
140
|
});
|
|
135
141
|
export const processSlackEvent = (options) => processChatEvent(delivery(options));
|
|
136
142
|
export const applySlackAction = (options, event, binding, repos) => applyChatAction(delivery(options), event, binding, repos);
|
|
@@ -156,6 +162,16 @@ export function slackBlocks(text, buttons = []) {
|
|
|
156
162
|
blocks.push({ type: "actions", elements: buttons });
|
|
157
163
|
return blocks;
|
|
158
164
|
}
|
|
165
|
+
/** The lead's reply as mrkdwn sections: no header block, so its first line stays an ordinary line. */
|
|
166
|
+
export function slackVoiceBlocks(mrkdwn, buttons = []) {
|
|
167
|
+
const blocks = split(mrkdwn).map((part) => ({
|
|
168
|
+
type: "section",
|
|
169
|
+
text: { type: "mrkdwn", text: part },
|
|
170
|
+
}));
|
|
171
|
+
if (buttons.length)
|
|
172
|
+
blocks.push({ type: "actions", elements: buttons });
|
|
173
|
+
return blocks;
|
|
174
|
+
}
|
|
159
175
|
function linkButton(origin, link) {
|
|
160
176
|
if (!origin ||
|
|
161
177
|
!link ||
|
|
@@ -217,6 +233,13 @@ export async function deliverSlackPart(options) {
|
|
|
217
233
|
if (content.task &&
|
|
218
234
|
!repos.includes(store.lookupRef(content.task)?.repo ?? ""))
|
|
219
235
|
throw new SlackError("Connected projects changed");
|
|
236
|
+
// A screenshot sent with a result goes in that result's thread once its message is placed.
|
|
237
|
+
const follows = content.shot?.follows == null ? undefined : state.db.prepare("SELECT message FROM slack_part WHERE id=? AND state='sent'").get(content.shot.follows)?.message;
|
|
238
|
+
const thread = slackTs(follows) ? String(follows) : event.thread;
|
|
239
|
+
if (content.image && content.shot && resultShotsPruned(options.evidenceRoot, content.image.run)) {
|
|
240
|
+
state.db.prepare("UPDATE slack_part SET state='dropped',problem='Removed by retention' WHERE id=?").run(row.id);
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
220
243
|
if (content.image) {
|
|
221
244
|
const verified = verifyResultImage(store, options.evidenceRoot, repos, content.image);
|
|
222
245
|
if (!verified.ok) {
|
|
@@ -257,7 +280,7 @@ export async function deliverSlackPart(options) {
|
|
|
257
280
|
: [];
|
|
258
281
|
const found = receipts
|
|
259
282
|
.map(object)
|
|
260
|
-
.find((receipt) => receipt.thread_ts ===
|
|
283
|
+
.find((receipt) => (receipt.thread_ts ?? "") === thread && slackTs(receipt.ts));
|
|
261
284
|
if (found) {
|
|
262
285
|
state.db
|
|
263
286
|
.prepare("UPDATE slack_part SET state='sent',message=?,problem=NULL WHERE id=?")
|
|
@@ -273,7 +296,7 @@ export async function deliverSlackPart(options) {
|
|
|
273
296
|
},
|
|
274
297
|
],
|
|
275
298
|
channel_id: destination,
|
|
276
|
-
thread_ts:
|
|
299
|
+
thread_ts: thread === "" ? undefined : thread,
|
|
277
300
|
initial_comment: safeResultImageCaption(content.text, content.image.taskId, content.image.run),
|
|
278
301
|
});
|
|
279
302
|
if (!Array.isArray(completed.files) ||
|
|
@@ -374,14 +397,16 @@ export async function deliverSlackPart(options) {
|
|
|
374
397
|
...(content.also ?? []).flatMap((one, index) => linkButton(options.origin(), one).map(button => ({ ...button, action_id: `toolroll_link_${index + 2}` }))),
|
|
375
398
|
];
|
|
376
399
|
const target = content.edit ?? row.message;
|
|
400
|
+
// The lead's own reply goes out in Slack's mrkdwn (bold anchors, labelled links); everything else stays plain.
|
|
401
|
+
const voiced = content.voice === true && !content.proposal && !content.image ? renderReply(text, "slack") : null;
|
|
377
402
|
const args = {
|
|
378
403
|
channel: destination,
|
|
379
|
-
text: text
|
|
404
|
+
text: voiced ?? text
|
|
380
405
|
.replaceAll("&", "&")
|
|
381
406
|
.replaceAll("<", "<")
|
|
382
407
|
.replaceAll(">", ">"),
|
|
383
|
-
blocks: slackBlocks(text, buttons),
|
|
384
|
-
mrkdwn:
|
|
408
|
+
blocks: voiced === null ? slackBlocks(text, buttons) : slackVoiceBlocks(voiced, buttons),
|
|
409
|
+
mrkdwn: voiced !== null,
|
|
385
410
|
parse: "none",
|
|
386
411
|
unfurl_links: false,
|
|
387
412
|
unfurl_media: false,
|
|
@@ -390,9 +415,9 @@ export async function deliverSlackPart(options) {
|
|
|
390
415
|
? await options.api("chat.update", { ...args, ts: target })
|
|
391
416
|
: await options.api("chat.postMessage", {
|
|
392
417
|
...args,
|
|
393
|
-
thread_ts: event.kind === "notice" &&
|
|
418
|
+
thread_ts: event.kind === "notice" && thread === ""
|
|
394
419
|
? undefined
|
|
395
|
-
:
|
|
420
|
+
: thread,
|
|
396
421
|
});
|
|
397
422
|
if (!slackTs(answer.ts) || (target && answer.ts !== target))
|
|
398
423
|
throw new SlackError("Slack did not confirm the message identity", 15_000, true);
|
|
@@ -405,6 +430,12 @@ export async function deliverSlackPart(options) {
|
|
|
405
430
|
return true;
|
|
406
431
|
}
|
|
407
432
|
catch (error) {
|
|
433
|
+
// No permission to upload files here: one plain line instead of the result's screenshots.
|
|
434
|
+
const content = JSON.parse(row.payload);
|
|
435
|
+
if (content.image && content.shot && error instanceof SlackError && ["missing_scope", "no_permission"].includes(error.code)) {
|
|
436
|
+
refuseResultShots(state, store, row, content, "Slack", nowOf(options));
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
408
439
|
const problem = error instanceof SlackError
|
|
409
440
|
? error
|
|
410
441
|
: new SlackError("Slack delivery is waiting to retry", 15_000, true);
|
package/dist/store.d.ts
CHANGED
|
@@ -47,6 +47,9 @@ export type TaskActRow = {
|
|
|
47
47
|
* "Something else" is implied. The tapped option comes back as the owner's next message, so nothing here is an answer:
|
|
48
48
|
* a question is open while no later message from the owner sits in its thread. */
|
|
49
49
|
export declare const MATE_ASK_SCHEMA = "\nCREATE TABLE IF NOT EXISTS mate_ask (\n turn INTEGER PRIMARY KEY REFERENCES mate_turn(id) ON DELETE CASCADE,\n thread INTEGER NOT NULL REFERENCES mate_thread(id) ON DELETE CASCADE,\n question TEXT NOT NULL,\n options_json TEXT NOT NULL,\n created_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS mate_ask_thread ON mate_ask (thread, turn);\n";
|
|
50
|
+
/** Decisions in the chat app (chat-decide.ts; no version bump: additive only). A token names one act on one card (the chat and message it rides); `digest` is
|
|
51
|
+
* what the card showed — a receipt, a revision source, a task stamp, a plan or a commit. */
|
|
52
|
+
export declare const CHAT_DECIDE_SCHEMA = "\nCREATE TABLE IF NOT EXISTS chat_decide_action (\n token TEXT PRIMARY KEY,\n channel TEXT NOT NULL,\n binding INTEGER NOT NULL,\n chat TEXT NOT NULL,\n message TEXT,\n act TEXT NOT NULL CHECK (act IN ('accept','changes','retry','approve','not-now','merge')),\n phase TEXT NOT NULL CHECK (phase IN ('offer','yes','cancel')),\n task_id TEXT NOT NULL,\n run INTEGER,\n digest TEXT NOT NULL,\n created_at TEXT NOT NULL,\n expires_at TEXT NOT NULL,\n consumed_at TEXT\n);\nCREATE INDEX IF NOT EXISTS chat_decide_action_message ON chat_decide_action (channel, chat, message);\nCREATE TABLE IF NOT EXISTS chat_decide_prompt (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n channel TEXT NOT NULL,\n binding INTEGER NOT NULL,\n chat TEXT NOT NULL,\n task_id TEXT NOT NULL,\n run INTEGER NOT NULL,\n digest TEXT NOT NULL,\n message TEXT,\n created_at TEXT NOT NULL,\n expires_at TEXT NOT NULL,\n consumed_at TEXT\n);\nCREATE INDEX IF NOT EXISTS chat_decide_prompt_open ON chat_decide_prompt (channel, binding, consumed_at);\n";
|
|
50
53
|
export type MateAsk = {
|
|
51
54
|
turn: number;
|
|
52
55
|
thread: number;
|
|
@@ -57,6 +60,8 @@ export type MateAsk = {
|
|
|
57
60
|
export declare const MATE_ASK_OTHER = "Something else";
|
|
58
61
|
/** How long the question's buttons work, on every channel. */
|
|
59
62
|
export declare const MATE_ASK_TTL_MS: number;
|
|
63
|
+
/** What a turn the lead starts itself for automatic crew updates says in the owner's place (lead-follow.ts): never their answer. */
|
|
64
|
+
export declare const LEAD_FOLLOW_MESSAGE = "Automatic crew update: inspect the saved results or decisions and tell me what needs attention. Do not rerun work.";
|
|
60
65
|
/** Settings → Integrations (no version bump): each integration's last check. `outcome` is ok, failed, or absent (the check found
|
|
61
66
|
* nothing set up, such as no `gh` sign-in). `ok_at` and `error`/`error_at` keep the last success and the last
|
|
62
67
|
* failure across checks, so a row can say both. `account` is a display name only, never a credential. */
|
|
@@ -990,12 +995,18 @@ export type TelegramDelivery = Notification & {
|
|
|
990
995
|
};
|
|
991
996
|
/** How chats reach one person (quiet chat): only when they are needed, or every step. */
|
|
992
997
|
export type NotificationMode = "quiet" | "all";
|
|
998
|
+
/** Screenshots with results: none (the default), the first one, or up to four (result-shots.ts). */
|
|
999
|
+
export type ResultScreenshots = "off" | "first" | "all";
|
|
1000
|
+
export declare const RESULT_SCREENSHOTS: readonly ResultScreenshots[];
|
|
993
1001
|
export type NotificationPreference = {
|
|
994
1002
|
mode: NotificationMode;
|
|
995
1003
|
digestAt: string | null;
|
|
996
1004
|
digestOn: string | null;
|
|
997
1005
|
updatedAt: string | null;
|
|
1006
|
+
screenshots: ResultScreenshots;
|
|
998
1007
|
};
|
|
1008
|
+
/** One person's saved screenshots following a result message: its own outbox row, addressed to them (result-shots.ts). */
|
|
1009
|
+
export declare const RESULT_SHOTS_KIND = "result-screenshots";
|
|
999
1010
|
/** Tasks filed this close together share one chat message. */
|
|
1000
1011
|
export declare const QUIET_GROUP_MS = 60000;
|
|
1001
1012
|
export declare function isDigestTime(value: string): boolean;
|
|
@@ -6702,8 +6713,9 @@ export declare class Store {
|
|
|
6702
6713
|
dropMateAsk(turn: number): void;
|
|
6703
6714
|
/** The question an ANSWERED turn asked; null when it asked none or did not answer. */
|
|
6704
6715
|
mateAsk(turn: number): MateAsk | null;
|
|
6705
|
-
/** What a tap on the question finds: open; answered (
|
|
6706
|
-
*
|
|
6716
|
+
/** What a tap on the question finds: open; answered (its owner has written in its thread since the reply that asked
|
|
6717
|
+
* it, typed or tapped; a turn the lead started itself, or another person's message, is no answer); or expired (older
|
|
6718
|
+
* than MATE_ASK_TTL_MS, or the question or the reply that asked it is gone, as after a retention purge). */
|
|
6707
6719
|
mateAskState(turn: number, now: Date): {
|
|
6708
6720
|
state: "open";
|
|
6709
6721
|
ask: MateAsk;
|
|
@@ -7492,7 +7504,20 @@ export declare class Store {
|
|
|
7492
7504
|
setNotificationPreference(account: string, change: {
|
|
7493
7505
|
mode?: NotificationMode;
|
|
7494
7506
|
digestAt?: string | null;
|
|
7507
|
+
screenshots?: ResultScreenshots;
|
|
7495
7508
|
}, by: string, now: Date): NotificationPreference;
|
|
7509
|
+
/** The screenshots of this result already sent (or being sent) to one destination. */
|
|
7510
|
+
resultShotsSent(destination: string, run: number): Set<number>;
|
|
7511
|
+
/** Mark screenshots as sent BEFORE they go: an unconfirmed send is never repeated. */
|
|
7512
|
+
markResultShotsSent(destination: string, run: number, artifacts: readonly number[], now: Date): void;
|
|
7513
|
+
/** Telegram refused them outright, so nothing arrived: they may be tried again. */
|
|
7514
|
+
unmarkResultShotsSent(destination: string, run: number, artifacts: readonly number[]): void;
|
|
7515
|
+
/**
|
|
7516
|
+
* A result message just became a fact: everyone who asked for screenshots with results, can see the project and
|
|
7517
|
+
* would be pinged by it gets one row of their own that follows it — once per result, whatever produced the fact.
|
|
7518
|
+
* Only a result that saved screenshots qualifies; what is sent, and whether at all, is decided again at send time.
|
|
7519
|
+
*/
|
|
7520
|
+
private enqueueResultShots;
|
|
7496
7521
|
/** Everyone who asked for an evening digest, with the local day theirs was last considered. */
|
|
7497
7522
|
eveningDigestPeople(): {
|
|
7498
7523
|
account: string;
|
|
@@ -7501,13 +7526,19 @@ export declare class Store {
|
|
|
7501
7526
|
}[];
|
|
7502
7527
|
/** Claim one local day's digest for one person: true once, false if it was already considered. */
|
|
7503
7528
|
claimEveningDigestDay(account: string, day: string): boolean;
|
|
7504
|
-
/** What this person named their lead and its persona, or null when they never saved one.
|
|
7529
|
+
/** What this person named their lead and its persona, or null when they never saved one. An empty name or persona
|
|
7530
|
+
* (a row saved only for what the lead knows about them) reads as the default. */
|
|
7505
7531
|
leadConfig(account: string): {
|
|
7506
7532
|
name: string;
|
|
7507
7533
|
persona: string;
|
|
7508
7534
|
} | null;
|
|
7509
7535
|
/** Save this person's lead name and persona (checked by the caller: lead-identity.ts). */
|
|
7510
7536
|
setLeadConfig(account: string, name: string, persona: string, now: Date): void;
|
|
7537
|
+
/** What this person's lead knows about them: their confirmed lines, in order (none when they never saved any). */
|
|
7538
|
+
leadAbout(account: string): string[];
|
|
7539
|
+
/** Save what this person's lead knows about them (checked by the caller: lead-about.ts). A person who never named
|
|
7540
|
+
* their lead gets an empty name and persona, so they keep following the defaults. */
|
|
7541
|
+
setLeadAbout(account: string, lines: readonly string[], now: Date): void;
|
|
7511
7542
|
/** A lead token for one person, shown once. A new one ends their earlier ones; the ledger names it "lead for <owner>". */
|
|
7512
7543
|
mintLeadCredential(owner: string, by: string, now: Date): {
|
|
7513
7544
|
id: string;
|
|
@@ -7579,6 +7610,8 @@ export declare class Store {
|
|
|
7579
7610
|
chatBatchFor(destination: string, taskRef: number, run: number | null, now: Date, windowMs: number, notification?: number | null): ChatBatch;
|
|
7580
7611
|
chatBatch(id: number): ChatBatch | null;
|
|
7581
7612
|
setChatBatchMessage(id: number, message: string, digest: string): void;
|
|
7613
|
+
/** The finished-work message that carries this result at one destination, if one was placed: screenshots follow it. */
|
|
7614
|
+
chatBatchMessageFor(destination: string, taskRef: number, run: number): string | null;
|
|
7582
7615
|
/** The message now showing this card, and what it shows. */
|
|
7583
7616
|
setChatCardMessage(id: number, message: string, digest: string): void;
|
|
7584
7617
|
markTelegramDigestSent(now: Date): void;
|