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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. 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.14",
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
- const r = await tool("post_message", { channelId, parentId, body });
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 };