hilos-agent 0.10.1 → 0.11.1

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
@@ -158,7 +158,16 @@ can never approve or merge hilos work. See the complete contract in
158
158
  PR thread is executed, not described. The daemon relays the request to hilos
159
159
  with the id of the message that asked; hilos verifies the person's role and
160
160
  that their message really asks for it, then acts with the workspace's GitHub
161
- App. The daemon never merges on its own judgment and holds no merge rights.
161
+ App. If GitHub reports a same-repository merge conflict, the daemon keeps the
162
+ same turn alive: it fetches the base branch, starts a real two-parent merge on
163
+ the existing PR branch, names the conflicted files to the coding agent,
164
+ verifies that no unmerged entries or conflict markers remain, and pushes the
165
+ repaired head for fresh human review. It never opens a replacement PR or
166
+ silently merges after repair. Fork PRs stop with an explicit writable-branch
167
+ boundary. The daemon never merges on its own judgment and holds no merge
168
+ rights. With `gate:true`, this existing-branch repair still pushes so the PR
169
+ can return for fresh review. It is the only approve-before-push exception;
170
+ the repaired head remains unmerged and needs a new approval.
162
171
  - **Open a PR** (default) — it commits, pushes with *your* `git`/`gh`, opens a PR,
163
172
  and posts a report card with the link. Review on the card: **Approve** merges,
164
173
  **Reject** closes, **Request changes** re-works.
@@ -396,6 +405,16 @@ hilos's GitHub App only for workspace owners/admins). Want a human checkpoint
396
405
  before anything is pushed? Set `"gate": true`. Keep your token in the config file
397
406
  or `HILOS_TOKEN`, never in shared shell history.
398
407
 
408
+ ### Durable activity, not raw output
409
+
410
+ On a current hilos server, daemon-launched Claude Code, Codex, Cursor, and
411
+ OpenCode repo runs automatically keep the same bounded activity facts used by
412
+ the live card—phases, tool/file activity, notes, reported usage, and completion—
413
+ in hilos's runtime-neutral event record. Writes are batched, retry-safe,
414
+ bounded to two one-second attempts, re-authorized and redacted on the server,
415
+ and never allowed to fail the coding run. Raw stdout is not sent by this path.
416
+ An older server simply leaves it off.
417
+
399
418
  ### Run transcripts are opt-in
400
419
 
401
420
  The room gets what a teammate needs to see: a plan, live progress, a report, a
@@ -426,3 +445,13 @@ restart — and **deleting** the key counts as off, not as "leave it as it was".
426
445
 
427
446
  Env: `HILOS_TOKEN`, `HILOS_URL`, `HILOS_CHANNEL`, `CODING_CMD`, `HILOS_ONCE=1`,
428
447
  `HILOS_BACKFILL=1`, `HILOS_UPLOAD_TRANSCRIPTS=1|0`.
448
+
449
+ ## Releasing
450
+
451
+ Bump `version` in `package.json`, merge, then tag that commit
452
+ `hilos-agent-v<version>` and push the tag. The `npm-publish` workflow checks
453
+ the tag against the tree, parses every source file on Node 20, 22, and 24,
454
+ and publishes to npm (without provenance while the repository is private:
455
+ npm refuses a provenance bundle from a private source repository, 1182).
456
+ `workflow_dispatch` runs the same pipeline as a dry run. Needs the
457
+ `NPM_TOKEN` repository secret (1094).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hilos-agent",
3
- "version": "0.10.1",
3
+ "version": "0.11.1",
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": {
@@ -12,7 +12,7 @@
12
12
  "README.md"
13
13
  ],
14
14
  "engines": {
15
- "node": ">=18"
15
+ "node": ">=20"
16
16
  },
17
17
  "homepage": "https://hilos.sh",
18
18
  "repository": {
@@ -0,0 +1,156 @@
1
+ // 0991 — deterministic Git ownership for local merge-conflict recovery.
2
+ // The coding model edits files; this wrapper creates and validates the merge.
3
+
4
+ const MAX_NAMED_FILES = 40;
5
+
6
+ const lines = (value) =>
7
+ String(value || "")
8
+ .split("\n")
9
+ .map((line) => line.trim())
10
+ .filter(Boolean);
11
+
12
+ export function validConflictRecovery(value, repoFullName) {
13
+ if (!value || typeof value !== "object" || value.repairable !== true) return null;
14
+ const required = ["repoFullName", "prUrl", "branch", "baseBranch", "headSha"];
15
+ if (required.some((key) => typeof value[key] !== "string" || !value[key].trim())) return null;
16
+ if (!Number.isInteger(value.prNumber) || value.prNumber <= 0) return null;
17
+ if (
18
+ repoFullName &&
19
+ value.repoFullName.toLowerCase() !== String(repoFullName).toLowerCase()
20
+ ) {
21
+ return null;
22
+ }
23
+ return {
24
+ repoFullName: value.repoFullName,
25
+ prNumber: value.prNumber,
26
+ prUrl: value.prUrl,
27
+ branch: value.branch,
28
+ baseBranch: value.baseBranch,
29
+ headSha: value.headSha,
30
+ };
31
+ }
32
+
33
+ export function conflictRepairBrief(conflict) {
34
+ return (
35
+ `Resolve the merge conflicts on existing pull request #${conflict.prNumber} ` +
36
+ `(${conflict.prUrl}). Preserve the intended work from both \`${conflict.branch}\` and ` +
37
+ `\`${conflict.baseBranch}\`, run the repository's required verification, and update only ` +
38
+ `that existing pull request. Do not merge it; return the changed head for fresh human review.`
39
+ );
40
+ }
41
+
42
+ export function conflictPromptBlock({ baseBranch, files }) {
43
+ if (!files.length) {
44
+ return (
45
+ `hilos has already merged \`${baseBranch}\` into this pull request branch and Git resolved every file cleanly. ` +
46
+ "The checkout is still mid-merge so hilos can make the two-parent merge commit after you finish. " +
47
+ "Run the repository's required verification and make no edits unless verification exposes a real integration problem. Do not run git."
48
+ );
49
+ }
50
+ const named = files.slice(0, MAX_NAMED_FILES);
51
+ const rest = files.length - named.length;
52
+ return [
53
+ `hilos has already started a real merge of \`${baseBranch}\` into this pull request branch. Resolve EVERY conflict by editing the files into the correct combined result. Keep BOTH sides' work unless they are genuinely exclusive; never discard the base branch merely to remove a marker. Regenerate generated files from their sources when the repository provides a generator. Do not run git — hilos owns the merge commit and refuses to push unresolved entries or marker lines.`,
54
+ ...(named.length ? ["Conflicted files:", ...named.map((file) => `- ${file}`)] : []),
55
+ ...(rest > 0 ? [`…and ${rest} more.`] : []),
56
+ ].join("\n");
57
+ }
58
+
59
+ /** Fetch + start the two-parent merge before the coding model runs. */
60
+ export function startConflictMerge({ git, cwd, conflict }) {
61
+ const fetched = git(cwd, [
62
+ "fetch",
63
+ "origin",
64
+ `+refs/heads/${conflict.baseBranch}:refs/remotes/origin/${conflict.baseBranch}`,
65
+ ]);
66
+ if (fetched.status !== 0) {
67
+ return {
68
+ ok: false,
69
+ reason: `couldn't fetch ${conflict.baseBranch}: ${String(fetched.stderr || "").trim().slice(0, 200)}`,
70
+ };
71
+ }
72
+
73
+ const merged = git(cwd, [
74
+ "merge",
75
+ "--no-commit",
76
+ "--no-ff",
77
+ `origin/${conflict.baseBranch}`,
78
+ ]);
79
+ const files = lines(git(cwd, ["diff", "--name-only", "--diff-filter=U"]).stdout);
80
+ if (merged.status !== 0 && files.length === 0) {
81
+ git(cwd, ["merge", "--abort"]);
82
+ return {
83
+ ok: false,
84
+ reason: `couldn't start the merge: ${String(merged.stderr || "").trim().slice(0, 200)}`,
85
+ };
86
+ }
87
+
88
+ const mergeHead = git(cwd, ["rev-parse", "-q", "--verify", "MERGE_HEAD"]);
89
+ if (merged.status === 0 && mergeHead.status !== 0) {
90
+ return { ok: true, state: "up-to-date", files: [] };
91
+ }
92
+ if (mergeHead.status !== 0) {
93
+ git(cwd, ["merge", "--abort"]);
94
+ return { ok: false, reason: "Git did not leave a merge to complete." };
95
+ }
96
+ return { ok: true, state: files.length ? "conflicts" : "clean", files };
97
+ }
98
+
99
+ /** Check BEFORE git add: staging clears unmerged index entries. Marker scanning
100
+ * catches a model that ran git add itself despite the prompt.
101
+ * @param {{ git: (cwd: string, args: string[]) => { stdout?: string }, cwd: string, knownFiles?: string[] }} args
102
+ */
103
+ export function remainingConflictFiles({ git, cwd, knownFiles = [] }) {
104
+ const unmerged = lines(git(cwd, ["diff", "--name-only", "--diff-filter=U"]).stdout);
105
+ const known = new Set(knownFiles);
106
+ // The original paths remain unmerged in Git's index until the wrapper stages
107
+ // the model's edited result. That state alone is expected here; only a NEW
108
+ // unmerged path (the model started another merge) is structurally unsafe.
109
+ const unexpectedUnmerged = unmerged.filter((file) => !known.has(file));
110
+ // With no original conflicts there is nothing useful to scan. Running
111
+ // `git grep` without pathspecs would inspect the entire repository and could
112
+ // mistake a deliberate marker example in documentation for this merge's
113
+ // unresolved work.
114
+ const markers = knownFiles.length
115
+ ? lines(
116
+ git(cwd, [
117
+ "grep",
118
+ "-l",
119
+ "-e",
120
+ "^<<<<<<< ",
121
+ "-e",
122
+ "^>>>>>>> ",
123
+ "--",
124
+ ...knownFiles,
125
+ ]).stdout,
126
+ )
127
+ : [];
128
+ return [...new Set([...unexpectedUnmerged, ...markers])].sort();
129
+ }
130
+
131
+ /** A coding CLI may run git despite the prompt. Accept its self-commit only
132
+ * when Git proves it completed the prepared merge; reject an aborted/reset
133
+ * merge before anything can be pushed.
134
+ * @param {{ git: (cwd: string, args: string[]) => { status: number, stdout?: string }, cwd: string, baseBranch: string }} args
135
+ */
136
+ export function validateConflictMergeState({ git, cwd, baseBranch }) {
137
+ if (git(cwd, ["rev-parse", "-q", "--verify", "MERGE_HEAD"]).status === 0) {
138
+ return { ok: true, committedByAgent: false };
139
+ }
140
+ const baseIsAncestor =
141
+ git(cwd, ["merge-base", "--is-ancestor", `origin/${baseBranch}`, "HEAD"]).status === 0;
142
+ const parents = lines(git(cwd, ["rev-list", "--parents", "-n", "1", "HEAD"]).stdout)[0]
143
+ ?.split(/\s+/)
144
+ .filter(Boolean) ?? [];
145
+ if (baseIsAncestor && parents.length >= 3) {
146
+ return { ok: true, committedByAgent: true };
147
+ }
148
+ return {
149
+ ok: false,
150
+ reason: `the prepared merge of ${baseBranch} was aborted or replaced before verification`,
151
+ };
152
+ }
153
+
154
+ export function conflictCommitTitle(baseBranch) {
155
+ return `Merge ${baseBranch}: resolve conflicts`;
156
+ }