hilos-agent 0.1.10 → 0.1.12
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/package.json +1 -1
- package/src/handler.mjs +70 -31
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hilos-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"description": "Run your own coding agent (Claude Code / Codex / Cursor) as an autonomous teammate in a hilos channel. Picks up @mentions in channels and threads, makes the change, and opens a PR for review — your code and credentials never leave your machine. (Approve-before-push is available via gate:true.)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/handler.mjs
CHANGED
|
@@ -171,14 +171,19 @@ const CODE_SIGNAL = "__CODE__";
|
|
|
171
171
|
|
|
172
172
|
/**
|
|
173
173
|
* Decide — with the LLM, not a word list — whether the latest message wants a
|
|
174
|
-
* code change or a conversational reply, and
|
|
175
|
-
*
|
|
174
|
+
* code change or a conversational reply, and produce the payload in the SAME
|
|
175
|
+
* call. The model reads the whole conversation, so it judges by intent and
|
|
176
176
|
* context, in any language: "just code it", "dale, hazlo", "yeah go for it" after
|
|
177
177
|
* a request → code; "how does this work?", "thoughts?", "thanks" → chat.
|
|
178
178
|
*
|
|
179
|
+
* For a code request it ALSO distills an imperative task brief — resolving
|
|
180
|
+
* references like "this" / "keep working on it" against the conversation — so the
|
|
181
|
+
* coding agent gets a clean spec instead of a noisy transcript (which, with the
|
|
182
|
+
* agent's own past "no changes" messages in it, made it reply instead of edit).
|
|
183
|
+
*
|
|
179
184
|
* Returns one of:
|
|
180
185
|
* { aborted: true }
|
|
181
|
-
* { code: true }
|
|
186
|
+
* { code: true, task } → run the coding flow with `task` as the spec
|
|
182
187
|
* { code: false, reply, error } → post `reply` as a chat message
|
|
183
188
|
*
|
|
184
189
|
* `error` is set when the model produced nothing (so the caller can be honest
|
|
@@ -192,8 +197,11 @@ async function routeIntent({ name, repoFullName, transcript, workspaceMemory, cf
|
|
|
192
197
|
`${repoFullName}. Read the conversation and judge what the LATEST message wants from you.\n\n` +
|
|
193
198
|
`If it is asking you to make a code or repository change — implement/fix/refactor/adjust ` +
|
|
194
199
|
`something, continue or finish work discussed above, or give a go-ahead to act ("yeah, do ` +
|
|
195
|
-
`it", "go for it", "just code it", "approved", "dale", in ANY language) — respond with ` +
|
|
196
|
-
`
|
|
200
|
+
`it", "go for it", "just code it", "approved", "dale", in ANY language) — respond with the ` +
|
|
201
|
+
`token ${CODE_SIGNAL} on the FIRST line, then on the next lines a short, imperative spec of ` +
|
|
202
|
+
`exactly what to build or change, resolving any references ("this", "keep working on it") ` +
|
|
203
|
+
`using the conversation. Example:\n${CODE_SIGNAL}\nAdd a hover popover to message reactions ` +
|
|
204
|
+
`that lists who reacted with each emoji.\n\n` +
|
|
197
205
|
`Otherwise — a question, a greeting, general discussion, or they explicitly don't want code ` +
|
|
198
206
|
`yet — just reply to them concisely and directly as a single chat message (no preamble, no ` +
|
|
199
207
|
`headings). When unsure, prefer to act (${CODE_SIGNAL}); this is a build channel.\n\n` +
|
|
@@ -208,30 +216,35 @@ async function routeIntent({ name, repoFullName, transcript, workspaceMemory, cf
|
|
|
208
216
|
if (run.aborted || signal?.aborted) return { aborted: true };
|
|
209
217
|
const out = (run.stdout || "").trim();
|
|
210
218
|
if (!out) return { code: false, reply: null, error: run.error || new Error("no output") };
|
|
211
|
-
if (new RegExp(`^${CODE_SIGNAL}\\b`).test(out))
|
|
219
|
+
if (new RegExp(`^${CODE_SIGNAL}\\b`).test(out)) {
|
|
220
|
+
const nl = out.indexOf("\n");
|
|
221
|
+
return { code: true, task: nl >= 0 ? out.slice(nl + 1).trim() : "" };
|
|
222
|
+
}
|
|
212
223
|
return { code: false, reply: out };
|
|
213
224
|
}
|
|
214
225
|
|
|
215
226
|
/**
|
|
216
|
-
* The
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
227
|
+
* The prompt handed to the CODING CLI. Framed as an engineering directive, NOT a
|
|
228
|
+
* chat: the old "you're in a team chat, do what the latest message asks" wording
|
|
229
|
+
* made the agent reply conversationally to stdout (e.g. to "how u doing? keep
|
|
230
|
+
* working on this") and edit nothing. So this is imperative — "you are a coding
|
|
231
|
+
* agent, edit files now" — led by the router's distilled `brief` (what to build),
|
|
232
|
+
* with the conversation included only as background. Falls back to the raw
|
|
233
|
+
* mention when there's no brief.
|
|
234
|
+
* @param {{ message?: { body?: string } | null, context?: { transcript?: string } | null, brief?: string, repoFullName?: string }} [o]
|
|
222
235
|
*/
|
|
223
|
-
export function codeTaskPrompt(
|
|
224
|
-
const
|
|
236
|
+
export function codeTaskPrompt(o) {
|
|
237
|
+
const { message, context, brief, repoFullName } = o || {};
|
|
238
|
+
const task = (brief && brief.trim()) || String(message?.body || "").trim();
|
|
225
239
|
const transcript = context?.transcript?.trim();
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
`You
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
);
|
|
240
|
+
const where = repoFullName ? ` in the git repository ${repoFullName}` : "";
|
|
241
|
+
let p =
|
|
242
|
+
`You are a coding agent working${where}. Implement the following by EDITING FILES now — ` +
|
|
243
|
+
`make the changes directly, do not just describe them, do not ask questions:\n\n${task}`;
|
|
244
|
+
if (transcript) {
|
|
245
|
+
p += `\n\nBackground from the team's discussion (context only — the task above is what to do):\n${transcript}`;
|
|
246
|
+
}
|
|
247
|
+
return p;
|
|
235
248
|
}
|
|
236
249
|
|
|
237
250
|
/** owner/name from a GitHub remote URL (https or ssh), or null. */
|
|
@@ -267,20 +280,38 @@ async function fetchContext({ channelId, tool, parentId }) {
|
|
|
267
280
|
return { rows, transcript: rows.map((m) => `${m.author}: ${m.body}`).join("\n").slice(-6000) };
|
|
268
281
|
}
|
|
269
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Prompt for the fast "here's my plan" ack. Carries the distilled task AND the
|
|
285
|
+
* conversation as background, so the ack is specific ("On it — I'll add X…")
|
|
286
|
+
* instead of asking for context it was already handed. (The old bug: fed only the
|
|
287
|
+
* bare mention, it replied "can't see the channel message yet.")
|
|
288
|
+
* @param {{ task?: string, transcript?: string, repoFullName?: string }} [o]
|
|
289
|
+
*/
|
|
290
|
+
export function planAckPrompt(o) {
|
|
291
|
+
const { task, transcript, repoFullName } = o || {};
|
|
292
|
+
const where = repoFullName ? ` in the repo ${repoFullName}` : "";
|
|
293
|
+
let p =
|
|
294
|
+
`A teammate asked you to do this${where}:\n"${(task || "").trim()}"\n\n` +
|
|
295
|
+
`Reply with ONE or TWO short sentences, first person: acknowledge, state your ` +
|
|
296
|
+
`plan, and say you'll open a PR and report back. No preamble, no lists, no code. ` +
|
|
297
|
+
`Tone: "On it — I'll add X by doing Y. I'll open a PR and report back."`;
|
|
298
|
+
const t = (transcript || "").trim();
|
|
299
|
+
if (t) {
|
|
300
|
+
p += `\n\nConversation so far (background — resolve references like "this" against it):\n${t}`;
|
|
301
|
+
}
|
|
302
|
+
return p;
|
|
303
|
+
}
|
|
304
|
+
|
|
270
305
|
/**
|
|
271
306
|
* A 1–2 sentence "here's my plan" line for the instant ack, via the FAST chat
|
|
272
307
|
* CLI. Bounded (≤45s) + best-effort: returns trimmed text, or null on
|
|
273
308
|
* empty/timeout/error so the run keeps the instant template and never stalls.
|
|
274
309
|
*/
|
|
275
|
-
async function proposePlanAck({ task, repoFullName, cfg, signal }) {
|
|
310
|
+
async function proposePlanAck({ task, transcript, repoFullName, cfg, signal }) {
|
|
276
311
|
const cmd = cfg.chatCmd;
|
|
277
312
|
if (!cmd) return null;
|
|
278
313
|
const parts = cmd.split(" ").filter(Boolean);
|
|
279
|
-
const prompt =
|
|
280
|
-
`A teammate asked you to do this in the repo ${repoFullName}:\n"${task}"\n\n` +
|
|
281
|
-
`Reply with ONE or TWO short sentences, first person: acknowledge, state your ` +
|
|
282
|
-
`plan, and say you'll open a PR and report back. No preamble, no lists, no code. ` +
|
|
283
|
-
`Tone: "On it — I'll add X by doing Y. I'll open a PR and report back."`;
|
|
314
|
+
const prompt = planAckPrompt({ task, transcript, repoFullName });
|
|
284
315
|
const run = await runCli({
|
|
285
316
|
cmd: parts[0],
|
|
286
317
|
args: [...parts.slice(1), prompt],
|
|
@@ -505,7 +536,15 @@ export async function handleTask({ message, channelId, tool, me, caps = {} }, cf
|
|
|
505
536
|
const ack = await tool("post_message", { channelId, parentId, body: ackText(repoFullName) });
|
|
506
537
|
const ackId = ack?.messageId ?? null;
|
|
507
538
|
if (ackId && caps.editMessage && cfg.chatCmd) {
|
|
508
|
-
|
|
539
|
+
// Feed the ack the router's distilled brief AND the conversation — not the raw
|
|
540
|
+
// mention — so it states a real plan instead of "what's the task?".
|
|
541
|
+
const plan = await proposePlanAck({
|
|
542
|
+
task: routed.task || message.body,
|
|
543
|
+
transcript: context.transcript,
|
|
544
|
+
repoFullName,
|
|
545
|
+
cfg,
|
|
546
|
+
signal,
|
|
547
|
+
}).catch(() => null);
|
|
509
548
|
if (plan && !signal?.aborted) await tool("edit_message", { messageId: ackId, body: plan }).catch(() => {});
|
|
510
549
|
}
|
|
511
550
|
|
|
@@ -663,7 +702,7 @@ export async function handleTask({ message, channelId, tool, me, caps = {} }, cf
|
|
|
663
702
|
return { status: "cancelled", branch };
|
|
664
703
|
};
|
|
665
704
|
|
|
666
|
-
const staged = await runAndStage(codeTaskPrompt({ message, context }));
|
|
705
|
+
const staged = await runAndStage(codeTaskPrompt({ message, context, brief: routed.task, repoFullName }));
|
|
667
706
|
if (staged.aborted) return await postStopped();
|
|
668
707
|
if (staged.empty) {
|
|
669
708
|
let body;
|