hilos-agent 0.1.13 → 0.1.14
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 +58 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hilos-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
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
|
@@ -254,6 +254,8 @@ async function routeIntent({ name, repoFullName, transcript, workspaceMemory, cf
|
|
|
254
254
|
`Otherwise — a question, a greeting, general discussion, or they explicitly don't want code ` +
|
|
255
255
|
`yet — just reply to them concisely and directly as a single chat message (no preamble, no ` +
|
|
256
256
|
`headings). When unsure, prefer to act (${CODE_SIGNAL}); this is a build channel.\n\n` +
|
|
257
|
+
`You are only routing here — output ONLY your text response. Do NOT use any tools, do NOT ` +
|
|
258
|
+
`edit files, do NOT run commands; a separate step does the actual coding.\n\n` +
|
|
257
259
|
`${memoryPreamble(workspaceMemory)}Conversation so far:\n${transcript}`;
|
|
258
260
|
const run = await runCli({
|
|
259
261
|
cmd: parts[0],
|
|
@@ -265,9 +267,16 @@ async function routeIntent({ name, repoFullName, transcript, workspaceMemory, cf
|
|
|
265
267
|
if (run.aborted || signal?.aborted) return { aborted: true };
|
|
266
268
|
const out = (run.stdout || "").trim();
|
|
267
269
|
if (!out) return { code: false, reply: null, error: run.error || new Error("no output") };
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
270
|
+
// Accept the sentinel ANYWHERE, not just the first line — models sometimes add a
|
|
271
|
+
// line of preamble before it ("Got it, …\n__CODE__\n<brief>"). If it appears,
|
|
272
|
+
// it's a code run; the brief is whatever follows the sentinel's line and any
|
|
273
|
+
// preamble before it is discarded. This is also what keeps the raw sentinel out
|
|
274
|
+
// of a chat message (markdown would render `__CODE__` as a bold "CODE").
|
|
275
|
+
const idx = out.indexOf(CODE_SIGNAL);
|
|
276
|
+
if (idx >= 0) {
|
|
277
|
+
const after = out.slice(idx + CODE_SIGNAL.length);
|
|
278
|
+
const nl = after.indexOf("\n");
|
|
279
|
+
return { code: true, task: (nl >= 0 ? after.slice(nl + 1) : after).trim() };
|
|
271
280
|
}
|
|
272
281
|
return { code: false, reply: out };
|
|
273
282
|
}
|
|
@@ -559,23 +568,55 @@ export async function handleTask({ message, channelId, tool, me, caps = {} }, cf
|
|
|
559
568
|
await tool("post_message", { channelId, parentId, body: `Can't read git status in ${repoPath}.` });
|
|
560
569
|
return { status: "git-error" };
|
|
561
570
|
}
|
|
571
|
+
// Remember where we started so cancel/cleanup/stash-restore can switch back
|
|
572
|
+
// explicitly (relying on `git switch -` breaks from a detached HEAD).
|
|
573
|
+
const symref = git(repoPath, ["symbolic-ref", "--quiet", "--short", "HEAD"]);
|
|
574
|
+
const startRef =
|
|
575
|
+
symref.status === 0 && symref.stdout.trim()
|
|
576
|
+
? { kind: "branch", ref: symref.stdout.trim() }
|
|
577
|
+
: { kind: "detached", ref: (git(repoPath, ["rev-parse", "HEAD"]).stdout || "").trim() };
|
|
578
|
+
|
|
579
|
+
// A dirty tree used to be a hard stop ("commit or stash first"). But the daemon
|
|
580
|
+
// runs in the user's own checkout, so that was a constant wall — and the user
|
|
581
|
+
// can't make the agent clear it. Instead: stash the uncommitted work, run on a
|
|
582
|
+
// clean branch, and restore it (git stash pop) on the original branch when done.
|
|
583
|
+
let stashed = false;
|
|
562
584
|
if (status.stdout.trim()) {
|
|
585
|
+
const st = git(repoPath, ["stash", "push", "-u", "-m", "hilos: auto-stash before agent run"]);
|
|
586
|
+
if (st.status !== 0) {
|
|
587
|
+
await tool("post_message", {
|
|
588
|
+
channelId,
|
|
589
|
+
parentId,
|
|
590
|
+
body: `Working tree at ${repoPath} is dirty and I couldn't stash it (${(st.stderr || "").trim().slice(0, 200)}). Commit or stash, then mention me again.`,
|
|
591
|
+
});
|
|
592
|
+
return { status: "dirty" };
|
|
593
|
+
}
|
|
594
|
+
stashed = true;
|
|
563
595
|
await tool("post_message", {
|
|
564
596
|
channelId,
|
|
565
597
|
parentId,
|
|
566
|
-
body: `
|
|
598
|
+
body: `Your working tree was dirty — I stashed your uncommitted changes so I can work on a clean branch, and I'll restore them when I'm done.`,
|
|
567
599
|
});
|
|
568
|
-
return { status: "dirty" };
|
|
569
600
|
}
|
|
601
|
+
// Restore the stash onto the original branch. Best-effort: a pop conflict (rare —
|
|
602
|
+
// the daemon's work lands on its own branch, not here) leaves the stash for the
|
|
603
|
+
// user. Called from `finally` so every exit path restores.
|
|
604
|
+
const restoreStash = async () => {
|
|
605
|
+
if (!stashed) return;
|
|
606
|
+
if (startRef.ref) {
|
|
607
|
+
git(repoPath, startRef.kind === "branch" ? ["switch", "--force", startRef.ref] : ["switch", "--detach", startRef.ref]);
|
|
608
|
+
}
|
|
609
|
+
const pop = git(repoPath, ["stash", "pop"]);
|
|
610
|
+
if (pop.status !== 0) {
|
|
611
|
+
await tool("post_message", {
|
|
612
|
+
channelId,
|
|
613
|
+
parentId,
|
|
614
|
+
body: `Heads up: I couldn't auto-restore your stashed changes (conflict). They're safe — run \`git stash pop\` in ${repoPath} when you're ready.`,
|
|
615
|
+
}).catch(() => {});
|
|
616
|
+
}
|
|
617
|
+
};
|
|
570
618
|
|
|
571
|
-
|
|
572
|
-
// (relying on `git switch -` breaks from a detached HEAD).
|
|
573
|
-
const symref = git(repoPath, ["symbolic-ref", "--quiet", "--short", "HEAD"]);
|
|
574
|
-
const startRef =
|
|
575
|
-
symref.status === 0 && symref.stdout.trim()
|
|
576
|
-
? { kind: "branch", ref: symref.stdout.trim() }
|
|
577
|
-
: { kind: "detached", ref: (git(repoPath, ["rev-parse", "HEAD"]).stdout || "").trim() };
|
|
578
|
-
|
|
619
|
+
try {
|
|
579
620
|
const branch = branchSlug(message.body, randomBytes(3).toString("hex"));
|
|
580
621
|
git(repoPath, ["fetch", "origin", cfg.defaultBranch]);
|
|
581
622
|
const co = git(repoPath, ["switch", "-c", branch]);
|
|
@@ -896,4 +937,8 @@ export async function handleTask({ message, channelId, tool, me, caps = {} }, cf
|
|
|
896
937
|
});
|
|
897
938
|
await finalizeProgress(`Done on \`${branch}\` — see the report below.`);
|
|
898
939
|
return { ...result, reportMessageId: proposal.reportMessageId, stat: proposal.stat, rounds: round };
|
|
940
|
+
} finally {
|
|
941
|
+
// Whatever happened, give the user their uncommitted work back.
|
|
942
|
+
await restoreStash();
|
|
943
|
+
}
|
|
899
944
|
}
|