@simplr-ai/connect 0.7.4 → 0.7.6

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 +293 -44
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -115,6 +115,84 @@ function mcpConfigurationPaths(homeDirectory, workingDirectory, organizationId)
115
115
  ];
116
116
  }
117
117
 
118
+ // src/work-order-support.ts
119
+ function workOrderClassification(payload) {
120
+ return {
121
+ ...typeof payload.work_order_kind === "string" ? { work_order_kind: payload.work_order_kind } : {},
122
+ ...typeof payload.source_type === "string" ? { source_type: payload.source_type } : {},
123
+ ...typeof payload.execution_mode === "string" ? { execution_mode: payload.execution_mode } : {}
124
+ };
125
+ }
126
+ function awaitsFeaturePlanApproval(classification) {
127
+ return classification.work_order_kind === "feature" && classification.source_type === "feedback" && classification.execution_mode === "plan";
128
+ }
129
+ function workOrderPayload(classification) {
130
+ return {
131
+ ...classification.work_order_kind ? { work_order_kind: classification.work_order_kind } : {},
132
+ ...classification.source_type ? { source_type: classification.source_type } : {},
133
+ ...classification.execution_mode ? { execution_mode: classification.execution_mode } : {}
134
+ };
135
+ }
136
+ async function postWithApiKey(url, body, apiKey) {
137
+ const response = await fetch(url, {
138
+ method: "POST",
139
+ headers: {
140
+ "Content-Type": "application/json",
141
+ "X-API-Key": apiKey
142
+ },
143
+ body: JSON.stringify(body),
144
+ signal: AbortSignal.timeout(1e4)
145
+ });
146
+ const payload = await response.json().catch(() => ({}));
147
+ if (!response.ok || !payload.content)
148
+ throw new Error(
149
+ payload.message || `Simplr request failed (${response.status})`
150
+ );
151
+ return payload.content;
152
+ }
153
+ function reservedSimplrConnectLane(organizationNamespace2) {
154
+ return `simplr-connect-reserved:${organizationNamespace2}`;
155
+ }
156
+ function isRunningTaskOwnedBy(task, expectedAssignee) {
157
+ return (task.task?.status || task.status) === "running" && (task.task?.assignee || task.assignee) === expectedAssignee;
158
+ }
159
+ var terminalCommentMarker = (action) => `[simplr-connect-terminal:${action}]`;
160
+ function taskStatus(task) {
161
+ return task.task?.status || task.status;
162
+ }
163
+ function taskBlockKind(task) {
164
+ const blockedEvent = [...task.events || []].reverse().find((event) => event.kind === "blocked");
165
+ return task.task?.block_kind || task.block_kind || blockedEvent?.payload?.kind;
166
+ }
167
+ function hasTerminalComment(task, action) {
168
+ const marker = terminalCommentMarker(action);
169
+ return (task.comments || []).some((comment) => comment.body?.includes(marker));
170
+ }
171
+ function reconcileTerminalBlock(action, detail, operations) {
172
+ const kind = action === "needs_input" ? "needs_input" : "transient";
173
+ const matches = (task) => taskStatus(task) === "blocked" && taskBlockKind(task) === kind && hasTerminalComment(task, action);
174
+ let current = operations.read();
175
+ if (!current)
176
+ throw new Error(`Hermes work-board ${action} preflight failed`);
177
+ if (matches(current)) return true;
178
+ if (!hasTerminalComment(current, action)) {
179
+ const body = `${detail}
180
+
181
+ ${terminalCommentMarker(action)}`;
182
+ const commented = operations.comment(body);
183
+ current = operations.read();
184
+ if (!current || !commented && !hasTerminalComment(current, action))
185
+ throw new Error(`Hermes work-board ${action} comment reconciliation failed`);
186
+ }
187
+ if (matches(current)) return true;
188
+ const blocked = operations.block(kind);
189
+ current = operations.read();
190
+ if (current && matches(current)) return true;
191
+ if (!blocked)
192
+ throw new Error(`Hermes work-board ${action} reconciliation failed`);
193
+ throw new Error(`Hermes work-board ${action} verification failed`);
194
+ }
195
+
118
196
  // src/index.ts
119
197
  var AmbiguousHermesRunError = class extends Error {
120
198
  };
@@ -122,7 +200,7 @@ var KanbanReconciliationError = class extends Error {
122
200
  };
123
201
  var DeferredUpdateError = class extends Error {
124
202
  };
125
- var APP_VERSION = "0.7.4";
203
+ var APP_VERSION = "0.7.6";
126
204
  var STANDALONE_EXECUTABLE = process.env.SIMPLR_CONNECT_STANDALONE === "true";
127
205
  var MANIFEST_PUBLIC_KEY = process.env.SIMPLR_CONNECT_MANIFEST_PUBLIC_KEY || "";
128
206
  var HERMES_INSTALL_COMMIT = "542e146b055f0d43767ac43cdb9e78054b240263";
@@ -1583,11 +1661,47 @@ function isSimplrWorkOrder(command) {
1583
1661
  String(command.payload.execution_mode)
1584
1662
  );
1585
1663
  }
1664
+ async function claimSimplrFeedbackInCloud(state, command) {
1665
+ if (String(command.payload.source_type) !== "feedback") return;
1666
+ const sourceId = String(command.payload.source_id || "");
1667
+ if (!sourceId) throw new Error("Simplr feedback source is missing");
1668
+ const agentToken = await ensureAgentCredential(state);
1669
+ await postWithApiKey(
1670
+ `${state.api_url}/v1/feedback/agent/items/${encodeURIComponent(sourceId)}/claim`,
1671
+ {},
1672
+ agentToken
1673
+ );
1674
+ }
1675
+ function readSimplrKanbanTask(executable, state, taskId) {
1676
+ for (let attempt = 0; attempt < 3; attempt += 1) {
1677
+ const shown = commandResult(
1678
+ executable,
1679
+ [
1680
+ ...hermesProfileArgs(state),
1681
+ "kanban",
1682
+ "--board",
1683
+ `simplr-${organizationNamespace(state.organization_id)}`,
1684
+ "show",
1685
+ taskId,
1686
+ "--json"
1687
+ ],
1688
+ 1e4
1689
+ );
1690
+ if (shown.ok) return JSON.parse(shown.output);
1691
+ }
1692
+ return null;
1693
+ }
1694
+ function simplrKanbanTaskStatus(task) {
1695
+ return task.task?.status || task.status;
1696
+ }
1586
1697
  function ensureSimplrKanbanTask(state, command, prompt) {
1587
1698
  if (!isSimplrWorkOrder(command)) return void 0;
1588
1699
  const executable = detectHermesExecutable();
1589
1700
  if (!executable) throw new Error("Hermes executable is unavailable");
1590
1701
  const boardSlug = `simplr-${organizationNamespace(state.organization_id)}`;
1702
+ const reservedAssignee = reservedSimplrConnectLane(
1703
+ organizationNamespace(state.organization_id)
1704
+ );
1591
1705
  const boards = commandResult(
1592
1706
  executable,
1593
1707
  [...hermesProfileArgs(state), "kanban", "boards", "list", "--json"],
@@ -1664,10 +1778,12 @@ ${prompt}`;
1664
1778
  `simplr-connect:${state.organization_id}:${command.id}`,
1665
1779
  "--created-by",
1666
1780
  "Simplr Connect",
1781
+ "--assignee",
1782
+ reservedAssignee,
1667
1783
  "--skill",
1668
1784
  "simplr-remote-operations",
1669
1785
  "--initial-status",
1670
- "blocked",
1786
+ "running",
1671
1787
  "--json"
1672
1788
  ],
1673
1789
  15e3
@@ -1676,12 +1792,33 @@ ${prompt}`;
1676
1792
  throw new Error("Hermes could not persist the Simplr work order");
1677
1793
  const task = JSON.parse(createdTask.output);
1678
1794
  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
- );
1795
+ let current = readSimplrKanbanTask(executable, state, task.id);
1796
+ if (!current) throw new Error("Hermes could not verify the Simplr work-order task");
1797
+ if (simplrKanbanTaskStatus(current) === "ready") {
1798
+ const claimed = commandResult(
1799
+ executable,
1800
+ [
1801
+ ...hermesProfileArgs(state),
1802
+ "kanban",
1803
+ "--board",
1804
+ boardSlug,
1805
+ "claim",
1806
+ task.id,
1807
+ "--ttl",
1808
+ "3600"
1809
+ ],
1810
+ 1e4
1811
+ );
1812
+ current = readSimplrKanbanTask(executable, state, task.id);
1813
+ if (!claimed.ok && simplrKanbanTaskStatus(current || {}) !== "running") {
1814
+ throw new Error("Hermes could not claim the Simplr work-order task");
1815
+ }
1816
+ }
1817
+ if (!current || !isRunningTaskOwnedBy(current, reservedAssignee)) {
1818
+ throw new Error(
1819
+ "Hermes Simplr work-order task is not running in its reserved ownership lane"
1820
+ );
1821
+ }
1685
1822
  return task.id;
1686
1823
  }
1687
1824
  function updateSimplrKanbanTask(state, taskId, action, detail) {
@@ -1710,7 +1847,47 @@ function updateSimplrKanbanTask(state, taskId, action, detail) {
1710
1847
  1e4
1711
1848
  ).ok;
1712
1849
  }
1713
- const args = action === "complete" ? [
1850
+ if (action === "failed" || action === "needs_input") {
1851
+ try {
1852
+ return reconcileTerminalBlock(action, detail.slice(0, 2e3), {
1853
+ read: () => readSimplrKanbanTask(executable, state, taskId),
1854
+ comment: (body) => commandResult(
1855
+ executable,
1856
+ [
1857
+ ...hermesProfileArgs(state),
1858
+ "kanban",
1859
+ "--board",
1860
+ `simplr-${organizationNamespace(state.organization_id)}`,
1861
+ "comment",
1862
+ "--author",
1863
+ "Simplr Connect",
1864
+ taskId,
1865
+ body
1866
+ ],
1867
+ 1e4
1868
+ ).ok,
1869
+ block: (kind) => commandResult(
1870
+ executable,
1871
+ [
1872
+ ...hermesProfileArgs(state),
1873
+ "kanban",
1874
+ "--board",
1875
+ `simplr-${organizationNamespace(state.organization_id)}`,
1876
+ "block",
1877
+ "--kind",
1878
+ kind,
1879
+ taskId
1880
+ ],
1881
+ 1e4
1882
+ ).ok
1883
+ });
1884
+ } catch (error) {
1885
+ throw new KanbanReconciliationError(
1886
+ error instanceof Error ? error.message : `Hermes work-board ${action} reconciliation failed`
1887
+ );
1888
+ }
1889
+ }
1890
+ const args = [
1714
1891
  ...hermesProfileArgs(state),
1715
1892
  "kanban",
1716
1893
  "--board",
@@ -1724,39 +1901,21 @@ function updateSimplrKanbanTask(state, taskId, action, detail) {
1724
1901
  source: "simplr-connect",
1725
1902
  rules_version: "simplr-work-order-v1"
1726
1903
  })
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
1904
  ];
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
1905
+ const matchesAction = (current) => {
1906
+ const currentStatus = simplrKanbanTaskStatus(current);
1907
+ return action === "complete" && currentStatus === "done";
1908
+ };
1909
+ const initial = readSimplrKanbanTask(executable, state, taskId);
1910
+ if (!initial) {
1911
+ throw new KanbanReconciliationError(
1912
+ `Hermes work-board ${action} preflight failed`
1752
1913
  );
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
1914
  }
1915
+ if (matchesAction(initial)) return true;
1916
+ if (commandResult(executable, args, 1e4).ok) return true;
1917
+ const reconciled = readSimplrKanbanTask(executable, state, taskId);
1918
+ if (reconciled && matchesAction(reconciled)) return true;
1760
1919
  throw new KanbanReconciliationError(
1761
1920
  `Hermes work-board ${action} reconciliation failed`
1762
1921
  );
@@ -2167,13 +2326,24 @@ async function monitorHermesRun(state, command, runId, startedAt) {
2167
2326
  command_id: command.id,
2168
2327
  type: command.type,
2169
2328
  label: command.reason,
2329
+ ...workOrderClassification(command.payload),
2170
2330
  run_id: runId,
2171
2331
  status: "completed",
2172
2332
  result: output,
2173
2333
  started_at: startedAt,
2174
2334
  updated_at: (/* @__PURE__ */ new Date()).toISOString()
2175
2335
  });
2176
- updateSimplrKanbanTask(state, taskId, "complete", output);
2336
+ const awaitingPlanApproval = awaitsFeaturePlanApproval(
2337
+ workOrderClassification(command.payload)
2338
+ );
2339
+ updateSimplrKanbanTask(
2340
+ state,
2341
+ taskId,
2342
+ awaitingPlanApproval ? "needs_input" : "complete",
2343
+ awaitingPlanApproval ? `Feature plan ready for comment and approval. Review the plan below, add guidance, then explicitly approve a new implementation work order.
2344
+
2345
+ ${output}` : output
2346
+ );
2177
2347
  return output;
2178
2348
  }
2179
2349
  if (status.status === "cancelled")
@@ -2186,6 +2356,7 @@ async function monitorHermesRun(state, command, runId, startedAt) {
2186
2356
  command_id: command.id,
2187
2357
  type: command.type,
2188
2358
  label: command.reason,
2359
+ ...workOrderClassification(command.payload),
2189
2360
  run_id: runId,
2190
2361
  status: run.status,
2191
2362
  started_at: startedAt,
@@ -2197,6 +2368,7 @@ async function monitorHermesRun(state, command, runId, startedAt) {
2197
2368
  command_id: command.id,
2198
2369
  type: command.type,
2199
2370
  label: command.reason,
2371
+ ...workOrderClassification(command.payload),
2200
2372
  run_id: runId,
2201
2373
  status: run.status,
2202
2374
  approval_command: approval.approval_command,
@@ -2254,11 +2426,14 @@ async function runHermesCommand(state, command) {
2254
2426
  const journal = await loadCommandJournal();
2255
2427
  const existing = journal[command.id];
2256
2428
  if (existing?.status === "completed") {
2429
+ const awaitingPlanApproval = awaitsFeaturePlanApproval(existing);
2257
2430
  updateSimplrKanbanTask(
2258
2431
  state,
2259
2432
  existing.kanban_task_id,
2260
- "complete",
2261
- existing.result || "Hermes completed the task"
2433
+ awaitingPlanApproval ? "needs_input" : "complete",
2434
+ awaitingPlanApproval ? `Feature plan ready for comment and approval. Review the plan below, add guidance, then explicitly approve a new implementation work order.
2435
+
2436
+ ${existing.result || "Hermes completed the plan"}` : existing.result || "Hermes completed the task"
2262
2437
  );
2263
2438
  return "Hermes completed this task before Simplr reconnected";
2264
2439
  }
@@ -2273,6 +2448,21 @@ async function runHermesCommand(state, command) {
2273
2448
  if (!runId) {
2274
2449
  const prompt = typeof command.payload.prompt === "string" ? command.payload.prompt : "";
2275
2450
  if (!prompt) throw new Error("Hermes task prompt is missing");
2451
+ if (existing?.status !== "preparing") {
2452
+ await updateCommandJournal({
2453
+ command_id: command.id,
2454
+ organization_id: state.organization_id,
2455
+ organization_name: state.organization_name,
2456
+ workstation_id: state.workstation_id,
2457
+ type: command.type,
2458
+ label: command.reason,
2459
+ ...workOrderClassification(command.payload),
2460
+ status: "preparing",
2461
+ started_at: startedAt,
2462
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
2463
+ });
2464
+ }
2465
+ await claimSimplrFeedbackInCloud(state, command);
2276
2466
  const kanbanTaskId = ensureSimplrKanbanTask(state, command, prompt);
2277
2467
  await updateCommandJournal({
2278
2468
  command_id: command.id,
@@ -2281,6 +2471,7 @@ async function runHermesCommand(state, command) {
2281
2471
  workstation_id: state.workstation_id,
2282
2472
  type: command.type,
2283
2473
  label: command.reason,
2474
+ ...workOrderClassification(command.payload),
2284
2475
  status: "creating",
2285
2476
  kanban_task_id: kanbanTaskId,
2286
2477
  started_at: startedAt,
@@ -2303,6 +2494,7 @@ async function runHermesCommand(state, command) {
2303
2494
  command_id: command.id,
2304
2495
  type: command.type,
2305
2496
  label: command.reason,
2497
+ ...workOrderClassification(command.payload),
2306
2498
  run_id: runId,
2307
2499
  status: "running",
2308
2500
  started_at: startedAt,
@@ -2612,6 +2804,7 @@ async function applyCommand(state, command, inventoryAlreadySynced) {
2612
2804
  command_id: command.id,
2613
2805
  type: command.type,
2614
2806
  label: command.reason,
2807
+ ...workOrderClassification(command.payload),
2615
2808
  run_id: existing?.run_id,
2616
2809
  status: "failed",
2617
2810
  started_at: existing?.started_at || (/* @__PURE__ */ new Date()).toISOString(),
@@ -2667,7 +2860,7 @@ async function resumeHermesRuns(state) {
2667
2860
  workstation_id: connection.workstation_id,
2668
2861
  type: "run_hermes",
2669
2862
  reason: entry.label,
2670
- payload: {}
2863
+ payload: workOrderPayload(entry)
2671
2864
  },
2672
2865
  false
2673
2866
  );
@@ -3520,6 +3713,62 @@ async function main() {
3520
3713
  token
3521
3714
  );
3522
3715
  process.stdout.write(`${JSON.stringify(result)}
3716
+ `);
3717
+ return;
3718
+ }
3719
+ if (command === "release-action-authorize") {
3720
+ const organizationId = argument("--organization-id");
3721
+ const action = argument("--action");
3722
+ if (!isUuid(organizationId) || !["review", "merge", "promote", "rollback_release", "rollback_flag"].includes(action || ""))
3723
+ throw new Error("Use: simplr-connect release-action-authorize --organization-id <uuid> --action <action> [exact target arguments]");
3724
+ const state = await loadState();
3725
+ const connection = state.connections.find((item) => item.organization_id === organizationId);
3726
+ if (!connection) throw new Error("Organization is not connected");
3727
+ const token = await loadCredential(stateForConnection(state, connection));
3728
+ const pullRequestNumber = argument("--pull-request-number");
3729
+ const body = {
3730
+ action,
3731
+ service_id: argument("--service-id"),
3732
+ repository: argument("--repository"),
3733
+ pull_request_number: pullRequestNumber ? Number(pullRequestNumber) : void 0,
3734
+ head_sha: argument("--head-sha"),
3735
+ base_branch: argument("--base-branch"),
3736
+ target_environment_id: argument("--target-environment-id"),
3737
+ feature_flag_id: argument("--feature-flag-id")
3738
+ };
3739
+ const result = await post(`${connection.api_url}/v1/ai-workstations/release-actions/authorize`, body, token);
3740
+ process.stdout.write(`${JSON.stringify(result)}
3741
+ `);
3742
+ return;
3743
+ }
3744
+ if (command === "release-action-consume") {
3745
+ const organizationId = argument("--organization-id");
3746
+ const authorizationId = argument("--authorization-id");
3747
+ if (!isUuid(organizationId) || !isUuid(authorizationId))
3748
+ throw new Error("Use: simplr-connect release-action-consume --organization-id <uuid> --authorization-id <uuid>");
3749
+ const state = await loadState();
3750
+ const connection = state.connections.find((item) => item.organization_id === organizationId);
3751
+ if (!connection) throw new Error("Organization is not connected");
3752
+ const token = await loadCredential(stateForConnection(state, connection));
3753
+ const result = await post(`${connection.api_url}/v1/ai-workstations/release-actions/${authorizationId}/consume`, {}, token);
3754
+ process.stdout.write(`${JSON.stringify(result)}
3755
+ `);
3756
+ return;
3757
+ }
3758
+ if (command === "release-action-complete") {
3759
+ const organizationId = argument("--organization-id");
3760
+ const authorizationId = argument("--authorization-id");
3761
+ const outcome = argument("--outcome");
3762
+ const resultingSha = argument("--resulting-sha");
3763
+ const summary = (await readStandardInput()).trim();
3764
+ if (!isUuid(organizationId) || !isUuid(authorizationId) || !["succeeded", "failed"].includes(outcome || "") || !summary || summary.length > 2e3)
3765
+ throw new Error("Use: simplr-connect release-action-complete --organization-id <uuid> --authorization-id <uuid> --outcome <succeeded|failed> [--resulting-sha <full-sha>] < summary");
3766
+ const state = await loadState();
3767
+ const connection = state.connections.find((item) => item.organization_id === organizationId);
3768
+ if (!connection) throw new Error("Organization is not connected");
3769
+ const token = await loadCredential(stateForConnection(state, connection));
3770
+ const result = await post(`${connection.api_url}/v1/ai-workstations/release-actions/${authorizationId}/complete`, { outcome, summary, resulting_sha: resultingSha }, token);
3771
+ process.stdout.write(`${JSON.stringify(result)}
3523
3772
  `);
3524
3773
  return;
3525
3774
  }
@@ -3592,7 +3841,7 @@ async function main() {
3592
3841
  return;
3593
3842
  }
3594
3843
  process.stdout.write(
3595
- "Simplr Connect\n\nCommands:\n wizard\n enroll --api-url <url> --code <code> [--setup-hermes] [--install-service]\n hermes-setup\n status\n overview --organization-id <uuid>\n work-order-traces --organization-id <uuid>\n service-install\n repair\n update-check\n update\n sync\n watch\n run -- <ai-command> [arguments]\n"
3844
+ "Simplr Connect\n\nCommands:\n wizard\n enroll --api-url <url> --code <code> [--setup-hermes] [--install-service]\n hermes-setup\n status\n overview --organization-id <uuid>\n work-order-traces --organization-id <uuid>\n release-action-authorize --organization-id <uuid> --action <action> [exact target arguments]\n service-install\n repair\n update-check\n update\n sync\n watch\n run -- <ai-command> [arguments]\n"
3596
3845
  );
3597
3846
  }
3598
3847
  main().catch(async (error) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simplr-ai/connect",
3
- "version": "0.7.4",
3
+ "version": "0.7.6",
4
4
  "description": "Simplr Connect workstation enrollment and AI tool inventory companion",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",