@rubytech/create-maxy-code 0.1.559 → 0.1.561

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 (22) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/plugins/admin/mcp/dist/index.js +15 -1
  3. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  4. package/payload/platform/plugins/admin/mcp/dist/lib/roster-census.d.ts +25 -0
  5. package/payload/platform/plugins/admin/mcp/dist/lib/roster-census.d.ts.map +1 -1
  6. package/payload/platform/plugins/admin/mcp/dist/lib/roster-census.js +50 -1
  7. package/payload/platform/plugins/admin/mcp/dist/lib/roster-census.js.map +1 -1
  8. package/payload/platform/plugins/admin/skills/agent-builder/SKILL.md +15 -10
  9. package/payload/platform/plugins/admin/skills/agent-builder/references/agent-pattern.md +6 -3
  10. package/payload/platform/plugins/admin/skills/specialist-management/SKILL.md +26 -0
  11. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +10 -0
  12. package/payload/platform/scripts/__tests__/authored-agent-durable-copy.test.sh +78 -0
  13. package/payload/platform/scripts/__tests__/provision-bundle-orphan.test.sh +132 -0
  14. package/payload/platform/scripts/lib/provision-account-dir.sh +47 -0
  15. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts +8 -4
  16. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  17. package/payload/platform/services/claude-session-manager/dist/http-server.js +39 -5
  18. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  19. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  20. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +26 -5
  21. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  22. package/payload/server/server.js +7 -7
@@ -3,12 +3,37 @@ export interface RosterCensusInput {
3
3
  agentsMdPath: string;
4
4
  /** `<accountDir>/specialists/agents` — the directory spawn-context reads. */
5
5
  specialistsAgentsDir: string;
6
+ /** Directories whose own `*.md` files are durable copies. Today just
7
+ * `$PLATFORM_ROOT/templates/specialists/agents`. Required, not optional:
8
+ * a caller that omitted them would report every live specialist as
9
+ * undurable. */
10
+ durableDirs: string[];
11
+ /** Plugin-tree roots. For each, every `<root>/<plugin>/agents/*.md` is a
12
+ * durable copy: the account tree (`store-skill`'s home for authored
13
+ * agents), the platform tree, and premium-plugins. A premium agent's only
14
+ * copy lives in its bundle and a reinstall re-delivers it, so omitting that
15
+ * root would report five standing false positives on beacons alone. */
16
+ durablePluginRoots: string[];
17
+ /** `<installRoot>/premium-plugins`. Omitted, missing, or holding no bundle
18
+ * directory yields an empty `bundleOrphans`: none of those states is
19
+ * evidence that the account's whole premium set is orphaned. */
20
+ premiumPluginsDir?: string;
6
21
  }
7
22
  export interface RosterCensusResult {
8
23
  declared: number;
9
24
  dispatchable: number;
10
25
  /** Declared names with no `<name>.md` in the dispatch directory. */
11
26
  orphaned: string[];
27
+ /** Live specialist filenames, `.md` included, absent from every durable
28
+ * root. Filenames rather than roster names because the durable copy is
29
+ * stored under the filename, and a `--` agent's two differ. */
30
+ undurable: string[];
31
+ /** Task 2418 — basenames of `*--*.md` in the dispatch directory with no file
32
+ * of that name under `<premiumPluginsDir>/<bundle>/agents/`. Dispatchable
33
+ * today, with no bundle behind them. Walks the directory rather than the
34
+ * roster, because dispatchability is a property of the directory
35
+ * `spawn-context.ts` reads, not of what AGENTS.md advertises. */
36
+ bundleOrphans: string[];
12
37
  }
13
38
  export declare function runRosterCensus(input: RosterCensusInput): RosterCensusResult;
14
39
  //# sourceMappingURL=roster-census.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"roster-census.d.ts","sourceRoot":"","sources":["../../src/lib/roster-census.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,iBAAiB;IAChC,yEAAyE;IACzE,YAAY,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,oBAAoB,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,oEAAoE;IACpE,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,iBAAiB,GAAG,kBAAkB,CAgB5E"}
1
+ {"version":3,"file":"roster-census.d.ts","sourceRoot":"","sources":["../../src/lib/roster-census.ts"],"names":[],"mappings":"AAgCA,MAAM,WAAW,iBAAiB;IAChC,yEAAyE;IACzE,YAAY,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,oBAAoB,EAAE,MAAM,CAAC;IAC7B;;;qBAGiB;IACjB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB;;;;4EAIwE;IACxE,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B;;qEAEiE;IACjE,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,oEAAoE;IACpE,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB;;oEAEgE;IAChE,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB;;;;sEAIkE;IAClE,aAAa,EAAE,MAAM,EAAE,CAAC;CACzB;AAyCD,wBAAgB,eAAe,CAAC,KAAK,EAAE,iBAAiB,GAAG,kBAAkB,CAgC5E"}
@@ -1,5 +1,40 @@
1
- import { existsSync, readFileSync } from "node:fs";
1
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ /** `*.md` entries directly in `dir`. A missing directory is normal — not every
4
+ * install carries premium-plugins — and contributes nothing. An unreadable one
5
+ * is not: the throw propagates to the emit point, which reports
6
+ * `[roster-census] error` rather than a short durable set, because a partial
7
+ * set would name durable specialists as undurable. */
8
+ function markdownIn(dir) {
9
+ if (!existsSync(dir))
10
+ return [];
11
+ return readdirSync(dir).filter((entry) => entry.endsWith(".md"));
12
+ }
13
+ function subdirs(dir) {
14
+ if (!existsSync(dir))
15
+ return [];
16
+ return readdirSync(dir, { withFileTypes: true })
17
+ .filter((entry) => entry.isDirectory())
18
+ .map((entry) => entry.name);
19
+ }
20
+ function findBundleOrphans(specialistsAgentsDir, premiumPluginsDir) {
21
+ if (!premiumPluginsDir)
22
+ return [];
23
+ const bundles = subdirs(premiumPluginsDir);
24
+ // No bundle resolvable, whether the root is missing or merely empty, is not
25
+ // evidence of a fleet of orphans: both are indistinguishable from a brand
26
+ // that legitimately ships none (paths.ts bundleMode defaults to 'none').
27
+ // Provisioning applies the identical guard, so the two surfaces never
28
+ // disagree. An unreadable root throws, the same doctrine as markdownIn.
29
+ if (bundles.length === 0)
30
+ return [];
31
+ // Any bundle's agents/ dir: `--` names are already bundle-unique, so this
32
+ // asks the same question as parsing the prefix without deriving meaning from
33
+ // a filename.
34
+ return markdownIn(specialistsAgentsDir)
35
+ .filter((file) => file.includes("--"))
36
+ .filter((file) => !bundles.some((b) => existsSync(join(premiumPluginsDir, b, "agents", file))));
37
+ }
3
38
  export function runRosterCensus(input) {
4
39
  const body = existsSync(input.agentsMdPath)
5
40
  ? readFileSync(input.agentsMdPath, "utf8")
@@ -9,10 +44,24 @@ export function runRosterCensus(input) {
9
44
  declared.push(match[1]);
10
45
  }
11
46
  const orphaned = declared.filter((name) => !existsSync(join(input.specialistsAgentsDir, `${name}.md`)));
47
+ const durable = new Set();
48
+ for (const dir of input.durableDirs) {
49
+ for (const filename of markdownIn(dir))
50
+ durable.add(filename);
51
+ }
52
+ for (const root of input.durablePluginRoots) {
53
+ for (const plugin of subdirs(root)) {
54
+ for (const filename of markdownIn(join(root, plugin, "agents")))
55
+ durable.add(filename);
56
+ }
57
+ }
58
+ const undurable = markdownIn(input.specialistsAgentsDir).filter((filename) => !durable.has(filename));
12
59
  return {
13
60
  declared: declared.length,
14
61
  dispatchable: declared.length - orphaned.length,
15
62
  orphaned,
63
+ undurable,
64
+ bundleOrphans: findBundleOrphans(input.specialistsAgentsDir, input.premiumPluginsDir),
16
65
  };
17
66
  }
18
67
  //# sourceMappingURL=roster-census.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"roster-census.js","sourceRoot":"","sources":["../../src/lib/roster-census.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AA+BjC,MAAM,UAAU,eAAe,CAAC,KAAwB;IACtD,MAAM,IAAI,GAAG,UAAU,CAAC,KAAK,CAAC,YAAY,CAAC;QACzC,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,YAAY,EAAE,MAAM,CAAC;QAC1C,CAAC,CAAC,EAAE,CAAC;IACP,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,mCAAmC,CAAC,EAAE,CAAC;QACvE,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1B,CAAC;IACD,MAAM,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAC9B,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,oBAAoB,EAAE,GAAG,IAAI,KAAK,CAAC,CAAC,CACtE,CAAC;IACF,OAAO;QACL,QAAQ,EAAE,QAAQ,CAAC,MAAM;QACzB,YAAY,EAAE,QAAQ,CAAC,MAAM,GAAG,QAAQ,CAAC,MAAM;QAC/C,QAAQ;KACT,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"roster-census.js","sourceRoot":"","sources":["../../src/lib/roster-census.ts"],"names":[],"mappings":"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"}
@@ -96,18 +96,23 @@ Show the operator the complete file and ask them to review.
96
96
  ## Step 6: Install through specialist-management
97
97
 
98
98
  The agent persists as part of an admin-managed plugin: the same durable home `store-skill` uses,
99
- so it survives a reinstall and the agent and its skills stay together. `store-skill` already
100
- created that plugin when you built the agent's skills (canonical under the account, mirrored
101
- under the platform plugins tree). Write the composed file, named `{plugin}--{name}.md`, into the
102
- plugin's `agents/` directory in both places, so it is durable under the account and immediately
103
- visible in the platform mirror that `specialist-management` reads. Then follow
104
- `specialist-management`'s "Activate premium plugin agents" path to copy it into
105
- `specialists/agents/` and append the `- **specialists:{name}**: {description}` line to the admin
106
- `AGENTS.md`. Do not invent install logic; drive that existing path. If the agent bundles only
107
- existing skills, so no plugin was created, scaffold the plugin first the way `store-skill` does
108
- (a `PLUGIN.md` and the `.user-mirror` marker in both the account and mirror trees) before
99
+ so it survives a reinstall and the agent and its skills stay together. That home is the account's
100
+ own plugin tree, and it is the only one: operator plugins never live under
101
+ `$PLATFORM_ROOT/plugins`, which is immutable.
102
+
103
+ Write the composed file to `plugins/{plugin}/agents/{plugin}--{name}.md`. That copy is the
104
+ durable one, and the live copy is only ever made by copying it, so no order of operations leaves
105
+ a dispatchable agent that nothing can restore. `store-skill` already created the plugin when you
106
+ built the agent's skills; if the agent bundles only existing skills, so no plugin was created,
107
+ scaffold it first the way `store-skill` does — a `PLUGIN.md` under `plugins/{plugin}/` — before
109
108
  writing the agent file.
110
109
 
110
+ Then follow `specialist-management`'s "Install an account-authored agent" path, which copies the
111
+ durable file into `specialists/agents/` and appends the `- **specialists:{name}**: {description}`
112
+ line to the admin `AGENTS.md`. Do not invent install logic, and never write an agent file
113
+ straight into `specialists/agents/`: a live file with no durable copy behind it is exactly what
114
+ this step exists to prevent.
115
+
111
116
  ## Step 7: Confirm
112
117
 
113
118
  Tell the operator:
@@ -16,9 +16,12 @@ The roster splits along one line: what an agent is *for*.
16
16
  writing, data, coding. It owns a capability surface, not a single artefact. These ship in
17
17
  `platform/templates/specialists/agents/` and resolve as `specialists:<name>`.
18
18
  - **Vertical bundle agent.** A job title that owns one deliverable and the skills that produce
19
- it: a negotiator owns the buyer-pipeline package, a quoter owns the priced quote. These ship
20
- inside a premium plugin's `agents/` directory, named `{plugin}--{agent}.md`, and resolve as
21
- `specialists:<name>`.
19
+ it: a negotiator owns the buyer-pipeline package, a quoter owns the priced quote. Named
20
+ `{plugin}--{agent}.md` and resolving as `specialists:<name>`, these have two homes. A
21
+ bundle-shipped one lives in its premium plugin's `agents/` directory. An operator-authored one
22
+ lives in the account's own `plugins/{plugin}/agents/`, the durable home `store-skill` uses;
23
+ that copy is what a reinstall preserves and what the live copy in `specialists/agents/` is
24
+ made from.
22
25
 
23
26
  `agent-builder` authors vertical bundle agents. The forcing question is "what one thing does
24
27
  this agent hand back?" If the honest answer is "several unrelated things", that is several
@@ -9,6 +9,11 @@ Invoked by the admin agent directly.
9
9
 
10
10
  Manage specialist subagents at any time via conversation.
11
11
 
12
+ Authoring a *new* agent from scratch is `agent-builder`, not this skill. That skill decides the
13
+ deliverable, the model, the skills it owns and its tool allowlist, composes the file, and writes
14
+ the durable copy; it then hands the finished agent back here to install. This skill installs,
15
+ removes, and activates agents whose files already exist.
16
+
12
17
  ## Install a specialist
13
18
 
14
19
  1. Use Bash: `cp $PLATFORM_ROOT/templates/specialists/agents/{name}.md specialists/agents/{name}.md`
@@ -45,3 +50,24 @@ For each `.md` file in `$PLATFORM_ROOT/plugins/{plugin-name}/agents/`:
45
50
  4. Confirm removal to the user.
46
51
 
47
52
  Core specialists (files without `--` in the name) are never affected by premium plugin operations.
53
+
54
+ ## Account-authored plugin agents
55
+
56
+ An agent authored through `agent-builder` lives in the account's own plugin tree at
57
+ `plugins/{plugin-name}/agents/{plugin-name}--{agent-name}.md`. That copy is the durable one: it
58
+ is what survives a reinstall, and it is the only source the live copy is made from. A file in
59
+ `specialists/agents/` with nothing behind it in a durable tree cannot be restored by anything —
60
+ the boot-time `[roster-census]` line names those in its `undurable=` field.
61
+
62
+ ### Install an account-authored agent
63
+
64
+ For each `.md` file in `plugins/{plugin-name}/agents/`:
65
+
66
+ 1. Check whether `specialists/agents/{filename}` already exists — skip if so (idempotent).
67
+ 2. Use Bash: `cp plugins/{plugin-name}/agents/{filename} specialists/agents/{filename}`
68
+ 3. Extract the `name` and `description` fields from the copied file's YAML frontmatter.
69
+ 4. Read `agents/admin/AGENTS.md`. If it already contains `specialists:{name}`, skip. Otherwise append: `- **specialists:{name}**: {description}` using Edit.
70
+ 5. Confirm each installed specialist to the user — active from next session start.
71
+
72
+ The copy source is always the durable file. Never compose an agent directly into
73
+ `specialists/agents/`: that produces a dispatchable agent nothing can restore.
@@ -9,6 +9,16 @@ Invoked by the admin agent directly.
9
9
 
10
10
  This is the platform's release timeline, newest first. Each entry shows the date it shipped and the version it shipped in, so you can tell the operator how current their install is. To compare, read the installed version from `capabilities-here` and match it against the versions below. Keep answers high level and in plain English; this is a summary, not a full commit log.
11
11
 
12
+ ## 2026-08-04 (0.1.561)
13
+
14
+ - A public Telegram bot now answers. Messages were arriving and the conversation staying alive, but nothing the visitor sent ever became a turn, so a public bot received everything and replied to nothing. Public bots now open through the same locked-down path the platform already uses elsewhere: the ordinary setup with only the reply tool available.
15
+
16
+ ## 2026-08-04 (0.1.560)
17
+
18
+ - A specialist you wrote is no longer lost if it is deleted. Authoring now always leaves a durable copy in the plugin that owns it, and the boot-time check names any live specialist that has no such copy.
19
+ - A specialist whose plugin has been removed no longer stays dispatchable forever. Upgrades now re-check those files instead of skipping them unconditionally, and the boot-time check names any that can still be dispatched with nothing behind them.
20
+ - A message sent to a public conversation now becomes a turn. A connection the platform needs in order to hand an incoming message to the assistant had been removed from public conversations as unnecessary; without it the message arrived and was never acted on.
21
+
12
22
  ## 2026-08-04 (0.1.559)
13
23
 
14
24
  - Upgrading no longer deletes specialists. Every upgrade was removing any specialist that did not ship with the install, including ones written for you, and never putting them back. An upgrade now only replaces what it actually ships, and reports at boot any specialist the roster cannot dispatch.
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env bash
2
+ # Task 2417 — the two admin skills that document specialist authoring must
3
+ # never document a live copy that is not made from a durable one.
4
+ #
5
+ # Task 1962 (ba626832b) dropped the platform mirror. agent-builder was never
6
+ # updated: it kept telling the agent to write the composed file "in both
7
+ # places" and to scaffold a `.user-mirror` marker in a mirror tree that no
8
+ # longer exists, then handed install to a specialist-management flow sourcing
9
+ # from $PLATFORM_ROOT/plugins/{plugin}/agents/ — a root an account-authored
10
+ # plugin never occupies. Every documented step after "compose the file" named a
11
+ # directory that was not there, so the agent improvised a bare Write into
12
+ # specialists/agents/ and the durable copy was never made. These are the four
13
+ # checkable shapes of that regression.
14
+ set -u
15
+ ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
16
+ AB="$ROOT/platform/plugins/admin/skills/agent-builder/SKILL.md"
17
+ SM="$ROOT/platform/plugins/admin/skills/specialist-management/SKILL.md"
18
+ # agent-builder Step 0 declares this reference authoritative and forbids
19
+ # paraphrasing it, so a wrong home here overrides Step 6 rather than merely
20
+ # duplicating it.
21
+ AP="$ROOT/platform/plugins/admin/skills/agent-builder/references/agent-pattern.md"
22
+ PASS=0; FAIL=0
23
+
24
+ for f in "$AB" "$SM" "$AP"; do
25
+ [[ -f "$f" ]] || { echo "FAIL: missing $f" >&2; exit 1; }
26
+ done
27
+
28
+ # 1. Every documented `cp` into specialists/agents/ sources from a durable root.
29
+ bad=$(grep -hoE 'cp [^`]*specialists/agents/[^`]*' "$AB" "$SM" "$AP" \
30
+ | grep -vE 'cp \$PLATFORM_ROOT/templates/specialists/agents/' \
31
+ | grep -vE 'cp \$PLATFORM_ROOT/plugins/\{plugin-name\}/agents/' \
32
+ | grep -vE 'cp plugins/\{plugin-name\}/agents/' || true)
33
+ if [[ -z "$bad" ]]; then
34
+ echo "PASS: every documented cp into specialists/agents/ sources from a durable root"; PASS=$((PASS+1))
35
+ else
36
+ echo "FAIL: cp into specialists/agents/ from a non-durable source:" >&2
37
+ printf ' %s\n' "$bad" >&2; FAIL=$((FAIL+1))
38
+ fi
39
+
40
+ # 2. The account-authored install flow exists. Without this, check 1 passes
41
+ # vacuously once the flow is deleted.
42
+ if grep -qE 'cp plugins/\{plugin-name\}/agents/' "$SM"; then
43
+ echo "PASS: specialist-management documents the account-authored install flow"; PASS=$((PASS+1))
44
+ else
45
+ echo "FAIL: specialist-management has no cp from plugins/{plugin-name}/agents/" >&2; FAIL=$((FAIL+1))
46
+ fi
47
+
48
+ # 3. No platform mirror. 1962 removed it; a reference to it here is stale by
49
+ # construction and sends the agent to a tree that is not there. Matched by
50
+ # the three shapes the stale text actually used, not by the bare word:
51
+ # "mirrors the reference agent.yaml" is ordinary English and must pass.
52
+ MIRROR='\.user-mirror|platform mirror|mirror tree|mirrored under'
53
+ if ! grep -qiE "$MIRROR" "$AB" "$SM" "$AP"; then
54
+ echo "PASS: no skill in the authoring path references a platform mirror"; PASS=$((PASS+1))
55
+ else
56
+ echo "FAIL: a platform-mirror reference survives (1962 removed the mirror):" >&2
57
+ grep -inE "$MIRROR" "$AB" "$SM" "$AP" >&2; FAIL=$((FAIL+1))
58
+ fi
59
+
60
+ # 4. agent-builder names the account plugin tree as the durable home.
61
+ if grep -qE 'plugins/\{plugin\}/agents/' "$AB"; then
62
+ echo "PASS: agent-builder names the account plugin tree as the durable home"; PASS=$((PASS+1))
63
+ else
64
+ echo "FAIL: agent-builder does not name plugins/{plugin}/agents/ as the durable home" >&2; FAIL=$((FAIL+1))
65
+ fi
66
+
67
+ # 5. So does the reference Step 0 defers to. Without this, agent-pattern.md can
68
+ # say an authored agent lives in a premium bundle — a tree the account cannot
69
+ # write — and that overrides Step 6, because Step 0 forbids paraphrasing it.
70
+ if grep -qE 'plugins/\{plugin\}/agents/' "$AP"; then
71
+ echo "PASS: agent-pattern names the account plugin tree for an authored agent"; PASS=$((PASS+1))
72
+ else
73
+ echo "FAIL: agent-pattern does not name plugins/{plugin}/agents/ as an authored agent's home" >&2; FAIL=$((FAIL+1))
74
+ fi
75
+
76
+ echo "---"
77
+ echo "authored-agent-durable-copy: $PASS passed, $FAIL failed"
78
+ [[ $FAIL -eq 0 ]]
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env bash
2
+ # Task 2418 — a `--` agent whose premium bundle is no longer on the install tree
3
+ # stays dispatchable forever.
4
+ #
5
+ # 2415's retirement loop deletes only what its manifest claims, and the manifest
6
+ # names only files copied from $TEMPLATES_DIR, so a premium `--` file is never in
7
+ # it. Nothing noticed when the bundle behind it left the tree: on beacons,
8
+ # writer-craft--manuscript-reviewer.md was dispatchable from May 13 onward with
9
+ # no writer-craft bundle under premium-plugins/.
10
+ set -uo pipefail
11
+ SCRIPT_DIR_T="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12
+ PLAT_SCRIPTS="$SCRIPT_DIR_T/.."
13
+ REAL_PROJECT="$(cd "$PLAT_SCRIPTS/.." && pwd)"
14
+ PASS=0; FAIL=0; FAILED=()
15
+ assert_absent() { if [ ! -e "$1" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); FAILED+=("$2: expected absent [$1]"); fi; }
16
+ assert_present() { if [ -e "$1" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); FAILED+=("$2: expected present [$1]"); fi; }
17
+ assert_eq() { if [ "$1" = "$2" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); FAILED+=("$3: expected [$2] got [$1]"); fi; }
18
+
19
+ . "$PLAT_SCRIPTS/lib/provision-account-dir.sh"
20
+ export SCRIPT_DIR="$PLAT_SCRIPTS"
21
+ export USERS_FILE="/nonexistent/users.json"
22
+
23
+ # A temp platform/ whose sibling premium-plugins/ this test controls. plugins and
24
+ # config are symlinked: provisioning reads both, and neither is under test here.
25
+ T=$(mktemp -d)
26
+ mkdir -p "$T/platform" "$T/premium-plugins/present-bundle/agents"
27
+ cp -R "$REAL_PROJECT/templates" "$T/platform/templates"
28
+ ln -s "$REAL_PROJECT/plugins" "$T/platform/plugins"
29
+ ln -s "$REAL_PROJECT/config" "$T/platform/config"
30
+ export PROJECT_DIR="$T/platform"
31
+ export TEMPLATES_DIR="$T/platform/templates"
32
+
33
+ ACCT="$T/acct"
34
+ ACCT_ID="24182418-2418-2418-2418-241824182418"
35
+ ORPHANED="$ACCT/specialists/agents-orphaned"
36
+
37
+ # One `--` agent whose bundle ships it, one whose bundle is gone.
38
+ mkdir -p "$ACCT/specialists/agents"
39
+ printf -- '---\nname: keeper\n---\nShipped by a bundle that is still installed.\n' \
40
+ > "$T/premium-plugins/present-bundle/agents/present-bundle--keeper.md"
41
+ cp "$T/premium-plugins/present-bundle/agents/present-bundle--keeper.md" \
42
+ "$ACCT/specialists/agents/present-bundle--keeper.md"
43
+ printf -- '---\nname: orphan\n---\nIts bundle is no longer on the tree.\n' \
44
+ > "$ACCT/specialists/agents/gone-bundle--orphan.md"
45
+
46
+ # --- Run 1 — the orphan is reconciled, the keeper is not. --------------------
47
+ OUT1="$(provision_account_dir "$ACCT" "$ACCT_ID" "house" 2>&1)"
48
+
49
+ assert_present "$ACCT/specialists/agents/present-bundle--keeper.md" "run1:bundle-present-survives"
50
+ assert_absent "$ACCT/specialists/agents/gone-bundle--orphan.md" "run1:orphan-undispatchable"
51
+ assert_present "$ORPHANED/gone-bundle--orphan.md" "run1:orphan-preserved"
52
+ assert_absent "$ACCT/specialists/agents-disabled/gone-bundle--orphan.md" "run1:not-in-disable-quarantine"
53
+ assert_eq "$(printf '%s' "$OUT1" | grep -c 'op=specialist-orphaned name=gone-bundle--orphan.md reason=bundle-absent')" "1" "run1:orphan-named"
54
+ assert_eq "$(printf '%s' "$OUT1" | grep -c 'op=specialist-orphan-reconcile orphaned=1')" "1" "run1:count-one"
55
+
56
+ # --- Run 2 — nothing left to reconcile: zero is still logged. ----------------
57
+ OUT2="$(provision_account_dir "$ACCT" "$ACCT_ID" "house" 2>&1)"
58
+ assert_eq "$(printf '%s' "$OUT2" | grep -c 'op=specialist-orphan-reconcile orphaned=0')" "1" "run2:zero-count-logged"
59
+ assert_eq "$(printf '%s' "$OUT2" | grep -c 'op=specialist-orphaned name=')" "0" "run2:nothing-reconciled"
60
+ assert_present "$ACCT/specialists/agents/present-bundle--keeper.md" "run2:keeper-still-dispatchable"
61
+
62
+ # --- An orphan of a name already quarantined replaces it. -------------------
63
+ # The copy leaving the dispatch directory is the one that was live, so it is the
64
+ # copy worth keeping. Pinned because a later change to `mv -n` would silently
65
+ # keep the stale one and contradict the contract doc.
66
+ printf -- '---\nname: orphan\n---\nSTALE QUARANTINED COPY\n' > "$ORPHANED/gone-bundle--orphan.md"
67
+ printf -- '---\nname: orphan\n---\nLIVE COPY\n' \
68
+ > "$ACCT/specialists/agents/gone-bundle--orphan.md"
69
+ provision_account_dir "$ACCT" "$ACCT_ID" "house" >/dev/null 2>&1
70
+ assert_eq "$(grep -c 'LIVE COPY' "$ORPHANED/gone-bundle--orphan.md")" "1" "requarantine:live-copy-kept"
71
+ assert_eq "$(grep -c 'STALE' "$ORPHANED/gone-bundle--orphan.md")" "0" "requarantine:stale-copy-replaced"
72
+
73
+ # --- Run 3 — premium root present but holding no bundles. -------------------
74
+ # Indistinguishable from a brand that ships none (paths.ts bundleMode defaults
75
+ # to 'none') and from a partial install. Reconciling here would strip the whole
76
+ # premium set off the account, so it reconciles nothing, exactly as 2415 refuses
77
+ # to read zero templates as twelve simultaneous retirements.
78
+ rm -rf "$T/premium-plugins/present-bundle"
79
+ OUT3="$(provision_account_dir "$ACCT" "$ACCT_ID" "house" 2>&1)"
80
+ assert_present "$ACCT/specialists/agents/present-bundle--keeper.md" "run3:empty-premium-root-moves-nothing"
81
+ assert_eq "$(printf '%s' "$OUT3" | grep -c 'op=specialist-orphan-reconcile orphaned=0 reason=no-premium-bundles')" "1" "run3:loud-line"
82
+ assert_eq "$(printf '%s' "$OUT3" | grep -c 'op=specialist-orphaned name=')" "0" "run3:no-reconciliations"
83
+
84
+ # --- Run 4 — premium root missing entirely. Same answer, same reason. --------
85
+ rm -rf "$T/premium-plugins"
86
+ OUT4="$(provision_account_dir "$ACCT" "$ACCT_ID" "house" 2>&1)"
87
+ assert_present "$ACCT/specialists/agents/present-bundle--keeper.md" "run4:no-premium-root-moves-nothing"
88
+ assert_eq "$(printf '%s' "$OUT4" | grep -c 'op=specialist-orphan-reconcile orphaned=0 reason=no-premium-bundles')" "1" "run4:loud-line"
89
+ assert_eq "$(printf '%s' "$OUT4" | grep -c 'op=specialist-orphaned name=')" "0" "run4:no-reconciliations"
90
+
91
+ rm -rf "$T"
92
+
93
+ # --- The install path runs under `set -euo pipefail`; this suite does not. ---
94
+ # setup-account.sh:17 sets all three and calls provision_account_dir at :63, so
95
+ # an `&&` list whose test fails could abort a real install where it cannot abort
96
+ # here. Both new branches end in one: the `[ -d "$_b" ]` bundle count and the
97
+ # `[ "$_found" = 1 ] && continue` miss. A subshell running the real options is
98
+ # the only place that difference is visible.
99
+ set_e_case() {
100
+ local premium_state="$1" expect_moved="$2" label="$3"
101
+ local out rc
102
+ out="$(
103
+ set -euo pipefail
104
+ . "$PLAT_SCRIPTS/lib/provision-account-dir.sh"
105
+ export SCRIPT_DIR="$PLAT_SCRIPTS" USERS_FILE="/nonexistent/users.json"
106
+ S=$(mktemp -d)
107
+ mkdir -p "$S/platform"
108
+ cp -R "$REAL_PROJECT/templates" "$S/platform/templates"
109
+ ln -s "$REAL_PROJECT/plugins" "$S/platform/plugins"
110
+ ln -s "$REAL_PROJECT/config" "$S/platform/config"
111
+ export PROJECT_DIR="$S/platform" TEMPLATES_DIR="$S/platform/templates"
112
+ [ "$premium_state" = "with-bundle" ] && {
113
+ mkdir -p "$S/premium-plugins/present-bundle/agents"
114
+ printf -- '---\n' > "$S/premium-plugins/present-bundle/agents/present-bundle--keeper.md"
115
+ }
116
+ A="$S/acct"; mkdir -p "$A/specialists/agents"
117
+ printf -- '---\nname: orphan\n---\n' > "$A/specialists/agents/gone-bundle--orphan.md"
118
+ provision_account_dir "$A" "24182418-2418-2418-2418-241824182418" "house" >/dev/null 2>&1
119
+ [ -f "$A/specialists/agents-orphaned/gone-bundle--orphan.md" ] && printf 'moved' || printf 'kept'
120
+ rm -rf "$S"
121
+ )"; rc=$?
122
+ assert_eq "$rc" "0" "$label:no-set-e-abort"
123
+ assert_eq "$out" "$expect_moved" "$label:outcome"
124
+ }
125
+
126
+ # No premium root: the guard branch, where the bundle count test never matches.
127
+ set_e_case "bare" "kept" "set-e:no-premium-root"
128
+ # A bundle present but not shipping this file: the `_found=0` reconcile branch.
129
+ set_e_case "with-bundle" "moved" "set-e:orphan-moved"
130
+
131
+ echo "PASS=$PASS FAIL=$FAIL"
132
+ if [ "$FAIL" -gt 0 ]; then printf '%s\n' "${FAILED[@]}"; exit 1; fi
@@ -373,6 +373,53 @@ for n in names:
373
373
  fi
374
374
  fi
375
375
 
376
+ # Task 2418 — a `--` agent whose bundle has left the install tree stays
377
+ # dispatchable forever. The retirement loop above deletes only what the
378
+ # manifest claims, and the manifest names only files copied from
379
+ # $TEMPLATES_DIR, so a premium `--` file is never in it and nothing notices
380
+ # when its bundle goes. Measured on beacons: writer-craft--manuscript-reviewer
381
+ # dispatchable since May 13 with no writer-craft bundle under premium-plugins/.
382
+ #
383
+ # MOVE, never delete. The bundle is gone, so unlike a retired template there is
384
+ # nothing to restore from and this may be the only copy on the machine. It does
385
+ # NOT go to agents-disabled/: that directory is paired with agents-disabled.json
386
+ # and a file there with no store entry is exactly the drift described above.
387
+ #
388
+ # Runs AFTER the disabled block so that in the drift state (store names it,
389
+ # file still live) the disable wins and the store and its directory agree.
390
+ local _premium_root="$PROJECT_DIR/../premium-plugins"
391
+ local _bundle_count=0 _b
392
+ for _b in "$_premium_root"/*/; do
393
+ [ -d "$_b" ] && _bundle_count=$((_bundle_count + 1))
394
+ done
395
+ if [ "$_bundle_count" -eq 0 ]; then
396
+ # No bundle resolvable, whether the root is missing or merely empty. Both
397
+ # make every `--` file test as orphaned, and both are indistinguishable from
398
+ # a brand that legitimately ships none (paths.ts bundleMode defaults to
399
+ # 'none'). Acting would strip the account's whole premium set. Same doctrine
400
+ # as no-templates-on-disk above.
401
+ echo " op=specialist-orphan-reconcile orphaned=0 reason=no-premium-bundles"
402
+ else
403
+ local _orphaned=0 _cand _base _twin _found
404
+ for _cand in "$ACCOUNT_DIR/specialists/agents/"*--*.md; do
405
+ [ -f "$_cand" ] || continue
406
+ _base="$(basename "$_cand")"
407
+ # Any bundle's agents/ dir. `--` names are already bundle-unique, so this
408
+ # answers the same question as parsing the prefix, without reintroducing a
409
+ # filename-derived predicate.
410
+ _found=0
411
+ for _twin in "$_premium_root"/*/agents/"$_base"; do
412
+ [ -f "$_twin" ] && { _found=1; break; }
413
+ done
414
+ [ "$_found" = 1 ] && continue
415
+ mkdir -p "$ACCOUNT_DIR/specialists/agents-orphaned"
416
+ mv -f "$_cand" "$ACCOUNT_DIR/specialists/agents-orphaned/$_base"
417
+ echo " op=specialist-orphaned name=$_base reason=bundle-absent"
418
+ _orphaned=$((_orphaned + 1))
419
+ done
420
+ echo " op=specialist-orphan-reconcile orphaned=$_orphaned"
421
+ fi
422
+
376
423
  rm -f "$ACCOUNT_DIR/.claude/agents/"*.md 2>/dev/null || true
377
424
 
378
425
  # SOUL.md — user-controlled. Only create if missing. Never overwrite.
@@ -15,10 +15,14 @@ import { TelegramChannelStore } from './telegram-channel-store.js';
15
15
  * widened this position to allow `undefined` because rc-spawn's role was
16
16
  * absent for a non-admin/non-public request; null now means "the body named
17
17
  * none", and the handler resolves it against the prior sidecar before
18
- * deciding whether that is an error. `passive` is not accepted here:
19
- * rc-spawn composes no persona at all (its --append-system-prompt segments
20
- * are reseat / voice / specialist-domains / authenticated-identity
21
- * fragments, never composeAppendSystemPrompt). */
18
+ * deciding whether that is an error. `passive` is not accepted here: it is a
19
+ * /public-spawn persona role, resolved at that boundary from role:'public'
20
+ * plus the passive flag, and no client may POST it.
21
+ *
22
+ * Task 2423 — rc-spawn DOES compose a persona now, for role:'public' only: the
23
+ * named agent's IDENTITY/SOUL/KNOWLEDGE via composeAppendSystemPrompt, the same
24
+ * composer /public-spawn uses. An admin spawn is unchanged and still composes
25
+ * only reseat / voice / specialist-domains / authenticated-identity fragments. */
22
26
  export declare function parseRcSpawnRole(body: {
23
27
  role?: unknown;
24
28
  }): Extract<AgentRole, 'admin' | 'public'> | null;
@@ -1 +1 @@
1
- {"version":3,"file":"http-server.d.ts","sourceRoot":"","sources":["../src/http-server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"http-server.d.ts","sourceRoot":"","sources":["../src/http-server.ts"],"names":[],"mappings":"AAqBA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAoB3B,OAAO,KAAK,EAAU,SAAS,EAAW,MAAM,YAAY,CAAA;AAK5D,OAAO,EAkBL,KAAK,SAAS,EAEf,MAAM,kBAAkB,CAAA;AAqBzB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAoBlD,OAAO,KAAK,EAAE,SAAS,EAAyB,MAAM,iBAAiB,CAAA;AACvE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAA;AAEzE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAE3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAA;AAC9D,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AACtD,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AAChE,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAA;AAQlE;;;;;;;;;;;mFAWmF;AACnF,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,GACvB,OAAO,CAAC,SAAS,EAAE,OAAO,GAAG,QAAQ,CAAC,GAAG,IAAI,CAE/C;AAED;;;;;;;;;;2EAU2E;AAC3E,wBAAgB,cAAc,CAC5B,IAAI,EAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,EAC3C,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,SAAS,EAAE,SAAS,CAAA;CAAE,GAAG,IAAI,CAOlD;AAsFD,eAAO,MAAM,kBAAkB,QAA2B,CAAA;AAS1D,eAAO,MAAM,4BAA4B,QAAS,CAAA;AAIlD,MAAM,WAAW,QAAS,SAAQ,IAAI,CAAC,SAAS,EAAE,gBAAgB,GAAG,SAAS,CAAC;IAC7E;;qEAEiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,SAAS,CAAA;IAClB;;oEAEgE;IAChE,qBAAqB,CAAC,EAAE,qBAAqB,CAAA;IAC7C,WAAW,EAAE,MAAM,CAAA;IACnB,iBAAiB,EAAE,MAAM,CAAA;IACzB,kBAAkB,EAAE,WAAW,CAAA;IAC/B,eAAe,EAAE,aAAa,CAAA;IAC9B;kFAC8E;IAC9E,WAAW,EAAE,kBAAkB,CAAA;IAC/B;;gFAE4E;IAC5E,cAAc,EAAE,cAAc,CAAA;IAC9B;;;6BAGyB;IACzB,mBAAmB,CAAC,EAAE,mBAAmB,CAAA;IACzC;;sBAEkB;IAClB,UAAU,CAAC,EAAE,UAAU,CAAA;IACvB;;;sCAGkC;IAClC,oBAAoB,CAAC,EAAE,oBAAoB,CAAA;IAC3C;;gFAE4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB;;;4EAGwE;IACxE,YAAY,CAAC,EAAE,MAAM,iBAAiB,GAAG,IAAI,CAAA;CAC9C;AAsFD;;;;;;mEAMmE;AACnE,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAiC9D;AAMD;;;;;;iBAMiB;AACjB,wBAAgB,yBAAyB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAmCvF;AAID;;;;;;yBAMyB;AACzB,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAiCzD;AAwCD,wBAAgB,eAAe,CAAC,aAAa,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAEpG;AAgBD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE1D;AAUD,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAMvE;AAMD,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,GAAG,IAAI,GACtB,MAAM,CAER;AAkCD,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAe5F;AAiGD,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CASvG;AAUD,wBAAgB,6BAA6B,CAC3C,IAAI,EAAE,QAAQ,EACd,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,OAAO,EAChB,gBAAgB,EAAE,OAAO,GACxB,IAAI,CA4BN;AAkED;;;kEAGkE;AAClE,MAAM,MAAM,OAAO,GAAG,IAAI,GAAG;IAC3B,2BAA2B,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACpD;;;+CAG2C;IAC3C,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CAChD,CAAA;AA8CD,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CA2mHpD"}
@@ -50,7 +50,7 @@ import { emitRcLife } from './rc-life.js';
50
50
  import { createRcChildLog } from './rc-child-log.js';
51
51
  // Task 1214 — the admin voice-section reader, injected into the rc-spawn
52
52
  // --append-system-prompt for channel-bound admin sessions.
53
- import { extractVoiceSection, renderSpecialistDomains } from './system-prompt.js';
53
+ import { extractVoiceSection, renderSpecialistDomains, composeAppendSystemPrompt } from './system-prompt.js';
54
54
  import { readSpecialistRoster } from './specialist-roster.js';
55
55
  import { readJsonlSession, readJsonlSessionAtPath } from './jsonl-enumerator.js';
56
56
  import { computeMetering } from './session-metering.js';
@@ -68,10 +68,14 @@ const ROLES = ['admin', 'public'];
68
68
  * widened this position to allow `undefined` because rc-spawn's role was
69
69
  * absent for a non-admin/non-public request; null now means "the body named
70
70
  * none", and the handler resolves it against the prior sidecar before
71
- * deciding whether that is an error. `passive` is not accepted here:
72
- * rc-spawn composes no persona at all (its --append-system-prompt segments
73
- * are reseat / voice / specialist-domains / authenticated-identity
74
- * fragments, never composeAppendSystemPrompt). */
71
+ * deciding whether that is an error. `passive` is not accepted here: it is a
72
+ * /public-spawn persona role, resolved at that boundary from role:'public'
73
+ * plus the passive flag, and no client may POST it.
74
+ *
75
+ * Task 2423 — rc-spawn DOES compose a persona now, for role:'public' only: the
76
+ * named agent's IDENTITY/SOUL/KNOWLEDGE via composeAppendSystemPrompt, the same
77
+ * composer /public-spawn uses. An admin spawn is unchanged and still composes
78
+ * only reseat / voice / specialist-domains / authenticated-identity fragments. */
75
79
  export function parseRcSpawnRole(body) {
76
80
  return body.role === 'admin' || body.role === 'public' ? body.role : null;
77
81
  }
@@ -2798,6 +2802,11 @@ export function buildHttpApp(deps) {
2798
2802
  // sidecar, so the refusal is not here; it is where both sources have been
2799
2803
  // consulted, just below `priorSidecar`.
2800
2804
  const rcRole = parseRcSpawnRole(body);
2805
+ // Task 2423 — the agent a public bot answers as. Same validation
2806
+ // /public-spawn applies, so one grammar governs both doors.
2807
+ const rcAgentSlug = typeof body.agentSlug === 'string' && /^[A-Za-z0-9_-]{1,64}$/.test(body.agentSlug)
2808
+ ? body.agentSlug
2809
+ : null;
2801
2810
  const adminChannel = typeof body.channel === 'string' && CHANNELS.includes(body.channel)
2802
2811
  ? body.channel
2803
2812
  : undefined;
@@ -3470,6 +3479,31 @@ export function buildHttpApp(deps) {
3470
3479
  appendSegments.push(authenticatedIdentityPrompt(resolvedName));
3471
3480
  deps.logger(`[rc-spawn] op=identity-injected userId=${resolvedAdminUserId ? resolvedAdminUserId.slice(0, 8) : 'none'} resolved=${resolvedName ? 1 : 0} source=${identitySource}`);
3472
3481
  }
3482
+ // Task 2423 — a public rc spawn composes its named agent's persona. This is
3483
+ // the one thing Task 2368 moved public Telegram off this door for, and it is
3484
+ // why that path landed on /public-spawn, which does not deliver at all
3485
+ // (Task 2420: a hand-spawn reproducing its argv AND all 60 env vars replies;
3486
+ // only spawnClaudeSession does not). The composer is the same one
3487
+ // /public-spawn uses, so the two doors cannot disagree about what a public
3488
+ // agent's prompt is. Refuses rather than answering personaless: a visitor
3489
+ // met by an agent with no identity is worse than one met by silence, and the
3490
+ // 503 matches the shape the other pre-spawn refusals on this door return.
3491
+ //
3492
+ // Gated on the slug being SUPPLIED, not on the role alone. A public WhatsApp
3493
+ // spawn also reaches this door with role:'public' and no slug, and has
3494
+ // always composed no persona here; refusing those would be a regression in
3495
+ // a path this task does not touch. Three cases, not four: slug present and
3496
+ // resolves, compose; slug present and does not resolve, refuse; slug absent,
3497
+ // unchanged.
3498
+ if (effectiveRcRole === 'public' && rcAgentSlug) {
3499
+ const composed = composeAppendSystemPrompt(deps.host, { role: 'public', agentSlug: rcAgentSlug, accountDir: effectiveAccountDir }, {});
3500
+ if (!composed.ok) {
3501
+ deps.logger(`[rc-spawn] op=public-persona-refused reason=${composed.reason} slug=${rcAgentSlug} detail=${composed.detail}`);
3502
+ return c.json({ error: 'spawn-failed', reason: composed.reason }, 503);
3503
+ }
3504
+ appendSegments.push(composed.block);
3505
+ deps.logger(`[rc-spawn] op=public-persona-composed slug=${rcAgentSlug} identityBytes=${composed.identityBytes} soulBytes=${composed.soulBytes} knowledgeBytes=${composed.knowledgeBytes}`);
3506
+ }
3473
3507
  if (appendSegments.length > 0) {
3474
3508
  argv.push('--append-system-prompt', appendSegments.join('\n\n'));
3475
3509
  }