@agentmeshhq/agent 0.2.0 → 0.2.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.
Files changed (137) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +39 -0
  3. package/dist/__tests__/orphan-process.test.d.ts +11 -0
  4. package/dist/__tests__/orphan-process.test.js +286 -0
  5. package/dist/__tests__/orphan-process.test.js.map +1 -0
  6. package/dist/__tests__/runner.test.js +16 -0
  7. package/dist/__tests__/runner.test.js.map +1 -1
  8. package/dist/__tests__/watchdog.test.js +138 -12
  9. package/dist/__tests__/watchdog.test.js.map +1 -1
  10. package/dist/cli/index.js +0 -0
  11. package/dist/cli/status.js +11 -0
  12. package/dist/cli/status.js.map +1 -1
  13. package/dist/cli/stop.js +7 -2
  14. package/dist/cli/stop.js.map +1 -1
  15. package/dist/config/schema.d.ts +4 -2
  16. package/dist/core/daemon/assignment-message.d.ts +12 -0
  17. package/dist/core/daemon/assignment-message.js +36 -0
  18. package/dist/core/daemon/assignment-message.js.map +1 -0
  19. package/dist/core/daemon/bootstrap.d.ts +35 -0
  20. package/dist/core/daemon/bootstrap.js +52 -0
  21. package/dist/core/daemon/bootstrap.js.map +1 -0
  22. package/dist/core/daemon/crash-log.d.ts +16 -0
  23. package/dist/core/daemon/crash-log.js +24 -0
  24. package/dist/core/daemon/crash-log.js.map +1 -0
  25. package/dist/core/daemon/health-policy.d.ts +21 -0
  26. package/dist/core/daemon/health-policy.js +32 -0
  27. package/dist/core/daemon/health-policy.js.map +1 -0
  28. package/dist/core/daemon/sandbox-config.d.ts +9 -0
  29. package/dist/core/daemon/sandbox-config.js +17 -0
  30. package/dist/core/daemon/sandbox-config.js.map +1 -0
  31. package/dist/core/daemon/state.d.ts +33 -0
  32. package/dist/core/daemon/state.js +77 -0
  33. package/dist/core/daemon/state.js.map +1 -0
  34. package/dist/core/daemon/tmux-session.d.ts +17 -0
  35. package/dist/core/daemon/tmux-session.js +34 -0
  36. package/dist/core/daemon/tmux-session.js.map +1 -0
  37. package/dist/core/daemon/workspace.d.ts +10 -0
  38. package/dist/core/daemon/workspace.js +51 -0
  39. package/dist/core/daemon/workspace.js.map +1 -0
  40. package/dist/core/daemon.d.ts +0 -6
  41. package/dist/core/daemon.js +123 -244
  42. package/dist/core/daemon.js.map +1 -1
  43. package/dist/core/injector.js +6 -0
  44. package/dist/core/injector.js.map +1 -1
  45. package/dist/core/runner/build.d.ts +9 -0
  46. package/dist/core/runner/build.js +53 -0
  47. package/dist/core/runner/build.js.map +1 -0
  48. package/dist/core/runner/detect.d.ts +5 -0
  49. package/dist/core/runner/detect.js +14 -0
  50. package/dist/core/runner/detect.js.map +1 -0
  51. package/dist/core/runner/index.d.ts +5 -0
  52. package/dist/core/runner/index.js +5 -0
  53. package/dist/core/runner/index.js.map +1 -0
  54. package/dist/core/runner/model.d.ts +5 -0
  55. package/dist/core/runner/model.js +7 -0
  56. package/dist/core/runner/model.js.map +1 -0
  57. package/dist/core/runner/opencode-models.d.ts +15 -0
  58. package/dist/core/runner/opencode-models.js +70 -0
  59. package/dist/core/runner/opencode-models.js.map +1 -0
  60. package/dist/core/runner/types.d.ts +19 -0
  61. package/dist/core/runner/types.js +8 -0
  62. package/dist/core/runner/types.js.map +1 -0
  63. package/dist/core/runner.d.ts +5 -47
  64. package/dist/core/runner.js +5 -167
  65. package/dist/core/runner.js.map +1 -1
  66. package/dist/core/tmux-runtime.d.ts +13 -0
  67. package/dist/core/tmux-runtime.js +72 -0
  68. package/dist/core/tmux-runtime.js.map +1 -0
  69. package/dist/core/tmux.d.ts +7 -1
  70. package/dist/core/tmux.js +75 -45
  71. package/dist/core/tmux.js.map +1 -1
  72. package/dist/core/watchdog.d.ts +18 -1
  73. package/dist/core/watchdog.js +78 -29
  74. package/dist/core/watchdog.js.map +1 -1
  75. package/package.json +30 -11
  76. package/dist/cli/inbox.d.ts +0 -5
  77. package/dist/cli/inbox.js +0 -123
  78. package/dist/cli/inbox.js.map +0 -1
  79. package/dist/cli/issue.d.ts +0 -42
  80. package/dist/cli/issue.js +0 -297
  81. package/dist/cli/issue.js.map +0 -1
  82. package/dist/cli/ready.d.ts +0 -5
  83. package/dist/cli/ready.js +0 -131
  84. package/dist/cli/ready.js.map +0 -1
  85. package/dist/cli/sync.d.ts +0 -8
  86. package/dist/cli/sync.js +0 -154
  87. package/dist/cli/sync.js.map +0 -1
  88. package/dist/core/issue-cache.d.ts +0 -44
  89. package/dist/core/issue-cache.js +0 -75
  90. package/dist/core/issue-cache.js.map +0 -1
  91. package/src/__tests__/context.test.ts +0 -464
  92. package/src/__tests__/injector.test.ts +0 -29
  93. package/src/__tests__/jwt.test.ts +0 -112
  94. package/src/__tests__/loader.test.ts +0 -239
  95. package/src/__tests__/runner.test.ts +0 -104
  96. package/src/__tests__/sandbox.test.ts +0 -435
  97. package/src/__tests__/watchdog.test.ts +0 -368
  98. package/src/cli/attach.ts +0 -22
  99. package/src/cli/build.ts +0 -145
  100. package/src/cli/config.ts +0 -148
  101. package/src/cli/context.ts +0 -231
  102. package/src/cli/deploy.ts +0 -155
  103. package/src/cli/index.ts +0 -376
  104. package/src/cli/init.ts +0 -75
  105. package/src/cli/list.ts +0 -70
  106. package/src/cli/local.ts +0 -183
  107. package/src/cli/logs.ts +0 -64
  108. package/src/cli/migrate.ts +0 -212
  109. package/src/cli/nudge.ts +0 -81
  110. package/src/cli/restart.ts +0 -59
  111. package/src/cli/slack.ts +0 -70
  112. package/src/cli/start.ts +0 -118
  113. package/src/cli/status.ts +0 -91
  114. package/src/cli/stop.ts +0 -48
  115. package/src/cli/test.ts +0 -143
  116. package/src/cli/token.ts +0 -188
  117. package/src/cli/whoami.ts +0 -142
  118. package/src/config/loader.ts +0 -121
  119. package/src/config/schema.ts +0 -68
  120. package/src/context/handoff.ts +0 -122
  121. package/src/context/index.ts +0 -8
  122. package/src/context/schema.ts +0 -111
  123. package/src/context/storage.ts +0 -197
  124. package/src/core/daemon.ts +0 -1317
  125. package/src/core/heartbeat.ts +0 -129
  126. package/src/core/injector.ts +0 -292
  127. package/src/core/registry.ts +0 -159
  128. package/src/core/runner.ts +0 -225
  129. package/src/core/sandbox.ts +0 -547
  130. package/src/core/session-id.ts +0 -111
  131. package/src/core/tmux.ts +0 -405
  132. package/src/core/watchdog.ts +0 -238
  133. package/src/core/websocket.ts +0 -94
  134. package/src/index.ts +0 -10
  135. package/src/utils/jwt.ts +0 -87
  136. package/tsconfig.json +0 -8
  137. package/vitest.config.ts +0 -12
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 therajushahi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -115,6 +115,45 @@ The AI assistant sees the injected message as if you typed it, maintaining full
115
115
  - Node.js 18+
116
116
  - An AgentMesh HQ account with API key
117
117
 
118
+ ## Versioning
119
+
120
+ This package uses [Semantic Versioning](https://semver.org). Releases are fully automated — **do not publish manually**.
121
+
122
+ ### How to release
123
+
124
+ 1. Update `version` in `packages/agent/package.json` on the `dev` branch (via PR).
125
+ 2. Push a tag matching `agent-v<version>` from the commit where the version was bumped:
126
+
127
+ ```bash
128
+ # After your version-bump PR is merged to dev and pulled locally:
129
+ git tag agent-v0.2.1 <merge-commit-sha>
130
+ git push origin agent-v0.2.1
131
+ ```
132
+
133
+ 3. The `publish-agent.yml` GitHub Actions workflow triggers automatically and:
134
+ - Builds and tests the package
135
+ - Verifies `package.json` version matches the tag
136
+ - Publishes to npm as `@agentmeshhq/agent`
137
+ - Creates a GitHub Release
138
+ - Updates the Homebrew formula in `therajushahi/homebrew-agentmesh`
139
+
140
+ ### Rules
141
+
142
+ - **Never run `npm publish` manually.** It bypasses the version check and can publish a mismatched version (e.g. a `package.json` that says `0.1.2` getting published as `0.2.0`).
143
+ - **Never skip a version number.** The Homebrew formula uses the version string; if the npm registry already has a higher version, `brew upgrade` will not install the new formula.
144
+ - **The tag drives everything.** The tag name (`agent-v0.2.1`) is the source of truth — the workflow validates that `package.json` matches before publishing.
145
+ - **All work targets `dev`.** `main` is for releases only, managed by the Product Owner. PRs must target `dev`.
146
+
147
+ ### Version history incident
148
+
149
+ In Feb 2026, `0.2.0` was published manually with `npm publish` from a commit whose `package.json` said `0.1.2`. This caused:
150
+ - npm registry to have `0.2.0` as the latest version
151
+ - The Homebrew formula to point at `0.2.0`
152
+ - All subsequent CI-published versions (`0.1.3`, `0.1.4`) to be *lower* than `0.2.0`, so `brew upgrade` ignored them
153
+ - Workers already on `0.2.0` unable to receive fixes
154
+
155
+ The fix was to bump past `0.2.0` → `0.2.1` using the correct automated pipeline.
156
+
118
157
  ## License
119
158
 
120
159
  MIT
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Tests for orphaned process prevention (P0 fix for the daemon process leak).
3
+ *
4
+ * Covers:
5
+ * - killProcessTree: SIGTERM → SIGKILL escalation
6
+ * - getTmuxPanePid: reads PID from tmux pane
7
+ * - getProcessTree: recursive child enumeration
8
+ * - captureAgentChildPids: captures all PIDs for a given agent
9
+ * - destroySession: kills child PIDs before killing tmux session
10
+ */
11
+ export {};
@@ -0,0 +1,286 @@
1
+ /**
2
+ * Tests for orphaned process prevention (P0 fix for the daemon process leak).
3
+ *
4
+ * Covers:
5
+ * - killProcessTree: SIGTERM → SIGKILL escalation
6
+ * - getTmuxPanePid: reads PID from tmux pane
7
+ * - getProcessTree: recursive child enumeration
8
+ * - captureAgentChildPids: captures all PIDs for a given agent
9
+ * - destroySession: kills child PIDs before killing tmux session
10
+ */
11
+ import { execSync } from "node:child_process";
12
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
13
+ import { captureAgentChildPids, getProcessTree, getTmuxPanePid } from "../core/daemon/state.js";
14
+ import { destroySession, killProcessTree } from "../core/tmux.js";
15
+ // ─── Mocks ────────────────────────────────────────────────────────────────────
16
+ vi.mock("node:child_process", () => ({
17
+ execSync: vi.fn(),
18
+ execFileSync: vi.fn(),
19
+ spawnSync: vi.fn(),
20
+ }));
21
+ vi.mock("../core/tmux-runtime.js", () => ({
22
+ buildInteractiveCommand: vi.fn().mockReturnValue("opencode"),
23
+ detectRunnerCommandKind: vi.fn().mockReturnValue("opencode"),
24
+ prepareOpenCodeRuntime: vi.fn().mockReturnValue("/tmp/test-runtime"),
25
+ }));
26
+ // Helper to mock process.kill without type gymnastics
27
+ function mockProcessKill(impl) {
28
+ return vi.spyOn(process, "kill").mockImplementation(impl);
29
+ }
30
+ const mockExecSync = vi.mocked(execSync);
31
+ // ─── killProcessTree ──────────────────────────────────────────────────────────
32
+ describe("killProcessTree", () => {
33
+ let killSpy;
34
+ beforeEach(() => {
35
+ killSpy = mockProcessKill(() => true);
36
+ vi.useFakeTimers();
37
+ });
38
+ afterEach(() => {
39
+ killSpy.mockRestore();
40
+ vi.useRealTimers();
41
+ });
42
+ it("returns true immediately if pids list is empty", () => {
43
+ const result = killProcessTree([]);
44
+ expect(result).toBe(true);
45
+ expect(killSpy).not.toHaveBeenCalled();
46
+ });
47
+ it("sends SIGTERM to all provided PIDs", () => {
48
+ // Track whether SIGTERM has been sent to each PID
49
+ const terminatedPids = new Set();
50
+ killSpy.mockImplementation((_pid, sig) => {
51
+ if (sig === "SIGTERM") {
52
+ terminatedPids.add(_pid);
53
+ return true;
54
+ }
55
+ if (sig === 0) {
56
+ // Alive until SIGTERM has been sent
57
+ if (terminatedPids.has(_pid))
58
+ throw new Error("no such process");
59
+ return true;
60
+ }
61
+ return true;
62
+ });
63
+ mockExecSync.mockReturnValue("");
64
+ killProcessTree([1234, 5678]);
65
+ expect(killSpy).toHaveBeenCalledWith(1234, "SIGTERM");
66
+ expect(killSpy).toHaveBeenCalledWith(5678, "SIGTERM");
67
+ });
68
+ it("escalates to SIGKILL after timeout when processes remain alive", () => {
69
+ // Process stays alive through SIGTERM, only dies after SIGKILL check
70
+ let killedWithSigkill = false;
71
+ killSpy.mockImplementation((_pid, sig) => {
72
+ if (sig === "SIGKILL") {
73
+ killedWithSigkill = true;
74
+ return true;
75
+ }
76
+ if (sig === 0) {
77
+ // Stay alive until SIGKILL has been sent
78
+ if (killedWithSigkill)
79
+ throw new Error("no such process");
80
+ return true;
81
+ }
82
+ return true;
83
+ });
84
+ mockExecSync.mockImplementation((cmd) => {
85
+ if (cmd === "sleep 0.1")
86
+ vi.advanceTimersByTime(100);
87
+ return "";
88
+ });
89
+ killProcessTree([1234], 200);
90
+ expect(killSpy).toHaveBeenCalledWith(1234, "SIGTERM");
91
+ expect(killSpy).toHaveBeenCalledWith(1234, "SIGKILL");
92
+ });
93
+ it("does not SIGKILL when processes exit gracefully before timeout", () => {
94
+ let sigkillCalled = false;
95
+ let terminatedPid = null;
96
+ killSpy.mockImplementation((_pid, sig) => {
97
+ if (sig === "SIGKILL") {
98
+ sigkillCalled = true;
99
+ return true;
100
+ }
101
+ if (sig === "SIGTERM") {
102
+ terminatedPid = _pid;
103
+ return true;
104
+ }
105
+ if (sig === 0) {
106
+ // Dead after SIGTERM
107
+ if (terminatedPid === _pid)
108
+ throw new Error("no such process");
109
+ return true;
110
+ }
111
+ return true;
112
+ });
113
+ mockExecSync.mockReturnValue("");
114
+ killProcessTree([1234]);
115
+ expect(sigkillCalled).toBe(false);
116
+ });
117
+ });
118
+ // ─── getTmuxPanePid ────────────────────────────────────────────────────────────
119
+ describe("getTmuxPanePid", () => {
120
+ beforeEach(() => {
121
+ vi.clearAllMocks();
122
+ });
123
+ it("returns parsed PID from tmux list-panes output", () => {
124
+ mockExecSync.mockReturnValue("42000\n");
125
+ const pid = getTmuxPanePid("agentmesh-test-agent");
126
+ expect(pid).toBe(42000);
127
+ });
128
+ it("returns null when tmux command fails", () => {
129
+ mockExecSync.mockImplementation(() => {
130
+ throw new Error("no server running");
131
+ });
132
+ const pid = getTmuxPanePid("agentmesh-nonexistent");
133
+ expect(pid).toBeNull();
134
+ });
135
+ it("returns null when output is not a valid number", () => {
136
+ mockExecSync.mockReturnValue("not-a-pid\n");
137
+ const pid = getTmuxPanePid("agentmesh-test-agent");
138
+ expect(pid).toBeNull();
139
+ });
140
+ });
141
+ // ─── getProcessTree ────────────────────────────────────────────────────────────
142
+ describe("getProcessTree", () => {
143
+ beforeEach(() => {
144
+ vi.clearAllMocks();
145
+ });
146
+ it("returns direct children of a process", () => {
147
+ mockExecSync.mockImplementation((cmd) => {
148
+ const c = cmd;
149
+ if (c.includes("pgrep -P 1000"))
150
+ return "1001\n1002\n";
151
+ return "";
152
+ });
153
+ const tree = getProcessTree(1000);
154
+ expect(tree).toContain(1001);
155
+ expect(tree).toContain(1002);
156
+ });
157
+ it("recursively includes grandchildren", () => {
158
+ mockExecSync.mockImplementation((cmd) => {
159
+ const c = cmd;
160
+ if (c.includes("pgrep -P 1000"))
161
+ return "1001\n";
162
+ if (c.includes("pgrep -P 1001"))
163
+ return "1002\n";
164
+ return "";
165
+ });
166
+ const tree = getProcessTree(1000);
167
+ expect(tree).toContain(1001);
168
+ expect(tree).toContain(1002);
169
+ });
170
+ it("returns empty array when process has no children", () => {
171
+ mockExecSync.mockReturnValue("");
172
+ const tree = getProcessTree(9999);
173
+ expect(tree).toEqual([]);
174
+ });
175
+ it("returns empty array when pgrep fails", () => {
176
+ mockExecSync.mockImplementation(() => {
177
+ throw new Error("pgrep: no matching processes");
178
+ });
179
+ const tree = getProcessTree(9999);
180
+ expect(tree).toEqual([]);
181
+ });
182
+ });
183
+ // ─── captureAgentChildPids ─────────────────────────────────────────────────────
184
+ describe("captureAgentChildPids", () => {
185
+ beforeEach(() => {
186
+ vi.clearAllMocks();
187
+ });
188
+ it("returns empty array when tmux session has no pane PID", () => {
189
+ mockExecSync.mockImplementation(() => {
190
+ throw new Error("no server");
191
+ });
192
+ const pids = captureAgentChildPids("test-agent");
193
+ expect(pids).toEqual([]);
194
+ });
195
+ it("returns pane PID and all descendants", () => {
196
+ mockExecSync.mockImplementation((cmd) => {
197
+ const c = cmd;
198
+ if (c.includes("list-panes"))
199
+ return "5000\n";
200
+ if (c.includes("pgrep -P 5000"))
201
+ return "5001\n5002\n";
202
+ return "";
203
+ });
204
+ const pids = captureAgentChildPids("test-agent");
205
+ expect(pids).toContain(5000);
206
+ expect(pids).toContain(5001);
207
+ expect(pids).toContain(5002);
208
+ });
209
+ it("deduplicates PIDs", () => {
210
+ mockExecSync.mockImplementation((cmd) => {
211
+ const c = cmd;
212
+ if (c.includes("list-panes"))
213
+ return "5000\n";
214
+ if (c.includes("pgrep -P 5000"))
215
+ return "5001\n5001\n"; // duplicate
216
+ return "";
217
+ });
218
+ const pids = captureAgentChildPids("test-agent");
219
+ const uniquePids = [...new Set(pids)];
220
+ expect(pids.length).toBe(uniquePids.length);
221
+ });
222
+ });
223
+ // ─── destroySession with childPids ────────────────────────────────────────────
224
+ describe("destroySession with childPids", () => {
225
+ let killSpy;
226
+ beforeEach(() => {
227
+ killSpy = mockProcessKill(() => true);
228
+ vi.clearAllMocks();
229
+ });
230
+ afterEach(() => {
231
+ killSpy.mockRestore();
232
+ });
233
+ it("kills child PIDs before killing the tmux session", () => {
234
+ const callOrder = [];
235
+ const terminatedPids = new Set();
236
+ killSpy.mockImplementation((_pid, sig) => {
237
+ if (sig === "SIGTERM") {
238
+ callOrder.push(`kill-${_pid}`);
239
+ terminatedPids.add(_pid);
240
+ return true;
241
+ }
242
+ if (sig === 0) {
243
+ if (terminatedPids.has(_pid))
244
+ throw new Error("dead");
245
+ return true;
246
+ }
247
+ return true;
248
+ });
249
+ mockExecSync.mockImplementation((cmd) => {
250
+ const c = cmd;
251
+ if (c.includes("has-session"))
252
+ return "";
253
+ if (c.includes("kill-session"))
254
+ callOrder.push("tmux-kill");
255
+ return "";
256
+ });
257
+ destroySession("test-agent", [1234, 5678]);
258
+ const childKillIndex = callOrder.findIndex((c) => c.startsWith("kill-"));
259
+ const tmuxKillIndex = callOrder.indexOf("tmux-kill");
260
+ expect(childKillIndex).toBeGreaterThanOrEqual(0);
261
+ expect(tmuxKillIndex).toBeGreaterThan(childKillIndex);
262
+ });
263
+ it("still destroys tmux session when no childPids provided", () => {
264
+ mockExecSync.mockImplementation((cmd) => {
265
+ const c = cmd;
266
+ if (c.includes("has-session"))
267
+ return "";
268
+ return "";
269
+ });
270
+ destroySession("test-agent");
271
+ expect(mockExecSync).toHaveBeenCalledWith(expect.stringContaining("kill-session"));
272
+ expect(killSpy).not.toHaveBeenCalled();
273
+ });
274
+ it("still destroys tmux session when childPids is empty array", () => {
275
+ mockExecSync.mockImplementation((cmd) => {
276
+ const c = cmd;
277
+ if (c.includes("has-session"))
278
+ return "";
279
+ return "";
280
+ });
281
+ destroySession("test-agent", []);
282
+ expect(mockExecSync).toHaveBeenCalledWith(expect.stringContaining("kill-session"));
283
+ expect(killSpy).not.toHaveBeenCalled();
284
+ });
285
+ });
286
+ //# sourceMappingURL=orphan-process.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"orphan-process.test.js","sourceRoot":"","sources":["../../src/__tests__/orphan-process.test.ts"],"names":[],"mappings":"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"}
@@ -12,6 +12,11 @@ describe("Runner Module", () => {
12
12
  expect(detectRunner("claude --some-flag")).toBe("claude");
13
13
  expect(detectRunner("CLAUDE")).toBe("claude");
14
14
  });
15
+ it("should detect codex runner", () => {
16
+ expect(detectRunner("codex")).toBe("codex");
17
+ expect(detectRunner("codex --some-flag")).toBe("codex");
18
+ expect(detectRunner("CODEX")).toBe("codex");
19
+ });
15
20
  it("should detect custom runner for unknown commands", () => {
16
21
  expect(detectRunner("aider")).toBe("custom");
17
22
  expect(detectRunner("cursor")).toBe("custom");
@@ -75,11 +80,22 @@ describe("Runner Module", () => {
75
80
  expect(config.model).toBe("gpt-4");
76
81
  expect(Object.keys(config.env)).toHaveLength(0);
77
82
  });
83
+ it("should build config for codex with CODEX_MODEL env", () => {
84
+ const config = buildRunnerConfig({
85
+ cliModel: "openai/gpt-5-codex",
86
+ defaultModel: "anthropic/claude-sonnet-4-5",
87
+ command: "codex",
88
+ });
89
+ expect(config.type).toBe("codex");
90
+ expect(config.model).toBe("openai/gpt-5-codex");
91
+ expect(config.env.CODEX_MODEL).toBe("openai/gpt-5-codex");
92
+ });
78
93
  });
79
94
  describe("getRunnerDisplayName", () => {
80
95
  it("should return correct display names", () => {
81
96
  expect(getRunnerDisplayName("opencode")).toBe("OpenCode");
82
97
  expect(getRunnerDisplayName("claude")).toBe("Claude CLI");
98
+ expect(getRunnerDisplayName("codex")).toBe("Codex CLI");
83
99
  expect(getRunnerDisplayName("custom")).toBe("Custom");
84
100
  });
85
101
  });
@@ -1 +1 @@
1
- {"version":3,"file":"runner.test.js","sourceRoot":"","sources":["../../src/__tests__/runner.test.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"runner.test.js","sourceRoot":"","sources":["../../src/__tests__/runner.test.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,10 @@
1
1
  import { execSync, spawnSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
2
5
  import { beforeEach, describe, expect, it, vi } from "vitest";
3
6
  import * as tmux from "../core/tmux.js";
4
- import { checkAgentProgress, cleanupOrphanContainers, detectPermissionPrompt, findOrphanContainers, getLastActivityTime, isProcessRunning, sendNudge, } from "../core/watchdog.js";
7
+ import { checkAgentProgress, cleanupOrphanContainers, clearWaitingForHuman, detectPermissionPrompt, findOrphanContainers, getLastActivityTime, isProcessRunning, isWaitingForHuman, sendNudge, setWaitingForHuman, } from "../core/watchdog.js";
5
8
  // Mock child_process
6
9
  vi.mock("node:child_process", () => ({
7
10
  execSync: vi.fn(),
@@ -11,6 +14,19 @@ vi.mock("node:child_process", () => ({
11
14
  vi.mock("../core/tmux.js", () => ({
12
15
  captureSessionOutput: vi.fn(),
13
16
  }));
17
+ // Mock node:fs for waiting signal file tests
18
+ vi.mock("node:fs", () => ({
19
+ default: {
20
+ mkdirSync: vi.fn(),
21
+ writeFileSync: vi.fn(),
22
+ unlinkSync: vi.fn(),
23
+ readFileSync: vi.fn(),
24
+ },
25
+ mkdirSync: vi.fn(),
26
+ writeFileSync: vi.fn(),
27
+ unlinkSync: vi.fn(),
28
+ readFileSync: vi.fn(),
29
+ }));
14
30
  describe("Watchdog Module", () => {
15
31
  beforeEach(() => {
16
32
  vi.resetAllMocks();
@@ -48,20 +64,36 @@ describe("Watchdog Module", () => {
48
64
  const result = detectPermissionPrompt("test-agent");
49
65
  expect(result).toBeNull();
50
66
  });
67
+ it("should not treat standalone 'Reject' text as a permission prompt", () => {
68
+ vi.mocked(tmux.captureSessionOutput).mockReturnValue("Unit test failed\nReject this PR until CI is green");
69
+ const result = detectPermissionPrompt("test-agent");
70
+ expect(result).toBeNull();
71
+ });
51
72
  });
52
73
  describe("getLastActivityTime", () => {
53
74
  it("should parse timestamp from local log file", () => {
54
- vi.mocked(spawnSync).mockReturnValue({
55
- status: 0,
56
- stdout: "INFO 2026-02-26T00:14:42 +0ms service=opencode message=test",
57
- stderr: "",
58
- pid: 123,
59
- signal: null,
60
- output: [],
61
- });
62
- const result = getLastActivityTime("test-agent");
63
- expect(result).toBeInstanceOf(Date);
64
- expect(result?.toISOString()).toContain("2026-02-26");
75
+ const originalHome = process.env.HOME;
76
+ try {
77
+ process.env.HOME = "/tmp/agent-home";
78
+ vi.mocked(spawnSync).mockReturnValue({
79
+ status: 0,
80
+ stdout: "INFO 2026-02-26T00:14:42 +0ms service=opencode message=test",
81
+ stderr: "",
82
+ pid: 123,
83
+ signal: null,
84
+ output: [],
85
+ });
86
+ const result = getLastActivityTime("test-agent");
87
+ expect(result).toBeInstanceOf(Date);
88
+ expect(result?.toISOString()).toContain("2026-02-26");
89
+ expect(spawnSync).toHaveBeenCalledWith("sh", [
90
+ "-c",
91
+ "ls -t /tmp/agent-home/.agentmesh/opencode-data/test-agent/opencode/log/*.log 2>/dev/null | head -1 | xargs tail -1 2>/dev/null",
92
+ ], { encoding: "utf-8", timeout: 5000 });
93
+ }
94
+ finally {
95
+ process.env.HOME = originalHome;
96
+ }
65
97
  });
66
98
  it("should parse timestamp from container log file", () => {
67
99
  vi.mocked(spawnSync).mockReturnValue({
@@ -286,5 +318,99 @@ describe("Watchdog Module", () => {
286
318
  expect(execSync).toHaveBeenCalledWith(expect.stringContaining('\\"quotes\\"'), expect.any(Object));
287
319
  });
288
320
  });
321
+ describe("waiting-for-human signal", () => {
322
+ const agentName = "test-agent";
323
+ const waitingFilePath = path.join(os.homedir(), ".agentmesh", "agents", `${agentName}.waiting`);
324
+ beforeEach(() => {
325
+ vi.resetAllMocks();
326
+ });
327
+ describe("setWaitingForHuman", () => {
328
+ it("should create the agents directory and write the signal file", () => {
329
+ setWaitingForHuman(agentName, "Waiting for approval");
330
+ expect(fs.mkdirSync).toHaveBeenCalledWith(path.join(os.homedir(), ".agentmesh", "agents"), {
331
+ recursive: true,
332
+ });
333
+ expect(fs.writeFileSync).toHaveBeenCalledWith(waitingFilePath, expect.stringContaining("Waiting for approval"), { mode: 0o600 });
334
+ });
335
+ it("should use default reason when none provided", () => {
336
+ setWaitingForHuman(agentName);
337
+ expect(fs.writeFileSync).toHaveBeenCalledWith(waitingFilePath, expect.stringContaining("Waiting for human input"), { mode: 0o600 });
338
+ });
339
+ it("should write valid JSON with reason and since fields", () => {
340
+ setWaitingForHuman(agentName, "Test reason");
341
+ const writeCall = vi.mocked(fs.writeFileSync).mock.calls[0];
342
+ const written = JSON.parse(writeCall[1]);
343
+ expect(written).toHaveProperty("reason", "Test reason");
344
+ expect(written).toHaveProperty("since");
345
+ expect(() => new Date(written.since)).not.toThrow();
346
+ });
347
+ });
348
+ describe("clearWaitingForHuman", () => {
349
+ it("should delete the signal file", () => {
350
+ clearWaitingForHuman(agentName);
351
+ expect(fs.unlinkSync).toHaveBeenCalledWith(waitingFilePath);
352
+ });
353
+ it("should not throw when the signal file does not exist", () => {
354
+ vi.mocked(fs.unlinkSync).mockImplementation(() => {
355
+ throw new Error("ENOENT: no such file");
356
+ });
357
+ expect(() => clearWaitingForHuman(agentName)).not.toThrow();
358
+ });
359
+ });
360
+ describe("isWaitingForHuman", () => {
361
+ it("should return waiting=true with reason and since when signal file exists", () => {
362
+ const payload = { reason: "Awaiting handoff reply", since: "2026-02-28T10:00:00.000Z" };
363
+ vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(payload));
364
+ const result = isWaitingForHuman(agentName);
365
+ expect(result.waiting).toBe(true);
366
+ expect(result.reason).toBe("Awaiting handoff reply");
367
+ expect(result.since).toBe("2026-02-28T10:00:00.000Z");
368
+ });
369
+ it("should return waiting=false when signal file does not exist", () => {
370
+ vi.mocked(fs.readFileSync).mockImplementation(() => {
371
+ throw new Error("ENOENT: no such file");
372
+ });
373
+ const result = isWaitingForHuman(agentName);
374
+ expect(result.waiting).toBe(false);
375
+ });
376
+ it("should return waiting=false on malformed JSON", () => {
377
+ vi.mocked(fs.readFileSync).mockReturnValue("not-json");
378
+ const result = isWaitingForHuman(agentName);
379
+ expect(result.waiting).toBe(false);
380
+ });
381
+ });
382
+ describe("checkAgentProgress with waiting_for_human", () => {
383
+ it("should return waiting_for_human when signal file exists, before any other check", () => {
384
+ const payload = {
385
+ reason: "Waiting for Terraform decision",
386
+ since: "2026-02-28T10:00:00.000Z",
387
+ };
388
+ vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(payload));
389
+ // Even with a permission prompt visible in tmux, waiting_for_human takes priority
390
+ vi.mocked(tmux.captureSessionOutput).mockReturnValue("Permission required\nAllow once");
391
+ const result = checkAgentProgress(agentName);
392
+ expect(result.status).toBe("waiting_for_human");
393
+ expect(result.details).toBe("Waiting for Terraform decision");
394
+ });
395
+ it("should fall through to normal checks when no signal file", () => {
396
+ vi.mocked(fs.readFileSync).mockImplementation(() => {
397
+ throw new Error("ENOENT");
398
+ });
399
+ vi.mocked(tmux.captureSessionOutput).mockReturnValue("Normal output");
400
+ // Mock recent activity so it returns active
401
+ const recentTime = new Date(Date.now() - 30 * 1000);
402
+ vi.mocked(spawnSync).mockReturnValue({
403
+ status: 0,
404
+ stdout: `INFO ${recentTime.toISOString().slice(0, 19)} +0ms service=opencode`,
405
+ stderr: "",
406
+ pid: 123,
407
+ signal: null,
408
+ output: [],
409
+ });
410
+ const result = checkAgentProgress(agentName);
411
+ expect(result.status).toBe("active");
412
+ });
413
+ });
414
+ });
289
415
  });
290
416
  //# sourceMappingURL=watchdog.test.js.map