crewly 1.20.189 → 1.20.191
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/attach-file/execute.sh +13 -4
- package/config/skills/agent/core/complete-task/execute.sh +3 -0
- package/config/skills/agent/core/reply/SKILL.md +32 -4
- package/config/skills/agent/core/reply/execute.sh +23 -1
- package/config/skills/agent/core/reply/execute.test.sh +9 -0
- package/config/skills/agent/core/reply-chat/SKILL.md +9 -2
- package/config/skills/agent/core/reply-chat/execute.sh +13 -1
- package/config/skills/agent/core/report-status/execute.sh +5 -0
- package/config/skills/agent/core/send-chat-response/execute.sh +1 -1
- package/dist/backend/backend/src/constants.d.ts +45 -1
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +55 -2
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/chat/agent-reply.controller.d.ts +14 -1
- package/dist/backend/backend/src/controllers/chat/agent-reply.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/chat/agent-reply.controller.js +66 -78
- package/dist/backend/backend/src/controllers/chat/agent-reply.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/chat/chat.controller.d.ts +12 -0
- package/dist/backend/backend/src/controllers/chat/chat.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/chat/chat.controller.js +148 -24
- package/dist/backend/backend/src/controllers/chat/chat.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/chat-v2/chat-v2.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/chat-v2/chat-v2.controller.js +62 -0
- package/dist/backend/backend/src/controllers/chat-v2/chat-v2.controller.js.map +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/slack/slack.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/slack/slack.controller.js +120 -35
- package/dist/backend/backend/src/controllers/slack/slack.controller.js.map +1 -1
- package/dist/backend/backend/src/services/chat-v2/chat-v2.service.d.ts +7 -0
- package/dist/backend/backend/src/services/chat-v2/chat-v2.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/chat-v2/chat-v2.service.js +9 -0
- package/dist/backend/backend/src/services/chat-v2/chat-v2.service.js.map +1 -1
- package/dist/backend/backend/src/services/chat-v2/sqlite/message.store.d.ts +7 -0
- package/dist/backend/backend/src/services/chat-v2/sqlite/message.store.d.ts.map +1 -1
- package/dist/backend/backend/src/services/chat-v2/sqlite/message.store.js +19 -0
- package/dist/backend/backend/src/services/chat-v2/sqlite/message.store.js.map +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 +68 -5
- package/dist/backend/backend/src/services/decisions/decision.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/decisions/decision.service.js +262 -33
- 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/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 +29 -0
- 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 +100 -10
- 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 +3 -2
- package/dist/backend/backend/src/services/open-items/open-items.wiring.js.map +1 -1
- package/dist/backend/backend/src/services/orc/agent-prompt-reference.service.d.ts +81 -0
- package/dist/backend/backend/src/services/orc/agent-prompt-reference.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/orc/agent-prompt-reference.service.js +88 -0
- package/dist/backend/backend/src/services/orc/agent-prompt-reference.service.js.map +1 -0
- package/dist/backend/backend/src/services/orc/orc-reply-route.service.d.ts +8 -0
- package/dist/backend/backend/src/services/orc/orc-reply-route.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orc/orc-reply-route.service.js +11 -0
- package/dist/backend/backend/src/services/orc/orc-reply-route.service.js.map +1 -1
- package/dist/backend/backend/src/services/orc/reply-destination-resolver.d.ts +218 -0
- package/dist/backend/backend/src/services/orc/reply-destination-resolver.d.ts.map +1 -0
- package/dist/backend/backend/src/services/orc/reply-destination-resolver.js +343 -0
- package/dist/backend/backend/src/services/orc/reply-destination-resolver.js.map +1 -0
- package/dist/backend/backend/src/services/orc/reply-destination.wiring.d.ts +98 -0
- package/dist/backend/backend/src/services/orc/reply-destination.wiring.d.ts.map +1 -0
- package/dist/backend/backend/src/services/orc/reply-destination.wiring.js +362 -0
- package/dist/backend/backend/src/services/orc/reply-destination.wiring.js.map +1 -0
- package/dist/backend/backend/src/services/slack/slack-agent-dm.service.d.ts +9 -1
- package/dist/backend/backend/src/services/slack/slack-agent-dm.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-agent-dm.service.js +32 -7
- package/dist/backend/backend/src/services/slack/slack-agent-dm.service.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-agent-post.service.d.ts +2 -2
- package/dist/backend/backend/src/services/slack/slack-agent-post.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-agent-post.service.js +9 -0
- package/dist/backend/backend/src/services/slack/slack-agent-post.service.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-initializer.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-initializer.js +4 -1
- package/dist/backend/backend/src/services/slack/slack-initializer.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts +1 -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 +24 -4
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-typing-placeholder.service.d.ts +89 -7
- package/dist/backend/backend/src/services/slack/slack-typing-placeholder.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-typing-placeholder.service.js +159 -21
- package/dist/backend/backend/src/services/slack/slack-typing-placeholder.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/v3/ticket-intake.service.d.ts +23 -4
- package/dist/backend/backend/src/services/v3/ticket-intake.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/ticket-intake.service.js +42 -4
- package/dist/backend/backend/src/services/v3/ticket-intake.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 +45 -1
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +55 -2
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/services/chat-v2/chat-v2.service.d.ts +7 -0
- package/dist/cli/backend/src/services/chat-v2/chat-v2.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/chat-v2/chat-v2.service.js +9 -0
- package/dist/cli/backend/src/services/chat-v2/chat-v2.service.js.map +1 -1
- package/dist/cli/backend/src/services/chat-v2/sqlite/message.store.d.ts +7 -0
- package/dist/cli/backend/src/services/chat-v2/sqlite/message.store.d.ts.map +1 -1
- package/dist/cli/backend/src/services/chat-v2/sqlite/message.store.js +19 -0
- package/dist/cli/backend/src/services/chat-v2/sqlite/message.store.js.map +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 +68 -5
- package/dist/cli/backend/src/services/decisions/decision.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/decisions/decision.service.js +262 -33
- package/dist/cli/backend/src/services/decisions/decision.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 +457 -0
- package/dist/cli/backend/src/services/open-items/open-items.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/open-items/open-items.service.js +1164 -0
- package/dist/cli/backend/src/services/open-items/open-items.service.js.map +1 -0
- package/dist/cli/backend/src/services/orc/agent-prompt-reference.service.d.ts +81 -0
- package/dist/cli/backend/src/services/orc/agent-prompt-reference.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/orc/agent-prompt-reference.service.js +88 -0
- package/dist/cli/backend/src/services/orc/agent-prompt-reference.service.js.map +1 -0
- package/dist/cli/backend/src/services/orc/orc-reply-route.service.d.ts +8 -0
- package/dist/cli/backend/src/services/orc/orc-reply-route.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/orc/orc-reply-route.service.js +11 -0
- package/dist/cli/backend/src/services/orc/orc-reply-route.service.js.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-agent-dm.service.d.ts +9 -1
- package/dist/cli/backend/src/services/slack/slack-agent-dm.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-agent-dm.service.js +32 -7
- package/dist/cli/backend/src/services/slack/slack-agent-dm.service.js.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts +1 -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 +24 -4
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-typing-placeholder.service.d.ts +89 -7
- package/dist/cli/backend/src/services/slack/slack-typing-placeholder.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-typing-placeholder.service.js +159 -21
- package/dist/cli/backend/src/services/slack/slack-typing-placeholder.service.js.map +1 -1
- package/dist/cli/backend/src/services/v3/ticket-intake.service.d.ts +23 -4
- package/dist/cli/backend/src/services/v3/ticket-intake.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/v3/ticket-intake.service.js +42 -4
- package/dist/cli/backend/src/services/v3/ticket-intake.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/package.json +1 -1
|
@@ -14,11 +14,13 @@
|
|
|
14
14
|
*
|
|
15
15
|
* @module services/decisions/decision.service
|
|
16
16
|
*/
|
|
17
|
+
import { isAudioOrVideo } from '../../utils/inbound-file-hint.utils.js';
|
|
18
|
+
import { AgentPromptReferenceService } from '../orc/agent-prompt-reference.service.js';
|
|
17
19
|
import { DECISION_CONSTANTS, OPEN_ITEMS_CONSTANTS, ORCHESTRATOR_SESSION_NAME } from '../../constants.js';
|
|
18
20
|
import { questionSimilarity } from '../open-items/open-item-card.js';
|
|
19
21
|
import { LoggerService } from '../core/logger.service.js';
|
|
20
22
|
import { DecisionContractError, matchOption, parseOptions, resolveDefault, validateAskOwner } from './decision-contract.js';
|
|
21
|
-
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';
|
|
22
24
|
import { PENDING_DECISION_STATUSES } from './decision-store.js';
|
|
23
25
|
/** Thrown for requests the caller must fix (HTTP 4xx). */
|
|
24
26
|
export class DecisionError extends Error {
|
|
@@ -84,6 +86,12 @@ export class DecisionService {
|
|
|
84
86
|
now;
|
|
85
87
|
timer = null;
|
|
86
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;
|
|
87
95
|
/**
|
|
88
96
|
* @param deps - Collaborators
|
|
89
97
|
*/
|
|
@@ -285,6 +293,7 @@ export class DecisionService {
|
|
|
285
293
|
...(ask.sensitive ? { sensitive: ask.sensitive } : {}),
|
|
286
294
|
...(ask.browser ? { browser: ask.browser } : {}),
|
|
287
295
|
...(ask.title ? { title: ask.title } : {}),
|
|
296
|
+
...(ask.body?.length ? { body: ask.body } : {}),
|
|
288
297
|
...(ask.place ? { place: ask.place } : {}),
|
|
289
298
|
...(ask.requestRef ? { requestRef: ask.requestRef } : {}),
|
|
290
299
|
...(ask.source ? { source: ask.source } : {}),
|
|
@@ -492,8 +501,12 @@ export class DecisionService {
|
|
|
492
501
|
return this.apply(decision, choice, 'reaction', event.user);
|
|
493
502
|
}
|
|
494
503
|
/**
|
|
495
|
-
* An owner reply in a card's thread
|
|
496
|
-
* 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.
|
|
497
510
|
*
|
|
498
511
|
* @param message - Inbound Slack message
|
|
499
512
|
* @returns What happened
|
|
@@ -508,14 +521,75 @@ export class DecisionService {
|
|
|
508
521
|
const candidates = await this.deps.store.list((d) => d.status === 'open' &&
|
|
509
522
|
!!d.card &&
|
|
510
523
|
d.card.slackChannelId === message.channelId &&
|
|
511
|
-
(d.card.threadTs === message.threadTs || d.card.messageTs === message.threadTs)
|
|
512
|
-
|
|
513
|
-
|
|
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)
|
|
514
528
|
return { handled: false, reason: 'no open card in this thread' };
|
|
515
|
-
const
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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] };
|
|
519
593
|
}
|
|
520
594
|
/**
|
|
521
595
|
* The owner answered from the dashboard.
|
|
@@ -660,7 +734,9 @@ export class DecisionService {
|
|
|
660
734
|
async cancelWhere(filter, note) {
|
|
661
735
|
const open = await this.deps.store.list((d) => PENDING_DECISION_STATUSES.has(d.status) && filter(d));
|
|
662
736
|
for (const d of open) {
|
|
663
|
-
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);
|
|
664
740
|
if (!done)
|
|
665
741
|
continue;
|
|
666
742
|
await this.refreshCard(done);
|
|
@@ -701,7 +777,7 @@ export class DecisionService {
|
|
|
701
777
|
* Apply a choice: resolve (or snooze), update the card, log the ticket,
|
|
702
778
|
* tell the asker, close the watchdog entry.
|
|
703
779
|
*/
|
|
704
|
-
async apply(decision, choice, via, user, batchNotes) {
|
|
780
|
+
async apply(decision, choice, via, user, batchNotes, files = []) {
|
|
705
781
|
const now = this.now();
|
|
706
782
|
if (choice.kind === 'skip')
|
|
707
783
|
return this.applySkip(decision, via, user, batchNotes);
|
|
@@ -734,14 +810,22 @@ export class DecisionService {
|
|
|
734
810
|
}
|
|
735
811
|
const patch = choice.kind === 'option'
|
|
736
812
|
? { status: 'resolved', chosenKey: choice.key, answerText: undefined }
|
|
737
|
-
:
|
|
813
|
+
: choice.kind === 'thread'
|
|
814
|
+
? { status: 'resolved', chosenKey: undefined, answerText: choice.text?.slice(0, 2000), answerFiles: choice.files }
|
|
815
|
+
: { status: 'resolved', chosenKey: undefined, answerText: choice.text.slice(0, 2000) };
|
|
816
|
+
if (files.length > 0 && choice.kind !== 'thread')
|
|
817
|
+
patch.answerFiles = files;
|
|
738
818
|
const resolved = await this.deps.store.update(decision.id, (cur) => PENDING_DECISION_STATUSES.has(cur.status)
|
|
739
819
|
? { ...patch, answeredVia: via, ...(user ? { answeredBy: user } : {}), resolvedAt: now.toISOString(), remindAt: undefined }
|
|
740
820
|
: null);
|
|
741
821
|
if (!resolved)
|
|
742
822
|
return { handled: false, reason: 'already settled', decision };
|
|
743
823
|
await this.refreshCard(resolved);
|
|
744
|
-
const answer = resolved.chosenKey
|
|
824
|
+
const answer = resolved.chosenKey
|
|
825
|
+
? optionLabel(resolved, resolved.chosenKey)
|
|
826
|
+
: resolved.answeredVia === 'thread'
|
|
827
|
+
? `answered in the thread with ${describeAnswerFiles(resolved.answerFiles ?? [])}`
|
|
828
|
+
: `“${resolved.answerText}”`;
|
|
745
829
|
if (resolved.ticket)
|
|
746
830
|
await this.logTicket(resolved, `owner decision ${resolved.id}: ${answer} (${via})`, true);
|
|
747
831
|
await this.notifyAsker(resolved, this.answerNote(resolved), batchNotes);
|
|
@@ -776,17 +860,26 @@ export class DecisionService {
|
|
|
776
860
|
answerNote(d) {
|
|
777
861
|
const where = this.whereLine(d);
|
|
778
862
|
const about = `for: "${d.question}"${d.ticket ? ` (ticket ${d.ticket.id})` : ''}`;
|
|
863
|
+
const files = filesLine(d.answerFiles ?? []);
|
|
779
864
|
if (d.chosenKey) {
|
|
780
|
-
return `[DECISION ${d.id}] The owner chose "${optionLabel(d, d.chosenKey)}" ${about}. Act on it now.${where}`;
|
|
865
|
+
return `[DECISION ${d.id}] The owner chose "${optionLabel(d, d.chosenKey)}" ${about}. Act on it now.${files}${where}`;
|
|
866
|
+
}
|
|
867
|
+
if (d.answeredVia === 'thread') {
|
|
868
|
+
const what = describeAnswerFiles(d.answerFiles ?? []);
|
|
869
|
+
const transcript = d.answerText ? ` Slack's transcript: "${d.answerText}".` : '';
|
|
870
|
+
return (`[DECISION ${d.id}] The owner answered ${about} in the card's thread with ${what} (no text).${transcript}${files} ` +
|
|
871
|
+
`Read it as their decision and act on it; if it is genuinely unclear, ask once more with ask-owner.${where}`);
|
|
781
872
|
}
|
|
782
|
-
return `[DECISION ${d.id}] The owner answered in words ${about}: "${d.answerText ?? ''}"
|
|
873
|
+
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}`;
|
|
783
874
|
}
|
|
784
|
-
/**
|
|
875
|
+
/**
|
|
876
|
+
* Where the asker's follow-up belongs: a command naming the decision — the
|
|
877
|
+
* harness finds the card's thread (specs/2026-10-02-harness-owned-routing.md §4).
|
|
878
|
+
*/
|
|
785
879
|
whereLine(d) {
|
|
786
880
|
if (!d.card)
|
|
787
881
|
return '';
|
|
788
|
-
|
|
789
|
-
return ` Post any update in the card's thread (--thread ${d.card.slackChannelId}:${thread}).`;
|
|
882
|
+
return ` To post an update for the owner, run: reply --decision ${d.id} "<your message>" — Crewly posts it in the card's thread.`;
|
|
790
883
|
}
|
|
791
884
|
// ---------------------------------------------------------------------------
|
|
792
885
|
// Deadlines and reminders
|
|
@@ -804,6 +897,9 @@ export class DecisionService {
|
|
|
804
897
|
const acted = [];
|
|
805
898
|
try {
|
|
806
899
|
const now = this.now();
|
|
900
|
+
if (this.startedAt === null)
|
|
901
|
+
this.startedAt = now.getTime();
|
|
902
|
+
const reminders = [];
|
|
807
903
|
for (const d of await this.deps.store.list((x) => x.status === 'open')) {
|
|
808
904
|
try {
|
|
809
905
|
if (!d.card) {
|
|
@@ -821,6 +917,14 @@ export class DecisionService {
|
|
|
821
917
|
}
|
|
822
918
|
if (Date.parse(d.deadline) > now.getTime())
|
|
823
919
|
continue;
|
|
920
|
+
if (d.defaultKey === DECISION_CONSTANTS.WAIT_DEFAULT && !d.sensitive) {
|
|
921
|
+
const step = await this.waitStep(d, now);
|
|
922
|
+
if (step === 'acted')
|
|
923
|
+
acted.push(d.id);
|
|
924
|
+
else if (step === 'remind')
|
|
925
|
+
reminders.push(d);
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
824
928
|
// A default that never lets anything through (a held browser
|
|
825
929
|
// action's No, a declined Terms card) is applied even when sensitive.
|
|
826
930
|
if (d.sensitive && !defaultIsSafe(d)) {
|
|
@@ -835,6 +939,7 @@ export class DecisionService {
|
|
|
835
939
|
this.logger.warn('Decision tick step failed', { decisionId: d.id, error: errText(err) });
|
|
836
940
|
}
|
|
837
941
|
}
|
|
942
|
+
acted.push(...(await this.postWaitReminders(reminders, now)));
|
|
838
943
|
await this.deps.store.prune().catch(() => 0);
|
|
839
944
|
}
|
|
840
945
|
finally {
|
|
@@ -842,30 +947,111 @@ export class DecisionService {
|
|
|
842
947
|
}
|
|
843
948
|
return acted;
|
|
844
949
|
}
|
|
950
|
+
/**
|
|
951
|
+
* Withdraw a card whose tracked item is already closed, silently: the
|
|
952
|
+
* card says why, nothing is posted in the thread and the asker is not woken
|
|
953
|
+
* (specs/2026-10-02-decision-card-thread-answers.md §3).
|
|
954
|
+
*
|
|
955
|
+
* @param d - Open decision about to post something to the owner
|
|
956
|
+
* @returns True when it was withdrawn
|
|
957
|
+
*/
|
|
958
|
+
async withdrawIfMoot(d) {
|
|
959
|
+
if (!this.deps.trackedClosed)
|
|
960
|
+
return false;
|
|
961
|
+
const reason = await this.deps.trackedClosed(d).catch(() => null);
|
|
962
|
+
if (!reason)
|
|
963
|
+
return false;
|
|
964
|
+
const n = await this.cancelWhere((x) => x.id === d.id, reason);
|
|
965
|
+
if (n === 0)
|
|
966
|
+
return false;
|
|
967
|
+
this.logger.info('Decision card withdrawn before posting: what it tracks is already closed', { decisionId: d.id, reason });
|
|
968
|
+
// One short note, so a ticket closed by mistake is recoverable.
|
|
969
|
+
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.`);
|
|
970
|
+
return true;
|
|
971
|
+
}
|
|
845
972
|
/** Post the "Remind me tomorrow" reminder in the card's thread. */
|
|
846
973
|
async remindNow(d) {
|
|
974
|
+
if (await this.withdrawIfMoot(d))
|
|
975
|
+
return;
|
|
847
976
|
const updated = await this.deps.store.update(d.id, (cur) => (cur.status === 'open' && cur.remindAt ? { remindAt: undefined } : null));
|
|
848
977
|
if (!updated)
|
|
849
978
|
return;
|
|
850
979
|
const owner = this.deps.ownerUserId?.();
|
|
851
|
-
await this.postInThread(updated, `${owner ? `<@${owner}> ` : ''}Reminder: ${updated.question}
|
|
980
|
+
await this.postInThread(updated, `${owner ? `<@${owner}> ` : ''}Reminder: ${updated.question} — tap an answer on the card above, or reply here.`);
|
|
852
981
|
await this.refreshCard(updated);
|
|
853
982
|
}
|
|
854
|
-
/**
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
983
|
+
/**
|
|
984
|
+
* A `wait` card past its deadline (specs/2026-10-02-decision-card-thread-answers.md §2):
|
|
985
|
+
* first only the asker is told — nothing goes to the owner; later, once, a
|
|
986
|
+
* reminder that says what to do, when the owner has not touched the thread.
|
|
987
|
+
*/
|
|
988
|
+
async waitStep(d, now) {
|
|
989
|
+
if (!d.deadlineNoticeAt) {
|
|
990
|
+
if (await this.withdrawIfMoot(d))
|
|
991
|
+
return 'acted';
|
|
860
992
|
const updated = await this.deps.store.update(d.id, (cur) => (cur.status === 'open' && !cur.deadlineNoticeAt ? { deadlineNoticeAt: now.toISOString() } : null));
|
|
861
993
|
if (!updated)
|
|
862
|
-
return
|
|
863
|
-
await this.postInThread(updated, line);
|
|
994
|
+
return null;
|
|
864
995
|
if (updated.ticket)
|
|
865
|
-
await this.logTicket(updated, `owner decision ${updated.id}: no answer by the deadline — still waiting`, false);
|
|
866
|
-
await this.tellAsker(updated, `[DECISION ${updated.id}]
|
|
867
|
-
|
|
996
|
+
await this.logTicket(updated, `owner decision ${updated.id}: no answer by the deadline — still waiting (nothing posted to the owner)`, false);
|
|
997
|
+
await this.tellAsker(updated, `[DECISION ${updated.id}] The deadline for "${updated.question}" passed with no answer. Nothing was posted to the owner. ` +
|
|
998
|
+
`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)}`);
|
|
999
|
+
this.logger.info('Wait-default decision past its deadline — asker told, nothing posted', { decisionId: updated.id });
|
|
1000
|
+
return 'acted';
|
|
1001
|
+
}
|
|
1002
|
+
if (d.waitReminderAt || d.ownerRepliedAt)
|
|
1003
|
+
return null;
|
|
1004
|
+
if (this.startedAt !== null && Date.parse(d.deadlineNoticeAt) < this.startedAt) {
|
|
1005
|
+
// Noticed before this process started (e.g. before the upgrade that
|
|
1006
|
+
// added reminders): marked, never reminded — no burst on first boot.
|
|
1007
|
+
await this.deps.store.update(d.id, (cur) => (cur.status === 'open' && !cur.waitReminderAt ? { waitReminderAt: now.toISOString() } : null));
|
|
1008
|
+
this.logger.debug('Wait-default decision noticed before this start — no reminder', { decisionId: d.id });
|
|
1009
|
+
return null;
|
|
1010
|
+
}
|
|
1011
|
+
if (now.getTime() < Date.parse(d.deadlineNoticeAt) + DECISION_CONSTANTS.WAIT_REMINDER_DELAY_MS)
|
|
1012
|
+
return null;
|
|
1013
|
+
if (await this.withdrawIfMoot(d))
|
|
1014
|
+
return 'acted';
|
|
1015
|
+
return 'remind';
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Post the due `wait` reminders: ONE per thread, listing that thread's open
|
|
1019
|
+
* questions; every listed card is marked as reminded.
|
|
1020
|
+
*
|
|
1021
|
+
* @param due - Cards due for their reminder (already checked)
|
|
1022
|
+
* @param now - Clock
|
|
1023
|
+
* @returns Ids reminded
|
|
1024
|
+
*/
|
|
1025
|
+
async postWaitReminders(due, now) {
|
|
1026
|
+
const byThread = new Map();
|
|
1027
|
+
for (const d of due) {
|
|
1028
|
+
if (!d.card)
|
|
1029
|
+
continue;
|
|
1030
|
+
const key = `${d.card.slackChannelId}:${d.card.threadTs ?? d.card.messageTs}`;
|
|
1031
|
+
byThread.set(key, [...(byThread.get(key) ?? []), d]);
|
|
868
1032
|
}
|
|
1033
|
+
const reminded = [];
|
|
1034
|
+
for (const group of byThread.values()) {
|
|
1035
|
+
const marked = [];
|
|
1036
|
+
for (const d of group) {
|
|
1037
|
+
const updated = await this.deps.store.update(d.id, (cur) => cur.status === 'open' && !cur.waitReminderAt && !cur.ownerRepliedAt ? { waitReminderAt: now.toISOString() } : null);
|
|
1038
|
+
if (updated)
|
|
1039
|
+
marked.push(updated);
|
|
1040
|
+
}
|
|
1041
|
+
if (marked.length === 0)
|
|
1042
|
+
continue;
|
|
1043
|
+
marked.sort((x, y) => Date.parse(x.createdAt) - Date.parse(y.createdAt) || idNumber(x.id) - idNumber(y.id));
|
|
1044
|
+
await this.postInThread(marked[marked.length - 1], waitReminderLine(marked, this.deps.ownerUserId?.()));
|
|
1045
|
+
reminded.push(...marked.map((d) => d.id));
|
|
1046
|
+
this.logger.info('Wait-default decisions: one reminder posted for the thread', { decisionIds: marked.map((d) => d.id) });
|
|
1047
|
+
}
|
|
1048
|
+
return reminded;
|
|
1049
|
+
}
|
|
1050
|
+
/** Non-sensitive deadline with a real default: apply it and say who does what. */
|
|
1051
|
+
async applyDefault(d, now) {
|
|
1052
|
+
if (await this.withdrawIfMoot(d))
|
|
1053
|
+
return true;
|
|
1054
|
+
const line = deadlineDefaultLine(d, now, await this.askerName(d));
|
|
869
1055
|
const resolved = await this.deps.store.update(d.id, (cur) => cur.status === 'open' ? { status: 'defaulted', chosenKey: cur.defaultKey, answeredVia: 'deadline', resolvedAt: now.toISOString() } : null);
|
|
870
1056
|
if (!resolved)
|
|
871
1057
|
return false;
|
|
@@ -884,6 +1070,8 @@ export class DecisionService {
|
|
|
884
1070
|
const due = Math.max(Date.parse(d.deadline), Date.parse(d.createdAt) + DECISION_CONSTANTS.SENSITIVE_REASK_AFTER_MS);
|
|
885
1071
|
if (now.getTime() < due)
|
|
886
1072
|
return false;
|
|
1073
|
+
if (await this.withdrawIfMoot(d))
|
|
1074
|
+
return true;
|
|
887
1075
|
const updated = await this.deps.store.update(d.id, (cur) => (cur.status === 'open' && !cur.reaskedAt ? { reaskedAt: now.toISOString() } : null));
|
|
888
1076
|
if (!updated)
|
|
889
1077
|
return false;
|
|
@@ -918,8 +1106,9 @@ export class DecisionService {
|
|
|
918
1106
|
let ownerName;
|
|
919
1107
|
if (!pending && d.answeredBy)
|
|
920
1108
|
ownerName = await this.deps.userName?.(d.answeredBy).catch(() => undefined);
|
|
921
|
-
const
|
|
922
|
-
const
|
|
1109
|
+
const askerName = d.status === 'defaulted' ? await this.askerName(d) : undefined;
|
|
1110
|
+
const blocks = pending ? renderOpenCard(d, this.deps.instanceId(), now) : renderSettledCard(d, ownerName, now, askerName);
|
|
1111
|
+
const text = pending ? cardFallbackText(d) : `${cardFallbackText(d)} — ${settledLine(d, ownerName, now, askerName)}`;
|
|
923
1112
|
const token = d.card.ownBot ? (await this.deps.identityOf(d.card.postedBy).catch(() => ({}))).botToken : undefined;
|
|
924
1113
|
try {
|
|
925
1114
|
await slack.updateMessage(d.card.slackChannelId, d.card.messageTs, text, blocks, token);
|
|
@@ -930,6 +1119,16 @@ export class DecisionService {
|
|
|
930
1119
|
return false;
|
|
931
1120
|
}
|
|
932
1121
|
}
|
|
1122
|
+
/** The asking agent's display name ("Owen"), when known. */
|
|
1123
|
+
async askerName(d) {
|
|
1124
|
+
if (d.system)
|
|
1125
|
+
return undefined;
|
|
1126
|
+
const named = await this.deps.displayName?.(d.asker).catch(() => undefined);
|
|
1127
|
+
if (named)
|
|
1128
|
+
return named;
|
|
1129
|
+
const identity = await this.deps.identityOf(d.asker).catch(() => ({}));
|
|
1130
|
+
return identity.username && identity.username !== d.asker ? identity.username : undefined;
|
|
1131
|
+
}
|
|
933
1132
|
/** Post a line in the card's thread, as the asker. */
|
|
934
1133
|
async postInThread(d, text) {
|
|
935
1134
|
const slack = this.deps.slack();
|
|
@@ -982,6 +1181,10 @@ export class DecisionService {
|
|
|
982
1181
|
if (d.system)
|
|
983
1182
|
return;
|
|
984
1183
|
const ok = await this.deps.deliverToAgent(d.asker, text).catch(() => false);
|
|
1184
|
+
// A bare `reply` after this prompt follows the decision, not an unrelated
|
|
1185
|
+
// newer work item — recorded only once it was delivered.
|
|
1186
|
+
if (ok && (d.card || d.ticket))
|
|
1187
|
+
AgentPromptReferenceService.getInstance().note(d.asker, { decisionId: d.id }, `[DECISION ${d.id}]`);
|
|
985
1188
|
if (!ok)
|
|
986
1189
|
this.logger.warn('Could not deliver the decision to the asking agent', { decisionId: d.id, asker: d.asker });
|
|
987
1190
|
// The orchestrator asked on the owner's behalf for a ticket it does not own: tell it too.
|
|
@@ -1024,6 +1227,32 @@ function systemChoiceFromText(d, text) {
|
|
|
1024
1227
|
}
|
|
1025
1228
|
return null;
|
|
1026
1229
|
}
|
|
1230
|
+
/**
|
|
1231
|
+
* The number of a `D-<n>` id (0 when unreadable), for ordering.
|
|
1232
|
+
*
|
|
1233
|
+
* @param id - Decision id
|
|
1234
|
+
* @returns n
|
|
1235
|
+
*/
|
|
1236
|
+
function idNumber(id) {
|
|
1237
|
+
return Number(/(\d+)$/.exec(id)?.[1] ?? 0);
|
|
1238
|
+
}
|
|
1239
|
+
/**
|
|
1240
|
+
* The files line of an answer note: name, type and link of each file, and how
|
|
1241
|
+
* to hear a voice note that came without a transcript.
|
|
1242
|
+
*
|
|
1243
|
+
* @param files - Answer files
|
|
1244
|
+
* @returns Text starting with a space, or '' for none
|
|
1245
|
+
*/
|
|
1246
|
+
function filesLine(files) {
|
|
1247
|
+
if (files.length === 0)
|
|
1248
|
+
return '';
|
|
1249
|
+
const list = files.map((f) => `${f.name}${f.mimetype ? ` (${f.mimetype})` : ''}${f.permalink ? ` ${f.permalink}` : ''}`).join('; ');
|
|
1250
|
+
const unheard = files.some((f) => !f.transcript && /^(audio|video)\//.test(f.mimetype ?? ''));
|
|
1251
|
+
const hint = unheard
|
|
1252
|
+
? ' 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).'
|
|
1253
|
+
: '';
|
|
1254
|
+
return ` Files: ${list}.${hint}`;
|
|
1255
|
+
}
|
|
1027
1256
|
/**
|
|
1028
1257
|
* Message of an unknown error.
|
|
1029
1258
|
*
|