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 +30 -1
- package/package.json +2 -2
- package/src/conflicts.mjs +156 -0
- package/src/handler.mjs +927 -97
- package/src/local-harness-events.mjs +370 -0
- package/src/local-harness-transport.mjs +157 -0
- package/src/redact.mjs +360 -7
- package/src/run-inputs.mjs +211 -0
- package/src/run.mjs +9 -0
- package/src/thread-pr.mjs +26 -4
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.
|
|
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.
|
|
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": ">=
|
|
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
|
+
}
|