@chipschat/hermes 1.0.8 → 1.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,7 +14,7 @@ on Hermes's own API server.
14
14
  its original's profile, in its original's folder: Hermes forks a session only within its
15
15
  profile, so a copy has no profile of its own.
16
16
 
17
- Apache-2.0. Not published: a local repository until Chips's SDK is.
17
+ Apache-2.0, published on npm as `@chipschat/hermes`.
18
18
 
19
19
  ## What Chips shows
20
20
 
@@ -83,11 +83,11 @@ hasn't been tried through Chips yet.
83
83
 
84
84
  ## From Chips's gallery
85
85
 
86
- The card's line is `chips connect hermes '<link>'` (Chips ADR 0218). It downloads this bridge as the npm package
86
+ The card's line is `CHIPS_JOIN_LINK='<link>' chips connect hermes` (Chips ADR 0218). It downloads this bridge as the npm package
87
87
  `@chipschat/hermes`, the newest 1.x, checks it against the registry's checksum, and runs it on the Node
88
88
  that Chips carries, so the machine needs no Node or npm of its own. The bridge checks Hermes,
89
89
  joins, and keeps itself running from login. `chips disconnect hermes` stops it. On another computer,
90
- `curl -fsSL https://chipschat.ai/install | sh -s -- connect hermes '<link>'` installs Chips there
90
+ `curl -fsSL https://chipschat.ai/install | CHIPS_JOIN_LINK='<link>' sh -s -- connect hermes` installs Chips there
91
91
  first. The steps below run the bridge from this checkout instead, to work on it.
92
92
 
93
93
  ## Run it
@@ -103,11 +103,11 @@ first. The steps below run the bridge from this checkout instead, to work on it.
103
103
 
104
104
  2. Once, in this repository: `npm install`.
105
105
 
106
- 3. Join Chips once, with a link from Chips's gallery (Add an agent → Hermes), whose card gives
106
+ 3. Join Chips once, with a link from Chips's gallery (Agents → Connect a harness → Hermes), whose card gives
107
107
  this line with the link in it:
108
108
 
109
109
  ```
110
- cd ~/Work/Chips/chips-hermes && CHIPS_JOIN_LINK='<link>' npm run keep
110
+ cd chips-hermes && CHIPS_JOIN_LINK='<link>' npm run keep
111
111
  ```
112
112
 
113
113
  `npm run keep` joins, then keeps the bridge running from login (below); `npm start` is the
@@ -1,6 +1,6 @@
1
1
  # Third-party notices
2
2
 
3
- `dist/main.js` in @chipschat/hermes 1.0.8 carries these packages, bundled in. Each one's
3
+ `dist/main.js` in @chipschat/hermes 1.0.11 carries these packages, bundled in. Each one's
4
4
  licence follows.
5
5
 
6
6
  ## @chipschat/harness 0.0.0 (Apache-2.0)
package/dist/main.js CHANGED
@@ -9,7 +9,7 @@ var __export = (target, all) => {
9
9
 
10
10
  // src/main.ts
11
11
  import { randomBytes as randomBytes3 } from "node:crypto";
12
- import { existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync5 } from "node:fs";
12
+ import { existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync5 } from "node:fs";
13
13
  import { createServer as createServer2 } from "node:net";
14
14
  import { homedir as homedir8 } from "node:os";
15
15
  import { dirname as dirname6, join as join13, resolve as resolve6 } from "node:path";
@@ -19945,8 +19945,9 @@ var policyRule = external_exports.object({
19945
19945
  external: external_exports.boolean().optional(),
19946
19946
  irreversible: external_exports.boolean().optional(),
19947
19947
  money_usd_over: external_exports.number().nonnegative().optional(),
19948
- risk: external_exports.array(riskClass).max(8).optional()
19949
- }).optional().describe("Extra conditions on the action's flags."),
19948
+ risk: external_exports.array(riskClass).max(8).optional(),
19949
+ starts_with: external_exports.array(external_exports.string().min(1).max(400)).min(1).max(8).optional().describe("For an action that carries a command (ADR 0239): the words a part of it starts with. A command goes ahead on such rules only when every part starts with one that allows it.")
19950
+ }).optional().describe("Extra conditions on the action's flags, and for a command, how its parts start."),
19950
19951
  decision: external_exports.enum(["allow", "ask", "deny"]).describe("What to do when the rule matches."),
19951
19952
  approvers: external_exports.array(approverRef).max(16).optional().describe("Who may answer, when the decision is `ask`."),
19952
19953
  reason: external_exports.string().max(400).optional().describe("Shown to the harness, and recorded in the audit log."),
@@ -20465,9 +20466,17 @@ var roomIdle = external_exports.object({
20465
20466
  var roomNote = external_exports.object({
20466
20467
  members: external_exports.array(roomMember).max(500).describe("The room's people, then its agents, each by name, anybody removed left out."),
20467
20468
  notes: external_exports.record(external_exports.string().max(120), external_exports.string().max(64e3)).describe("The note for each who takes the turn (`for`), by their id: who is here, with handles, and that only those @-mentioned act. A harness puts it at the top of the turn."),
20469
+ rules: external_exports.record(external_exports.string().max(120), external_exports.string().max(4e3)).optional().describe("How the space works, for each who takes the turn, by their id: the words its note ends with (ADR 0250), that everyone here reads what it writes and, beside other agents, what an @ does and when to answer NO_REPLY. They stay the same while the space's agents do, so a harness whose session keeps its earlier turns may say them once, and the note without them after."),
20468
20470
  working: external_exports.array(roomWork).max(500).optional().describe("The room's agents with a turn going as Chips sends this one (ADR 0104), the takers' own included. A turn nothing has been heard from for ten minutes is not counted."),
20469
20471
  idle: external_exports.array(roomIdle).max(500).optional().describe("The room's other agents that have had a turn here: how long since the last one ended (ADR 0104).")
20470
20472
  });
20473
+ function noteWithoutRules(room, taker) {
20474
+ const note = room?.notes[taker];
20475
+ const rules = room?.rules?.[taker];
20476
+ if (note === void 0 || rules === void 0 || !note.endsWith(rules))
20477
+ return note;
20478
+ return note.slice(0, note.length - rules.length).trimEnd();
20479
+ }
20471
20480
  var MONTHS = [
20472
20481
  "January",
20473
20482
  "February",
@@ -20487,8 +20496,8 @@ var MONTHS = [
20487
20496
  var CONTEXT_CEILING = { lines: 200, chars: 32e3 };
20488
20497
  var CONTEXT_HEADING = "Said in this space since you last spoke:";
20489
20498
  var contextBudget = external_exports.object({
20490
- lines: external_exports.int().nonnegative().optional().describe("The most lines to be handed, 20 when absent. Chips hands over 200 at most, and none when this is 0."),
20491
- chars: external_exports.int().nonnegative().optional().describe("The most characters those lines may run to, as `Name: words` one per line, 4,000 when absent. Chips hands over 32,000 at most.")
20499
+ lines: external_exports.int().nonnegative().optional().describe("The most lines to be handed, 40 when absent. Chips hands over 200 at most, and none when this is 0."),
20500
+ chars: external_exports.int().nonnegative().optional().describe("The most characters those lines may run to, as `Name: words` one per line, 16,000 when absent. Chips hands over 32,000 at most.")
20492
20501
  }).describe("How much of the room a harness is handed with a message for it (ADR 0088). Its own to say: a budget promises nothing to anybody else.");
20493
20502
  var contextLine = external_exports.object({
20494
20503
  seq: external_exports.int().positive().describe("Where in the log it happened."),
@@ -20868,7 +20877,7 @@ var messageNew = defineEvent({
20868
20877
  summary: "Someone said something, with any attachments as signed URLs the harness can fetch.",
20869
20878
  requiresRun: false,
20870
20879
  payload: deliveredMessage.extend({
20871
- context: roomContext.optional().describe("What was said in the room since this harness last spoke there, or since it joined when it has not, up to this message and without it (ADR 0088). From when the turn that said its last words began, so what was said while it worked is here, and its own words are not (ADR 0105). Worked out from the log, so a replay hands over the same lines. As many as the budget the harness gave in its `hello`: 20 lines and 4,000 characters when it gave none, and none when it asked for 0. When older lines were left out, the taker's room note says so."),
20880
+ context: roomContext.optional().describe("What was said in the room since this harness last spoke there, or since it joined when it has not, up to this message and without it (ADR 0088). From when the turn that said its last words began, so what was said while it worked is here, and its own words are not (ADR 0105). Worked out from the log, so a replay hands over the same lines. As many as the budget the harness gave in its `hello`: 40 lines and 16,000 characters when it gave none, and none when it asked for 0. When older lines were left out, the taker's room note says so."),
20872
20881
  room: roomNote.optional().describe("Who is in the room, with the handle that reaches each, and a note for each who takes the turn saying so and what an @ does (ADR 0092). A harness puts `notes[<taker>]` at the top of that turn. Absent on a message in no room."),
20873
20882
  exchange: deliveredExchange.optional().describe("How the agents' talk in the room stands, once it is worth a word (ADR 0097): from eight messages between agents since a person last acted or an agent last made something new. A harness puts `note` at the top of the turn, as Chips's, ahead of the room's note. Absent below that, and worked out from the log, so a replay hands over the same facts."),
20874
20883
  handed: external_exports.literal(true).optional().describe("Chips held this message when it was said and hands it over now a person has acted (ADR 0097): the newest of an agent's held messages, once; or, with `lead`, hands it to the room's lead. Its seq and id are the hand-over's, its `message_id` the message's own."),
@@ -20918,6 +20927,7 @@ var approvalResolved = defineEvent({
20918
20927
  approval_id: id("approval"),
20919
20928
  decision: external_exports.enum(["approved", "edited", "denied", "expired"]).describe("`expired` means nobody answered and the fallback applied."),
20920
20929
  scope: external_exports.enum(["once", "session", "always"]).default("once").describe("How far the answer reaches. `always` becomes a policy rule."),
20930
+ everywhere: external_exports.boolean().optional().describe("With `always`: the rule is the harness's in every space, not only this one (ADR 0239). An owner or an admin only."),
20921
20931
  updated_input: external_exports.unknown().optional().describe("The edited action input, when the decision is `edited`."),
20922
20932
  reason: external_exports.string().max(4e3).optional().describe("Why. The harness gets to read this."),
20923
20933
  resolved_by: external_exports.string().min(1).max(120).describe("The participant who answered, or `system` on expiry \u2014 a harness when it settles its own ask, answered in its own tools. Filled in by Chips from the credential that sent it; a value the sender writes is replaced."),
@@ -21182,6 +21192,10 @@ function namesInWords(names) {
21182
21192
  return `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
21183
21193
  }
21184
21194
  function renderProjectNote(facts, options) {
21195
+ return `${renderProjectFacts(facts)}
21196
+ ${renderProjectAsks(facts, options)}`;
21197
+ }
21198
+ function renderProjectFacts(facts) {
21185
21199
  const files = `${facts.files} file${facts.files === 1 ? "" : "s"}`;
21186
21200
  const lines2 = [
21187
21201
  `The space's project is a git repository. Your copy is ${facts.folder} (${files}), ${facts.trouble === void 0 ? "brought up to date just now" : "not brought up to date"}.`
@@ -21199,9 +21213,9 @@ function renderProjectNote(facts, options) {
21199
21213
  for (const key of facts.keys) {
21200
21214
  lines2.push(`${key.path} holds what looks like ${key.what}, so your last change wasn't sent: take it out and read it from outside the project.`);
21201
21215
  }
21202
- return [...lines2, ...asks(facts, options)].join("\n");
21216
+ return lines2.join("\n");
21203
21217
  }
21204
- function asks(facts, options) {
21218
+ function renderProjectAsks(facts, options) {
21205
21219
  const lines2 = [
21206
21220
  "Work on the project in that folder and leave git to Chips: when your turn ends, what you changed is committed under your name and sent to everyone here. Keep scratch work outside it.",
21207
21221
  // Sent with the answer too, a project's file became a room file of its own, with versions of
@@ -21222,7 +21236,7 @@ function asks(facts, options) {
21222
21236
  if (facts.pages === true) {
21223
21237
  lines2.push("A page opens in Chips as one document: name every file it needs in its tags and imports (<link>, <script src>, <img src>, import), since it can't fetch files while it runs.");
21224
21238
  }
21225
- return lines2;
21239
+ return lines2.join("\n");
21226
21240
  }
21227
21241
 
21228
21242
  // node_modules/@chipschat/protocol/dist/src/events/harness-to-chips.js
@@ -21874,7 +21888,7 @@ var membershipRemoved = defineEvent({
21874
21888
  payload: external_exports.object({
21875
21889
  participant_id: external_exports.string().min(1).max(120).describe("Who left."),
21876
21890
  removed_by: external_exports.string().min(1).max(120).optional().describe("Who removed them, if not themselves."),
21877
- reason: external_exports.enum([...RELEASE_REASONS, "person_removed"]).optional().describe("For an agent (ADR 0087): why it left, as `agent.released` says. For a person, `person_removed`: they were removed from the workspace (ADR 0160). Written by Chips; absent, a person took them out or they left."),
21891
+ reason: external_exports.enum([...RELEASE_REASONS, "person_removed", "person_left"]).optional().describe("For an agent (ADR 0087): why it left, as `agent.released` says. For a person, `person_removed`: they were removed from the workspace (ADR 0160); `person_left`: they deleted their account and left it (ADR 0243). Written by Chips; absent, a person took them out or they left the room."),
21878
21892
  moved_to: id("space").optional().describe("For an agent moved away: the room it went to, so this room can say so.")
21879
21893
  })
21880
21894
  });
@@ -22183,14 +22197,14 @@ var personRemoved = defineEvent({
22183
22197
  type: "person.removed",
22184
22198
  direction: "workspace",
22185
22199
  sender: "chips",
22186
- summary: "A person was removed from the workspace: signed out everywhere, out of every room, and the harnesses they owned handed to whoever removed them. What they said stays, under their name.",
22200
+ summary: "A person was removed from the workspace, or deleted their account: signed out everywhere, out of every room, and the harnesses they owned handed on. What they said stays, under their name.",
22187
22201
  requiresRun: false,
22188
22202
  payload: external_exports.object({
22189
22203
  participant_id: id("person").describe("Who was removed."),
22190
22204
  display_name: external_exports.string().min(1).max(120).describe("What they were called when they were removed, so the audit needs no lookup."),
22191
- removed_by: id("person").describe("The owner or admin who removed them."),
22205
+ removed_by: id("person").describe("The owner or admin who removed them, or they themselves when they deleted their account (ADR 0243)."),
22192
22206
  spaces: external_exports.array(id("space")).max(1e3).optional().describe("The rooms they were taken out of, each with its own `membership.removed`."),
22193
- harnesses: external_exports.array(id("harness")).max(1e3).optional().describe("The harnesses they owned, handed to `removed_by` by `harness.transferred`.")
22207
+ harnesses: external_exports.array(id("harness")).max(1e3).optional().describe("The harnesses they owned, each handed on by `harness.transferred`: to `removed_by`, or, for an account deleted, to an owner.")
22194
22208
  })
22195
22209
  });
22196
22210
  var joinWho = {
@@ -22536,7 +22550,7 @@ var clientHelloFrame = external_exports.object({
22536
22550
  })).max(64).optional().describe("Not read by Chips: a connection is its token's, and the server's `hello` names the token's harness and its agents (ADR 0087). Absent for a person's client."),
22537
22551
  since: external_exports.int().nonnegative().optional().describe("Not read by Chips: a stream resumes from `subscribe`'s `since`."),
22538
22552
  accepts: external_exports.array(external_exports.string().min(1).max(40)).max(32).optional().describe("Server frames beyond the base set this client understands, by the names in `SERVER_EXTENSIONS`. A name the server does not know is ignored rather than refused, so a client may name a frame a newer server sends and still be answered by an older one (ADR 0211, Phase 3). A client that says `events` is sent its replay in pages rather than one frame per event; one that says `presence` is sent an agent's state as it changes (ADR 0083); a harness's socket that says `restart` is sent `restart` when a person asks it to restart (ADR 0094); an owner's or admin's that says `join` is sent each join request as it changes (ADR 0122); a harness's that says `context` is sent `context.edit` when a person changes one of its context files (ADR 0134); a harness's that says `models` is sent `model.change` when a person changes an agent's model or effort (ADR 0140); a harness's that says `heartbeats` is sent `heartbeat.change` when a person switches an agent's heartbeat or changes how often it beats (ADR 0143); a harness's that says `context_windows` is sent `context_window.compact` and `context_window.change` when a person asks an agent to compact now or changes the limit it compacts itself at (ADR 0146); a person's that says `folders` is sent `folder.open` when they ask, from another of their devices, for a room's folder to be opened in Finder (ADR 0225), and `folder.run` when they ask for its project to be run or stopped there (ADR 0226); a person's that says `folder_runs` is sent `folder.run_state` when their Mac says how a run of theirs stands (ADR 0226); one that says nothing is sent what it was always sent, since an unknown frame is dropped in silence."),
22539
- context: contextBudget.optional().describe("How much of a room a harness is handed with each message for it (ADR 0088): 20 lines and 4,000 characters when absent, 200 and 32,000 at most. For this connection. A person's client is handed none.")
22553
+ context: contextBudget.optional().describe("How much of a room a harness is handed with each message for it (ADR 0088): 40 lines and 16,000 characters when absent, 200 and 32,000 at most. For this connection. A person's client is handed none.")
22540
22554
  }).describe("The first frame a client sends.");
22541
22555
  var subscribeFrame = external_exports.object({
22542
22556
  frame: external_exports.literal("subscribe"),
@@ -23793,6 +23807,14 @@ var Run = class {
23793
23807
  ...cost2(input2.costUsd)
23794
23808
  });
23795
23809
  }
23810
+ /**
23811
+ * That this turn heard a message said in the room while it worked (ADR 0106): one that
23812
+ * `message.overheard` named this run for in `working_turns`, handed to the turn at a step. The
23813
+ * room shows its agent heard it while working. Say it once the turn has the words, never before.
23814
+ */
23815
+ heard(messageId) {
23816
+ return this.wire.send("message.heard", { message_id: messageId });
23817
+ }
23796
23818
  checkpoint(label2, next, compacted) {
23797
23819
  return this.wire.send("checkpoint", {
23798
23820
  label: label2,
@@ -24824,6 +24846,25 @@ function connect(options) {
24824
24846
  return Chips.connect(options);
24825
24847
  }
24826
24848
 
24849
+ // node_modules/@chipschat/harness/dist/heard.js
24850
+ function workingTurnsOf(message) {
24851
+ return (message.working_turns ?? []).map((turn) => ({
24852
+ agentId: turn.agent_id,
24853
+ runId: turn.run_id
24854
+ }));
24855
+ }
24856
+ function overheardLine(message) {
24857
+ const files = message.attachments ?? [];
24858
+ const said2 = files.flatMap((file3) => file3.transcript === void 0 ? [] : [file3.transcript]);
24859
+ const words4 = [
24860
+ message.text.trim(),
24861
+ ...said2.map((each) => `(voice note: ${each})`),
24862
+ files.length === 0 ? "" : `(sent ${files.map((file3) => file3.filename).join(", ")})`
24863
+ ].filter((part) => part !== "");
24864
+ const who = withRole(message.from.display_name, message.from);
24865
+ return `${who} said in the room, not to you: ${words4.join(" ")}`;
24866
+ }
24867
+
24827
24868
  // node_modules/@chipschat/harness/dist/join.js
24828
24869
  import { createHash } from "node:crypto";
24829
24870
  import { readFileSync as readFileSync2 } from "node:fs";
@@ -25098,8 +25139,19 @@ async function redeem(url2, secret, options, send, machine) {
25098
25139
  throw await refused(answered);
25099
25140
  return await answered.json();
25100
25141
  }
25142
+ function reaching(send, url2) {
25143
+ return async (input2, init) => {
25144
+ try {
25145
+ return await send(input2, init);
25146
+ } catch (error62) {
25147
+ if (error62 instanceof Error && error62.name === "AbortError")
25148
+ throw error62;
25149
+ throw new JoinError("unreachable", `Can't reach Chips at ${url2} from this computer. It has to be on the same network as Chips, on its tailnet, or Chips has to be at an address this computer can reach.`);
25150
+ }
25151
+ };
25152
+ }
25101
25153
  function askOrRedeem(server, options) {
25102
- const send = options.fetch ?? fetch;
25154
+ const send = reaching(options.fetch ?? fetch, server.url);
25103
25155
  const machine = options.machine ?? hostname3();
25104
25156
  return server.secret === void 0 ? ask(server.url, options, send, machine) : redeem(server.url, server.secret, options, send, machine);
25105
25157
  }
@@ -25265,14 +25317,31 @@ async function sendAnswer(answer, options) {
25265
25317
  }
25266
25318
 
25267
25319
  // node_modules/@chipschat/harness/dist/prompt.js
25320
+ function takerOf(message, options) {
25321
+ return options.taker ?? (message.for?.length === 1 ? message.for[0] : void 0);
25322
+ }
25323
+ function othersBeside(message, taker) {
25324
+ return (message.room?.members ?? []).some((member) => member.kind === "agent" && member.participant_id !== taker);
25325
+ }
25326
+ function rulesFor(message, options = {}) {
25327
+ const taker = takerOf(message, options);
25328
+ const { project } = options;
25329
+ const rules = message.room?.rules ?? {};
25330
+ return [
25331
+ taker === void 0 ? void 0 : rules[taker],
25332
+ project === void 0 ? void 0 : renderProjectAsks(project, { othersInRoom: othersBeside(message, taker) }),
25333
+ options.sendsFiles === true ? sendingFiles({ project: project !== void 0 }) : void 0
25334
+ ].filter((part) => part !== void 0 && part !== "").join("\n\n");
25335
+ }
25268
25336
  function promptFor(message, options = {}) {
25269
- const taker = options.taker ?? (message.for?.length === 1 ? message.for[0] : void 0);
25337
+ const taker = takerOf(message, options);
25338
+ const held = options.rulesHeld === true;
25270
25339
  const notes = message.room?.notes ?? {};
25271
- const note = taker === void 0 ? void 0 : notes[taker];
25340
+ const note = taker === void 0 ? void 0 : held ? noteWithoutRules(message.room, taker) : notes[taker];
25272
25341
  const who = options.sayWho === false ? "" : `${withRole(message.from.display_name, message.from)}: `;
25273
25342
  const files = (message.attachments ?? []).map(renderAttachment);
25274
25343
  const words4 = [`${who}${message.text}`, ...files].join("\n");
25275
- const sending = options.sendsFiles === true ? sendingFiles({ project: options.project !== void 0 }) : void 0;
25344
+ const sending = options.sendsFiles === true && !held ? sendingFiles({ project: options.project !== void 0 }) : void 0;
25276
25345
  const replying = renderReplyingTo(message.replying_to, {
25277
25346
  sender: {
25278
25347
  id: message.from.participant_id,
@@ -25284,8 +25353,7 @@ function promptFor(message, options = {}) {
25284
25353
  at: message.ts,
25285
25354
  zone: options.zone
25286
25355
  });
25287
- const others = (message.room?.members ?? []).some((member) => member.kind === "agent" && member.participant_id !== taker);
25288
- const project = options.project === void 0 ? void 0 : renderProjectNote(options.project, { othersInRoom: others });
25356
+ const project = options.project === void 0 ? void 0 : held ? renderProjectFacts(options.project) : renderProjectNote(options.project, { othersInRoom: othersBeside(message, taker) });
25289
25357
  return [
25290
25358
  message.exchange?.note,
25291
25359
  note,
@@ -25472,7 +25540,10 @@ function nodeOnPath(env = process.env) {
25472
25540
  }
25473
25541
  var pause = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
25474
25542
  var LOAD_TRIES = 40;
25475
- var runOf = (options) => options.run ?? ((command) => void execFileSync2(command[0], command.slice(1), { stdio: "ignore" }));
25543
+ var runOf = (options) => options.run ?? ((command) => execFileSync2(command[0], command.slice(1), {
25544
+ encoding: "utf8",
25545
+ stdio: ["ignore", "pipe", "ignore"]
25546
+ }));
25476
25547
  var ours = (path) => !existsSync3(path) || readFileSync3(path, "utf8").includes(MARK);
25477
25548
  function runOne(command, run2, retryMs) {
25478
25549
  const tries = command[0] === "launchctl" && command[1] === "bootstrap" ? LOAD_TRIES : 1;
@@ -25495,6 +25566,24 @@ function runAll(commands, options) {
25495
25566
  runOne(command, run2, options.retryMs ?? 250);
25496
25567
  }
25497
25568
  var uidOf = (options) => options.uid ?? process.getuid?.() ?? 0;
25569
+ function lingers(options) {
25570
+ const run2 = runOf(options);
25571
+ const uid = String(uidOf(options));
25572
+ try {
25573
+ const on = run2(["loginctl", "show-user", uid, "--property=Linger", "--value"]);
25574
+ if (typeof on === "string" && on.trim() === "yes")
25575
+ return true;
25576
+ } catch {
25577
+ }
25578
+ try {
25579
+ run2(["loginctl", "enable-linger", uid]);
25580
+ return true;
25581
+ } catch {
25582
+ return false;
25583
+ }
25584
+ }
25585
+ var sayOf = (options) => options.say ?? ((line2) => void process.stderr.write(`${line2}
25586
+ `));
25498
25587
  function keepRunning(spec, options = {}) {
25499
25588
  if (machineOf(options.machine).env["CHIPS_KEEP"] === "off")
25500
25589
  return { state: "off" };
@@ -25509,6 +25598,9 @@ function keepRunning(spec, options = {}) {
25509
25598
  writeFileSync2(file3.path, file3.contents, { mode: 384 });
25510
25599
  chmodSync2(file3.path, 384);
25511
25600
  runAll(startCommands(file3.kind, spec.label, file3.path, uidOf(options)), options);
25601
+ if (file3.kind === "systemd" && !lingers(options)) {
25602
+ sayOf(options)("It stops when you log out. To keep it running on a machine nobody logs in to, run this once: sudo loginctl enable-linger $USER");
25603
+ }
25512
25604
  return { state: "kept", path: file3.path, logFile: file3.logFile };
25513
25605
  }
25514
25606
  function stopKeeping(label2, options = {}) {
@@ -27340,9 +27432,11 @@ function filesHere(project = false) {
27340
27432
  return `${sendingFiles({ project })} Hermes's note that this API server sends only pictures, and shows a MEDIA: tag on the runs endpoint as text, does not hold for these runs: they come from Chips, whose bridge takes each MEDIA: line out of your answer and attaches that file to your message, whatever its type, up to 100 MB.`;
27341
27433
  }
27342
27434
  var FILES_HERE = filesHere();
27343
- function instructionsFor(folder2, project = false) {
27435
+ var MARKDOWN_HERE = "Hermes's note that this API server's replies are plain text does not hold for these runs either: Chips shows your answer as Markdown, so use headings, lists, bold, tables and code blocks where they help a reader.";
27436
+ function instructionsFor(folder2, project = false, rules = "") {
27344
27437
  const files = filesHere(project);
27345
- return folder2 === void 0 ? files : `${files} Your own folder is ${folder2}: make and keep your files there, and run your commands from there.`;
27438
+ const here2 = folder2 === void 0 ? files : `${files} Your own folder is ${folder2}: make and keep your files there, and run your commands from there.`;
27439
+ return [here2, MARKDOWN_HERE, rules].filter((part) => part !== "").join("\n\n");
27346
27440
  }
27347
27441
  function freeFolder(root2, name2, taken) {
27348
27442
  const slug = name2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
@@ -27408,6 +27502,8 @@ var Bridge = class {
27408
27502
  options;
27409
27503
  agents = [];
27410
27504
  rooms = /* @__PURE__ */ new Map();
27505
+ /** The turns going, by their Chips run, for what is said in their rooms while they work (Chips ADR 0106). */
27506
+ working = /* @__PURE__ */ new Map();
27411
27507
  /** Every Chips agent of the harness's, configured here or not. */
27412
27508
  ours = /* @__PURE__ */ new Set();
27413
27509
  /** The harness's agents' names, by Chips id, for the folders named after them. */
@@ -27455,6 +27551,7 @@ var Bridge = class {
27455
27551
  }
27456
27552
  this.ours = /* @__PURE__ */ new Set([...known.map((agent) => agent.agentId), ...this.agents.map((agent) => agent.agentId)]);
27457
27553
  chips2.on("message.new", (message) => void this.hear(message));
27554
+ chips2.on("message.overheard", (message) => void this.overheard(message));
27458
27555
  chips2.on("agent.asked", (asked) => {
27459
27556
  this.making = this.making.then(() => this.make(asked)).catch((error62) => this.log(`a new agent went wrong: ${messageOf3(error62)}`));
27460
27557
  });
@@ -28071,8 +28168,20 @@ ${hook.lines}`);
28071
28168
  counted: 0,
28072
28169
  approvals: /* @__PURE__ */ new Map(),
28073
28170
  allowed: [],
28074
- interrupted: false
28171
+ interrupted: false,
28172
+ overheard: []
28075
28173
  };
28174
+ this.working.set(run2.id, turn);
28175
+ try {
28176
+ await this.take(turn, room, { message, speakerId, sayWho });
28177
+ } finally {
28178
+ this.working.delete(run2.id);
28179
+ }
28180
+ }
28181
+ /** A turn begun: Hermes's runs of it, from the first words to its end. */
28182
+ async take(turn, room, { message, speakerId, sayWho }) {
28183
+ const { agent } = room;
28184
+ const { run: run2 } = turn;
28076
28185
  run2.on("cancel", () => {
28077
28186
  turn.cancelled = true;
28078
28187
  if (turn.hermesRun !== "") void agent.client.stop(turn.hermesRun).catch(() => void 0);
@@ -28091,7 +28200,8 @@ ${hook.lines}`);
28091
28200
  const name2 = speakerId === void 0 ? agent.config.name : await this.nameOf(speakerId, agent.config.name);
28092
28201
  turn.folder = this.folderOf(agent, speakerId ?? agent.agentId, name2);
28093
28202
  const project = await this.projectFor(turn, room.spaceId, speakerId ?? agent.agentId, name2);
28094
- let text = promptFor(message, { taker: speakerId, sayWho, project });
28203
+ turn.rules = rulesFor(message, { taker: speakerId, project });
28204
+ let text = promptFor(message, { taker: speakerId, sayWho, project, rulesHeld: true });
28095
28205
  for (let round = 0; text !== void 0 && !turn.cancelled && !run2.over; round += 1) {
28096
28206
  const last = await this.once(turn, session, text, `${run2.id}:${round}`);
28097
28207
  if (last === void 0) return;
@@ -28117,19 +28227,53 @@ ${hook.lines}`);
28117
28227
  sessionId: session,
28118
28228
  ...this.runModel(turn),
28119
28229
  key,
28120
- instructions: instructionsFor(turn.folder, turn.project !== void 0)
28230
+ instructions: instructionsFor(turn.folder, turn.project !== void 0, turn.rules)
28121
28231
  });
28122
28232
  } catch (error62) {
28123
28233
  await this.failed(turn, `${agent.config.name} couldn't start in Hermes: ${messageOf3(error62)}`, true);
28124
28234
  return void 0;
28125
28235
  }
28126
28236
  if (turn.cancelled) await agent.client.stop(turn.hermesRun).catch(() => void 0);
28237
+ let last;
28127
28238
  try {
28128
- const last = await agent.client.events(turn.hermesRun, (event) => this.heard(turn, event));
28129
- return last ?? await this.endOf(turn);
28239
+ last = await agent.client.events(turn.hermesRun, (event) => this.heard(turn, event)) ?? await this.endOf(turn);
28130
28240
  } catch (error62) {
28131
28241
  this.log(`lost ${agent.config.name}'s run ${turn.hermesRun}: ${messageOf3(error62)}`);
28132
- return await this.endOf(turn);
28242
+ last = await this.endOf(turn);
28243
+ }
28244
+ await this.toldWhileWorking(turn, last);
28245
+ return last;
28246
+ }
28247
+ /**
28248
+ * Words said in a room, not to the harness, while its agents work there (Chips ADRs 0105, 0106):
28249
+ * each turn Chips names hears them as a steer of its Hermes run, which Hermes gives the model
28250
+ * after the step it is on. A run that won't take a steer is ending, and its agent's next turn is
28251
+ * handed the words.
28252
+ */
28253
+ async overheard(message) {
28254
+ const line2 = overheardLine(message);
28255
+ for (const { runId } of workingTurnsOf(message)) {
28256
+ const turn = this.working.get(runId);
28257
+ if (turn === void 0 || turn.run.over || turn.hermesRun === "") continue;
28258
+ try {
28259
+ await turn.agent.client.steer(turn.hermesRun, line2);
28260
+ turn.overheard.push({ messageId: message.message_id, line: line2 });
28261
+ } catch {
28262
+ }
28263
+ }
28264
+ }
28265
+ /**
28266
+ * Which of the words steered into a run it heard: those Hermes gave it before it completed, not
28267
+ * those it kept back as `pending_steer`, nor any in a run that failed or was stopped. Chips is
28268
+ * told of each, and the room shows the agent heard it while working (Chips ADR 0106).
28269
+ */
28270
+ async toldWhileWorking(turn, last) {
28271
+ const told = turn.overheard.splice(0);
28272
+ if (last?.event !== "run.completed") return;
28273
+ const kept = typeof last["pending_steer"] === "string" ? last["pending_steer"] : "";
28274
+ for (const { messageId, line: line2 } of told) {
28275
+ if (kept.includes(line2)) continue;
28276
+ await turn.run.heard(messageId).catch((error62) => this.log(`couldn't say a turn heard ${messageId}: ${messageOf3(error62)}`));
28133
28277
  }
28134
28278
  }
28135
28279
  /** The end of a run whose stream was lost: its state read until it is over, as a terminal event. */
@@ -28672,6 +28816,10 @@ async function checkHermes(config3, clientFor = (agent) => new HermesClient(agen
28672
28816
  }
28673
28817
  }
28674
28818
 
28819
+ // src/version.ts
28820
+ import { readFileSync as readFileSync11 } from "node:fs";
28821
+ var VERSION = JSON.parse(readFileSync11(new URL("../package.json", import.meta.url), "utf8")).version;
28822
+
28675
28823
  // src/main.ts
28676
28824
  var home = (path) => path.startsWith("~/") ? join13(homedir8(), path.slice(2)) : path;
28677
28825
  var link = process.env["CHIPS_JOIN_LINK"] || void 0;
@@ -28680,7 +28828,7 @@ stopIfAsked(LABEL);
28680
28828
  var configPath = resolve6(home(words3[0] ?? "~/.chips-hermes/config.json"));
28681
28829
  var folder = dirname6(configPath);
28682
28830
  var statePath = join13(folder, "sessions.json");
28683
- var state = existsSync11(statePath) ? JSON.parse(readFileSync11(statePath, "utf8")) : { sessions: {} };
28831
+ var state = existsSync11(statePath) ? JSON.parse(readFileSync12(statePath, "utf8")) : { sessions: {} };
28684
28832
  var saveState = (next) => {
28685
28833
  mkdirSync6(folder, { recursive: true, mode: 448 });
28686
28834
  writeFileSync5(statePath, `${JSON.stringify(next, null, 2)}
@@ -28741,7 +28889,7 @@ var joined = await join3({
28741
28889
  link,
28742
28890
  url: process.env["CHIPS_URL"],
28743
28891
  name,
28744
- software: "chips-hermes 1.0.0",
28892
+ software: `chips-hermes ${VERSION}`,
28745
28893
  wants: WANTS,
28746
28894
  onCode: (code) => log(`Allow this harness in Chips: ${code}`)
28747
28895
  }).catch(refused2);
@@ -28759,7 +28907,7 @@ var restart = restarter({
28759
28907
  var chips = await connect({
28760
28908
  url: joined.url,
28761
28909
  token: joined.token,
28762
- client: { name: "chips-hermes", version: "1.0.0" },
28910
+ client: { name: "chips-hermes", version: VERSION },
28763
28911
  store: fileStore(join13(folder, "chips-state.json")),
28764
28912
  onRestart: restart,
28765
28913
  settings: settingsFor(() => bridge)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chipschat/hermes",
3
- "version": "1.0.8",
3
+ "version": "1.0.11",
4
4
  "description": "Hermes Agent as a Chips harness, on its Runs API: each profile a Chips agent, each room a Hermes session.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",