@simplr-ai/connect 0.7.5 → 0.7.7

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/dist/index.js +489 -128
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  spawnSync
8
8
  } from "child_process";
9
9
  import {
10
- createHash,
10
+ createHash as createHash2,
11
11
  createPublicKey,
12
12
  randomBytes,
13
13
  randomUUID,
@@ -18,18 +18,52 @@ import {
18
18
  access,
19
19
  chmod,
20
20
  copyFile,
21
- mkdir,
21
+ mkdir as mkdir2,
22
22
  open,
23
- readFile,
24
- readdir,
23
+ readFile as readFile2,
24
+ readdir as readdir2,
25
25
  rename,
26
+ symlink,
26
27
  unlink,
27
28
  writeFile
28
29
  } from "fs/promises";
29
30
  import { arch, homedir, hostname, platform } from "os";
30
- import { basename, join as join2 } from "path";
31
+ import { basename, join as join3 } from "path";
31
32
  import { createServer } from "http";
32
33
 
34
+ // src/execution-tools.ts
35
+ import { createHash } from "crypto";
36
+ import { cp, mkdir, readFile, readdir, rm } from "fs/promises";
37
+ import { join, relative } from "path";
38
+ async function directoryFingerprint(directory) {
39
+ const hash = createHash("sha256");
40
+ const visit = async (current) => {
41
+ const entries = await readdir(current, { withFileTypes: true });
42
+ for (const entry of entries.sort(
43
+ (left, right) => left.name.localeCompare(right.name)
44
+ )) {
45
+ const path = join(current, entry.name);
46
+ const name = relative(directory, path);
47
+ hash.update(`${entry.isDirectory() ? "directory" : "file"}:${name}\0`);
48
+ if (entry.isDirectory()) await visit(path);
49
+ else if (entry.isFile()) hash.update(await readFile(path));
50
+ }
51
+ };
52
+ await visit(directory);
53
+ return hash.digest("hex");
54
+ }
55
+ async function syncDirectoryContents(source, destination) {
56
+ const sourceFingerprint = await directoryFingerprint(source);
57
+ const destinationFingerprint = await directoryFingerprint(destination).catch(
58
+ () => void 0
59
+ );
60
+ if (sourceFingerprint === destinationFingerprint) return false;
61
+ await rm(destination, { recursive: true, force: true });
62
+ await mkdir(destination, { recursive: true, mode: 448 });
63
+ await cp(source, destination, { recursive: true, force: true });
64
+ return true;
65
+ }
66
+
33
67
  // src/connections.ts
34
68
  function connectionKey(connection) {
35
69
  return `${connection.api_url}|${connection.organization_id}`;
@@ -86,7 +120,7 @@ function assertTenantEnvelope(connection, command) {
86
120
  }
87
121
 
88
122
  // src/hermes-config.ts
89
- import { join } from "path";
123
+ import { join as join2 } from "path";
90
124
  function organizationIdsNeedingMcpConfiguration(connections, configuredVersion, requiredVersion) {
91
125
  if (configuredVersion === requiredVersion) return [];
92
126
  return connections.filter((connection) => connection.setup_status === "ready").map((connection) => connection.organization_id);
@@ -102,10 +136,10 @@ function mcpServerNamesFromConfiguration(contents) {
102
136
  }
103
137
  function mcpConfigurationPaths(homeDirectory, workingDirectory, organizationId) {
104
138
  return [
105
- join(homeDirectory, ".codex", "config.toml"),
106
- join(homeDirectory, ".claude.json"),
107
- join(workingDirectory, ".mcp.json"),
108
- join(
139
+ join2(homeDirectory, ".codex", "config.toml"),
140
+ join2(homeDirectory, ".claude.json"),
141
+ join2(workingDirectory, ".mcp.json"),
142
+ join2(
109
143
  homeDirectory,
110
144
  ".hermes",
111
145
  "profiles",
@@ -115,6 +149,102 @@ function mcpConfigurationPaths(homeDirectory, workingDirectory, organizationId)
115
149
  ];
116
150
  }
117
151
 
152
+ // src/work-order-support.ts
153
+ function workOrderClassification(payload) {
154
+ return {
155
+ ...typeof payload.work_order_kind === "string" ? { work_order_kind: payload.work_order_kind } : {},
156
+ ...typeof payload.source_type === "string" ? { source_type: payload.source_type } : {},
157
+ ...typeof payload.execution_mode === "string" ? { execution_mode: payload.execution_mode } : {}
158
+ };
159
+ }
160
+ function awaitsFeaturePlanApproval(classification) {
161
+ return classification.work_order_kind === "feature" && classification.source_type === "feedback" && classification.execution_mode === "plan";
162
+ }
163
+ function isIncompleteCodeWorkResult(classification, output) {
164
+ if (!["fix", "fix_and_ship"].includes(String(classification.execution_mode)))
165
+ return false;
166
+ if (typeof output !== "string") return false;
167
+ const hasExecutorBlocker = /(?:executor|codex|claude code)[\s\S]{0,160}(?:blocked|unavailable|not installed|oauth)|(?:blocked|unavailable|not installed|oauth)[\s\S]{0,160}(?:executor|codex|claude code)/i.test(
168
+ output
169
+ ) || /stopped before code changes due to required executor blocker/i.test(output);
170
+ return /files changed:\s*(?:none|0)\b/i.test(output) && /pull request:\s*(?:none|not created)\b/i.test(output) && hasExecutorBlocker;
171
+ }
172
+ function isRecoverableIncompleteCompletion(entry) {
173
+ return entry.type === "run_hermes" && entry.status === "completed" && entry.source_type === "feedback" && isIncompleteCodeWorkResult(entry, entry.result);
174
+ }
175
+ function matchesCommandAcknowledgement(commandId, status, acknowledgement) {
176
+ return acknowledgement.command_id === commandId && acknowledgement.status === status;
177
+ }
178
+ function isHermesRunCancellation(message) {
179
+ return message === "Hermes run cancelled" || message === "Hermes run exceeded the 30 minute monitoring window and was stopped";
180
+ }
181
+ function workOrderPayload(classification) {
182
+ return {
183
+ ...classification.work_order_kind ? { work_order_kind: classification.work_order_kind } : {},
184
+ ...classification.source_type ? { source_type: classification.source_type } : {},
185
+ ...classification.execution_mode ? { execution_mode: classification.execution_mode } : {}
186
+ };
187
+ }
188
+ async function postWithApiKey(url, body, apiKey) {
189
+ const response = await fetch(url, {
190
+ method: "POST",
191
+ headers: {
192
+ "Content-Type": "application/json",
193
+ "X-API-Key": apiKey
194
+ },
195
+ body: JSON.stringify(body),
196
+ signal: AbortSignal.timeout(1e4)
197
+ });
198
+ const payload = await response.json().catch(() => ({}));
199
+ if (!response.ok || !payload.content)
200
+ throw new Error(
201
+ payload.message || `Simplr request failed (${response.status})`
202
+ );
203
+ return payload.content;
204
+ }
205
+ function reservedSimplrConnectLane(organizationNamespace2) {
206
+ return `simplr-connect-reserved:${organizationNamespace2}`;
207
+ }
208
+ function isRunningTaskOwnedBy(task, expectedAssignee) {
209
+ return (task.task?.status || task.status) === "running" && (task.task?.assignee || task.assignee) === expectedAssignee;
210
+ }
211
+ var terminalCommentMarker = (action) => `[simplr-connect-terminal:${action}]`;
212
+ function taskStatus(task) {
213
+ return task.task?.status || task.status;
214
+ }
215
+ function taskBlockKind(task) {
216
+ const blockedEvent = [...task.events || []].reverse().find((event) => event.kind === "blocked");
217
+ return task.task?.block_kind || task.block_kind || blockedEvent?.payload?.kind;
218
+ }
219
+ function hasTerminalComment(task, action) {
220
+ const marker = terminalCommentMarker(action);
221
+ return (task.comments || []).some((comment) => comment.body?.includes(marker));
222
+ }
223
+ function reconcileTerminalBlock(action, detail, operations) {
224
+ const kind = action === "needs_input" ? "needs_input" : "transient";
225
+ const matches = (task) => taskStatus(task) === "blocked" && taskBlockKind(task) === kind && hasTerminalComment(task, action);
226
+ let current = operations.read();
227
+ if (!current)
228
+ throw new Error(`Hermes work-board ${action} preflight failed`);
229
+ if (matches(current)) return true;
230
+ if (!hasTerminalComment(current, action)) {
231
+ const body = `${detail}
232
+
233
+ ${terminalCommentMarker(action)}`;
234
+ const commented = operations.comment(body);
235
+ current = operations.read();
236
+ if (!current || !commented && !hasTerminalComment(current, action))
237
+ throw new Error(`Hermes work-board ${action} comment reconciliation failed`);
238
+ }
239
+ if (matches(current)) return true;
240
+ const blocked = operations.block(kind);
241
+ current = operations.read();
242
+ if (current && matches(current)) return true;
243
+ if (!blocked)
244
+ throw new Error(`Hermes work-board ${action} reconciliation failed`);
245
+ throw new Error(`Hermes work-board ${action} verification failed`);
246
+ }
247
+
118
248
  // src/index.ts
119
249
  var AmbiguousHermesRunError = class extends Error {
120
250
  };
@@ -122,7 +252,7 @@ var KanbanReconciliationError = class extends Error {
122
252
  };
123
253
  var DeferredUpdateError = class extends Error {
124
254
  };
125
- var APP_VERSION = "0.7.5";
255
+ var APP_VERSION = "0.7.7";
126
256
  var STANDALONE_EXECUTABLE = process.env.SIMPLR_CONNECT_STANDALONE === "true";
127
257
  var MANIFEST_PUBLIC_KEY = process.env.SIMPLR_CONNECT_MANIFEST_PUBLIC_KEY || "";
128
258
  var HERMES_INSTALL_COMMIT = "542e146b055f0d43767ac43cdb9e78054b240263";
@@ -138,45 +268,45 @@ var hermesTraceMutation = Promise.resolve();
138
268
  var serviceExecutableSnapshot;
139
269
  function stateDirectory() {
140
270
  if (platform() === "darwin")
141
- return join2(homedir(), "Library", "Application Support", "Simplr Connect");
271
+ return join3(homedir(), "Library", "Application Support", "Simplr Connect");
142
272
  if (platform() === "win32")
143
- return join2(
144
- process.env.APPDATA || join2(homedir(), "AppData", "Roaming"),
273
+ return join3(
274
+ process.env.APPDATA || join3(homedir(), "AppData", "Roaming"),
145
275
  "Simplr Connect"
146
276
  );
147
- return join2(
148
- process.env.XDG_CONFIG_HOME || join2(homedir(), ".config"),
277
+ return join3(
278
+ process.env.XDG_CONFIG_HOME || join3(homedir(), ".config"),
149
279
  "simplr-connect"
150
280
  );
151
281
  }
152
282
  function statePath() {
153
- return join2(stateDirectory(), "state.json");
283
+ return join3(stateDirectory(), "state.json");
154
284
  }
155
285
  function encryptedCredentialPath(workstationId) {
156
- return join2(
286
+ return join3(
157
287
  stateDirectory(),
158
288
  workstationId ? `credential-${workstationId}.bin` : "credential.bin"
159
289
  );
160
290
  }
161
291
  function encryptedAgentCredentialPath(workstationId) {
162
- return join2(stateDirectory(), `agent-credential-${workstationId}.bin`);
292
+ return join3(stateDirectory(), `agent-credential-${workstationId}.bin`);
163
293
  }
164
294
  function encryptedHermesCredentialPath() {
165
- return join2(stateDirectory(), "hermes-credential.bin");
295
+ return join3(stateDirectory(), "hermes-credential.bin");
166
296
  }
167
297
  function commandJournalPath() {
168
- return join2(stateDirectory(), "commands.json");
298
+ return join3(stateDirectory(), "commands.json");
169
299
  }
170
300
  function hermesTracePath() {
171
- return join2(stateDirectory(), "hermes-traces.json");
301
+ return join3(stateDirectory(), "hermes-traces.json");
172
302
  }
173
303
  function serviceLogPath() {
174
- return join2(stateDirectory(), "connect.log");
304
+ return join3(stateDirectory(), "connect.log");
175
305
  }
176
306
  function serviceExecutablePath() {
177
307
  if (!STANDALONE_EXECUTABLE)
178
- return join2(stateDirectory(), "simplr-connect.js");
179
- return join2(
308
+ return join3(stateDirectory(), "simplr-connect.js");
309
+ return join3(
180
310
  stateDirectory(),
181
311
  platform() === "win32" ? "simplr-connect.exe" : "simplr-connect"
182
312
  );
@@ -188,28 +318,28 @@ function updateRollbackPath() {
188
318
  return `${serviceExecutablePath()}.rollback`;
189
319
  }
190
320
  function supervisorLockPath() {
191
- return join2(stateDirectory(), "supervisor.lock");
321
+ return join3(stateDirectory(), "supervisor.lock");
192
322
  }
193
323
  function serviceLockPath() {
194
- return join2(stateDirectory(), "service.lock");
324
+ return join3(stateDirectory(), "service.lock");
195
325
  }
196
326
  function serviceDefinitionPath() {
197
327
  if (platform() === "darwin")
198
- return join2(
328
+ return join3(
199
329
  homedir(),
200
330
  "Library",
201
331
  "LaunchAgents",
202
332
  "ai.simplr.connect.plist"
203
333
  );
204
334
  if (platform() === "linux")
205
- return join2(
335
+ return join3(
206
336
  homedir(),
207
337
  ".config",
208
338
  "systemd",
209
339
  "user",
210
340
  "simplr-connect.service"
211
341
  );
212
- return join2(stateDirectory(), "simplr-connect-service.cmd");
342
+ return join3(stateDirectory(), "simplr-connect-service.cmd");
213
343
  }
214
344
  function apiUrl(override) {
215
345
  const value = override || process.env.SIMPLR_API_URL;
@@ -319,11 +449,11 @@ function hermesExecutableCandidates() {
319
449
  const localAppData = process.env.LOCALAPPDATA;
320
450
  if (localAppData)
321
451
  candidates.push(
322
- join2(localAppData, "hermes", "hermes-agent", "bin", "hermes.exe")
452
+ join3(localAppData, "hermes", "hermes-agent", "bin", "hermes.exe")
323
453
  );
324
454
  } else {
325
455
  candidates.push(
326
- join2(homedir(), ".local", "bin", "hermes"),
456
+ join3(homedir(), ".local", "bin", "hermes"),
327
457
  "/usr/local/bin/hermes"
328
458
  );
329
459
  if (platform() === "darwin") candidates.push("/opt/homebrew/bin/hermes");
@@ -644,15 +774,15 @@ async function existingDirectories(paths) {
644
774
  }
645
775
  async function detectSkills() {
646
776
  const paths = await existingDirectories([
647
- join2(homedir(), ".agents", "skills"),
648
- join2(homedir(), ".claude", "skills"),
649
- join2(process.cwd(), ".agents", "skills"),
650
- join2(process.cwd(), ".claude", "skills"),
651
- join2(homedir(), ".hermes", "skills")
777
+ join3(homedir(), ".agents", "skills"),
778
+ join3(homedir(), ".claude", "skills"),
779
+ join3(process.cwd(), ".agents", "skills"),
780
+ join3(process.cwd(), ".claude", "skills"),
781
+ join3(homedir(), ".hermes", "skills")
652
782
  ]);
653
783
  const skills = /* @__PURE__ */ new Map();
654
784
  for (const path of paths) {
655
- const entries = await readdir(path, { withFileTypes: true }).catch(
785
+ const entries = await readdir2(path, { withFileTypes: true }).catch(
656
786
  () => []
657
787
  );
658
788
  for (const entry of entries) {
@@ -673,7 +803,7 @@ async function detectMcpServers(state) {
673
803
  mcpConfigurationPaths(homedir(), process.cwd(), state.organization_id)
674
804
  );
675
805
  const contents = await Promise.all(
676
- configPaths.map((path) => readFile(path, "utf8").catch(() => ""))
806
+ configPaths.map((path) => readFile2(path, "utf8").catch(() => ""))
677
807
  );
678
808
  const names = new Set(mcpServerNamesFromConfiguration(contents));
679
809
  for (const content of contents) {
@@ -767,7 +897,7 @@ async function get(url, token) {
767
897
  return payload.content;
768
898
  }
769
899
  async function saveState(state) {
770
- await mkdir(stateDirectory(), { recursive: true, mode: 448 });
900
+ await mkdir2(stateDirectory(), { recursive: true, mode: 448 });
771
901
  const { device_token: legacyToken, ...safeState } = state;
772
902
  void legacyToken;
773
903
  await writeFile(statePath(), `${JSON.stringify(safeState, null, 2)}
@@ -778,7 +908,7 @@ async function saveState(state) {
778
908
  if (platform() !== "win32") await chmod(statePath(), 384);
779
909
  }
780
910
  async function loadState() {
781
- const stored = JSON.parse(await readFile(statePath(), "utf8"));
911
+ const stored = JSON.parse(await readFile2(statePath(), "utf8"));
782
912
  if (stored.device_token) {
783
913
  await storeCredential(stored.workstation_id, stored.device_token);
784
914
  delete stored.device_token;
@@ -789,13 +919,13 @@ async function loadState() {
789
919
  }
790
920
  async function loadCommandJournal() {
791
921
  try {
792
- return JSON.parse(await readFile(commandJournalPath(), "utf8"));
922
+ return JSON.parse(await readFile2(commandJournalPath(), "utf8"));
793
923
  } catch {
794
924
  return {};
795
925
  }
796
926
  }
797
927
  async function saveCommandJournal(journal) {
798
- await mkdir(stateDirectory(), { recursive: true, mode: 448 });
928
+ await mkdir2(stateDirectory(), { recursive: true, mode: 448 });
799
929
  const temporaryPath = `${commandJournalPath()}.${process.pid}.tmp`;
800
930
  await writeFile(temporaryPath, `${JSON.stringify(journal, null, 2)}
801
931
  `, {
@@ -825,13 +955,13 @@ async function removeCommandJournalEntry(commandId) {
825
955
  }
826
956
  async function loadHermesTraces() {
827
957
  try {
828
- return JSON.parse(await readFile(hermesTracePath(), "utf8"));
958
+ return JSON.parse(await readFile2(hermesTracePath(), "utf8"));
829
959
  } catch {
830
960
  return {};
831
961
  }
832
962
  }
833
963
  async function saveHermesTraces(traces) {
834
- await mkdir(stateDirectory(), { recursive: true, mode: 448 });
964
+ await mkdir2(stateDirectory(), { recursive: true, mode: 448 });
835
965
  const retained = Object.fromEntries(Object.entries(traces).sort(([, left], [, right]) => Date.parse(right.last_event_at) - Date.parse(left.last_event_at)).slice(0, 25));
836
966
  const temporaryPath = `${hermesTracePath()}.${process.pid}.tmp`;
837
967
  await writeFile(temporaryPath, `${JSON.stringify(retained, null, 2)}
@@ -916,7 +1046,7 @@ async function storeMacKeychainCredential(account, service, token) {
916
1046
  throw new Error("Could not protect the credential in macOS Keychain");
917
1047
  }
918
1048
  async function storeCredential(workstationId, token) {
919
- await mkdir(stateDirectory(), { recursive: true, mode: 448 });
1049
+ await mkdir2(stateDirectory(), { recursive: true, mode: 448 });
920
1050
  if (platform() === "darwin") {
921
1051
  await storeMacKeychainCredential(workstationId, "Simplr Connect", token);
922
1052
  return;
@@ -994,10 +1124,10 @@ async function loadCredential(state) {
994
1124
  );
995
1125
  if (result.ok && result.output) return result.output;
996
1126
  } else {
997
- const encrypted = await readFile(
1127
+ const encrypted = await readFile2(
998
1128
  encryptedCredentialPath(state.workstation_id),
999
1129
  "utf8"
1000
- ).catch(() => readFile(encryptedCredentialPath(), "utf8"));
1130
+ ).catch(() => readFile2(encryptedCredentialPath(), "utf8"));
1001
1131
  const script = "$e=[Convert]::FromBase64String([Console]::In.ReadToEnd());$b=[Security.Cryptography.ProtectedData]::Unprotect($e,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser);[Text.Encoding]::UTF8.GetString($b)";
1002
1132
  const result = spawnSync(
1003
1133
  "powershell.exe",
@@ -1085,7 +1215,7 @@ async function loadAgentCredential(state) {
1085
1215
  );
1086
1216
  if (result.ok && result.output) return result.output;
1087
1217
  } else {
1088
- const encrypted = await readFile(
1218
+ const encrypted = await readFile2(
1089
1219
  encryptedAgentCredentialPath(state.workstation_id),
1090
1220
  "utf8"
1091
1221
  );
@@ -1217,7 +1347,7 @@ async function loadHermesCredential(state) {
1217
1347
  }
1218
1348
  }
1219
1349
  } else {
1220
- const encrypted = await readFile(encryptedHermesCredentialPath(), "utf8");
1350
+ const encrypted = await readFile2(encryptedHermesCredentialPath(), "utf8");
1221
1351
  const script = "$e=[Convert]::FromBase64String([Console]::In.ReadToEnd());$b=[Security.Cryptography.ProtectedData]::Unprotect($e,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser);[Text.Encoding]::UTF8.GetString($b)";
1222
1352
  const result = spawnSync(
1223
1353
  "powershell.exe",
@@ -1306,7 +1436,7 @@ async function streamHermesRunEvents(state, command, runId) {
1306
1436
  hermesRunApprovals.set(runId, {
1307
1437
  approval_command: approvalCommand,
1308
1438
  approval_tool: tool,
1309
- approval_fingerprint: createHash("sha256").update(JSON.stringify({ run_id: runId, tool, command: approvalCommand })).digest("hex"),
1439
+ approval_fingerprint: createHash2("sha256").update(JSON.stringify({ run_id: runId, tool, command: approvalCommand })).digest("hex"),
1310
1440
  approval_policy_decision: "approval_required"
1311
1441
  });
1312
1442
  }
@@ -1345,9 +1475,9 @@ async function waitForProcess(child) {
1345
1475
  });
1346
1476
  }
1347
1477
  async function installHermes() {
1348
- await mkdir(stateDirectory(), { recursive: true, mode: 448 });
1478
+ await mkdir2(stateDirectory(), { recursive: true, mode: 448 });
1349
1479
  const windows = platform() === "win32";
1350
- const scriptPath = join2(
1480
+ const scriptPath = join3(
1351
1481
  stateDirectory(),
1352
1482
  windows ? "hermes-install.ps1" : "hermes-install.sh"
1353
1483
  );
@@ -1404,14 +1534,14 @@ async function installHermes() {
1404
1534
  if (userPath.ok && userPath.output)
1405
1535
  process.env.PATH = `${userPath.output};${process.env.PATH || ""}`;
1406
1536
  } else {
1407
- process.env.PATH = `${join2(homedir(), ".local", "bin")}:${process.env.PATH || ""}`;
1537
+ process.env.PATH = `${join3(homedir(), ".local", "bin")}:${process.env.PATH || ""}`;
1408
1538
  }
1409
1539
  }
1410
1540
  async function configureHermesEnvironment(apiKey) {
1411
- const directory = join2(homedir(), ".hermes");
1412
- const path = join2(directory, ".env");
1413
- await mkdir(directory, { recursive: true, mode: 448 });
1414
- const existing = await readFile(path, "utf8").catch(() => "");
1541
+ const directory = join3(homedir(), ".hermes");
1542
+ const path = join3(directory, ".env");
1543
+ await mkdir2(directory, { recursive: true, mode: 448 });
1544
+ const existing = await readFile2(path, "utf8").catch(() => "");
1415
1545
  const settings = {
1416
1546
  API_SERVER_ENABLED: "true",
1417
1547
  API_SERVER_HOST: "127.0.0.1",
@@ -1441,15 +1571,15 @@ function hermesProfileArgs(state) {
1441
1571
  return ["--profile", hermesProfile(state.organization_id)];
1442
1572
  }
1443
1573
  async function configureSimplrMcp(state, executable) {
1444
- const profileDirectory = join2(
1574
+ const profileDirectory = join3(
1445
1575
  homedir(),
1446
1576
  ".hermes",
1447
1577
  "profiles",
1448
1578
  hermesProfile(state.organization_id)
1449
1579
  );
1450
- await mkdir(profileDirectory, { recursive: true, mode: 448 });
1451
- const environmentPath = join2(profileDirectory, ".env");
1452
- const existing = await readFile(environmentPath, "utf8").catch(() => "");
1580
+ await mkdir2(profileDirectory, { recursive: true, mode: 448 });
1581
+ const environmentPath = join3(profileDirectory, ".env");
1582
+ const existing = await readFile2(environmentPath, "utf8").catch(() => "");
1453
1583
  const settings = {
1454
1584
  SIMPLR_API_URL: state.api_url,
1455
1585
  SIMPLR_API_KEY: await ensureAgentCredential(state),
@@ -1471,7 +1601,7 @@ async function configureSimplrMcp(state, executable) {
1471
1601
  });
1472
1602
  if (platform() !== "win32") await chmod(environmentPath, 384);
1473
1603
  const profileArgs = hermesProfileArgs(state);
1474
- const wrapperPath = join2(profileDirectory, "simplr-mcp.mjs");
1604
+ const wrapperPath = join3(profileDirectory, "simplr-mcp.mjs");
1475
1605
  const wrapper = `#!/usr/bin/env node
1476
1606
  import { readFileSync } from "node:fs";
1477
1607
  import { spawn } from "node:child_process";
@@ -1530,8 +1660,8 @@ async function ensureSimplrMcpConfiguration(state) {
1530
1660
  state.mcp_configuration_version = SIMPLR_MCP_CONFIGURATION_VERSION;
1531
1661
  await saveState(state);
1532
1662
  }
1533
- async function installHermesSkill(state) {
1534
- const directory = join2(
1663
+ async function installHermesSkills(state) {
1664
+ const directory = join3(
1535
1665
  homedir(),
1536
1666
  ".hermes",
1537
1667
  "profiles",
@@ -1539,9 +1669,9 @@ async function installHermesSkill(state) {
1539
1669
  "skills",
1540
1670
  "simplr-remote-operations"
1541
1671
  );
1542
- await mkdir(directory, { recursive: true, mode: 448 });
1672
+ await mkdir2(directory, { recursive: true, mode: 448 });
1543
1673
  await writeFile(
1544
- join2(directory, "SKILL.md"),
1674
+ join3(directory, "SKILL.md"),
1545
1675
  `---
1546
1676
  name: simplr-remote-operations
1547
1677
  description: Execute a verified Simplr work-order envelope delivered by Simplr Connect. Do not use for direct local Hermes requests.
@@ -1575,6 +1705,79 @@ Never expose credentials, environment variables, tokens, keys, cookies, secret f
1575
1705
  `,
1576
1706
  { encoding: "utf8", mode: 384 }
1577
1707
  );
1708
+ const bundledCodexSources = [
1709
+ join3(homedir(), ".hermes", "skills", "autonomous-ai-agents", "codex"),
1710
+ join3(
1711
+ homedir(),
1712
+ ".hermes",
1713
+ "hermes-agent",
1714
+ "skills",
1715
+ "autonomous-ai-agents",
1716
+ "codex"
1717
+ )
1718
+ ];
1719
+ const codexSource = (await Promise.all(
1720
+ bundledCodexSources.map(async (candidate) => {
1721
+ try {
1722
+ await access(join3(candidate, "SKILL.md"), constants.R_OK);
1723
+ return candidate;
1724
+ } catch {
1725
+ return void 0;
1726
+ }
1727
+ })
1728
+ )).find(Boolean);
1729
+ if (!codexSource)
1730
+ throw new Error("Hermes bundled Codex skill is unavailable");
1731
+ const codexDestination = join3(
1732
+ homedir(),
1733
+ ".hermes",
1734
+ "profiles",
1735
+ hermesProfile(state.organization_id),
1736
+ "skills",
1737
+ "autonomous-ai-agents",
1738
+ "codex"
1739
+ );
1740
+ return syncDirectoryContents(codexSource, codexDestination);
1741
+ }
1742
+ async function ensureCodexExecutor() {
1743
+ if (platform() === "win32") return false;
1744
+ const localBin = join3(homedir(), ".local", "bin");
1745
+ const pathEntries = (process.env.PATH || "").split(":");
1746
+ const pathChanged = !pathEntries.includes(localBin);
1747
+ if (pathChanged)
1748
+ process.env.PATH = `${localBin}:${process.env.PATH || ""}`;
1749
+ if (commandVersion("codex")) return pathChanged;
1750
+ if (platform() !== "darwin") return pathChanged;
1751
+ const destination = join3(localBin, "codex");
1752
+ const candidates = [
1753
+ "/Applications/ChatGPT.app/Contents/Resources/codex",
1754
+ "/Applications/Codex.app/Contents/Resources/codex"
1755
+ ];
1756
+ for (const candidate of candidates) {
1757
+ try {
1758
+ await access(candidate, constants.X_OK);
1759
+ await mkdir2(localBin, { recursive: true, mode: 448 });
1760
+ await symlink(candidate, destination).catch((error) => {
1761
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "EEXIST")
1762
+ throw error;
1763
+ });
1764
+ if (commandVersion("codex")) return true;
1765
+ } catch {
1766
+ }
1767
+ }
1768
+ return pathChanged;
1769
+ }
1770
+ async function ensureHermesExecutionTools(state) {
1771
+ let changed = await ensureCodexExecutor();
1772
+ for (const connection of state.connections.filter(
1773
+ (candidate) => candidate.setup_status === "ready"
1774
+ )) {
1775
+ const installed = await installHermesSkills(
1776
+ stateForConnection(state, connection)
1777
+ );
1778
+ changed = changed || installed;
1779
+ }
1780
+ return changed;
1578
1781
  }
1579
1782
  function isSimplrWorkOrder(command) {
1580
1783
  return ["incident", "bug", "feature"].includes(
@@ -1583,11 +1786,47 @@ function isSimplrWorkOrder(command) {
1583
1786
  String(command.payload.execution_mode)
1584
1787
  );
1585
1788
  }
1789
+ async function claimSimplrFeedbackInCloud(state, command) {
1790
+ if (String(command.payload.source_type) !== "feedback") return;
1791
+ const sourceId = String(command.payload.source_id || "");
1792
+ if (!sourceId) throw new Error("Simplr feedback source is missing");
1793
+ const agentToken = await ensureAgentCredential(state);
1794
+ await postWithApiKey(
1795
+ `${state.api_url}/v1/feedback/agent/items/${encodeURIComponent(sourceId)}/claim`,
1796
+ {},
1797
+ agentToken
1798
+ );
1799
+ }
1800
+ function readSimplrKanbanTask(executable, state, taskId) {
1801
+ for (let attempt = 0; attempt < 3; attempt += 1) {
1802
+ const shown = commandResult(
1803
+ executable,
1804
+ [
1805
+ ...hermesProfileArgs(state),
1806
+ "kanban",
1807
+ "--board",
1808
+ `simplr-${organizationNamespace(state.organization_id)}`,
1809
+ "show",
1810
+ taskId,
1811
+ "--json"
1812
+ ],
1813
+ 1e4
1814
+ );
1815
+ if (shown.ok) return JSON.parse(shown.output);
1816
+ }
1817
+ return null;
1818
+ }
1819
+ function simplrKanbanTaskStatus(task) {
1820
+ return task.task?.status || task.status;
1821
+ }
1586
1822
  function ensureSimplrKanbanTask(state, command, prompt) {
1587
1823
  if (!isSimplrWorkOrder(command)) return void 0;
1588
1824
  const executable = detectHermesExecutable();
1589
1825
  if (!executable) throw new Error("Hermes executable is unavailable");
1590
1826
  const boardSlug = `simplr-${organizationNamespace(state.organization_id)}`;
1827
+ const reservedAssignee = reservedSimplrConnectLane(
1828
+ organizationNamespace(state.organization_id)
1829
+ );
1591
1830
  const boards = commandResult(
1592
1831
  executable,
1593
1832
  [...hermesProfileArgs(state), "kanban", "boards", "list", "--json"],
@@ -1664,10 +1903,12 @@ ${prompt}`;
1664
1903
  `simplr-connect:${state.organization_id}:${command.id}`,
1665
1904
  "--created-by",
1666
1905
  "Simplr Connect",
1906
+ "--assignee",
1907
+ reservedAssignee,
1667
1908
  "--skill",
1668
1909
  "simplr-remote-operations",
1669
1910
  "--initial-status",
1670
- "blocked",
1911
+ "running",
1671
1912
  "--json"
1672
1913
  ],
1673
1914
  15e3
@@ -1676,12 +1917,33 @@ ${prompt}`;
1676
1917
  throw new Error("Hermes could not persist the Simplr work order");
1677
1918
  const task = JSON.parse(createdTask.output);
1678
1919
  if (!task.id) throw new Error("Hermes did not return a work-order task id");
1679
- updateSimplrKanbanTask(
1680
- state,
1681
- task.id,
1682
- "comment",
1683
- "Simplr Connect started the governed Hermes run. Use Simplr for approvals and deployment decisions; this card preserves the durable handoff."
1684
- );
1920
+ let current = readSimplrKanbanTask(executable, state, task.id);
1921
+ if (!current) throw new Error("Hermes could not verify the Simplr work-order task");
1922
+ if (simplrKanbanTaskStatus(current) === "ready") {
1923
+ const claimed = commandResult(
1924
+ executable,
1925
+ [
1926
+ ...hermesProfileArgs(state),
1927
+ "kanban",
1928
+ "--board",
1929
+ boardSlug,
1930
+ "claim",
1931
+ task.id,
1932
+ "--ttl",
1933
+ "3600"
1934
+ ],
1935
+ 1e4
1936
+ );
1937
+ current = readSimplrKanbanTask(executable, state, task.id);
1938
+ if (!claimed.ok && simplrKanbanTaskStatus(current || {}) !== "running") {
1939
+ throw new Error("Hermes could not claim the Simplr work-order task");
1940
+ }
1941
+ }
1942
+ if (!current || !isRunningTaskOwnedBy(current, reservedAssignee)) {
1943
+ throw new Error(
1944
+ "Hermes Simplr work-order task is not running in its reserved ownership lane"
1945
+ );
1946
+ }
1685
1947
  return task.id;
1686
1948
  }
1687
1949
  function updateSimplrKanbanTask(state, taskId, action, detail) {
@@ -1710,7 +1972,47 @@ function updateSimplrKanbanTask(state, taskId, action, detail) {
1710
1972
  1e4
1711
1973
  ).ok;
1712
1974
  }
1713
- const args = action === "complete" ? [
1975
+ if (action === "failed" || action === "needs_input") {
1976
+ try {
1977
+ return reconcileTerminalBlock(action, detail.slice(0, 2e3), {
1978
+ read: () => readSimplrKanbanTask(executable, state, taskId),
1979
+ comment: (body) => commandResult(
1980
+ executable,
1981
+ [
1982
+ ...hermesProfileArgs(state),
1983
+ "kanban",
1984
+ "--board",
1985
+ `simplr-${organizationNamespace(state.organization_id)}`,
1986
+ "comment",
1987
+ "--author",
1988
+ "Simplr Connect",
1989
+ taskId,
1990
+ body
1991
+ ],
1992
+ 1e4
1993
+ ).ok,
1994
+ block: (kind) => commandResult(
1995
+ executable,
1996
+ [
1997
+ ...hermesProfileArgs(state),
1998
+ "kanban",
1999
+ "--board",
2000
+ `simplr-${organizationNamespace(state.organization_id)}`,
2001
+ "block",
2002
+ "--kind",
2003
+ kind,
2004
+ taskId
2005
+ ],
2006
+ 1e4
2007
+ ).ok
2008
+ });
2009
+ } catch (error) {
2010
+ throw new KanbanReconciliationError(
2011
+ error instanceof Error ? error.message : `Hermes work-board ${action} reconciliation failed`
2012
+ );
2013
+ }
2014
+ }
2015
+ const args = [
1714
2016
  ...hermesProfileArgs(state),
1715
2017
  "kanban",
1716
2018
  "--board",
@@ -1724,44 +2026,27 @@ function updateSimplrKanbanTask(state, taskId, action, detail) {
1724
2026
  source: "simplr-connect",
1725
2027
  rules_version: "simplr-work-order-v1"
1726
2028
  })
1727
- ] : [
1728
- ...hermesProfileArgs(state),
1729
- "kanban",
1730
- "--board",
1731
- `simplr-${organizationNamespace(state.organization_id)}`,
1732
- "block",
1733
- "--kind",
1734
- "transient",
1735
- taskId,
1736
- detail.slice(0, 2e3)
1737
2029
  ];
1738
- for (let attempt = 0; attempt < 3; attempt += 1) {
1739
- if (commandResult(executable, args, 1e4).ok) return true;
1740
- const shown = commandResult(
1741
- executable,
1742
- [
1743
- ...hermesProfileArgs(state),
1744
- "kanban",
1745
- "--board",
1746
- `simplr-${organizationNamespace(state.organization_id)}`,
1747
- "show",
1748
- taskId,
1749
- "--json"
1750
- ],
1751
- 1e4
2030
+ const matchesAction = (current) => {
2031
+ const currentStatus = simplrKanbanTaskStatus(current);
2032
+ return action === "complete" && currentStatus === "done";
2033
+ };
2034
+ const initial = readSimplrKanbanTask(executable, state, taskId);
2035
+ if (!initial) {
2036
+ throw new KanbanReconciliationError(
2037
+ `Hermes work-board ${action} preflight failed`
1752
2038
  );
1753
- if (shown.ok) {
1754
- const current = JSON.parse(shown.output);
1755
- const currentStatus = current.task?.status || current.status;
1756
- if (action === "complete" && currentStatus === "done" || action === "failed" && currentStatus === "blocked")
1757
- return true;
1758
- }
1759
2039
  }
2040
+ if (matchesAction(initial)) return true;
2041
+ if (commandResult(executable, args, 1e4).ok) return true;
2042
+ const reconciled = readSimplrKanbanTask(executable, state, taskId);
2043
+ if (reconciled && matchesAction(reconciled)) return true;
1760
2044
  throw new KanbanReconciliationError(
1761
2045
  `Hermes work-board ${action} reconciliation failed`
1762
2046
  );
1763
2047
  }
1764
2048
  async function setupHermes(state, interactiveOAuth) {
2049
+ await ensureCodexExecutor();
1765
2050
  let executable = detectHermesExecutable();
1766
2051
  if (!executable) {
1767
2052
  await installHermes();
@@ -1812,7 +2097,7 @@ async function setupHermes(state, interactiveOAuth) {
1812
2097
  );
1813
2098
  await configureHermesEnvironment(apiKey);
1814
2099
  await configureSimplrMcp(state, executable);
1815
- await installHermesSkill(state);
2100
+ await installHermesSkills(state);
1816
2101
  const multiplex = commandResult(
1817
2102
  executable,
1818
2103
  [
@@ -2073,11 +2358,15 @@ async function acknowledgeCommand(state, commandId, status, result) {
2073
2358
  let lastError;
2074
2359
  for (let attempt = 1; attempt <= 5; attempt += 1) {
2075
2360
  try {
2076
- await post(
2361
+ const acknowledged = await post(
2077
2362
  `${state.api_url}/v1/ai-workstations/commands/${commandId}/acknowledge`,
2078
2363
  { status, result: result.slice(0, 5e3) },
2079
2364
  token
2080
2365
  );
2366
+ if (!matchesCommandAcknowledgement(commandId, status, acknowledged))
2367
+ throw new Error(
2368
+ `Command acknowledgement remained ${acknowledged.status}`
2369
+ );
2081
2370
  await removeCommandJournalEntry(commandId);
2082
2371
  return;
2083
2372
  } catch (error) {
@@ -2156,6 +2445,23 @@ async function monitorHermesRun(state, command, runId, startedAt) {
2156
2445
  }
2157
2446
  if (status.status === "completed") {
2158
2447
  const output = status.output || "Hermes completed the task";
2448
+ if (isIncompleteCodeWorkResult(
2449
+ workOrderClassification(command.payload),
2450
+ output
2451
+ )) {
2452
+ await updateCommandJournal({
2453
+ command_id: command.id,
2454
+ type: command.type,
2455
+ label: command.reason,
2456
+ ...workOrderClassification(command.payload),
2457
+ run_id: runId,
2458
+ status: "failed",
2459
+ result: output,
2460
+ started_at: startedAt,
2461
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
2462
+ });
2463
+ throw new Error(output);
2464
+ }
2159
2465
  await updateHermesTrace(command, state, runId, {
2160
2466
  status: "completed",
2161
2467
  current_action: "Hermes run completed",
@@ -2167,13 +2473,24 @@ async function monitorHermesRun(state, command, runId, startedAt) {
2167
2473
  command_id: command.id,
2168
2474
  type: command.type,
2169
2475
  label: command.reason,
2476
+ ...workOrderClassification(command.payload),
2170
2477
  run_id: runId,
2171
2478
  status: "completed",
2172
2479
  result: output,
2173
2480
  started_at: startedAt,
2174
2481
  updated_at: (/* @__PURE__ */ new Date()).toISOString()
2175
2482
  });
2176
- updateSimplrKanbanTask(state, taskId, "complete", output);
2483
+ const awaitingPlanApproval = awaitsFeaturePlanApproval(
2484
+ workOrderClassification(command.payload)
2485
+ );
2486
+ updateSimplrKanbanTask(
2487
+ state,
2488
+ taskId,
2489
+ awaitingPlanApproval ? "needs_input" : "complete",
2490
+ awaitingPlanApproval ? `Feature plan ready for comment and approval. Review the plan below, add guidance, then explicitly approve a new implementation work order.
2491
+
2492
+ ${output}` : output
2493
+ );
2177
2494
  return output;
2178
2495
  }
2179
2496
  if (status.status === "cancelled")
@@ -2186,6 +2503,7 @@ async function monitorHermesRun(state, command, runId, startedAt) {
2186
2503
  command_id: command.id,
2187
2504
  type: command.type,
2188
2505
  label: command.reason,
2506
+ ...workOrderClassification(command.payload),
2189
2507
  run_id: runId,
2190
2508
  status: run.status,
2191
2509
  started_at: startedAt,
@@ -2197,6 +2515,7 @@ async function monitorHermesRun(state, command, runId, startedAt) {
2197
2515
  command_id: command.id,
2198
2516
  type: command.type,
2199
2517
  label: command.reason,
2518
+ ...workOrderClassification(command.payload),
2200
2519
  run_id: runId,
2201
2520
  status: run.status,
2202
2521
  approval_command: approval.approval_command,
@@ -2236,7 +2555,7 @@ async function monitorHermesRun(state, command, runId, startedAt) {
2236
2555
  );
2237
2556
  } catch (error) {
2238
2557
  const message = error instanceof Error ? error.message : "Hermes run failed";
2239
- const cancelled = /cancelled|stopped/i.test(message);
2558
+ const cancelled = isHermesRunCancellation(message);
2240
2559
  await updateHermesTrace(command, state, runId, {
2241
2560
  status: cancelled ? "cancelled" : "failed",
2242
2561
  current_action: cancelled ? "Hermes run stopped" : "Hermes run failed",
@@ -2254,11 +2573,16 @@ async function runHermesCommand(state, command) {
2254
2573
  const journal = await loadCommandJournal();
2255
2574
  const existing = journal[command.id];
2256
2575
  if (existing?.status === "completed") {
2576
+ if (isIncompleteCodeWorkResult(existing, existing.result))
2577
+ throw new Error(existing.result || "Hermes code work did not complete");
2578
+ const awaitingPlanApproval = awaitsFeaturePlanApproval(existing);
2257
2579
  updateSimplrKanbanTask(
2258
2580
  state,
2259
2581
  existing.kanban_task_id,
2260
- "complete",
2261
- existing.result || "Hermes completed the task"
2582
+ awaitingPlanApproval ? "needs_input" : "complete",
2583
+ awaitingPlanApproval ? `Feature plan ready for comment and approval. Review the plan below, add guidance, then explicitly approve a new implementation work order.
2584
+
2585
+ ${existing.result || "Hermes completed the plan"}` : existing.result || "Hermes completed the task"
2262
2586
  );
2263
2587
  return "Hermes completed this task before Simplr reconnected";
2264
2588
  }
@@ -2273,6 +2597,21 @@ async function runHermesCommand(state, command) {
2273
2597
  if (!runId) {
2274
2598
  const prompt = typeof command.payload.prompt === "string" ? command.payload.prompt : "";
2275
2599
  if (!prompt) throw new Error("Hermes task prompt is missing");
2600
+ if (existing?.status !== "preparing") {
2601
+ await updateCommandJournal({
2602
+ command_id: command.id,
2603
+ organization_id: state.organization_id,
2604
+ organization_name: state.organization_name,
2605
+ workstation_id: state.workstation_id,
2606
+ type: command.type,
2607
+ label: command.reason,
2608
+ ...workOrderClassification(command.payload),
2609
+ status: "preparing",
2610
+ started_at: startedAt,
2611
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
2612
+ });
2613
+ }
2614
+ await claimSimplrFeedbackInCloud(state, command);
2276
2615
  const kanbanTaskId = ensureSimplrKanbanTask(state, command, prompt);
2277
2616
  await updateCommandJournal({
2278
2617
  command_id: command.id,
@@ -2281,6 +2620,7 @@ async function runHermesCommand(state, command) {
2281
2620
  workstation_id: state.workstation_id,
2282
2621
  type: command.type,
2283
2622
  label: command.reason,
2623
+ ...workOrderClassification(command.payload),
2284
2624
  status: "creating",
2285
2625
  kanban_task_id: kanbanTaskId,
2286
2626
  started_at: startedAt,
@@ -2303,6 +2643,7 @@ async function runHermesCommand(state, command) {
2303
2643
  command_id: command.id,
2304
2644
  type: command.type,
2305
2645
  label: command.reason,
2646
+ ...workOrderClassification(command.payload),
2306
2647
  run_id: runId,
2307
2648
  status: "running",
2308
2649
  started_at: startedAt,
@@ -2446,7 +2787,7 @@ async function stopManagedProcesses() {
2446
2787
  return `${processes.length} Simplr-managed AI process${processes.length === 1 ? "" : "es"} terminated`;
2447
2788
  }
2448
2789
  async function acquireProcessLock(lockPath, activeMessage) {
2449
- await mkdir(stateDirectory(), { recursive: true, mode: 448 });
2790
+ await mkdir2(stateDirectory(), { recursive: true, mode: 448 });
2450
2791
  const create = async () => {
2451
2792
  const handle = await open(lockPath, "wx", 384);
2452
2793
  await handle.writeFile(`${process.pid}
@@ -2459,7 +2800,7 @@ async function acquireProcessLock(lockPath, activeMessage) {
2459
2800
  if (!(error instanceof Error) || !("code" in error) || error.code !== "EEXIST")
2460
2801
  throw error;
2461
2802
  const existingPid = Number.parseInt(
2462
- (await readFile(lockPath, "utf8").catch(() => "0")).trim(),
2803
+ (await readFile2(lockPath, "utf8").catch(() => "0")).trim(),
2463
2804
  10
2464
2805
  );
2465
2806
  let active = false;
@@ -2612,6 +2953,7 @@ async function applyCommand(state, command, inventoryAlreadySynced) {
2612
2953
  command_id: command.id,
2613
2954
  type: command.type,
2614
2955
  label: command.reason,
2956
+ ...workOrderClassification(command.payload),
2615
2957
  run_id: existing?.run_id,
2616
2958
  status: "failed",
2617
2959
  started_at: existing?.started_at || (/* @__PURE__ */ new Date()).toISOString(),
@@ -2667,22 +3009,38 @@ async function resumeHermesRuns(state) {
2667
3009
  workstation_id: connection.workstation_id,
2668
3010
  type: "run_hermes",
2669
3011
  reason: entry.label,
2670
- payload: {}
3012
+ payload: workOrderPayload(entry)
2671
3013
  },
2672
3014
  false
2673
3015
  );
2674
3016
  }
2675
3017
  }
3018
+ async function reconcileIncompleteCompletedRuns(state) {
3019
+ const journal = await loadCommandJournal();
3020
+ for (const entry of Object.values(journal)) {
3021
+ if (!isRecoverableIncompleteCompletion(entry)) continue;
3022
+ const connection = entry.organization_id ? state.connections.find(
3023
+ (candidate) => candidate.organization_id === entry.organization_id && candidate.workstation_id === entry.workstation_id
3024
+ ) : state.connections.length === 1 ? state.connections[0] : void 0;
3025
+ if (!connection) continue;
3026
+ await acknowledgeCommand(
3027
+ stateForConnection(state, connection),
3028
+ entry.command_id,
3029
+ "failed",
3030
+ entry.result || "Hermes code work did not complete"
3031
+ );
3032
+ }
3033
+ }
2676
3034
  async function serviceInstallationPresent() {
2677
3035
  try {
2678
3036
  await access(serviceExecutablePath(), constants.R_OK);
2679
3037
  if (serviceExecutableSnapshot) {
2680
- const deployed = await readFile(serviceExecutablePath());
2681
- const expectedHash = createHash("sha256").update(serviceExecutableSnapshot).digest("hex");
2682
- const deployedHash = createHash("sha256").update(deployed).digest("hex");
3038
+ const deployed = await readFile2(serviceExecutablePath());
3039
+ const expectedHash = createHash2("sha256").update(serviceExecutableSnapshot).digest("hex");
3040
+ const deployedHash = createHash2("sha256").update(deployed).digest("hex");
2683
3041
  if (expectedHash !== deployedHash) return false;
2684
3042
  }
2685
- const definition = await readFile(serviceDefinitionPath(), "utf8");
3043
+ const definition = await readFile2(serviceDefinitionPath(), "utf8");
2686
3044
  if (!definition.includes(serviceExecutablePath()) || !definition.includes("watch"))
2687
3045
  return false;
2688
3046
  if (platform() !== "win32") return true;
@@ -2724,10 +3082,13 @@ async function watch() {
2724
3082
  const state = await loadState();
2725
3083
  const executableSource = STANDALONE_EXECUTABLE ? process.execPath : process.argv[1];
2726
3084
  if (executableSource)
2727
- serviceExecutableSnapshot = await readFile(executableSource).catch(
3085
+ serviceExecutableSnapshot = await readFile2(executableSource).catch(
2728
3086
  () => void 0
2729
3087
  );
3088
+ const executionToolsChanged = await ensureHermesExecutionTools(state);
2730
3089
  await ensureSimplrMcpConfiguration(state);
3090
+ if (executionToolsChanged) await restartHermesGateway(state);
3091
+ await reconcileIncompleteCompletedRuns(state);
2731
3092
  await resumeHermesRuns(state);
2732
3093
  let nextInventoryAt = 0;
2733
3094
  let nextServiceAuditAt = 0;
@@ -2940,10 +3301,10 @@ function systemdValue(value) {
2940
3301
  }
2941
3302
  async function deployServiceExecutable() {
2942
3303
  const source = STANDALONE_EXECUTABLE ? process.execPath : process.argv[1];
2943
- const executable = serviceExecutableSnapshot || (source ? await readFile(source) : void 0);
3304
+ const executable = serviceExecutableSnapshot || (source ? await readFile2(source) : void 0);
2944
3305
  if (!executable)
2945
3306
  throw new Error("Simplr Connect executable path is unavailable");
2946
- await mkdir(stateDirectory(), { recursive: true, mode: 448 });
3307
+ await mkdir2(stateDirectory(), { recursive: true, mode: 448 });
2947
3308
  await writeFile(serviceExecutablePath(), executable, {
2948
3309
  mode: 448
2949
3310
  });
@@ -2956,9 +3317,9 @@ async function installService(activate = true) {
2956
3317
  const nodePath = process.execPath;
2957
3318
  const connectPath = serviceExecutablePath();
2958
3319
  if (platform() === "darwin") {
2959
- const directory = join2(homedir(), "Library", "LaunchAgents");
3320
+ const directory = join3(homedir(), "Library", "LaunchAgents");
2960
3321
  const path = serviceDefinitionPath();
2961
- await mkdir(directory, { recursive: true, mode: 448 });
3322
+ await mkdir2(directory, { recursive: true, mode: 448 });
2962
3323
  const programArguments = STANDALONE_EXECUTABLE ? [connectPath, "watch"] : [nodePath, connectPath, "watch"];
2963
3324
  const plistArguments = programArguments.map((value) => `<string>${xmlValue(value)}</string>`).join("");
2964
3325
  const plist = `<?xml version="1.0" encoding="UTF-8"?>
@@ -2989,9 +3350,9 @@ async function installService(activate = true) {
2989
3350
  return "Simplr Connect self-healing service installed";
2990
3351
  }
2991
3352
  if (platform() === "linux") {
2992
- const directory = join2(homedir(), ".config", "systemd", "user");
3353
+ const directory = join3(homedir(), ".config", "systemd", "user");
2993
3354
  const path = serviceDefinitionPath();
2994
- await mkdir(directory, { recursive: true, mode: 448 });
3355
+ await mkdir2(directory, { recursive: true, mode: 448 });
2995
3356
  const serviceCommand = STANDALONE_EXECUTABLE ? `${systemdValue(connectPath)} watch` : `${systemdValue(nodePath)} ${systemdValue(connectPath)} watch`;
2996
3357
  const unit = `[Unit]
2997
3358
  Description=Simplr Connect secure workstation proxy
@@ -3006,7 +3367,7 @@ RestartSec=10
3006
3367
  NoNewPrivileges=true
3007
3368
  PrivateTmp=true
3008
3369
  ProtectSystem=strict
3009
- ReadWritePaths=${systemdValue(stateDirectory())} ${systemdValue(join2(homedir(), ".hermes"))}
3370
+ ReadWritePaths=${systemdValue(stateDirectory())} ${systemdValue(join3(homedir(), ".hermes"))}
3010
3371
 
3011
3372
  [Install]
3012
3373
  WantedBy=default.target
@@ -3195,7 +3556,7 @@ async function updateConnector(apply, expectedVersion, reportStage, commandId, o
3195
3556
  status: "downloaded",
3196
3557
  artifact_sha256: artifact.sha256
3197
3558
  });
3198
- if (bytes.byteLength !== artifact.size || createHash("sha256").update(bytes).digest("hex") !== artifact.sha256)
3559
+ if (bytes.byteLength !== artifact.size || createHash2("sha256").update(bytes).digest("hex") !== artifact.sha256)
3199
3560
  throw new Error("Downloaded update failed integrity verification");
3200
3561
  await reportStage?.({
3201
3562
  status: "verified",
@@ -3242,7 +3603,7 @@ async function updateConnector(apply, expectedVersion, reportStage, commandId, o
3242
3603
  1e4
3243
3604
  );
3244
3605
  else {
3245
- const helperPath = join2(stateDirectory(), "complete-update.ps1");
3606
+ const helperPath = join3(stateDirectory(), "complete-update.ps1");
3246
3607
  const powershellValue = (value) => `'${value.replace(/'/g, "''")}'`;
3247
3608
  const helper = `$ErrorActionPreference = "Stop"\r
3248
3609
  & schtasks.exe /End /TN "Simplr Connect" | Out-Null\r
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simplr-ai/connect",
3
- "version": "0.7.5",
3
+ "version": "0.7.7",
4
4
  "description": "Simplr Connect workstation enrollment and AI tool inventory companion",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",