hilos-agent 0.1.9 → 0.1.11
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 +4 -0
- package/package.json +1 -1
- package/src/handler.mjs +108 -88
package/README.md
CHANGED
|
@@ -72,6 +72,10 @@ hilos-agent --channel <id> # scope to one channel
|
|
|
72
72
|
## How it works
|
|
73
73
|
|
|
74
74
|
- **Trigger** — an `@mention` of your agent in a channel that's linked to a repo.
|
|
75
|
+
- **Chat or code?** — the agent reads the conversation and decides with the model
|
|
76
|
+
(via `chatCmd`), not a keyword list: a question/greeting/"let's just discuss" →
|
|
77
|
+
a chat reply; anything asking for a change — including "just code it", "finish
|
|
78
|
+
it", "approved", or "go for it" after a request, in any language → a code run.
|
|
75
79
|
- **Repo resolution** — the channel's linked repo is mapped to a local path via
|
|
76
80
|
`repos`. No mapping → the agent says so and stops.
|
|
77
81
|
- **Run** — it branches off `defaultBranch` (refuses a dirty tree), runs
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hilos-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
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
|
@@ -165,87 +165,86 @@ async function applyDecision({ decision, repoPath, branch, task, requester, cfg,
|
|
|
165
165
|
return { status: "timeout", branch };
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
* they mention code; an action verb means work. Keeps casual talk in a
|
|
172
|
-
* repo-linked channel from spawning empty branches.
|
|
173
|
-
*/
|
|
174
|
-
export function looksLikeCodeTask(text) {
|
|
175
|
-
const t = String(text || "")
|
|
176
|
-
.toLowerCase()
|
|
177
|
-
.replace(/@[a-z0-9-]+/g, " ")
|
|
178
|
-
.trim();
|
|
179
|
-
if (!t) return false;
|
|
180
|
-
// Greetings / questions → chat (even if they name code things).
|
|
181
|
-
if (
|
|
182
|
-
/^(hi|hey|hello|yo|sup|gm|thanks|thank you|nice|cool|ok|okay|how|what|why|when|who|where|which|is\b|are\b|do you|does|did|could you (tell|explain|show|describe)|can you (tell|explain|show|describe))/.test(
|
|
183
|
-
t,
|
|
184
|
-
)
|
|
185
|
-
) {
|
|
186
|
-
return false;
|
|
187
|
-
}
|
|
188
|
-
const verb =
|
|
189
|
-
/\b(fix|add|implement|refactor|change|update|create|remove|delete|rename|write|build|bump|migrate|wire|patch|optimi[sz]e|debug|revert|rebase|configure|improve|replace|extract|split|move|generate|scaffold|make|set up|hook up|adjust|tweak|ship|commit|push|merge)\b/;
|
|
190
|
-
return verb.test(t);
|
|
191
|
-
}
|
|
168
|
+
// Sentinel the router model emits when the latest message is a request to change
|
|
169
|
+
// code. Unusual on purpose so it can't be confused with a real chat reply.
|
|
170
|
+
const CODE_SIGNAL = "__CODE__";
|
|
192
171
|
|
|
193
172
|
/**
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
173
|
+
* Decide — with the LLM, not a word list — whether the latest message wants a
|
|
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
|
+
* context, in any language: "just code it", "dale, hazlo", "yeah go for it" after
|
|
177
|
+
* a request → code; "how does this work?", "thoughts?", "thanks" → chat.
|
|
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
|
+
*
|
|
184
|
+
* Returns one of:
|
|
185
|
+
* { aborted: true }
|
|
186
|
+
* { code: true, task } → run the coding flow with `task` as the spec
|
|
187
|
+
* { code: false, reply, error } → post `reply` as a chat message
|
|
188
|
+
*
|
|
189
|
+
* `error` is set when the model produced nothing (so the caller can be honest
|
|
190
|
+
* about a timeout vs a missing binary instead of inventing a reply).
|
|
199
191
|
*/
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
.
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
192
|
+
async function routeIntent({ name, repoFullName, transcript, workspaceMemory, cfg, signal }) {
|
|
193
|
+
const cmd = cfg.chatCmd || cfg.codingCmd;
|
|
194
|
+
const parts = cmd.split(" ").filter(Boolean);
|
|
195
|
+
const prompt =
|
|
196
|
+
`You are ${name}, a teammate in a team chat (hilos) connected to the git repository ` +
|
|
197
|
+
`${repoFullName}. Read the conversation and judge what the LATEST message wants from you.\n\n` +
|
|
198
|
+
`If it is asking you to make a code or repository change — implement/fix/refactor/adjust ` +
|
|
199
|
+
`something, continue or finish work discussed above, or give a go-ahead to act ("yeah, do ` +
|
|
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` +
|
|
205
|
+
`Otherwise — a question, a greeting, general discussion, or they explicitly don't want code ` +
|
|
206
|
+
`yet — just reply to them concisely and directly as a single chat message (no preamble, no ` +
|
|
207
|
+
`headings). When unsure, prefer to act (${CODE_SIGNAL}); this is a build channel.\n\n` +
|
|
208
|
+
`${memoryPreamble(workspaceMemory)}Conversation so far:\n${transcript}`;
|
|
209
|
+
const run = await runCli({
|
|
210
|
+
cmd: parts[0],
|
|
211
|
+
args: [...parts.slice(1), prompt],
|
|
212
|
+
timeoutMs: cfg.chatTimeoutMs || cfg.runTimeoutMs,
|
|
213
|
+
label: "thinking",
|
|
214
|
+
signal,
|
|
215
|
+
});
|
|
216
|
+
if (run.aborted || signal?.aborted) return { aborted: true };
|
|
217
|
+
const out = (run.stdout || "").trim();
|
|
218
|
+
if (!out) return { code: false, reply: null, error: run.error || new Error("no output") };
|
|
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() : "" };
|
|
213
222
|
}
|
|
214
|
-
|
|
215
|
-
if (t.length > 40) return false;
|
|
216
|
-
return /^(go|do it|do that|yes|yep|yeah|yup|sure|ship|proceed|sounds good|sgtm|please do|please)\b/.test(t);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/** Does the agent's most recent message read like a plan/proposal it offered? */
|
|
220
|
-
export function agentProposedWork(rows, agentName) {
|
|
221
|
-
const mine = (rows || []).filter((m) => m && m.author === agentName);
|
|
222
|
-
const last = mine[mine.length - 1];
|
|
223
|
-
if (!last) return false;
|
|
224
|
-
return /\b(i'?ll|i will|plan|propose|pr\b|pull request|branch|here'?s|let me|on it|i'?d|i can)\b/i.test(
|
|
225
|
-
String(last.body || ""),
|
|
226
|
-
);
|
|
223
|
+
return { code: false, reply: out };
|
|
227
224
|
}
|
|
228
225
|
|
|
229
226
|
/**
|
|
230
|
-
* The
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
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]
|
|
236
235
|
*/
|
|
237
|
-
export function codeTaskPrompt(
|
|
238
|
-
const
|
|
236
|
+
export function codeTaskPrompt(o) {
|
|
237
|
+
const { message, context, brief, repoFullName } = o || {};
|
|
238
|
+
const task = (brief && brief.trim()) || String(message?.body || "").trim();
|
|
239
239
|
const transcript = context?.transcript?.trim();
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
`You
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
);
|
|
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;
|
|
249
248
|
}
|
|
250
249
|
|
|
251
250
|
/** owner/name from a GitHub remote URL (https or ssh), or null. */
|
|
@@ -415,27 +414,48 @@ export async function handleTask({ message, channelId, tool, me, caps = {} }, cf
|
|
|
415
414
|
const { memory: workspaceMemory = null } = await tool("get_workspace_memory", {}).catch(() => ({
|
|
416
415
|
memory: null,
|
|
417
416
|
}));
|
|
418
|
-
// The conversation the agent is replying within — fetched ONCE and
|
|
419
|
-
//
|
|
420
|
-
// like "yeah, do it" only means something against what was
|
|
421
|
-
//
|
|
422
|
-
// always had this; the code path used to run on the bare mention alone (so
|
|
423
|
-
// "go for it" reached the agent with no spec → nothing built). Now both share it.
|
|
417
|
+
// The conversation the agent is replying within — fetched ONCE and used to
|
|
418
|
+
// route AND (for a code run) handed to the coding prompt. Context is always
|
|
419
|
+
// crucial: a reply like "yeah, do it" only means something against what was
|
|
420
|
+
// just said.
|
|
424
421
|
const context = await fetchContext({ channelId, tool, parentId });
|
|
425
|
-
|
|
426
|
-
//
|
|
427
|
-
|
|
428
|
-
// a conversational reply.
|
|
429
|
-
let isCode = looksLikeCodeTask(message.body);
|
|
430
|
-
if (!isCode && repoLink && isAffirmation(message.body) && agentProposedWork(context.rows, me?.agentName)) {
|
|
431
|
-
isCode = true;
|
|
432
|
-
}
|
|
433
|
-
if (!repoLink || !isCode) {
|
|
422
|
+
|
|
423
|
+
// No repo linked → nothing to build; just reply.
|
|
424
|
+
if (!repoLink) {
|
|
434
425
|
await respondConversationally({ message, channelId, tool, me, cfg, repoLink, parentId, workspaceMemory, signal, context, caps });
|
|
435
426
|
return { status: "chat" };
|
|
436
427
|
}
|
|
437
428
|
const repoFullName = repoLink.repo_full_name;
|
|
438
429
|
|
|
430
|
+
// Chat vs code is the LLM's call, not a word list: it reads the whole
|
|
431
|
+
// conversation and either returns a chat reply or signals a code run. Handles
|
|
432
|
+
// any phrasing, any language, and "go for it" obviously means "do the thing we
|
|
433
|
+
// just discussed". When it replies (chat), post that and we're done.
|
|
434
|
+
const routed = await routeIntent({
|
|
435
|
+
name: me?.agentName || "an assistant",
|
|
436
|
+
repoFullName,
|
|
437
|
+
transcript: context.transcript,
|
|
438
|
+
workspaceMemory,
|
|
439
|
+
cfg,
|
|
440
|
+
signal,
|
|
441
|
+
});
|
|
442
|
+
if (routed.aborted || signal?.aborted) {
|
|
443
|
+
await tool("post_message", { channelId, parentId, body: "Stopped." });
|
|
444
|
+
return { status: "chat" };
|
|
445
|
+
}
|
|
446
|
+
if (!routed.code) {
|
|
447
|
+
let body = routed.reply;
|
|
448
|
+
if (!body) {
|
|
449
|
+
body =
|
|
450
|
+
routed.error?.code === "ENOENT"
|
|
451
|
+
? `(my chat command \`${cfg.chatCmd || cfg.codingCmd}\` isn't installed or on PATH.)`
|
|
452
|
+
: `Still thinking on this — it's taking longer than usual. I'll follow up shortly.`;
|
|
453
|
+
}
|
|
454
|
+
await tool("post_message", { channelId, parentId, body });
|
|
455
|
+
return { status: "chat" };
|
|
456
|
+
}
|
|
457
|
+
// routed.code → fall through to the coding flow below.
|
|
458
|
+
|
|
439
459
|
let repoPath = resolveRepoPath(cfg, repoFullName);
|
|
440
460
|
if (!repoPath) {
|
|
441
461
|
// Zero-config path: if the daemon is running INSIDE a checkout of this repo
|
|
@@ -656,7 +676,7 @@ export async function handleTask({ message, channelId, tool, me, caps = {} }, cf
|
|
|
656
676
|
return { status: "cancelled", branch };
|
|
657
677
|
};
|
|
658
678
|
|
|
659
|
-
const staged = await runAndStage(codeTaskPrompt({ message, context }));
|
|
679
|
+
const staged = await runAndStage(codeTaskPrompt({ message, context, brief: routed.task, repoFullName }));
|
|
660
680
|
if (staged.aborted) return await postStopped();
|
|
661
681
|
if (staged.empty) {
|
|
662
682
|
let body;
|