@chipschat/hermes 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  # Third-party notices
2
2
 
3
- `dist/main.js` in @chipschat/hermes 1.0.0 carries these packages, bundled in. Each one's
3
+ `dist/main.js` in @chipschat/hermes 1.0.1 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
@@ -20231,6 +20231,27 @@ var projectToken = external_exports.object({
20231
20231
  expires_at: isoTimestamp,
20232
20232
  path: external_exports.string().describe("The project's path on this server, which the device puts its own address in front of.")
20233
20233
  });
20234
+ var projectFolderOpenAnswer = external_exports.object({
20235
+ macs: external_exports.int().nonnegative().describe("How many of the person's Macs were asked.")
20236
+ });
20237
+ var projectRun = external_exports.object({
20238
+ state: external_exports.enum(["never", "starting", "running", "ended", "failed", "stopped", "opened"]).describe("Never run on that Mac; starting (setup, or waiting for it to answer); running; ended by itself; failed; stopped by the person; or opened, for pages that need no starting."),
20239
+ command: external_exports.string().max(1100).optional().describe("What ran: `setup`, a newline, then `run`, or `open` and the file; the bar compares it with the recipe's."),
20240
+ since: isoTimestamp.optional(),
20241
+ ended: isoTimestamp.optional(),
20242
+ code: external_exports.int().optional().describe("How it ended, as its shell said."),
20243
+ opened: external_exports.string().max(600).optional().describe("What it opened, once it answered."),
20244
+ lines: external_exports.array(external_exports.string().max(400)).max(20).optional().describe("Its last lines, when it ended."),
20245
+ why: external_exports.string().max(400).optional().describe("Why the Mac didn't run it when it was last asked, in its words: no run file, or a different one.")
20246
+ });
20247
+ var heldRun = projectRun.extend({
20248
+ mac: external_exports.string().min(1).max(120).describe("The name of the Mac it runs on."),
20249
+ said_at: isoTimestamp
20250
+ });
20251
+ var projectRunAnswer = external_exports.object({
20252
+ run: heldRun.optional(),
20253
+ macs: external_exports.int().nonnegative()
20254
+ });
20234
20255
  var commitId = external_exports.string().regex(/^[0-9a-f]{40}(?:[0-9a-f]{24})?$/).describe("A commit, by its full git object id.");
20235
20256
  var PROJECT_CHANGED_FILES = 200;
20236
20257
  var projectChangedFile = external_exports.object({
@@ -20261,8 +20282,9 @@ function isSecretsFile(path) {
20261
20282
  }
20262
20283
  var SECRET_SHAPES = [
20263
20284
  { what: "a Chips token", source: "chips_(?:at|inv|j|pt)_[A-Za-z0-9_-]{20,}" },
20264
- { what: "an Anthropic key", source: "sk-ant-[A-Za-z0-9_-]{20,}" },
20265
- { what: "an OpenAI key", source: "sk-(?:proj-|svcacct-|admin-)?[A-Za-z0-9_-]{40,}" },
20285
+ // `sk-` begins a word, so a link's words ("…/elon-musk-terafab-project-…") are never a key.
20286
+ { what: "an Anthropic key", source: "\\bsk-ant-[A-Za-z0-9_-]{20,}" },
20287
+ { what: "an OpenAI key", source: "\\bsk-(?:proj-|svcacct-|admin-)?[A-Za-z0-9_-]{40,}" },
20266
20288
  { what: "an AWS access key", source: "\\b(?:AKIA|ASIA)[0-9A-Z]{16}\\b" },
20267
20289
  {
20268
20290
  what: "a GitHub token",
@@ -20332,6 +20354,7 @@ function asks(facts, options) {
20332
20354
  "When your file and another's use different names for the same thing, the one that reached the project first keeps its names, and the other changes its names to match."
20333
20355
  );
20334
20356
  }
20357
+ lines2.push("When what you make is to be read (pages, PDFs, pictures), keep a start page at .chips/index.html that links to the documents worth seeing (its links start ../), with the documents themselves at the top or in folders of their own, and bring it up to date whenever you add, rename or replace one. Chips opens it in the app, and its links open there too. One document needs no start page.", `When what you make has to run on the person's Mac (a server, a script, an app), keep .chips/run.json so Run in Chips can start it from the project's folder: {"run": "the one shell command that starts it", "opens": "http://localhost:<port>, to open once it answers", "setup": "a command run first when it changes, like npm install"}. Start an app by its own program rather than with open, so Stop can close it. Leave out what it doesn't need, and check it works in your copy before you say it does.`);
20335
20358
  if (facts.markdown)
20336
20359
  lines2.push("In Markdown, put each sentence on a line of its own.");
20337
20360
  if (facts.pages === true) {
@@ -21562,7 +21585,7 @@ var clientHelloFrame = external_exports.object({
21562
21585
  autonomy: autonomyLevel.optional().describe("The level the harness believes it has.")
21563
21586
  })).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."),
21564
21587
  since: external_exports.int().nonnegative().optional().describe("Not read by Chips: a stream resumes from `subscribe`'s `since`."),
21565
- 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); one that says nothing is sent what it was always sent, since an unknown frame is dropped in silence."),
21588
+ 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."),
21566
21589
  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.")
21567
21590
  }).describe("The first frame a client sends.");
21568
21591
  var subscribeFrame = external_exports.object({
@@ -21623,6 +21646,26 @@ var restartFrame = external_exports.object({
21623
21646
  asked_by: id("person").describe("The person who asked."),
21624
21647
  asked_at: isoTimestamp.describe("When they asked.")
21625
21648
  }).describe("A person asks the harness to restart (ADR 0094). Sent to a harness's socket that said `restart` in its `hello`, now or never: it is not in the log, so a reconnect never hears it again. What runs is the harness's own restart.");
21649
+ var folderOpenFrame = external_exports.object({
21650
+ frame: external_exports.literal("folder.open"),
21651
+ space_id: id("space").describe("The room whose folder, or page, to open."),
21652
+ what: external_exports.enum(["folder", "page"]).optional().describe("`page`: open the room's start page, or its one document, in Chips for Mac (ADR 0227). Left out, the folder in Finder."),
21653
+ asked_from: external_exports.string().min(1).max(120).describe("The name of the device that asked: `iPhone`."),
21654
+ asked_at: isoTimestamp.describe("When they asked.")
21655
+ }).describe("A person asks their own Mac to open a room's folder in Finder (ADR 0225), or its page in Chips for Mac (ADR 0227), from another of their devices. Sent to that person's sockets that said `folders` in their `hello`, now or never: it is not in the log. The Mac finds the folder and the page itself: nothing here is a path.");
21656
+ var folderRunFrame = external_exports.object({
21657
+ frame: external_exports.literal("folder.run"),
21658
+ space_id: id("space").describe("The room whose project to run, or stop."),
21659
+ op: external_exports.enum(["run", "stop"]),
21660
+ command: external_exports.string().min(1).max(1100).optional().describe("For `run`, what the asking device showed would run: the Mac runs nothing else, and says so when its folder's run file says something different."),
21661
+ asked_from: external_exports.string().min(1).max(120).describe("The name of the device that asked: `iPhone`."),
21662
+ asked_at: isoTimestamp.describe("When they asked.")
21663
+ }).describe("A person asks their own Mac to run a room's project as its run file says, or to stop it (ADR 0226), from another of their devices. Sent to that person's sockets that said `folders` in their `hello`, now or never: it is not in the log. Nothing here is a command: the Mac reads it from the room's folder.");
21664
+ var folderRunStateFrame = external_exports.object({
21665
+ frame: external_exports.literal("folder.run_state"),
21666
+ space_id: id("space"),
21667
+ run: heldRun
21668
+ }).describe("How a person's run of a room's project on their Mac stands (ADR 0226), as that Mac said it. Sent to that person's sockets that said `folder_runs` in their `hello`, never to anyone else's, now or never: `GET /v1/spaces/:id/project/run` reads it again.");
21626
21669
  var joinFrame = external_exports.object({
21627
21670
  frame: external_exports.literal("join"),
21628
21671
  request: joinRequestView
@@ -21665,6 +21708,9 @@ var serverFrame = external_exports.discriminatedUnion("frame", [
21665
21708
  presenceFrame,
21666
21709
  restartFrame,
21667
21710
  joinFrame,
21711
+ folderOpenFrame,
21712
+ folderRunFrame,
21713
+ folderRunStateFrame,
21668
21714
  contextEditFrame,
21669
21715
  modelChangeFrame,
21670
21716
  heartbeatChangeFrame,
@@ -21948,6 +21994,7 @@ import { existsSync } from "node:fs";
21948
21994
  import { mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
21949
21995
  import { join } from "node:path";
21950
21996
  var READ_UP_TO = 4 * 1024 * 1024;
21997
+ var MARKED_LINES = 14;
21951
21998
  var TRIES = 4;
21952
21999
  var MARKS = /^(<<<<<<<|>>>>>>>) /m;
21953
22000
  var REJECTED = /\[rejected\]|fetch first|non-fast-forward/;
@@ -21986,6 +22033,27 @@ function pushedInWords(pushed) {
21986
22033
  return `not sent: ${pushed.why}`;
21987
22034
  }
21988
22035
  }
22036
+ var stopped = (words4) => `${words4.trim().replace(/[.\s]+$/, "")}.`;
22037
+ function heldNote(pushed, name2) {
22038
+ switch (pushed.state) {
22039
+ case "sent":
22040
+ case "nothing":
22041
+ return void 0;
22042
+ case "clash": {
22043
+ const whose = pushed.with.length === 0 ? "another change" : `${namesInWords(pushed.with)}'s change`;
22044
+ return `(Not in the room's project yet: ${namesInWords(pushed.files)} clashed with ${whose}. ${name2} is asked to settle it next turn.)`;
22045
+ }
22046
+ case "keys": {
22047
+ const [first] = pushed.keys;
22048
+ const held = pushed.keys.length === 1 && first !== void 0 ? `${first.path} looks like it holds ${first.what.replace(/, by its name$/, "")}` : `${namesInWords(pushed.keys.map((key) => key.path))} look like they hold keys`;
22049
+ return `(Not in the room's project: ${held}, so nothing was sent.)`;
22050
+ }
22051
+ case "refused":
22052
+ return `(Not in the room's project: it was turned back: ${stopped(pushed.why)})`;
22053
+ case "failed":
22054
+ return `(Not in the room's project: it couldn't be sent: ${stopped(pushed.why)})`;
22055
+ }
22056
+ }
21989
22057
  async function checkGit(git2 = "git", path = process.env["PATH"] ?? "/usr/bin:/bin") {
21990
22058
  const [major, minor] = versionIn(PROJECT_GIT_VERSION) ?? [0, 0];
21991
22059
  return new Promise((resolve5) => {
@@ -22005,7 +22073,7 @@ var Project = class {
22005
22073
  remote;
22006
22074
  constructor(options) {
22007
22075
  this.options = options;
22008
- this.copy = join(options.folder, "room");
22076
+ this.copy = options.person === true ? options.folder : join(options.folder, "room");
22009
22077
  this.remote = `${options.url.replace(/\/$/, "").replace(/^ws/, "http")}${projectPath(options.spaceId)}`;
22010
22078
  }
22011
22079
  /** Before a turn: the copy made or brought up to date, and what the turn is to be told of it. */
@@ -22059,6 +22127,121 @@ var Project = class {
22059
22127
  }
22060
22128
  return { state: "failed", why: "others kept sending first." };
22061
22129
  }
22130
+ /**
22131
+ * A person's folder made, or brought up to the room's `main` while nothing in it is theirs: no
22132
+ * file changed or added, nothing waiting to be sent, no clash (ADR 0224). Once something is
22133
+ * theirs, nothing in the folder changes; what others changed since is told instead.
22134
+ */
22135
+ async look() {
22136
+ await this.ready();
22137
+ const fetched = await this.git(["fetch", "--quiet", "origin"]);
22138
+ const trouble = fetched.ok ? void 0 : remoteWords(fetched.err);
22139
+ const remote = await this.head("refs/remotes/origin/main");
22140
+ await this.settledAsTheirs(remote);
22141
+ const brought = await this.bringUntouched(remote);
22142
+ return { ...await this.standing(remote), ...brought, trouble };
22143
+ }
22144
+ /** The folder moved on to the room's `main` while nothing in it is theirs: what came in, whose. */
22145
+ async bringUntouched(remote) {
22146
+ const mine = await this.head("HEAD");
22147
+ const none = { came: [], cameBy: [] };
22148
+ if (remote === "" || mine === remote || !await this.untouched(mine, remote))
22149
+ return none;
22150
+ const moved = mine === "" ? await this.git(["checkout", "--quiet", "-B", PROJECT_BRANCH, remote]) : await this.git(["merge", "--quiet", "--ff-only", remote]);
22151
+ if (!moved.ok)
22152
+ return none;
22153
+ const from = mine === "" ? await this.emptyTree() : mine;
22154
+ return {
22155
+ came: lines((await this.git(["diff", "--name-only", from, remote])).out),
22156
+ cameBy: await this.authors(mine === "" ? remote : `${mine}..${remote}`)
22157
+ };
22158
+ }
22159
+ /**
22160
+ * A clash the person settled by taking the room's side whole: nothing of theirs is left to send,
22161
+ * so the folder goes back to the room's `main` and is kept up to date again. Every tracked file
22162
+ * is already the room's, and a file git doesn't track is never touched.
22163
+ */
22164
+ async settledAsTheirs(remote) {
22165
+ if (remote === "" || !existsSync(join(this.copy, ".git", "MERGE_HEAD")))
22166
+ return;
22167
+ if ((await this.marked()).length > 0)
22168
+ return;
22169
+ if (!(await this.git(["diff", "--quiet", remote])).ok)
22170
+ return;
22171
+ await this.git(["reset", "--quiet", "--hard", remote]);
22172
+ }
22173
+ /** Whether nothing in the folder is the person's own: no change, no unsent commit, no clash. */
22174
+ async untouched(mine, remote) {
22175
+ if (existsSync(join(this.copy, ".git", "MERGE_HEAD")))
22176
+ return false;
22177
+ const status = await this.git(["status", "--porcelain", "--untracked-files=all"]);
22178
+ if (!status.ok || status.out.trim() !== "")
22179
+ return false;
22180
+ if (mine === "")
22181
+ return true;
22182
+ return (await this.git(["merge-base", "--is-ancestor", mine, remote])).ok;
22183
+ }
22184
+ /** What the person changed, a clash, and what others changed that the folder hasn't. */
22185
+ async standing(remote) {
22186
+ const mine = await this.head("HEAD");
22187
+ const base = await this.baseOf(mine, remote);
22188
+ const counted = lines((await this.git(["diff", "--numstat", "--no-renames", "-z", base])).out.replace(/\0/g, "\n")).map((line) => {
22189
+ const [added2 = "0", removed = "0", ...path] = line.split(" ");
22190
+ return { path: path.join(" "), added: Number(added2) || 0, removed: Number(removed) || 0 };
22191
+ });
22192
+ const added = lines((await this.git(["ls-files", "--others", "--exclude-standard", "-z"])).out.replace(/\0/g, "\n"));
22193
+ const fresh = await Promise.all(added.map(async (path) => ({ path, added: await this.lineCount(path), removed: 0 })));
22194
+ const changed = [...counted, ...fresh].filter((change) => change.path !== "").sort((a, b) => a.path.localeCompare(b.path));
22195
+ const clashes = await this.marked();
22196
+ const clashedWith = await this.mergingWith();
22197
+ const first = clashes[0];
22198
+ const marked = first === void 0 ? void 0 : await this.markedLines(first);
22199
+ const since = remote === "" || remote === base ? [] : lines((await this.git(["diff", "--name-only", base, remote])).out);
22200
+ const by = since.length === 0 ? [] : await this.authors(mine === "" ? remote : `${base}..${remote}`);
22201
+ return { changed, clashes, clashedWith, marked, since, by };
22202
+ }
22203
+ /**
22204
+ * What the person's changes are measured from: where the folder and the room's `main` parted.
22205
+ * In a clash the room's side is already in the folder, so it is `main` itself: what differs from
22206
+ * it is what Send would change.
22207
+ */
22208
+ async baseOf(mine, remote) {
22209
+ if (mine === "")
22210
+ return this.emptyTree();
22211
+ if (remote === "")
22212
+ return mine;
22213
+ if (existsSync(join(this.copy, ".git", "MERGE_HEAD")))
22214
+ return remote;
22215
+ return (await this.git(["merge-base", mine, remote])).out.trim() || mine;
22216
+ }
22217
+ /** How many lines a file the person added has: the lines it adds. None for a picture. */
22218
+ async lineCount(path) {
22219
+ try {
22220
+ const at = join(this.copy, path);
22221
+ if ((await stat(at)).size > READ_UP_TO)
22222
+ return 0;
22223
+ const words4 = await readFile(at, "utf8");
22224
+ if (words4.includes("\0"))
22225
+ return 0;
22226
+ return words4 === "" ? 0 : words4.split("\n").length - (words4.endsWith("\n") ? 1 : 0);
22227
+ } catch {
22228
+ return 0;
22229
+ }
22230
+ }
22231
+ /** The first clash in a file, from the line before its first mark to the line after its last. */
22232
+ async markedLines(path) {
22233
+ try {
22234
+ const all = (await readFile(join(this.copy, path), "utf8")).split("\n");
22235
+ const start = all.findIndex((line) => line.startsWith("<<<<<<< "));
22236
+ const end2 = all.findIndex((line, at) => at > start && line.startsWith(">>>>>>> "));
22237
+ if (start < 0 || end2 < 0)
22238
+ return void 0;
22239
+ const shown2 = all.slice(Math.max(0, start - 1), end2 + 2);
22240
+ return shown2.length <= MARKED_LINES ? shown2.join("\n") : [...shown2.slice(0, MARKED_LINES - 1), "\u2026"].join("\n");
22241
+ } catch {
22242
+ return void 0;
22243
+ }
22244
+ }
22062
22245
  /**
22063
22246
  * One try at sending: sent, nothing to send, or turned back; or, when somebody sent first, the
22064
22247
  * agent's commits replayed on top of theirs, for the next try, unless they clashed.
@@ -22111,6 +22294,10 @@ ${sent.err}`;
22111
22294
  const pointed = (await this.git(["remote", "get-url", "origin"])).out.trim();
22112
22295
  if (pointed === this.remote)
22113
22296
  return false;
22297
+ if (this.options.person === true) {
22298
+ await this.git(["remote", "set-url", "origin", this.remote]);
22299
+ return false;
22300
+ }
22114
22301
  const was = /\/v1\/spaces\/([^/]+)\/project\.git$/.exec(pointed)?.[1] ?? "before";
22115
22302
  await rename(this.copy, join(this.options.folder, `room-${was}`));
22116
22303
  }
@@ -22264,9 +22451,12 @@ ${sent.err}`;
22264
22451
  }
22265
22452
  git(args) {
22266
22453
  const { options } = this;
22454
+ const person = options.person === true;
22455
+ const email3 = `${options.agentId}@${person ? "people" : "agents"}.chips.invalid`;
22267
22456
  const pairs = [
22268
22457
  ["http.extraHeader", `Authorization: Bearer ${options.token}`],
22269
- ["http.extraHeader", `${PROJECT_ACTOR_HEADER}: ${options.agentId}`],
22458
+ // A person's project token says who they are; an agent's harness token needs the agent.
22459
+ ...person ? [] : [["http.extraHeader", `${PROJECT_ACTOR_HEADER}: ${options.agentId}`]],
22270
22460
  ["core.hooksPath", "/dev/null"],
22271
22461
  ["commit.gpgSign", "false"],
22272
22462
  ["merge.conflictStyle", "merge"],
@@ -22279,9 +22469,9 @@ ${sent.err}`;
22279
22469
  GIT_CONFIG_GLOBAL: "/dev/null",
22280
22470
  GIT_TERMINAL_PROMPT: "0",
22281
22471
  GIT_AUTHOR_NAME: options.name,
22282
- GIT_AUTHOR_EMAIL: `${options.agentId}@agents.chips.invalid`,
22472
+ GIT_AUTHOR_EMAIL: email3,
22283
22473
  GIT_COMMITTER_NAME: options.name,
22284
- GIT_COMMITTER_EMAIL: `${options.agentId}@agents.chips.invalid`,
22474
+ GIT_COMMITTER_EMAIL: email3,
22285
22475
  GIT_EDITOR: "true",
22286
22476
  GIT_CONFIG_COUNT: String(pairs.length)
22287
22477
  };
@@ -23990,7 +24180,11 @@ import { readFile as readFile3, realpath, stat as stat2 } from "node:fs/promises
23990
24180
  import { homedir as homedir2 } from "node:os";
23991
24181
  import { basename as basename2, isAbsolute, resolve, sep } from "node:path";
23992
24182
  import { fileURLToPath } from "node:url";
23993
- var SENDING_FILES = "A file reaches this room only when you send it: put MEDIA:<path> on a line of its own in your answer, one line for each file, from your own folder. A path, or a link to this machine, reaches nobody: the people and agents here cannot open your folder.";
24183
+ function sendingFiles(options = {}) {
24184
+ const which = options.project === true ? "A file that isn't in the room's project" : "A file";
24185
+ return `${which} reaches this room only when you send it: put MEDIA:<path> on a line of its own in your answer, one line for each file, from your own folder. A path, or a link to this machine, reaches nobody: the people and agents here cannot open your folder.`;
24186
+ }
24187
+ var SENDING_FILES = sendingFiles();
23994
24188
  var MOST_FILES = 20;
23995
24189
  var MOST_FILE_BYTES = 100 * 1024 * 1024;
23996
24190
  function filesNamedIn(answer) {
@@ -24098,7 +24292,7 @@ function promptFor(message, options = {}) {
24098
24292
  const who = options.sayWho === false ? "" : `${withRole(message.from.display_name, message.from)}: `;
24099
24293
  const files = (message.attachments ?? []).map(renderAttachment);
24100
24294
  const words4 = [`${who}${message.text}`, ...files].join("\n");
24101
- const sending = options.sendsFiles === true ? SENDING_FILES : void 0;
24295
+ const sending = options.sendsFiles === true ? sendingFiles({ project: options.project !== void 0 }) : void 0;
24102
24296
  const replying = renderReplyingTo(message.replying_to, {
24103
24297
  sender: { id: message.from.participant_id, name: message.from.display_name },
24104
24298
  taker,
@@ -24576,9 +24770,13 @@ async function failureOf(answer) {
24576
24770
 
24577
24771
  // src/bridge.ts
24578
24772
  var WANTS = ["agents", "new_agents", "forks"];
24579
- var FILES_HERE = `${SENDING_FILES} 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.`;
24580
- function instructionsFor(folder2) {
24581
- return folder2 === void 0 ? FILES_HERE : `${FILES_HERE} Your own folder is ${folder2}: make and keep your files there, and run your commands from there.`;
24773
+ function filesHere(project = false) {
24774
+ 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.`;
24775
+ }
24776
+ var FILES_HERE = filesHere();
24777
+ function instructionsFor(folder2, project = false) {
24778
+ const files = filesHere(project);
24779
+ return folder2 === void 0 ? files : `${files} Your own folder is ${folder2}: make and keep your files there, and run your commands from there.`;
24582
24780
  }
24583
24781
  function freeFolder(root, name2, taken) {
24584
24782
  const slug = name2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
@@ -24800,18 +24998,22 @@ var Bridge = class {
24800
24998
  }
24801
24999
  /**
24802
25000
  * What the turn changed in its copy, sent to the room's project before its words are posted: the
24803
- * commit it left `main` at, for the reply to carry (Chips ADR 0222), when it sent one.
25001
+ * commit it left `main` at, for the reply to carry (Chips ADR 0222), when it sent one; else, when
25002
+ * the change didn't go, the line the reply says so in (Chips AP-6).
24804
25003
  */
24805
25004
  async sendProject(turn, words4) {
24806
- if (turn.project === void 0) return void 0;
25005
+ if (turn.project === void 0) return {};
24807
25006
  const { copy, name: name2 } = turn.project;
24808
25007
  try {
24809
25008
  const pushed = await copy.push({ words: words4 });
24810
25009
  this.log(`${name2}'s project: ${pushedInWords(pushed)}`);
24811
- return pushed.state === "sent" ? pushed.commit : void 0;
25010
+ if (pushed.state === "sent") return { change: pushed.commit };
25011
+ const note = heldNote(pushed, name2);
25012
+ return note === void 0 ? {} : { note };
24812
25013
  } catch (error61) {
24813
25014
  this.log(`${name2}'s project couldn't be sent: ${messageOf(error61)}`);
24814
- return void 0;
25015
+ const note = heldNote({ state: "failed", why: messageOf(error61) }, name2);
25016
+ return note === void 0 ? {} : { note };
24815
25017
  }
24816
25018
  }
24817
25019
  /** Trouble while a profile's gateway doesn't answer, and fine once all do (ADR 0083). */
@@ -24979,7 +25181,7 @@ var Bridge = class {
24979
25181
  async once(turn, session, text, key) {
24980
25182
  const { agent } = turn;
24981
25183
  try {
24982
- turn.hermesRun = await agent.client.startRun({ text, sessionId: session, model: agent.config.model, key, instructions: instructionsFor(turn.folder) });
25184
+ turn.hermesRun = await agent.client.startRun({ text, sessionId: session, model: agent.config.model, key, instructions: instructionsFor(turn.folder, turn.project !== void 0) });
24983
25185
  } catch (error61) {
24984
25186
  await this.failed(turn, `${agent.config.name} couldn't start in Hermes: ${messageOf(error61)}`, true);
24985
25187
  return void 0;
@@ -25142,8 +25344,8 @@ var Bridge = class {
25142
25344
  ${command}
25143
25345
  \`\`\`` } }
25144
25346
  });
25145
- const stopped = new Promise((resolve5) => turn.run.on("cancel", () => resolve5(void 0)));
25146
- const decision = await Promise.race([asked, stopped]);
25347
+ const stopped2 = new Promise((resolve5) => turn.run.on("cancel", () => resolve5(void 0)));
25348
+ const decision = await Promise.race([asked, stopped2]);
25147
25349
  const pending = turn.approvals.get(requestId ?? approvalId);
25148
25350
  if (decision === void 0 || pending === void 0 || pending.answered) return;
25149
25351
  pending.answered = true;
@@ -25186,10 +25388,11 @@ ${command}
25186
25388
  await this.counted(turn, last);
25187
25389
  const output2 = typeof last["output"] === "string" ? last["output"].trim() : "";
25188
25390
  const answer = output2 !== "" ? output2 : turn.reply.trim();
25189
- const change = await this.sendProject(turn, answer);
25391
+ const { change, note } = await this.sendProject(turn, answer);
25190
25392
  const { folder: folder2 } = turn;
25191
25393
  const { chips: chips2 } = this.options;
25192
- const post = await sendAnswer(answer, {
25394
+ const words4 = [answer, note ?? ""].filter((part) => part !== "").join("\n\n");
25395
+ const post = await sendAnswer(words4, {
25193
25396
  folders: folder2 === void 0 ? [] : [folder2],
25194
25397
  upload: (input2) => chips2.upload(input2)
25195
25398
  });
@@ -25331,9 +25534,9 @@ var end = (line, code) => {
25331
25534
  };
25332
25535
  function stopIfAsked(label2) {
25333
25536
  if (!flags.includes("--stop")) return;
25334
- const stopped = stopKeeping(label2);
25335
- if (stopped.state === "stopped") end(`Stopped ${label2}, and removed ${stopped.path}.`, 0);
25336
- if (stopped.state === "theirs") end(`${stopped.path} wasn't written by this bridge: it was left as it is.`, 1);
25537
+ const stopped2 = stopKeeping(label2);
25538
+ if (stopped2.state === "stopped") end(`Stopped ${label2}, and removed ${stopped2.path}.`, 0);
25539
+ if (stopped2.state === "theirs") end(`${stopped2.path} wasn't written by this bridge: it was left as it is.`, 1);
25337
25540
  end(`Nothing called ${label2} was kept running here.`, 0);
25338
25541
  }
25339
25542
  function keepIfAsked(label2, url2, names, log2, before = () => void 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chipschat/hermes",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
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",