hilos-agent 0.1.9 → 0.1.10

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 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.9",
3
+ "version": "0.1.10",
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,65 +165,51 @@ async function applyDecision({ decision, repoPath, branch, task, requester, cfg,
165
165
  return { status: "timeout", branch };
166
166
  }
167
167
 
168
- /**
169
- * Does this mention ask for a code change (→ branch/diff/PR flow), or is it
170
- * chat (→ a conversational reply)? Greetings and questions are chat even when
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
- * A short go-ahead that approves whatever was just discussed"go", "ship it",
195
- * "do it", "lfg", "send a PR". On its own this isn't a code task (no verb), but
196
- * after the agent has proposed a plan it MEANS "execute it". handleTask treats
197
- * an affirmation as code only when a repo is linked AND the agent's last message
198
- * looked like a plan so casual "yes"/"cool" in a repo channel stays chat.
173
+ * Decide with the LLM, not a word listwhether the latest message wants a
174
+ * code change or a conversational reply, and (for chat) produce that reply in the
175
+ * SAME 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
+ * Returns one of:
180
+ * { aborted: true }
181
+ * { code: true } → run the coding flow
182
+ * { code: false, reply, error } → post `reply` as a chat message
183
+ *
184
+ * `error` is set when the model produced nothing (so the caller can be honest
185
+ * about a timeout vs a missing binary instead of inventing a reply).
199
186
  */
200
- export function isAffirmation(text) {
201
- const t = String(text || "")
202
- .toLowerCase()
203
- .replace(/@[a-z0-9-]+/g, " ")
204
- .replace(/[^a-z0-9'\s]/g, " ")
205
- .replace(/\s+/g, " ")
206
- .trim();
207
- if (!t) return false;
208
- // A strong go-ahead phrase ANYWHERE means "execute it" even inside a longer,
209
- // excited message ("…so it's safe to start. go for it, babyyyy"). Safe because
210
- // handleTask only treats it as code when the agent had just proposed a plan.
211
- if (/\b(go for it|go ahead|ship it|send it|send a pr|open a pr|make it so|lfg|let'?s go|fire away)\b/.test(t)) {
212
- return true;
213
- }
214
- // Otherwise only a SHORT message counts — a long one carries its own intent.
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
- );
187
+ async function routeIntent({ name, repoFullName, transcript, workspaceMemory, cfg, signal }) {
188
+ const cmd = cfg.chatCmd || cfg.codingCmd;
189
+ const parts = cmd.split(" ").filter(Boolean);
190
+ const prompt =
191
+ `You are ${name}, a teammate in a team chat (hilos) connected to the git repository ` +
192
+ `${repoFullName}. Read the conversation and judge what the LATEST message wants from you.\n\n` +
193
+ `If it is asking you to make a code or repository change — implement/fix/refactor/adjust ` +
194
+ `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
+ `EXACTLY this and nothing else:\n${CODE_SIGNAL}\n\n` +
197
+ `Otherwise a question, a greeting, general discussion, or they explicitly don't want code ` +
198
+ `yet just reply to them concisely and directly as a single chat message (no preamble, no ` +
199
+ `headings). When unsure, prefer to act (${CODE_SIGNAL}); this is a build channel.\n\n` +
200
+ `${memoryPreamble(workspaceMemory)}Conversation so far:\n${transcript}`;
201
+ const run = await runCli({
202
+ cmd: parts[0],
203
+ args: [...parts.slice(1), prompt],
204
+ timeoutMs: cfg.chatTimeoutMs || cfg.runTimeoutMs,
205
+ label: "thinking",
206
+ signal,
207
+ });
208
+ if (run.aborted || signal?.aborted) return { aborted: true };
209
+ const out = (run.stdout || "").trim();
210
+ if (!out) return { code: false, reply: null, error: run.error || new Error("no output") };
211
+ if (new RegExp(`^${CODE_SIGNAL}\\b`).test(out)) return { code: true };
212
+ return { code: false, reply: out };
227
213
  }
228
214
 
229
215
  /**
@@ -415,27 +401,48 @@ export async function handleTask({ message, channelId, tool, me, caps = {} }, cf
415
401
  const { memory: workspaceMemory = null } = await tool("get_workspace_memory", {}).catch(() => ({
416
402
  memory: null,
417
403
  }));
418
- // The conversation the agent is replying within — fetched ONCE and handed to
419
- // BOTH the chat reply and the coding prompt. Context is always crucial: a reply
420
- // like "yeah, do it" only means something against what was just said, and even
421
- // a direct request is clearer with the surrounding discussion. The chat path
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.
404
+ // The conversation the agent is replying within — fetched ONCE and used to
405
+ // route AND (for a code run) handed to the coding prompt. Context is always
406
+ // crucial: a reply like "yeah, do it" only means something against what was
407
+ // just said.
424
408
  const context = await fetchContext({ channelId, tool, parentId });
425
- // Chat vs code. A clear action verb → code. With a repo linked, a bare
426
- // affirmation ("go", "ship it") that follows a plan the agent just proposed
427
- // ALSO means "execute it" → code path. Otherwise (greeting/question/no repo)
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) {
409
+
410
+ // No repo linked nothing to build; just reply.
411
+ if (!repoLink) {
434
412
  await respondConversationally({ message, channelId, tool, me, cfg, repoLink, parentId, workspaceMemory, signal, context, caps });
435
413
  return { status: "chat" };
436
414
  }
437
415
  const repoFullName = repoLink.repo_full_name;
438
416
 
417
+ // Chat vs code is the LLM's call, not a word list: it reads the whole
418
+ // conversation and either returns a chat reply or signals a code run. Handles
419
+ // any phrasing, any language, and "go for it" obviously means "do the thing we
420
+ // just discussed". When it replies (chat), post that and we're done.
421
+ const routed = await routeIntent({
422
+ name: me?.agentName || "an assistant",
423
+ repoFullName,
424
+ transcript: context.transcript,
425
+ workspaceMemory,
426
+ cfg,
427
+ signal,
428
+ });
429
+ if (routed.aborted || signal?.aborted) {
430
+ await tool("post_message", { channelId, parentId, body: "Stopped." });
431
+ return { status: "chat" };
432
+ }
433
+ if (!routed.code) {
434
+ let body = routed.reply;
435
+ if (!body) {
436
+ body =
437
+ routed.error?.code === "ENOENT"
438
+ ? `(my chat command \`${cfg.chatCmd || cfg.codingCmd}\` isn't installed or on PATH.)`
439
+ : `Still thinking on this — it's taking longer than usual. I'll follow up shortly.`;
440
+ }
441
+ await tool("post_message", { channelId, parentId, body });
442
+ return { status: "chat" };
443
+ }
444
+ // routed.code → fall through to the coding flow below.
445
+
439
446
  let repoPath = resolveRepoPath(cfg, repoFullName);
440
447
  if (!repoPath) {
441
448
  // Zero-config path: if the daemon is running INSIDE a checkout of this repo