agent-hitch 0.1.0

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.
@@ -0,0 +1,44 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://agent-hitch.local/schemas/result.schema.json",
4
+ "title": "Hitch run result",
5
+ "type": "object",
6
+ "additionalProperties": true,
7
+ "required": ["schema_version", "run_id", "status", "exit_code", "completed_at"],
8
+ "properties": {
9
+ "schema_version": {
10
+ "const": "1"
11
+ },
12
+ "run_id": {
13
+ "type": "string",
14
+ "pattern": "^run_[a-f0-9]{32}$"
15
+ },
16
+ "status": {
17
+ "enum": ["succeeded", "failed", "timed_out", "cancelled"]
18
+ },
19
+ "exit_code": {
20
+ "type": "integer",
21
+ "minimum": 0
22
+ },
23
+ "output": {
24
+ "type": "string"
25
+ },
26
+ "error": {
27
+ "type": "object",
28
+ "required": ["code", "message"],
29
+ "properties": {
30
+ "code": { "type": "string", "minLength": 1 },
31
+ "message": { "type": "string" }
32
+ },
33
+ "additionalProperties": true
34
+ },
35
+ "started_at": {
36
+ "type": "string",
37
+ "format": "date-time"
38
+ },
39
+ "completed_at": {
40
+ "type": "string",
41
+ "format": "date-time"
42
+ }
43
+ }
44
+ }
@@ -0,0 +1,56 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://agent-hitch.local/schemas/run-request.schema.json",
4
+ "title": "Hitch run request",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["prompt"],
8
+ "oneOf": [
9
+ {
10
+ "required": ["harness_ref"],
11
+ "not": { "required": ["agent"] }
12
+ },
13
+ {
14
+ "required": ["agent"],
15
+ "not": { "required": ["harness_ref"] }
16
+ }
17
+ ],
18
+ "properties": {
19
+ "schema_version": {
20
+ "const": "1"
21
+ },
22
+ "agent": {
23
+ "type": "string",
24
+ "minLength": 1
25
+ },
26
+ "harness_ref": {
27
+ "type": "string",
28
+ "minLength": 1
29
+ },
30
+ "model": {
31
+ "type": "string"
32
+ },
33
+ "cwd": {
34
+ "type": "string",
35
+ "minLength": 1
36
+ },
37
+ "workspace_mode": {
38
+ "enum": ["shared", "worktree", "copy"],
39
+ "default": "shared"
40
+ },
41
+ "prompt": {
42
+ "type": "string",
43
+ "minLength": 1
44
+ },
45
+ "timeout_ms": {
46
+ "type": "number",
47
+ "minimum": 0
48
+ },
49
+ "agent_args": {
50
+ "type": "array",
51
+ "items": {
52
+ "type": "string"
53
+ }
54
+ }
55
+ }
56
+ }
@@ -0,0 +1,55 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://agent-hitch.local/schemas/workspace.schema.json",
4
+ "title": "Hitch run workspace record",
5
+ "type": "object",
6
+ "additionalProperties": true,
7
+ "required": [
8
+ "schema_version",
9
+ "run_id",
10
+ "mode",
11
+ "status",
12
+ "source_workspace",
13
+ "source_realpath",
14
+ "execution_workspace",
15
+ "snapshot",
16
+ "retained"
17
+ ],
18
+ "properties": {
19
+ "schema_version": { "const": "1" },
20
+ "run_id": { "type": "string", "pattern": "^run_[a-f0-9]{32}$" },
21
+ "mode": { "enum": ["shared", "worktree", "copy"] },
22
+ "status": {
23
+ "enum": [
24
+ "planned",
25
+ "provisioning",
26
+ "ready",
27
+ "running",
28
+ "retained",
29
+ "orphaned",
30
+ "released",
31
+ "cancelled",
32
+ "failed",
33
+ "unused",
34
+ "removed"
35
+ ]
36
+ },
37
+ "source_workspace": { "type": "string", "minLength": 1 },
38
+ "source_realpath": { "type": "string", "minLength": 1 },
39
+ "execution_workspace": { "type": "string", "minLength": 1 },
40
+ "changed": { "type": ["boolean", "null"] },
41
+ "retained": { "type": "boolean" },
42
+ "snapshot": {
43
+ "type": "object",
44
+ "required": ["consistency", "source_subdirectory"],
45
+ "properties": {
46
+ "consistency": { "enum": ["none", "git_commit", "best_effort"] },
47
+ "commit": { "type": "string", "pattern": "^[a-f0-9]{40,64}$" },
48
+ "source_subdirectory": { "type": "string" },
49
+ "content_digest": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" },
50
+ "captured_at": { "type": "string", "format": "date-time" }
51
+ },
52
+ "additionalProperties": true
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,217 @@
1
+ """Harbor custom agent that runs an immutable harness through Hitch in a trial container."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import shlex
7
+ import tempfile
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from harbor.agents.base import BaseAgent
12
+ from harbor.environments.base import BaseEnvironment, ExecResult
13
+ from harbor.models.agent.context import AgentContext
14
+
15
+
16
+ class HitchHarborAgent(BaseAgent):
17
+ """Upload Hitch into a Harbor environment and delegate the agent phase to it."""
18
+
19
+ def __init__(
20
+ self,
21
+ logs_dir: Path,
22
+ harness_ref: str,
23
+ revision_identity: str,
24
+ hitch_runtime_dir: str,
25
+ candidate_id: str = "candidate-1",
26
+ hitch_timeout_ms: int = 900_000,
27
+ agent_args: list[str] | None = None,
28
+ workdir: str = "/app",
29
+ **kwargs: Any,
30
+ ) -> None:
31
+ super().__init__(logs_dir=logs_dir, **kwargs)
32
+ self.harness_ref = harness_ref
33
+ self.revision_identity = revision_identity
34
+ self.hitch_runtime_dir = Path(hitch_runtime_dir)
35
+ self.candidate_id = candidate_id
36
+ self.hitch_timeout_ms = int(hitch_timeout_ms)
37
+ self.agent_args = list(agent_args or [])
38
+ self.workdir = workdir
39
+ self._hitch_version: str | None = None
40
+
41
+ @staticmethod
42
+ def name() -> str:
43
+ return "hitch"
44
+
45
+ def version(self) -> str | None:
46
+ return self._hitch_version
47
+
48
+ async def setup(self, environment: BaseEnvironment) -> None:
49
+ if not self.hitch_runtime_dir.is_dir():
50
+ raise RuntimeError(f"Hitch runtime directory does not exist: {self.hitch_runtime_dir}")
51
+ await environment.upload_dir(self.hitch_runtime_dir, "/opt/hitch")
52
+ await self._ensure_node(environment)
53
+ version = await self._exec(environment, f"{self._node_prefix()} node /opt/hitch/bin/hitch.js --version")
54
+ self._hitch_version = (version.stdout or "").strip() or None
55
+ prepare = " ".join(
56
+ [
57
+ self._node_prefix(),
58
+ "HITCH_ROOT=/tmp/hitch-state",
59
+ "node /opt/hitch/bin/hitch.js prepare",
60
+ shlex.quote(self.harness_ref),
61
+ "--json",
62
+ ]
63
+ )
64
+ await self._exec(environment, prepare)
65
+
66
+ async def run(
67
+ self,
68
+ instruction: str,
69
+ environment: BaseEnvironment,
70
+ context: AgentContext,
71
+ ) -> None:
72
+ self.logs_dir.mkdir(parents=True, exist_ok=True)
73
+ temporary: Path | None = None
74
+ try:
75
+ with tempfile.NamedTemporaryFile(
76
+ mode="w",
77
+ encoding="utf-8",
78
+ prefix="hitch-instruction-",
79
+ suffix=".txt",
80
+ dir=self.logs_dir,
81
+ delete=False,
82
+ ) as handle:
83
+ handle.write(instruction)
84
+ temporary = Path(handle.name)
85
+ remote_instruction = "/tmp/hitch-instruction.txt"
86
+ await environment.upload_file(temporary, remote_instruction)
87
+ finally:
88
+ if temporary is not None:
89
+ temporary.unlink(missing_ok=True)
90
+
91
+ arguments = [
92
+ self._node_prefix(),
93
+ "HITCH_ROOT=/tmp/hitch-state",
94
+ "node /opt/hitch/bin/hitch.js run",
95
+ "--harness",
96
+ shlex.quote(self.harness_ref),
97
+ "--cwd",
98
+ shlex.quote(self.workdir),
99
+ "--workspace-mode",
100
+ "shared",
101
+ "--prompt-file",
102
+ shlex.quote(remote_instruction),
103
+ "--timeout",
104
+ str(self.hitch_timeout_ms),
105
+ "--output",
106
+ "jsonl",
107
+ ]
108
+ if self.model_name:
109
+ arguments.extend(["--model", shlex.quote(self.model_name)])
110
+ for value in self.agent_args:
111
+ arguments.extend(["--agent-arg", shlex.quote(value)])
112
+ command = (
113
+ "set -o pipefail; "
114
+ + " ".join(arguments)
115
+ + " 2> >(tee /logs/agent/hitch-stderr.log >&2)"
116
+ + " | tee /logs/agent/hitch-events.jsonl"
117
+ )
118
+ execution = await environment.exec(command, cwd=self.workdir)
119
+ events = self._events(execution.stdout or "")
120
+ run_id = next((event.get("run_id") for event in events if event.get("run_id")), None)
121
+ hitch_result = None
122
+ if run_id:
123
+ result_path = f"/tmp/hitch-state/runs/{run_id}/result.json"
124
+ result = await environment.exec(
125
+ f"cat {shlex.quote(result_path)} | tee /logs/agent/hitch-result.json"
126
+ )
127
+ if result.return_code == 0 and result.stdout:
128
+ hitch_result = json.loads(result.stdout)
129
+ context.metadata = {
130
+ "candidate_id": self.candidate_id,
131
+ "harness_ref": self.harness_ref,
132
+ "revision_identity": self.revision_identity,
133
+ "hitch_run_id": run_id,
134
+ "hitch_status": hitch_result.get("status") if hitch_result else None,
135
+ "hitch_artifact_id": hitch_result.get("artifact_id") if hitch_result else None,
136
+ }
137
+ if execution.return_code != 0:
138
+ message = (execution.stderr or "").strip()
139
+ if hitch_result and hitch_result.get("error", {}).get("message"):
140
+ message = hitch_result["error"]["message"]
141
+ raise RuntimeError(
142
+ f"Hitch agent run failed with code {execution.return_code}: {message or 'no diagnostic output'}"
143
+ )
144
+ if hitch_result is None:
145
+ raise RuntimeError("Hitch agent run completed without a persisted result")
146
+ if hitch_result.get("revision_identity") != self.revision_identity:
147
+ raise RuntimeError(
148
+ "Hitch resolved a different harness revision inside the trial container: "
149
+ f"expected {self.revision_identity}, got {hitch_result.get('revision_identity')}"
150
+ )
151
+
152
+ async def _ensure_node(self, environment: BaseEnvironment) -> None:
153
+ probe = await environment.exec(
154
+ "node -e 'process.exit(Number(process.versions.node.split(\".\")[0]) >= 22 ? 0 : 1)'"
155
+ )
156
+ needs_git = "@commit:" in self.harness_ref
157
+ git_probe = await environment.exec("command -v git >/dev/null 2>&1")
158
+ if probe.return_code == 0 and (not needs_git or git_probe.return_code == 0):
159
+ return
160
+ prerequisites = """
161
+ set -eu
162
+ if command -v curl >/dev/null 2>&1 && command -v git >/dev/null 2>&1; then exit 0; fi
163
+ if command -v apt-get >/dev/null 2>&1; then
164
+ apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y curl ca-certificates git
165
+ elif command -v apk >/dev/null 2>&1; then
166
+ apk add --no-cache curl ca-certificates git bash
167
+ elif command -v dnf >/dev/null 2>&1; then
168
+ dnf install -y curl ca-certificates git
169
+ elif command -v yum >/dev/null 2>&1; then
170
+ yum install -y curl ca-certificates git
171
+ else
172
+ echo 'Hitch requires Node.js 22+ and could not install curl in this task image' >&2
173
+ exit 1
174
+ fi
175
+ """
176
+ await self._exec(environment, prerequisites, user=0)
177
+ if probe.return_code == 0:
178
+ return
179
+ install = """
180
+ set -eu
181
+ export NVM_DIR=/opt/hitch-node
182
+ mkdir -p "$NVM_DIR"
183
+ curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
184
+ . "$NVM_DIR/nvm.sh"
185
+ nvm install 22
186
+ nvm alias default 22
187
+ node -e 'process.exit(Number(process.versions.node.split(".")[0]) >= 22 ? 0 : 1)'
188
+ """
189
+ await self._exec(environment, install, user=0)
190
+
191
+ @staticmethod
192
+ def _node_prefix() -> str:
193
+ return "if [ -s /opt/hitch-node/nvm.sh ]; then export NVM_DIR=/opt/hitch-node; . /opt/hitch-node/nvm.sh; fi;"
194
+
195
+ @staticmethod
196
+ def _events(output: str) -> list[dict[str, Any]]:
197
+ events: list[dict[str, Any]] = []
198
+ for line in output.splitlines():
199
+ try:
200
+ value = json.loads(line)
201
+ except json.JSONDecodeError:
202
+ continue
203
+ if isinstance(value, dict):
204
+ events.append(value)
205
+ return events
206
+
207
+ @staticmethod
208
+ async def _exec(
209
+ environment: BaseEnvironment,
210
+ command: str,
211
+ user: str | int | None = None,
212
+ ) -> ExecResult:
213
+ result = await environment.exec(command, user=user)
214
+ if result.return_code != 0:
215
+ diagnostic = (result.stderr or result.stdout or "no output").strip()
216
+ raise RuntimeError(f"container setup command failed ({result.return_code}): {diagnostic}")
217
+ return result
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "agent-hitch",
3
+ "version": "0.1.0",
4
+ "description": "A local runtime and daemon for coding agents",
5
+ "keywords": [
6
+ "ai-agents",
7
+ "cli",
8
+ "coding-agents",
9
+ "daemon"
10
+ ],
11
+ "homepage": "https://github.com/rsi-gear/agent-hitch#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/rsi-gear/agent-hitch/issues"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/rsi-gear/agent-hitch.git"
18
+ },
19
+ "type": "module",
20
+ "bin": {
21
+ "hitch": "bin/hitch.js"
22
+ },
23
+ "files": [
24
+ "bin/",
25
+ "src/",
26
+ "integrations/harbor/hitch_harbor_agent.py",
27
+ "docs/schemas/"
28
+ ],
29
+ "scripts": {
30
+ "test": "node --test",
31
+ "coverage": "node --experimental-test-coverage --test-coverage-include=src/*.js --test-coverage-lines=75 --test-coverage-branches=65 --test-coverage-functions=70 --test",
32
+ "check": "node scripts/check-syntax.js && node --test",
33
+ "prepublishOnly": "npm run check"
34
+ },
35
+ "engines": {
36
+ "node": ">=22"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "license": "Apache-2.0"
42
+ }