From 30ba34f41176426411ddb52dab3e01c3e50122ee Mon Sep 17 00:00:00 2001
From: iris
Date: Sun, 18 Oct 2026 15:59:19 +0200
Subject: [PATCH 1/3] swarm-ui: let the create-agent form set the new agent's
initial AGENTS.md
The form gets a collapsed "instructions" section with a plain text box. Its
text goes in the optional agents_md field of the create request, and is left
out while the box is empty or only whitespace. The controller's size refusal
shows under the box instead of in the form's general error panel. The text
box empties after a successful create.
The send and refusal logic sits in its own module with tests, and
docs/swarm/ui.md describes the field.
---
docs/swarm/ui.md | 13 ++++++
.../swarm-ui/src/pages/CreateAgentAgentsMd.ts | 23 ++++++++++
.../swarm-ui/src/pages/CreateAgentForm.css | 5 +++
.../swarm-ui/src/pages/CreateAgentForm.tsx | 31 +++++++++++--
.../swarm-ui/test/create-agents-md.test.mjs | 43 +++++++++++++++++++
5 files changed, 112 insertions(+), 3 deletions(-)
create mode 100644 frontend/packages/swarm-ui/src/pages/CreateAgentAgentsMd.ts
create mode 100644 frontend/packages/swarm-ui/test/create-agents-md.test.mjs
diff --git a/docs/swarm/ui.md b/docs/swarm/ui.md
index 0bf00d7d..038efc3b 100644
--- a/docs/swarm/ui.md
+++ b/docs/swarm/ui.md
@@ -386,6 +386,19 @@ controller puts back what it replaced. Each agent name keys its settings and
destroying an agent doesn't clear them, so a name set up before keeps its
stored settings when you leave the fields empty.
+### Initial AGENTS.md at creation
+
+A third collapsed section, **instructions**, holds a plain text box for the
+text the new agent's `AGENTS.md` starts with. Left empty, or holding only
+whitespace, it sends nothing and the agent starts without the file. Otherwise
+the text goes as typed in the optional `agents_md` field of `POST /api/agents`.
+The agent writes it on its first start, and only when it has no `AGENTS.md`
+yet, so creating an agent again under a name that has one never replaces it.
+The controller refuses a text over 64 KiB; its reason shows under the box and
+no agent is created. After a successful create the box empties. The
+[instructions editor](#editing-an-agents-instructions) changes the file later.
+The [swarm README](README.md#an-agents-agentsmd) has the wire details.
+
### Linking external accounts
Each agent on `/agents` opens three dialogs that write a credential for it
diff --git a/frontend/packages/swarm-ui/src/pages/CreateAgentAgentsMd.ts b/frontend/packages/swarm-ui/src/pages/CreateAgentAgentsMd.ts
new file mode 100644
index 00000000..94c4c04e
--- /dev/null
+++ b/frontend/packages/swarm-ui/src/pages/CreateAgentAgentsMd.ts
@@ -0,0 +1,23 @@
+// The create-agent form's initial `AGENTS.md` field, kept out of the
+// component so `npm test` can run it.
+import type { ProblemDetails } from "@hive/shared/api-error.js";
+
+/**
+ * The `agents_md` to send with a create: nothing while the box is empty or
+ * holds only whitespace, so the agent starts without the file; otherwise the
+ * text as typed.
+ */
+export function agentsMdBody(text: string): { agents_md?: string } {
+ return text.trim() === "" ? {} : { agents_md: text };
+}
+
+/**
+ * The controller's refusal of the text, when `problem` is one: a 400 whose
+ * detail starts with the field's name. It shows beside the box, so any other
+ * failure stays with the form's general error.
+ */
+export function agentsMdRefusal(problem: ProblemDetails): string | null {
+ return problem.status === 400 && problem.detail?.startsWith("agents_md ")
+ ? problem.detail
+ : null;
+}
diff --git a/frontend/packages/swarm-ui/src/pages/CreateAgentForm.css b/frontend/packages/swarm-ui/src/pages/CreateAgentForm.css
index e6e2ac3a..5e131ef1 100644
--- a/frontend/packages/swarm-ui/src/pages/CreateAgentForm.css
+++ b/frontend/packages/swarm-ui/src/pages/CreateAgentForm.css
@@ -69,6 +69,11 @@
.create-agent-result-ok {
color: var(--green);
}
+/* The controller's refusal of the AGENTS.md text, under its box. */
+.create-agent-field-error {
+ margin: 0;
+ color: var(--red);
+}
/* The picked archetype's description, under its select. */
.create-agent-archetype-description {
margin: 0;
diff --git a/frontend/packages/swarm-ui/src/pages/CreateAgentForm.tsx b/frontend/packages/swarm-ui/src/pages/CreateAgentForm.tsx
index 3074c765..d27bbd8d 100644
--- a/frontend/packages/swarm-ui/src/pages/CreateAgentForm.tsx
+++ b/frontend/packages/swarm-ui/src/pages/CreateAgentForm.tsx
@@ -38,6 +38,7 @@ import {
SelectField,
type SelectOption,
} from "../ui/select-field/SelectField.js";
+import { TextAreaField } from "../ui/text-area-field/TextAreaField.js";
import { Button } from "../ui/button/Button.js";
import { capsBody, LimitsFields } from "../agent/detail/AgentLimits.js";
import {
@@ -60,6 +61,7 @@ import {
requestArchetype,
type ArchetypeList,
} from "./CreateAgentArchetype.js";
+import { agentsMdBody, agentsMdRefusal } from "./CreateAgentAgentsMd.js";
import "./CreateAgentForm.css";
// Mirrors swarm-controller's `HiveEntry` — same shape `HivesPage`
@@ -144,6 +146,8 @@ export function CreateAgentForm({
null,
);
const [archetype, setArchetype] = useState("");
+ // The text the agent's `AGENTS.md` starts with; empty sends none.
+ const [agentsMd, setAgentsMd] = useState("");
const picked = archetypes?.archetypes.find(
(a) => archetypeKey(a) === archetype,
);
@@ -211,6 +215,9 @@ export function CreateAgentForm({
label: h.name,
}));
const hivesReady = hives !== null && hives.length > 0;
+ // The controller's size refusal of the AGENTS.md text, shown by its box.
+ const refusal =
+ result.status === "error" ? agentsMdRefusal(result.problem) : null;
async function submit(e: Event) {
e.preventDefault();
@@ -232,6 +239,7 @@ export function CreateAgentForm({
? requestPermissions(selected, catalogue.default)
: undefined,
archetype: requestArchetype(picked),
+ ...agentsMdBody(agentsMd),
}),
});
if (!r.ok) {
@@ -241,6 +249,7 @@ export function CreateAgentForm({
const data = (await r.json()) as CreateAgentResponse;
setResult({ status: "done", nodeId: data.node_id });
setName("");
+ setAgentsMd("");
pickArchetype("");
if (catalogue)
setSelected(
@@ -258,8 +267,8 @@ export function CreateAgentForm({
Create a new agent's swarm-level identity. This only queues the job
— check jobs to watch it settle. Resource
- caps and inference are optional; leave them empty to set nothing. A
- name set up before keeps the settings stored for it.
+ caps, inference and instructions are optional; leave them empty to
+ set nothing. A name set up before keeps the settings stored for it.
{hivesError && (
+
+ instructions
+
+
+ {refusal !== null && (
+
{refusal}
+ )}
+
+
)}
- {result.status === "error" && (
+ {result.status === "error" && refusal === null && (
)}
diff --git a/frontend/packages/swarm-ui/test/create-agents-md.test.mjs b/frontend/packages/swarm-ui/test/create-agents-md.test.mjs
new file mode 100644
index 00000000..1edc8218
--- /dev/null
+++ b/frontend/packages/swarm-ui/test/create-agents-md.test.mjs
@@ -0,0 +1,43 @@
+// What the create-agent form sends for the initial AGENTS.md, and which error
+// it shows beside the box. Run by `npm test`, which Node runs with its
+// built-in test runner and type stripping, so the `.ts` module is imported as
+// it is.
+import assert from "node:assert/strict";
+import { test } from "node:test";
+import {
+ agentsMdBody,
+ agentsMdRefusal,
+} from "../src/pages/CreateAgentAgentsMd.ts";
+
+test("an empty or whitespace-only box sends no agents_md", () => {
+ assert.deepEqual(agentsMdBody(""), {});
+ assert.deepEqual(agentsMdBody(" \n\t \n"), {});
+});
+
+test("text is sent as typed, whitespace and all", () => {
+ assert.deepEqual(agentsMdBody("# me\n\nDo the thing.\n"), {
+ agents_md: "# me\n\nDo the thing.\n",
+ });
+ assert.deepEqual(agentsMdBody(" indented"), { agents_md: " indented" });
+});
+
+test("the size refusal shows beside the box", () => {
+ const detail = "agents_md is 70000 bytes; the limit is 65536";
+ assert.equal(agentsMdRefusal({ status: 400, detail }), detail);
+});
+
+test("other failures stay with the general error", () => {
+ assert.equal(
+ agentsMdRefusal({ status: 400, detail: "name is not a valid identifier" }),
+ null,
+ );
+ assert.equal(
+ agentsMdRefusal({
+ status: 500,
+ detail: "agents_md is 70000 bytes; the limit is 65536",
+ }),
+ null,
+ );
+ assert.equal(agentsMdRefusal({ status: 400 }), null);
+ assert.equal(agentsMdRefusal({ detail: "agents_md is too long" }), null);
+});
From f068d0b2fccf256f865cfc9e1dd15a2b45d983a5 Mon Sep 17 00:00:00 2001
From: iris
Date: Sun, 18 Oct 2026 16:00:50 +0200
Subject: [PATCH 2/3] docs: say the swarm refuses an oversized initial
AGENTS.md in the active voice
---
docs/swarm/ui.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/swarm/ui.md b/docs/swarm/ui.md
index 038efc3b..ec14c56e 100644
--- a/docs/swarm/ui.md
+++ b/docs/swarm/ui.md
@@ -395,7 +395,7 @@ the text goes as typed in the optional `agents_md` field of `POST /api/agents`.
The agent writes it on its first start, and only when it has no `AGENTS.md`
yet, so creating an agent again under a name that has one never replaces it.
The controller refuses a text over 64 KiB; its reason shows under the box and
-no agent is created. After a successful create the box empties. The
+the swarm creates no agent. After a successful create the box empties. The
[instructions editor](#editing-an-agents-instructions) changes the file later.
The [swarm README](README.md#an-agents-agentsmd) has the wire details.
From dd7fc1c9afddfb665b180d473312a8b3ad1a7048 Mon Sep 17 00:00:00 2001
From: iris
Date: Sun, 18 Oct 2026 16:02:10 +0200
Subject: [PATCH 3/3] swarm-ui: show the AGENTS.md size refusal when the
instructions section is collapsed
The refusal sat inside the collapsed section, and the general error panel
hides while a refusal is set, so a refused submit with the section closed
showed nothing. The refusal now renders under the section, outside the
details element.
---
docs/swarm/ui.md | 5 +++--
frontend/packages/swarm-ui/src/pages/CreateAgentForm.css | 2 +-
frontend/packages/swarm-ui/src/pages/CreateAgentForm.tsx | 9 +++++----
3 files changed, 9 insertions(+), 7 deletions(-)
diff --git a/docs/swarm/ui.md b/docs/swarm/ui.md
index ec14c56e..bce82c8d 100644
--- a/docs/swarm/ui.md
+++ b/docs/swarm/ui.md
@@ -394,8 +394,9 @@ whitespace, it sends nothing and the agent starts without the file. Otherwise
the text goes as typed in the optional `agents_md` field of `POST /api/agents`.
The agent writes it on its first start, and only when it has no `AGENTS.md`
yet, so creating an agent again under a name that has one never replaces it.
-The controller refuses a text over 64 KiB; its reason shows under the box and
-the swarm creates no agent. After a successful create the box empties. The
+The controller refuses a text over 64 KiB. Its reason shows under the
+section, open or not, and the swarm creates no agent. After a successful
+create the box empties. The
[instructions editor](#editing-an-agents-instructions) changes the file later.
The [swarm README](README.md#an-agents-agentsmd) has the wire details.
diff --git a/frontend/packages/swarm-ui/src/pages/CreateAgentForm.css b/frontend/packages/swarm-ui/src/pages/CreateAgentForm.css
index 5e131ef1..a1960f15 100644
--- a/frontend/packages/swarm-ui/src/pages/CreateAgentForm.css
+++ b/frontend/packages/swarm-ui/src/pages/CreateAgentForm.css
@@ -69,7 +69,7 @@
.create-agent-result-ok {
color: var(--green);
}
-/* The controller's refusal of the AGENTS.md text, under its box. */
+/* The controller's refusal of the AGENTS.md text, under its section. */
.create-agent-field-error {
margin: 0;
color: var(--red);
diff --git a/frontend/packages/swarm-ui/src/pages/CreateAgentForm.tsx b/frontend/packages/swarm-ui/src/pages/CreateAgentForm.tsx
index d27bbd8d..cb3c1f18 100644
--- a/frontend/packages/swarm-ui/src/pages/CreateAgentForm.tsx
+++ b/frontend/packages/swarm-ui/src/pages/CreateAgentForm.tsx
@@ -215,7 +215,8 @@ export function CreateAgentForm({
label: h.name,
}));
const hivesReady = hives !== null && hives.length > 0;
- // The controller's size refusal of the AGENTS.md text, shown by its box.
+ // The controller's size refusal of the AGENTS.md text, shown under its
+ // section so a collapsed section still shows it.
const refusal =
result.status === "error" ? agentsMdRefusal(result.problem) : null;
@@ -385,11 +386,11 @@ export function CreateAgentForm({
placeholder="the text the agent's AGENTS.md starts with; leave empty for none"
onInput={setAgentsMd}
/>
- {refusal !== null && (
-