hilos-agent 0.1.14 → 0.1.15
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 +34 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hilos-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
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
|
@@ -104,12 +104,16 @@ async function linkPrFromUrl({ tool, channelId, url }) {
|
|
|
104
104
|
async function applyDecision({ decision, repoPath, branch, task, requester, cfg, tool, channelId, deps, parentId }) {
|
|
105
105
|
const tag = requesterTag(requester);
|
|
106
106
|
const lead = tag ? `${tag} — ` : "";
|
|
107
|
+
// `parentId` here is the run's thread root: every outcome below is terminal, so
|
|
108
|
+
// it broadcasts back to the channel (visible in both thread and channel) — the
|
|
109
|
+
// hosted runner's reply_broadcast. A no-op when there's no thread root.
|
|
107
110
|
if (decision.kind === "approved") {
|
|
108
111
|
// Guard: nothing staged → don't push an empty branch and falsely report "shipped".
|
|
109
112
|
if (deps.git(repoPath, ["diff", "--cached", "--quiet"]).status === 0) {
|
|
110
113
|
await tool("post_message", {
|
|
111
114
|
channelId,
|
|
112
115
|
parentId,
|
|
116
|
+
broadcast: true,
|
|
113
117
|
body: `Approved, but there are no staged changes to commit on \`${branch}\`.`,
|
|
114
118
|
});
|
|
115
119
|
return { status: "nothing-staged", branch };
|
|
@@ -119,6 +123,7 @@ async function applyDecision({ decision, repoPath, branch, task, requester, cfg,
|
|
|
119
123
|
await tool("post_message", {
|
|
120
124
|
channelId,
|
|
121
125
|
parentId,
|
|
126
|
+
broadcast: true,
|
|
122
127
|
body: `Approved, but the commit failed: ${(commit.stderr || "").trim().slice(0, 300)}`,
|
|
123
128
|
});
|
|
124
129
|
return { status: "commit-failed", branch };
|
|
@@ -128,6 +133,7 @@ async function applyDecision({ decision, repoPath, branch, task, requester, cfg,
|
|
|
128
133
|
await tool("post_message", {
|
|
129
134
|
channelId,
|
|
130
135
|
parentId,
|
|
136
|
+
broadcast: true,
|
|
131
137
|
body: `Approved, but the push failed: ${(push.stderr || "").trim().slice(0, 300)}`,
|
|
132
138
|
});
|
|
133
139
|
return { status: "push-failed", branch };
|
|
@@ -137,6 +143,7 @@ async function applyDecision({ decision, repoPath, branch, task, requester, cfg,
|
|
|
137
143
|
await tool("post_report", {
|
|
138
144
|
channelId,
|
|
139
145
|
parentId,
|
|
146
|
+
broadcast: true,
|
|
140
147
|
title: `Shipped: ${title}`,
|
|
141
148
|
summary:
|
|
142
149
|
pr.ok && pr.url
|
|
@@ -155,7 +162,7 @@ async function applyDecision({ decision, repoPath, branch, task, requester, cfg,
|
|
|
155
162
|
deps.git(repoPath, ["clean", "-fd"]);
|
|
156
163
|
deps.git(repoPath, ["switch", "-"]);
|
|
157
164
|
deps.git(repoPath, ["branch", "-D", branch]);
|
|
158
|
-
await tool("post_message", { channelId, parentId, body: `Rejected — discarded \`${branch}\`.` });
|
|
165
|
+
await tool("post_message", { channelId, parentId, broadcast: true, body: `Rejected — discarded \`${branch}\`.` });
|
|
159
166
|
return { status: "discarded", branch };
|
|
160
167
|
}
|
|
161
168
|
|
|
@@ -163,6 +170,7 @@ async function applyDecision({ decision, repoPath, branch, task, requester, cfg,
|
|
|
163
170
|
await tool("post_message", {
|
|
164
171
|
channelId,
|
|
165
172
|
parentId,
|
|
173
|
+
broadcast: true,
|
|
166
174
|
body: `Got it${decision.note ? `: ${decision.note}` : ""}. Leaving \`${branch}\` for a follow-up.`,
|
|
167
175
|
});
|
|
168
176
|
return { status: "changes", branch, note: decision.note };
|
|
@@ -171,6 +179,7 @@ async function applyDecision({ decision, repoPath, branch, task, requester, cfg,
|
|
|
171
179
|
await tool("post_message", {
|
|
172
180
|
channelId,
|
|
173
181
|
parentId,
|
|
182
|
+
broadcast: true,
|
|
174
183
|
body: `Still awaiting review on \`${branch}\`. Re-mention me once you've decided.`,
|
|
175
184
|
});
|
|
176
185
|
return { status: "timeout", branch };
|
|
@@ -201,6 +210,7 @@ async function shipSelfDriven({ repoPath, branch, task, requester, cfg, tool, ch
|
|
|
201
210
|
await tool("post_report", {
|
|
202
211
|
channelId,
|
|
203
212
|
parentId,
|
|
213
|
+
broadcast: true,
|
|
204
214
|
title: `Shipped: ${title}`,
|
|
205
215
|
summary: prUrl
|
|
206
216
|
? `${lead}the coding agent committed on \`${cur}\` and opened a pull request.`
|
|
@@ -635,6 +645,13 @@ export async function handleTask({ message, channelId, tool, me, caps = {} }, cf
|
|
|
635
645
|
// Best-effort — a slow or failed ack just leaves the template, never blocks.
|
|
636
646
|
const ack = await tool("post_message", { channelId, parentId, body: ackText(repoFullName) });
|
|
637
647
|
const ackId = ack?.messageId ?? null;
|
|
648
|
+
// Where the run streams + reports. When the mention was already in a thread,
|
|
649
|
+
// stay in it. When it was top-level, the ack becomes the thread anchor so the
|
|
650
|
+
// progress worklog and the report card live UNDER it — one tidy thread instead
|
|
651
|
+
// of three separate top-level posts — exactly like the hosted/native runner.
|
|
652
|
+
// The ack itself stays top-level (the channel-visible "on it"); terminal
|
|
653
|
+
// outcomes + report cards broadcast back to the channel from the thread.
|
|
654
|
+
const threadRoot = parentId ?? ackId;
|
|
638
655
|
if (ackId && caps.editMessage && cfg.chatCmd) {
|
|
639
656
|
// Feed the ack the router's distilled brief AND the conversation — not the raw
|
|
640
657
|
// mention — so it states a real plan instead of "what's the task?".
|
|
@@ -666,7 +683,8 @@ export async function handleTask({ message, channelId, tool, me, caps = {} }, cf
|
|
|
666
683
|
const body = buildHeartbeat({ repoFullName, branch, elapsedMs: deps.now() - start, lastLine });
|
|
667
684
|
try {
|
|
668
685
|
if (!progressId) {
|
|
669
|
-
|
|
686
|
+
// Progress is the worklog — it streams in the thread only (no broadcast).
|
|
687
|
+
const r = await tool("post_message", { channelId, parentId: threadRoot, body });
|
|
670
688
|
progressId = r?.messageId ?? null;
|
|
671
689
|
} else {
|
|
672
690
|
await tool("edit_message", { messageId: progressId, body });
|
|
@@ -774,7 +792,7 @@ export async function handleTask({ message, channelId, tool, me, caps = {} }, cf
|
|
|
774
792
|
stat: staged.stat,
|
|
775
793
|
runFailed: staged.runFailed,
|
|
776
794
|
});
|
|
777
|
-
const res = await tool("post_report", { channelId, parentId, ...report });
|
|
795
|
+
const res = await tool("post_report", { channelId, parentId: threadRoot, broadcast: true, ...report });
|
|
778
796
|
return { reportMessageId: res?.messageId ?? null, stat: staged.stat };
|
|
779
797
|
};
|
|
780
798
|
|
|
@@ -805,7 +823,8 @@ export async function handleTask({ message, channelId, tool, me, caps = {} }, cf
|
|
|
805
823
|
const clean = discardBranch();
|
|
806
824
|
await tool("post_message", {
|
|
807
825
|
channelId,
|
|
808
|
-
parentId,
|
|
826
|
+
parentId: threadRoot,
|
|
827
|
+
broadcast: true,
|
|
809
828
|
body: clean
|
|
810
829
|
? `Stopped — discarded \`${branch}\`.`
|
|
811
830
|
: `Stopped, but couldn't fully clean \`${branch}\` — run \`git reset --hard && git switch ${startRef.ref || cfg.defaultBranch}\` in ${repoPath}.`,
|
|
@@ -823,7 +842,8 @@ export async function handleTask({ message, channelId, tool, me, caps = {} }, cf
|
|
|
823
842
|
// honest and let the human review instead of auto-shipping. Leave the branch.
|
|
824
843
|
await tool("post_message", {
|
|
825
844
|
channelId,
|
|
826
|
-
parentId,
|
|
845
|
+
parentId: threadRoot,
|
|
846
|
+
broadcast: true,
|
|
827
847
|
body:
|
|
828
848
|
`The coding agent committed on \`${branch}\` itself (I asked it to only edit files). ` +
|
|
829
849
|
`Under approve-before-push I won't auto-push it — review \`${branch}\` locally, then ` +
|
|
@@ -842,7 +862,7 @@ export async function handleTask({ message, channelId, tool, me, caps = {} }, cf
|
|
|
842
862
|
tool,
|
|
843
863
|
channelId,
|
|
844
864
|
deps,
|
|
845
|
-
parentId,
|
|
865
|
+
parentId: threadRoot,
|
|
846
866
|
});
|
|
847
867
|
}
|
|
848
868
|
if (staged.empty) {
|
|
@@ -860,7 +880,7 @@ export async function handleTask({ message, channelId, tool, me, caps = {} }, cf
|
|
|
860
880
|
} else {
|
|
861
881
|
body = `No changes were produced. Cleaning up \`${branch}\`.`;
|
|
862
882
|
}
|
|
863
|
-
await tool("post_message", { channelId, parentId, body });
|
|
883
|
+
await tool("post_message", { channelId, parentId: threadRoot, broadcast: true, body });
|
|
864
884
|
await finalizeProgress(
|
|
865
885
|
staged.failed ? `Run ended early on \`${branch}\` — see the note below.` : `No changes needed on \`${branch}\`.`,
|
|
866
886
|
);
|
|
@@ -885,7 +905,7 @@ export async function handleTask({ message, channelId, tool, me, caps = {} }, cf
|
|
|
885
905
|
tool,
|
|
886
906
|
channelId,
|
|
887
907
|
deps,
|
|
888
|
-
parentId,
|
|
908
|
+
parentId: threadRoot,
|
|
889
909
|
});
|
|
890
910
|
await finalizeProgress(`Done on \`${branch}\` — see the report below.`);
|
|
891
911
|
return { ...result, stat: staged.stat };
|
|
@@ -893,14 +913,14 @@ export async function handleTask({ message, channelId, tool, me, caps = {} }, cf
|
|
|
893
913
|
|
|
894
914
|
await finalizeProgress(`Coding done on \`${branch}\` — proposal below for review.`);
|
|
895
915
|
let proposal = await postProposal(staged);
|
|
896
|
-
let decision = await awaitDecision({ tool, channelId, reportMessageId: proposal.reportMessageId, cfg, deps, parentId, signal });
|
|
916
|
+
let decision = await awaitDecision({ tool, channelId, reportMessageId: proposal.reportMessageId, cfg, deps, parentId: threadRoot, signal });
|
|
897
917
|
if (decision.kind === "cancelled") return await postStopped();
|
|
898
918
|
const maxRounds = cfg.maxRounds || 3;
|
|
899
919
|
let round = 1;
|
|
900
920
|
while (decision.kind === "changes" && round < maxRounds) {
|
|
901
921
|
await tool("post_message", {
|
|
902
922
|
channelId,
|
|
903
|
-
parentId,
|
|
923
|
+
parentId: threadRoot,
|
|
904
924
|
body: `Revising with your feedback${decision.note ? `: ${decision.note}` : ""} (round ${round + 1}/${maxRounds}).`,
|
|
905
925
|
});
|
|
906
926
|
const refined = await runAndStage(
|
|
@@ -910,13 +930,14 @@ export async function handleTask({ message, channelId, tool, me, caps = {} }, cf
|
|
|
910
930
|
if (refined.empty) {
|
|
911
931
|
await tool("post_message", {
|
|
912
932
|
channelId,
|
|
913
|
-
parentId,
|
|
933
|
+
parentId: threadRoot,
|
|
934
|
+
broadcast: true,
|
|
914
935
|
body: `That feedback produced no further changes — leaving \`${branch}\` as proposed.`,
|
|
915
936
|
});
|
|
916
937
|
break;
|
|
917
938
|
}
|
|
918
939
|
proposal = await postProposal(refined);
|
|
919
|
-
decision = await awaitDecision({ tool, channelId, reportMessageId: proposal.reportMessageId, cfg, deps, parentId, signal });
|
|
940
|
+
decision = await awaitDecision({ tool, channelId, reportMessageId: proposal.reportMessageId, cfg, deps, parentId: threadRoot, signal });
|
|
920
941
|
if (decision.kind === "cancelled") return await postStopped();
|
|
921
942
|
round += 1;
|
|
922
943
|
}
|
|
@@ -933,7 +954,7 @@ export async function handleTask({ message, channelId, tool, me, caps = {} }, cf
|
|
|
933
954
|
tool,
|
|
934
955
|
channelId,
|
|
935
956
|
deps,
|
|
936
|
-
parentId,
|
|
957
|
+
parentId: threadRoot,
|
|
937
958
|
});
|
|
938
959
|
await finalizeProgress(`Done on \`${branch}\` — see the report below.`);
|
|
939
960
|
return { ...result, reportMessageId: proposal.reportMessageId, stat: proposal.stat, rounds: round };
|