crewly 1.20.144 → 1.20.146
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/README.md +5 -1
- package/config/roles/orchestrator/prompt.md +9 -0
- package/config/skills/orchestrator/harness-login/SKILL.md +88 -0
- package/config/skills/orchestrator/harness-login/execute.sh +75 -0
- package/config/skills/orchestrator/harness-login/execute.test.sh +72 -0
- package/dist/backend/backend/src/constants.d.ts +12 -0
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +12 -0
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/harness/harness.routes.d.ts +1 -0
- package/dist/backend/backend/src/controllers/harness/harness.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/harness/harness.routes.js +3 -0
- package/dist/backend/backend/src/controllers/harness/harness.routes.js.map +1 -1
- package/dist/backend/backend/src/controllers/harness/owner-login.controller.d.ts +61 -0
- package/dist/backend/backend/src/controllers/harness/owner-login.controller.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/harness/owner-login.controller.js +133 -0
- package/dist/backend/backend/src/controllers/harness/owner-login.controller.js.map +1 -0
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +4 -1
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.js +23 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/codex-runtime.service.d.ts +11 -1
- package/dist/backend/backend/src/services/agent/codex-runtime.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/codex-runtime.service.js +18 -0
- package/dist/backend/backend/src/services/agent/codex-runtime.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts +12 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js +18 -3
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.d.ts +29 -0
- package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.js +48 -0
- package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.js.map +1 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js +1 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-sync.service.d.ts +14 -0
- package/dist/backend/backend/src/services/cloud/cloud-sync.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-sync.service.js +18 -2
- package/dist/backend/backend/src/services/cloud/cloud-sync.service.js.map +1 -1
- package/dist/backend/backend/src/services/harness/harness-relogin.service.d.ts +147 -5
- package/dist/backend/backend/src/services/harness/harness-relogin.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/harness/harness-relogin.service.js +210 -20
- package/dist/backend/backend/src/services/harness/harness-relogin.service.js.map +1 -1
- package/dist/backend/backend/src/services/harness/owner-login-request.d.ts +87 -0
- package/dist/backend/backend/src/services/harness/owner-login-request.d.ts.map +1 -0
- package/dist/backend/backend/src/services/harness/owner-login-request.js +191 -0
- package/dist/backend/backend/src/services/harness/owner-login-request.js.map +1 -0
- package/dist/backend/backend/src/services/slack/slack-relogin-dm.service.d.ts +68 -9
- package/dist/backend/backend/src/services/slack/slack-relogin-dm.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-relogin-dm.service.js +121 -11
- package/dist/backend/backend/src/services/slack/slack-relogin-dm.service.js.map +1 -1
- package/dist/backend/backend/src/utils/agent-reply-filter.utils.d.ts +75 -0
- package/dist/backend/backend/src/utils/agent-reply-filter.utils.d.ts.map +1 -0
- package/dist/backend/backend/src/utils/agent-reply-filter.utils.js +127 -0
- package/dist/backend/backend/src/utils/agent-reply-filter.utils.js.map +1 -0
- package/dist/backend/build-info.json +2 -2
- package/dist/cli/backend/src/constants.d.ts +12 -0
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +12 -0
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/agent-registration.service.js +23 -1
- package/dist/cli/backend/src/services/agent/agent-registration.service.js.map +1 -1
- package/dist/cli/backend/src/services/agent/codex-runtime.service.d.ts +11 -1
- package/dist/cli/backend/src/services/agent/codex-runtime.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/codex-runtime.service.js +18 -0
- package/dist/cli/backend/src/services/agent/codex-runtime.service.js.map +1 -1
- package/dist/cli/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts +12 -0
- package/dist/cli/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js +18 -3
- package/dist/cli/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js.map +1 -1
- package/dist/cli/backend/src/services/agent/runtime-agent.service.abstract.d.ts +29 -0
- package/dist/cli/backend/src/services/agent/runtime-agent.service.abstract.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/runtime-agent.service.abstract.js +48 -0
- package/dist/cli/backend/src/services/agent/runtime-agent.service.abstract.js.map +1 -1
- package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
- package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js +1 -1
- package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
- package/dist/cli/backend/src/services/cloud/cloud-sync.service.d.ts +14 -0
- package/dist/cli/backend/src/services/cloud/cloud-sync.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/cloud/cloud-sync.service.js +18 -2
- package/dist/cli/backend/src/services/cloud/cloud-sync.service.js.map +1 -1
- package/dist/cli/backend/src/services/harness/harness-relogin.service.d.ts +147 -5
- package/dist/cli/backend/src/services/harness/harness-relogin.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/harness/harness-relogin.service.js +210 -20
- package/dist/cli/backend/src/services/harness/harness-relogin.service.js.map +1 -1
- package/dist/cli/backend/src/services/harness/owner-login-request.d.ts +87 -0
- package/dist/cli/backend/src/services/harness/owner-login-request.d.ts.map +1 -0
- package/dist/cli/backend/src/services/harness/owner-login-request.js +191 -0
- package/dist/cli/backend/src/services/harness/owner-login-request.js.map +1 -0
- package/dist/cli/backend/src/utils/agent-reply-filter.utils.d.ts +75 -0
- package/dist/cli/backend/src/utils/agent-reply-filter.utils.d.ts.map +1 -0
- package/dist/cli/backend/src/utils/agent-reply-filter.utils.js +127 -0
- package/dist/cli/backend/src/utils/agent-reply-filter.utils.js.map +1 -0
- package/package.json +1 -1
- package/packages/crewly-agent/src/runtime/agent-runner.service.test.ts +39 -0
- package/packages/crewly-agent/src/runtime/agent-runner.service.ts +17 -17
- package/packages/crewly-agent/src/runtime/interactive-login-guard.test.ts +60 -0
- package/packages/crewly-agent/src/runtime/interactive-login-guard.ts +76 -0
- package/packages/crewly-agent/src/runtime/reply-tool-calls.test.ts +53 -0
- package/packages/crewly-agent/src/runtime/reply-tool-calls.ts +76 -0
- package/packages/crewly-agent/src/runtime/tool-registry.test.ts +8 -0
- package/packages/crewly-agent/src/runtime/tool-registry.ts +14 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owner login requests — recognising "log harness X in" in plain words.
|
|
3
|
+
*
|
|
4
|
+
* Two consumers:
|
|
5
|
+
* - {@link parseOwnerLoginRequest}: the Slack DM interceptor. An owner who
|
|
6
|
+
* writes 「重新登录 claude」, 「换个账号登录 claude」 or "relogin codex" in the
|
|
7
|
+
* orchestrator's DM gets the login flow directly, without the LLM. The
|
|
8
|
+
* match is deliberately strict — the whole message must be the request —
|
|
9
|
+
* so a sentence that merely mentions a login still goes to the orc.
|
|
10
|
+
* - {@link isOwnerLoginRequestEvidence}: the orchestrator's `harness-login`
|
|
11
|
+
* skill. The orc may start a login only when a real owner message asked for
|
|
12
|
+
* one; this is the evidence check over the owner's recent messages.
|
|
13
|
+
*
|
|
14
|
+
* Incident 2026-09-26: the orc ran `claude setup-token` in its one-shot bash
|
|
15
|
+
* tool (the process died with the call, every pasted code went stale) and
|
|
16
|
+
* then claimed several times that it had sent a new link. The broker-backed
|
|
17
|
+
* flow started from here keeps the login alive in its own PTY.
|
|
18
|
+
*
|
|
19
|
+
* @module services/harness/owner-login-request
|
|
20
|
+
*/
|
|
21
|
+
import { HARNESS_CONSTANTS } from '../../constants.js';
|
|
22
|
+
/** Harness names and aliases → id, longest first so "claude code" wins over "claude". */
|
|
23
|
+
const HARNESS_NAME_PATTERNS = [
|
|
24
|
+
{ re: /^claude[\s_-]*code$/, id: HARNESS_CONSTANTS.IDS.CLAUDE_CODE },
|
|
25
|
+
{ re: /^claude$/, id: HARNESS_CONSTANTS.IDS.CLAUDE_CODE },
|
|
26
|
+
{ re: /^codex(?:[\s_-]*cli)?$/, id: HARNESS_CONSTANTS.IDS.CODEX_CLI },
|
|
27
|
+
{ re: /^antigravity(?:[\s_-]*cli)?$/, id: HARNESS_CONSTANTS.IDS.ANTIGRAVITY_CLI },
|
|
28
|
+
{ re: /^agy$/, id: HARNESS_CONSTANTS.IDS.ANTIGRAVITY_CLI },
|
|
29
|
+
{ re: /^gemini(?:[\s_-]*cli)?$/, id: HARNESS_CONSTANTS.IDS.GEMINI_CLI },
|
|
30
|
+
];
|
|
31
|
+
/** Any harness mention inside a longer message (evidence check). */
|
|
32
|
+
const HARNESS_MENTIONS = [
|
|
33
|
+
{ re: /claude/i, id: HARNESS_CONSTANTS.IDS.CLAUDE_CODE },
|
|
34
|
+
{ re: /codex/i, id: HARNESS_CONSTANTS.IDS.CODEX_CLI },
|
|
35
|
+
{ re: /antigravity|\bagy\b/i, id: HARNESS_CONSTANTS.IDS.ANTIGRAVITY_CLI },
|
|
36
|
+
{ re: /gemini/i, id: HARNESS_CONSTANTS.IDS.GEMINI_CLI },
|
|
37
|
+
];
|
|
38
|
+
/**
|
|
39
|
+
* Names that are coding assistants Crewly does not run. "登录 cursor" is
|
|
40
|
+
* answered with the supported list; any other unknown word after a login
|
|
41
|
+
* verb ("登录 gmail") is not a harness request and goes to the orc.
|
|
42
|
+
*/
|
|
43
|
+
const OTHER_CODING_TOOLS = /^(?:cursor|copilot|github\s*copilot|aider|opencode|cline|windsurf|kimi(?:\s*cli)?|qwen(?:\s*code)?|amp|goose|kiro|trae)$/;
|
|
44
|
+
/** Generic words for "the coding assistant" — a request for a harness without naming one. */
|
|
45
|
+
const GENERIC_HARNESS_WORDS = /^(?:harness|编程助手|编码助手|代码助手|ai\s*harness|cli)$/;
|
|
46
|
+
/** Chinese login verbs. The account-switch forms come first so they win. */
|
|
47
|
+
const ZH_VERB = '(?:换(?:一)?个(?:新)?账号(?:重新)?登[录陆]|换(?:一)?个号(?:重新)?登[录陆]|换账号(?:重新)?登[录陆]|换号(?:重新)?登[录陆]|用(?:另|别)(?:一)?个账号(?:重新)?登[录陆]|切换账号|换(?:一)?个(?:新)?账号|换账号|换号|重新登[录陆入]|重登[录陆]?|再登[录陆]|登[录陆入])';
|
|
48
|
+
/** English login verbs. */
|
|
49
|
+
const EN_VERB = '(?:re-?log\\s*in(?:\\s+to)?|re-?login(?:\\s+to)?|log\\s*in(?:\\s+to)?|login(?:\\s+to)?|sign\\s*in(?:\\s+to)?|re-?auth(?:enticate)?|switch\\s+(?:the\\s+)?accounts?(?:\\s+(?:for|of|on))?)';
|
|
50
|
+
/** Polite lead-ins that do not change the request. */
|
|
51
|
+
const PREFIX = '(?:(?:please|pls|can\\s+you|could\\s+you)\\s+)?(?:帮我|帮忙|麻烦(?:你)?|请(?:你)?|给我|你)?\\s*(?:(?:把|给|帮)\\s*)?';
|
|
52
|
+
/** Harmless endings. No 吗/了: "登录 claude 了吗" asks about a login, it does not request one. */
|
|
53
|
+
const SUFFIX = '\\s*(?:一下|下|吧)?\\s*[.。!!~~]*';
|
|
54
|
+
/** A name after or before the verb: up to three ASCII words, fewest first ("claude code", not "claude account"). */
|
|
55
|
+
const NAME = '([a-z][a-z0-9_-]*(?:\\s+[a-z][a-z0-9_-]*){0,2}?|编程助手|编码助手|代码助手)';
|
|
56
|
+
/** Optional "account" noun after the name ("claude 的账号", "claude account"). */
|
|
57
|
+
const ACCOUNT_NOUN = '(?:\\s*(?:的)?(?:账号|帐号|account))?';
|
|
58
|
+
/** "登录 claude" / "relogin to codex" / "换个账号登录 claude code". */
|
|
59
|
+
const VERB_FIRST = new RegExp(`^${PREFIX}(${ZH_VERB}|${EN_VERB})\\s*(?:到\\s*)?${NAME}${ACCOUNT_NOUN}${SUFFIX}$`, 'i');
|
|
60
|
+
/** "claude 重新登录" / "codex login" / "把 claude 换个账号登录". */
|
|
61
|
+
const NAME_FIRST = new RegExp(`^${PREFIX}${NAME}${ACCOUNT_NOUN}\\s*(?:重新)?(${ZH_VERB}|${EN_VERB})${SUFFIX}$`, 'i');
|
|
62
|
+
/** "给 claude 换个账号" / "switch claude account" / "switch claude to another account". */
|
|
63
|
+
const SWITCH_NAME = new RegExp(`^${PREFIX}(?:switch|change)\\s+${NAME}\\s+(?:to\\s+(?:another|a\\s+different|a\\s+new)\\s+)?accounts?${SUFFIX}$`, 'i');
|
|
64
|
+
/** A bare verb ("重新登录", "login") — a request with no harness named. */
|
|
65
|
+
const BARE_VERB = new RegExp(`^${PREFIX}(${ZH_VERB}|${EN_VERB})${SUFFIX}$`, 'i');
|
|
66
|
+
/** Words that mark an account switch. */
|
|
67
|
+
const SWITCH_WORDS = /换|切换|switch|another\s+account|different\s+account|other\s+account|另一个账号|别的账号|其他账号/i;
|
|
68
|
+
/**
|
|
69
|
+
* Normalise a DM for matching: drop Slack mention/link markup, collapse
|
|
70
|
+
* whitespace, lower-case, trim.
|
|
71
|
+
*
|
|
72
|
+
* @param text - Raw message text
|
|
73
|
+
* @returns Normalised text
|
|
74
|
+
*/
|
|
75
|
+
export function normaliseLoginText(text) {
|
|
76
|
+
return text
|
|
77
|
+
.replace(/<@[A-Z0-9]+>/g, ' ')
|
|
78
|
+
.replace(/<([^|>]+)\|([^>]+)>/g, '$2')
|
|
79
|
+
.replace(/[「」“”"'`]/g, ' ')
|
|
80
|
+
.replace(/\s+/g, ' ')
|
|
81
|
+
.trim()
|
|
82
|
+
.toLowerCase();
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Resolve a harness name or alias as written by a person.
|
|
86
|
+
*
|
|
87
|
+
* @param name - e.g. "claude code", "Claude", "agy", "codex cli"
|
|
88
|
+
* @returns The harness id, or null
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```ts
|
|
92
|
+
* resolveHarnessName('claude code'); // 'claude-code'
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export function resolveHarnessName(name) {
|
|
96
|
+
const key = name.trim().toLowerCase().replace(/\s+/g, ' ');
|
|
97
|
+
for (const { re, id } of HARNESS_NAME_PATTERNS) {
|
|
98
|
+
if (re.test(key))
|
|
99
|
+
return id;
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Turn a matched name into a request, or null when the name is not about a
|
|
105
|
+
* coding assistant at all ("登录 gmail" is for the orc, not for us).
|
|
106
|
+
*
|
|
107
|
+
* @param rawName - Name as matched
|
|
108
|
+
* @param switchAccount - Whether the owner asked for a different account
|
|
109
|
+
* @returns The request or null
|
|
110
|
+
*/
|
|
111
|
+
function requestForName(rawName, switchAccount) {
|
|
112
|
+
const name = rawName.trim();
|
|
113
|
+
const harnessId = resolveHarnessName(name);
|
|
114
|
+
if (harnessId)
|
|
115
|
+
return { kind: 'harness', harnessId, switchAccount };
|
|
116
|
+
if (GENERIC_HARNESS_WORDS.test(name))
|
|
117
|
+
return { kind: 'unknown', name: null, switchAccount };
|
|
118
|
+
if (OTHER_CODING_TOOLS.test(name))
|
|
119
|
+
return { kind: 'unknown', name, switchAccount };
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Recognise an owner's DM that asks Crewly to log a harness in (or switch
|
|
124
|
+
* its account). The whole message must be the request; anything longer or
|
|
125
|
+
* different returns null and is handled by the orchestrator as usual.
|
|
126
|
+
*
|
|
127
|
+
* @param text - Owner DM text
|
|
128
|
+
* @returns The request, or null when the message is not a login request
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```ts
|
|
132
|
+
* parseOwnerLoginRequest('换个账号登录 claude');
|
|
133
|
+
* // { kind: 'harness', harnessId: 'claude-code', switchAccount: true }
|
|
134
|
+
* parseOwnerLoginRequest('重新登录');
|
|
135
|
+
* // { kind: 'unknown', name: null, switchAccount: false }
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
export function parseOwnerLoginRequest(text) {
|
|
139
|
+
if (typeof text !== 'string')
|
|
140
|
+
return null;
|
|
141
|
+
const normalised = normaliseLoginText(text);
|
|
142
|
+
if (!normalised || normalised.length > HARNESS_CONSTANTS.OWNER_LOGIN.MAX_TRIGGER_LENGTH)
|
|
143
|
+
return null;
|
|
144
|
+
const switchAccount = SWITCH_WORDS.test(normalised);
|
|
145
|
+
// Each form in turn; a form whose "name" is not a harness ("relogin" read
|
|
146
|
+
// as name "re" + verb "login") does not end the search.
|
|
147
|
+
const verbFirst = VERB_FIRST.exec(normalised);
|
|
148
|
+
const fromVerbFirst = verbFirst ? requestForName(verbFirst[2], switchAccount) : null;
|
|
149
|
+
if (fromVerbFirst)
|
|
150
|
+
return fromVerbFirst;
|
|
151
|
+
const nameFirst = NAME_FIRST.exec(normalised);
|
|
152
|
+
const fromNameFirst = nameFirst ? requestForName(nameFirst[1], switchAccount) : null;
|
|
153
|
+
if (fromNameFirst)
|
|
154
|
+
return fromNameFirst;
|
|
155
|
+
const switchName = SWITCH_NAME.exec(normalised);
|
|
156
|
+
const fromSwitch = switchName ? requestForName(switchName[1], true) : null;
|
|
157
|
+
if (fromSwitch)
|
|
158
|
+
return fromSwitch;
|
|
159
|
+
if (BARE_VERB.test(normalised))
|
|
160
|
+
return { kind: 'unknown', name: null, switchAccount };
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
/** A login verb anywhere in a message (evidence check; broader than the trigger). */
|
|
164
|
+
const LOGIN_MENTION = /登[录陆入]|重登|换(?:一)?个?账号|切换账号|换号|re-?log\s*in|re-?login|\blog\s*in\b|\blog\s+(?:\S+\s+){1,2}in\b|\blogin\b|sign\s*in|setup-token|\/login|switch\s+(?:the\s+)?accounts?|login\s+link|登[录陆]链接/i;
|
|
165
|
+
/** Status questions — asking whether it is logged in is not asking to log in. */
|
|
166
|
+
const STATUS_QUESTION = /登[录陆]了吗|登[录陆]状态|是否(?:已经)?登[录陆]|有没有登[录陆]|登[录陆]过期了吗|(?:is|are)\s+\S+\s+logged\s+in|login\s+status|still\s+logged\s+in/i;
|
|
167
|
+
/**
|
|
168
|
+
* Whether one owner message asks for a (re-)login of a harness. Mirrors the
|
|
169
|
+
* install-skill approval check: the orchestrator's claim is not trusted, the
|
|
170
|
+
* owner's own words are.
|
|
171
|
+
*
|
|
172
|
+
* Counts a message with a login verb that either names this harness or names
|
|
173
|
+
* none (「不 我要重新登陆一个账号」 in a conversation about Claude). A message
|
|
174
|
+
* naming only other harnesses, or a status question, does not count.
|
|
175
|
+
*
|
|
176
|
+
* @param message - Owner message text
|
|
177
|
+
* @param harnessId - Harness the orchestrator wants to log in
|
|
178
|
+
* @returns True when the message is evidence for that login
|
|
179
|
+
*/
|
|
180
|
+
export function isOwnerLoginRequestEvidence(message, harnessId) {
|
|
181
|
+
if (typeof message !== 'string' || !message.trim())
|
|
182
|
+
return false;
|
|
183
|
+
const parsed = parseOwnerLoginRequest(message);
|
|
184
|
+
if (parsed?.kind === 'harness')
|
|
185
|
+
return parsed.harnessId === harnessId;
|
|
186
|
+
if (!LOGIN_MENTION.test(message) || STATUS_QUESTION.test(message))
|
|
187
|
+
return false;
|
|
188
|
+
const named = HARNESS_MENTIONS.filter(({ re }) => re.test(message)).map(({ id }) => id);
|
|
189
|
+
return named.length === 0 || named.includes(harnessId);
|
|
190
|
+
}
|
|
191
|
+
//# sourceMappingURL=owner-login-request.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"owner-login-request.js","sourceRoot":"","sources":["../../../../../../backend/src/services/harness/owner-login-request.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filter for an in-process agent's final turn text before it is posted to a
|
|
3
|
+
* person (chat, Slack DM).
|
|
4
|
+
*
|
|
5
|
+
* Incident 2026-09-26 (owner's Slack DM with the orchestrator): after each
|
|
6
|
+
* turn the owner got an English meta report — "I've sent the reply. Here's
|
|
7
|
+
* my status: *What the user asked* … *What I did* … *Next step* … *Note on
|
|
8
|
+
* the ticket instruction*". Several turns in a row had made no tool call at
|
|
9
|
+
* all and still said "I generated a fresh link and sent it via reply-chat":
|
|
10
|
+
* nothing had been sent. Both kinds of text are noise at best and false at
|
|
11
|
+
* worst, so they are not posted:
|
|
12
|
+
*
|
|
13
|
+
* - {@link isMetaStatusReport}: the text reports on the turn instead of
|
|
14
|
+
* answering ("Here's my status", or self-referential section headers such
|
|
15
|
+
* as "What I did" plus another report header).
|
|
16
|
+
* - {@link claimsSentReply} with no reply tool call in the turn: the text
|
|
17
|
+
* says a reply / link was sent to the person, but nothing sent one. Text
|
|
18
|
+
* that carries a URL itself is kept — posting it delivers what it claims.
|
|
19
|
+
*
|
|
20
|
+
* A genuine answer — even one mentioning "next steps" or "I sent the task
|
|
21
|
+
* to Joe" — passes.
|
|
22
|
+
*
|
|
23
|
+
* @module utils/agent-reply-filter
|
|
24
|
+
*/
|
|
25
|
+
/** The slice of a tool call record the filter reads. */
|
|
26
|
+
export interface TurnToolCall {
|
|
27
|
+
toolName: string;
|
|
28
|
+
args?: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
/** Why a turn's text was not posted. */
|
|
31
|
+
export type SuppressedReplyReason = 'meta_status_report' | 'unsent_reply_claim';
|
|
32
|
+
/** Result of {@link filterAgentTurnReply}. */
|
|
33
|
+
export interface FilteredTurnReply {
|
|
34
|
+
/** Text to post ('' when suppressed) */
|
|
35
|
+
text: string;
|
|
36
|
+
/** Why it was suppressed, or null when it passes */
|
|
37
|
+
suppressed: SuppressedReplyReason | null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Whether a tool call sent a reply to a person.
|
|
41
|
+
*
|
|
42
|
+
* @param call - Tool call record
|
|
43
|
+
* @returns True for a reply tool, or a shell call that runs a reply skill
|
|
44
|
+
*/
|
|
45
|
+
export declare function isReplyToolCall(call: TurnToolCall): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Whether a text is a report on the turn rather than an answer.
|
|
48
|
+
*
|
|
49
|
+
* @param text - Final turn text
|
|
50
|
+
* @returns True for "Here's my status" openers, or a self-report header plus
|
|
51
|
+
* at least one more report header
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* isMetaStatusReport("I've sent the reply. Here's my status:\n**What I did:** …"); // true
|
|
56
|
+
* isMetaStatusReport('部署好了:https://ce.example.com'); // false
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export declare function isMetaStatusReport(text: string): boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Whether a text claims a reply, link or message was sent to the person.
|
|
62
|
+
*
|
|
63
|
+
* @param text - Final turn text
|
|
64
|
+
* @returns True for "I've sent the link", "The link has been sent", 「链接已经发给你了」 …
|
|
65
|
+
*/
|
|
66
|
+
export declare function claimsSentReply(text: string): boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Decide whether an in-process agent's final text may be posted.
|
|
69
|
+
*
|
|
70
|
+
* @param text - Final turn text (already stripped of tool-call markup)
|
|
71
|
+
* @param toolCalls - The turn's tool calls
|
|
72
|
+
* @returns The text to post, or '' with the reason it was suppressed
|
|
73
|
+
*/
|
|
74
|
+
export declare function filterAgentTurnReply(text: string, toolCalls: readonly TurnToolCall[] | undefined): FilteredTurnReply;
|
|
75
|
+
//# sourceMappingURL=agent-reply-filter.utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-reply-filter.utils.d.ts","sourceRoot":"","sources":["../../../../../backend/src/utils/agent-reply-filter.utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,wDAAwD;AACxD,MAAM,WAAW,YAAY;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AAED,wCAAwC;AACxC,MAAM,MAAM,qBAAqB,GAAG,oBAAoB,GAAG,oBAAoB,CAAC;AAEhF,8CAA8C;AAC9C,MAAM,WAAW,iBAAiB;IACjC,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,oDAAoD;IACpD,UAAU,EAAE,qBAAqB,GAAG,IAAI,CAAC;CACzC;AA2CD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAK3D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAWxD;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAGrD;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,YAAY,EAAE,GAAG,SAAS,GAAG,iBAAiB,CAQpH"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filter for an in-process agent's final turn text before it is posted to a
|
|
3
|
+
* person (chat, Slack DM).
|
|
4
|
+
*
|
|
5
|
+
* Incident 2026-09-26 (owner's Slack DM with the orchestrator): after each
|
|
6
|
+
* turn the owner got an English meta report — "I've sent the reply. Here's
|
|
7
|
+
* my status: *What the user asked* … *What I did* … *Next step* … *Note on
|
|
8
|
+
* the ticket instruction*". Several turns in a row had made no tool call at
|
|
9
|
+
* all and still said "I generated a fresh link and sent it via reply-chat":
|
|
10
|
+
* nothing had been sent. Both kinds of text are noise at best and false at
|
|
11
|
+
* worst, so they are not posted:
|
|
12
|
+
*
|
|
13
|
+
* - {@link isMetaStatusReport}: the text reports on the turn instead of
|
|
14
|
+
* answering ("Here's my status", or self-referential section headers such
|
|
15
|
+
* as "What I did" plus another report header).
|
|
16
|
+
* - {@link claimsSentReply} with no reply tool call in the turn: the text
|
|
17
|
+
* says a reply / link was sent to the person, but nothing sent one. Text
|
|
18
|
+
* that carries a URL itself is kept — posting it delivers what it claims.
|
|
19
|
+
*
|
|
20
|
+
* A genuine answer — even one mentioning "next steps" or "I sent the task
|
|
21
|
+
* to Joe" — passes.
|
|
22
|
+
*
|
|
23
|
+
* @module utils/agent-reply-filter
|
|
24
|
+
*/
|
|
25
|
+
/** Tools that post a reply by themselves. */
|
|
26
|
+
const REPLY_TOOL_NAMES = new Set(['reply_slack', 'reply-slack', 'reply_chat', 'reply-chat', 'send_chat_response']);
|
|
27
|
+
/** Shell tools whose command can run a reply skill. */
|
|
28
|
+
const SHELL_TOOL_NAMES = new Set(['bash_exec', 'bash', 'Bash', 'shell']);
|
|
29
|
+
/** Reply skills run through the shell. */
|
|
30
|
+
const REPLY_SKILL_PATTERN = /\b(?:reply-chat|reply-slack|reply-gchat|reply-remote|reply-channel|send-chat-response)\b/;
|
|
31
|
+
/**
|
|
32
|
+
* Opening line of a turn report: "I've sent the reply. Here's my status:",
|
|
33
|
+
* "I've replied to the Slack DM. Here's what I found and did:",
|
|
34
|
+
* "The link has been sent. Here's my status:".
|
|
35
|
+
*/
|
|
36
|
+
const REPORT_OPENER = /^[^\n]{0,160}?\bhere(?:'s| is)\s+(?:my\s+status|what\s+i\s+(?:found|did|have\s+done))\b/i;
|
|
37
|
+
/** A markdown-ish section header line; group 1 is the label. */
|
|
38
|
+
const HEADER_LINE = /^\s*(?:#{1,6}\s*)?(?:[-*>]\s+)?\*{0,2}_?([^\n::*]{2,80}?)\s*(?:\([^)\n]{0,80}\))?\s*_?\*{0,2}\s*[::]/gm;
|
|
39
|
+
/** Headers in which the agent reports on itself or on "the user" (strong signal). */
|
|
40
|
+
const SELF_REPORT_LABEL = /^(?:what\s+(?:the\s+)?(?:user|owner)\b|what\s+i\b|why\s+i\b|note\s+on\s+(?:the\s+)?ticket|correction\s+first|my\s+status\b|用户(?:问|要|说|想|的问题)|我(?:做了|完成了|检查了|验证了|发现)|我的状态)/i;
|
|
41
|
+
/** Other headers a turn report uses (weak alone). */
|
|
42
|
+
const REPORT_LABEL = /^(?:next\s+steps?|result|root\s+cause|investigation|caveats?(?:\s+i\s+flagged.*)?|status|summary|findings|verified|下一步|结果|原因|结论|总结)\b/i;
|
|
43
|
+
/** Claims that a reply / link / message went to the person. */
|
|
44
|
+
const SENT_REPLY_CLAIMS = [
|
|
45
|
+
/\bi(?:'ve|\s+have)?\s+(?:just\s+|now\s+|also\s+)?(?:re-?)?(?:sent|posted|shared|dm'?e?d|delivered)\s+(?:you\b|it\s+(?:to\s+you|to\s+the\s+(?:user|owner)|via\b|in\s+(?:the\s+)?(?:dm|thread|chat))|the\s+(?:(?:new|fresh|real|login|authorization|auth|interim)\s+)*(?:link|reply|url|message|code|answer)\b|a\s+(?:(?:new|fresh|short)\s+)?(?:link|reply|message)\b)/i,
|
|
46
|
+
/\bi(?:'ve|\s+have)\s+(?:just\s+)?replied\b/i,
|
|
47
|
+
/\b(?:link|reply|url|message|answer)\s+(?:has|have|was)\s+(?:just\s+)?(?:been\s+)?(?:sent|posted|delivered)\b/i,
|
|
48
|
+
/\b(?:sent|delivered|posted)\s+(?:it\s+|the\s+\w+\s+)?via\s+`?reply-(?:chat|slack)/i,
|
|
49
|
+
/(?:链接|回复|消息|验证码|登录链接|授权链接)(?:已经?|刚刚?|都)?(?:发|发送|发出)(?:给你|过去|出去|到)?了/,
|
|
50
|
+
/(?:已经?|刚刚?)(?:把[^。\n]{0,30})?(?:发给你|发送给你|私信给你|回复你|回复了你)/,
|
|
51
|
+
/发给你了/,
|
|
52
|
+
];
|
|
53
|
+
/** A URL in the text: posting it delivers the link it talks about. */
|
|
54
|
+
const URL_PATTERN = /https?:\/\/\S+/i;
|
|
55
|
+
/**
|
|
56
|
+
* Whether a tool call sent a reply to a person.
|
|
57
|
+
*
|
|
58
|
+
* @param call - Tool call record
|
|
59
|
+
* @returns True for a reply tool, or a shell call that runs a reply skill
|
|
60
|
+
*/
|
|
61
|
+
export function isReplyToolCall(call) {
|
|
62
|
+
if (REPLY_TOOL_NAMES.has(call.toolName))
|
|
63
|
+
return true;
|
|
64
|
+
if (!SHELL_TOOL_NAMES.has(call.toolName))
|
|
65
|
+
return false;
|
|
66
|
+
const command = call.args?.command;
|
|
67
|
+
return typeof command === 'string' && REPLY_SKILL_PATTERN.test(command);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Whether a text is a report on the turn rather than an answer.
|
|
71
|
+
*
|
|
72
|
+
* @param text - Final turn text
|
|
73
|
+
* @returns True for "Here's my status" openers, or a self-report header plus
|
|
74
|
+
* at least one more report header
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```ts
|
|
78
|
+
* isMetaStatusReport("I've sent the reply. Here's my status:\n**What I did:** …"); // true
|
|
79
|
+
* isMetaStatusReport('部署好了:https://ce.example.com'); // false
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export function isMetaStatusReport(text) {
|
|
83
|
+
if (typeof text !== 'string' || !text.trim())
|
|
84
|
+
return false;
|
|
85
|
+
if (REPORT_OPENER.test(text.trim()))
|
|
86
|
+
return true;
|
|
87
|
+
let self = 0;
|
|
88
|
+
let other = 0;
|
|
89
|
+
for (const match of text.matchAll(HEADER_LINE)) {
|
|
90
|
+
const label = match[1].trim();
|
|
91
|
+
if (SELF_REPORT_LABEL.test(label))
|
|
92
|
+
self += 1;
|
|
93
|
+
else if (REPORT_LABEL.test(label))
|
|
94
|
+
other += 1;
|
|
95
|
+
}
|
|
96
|
+
return self >= 1 && self + other >= 2;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Whether a text claims a reply, link or message was sent to the person.
|
|
100
|
+
*
|
|
101
|
+
* @param text - Final turn text
|
|
102
|
+
* @returns True for "I've sent the link", "The link has been sent", 「链接已经发给你了」 …
|
|
103
|
+
*/
|
|
104
|
+
export function claimsSentReply(text) {
|
|
105
|
+
if (typeof text !== 'string' || !text.trim())
|
|
106
|
+
return false;
|
|
107
|
+
return SENT_REPLY_CLAIMS.some((re) => re.test(text));
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Decide whether an in-process agent's final text may be posted.
|
|
111
|
+
*
|
|
112
|
+
* @param text - Final turn text (already stripped of tool-call markup)
|
|
113
|
+
* @param toolCalls - The turn's tool calls
|
|
114
|
+
* @returns The text to post, or '' with the reason it was suppressed
|
|
115
|
+
*/
|
|
116
|
+
export function filterAgentTurnReply(text, toolCalls) {
|
|
117
|
+
if (!text)
|
|
118
|
+
return { text: '', suppressed: null };
|
|
119
|
+
if (isMetaStatusReport(text))
|
|
120
|
+
return { text: '', suppressed: 'meta_status_report' };
|
|
121
|
+
const replied = (toolCalls ?? []).some(isReplyToolCall);
|
|
122
|
+
if (!replied && claimsSentReply(text) && !URL_PATTERN.test(text)) {
|
|
123
|
+
return { text: '', suppressed: 'unsent_reply_claim' };
|
|
124
|
+
}
|
|
125
|
+
return { text, suppressed: null };
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=agent-reply-filter.utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-reply-filter.utils.js","sourceRoot":"","sources":["../../../../../backend/src/utils/agent-reply-filter.utils.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "crewly",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.146",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Multi-agent orchestration platform for AI coding teams — coordinates Claude Code, Gemini CLI, and Codex agents with a real-time web dashboard",
|
|
6
6
|
"workspaces": [
|
|
@@ -136,6 +136,45 @@ describe('AgentRunnerService', () => {
|
|
|
136
136
|
expect(runner.getHistoryLength()).toBe(1); // only user message
|
|
137
137
|
});
|
|
138
138
|
|
|
139
|
+
it('asks for a short user-facing reply (not a status report) when tools ran and no text came back', async () => {
|
|
140
|
+
mockGenerateText
|
|
141
|
+
.mockResolvedValueOnce({
|
|
142
|
+
text: '',
|
|
143
|
+
steps: [{ toolCalls: [{ toolCallId: 'tc-1', toolName: 'get_team_status', input: {} }], toolResults: [{ toolCallId: 'tc-1', output: {} }] }],
|
|
144
|
+
usage: { inputTokens: 10, outputTokens: 0 },
|
|
145
|
+
finishReason: 'stop',
|
|
146
|
+
})
|
|
147
|
+
.mockResolvedValueOnce({ text: '两个团队都在跑。', usage: { inputTokens: 5, outputTokens: 5 } });
|
|
148
|
+
|
|
149
|
+
const result = await runner.run('团队怎么样');
|
|
150
|
+
|
|
151
|
+
expect(result.text).toBe('两个团队都在跑。');
|
|
152
|
+
expect(mockGenerateText).toHaveBeenCalledTimes(2);
|
|
153
|
+
const fallbackCall = mockGenerateText.mock.calls[1][0] as { messages: Array<{ role: string; content: string }>; tools?: unknown };
|
|
154
|
+
expect(fallbackCall.tools).toBeUndefined();
|
|
155
|
+
// The history array is shared, so find the injected prompt rather than taking the last entry.
|
|
156
|
+
const prompt = fallbackCall.messages.find((m) => m.role === 'user' && /No status report/.test(String(m.content)));
|
|
157
|
+
expect(prompt).toBeDefined();
|
|
158
|
+
expect(fallbackCall.messages.some((m) => /summarize what you just did/i.test(String(m.content)))).toBe(false);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('asks for nothing more when the turn already replied with reply-chat (2026-09-26 status noise)', async () => {
|
|
162
|
+
mockGenerateText.mockResolvedValueOnce({
|
|
163
|
+
text: '',
|
|
164
|
+
steps: [{
|
|
165
|
+
toolCalls: [{ toolCallId: 'tc-1', toolName: 'bash_exec', input: { command: "bash config/skills/orchestrator/reply-chat/execute.sh '{\"content\":\"好了\"}'" } }],
|
|
166
|
+
toolResults: [{ toolCallId: 'tc-1', output: { success: true } }],
|
|
167
|
+
}],
|
|
168
|
+
usage: { inputTokens: 10, outputTokens: 0 },
|
|
169
|
+
finishReason: 'stop',
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const result = await runner.run('帮我重新登陆claude code');
|
|
173
|
+
|
|
174
|
+
expect(result.text).toBe('');
|
|
175
|
+
expect(mockGenerateText).toHaveBeenCalledTimes(1);
|
|
176
|
+
});
|
|
177
|
+
|
|
139
178
|
it('should track tool calls across steps', async () => {
|
|
140
179
|
mockGenerateText.mockResolvedValueOnce({
|
|
141
180
|
text: 'Done',
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
MODEL_CONTEXT_WINDOWS,
|
|
39
39
|
resolveMaxOutputTokens,
|
|
40
40
|
} from './types.js';
|
|
41
|
+
import { SUMMARY_FALLBACK_PROMPT, shouldRequestSummaryFallback } from './reply-tool-calls.js';
|
|
41
42
|
|
|
42
43
|
/**
|
|
43
44
|
* What a provider's finish reason means for the turn, and how to recover.
|
|
@@ -1572,9 +1573,11 @@ export class AgentRunnerService {
|
|
|
1572
1573
|
this.state.messages.push({ role: 'assistant', content: text });
|
|
1573
1574
|
}
|
|
1574
1575
|
|
|
1575
|
-
// Empty response fallback: if model made tool calls but produced no text
|
|
1576
|
-
// prompt it once more
|
|
1577
|
-
|
|
1576
|
+
// Empty response fallback: if model made tool calls but produced no text,
|
|
1577
|
+
// prompt it once more for a short reply (prevents silent completions).
|
|
1578
|
+
// Skipped when a reply tool already sent the answer: the "summary" that
|
|
1579
|
+
// followed was the status report the owner got as noise (2026-09-26).
|
|
1580
|
+
if (shouldRequestSummaryFallback(text, toolCalls)) {
|
|
1578
1581
|
console.warn('[AgentRunner] Empty text response after tool calls, requesting summary fallback');
|
|
1579
1582
|
const fallbackResult = await this.requestSummaryFallback();
|
|
1580
1583
|
if (fallbackResult) {
|
|
@@ -1760,9 +1763,9 @@ export class AgentRunnerService {
|
|
|
1760
1763
|
this.state.messages.push({ role: 'assistant', content: finalText });
|
|
1761
1764
|
}
|
|
1762
1765
|
|
|
1763
|
-
// Empty response fallback
|
|
1764
|
-
//
|
|
1765
|
-
if (
|
|
1766
|
+
// Empty response fallback (see the streaming path): a short reply, and
|
|
1767
|
+
// none at all when a reply tool already sent the answer.
|
|
1768
|
+
if (shouldRequestSummaryFallback(finalText, toolCalls)) {
|
|
1766
1769
|
console.warn('[AgentRunner] Empty text response after tool calls, requesting summary fallback');
|
|
1767
1770
|
const fallbackResult = await this.requestSummaryFallback();
|
|
1768
1771
|
if (fallbackResult) {
|
|
@@ -1991,24 +1994,21 @@ export class AgentRunnerService {
|
|
|
1991
1994
|
}
|
|
1992
1995
|
|
|
1993
1996
|
/**
|
|
1994
|
-
*
|
|
1995
|
-
* but no text output. Injects
|
|
1996
|
-
*
|
|
1997
|
+
* Ask the model for a closing reply when the previous response had tool
|
|
1998
|
+
* calls but no text output. Injects {@link SUMMARY_FALLBACK_PROMPT} (a short
|
|
1999
|
+
* reply to the person, in their language — not a status report) and makes a
|
|
2000
|
+
* single generateText call with no tools to force a text-only response.
|
|
1997
2001
|
*
|
|
1998
|
-
* @returns The
|
|
2002
|
+
* @returns The reply text, or empty string if the fallback also fails
|
|
1999
2003
|
*/
|
|
2000
2004
|
private async requestSummaryFallback(): Promise<string> {
|
|
2001
2005
|
if (!this.model) return '';
|
|
2002
2006
|
|
|
2003
|
-
|
|
2004
|
-
'请用文字总结你刚才完成的工作和发现的结果,然后调用report-status汇报。' +
|
|
2005
|
-
'Please summarize what you just did, what you found, and any issues encountered. ' +
|
|
2006
|
-
'Then call report-status to report your status.';
|
|
2007
|
-
|
|
2008
|
-
this.state.messages.push({ role: 'user', content: prompt });
|
|
2007
|
+
this.state.messages.push({ role: 'user', content: SUMMARY_FALLBACK_PROMPT });
|
|
2009
2008
|
|
|
2010
2009
|
try {
|
|
2011
|
-
const
|
|
2010
|
+
const generateFn = this._generateTextFn || (generateText as Function);
|
|
2011
|
+
const fallback = await generateFn({
|
|
2012
2012
|
model: this.model,
|
|
2013
2013
|
system: this.state.systemPrompt,
|
|
2014
2014
|
messages: this.state.messages,
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the interactive harness login guard of bash_exec.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import { HARNESS_LOGIN_SKILL_PATH, checkInteractiveLoginCommand, detectInteractiveLogin } from './interactive-login-guard.js';
|
|
7
|
+
|
|
8
|
+
describe('detectInteractiveLogin', () => {
|
|
9
|
+
it.each([
|
|
10
|
+
['claude setup-token', 'claude setup-token'],
|
|
11
|
+
['~/.local/bin/claude setup-token', 'claude setup-token'],
|
|
12
|
+
['cd ~ && claude setup-token 2>&1 | tee /tmp/x', 'claude setup-token'],
|
|
13
|
+
['script -q /dev/null claude setup-token', 'claude setup-token'],
|
|
14
|
+
["tmux send-keys -t login 'claude setup-token' Enter", 'claude setup-token'],
|
|
15
|
+
['claude /login', 'claude /login'],
|
|
16
|
+
['claude "/login"', 'claude /login'],
|
|
17
|
+
['echo /login | claude', 'claude /login'],
|
|
18
|
+
["printf '/login\\n' | ~/.local/bin/claude", 'claude /login'],
|
|
19
|
+
['claude auth login', 'claude auth login'],
|
|
20
|
+
['claude auth login --email someone@example.com', 'claude auth login'],
|
|
21
|
+
['claude login', 'claude auth login'],
|
|
22
|
+
['codex login', 'codex login'],
|
|
23
|
+
['codex login --device-auth', 'codex login'],
|
|
24
|
+
['nohup codex login --device-auth > /tmp/l.log 2>&1 &', 'codex login'],
|
|
25
|
+
['agy login', 'agy login'],
|
|
26
|
+
['antigravity auth login', 'agy login'],
|
|
27
|
+
])('%s', (command, label) => {
|
|
28
|
+
expect(detectInteractiveLogin(command)).toBe(label);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it.each([
|
|
32
|
+
'claude auth status',
|
|
33
|
+
'claude --version',
|
|
34
|
+
'claude -p "summarise the repo"',
|
|
35
|
+
'codex login status',
|
|
36
|
+
'codex exec "fix the test"',
|
|
37
|
+
'cat backend/src/services/harness/claude-config.utils.ts',
|
|
38
|
+
'grep -rn setup-token backend/src/services/harness/login-rules.ts',
|
|
39
|
+
'bash config/skills/orchestrator/harness-login/execute.sh --harness claude',
|
|
40
|
+
'ls ~/.claude',
|
|
41
|
+
'echo login',
|
|
42
|
+
'',
|
|
43
|
+
])('allows %s', (command) => {
|
|
44
|
+
expect(detectInteractiveLogin(command)).toBeNull();
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('checkInteractiveLoginCommand', () => {
|
|
49
|
+
it('explains why and names the skill', () => {
|
|
50
|
+
const message = checkInteractiveLoginCommand('claude setup-token');
|
|
51
|
+
expect(message).toContain('`claude setup-token` is an interactive login');
|
|
52
|
+
expect(message).toContain('stale');
|
|
53
|
+
expect(message).toContain(HARNESS_LOGIN_SKILL_PATH);
|
|
54
|
+
expect(message).toContain('Do not retry');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('returns null for ordinary commands', () => {
|
|
58
|
+
expect(checkInteractiveLoginCommand('npm run build')).toBeNull();
|
|
59
|
+
});
|
|
60
|
+
});
|