crewly 1.20.190 → 1.20.192
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/config/constants.ts +2 -2
- package/config/skills/agent/core/ask-owner/SKILL.md +9 -2
- package/config/skills/agent/core/ask-owner/execute.sh +7 -3
- package/config/skills/agent/core/ask-owner/execute.test.sh +3 -0
- package/config/skills/agent/core/send-message/SKILL.md +6 -0
- package/config/skills/agent/core/send-message/execute.sh +17 -1
- package/config/skills/agent/core/send-message/execute.test.sh +81 -0
- package/dist/backend/backend/src/constants.d.ts +68 -1
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +75 -2
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/canva/canva.controller.js +2 -2
- package/dist/backend/backend/src/controllers/canva/canva.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud-device-pairing.controller.d.ts +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud-device-pairing.controller.js +1 -1
- package/dist/backend/backend/src/controllers/decisions/decisions.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/decisions/decisions.controller.js +3 -1
- package/dist/backend/backend/src/controllers/decisions/decisions.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/google/google.controller.js +2 -2
- package/dist/backend/backend/src/controllers/google/google.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/microsoft/microsoft-todo.controller.js +2 -2
- package/dist/backend/backend/src/controllers/microsoft/microsoft-todo.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/monitoring/terminal.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/monitoring/terminal.controller.js +45 -2
- package/dist/backend/backend/src/controllers/monitoring/terminal.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/security/approvals.controller.d.ts +26 -0
- package/dist/backend/backend/src/controllers/security/approvals.controller.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/security/approvals.controller.js +73 -0
- package/dist/backend/backend/src/controllers/security/approvals.controller.js.map +1 -0
- package/dist/backend/backend/src/controllers/session/session.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/session/session.controller.js +9 -0
- package/dist/backend/backend/src/controllers/session/session.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/slack/slack.controller.js +1 -1
- package/dist/backend/backend/src/controllers/slack/slack.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/system/usage.controller.d.ts +3 -3
- package/dist/backend/backend/src/controllers/system/usage.controller.js +3 -3
- package/dist/backend/backend/src/routes/modules/system.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/routes/modules/system.routes.js +3 -0
- package/dist/backend/backend/src/routes/modules/system.routes.js.map +1 -1
- package/dist/backend/backend/src/services/browser/browser-approval.service.d.ts +8 -0
- package/dist/backend/backend/src/services/browser/browser-approval.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/browser/browser-approval.service.js +10 -0
- package/dist/backend/backend/src/services/browser/browser-approval.service.js.map +1 -1
- package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts +31 -0
- package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.js +28 -0
- package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.js.map +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-device-pairing.client.d.ts +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-device-pairing.client.js +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-device-pairing.service.d.ts +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-device-pairing.service.js +1 -1
- package/dist/backend/backend/src/services/decisions/decision-card.d.ts +56 -5
- package/dist/backend/backend/src/services/decisions/decision-card.d.ts.map +1 -1
- package/dist/backend/backend/src/services/decisions/decision-card.js +118 -11
- package/dist/backend/backend/src/services/decisions/decision-card.js.map +1 -1
- package/dist/backend/backend/src/services/decisions/decision-routing.d.ts +26 -1
- package/dist/backend/backend/src/services/decisions/decision-routing.d.ts.map +1 -1
- package/dist/backend/backend/src/services/decisions/decision-routing.js +36 -2
- package/dist/backend/backend/src/services/decisions/decision-routing.js.map +1 -1
- package/dist/backend/backend/src/services/decisions/decision.service.d.ts +73 -4
- package/dist/backend/backend/src/services/decisions/decision.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/decisions/decision.service.js +263 -30
- package/dist/backend/backend/src/services/decisions/decision.service.js.map +1 -1
- package/dist/backend/backend/src/services/decisions/decision.wiring.d.ts.map +1 -1
- package/dist/backend/backend/src/services/decisions/decision.wiring.js +16 -1
- package/dist/backend/backend/src/services/decisions/decision.wiring.js.map +1 -1
- package/dist/backend/backend/src/services/messaging/spend-capped-delivery.d.ts +54 -0
- package/dist/backend/backend/src/services/messaging/spend-capped-delivery.d.ts.map +1 -0
- package/dist/backend/backend/src/services/messaging/spend-capped-delivery.js +65 -0
- package/dist/backend/backend/src/services/messaging/spend-capped-delivery.js.map +1 -0
- package/dist/backend/backend/src/services/monitoring/claude-session-tokens.service.d.ts +71 -1
- package/dist/backend/backend/src/services/monitoring/claude-session-tokens.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/monitoring/claude-session-tokens.service.js +149 -44
- package/dist/backend/backend/src/services/monitoring/claude-session-tokens.service.js.map +1 -1
- package/dist/backend/backend/src/services/monitoring/claude-transcript-sync.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/monitoring/claude-transcript-sync.service.js +9 -33
- package/dist/backend/backend/src/services/monitoring/claude-transcript-sync.service.js.map +1 -1
- package/dist/backend/backend/src/services/monitoring/model-pricing.d.ts +2 -1
- package/dist/backend/backend/src/services/monitoring/model-pricing.d.ts.map +1 -1
- package/dist/backend/backend/src/services/monitoring/model-pricing.js +13 -0
- package/dist/backend/backend/src/services/monitoring/model-pricing.js.map +1 -1
- package/dist/backend/backend/src/services/monitoring/token-usage.service.d.ts +9 -0
- package/dist/backend/backend/src/services/monitoring/token-usage.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/monitoring/token-usage.service.js +32 -3
- package/dist/backend/backend/src/services/monitoring/token-usage.service.js.map +1 -1
- package/dist/backend/backend/src/services/open-items/open-item-card.d.ts +34 -0
- package/dist/backend/backend/src/services/open-items/open-item-card.d.ts.map +1 -1
- package/dist/backend/backend/src/services/open-items/open-item-card.js +88 -0
- package/dist/backend/backend/src/services/open-items/open-item-card.js.map +1 -1
- package/dist/backend/backend/src/services/open-items/open-items.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/open-items/open-items.service.js +8 -2
- package/dist/backend/backend/src/services/open-items/open-items.service.js.map +1 -1
- package/dist/backend/backend/src/services/open-items/open-items.wiring.d.ts.map +1 -1
- package/dist/backend/backend/src/services/open-items/open-items.wiring.js +1 -0
- package/dist/backend/backend/src/services/open-items/open-items.wiring.js.map +1 -1
- package/dist/backend/backend/src/services/security/approval-activity.service.d.ts +203 -0
- package/dist/backend/backend/src/services/security/approval-activity.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/security/approval-activity.service.js +279 -0
- package/dist/backend/backend/src/services/security/approval-activity.service.js.map +1 -0
- package/dist/backend/backend/src/services/skill/skill.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/skill/skill.service.js +14 -3
- package/dist/backend/backend/src/services/skill/skill.service.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-mention-resolver.d.ts +22 -0
- package/dist/backend/backend/src/services/slack/slack-mention-resolver.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-mention-resolver.js +50 -0
- package/dist/backend/backend/src/services/slack/slack-mention-resolver.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts +45 -1
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.js +214 -20
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
- package/dist/backend/backend/src/services/spend/spend-cap-command.js +1 -1
- package/dist/backend/backend/src/services/spend/spend-cap-command.js.map +1 -1
- package/dist/backend/backend/src/services/spend/spend-cap.service.d.ts +48 -1
- package/dist/backend/backend/src/services/spend/spend-cap.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/spend/spend-cap.service.js +85 -2
- package/dist/backend/backend/src/services/spend/spend-cap.service.js.map +1 -1
- package/dist/backend/backend/src/services/system/auto-update.service.d.ts +2 -2
- package/dist/backend/backend/src/services/system/auto-update.service.js +2 -2
- package/dist/backend/backend/src/services/usage/usage-stats.service.d.ts +73 -13
- package/dist/backend/backend/src/services/usage/usage-stats.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/usage/usage-stats.service.js +137 -56
- package/dist/backend/backend/src/services/usage/usage-stats.service.js.map +1 -1
- package/dist/backend/backend/src/types/decision.types.d.ts +30 -2
- package/dist/backend/backend/src/types/decision.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/slack.types.d.ts +11 -0
- package/dist/backend/backend/src/types/slack.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/slack.types.js.map +1 -1
- package/dist/backend/build-info.json +2 -2
- package/dist/backend/config/constants.d.ts +1 -1
- package/dist/backend/config/constants.d.ts.map +1 -1
- package/dist/backend/config/constants.js +2 -2
- package/dist/backend/config/constants.js.map +1 -1
- package/dist/cli/backend/src/constants.d.ts +68 -1
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +75 -2
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts +31 -0
- package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.js +28 -0
- package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.js.map +1 -1
- package/dist/cli/backend/src/services/cloud/cloud-device-pairing.client.d.ts +1 -1
- package/dist/cli/backend/src/services/cloud/cloud-device-pairing.client.js +1 -1
- package/dist/cli/backend/src/services/decisions/decision-card.d.ts +56 -5
- package/dist/cli/backend/src/services/decisions/decision-card.d.ts.map +1 -1
- package/dist/cli/backend/src/services/decisions/decision-card.js +118 -11
- package/dist/cli/backend/src/services/decisions/decision-card.js.map +1 -1
- package/dist/cli/backend/src/services/decisions/decision.service.d.ts +73 -4
- package/dist/cli/backend/src/services/decisions/decision.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/decisions/decision.service.js +263 -30
- package/dist/cli/backend/src/services/decisions/decision.service.js.map +1 -1
- package/dist/cli/backend/src/services/monitoring/model-pricing.d.ts +2 -1
- package/dist/cli/backend/src/services/monitoring/model-pricing.d.ts.map +1 -1
- package/dist/cli/backend/src/services/monitoring/model-pricing.js +13 -0
- package/dist/cli/backend/src/services/monitoring/model-pricing.js.map +1 -1
- package/dist/cli/backend/src/services/monitoring/token-usage.service.d.ts +9 -0
- package/dist/cli/backend/src/services/monitoring/token-usage.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/monitoring/token-usage.service.js +32 -3
- package/dist/cli/backend/src/services/monitoring/token-usage.service.js.map +1 -1
- package/dist/cli/backend/src/services/open-items/open-item-card.d.ts +34 -0
- package/dist/cli/backend/src/services/open-items/open-item-card.d.ts.map +1 -1
- package/dist/cli/backend/src/services/open-items/open-item-card.js +88 -0
- package/dist/cli/backend/src/services/open-items/open-item-card.js.map +1 -1
- package/dist/cli/backend/src/services/open-items/open-items.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/open-items/open-items.service.js +8 -2
- package/dist/cli/backend/src/services/open-items/open-items.service.js.map +1 -1
- package/dist/cli/backend/src/services/skill/skill.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/skill/skill.service.js +14 -3
- package/dist/cli/backend/src/services/skill/skill.service.js.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-mention-resolver.d.ts +22 -0
- package/dist/cli/backend/src/services/slack/slack-mention-resolver.d.ts.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-mention-resolver.js +50 -0
- package/dist/cli/backend/src/services/slack/slack-mention-resolver.js.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts +45 -1
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.js +214 -20
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
- package/dist/cli/backend/src/types/decision.types.d.ts +30 -2
- package/dist/cli/backend/src/types/decision.types.d.ts.map +1 -1
- package/dist/cli/backend/src/types/slack.types.d.ts +11 -0
- package/dist/cli/backend/src/types/slack.types.d.ts.map +1 -1
- package/dist/cli/backend/src/types/slack.types.js.map +1 -1
- package/dist/cli/config/constants.d.ts +1 -1
- package/dist/cli/config/constants.d.ts.map +1 -1
- package/dist/cli/config/constants.js +2 -2
- package/dist/cli/config/constants.js.map +1 -1
- package/frontend/dist/assets/index-5b7af029.js +4955 -0
- package/frontend/dist/assets/index-fc715ec3.css +42 -0
- package/frontend/dist/index.html +2 -2
- package/package.json +1 -1
- package/frontend/dist/assets/index-20a67829.css +0 -42
- package/frontend/dist/assets/index-49f50b45.js +0 -4952
|
@@ -14,12 +14,13 @@
|
|
|
14
14
|
*
|
|
15
15
|
* @module services/decisions/decision.service
|
|
16
16
|
*/
|
|
17
|
+
import { isAudioOrVideo } from '../../utils/inbound-file-hint.utils.js';
|
|
17
18
|
import { AgentPromptReferenceService } from '../orc/agent-prompt-reference.service.js';
|
|
18
19
|
import { DECISION_CONSTANTS, OPEN_ITEMS_CONSTANTS, ORCHESTRATOR_SESSION_NAME } from '../../constants.js';
|
|
19
20
|
import { questionSimilarity } from '../open-items/open-item-card.js';
|
|
20
21
|
import { LoggerService } from '../core/logger.service.js';
|
|
21
22
|
import { DecisionContractError, matchOption, parseOptions, resolveDefault, validateAskOwner } from './decision-contract.js';
|
|
22
|
-
import { canRemind, cardFallbackText, choiceFromReaction, choiceFromText, deadlineDefaultLine, defaultIsSafe, defaultLabel, formatWhen, optionLabel, parseButtonValue, renderOpenCard, renderSettledCard, settledLine, noOption, isSkipWord, skipChoice, ticketThreadRootText, } from './decision-card.js';
|
|
23
|
+
import { answerFilesOf, canRemind, cardFallbackText, choiceFromReaction, choiceFromText, deadlineDefaultLine, defaultIsSafe, describeAnswerFiles, slackTsAfter, waitReminderLine, defaultLabel, formatWhen, optionLabel, parseButtonValue, renderOpenCard, renderSettledCard, settledLine, noOption, isSkipWord, skipChoice, ticketThreadRootText, } from './decision-card.js';
|
|
23
24
|
import { PENDING_DECISION_STATUSES } from './decision-store.js';
|
|
24
25
|
/** Thrown for requests the caller must fix (HTTP 4xx). */
|
|
25
26
|
export class DecisionError extends Error {
|
|
@@ -85,6 +86,12 @@ export class DecisionService {
|
|
|
85
86
|
now;
|
|
86
87
|
timer = null;
|
|
87
88
|
ticking = false;
|
|
89
|
+
/**
|
|
90
|
+
* When this process started ticking (epoch ms; set by the first tick). A
|
|
91
|
+
* `wait` card whose asker was told about the deadline before that is never
|
|
92
|
+
* reminded: after an upgrade, old cards must not all ping the owner at once.
|
|
93
|
+
*/
|
|
94
|
+
startedAt = null;
|
|
88
95
|
/**
|
|
89
96
|
* @param deps - Collaborators
|
|
90
97
|
*/
|
|
@@ -286,6 +293,7 @@ export class DecisionService {
|
|
|
286
293
|
...(ask.sensitive ? { sensitive: ask.sensitive } : {}),
|
|
287
294
|
...(ask.browser ? { browser: ask.browser } : {}),
|
|
288
295
|
...(ask.title ? { title: ask.title } : {}),
|
|
296
|
+
...(ask.body?.length ? { body: ask.body } : {}),
|
|
289
297
|
...(ask.place ? { place: ask.place } : {}),
|
|
290
298
|
...(ask.requestRef ? { requestRef: ask.requestRef } : {}),
|
|
291
299
|
...(ask.source ? { source: ask.source } : {}),
|
|
@@ -493,8 +501,12 @@ export class DecisionService {
|
|
|
493
501
|
return this.apply(decision, choice, 'reaction', event.user);
|
|
494
502
|
}
|
|
495
503
|
/**
|
|
496
|
-
* An owner reply in a card's thread
|
|
497
|
-
* takes it; words that match no
|
|
504
|
+
* An owner reply in a card's thread (channel or DM thread), posted after
|
|
505
|
+
* the card. Text: the newest open card takes it; words that match no
|
|
506
|
+
* option are passed to the asker verbatim. A voice note, audio or other
|
|
507
|
+
* file with no text answers every open card in the thread "in thread"
|
|
508
|
+
* (specs/2026-10-02-decision-card-thread-answers.md §1). Every such owner
|
|
509
|
+
* message stamps `ownerRepliedAt` on the open cards there.
|
|
498
510
|
*
|
|
499
511
|
* @param message - Inbound Slack message
|
|
500
512
|
* @returns What happened
|
|
@@ -509,14 +521,75 @@ export class DecisionService {
|
|
|
509
521
|
const candidates = await this.deps.store.list((d) => d.status === 'open' &&
|
|
510
522
|
!!d.card &&
|
|
511
523
|
d.card.slackChannelId === message.channelId &&
|
|
512
|
-
(d.card.threadTs === message.threadTs || d.card.messageTs === message.threadTs)
|
|
513
|
-
|
|
514
|
-
|
|
524
|
+
(d.card.threadTs === message.threadTs || d.card.messageTs === message.threadTs) &&
|
|
525
|
+
// Only what the owner said after the card went up answers it.
|
|
526
|
+
slackTsAfter(message.ts, d.card.messageTs));
|
|
527
|
+
if (candidates.length === 0)
|
|
515
528
|
return { handled: false, reason: 'no open card in this thread' };
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
529
|
+
const at = this.now().toISOString();
|
|
530
|
+
for (const c of candidates) {
|
|
531
|
+
await this.deps.store.update(c.id, (cur) => (cur.status === 'open' ? { ownerRepliedAt: at } : null)).catch(() => null);
|
|
532
|
+
}
|
|
533
|
+
const decision = candidates[0];
|
|
534
|
+
const files = answerFilesOf(message.files);
|
|
535
|
+
const text = message.text ?? '';
|
|
536
|
+
if (decision.system) {
|
|
537
|
+
const choice = systemChoiceFromText(decision, text);
|
|
538
|
+
if (!choice)
|
|
539
|
+
return { handled: false, reason: 'not one of the options', decision };
|
|
540
|
+
return this.apply(decision, choice, 'reply', message.userId);
|
|
541
|
+
}
|
|
542
|
+
if (text.replace(/<@[A-Z0-9]+>/g, '').trim()) {
|
|
543
|
+
const choice = choiceFromText(decision, text);
|
|
544
|
+
if (!choice)
|
|
545
|
+
return { handled: false, reason: 'empty reply', decision };
|
|
546
|
+
return this.apply(decision, choice, 'reply', message.userId, undefined, files);
|
|
547
|
+
}
|
|
548
|
+
if (files.length === 0)
|
|
549
|
+
return { handled: false, reason: 'empty reply', decision };
|
|
550
|
+
return this.applyThreadFiles(candidates, files, message.userId);
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* A voice note / file with no text in a thread with open cards
|
|
554
|
+
* (specs/2026-10-02-decision-card-thread-answers.md §1):
|
|
555
|
+
*
|
|
556
|
+
* - sensitive, system and browser cards are never settled by a file; their
|
|
557
|
+
* askers get the file as information;
|
|
558
|
+
* - a voice note / audio / video answers the newest other card; any file
|
|
559
|
+
* answers the only other card when there is exactly one;
|
|
560
|
+
* - an image or other file with several open cards settles nothing.
|
|
561
|
+
*
|
|
562
|
+
* Every asker whose card stays open is told what the owner posted, once.
|
|
563
|
+
*/
|
|
564
|
+
async applyThreadFiles(candidates, files, user) {
|
|
565
|
+
const plain = candidates.filter((c) => !c.system && c.kind !== 'browser_action' && !c.sensitive);
|
|
566
|
+
const voice = files.some((f) => isAudioOrVideo({ name: f.name, mimetype: f.mimetype ?? '' }));
|
|
567
|
+
const newest = [...plain].sort((x, y) => Date.parse(y.createdAt) - Date.parse(x.createdAt) || idNumber(y.id) - idNumber(x.id))[0];
|
|
568
|
+
const target = newest && (voice || plain.length === 1) ? newest : null;
|
|
569
|
+
const notes = new Map();
|
|
570
|
+
let outcome = null;
|
|
571
|
+
if (target) {
|
|
572
|
+
const transcript = files.map((f) => f.transcript).filter((t) => !!t).join(' ');
|
|
573
|
+
outcome = await this.apply(target, { kind: 'thread', files, ...(transcript ? { text: transcript } : {}) }, 'thread', user, notes);
|
|
574
|
+
}
|
|
575
|
+
const what = describeAnswerFiles(files);
|
|
576
|
+
for (const c of candidates) {
|
|
577
|
+
if (c.system || (target && c.id === target.id))
|
|
578
|
+
continue;
|
|
579
|
+
const why = c.sensitive
|
|
580
|
+
? `It needs the owner's explicit OK (${c.sensitive}), so it stays open — do not go ahead on this file alone.`
|
|
581
|
+
: c.kind === 'browser_action'
|
|
582
|
+
? 'A held browser action needs a Let it / No, so it stays open.'
|
|
583
|
+
: `It is not clear which open question it answers, so this card stays open. If it answers yours, act on it and withdraw the card: ask-owner --cancel ${c.id} --reason "answered in the thread".`;
|
|
584
|
+
notes.set(c.asker, [...(notes.get(c.asker) ?? []), `[DECISION ${c.id}] The owner posted ${what} in the thread of your question "${c.question}".${filesLine(files)} ${why}`]);
|
|
585
|
+
}
|
|
586
|
+
for (const [asker, lines] of notes) {
|
|
587
|
+
const d = candidates.find((c) => c.asker === asker && !c.system) ?? candidates[0];
|
|
588
|
+
await this.tellAsker(d, lines.join('\n'));
|
|
589
|
+
}
|
|
590
|
+
if (outcome)
|
|
591
|
+
return outcome;
|
|
592
|
+
return { handled: false, reason: plain.length === 0 ? 'a file does not answer this card' : 'a file does not say which card it answers', decision: candidates[0] };
|
|
520
593
|
}
|
|
521
594
|
/**
|
|
522
595
|
* The owner answered from the dashboard.
|
|
@@ -661,7 +734,9 @@ export class DecisionService {
|
|
|
661
734
|
async cancelWhere(filter, note) {
|
|
662
735
|
const open = await this.deps.store.list((d) => PENDING_DECISION_STATUSES.has(d.status) && filter(d));
|
|
663
736
|
for (const d of open) {
|
|
664
|
-
const done = await this.deps.store.update(d.id, (cur) =>
|
|
737
|
+
const done = await this.deps.store.update(d.id, (cur) => PENDING_DECISION_STATUSES.has(cur.status)
|
|
738
|
+
? { status: 'cancelled', resolvedAt: this.now().toISOString(), remindAt: undefined, ...(note?.trim() ? { closedReason: note.trim() } : {}) }
|
|
739
|
+
: null);
|
|
665
740
|
if (!done)
|
|
666
741
|
continue;
|
|
667
742
|
await this.refreshCard(done);
|
|
@@ -681,6 +756,17 @@ export class DecisionService {
|
|
|
681
756
|
const all = await this.deps.store.list(which === 'open' ? (d) => PENDING_DECISION_STATUSES.has(d.status) : undefined);
|
|
682
757
|
return all.slice(0, DECISION_CONSTANTS.MAX_LISTED);
|
|
683
758
|
}
|
|
759
|
+
/**
|
|
760
|
+
* Every decision created at or after `sinceMs`, plus every one still
|
|
761
|
+
* waiting (any age). Not capped like {@link list}: for counting
|
|
762
|
+
* (Settings › Security). Settled decisions are kept 30 days.
|
|
763
|
+
*
|
|
764
|
+
* @param sinceMs - Lower bound (epoch ms)
|
|
765
|
+
* @returns Decisions, newest first
|
|
766
|
+
*/
|
|
767
|
+
listSince(sinceMs) {
|
|
768
|
+
return this.deps.store.list((d) => PENDING_DECISION_STATUSES.has(d.status) || Date.parse(d.createdAt) >= sinceMs);
|
|
769
|
+
}
|
|
684
770
|
/**
|
|
685
771
|
* One decision.
|
|
686
772
|
*
|
|
@@ -702,7 +788,7 @@ export class DecisionService {
|
|
|
702
788
|
* Apply a choice: resolve (or snooze), update the card, log the ticket,
|
|
703
789
|
* tell the asker, close the watchdog entry.
|
|
704
790
|
*/
|
|
705
|
-
async apply(decision, choice, via, user, batchNotes) {
|
|
791
|
+
async apply(decision, choice, via, user, batchNotes, files = []) {
|
|
706
792
|
const now = this.now();
|
|
707
793
|
if (choice.kind === 'skip')
|
|
708
794
|
return this.applySkip(decision, via, user, batchNotes);
|
|
@@ -735,14 +821,22 @@ export class DecisionService {
|
|
|
735
821
|
}
|
|
736
822
|
const patch = choice.kind === 'option'
|
|
737
823
|
? { status: 'resolved', chosenKey: choice.key, answerText: undefined }
|
|
738
|
-
:
|
|
824
|
+
: choice.kind === 'thread'
|
|
825
|
+
? { status: 'resolved', chosenKey: undefined, answerText: choice.text?.slice(0, 2000), answerFiles: choice.files }
|
|
826
|
+
: { status: 'resolved', chosenKey: undefined, answerText: choice.text.slice(0, 2000) };
|
|
827
|
+
if (files.length > 0 && choice.kind !== 'thread')
|
|
828
|
+
patch.answerFiles = files;
|
|
739
829
|
const resolved = await this.deps.store.update(decision.id, (cur) => PENDING_DECISION_STATUSES.has(cur.status)
|
|
740
830
|
? { ...patch, answeredVia: via, ...(user ? { answeredBy: user } : {}), resolvedAt: now.toISOString(), remindAt: undefined }
|
|
741
831
|
: null);
|
|
742
832
|
if (!resolved)
|
|
743
833
|
return { handled: false, reason: 'already settled', decision };
|
|
744
834
|
await this.refreshCard(resolved);
|
|
745
|
-
const answer = resolved.chosenKey
|
|
835
|
+
const answer = resolved.chosenKey
|
|
836
|
+
? optionLabel(resolved, resolved.chosenKey)
|
|
837
|
+
: resolved.answeredVia === 'thread'
|
|
838
|
+
? `answered in the thread with ${describeAnswerFiles(resolved.answerFiles ?? [])}`
|
|
839
|
+
: `“${resolved.answerText}”`;
|
|
746
840
|
if (resolved.ticket)
|
|
747
841
|
await this.logTicket(resolved, `owner decision ${resolved.id}: ${answer} (${via})`, true);
|
|
748
842
|
await this.notifyAsker(resolved, this.answerNote(resolved), batchNotes);
|
|
@@ -777,10 +871,17 @@ export class DecisionService {
|
|
|
777
871
|
answerNote(d) {
|
|
778
872
|
const where = this.whereLine(d);
|
|
779
873
|
const about = `for: "${d.question}"${d.ticket ? ` (ticket ${d.ticket.id})` : ''}`;
|
|
874
|
+
const files = filesLine(d.answerFiles ?? []);
|
|
780
875
|
if (d.chosenKey) {
|
|
781
|
-
return `[DECISION ${d.id}] The owner chose "${optionLabel(d, d.chosenKey)}" ${about}. Act on it now.${where}`;
|
|
876
|
+
return `[DECISION ${d.id}] The owner chose "${optionLabel(d, d.chosenKey)}" ${about}. Act on it now.${files}${where}`;
|
|
782
877
|
}
|
|
783
|
-
|
|
878
|
+
if (d.answeredVia === 'thread') {
|
|
879
|
+
const what = describeAnswerFiles(d.answerFiles ?? []);
|
|
880
|
+
const transcript = d.answerText ? ` Slack's transcript: "${d.answerText}".` : '';
|
|
881
|
+
return (`[DECISION ${d.id}] The owner answered ${about} in the card's thread with ${what} (no text).${transcript}${files} ` +
|
|
882
|
+
`Read it as their decision and act on it; if it is genuinely unclear, ask once more with ask-owner.${where}`);
|
|
883
|
+
}
|
|
884
|
+
return `[DECISION ${d.id}] The owner answered in words ${about}: "${d.answerText ?? ''}".${files} Read it as their decision and act on it; if it is genuinely unclear, ask once more with ask-owner.${where}`;
|
|
784
885
|
}
|
|
785
886
|
/**
|
|
786
887
|
* Where the asker's follow-up belongs: a command naming the decision — the
|
|
@@ -807,6 +908,9 @@ export class DecisionService {
|
|
|
807
908
|
const acted = [];
|
|
808
909
|
try {
|
|
809
910
|
const now = this.now();
|
|
911
|
+
if (this.startedAt === null)
|
|
912
|
+
this.startedAt = now.getTime();
|
|
913
|
+
const reminders = [];
|
|
810
914
|
for (const d of await this.deps.store.list((x) => x.status === 'open')) {
|
|
811
915
|
try {
|
|
812
916
|
if (!d.card) {
|
|
@@ -824,6 +928,14 @@ export class DecisionService {
|
|
|
824
928
|
}
|
|
825
929
|
if (Date.parse(d.deadline) > now.getTime())
|
|
826
930
|
continue;
|
|
931
|
+
if (d.defaultKey === DECISION_CONSTANTS.WAIT_DEFAULT && !d.sensitive) {
|
|
932
|
+
const step = await this.waitStep(d, now);
|
|
933
|
+
if (step === 'acted')
|
|
934
|
+
acted.push(d.id);
|
|
935
|
+
else if (step === 'remind')
|
|
936
|
+
reminders.push(d);
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
827
939
|
// A default that never lets anything through (a held browser
|
|
828
940
|
// action's No, a declined Terms card) is applied even when sensitive.
|
|
829
941
|
if (d.sensitive && !defaultIsSafe(d)) {
|
|
@@ -838,6 +950,7 @@ export class DecisionService {
|
|
|
838
950
|
this.logger.warn('Decision tick step failed', { decisionId: d.id, error: errText(err) });
|
|
839
951
|
}
|
|
840
952
|
}
|
|
953
|
+
acted.push(...(await this.postWaitReminders(reminders, now)));
|
|
841
954
|
await this.deps.store.prune().catch(() => 0);
|
|
842
955
|
}
|
|
843
956
|
finally {
|
|
@@ -845,30 +958,111 @@ export class DecisionService {
|
|
|
845
958
|
}
|
|
846
959
|
return acted;
|
|
847
960
|
}
|
|
961
|
+
/**
|
|
962
|
+
* Withdraw a card whose tracked item is already closed, silently: the
|
|
963
|
+
* card says why, nothing is posted in the thread and the asker is not woken
|
|
964
|
+
* (specs/2026-10-02-decision-card-thread-answers.md §3).
|
|
965
|
+
*
|
|
966
|
+
* @param d - Open decision about to post something to the owner
|
|
967
|
+
* @returns True when it was withdrawn
|
|
968
|
+
*/
|
|
969
|
+
async withdrawIfMoot(d) {
|
|
970
|
+
if (!this.deps.trackedClosed)
|
|
971
|
+
return false;
|
|
972
|
+
const reason = await this.deps.trackedClosed(d).catch(() => null);
|
|
973
|
+
if (!reason)
|
|
974
|
+
return false;
|
|
975
|
+
const n = await this.cancelWhere((x) => x.id === d.id, reason);
|
|
976
|
+
if (n === 0)
|
|
977
|
+
return false;
|
|
978
|
+
this.logger.info('Decision card withdrawn before posting: what it tracks is already closed', { decisionId: d.id, reason });
|
|
979
|
+
// One short note, so a ticket closed by mistake is recoverable.
|
|
980
|
+
await this.tellAsker(d, `[DECISION ${d.id}] Crewly closed your question "${d.question}" without asking the owner: ${reason}. Nothing was posted to the owner. If that is wrong and the work is still open, ask again with ask-owner.`);
|
|
981
|
+
return true;
|
|
982
|
+
}
|
|
848
983
|
/** Post the "Remind me tomorrow" reminder in the card's thread. */
|
|
849
984
|
async remindNow(d) {
|
|
985
|
+
if (await this.withdrawIfMoot(d))
|
|
986
|
+
return;
|
|
850
987
|
const updated = await this.deps.store.update(d.id, (cur) => (cur.status === 'open' && cur.remindAt ? { remindAt: undefined } : null));
|
|
851
988
|
if (!updated)
|
|
852
989
|
return;
|
|
853
990
|
const owner = this.deps.ownerUserId?.();
|
|
854
|
-
await this.postInThread(updated, `${owner ? `<@${owner}> ` : ''}Reminder: ${updated.question}
|
|
991
|
+
await this.postInThread(updated, `${owner ? `<@${owner}> ` : ''}Reminder: ${updated.question} — tap an answer on the card above, or reply here.`);
|
|
855
992
|
await this.refreshCard(updated);
|
|
856
993
|
}
|
|
857
|
-
/**
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
994
|
+
/**
|
|
995
|
+
* A `wait` card past its deadline (specs/2026-10-02-decision-card-thread-answers.md §2):
|
|
996
|
+
* first only the asker is told — nothing goes to the owner; later, once, a
|
|
997
|
+
* reminder that says what to do, when the owner has not touched the thread.
|
|
998
|
+
*/
|
|
999
|
+
async waitStep(d, now) {
|
|
1000
|
+
if (!d.deadlineNoticeAt) {
|
|
1001
|
+
if (await this.withdrawIfMoot(d))
|
|
1002
|
+
return 'acted';
|
|
863
1003
|
const updated = await this.deps.store.update(d.id, (cur) => (cur.status === 'open' && !cur.deadlineNoticeAt ? { deadlineNoticeAt: now.toISOString() } : null));
|
|
864
1004
|
if (!updated)
|
|
865
|
-
return
|
|
866
|
-
await this.postInThread(updated, line);
|
|
1005
|
+
return null;
|
|
867
1006
|
if (updated.ticket)
|
|
868
|
-
await this.logTicket(updated, `owner decision ${updated.id}: no answer by the deadline — still waiting`, false);
|
|
869
|
-
await this.tellAsker(updated, `[DECISION ${updated.id}]
|
|
870
|
-
|
|
1007
|
+
await this.logTicket(updated, `owner decision ${updated.id}: no answer by the deadline — still waiting (nothing posted to the owner)`, false);
|
|
1008
|
+
await this.tellAsker(updated, `[DECISION ${updated.id}] The deadline for "${updated.question}" passed with no answer. Nothing was posted to the owner. ` +
|
|
1009
|
+
`Keep this work parked until they answer. If it is already settled or no longer needed, withdraw it: ask-owner --cancel ${updated.id} --reason "<why>".${this.whereLine(updated)}`);
|
|
1010
|
+
this.logger.info('Wait-default decision past its deadline — asker told, nothing posted', { decisionId: updated.id });
|
|
1011
|
+
return 'acted';
|
|
1012
|
+
}
|
|
1013
|
+
if (d.waitReminderAt || d.ownerRepliedAt)
|
|
1014
|
+
return null;
|
|
1015
|
+
if (this.startedAt !== null && Date.parse(d.deadlineNoticeAt) < this.startedAt) {
|
|
1016
|
+
// Noticed before this process started (e.g. before the upgrade that
|
|
1017
|
+
// added reminders): marked, never reminded — no burst on first boot.
|
|
1018
|
+
await this.deps.store.update(d.id, (cur) => (cur.status === 'open' && !cur.waitReminderAt ? { waitReminderAt: now.toISOString() } : null));
|
|
1019
|
+
this.logger.debug('Wait-default decision noticed before this start — no reminder', { decisionId: d.id });
|
|
1020
|
+
return null;
|
|
1021
|
+
}
|
|
1022
|
+
if (now.getTime() < Date.parse(d.deadlineNoticeAt) + DECISION_CONSTANTS.WAIT_REMINDER_DELAY_MS)
|
|
1023
|
+
return null;
|
|
1024
|
+
if (await this.withdrawIfMoot(d))
|
|
1025
|
+
return 'acted';
|
|
1026
|
+
return 'remind';
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Post the due `wait` reminders: ONE per thread, listing that thread's open
|
|
1030
|
+
* questions; every listed card is marked as reminded.
|
|
1031
|
+
*
|
|
1032
|
+
* @param due - Cards due for their reminder (already checked)
|
|
1033
|
+
* @param now - Clock
|
|
1034
|
+
* @returns Ids reminded
|
|
1035
|
+
*/
|
|
1036
|
+
async postWaitReminders(due, now) {
|
|
1037
|
+
const byThread = new Map();
|
|
1038
|
+
for (const d of due) {
|
|
1039
|
+
if (!d.card)
|
|
1040
|
+
continue;
|
|
1041
|
+
const key = `${d.card.slackChannelId}:${d.card.threadTs ?? d.card.messageTs}`;
|
|
1042
|
+
byThread.set(key, [...(byThread.get(key) ?? []), d]);
|
|
1043
|
+
}
|
|
1044
|
+
const reminded = [];
|
|
1045
|
+
for (const group of byThread.values()) {
|
|
1046
|
+
const marked = [];
|
|
1047
|
+
for (const d of group) {
|
|
1048
|
+
const updated = await this.deps.store.update(d.id, (cur) => cur.status === 'open' && !cur.waitReminderAt && !cur.ownerRepliedAt ? { waitReminderAt: now.toISOString() } : null);
|
|
1049
|
+
if (updated)
|
|
1050
|
+
marked.push(updated);
|
|
1051
|
+
}
|
|
1052
|
+
if (marked.length === 0)
|
|
1053
|
+
continue;
|
|
1054
|
+
marked.sort((x, y) => Date.parse(x.createdAt) - Date.parse(y.createdAt) || idNumber(x.id) - idNumber(y.id));
|
|
1055
|
+
await this.postInThread(marked[marked.length - 1], waitReminderLine(marked, this.deps.ownerUserId?.()));
|
|
1056
|
+
reminded.push(...marked.map((d) => d.id));
|
|
1057
|
+
this.logger.info('Wait-default decisions: one reminder posted for the thread', { decisionIds: marked.map((d) => d.id) });
|
|
871
1058
|
}
|
|
1059
|
+
return reminded;
|
|
1060
|
+
}
|
|
1061
|
+
/** Non-sensitive deadline with a real default: apply it and say who does what. */
|
|
1062
|
+
async applyDefault(d, now) {
|
|
1063
|
+
if (await this.withdrawIfMoot(d))
|
|
1064
|
+
return true;
|
|
1065
|
+
const line = deadlineDefaultLine(d, now, await this.askerName(d));
|
|
872
1066
|
const resolved = await this.deps.store.update(d.id, (cur) => cur.status === 'open' ? { status: 'defaulted', chosenKey: cur.defaultKey, answeredVia: 'deadline', resolvedAt: now.toISOString() } : null);
|
|
873
1067
|
if (!resolved)
|
|
874
1068
|
return false;
|
|
@@ -887,6 +1081,8 @@ export class DecisionService {
|
|
|
887
1081
|
const due = Math.max(Date.parse(d.deadline), Date.parse(d.createdAt) + DECISION_CONSTANTS.SENSITIVE_REASK_AFTER_MS);
|
|
888
1082
|
if (now.getTime() < due)
|
|
889
1083
|
return false;
|
|
1084
|
+
if (await this.withdrawIfMoot(d))
|
|
1085
|
+
return true;
|
|
890
1086
|
const updated = await this.deps.store.update(d.id, (cur) => (cur.status === 'open' && !cur.reaskedAt ? { reaskedAt: now.toISOString() } : null));
|
|
891
1087
|
if (!updated)
|
|
892
1088
|
return false;
|
|
@@ -921,8 +1117,9 @@ export class DecisionService {
|
|
|
921
1117
|
let ownerName;
|
|
922
1118
|
if (!pending && d.answeredBy)
|
|
923
1119
|
ownerName = await this.deps.userName?.(d.answeredBy).catch(() => undefined);
|
|
924
|
-
const
|
|
925
|
-
const
|
|
1120
|
+
const askerName = d.status === 'defaulted' ? await this.askerName(d) : undefined;
|
|
1121
|
+
const blocks = pending ? renderOpenCard(d, this.deps.instanceId(), now) : renderSettledCard(d, ownerName, now, askerName);
|
|
1122
|
+
const text = pending ? cardFallbackText(d) : `${cardFallbackText(d)} — ${settledLine(d, ownerName, now, askerName)}`;
|
|
926
1123
|
const token = d.card.ownBot ? (await this.deps.identityOf(d.card.postedBy).catch(() => ({}))).botToken : undefined;
|
|
927
1124
|
try {
|
|
928
1125
|
await slack.updateMessage(d.card.slackChannelId, d.card.messageTs, text, blocks, token);
|
|
@@ -933,6 +1130,16 @@ export class DecisionService {
|
|
|
933
1130
|
return false;
|
|
934
1131
|
}
|
|
935
1132
|
}
|
|
1133
|
+
/** The asking agent's display name ("Owen"), when known. */
|
|
1134
|
+
async askerName(d) {
|
|
1135
|
+
if (d.system)
|
|
1136
|
+
return undefined;
|
|
1137
|
+
const named = await this.deps.displayName?.(d.asker).catch(() => undefined);
|
|
1138
|
+
if (named)
|
|
1139
|
+
return named;
|
|
1140
|
+
const identity = await this.deps.identityOf(d.asker).catch(() => ({}));
|
|
1141
|
+
return identity.username && identity.username !== d.asker ? identity.username : undefined;
|
|
1142
|
+
}
|
|
936
1143
|
/** Post a line in the card's thread, as the asker. */
|
|
937
1144
|
async postInThread(d, text) {
|
|
938
1145
|
const slack = this.deps.slack();
|
|
@@ -1031,6 +1238,32 @@ function systemChoiceFromText(d, text) {
|
|
|
1031
1238
|
}
|
|
1032
1239
|
return null;
|
|
1033
1240
|
}
|
|
1241
|
+
/**
|
|
1242
|
+
* The number of a `D-<n>` id (0 when unreadable), for ordering.
|
|
1243
|
+
*
|
|
1244
|
+
* @param id - Decision id
|
|
1245
|
+
* @returns n
|
|
1246
|
+
*/
|
|
1247
|
+
function idNumber(id) {
|
|
1248
|
+
return Number(/(\d+)$/.exec(id)?.[1] ?? 0);
|
|
1249
|
+
}
|
|
1250
|
+
/**
|
|
1251
|
+
* The files line of an answer note: name, type and link of each file, and how
|
|
1252
|
+
* to hear a voice note that came without a transcript.
|
|
1253
|
+
*
|
|
1254
|
+
* @param files - Answer files
|
|
1255
|
+
* @returns Text starting with a space, or '' for none
|
|
1256
|
+
*/
|
|
1257
|
+
function filesLine(files) {
|
|
1258
|
+
if (files.length === 0)
|
|
1259
|
+
return '';
|
|
1260
|
+
const list = files.map((f) => `${f.name}${f.mimetype ? ` (${f.mimetype})` : ''}${f.permalink ? ` ${f.permalink}` : ''}`).join('; ');
|
|
1261
|
+
const unheard = files.some((f) => !f.transcript && /^(audio|video)\//.test(f.mimetype ?? ''));
|
|
1262
|
+
const hint = unheard
|
|
1263
|
+
? ' If you have not heard it yet, transcribe it with the transcribe-audio skill (the file reached you with the owner\'s message, or fetch it from the link).'
|
|
1264
|
+
: '';
|
|
1265
|
+
return ` Files: ${list}.${hint}`;
|
|
1266
|
+
}
|
|
1034
1267
|
/**
|
|
1035
1268
|
* Message of an unknown error.
|
|
1036
1269
|
*
|