@rubytech/create-sitedesk-code 0.1.514 → 0.1.515

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 (25) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/docs/superpowers/plans/2026-07-27-task-2016-disabled-agent-routing.md +624 -0
  3. package/payload/platform/docs/superpowers/specs/2026-07-27-task-2016-disabled-agent-routing-design.md +139 -0
  4. package/payload/platform/plugins/admin/mcp/dist/index.js +14 -16
  5. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  6. package/payload/platform/plugins/admin/mcp/dist/specialist-registry.d.ts +14 -0
  7. package/payload/platform/plugins/admin/mcp/dist/specialist-registry.d.ts.map +1 -0
  8. package/payload/platform/plugins/admin/mcp/dist/specialist-registry.js +57 -0
  9. package/payload/platform/plugins/admin/mcp/dist/specialist-registry.js.map +1 -0
  10. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +15 -3
  11. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +6 -0
  12. package/payload/platform/plugins/docs/references/admin-ui.md +14 -2
  13. package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +2 -2
  14. package/payload/platform/scripts/__tests__/agents-md-bootstrap.test.sh +47 -0
  15. package/payload/platform/scripts/lib/agents-md-bootstrap.sh +58 -1
  16. package/payload/platform/services/claude-session-manager/dist/config.d.ts +48 -12
  17. package/payload/platform/services/claude-session-manager/dist/config.d.ts.map +1 -1
  18. package/payload/platform/services/claude-session-manager/dist/config.js +65 -15
  19. package/payload/platform/services/claude-session-manager/dist/config.js.map +1 -1
  20. package/payload/platform/services/claude-session-manager/dist/index.js +7 -4
  21. package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
  22. package/payload/platform/services/claude-session-manager/dist/rootless-client-audit.d.ts.map +1 -1
  23. package/payload/platform/services/claude-session-manager/dist/rootless-client-audit.js +17 -2
  24. package/payload/platform/services/claude-session-manager/dist/rootless-client-audit.js.map +1 -1
  25. package/payload/server/server.js +101 -13
@@ -0,0 +1,139 @@
1
+ # Task 2016 — a disabled agent must leave the admin's routing table, and read as disabled rather than missing
2
+
3
+ Design, 2026-07-27. Depends on Task 1996 (landed).
4
+
5
+ ## The defect
6
+
7
+ Task 1996 stops dispatch by moving an agent file out of `<accountDir>/specialists/agents/`,
8
+ the directory `spawn-context.ts` reads to build the spawn manifest. Two surfaces still
9
+ believe the file is there.
10
+
11
+ `platform/scripts/lib/agents-md-bootstrap.sh:42-47` only ever appends. A line matching
12
+ `^- \*\*specialists:<name>\*\*:` in `agents/admin/AGENTS.md` is preserved verbatim and a
13
+ missing one is appended; nothing is ever removed. So the admin's routing prose keeps
14
+ advertising a specialist the operator switched off, and dispatch fails inside a turn
15
+ against a file that is gone.
16
+
17
+ `platform/plugins/admin/mcp/src/index.ts:520-533` walks the AGENTS.md entries against
18
+ `specialists/agents/` and calls a name with no file `missing file`. An operator action is
19
+ rendered as an install fault.
20
+
21
+ ## What the fix delivers
22
+
23
+ 1. A name the operator disabled has no routing line.
24
+ 2. Re-enabling restores it.
25
+ 3. `system-status` reports `disabled` and `missing file` as different things, and counts
26
+ them separately.
27
+
28
+ ## Surface 1 — `agents-md-bootstrap.sh` reconciles
29
+
30
+ The store is `<accountDir>/agents-disabled.json`, `{ "disabled": ["<basename>.md", …] }`,
31
+ written whole at 0600 by `platform/ui/server/routes/admin/agents.ts`. The bootstrap reads
32
+ it with the same python3 extractor `provision-account-dir.sh:264-276` already uses, which
33
+ accepts only strings ending `.md` with no `/` and no `..`.
34
+
35
+ **The name rule.** A store entry maps to a routing line by stripping `.md`. One rule, no
36
+ branch: all twelve files under `platform/templates/specialists/agents/` have `name:` equal
37
+ to their filename, the disable route keys on `slug` equal to the basename, and the status
38
+ walker at `index.ts:526` already maps a name back to `<name>.md`.
39
+
40
+ **The reconcile rule.** A name in the store gets no routing line. It applies at both points
41
+ in the script: an existing line for that name is removed, and the append loop skips that
42
+ name. Stating it once and applying it uniformly is what makes the invariant hold in the
43
+ drift state the standing `op=agent-parity` check exists to catch (store names it, file is
44
+ still live) rather than only on the happy path.
45
+
46
+ **An unreadable store withholds nothing.** `provision-account-dir.sh:293` already prints
47
+ `specialists agents-disabled-store-unreadable — withholding nothing` and carries on, so
48
+ the prose posture matches the file-withholding posture that runs immediately before it.
49
+
50
+ **Observability.** The existing line stays byte-identical, because the current suite
51
+ asserts its counts:
52
+
53
+ ```
54
+ [admin-bootstrap] AGENTS.md specialists=<n> appended=<m> existing=<k>
55
+ ```
56
+
57
+ A second line is added:
58
+
59
+ ```
60
+ [agents-md] op=specialist-lines account=<id8> kept=<n> withheld=<comma-list|none|store-unreadable>
61
+ ```
62
+
63
+ `id8` is the first eight characters of the account directory's basename. `kept` counts the
64
+ `- **specialists:` lines in AGENTS.md after the run. `withheld` names the store's entries,
65
+ sorted, as names rather than basenames; `none` when the store is absent or empty; the
66
+ literal `store-unreadable` when it is present but does not parse.
67
+
68
+ ## Surface 2 — `system-status` separates disabled from missing
69
+
70
+ The classification at `index.ts:520-533` moves into a new
71
+ `platform/plugins/admin/mcp/src/specialist-registry.ts`, beside `skill-resolution.ts` and
72
+ `store-skill-core.ts`, which are the same shape: a module `index.ts` imports and a
73
+ `node:test` suite covers. It exports one function taking the AGENTS.md text, the
74
+ specialists directory, the account directory and whether Chrome is up, and returning the
75
+ per-name lines plus the two counts.
76
+
77
+ Status, in order:
78
+
79
+ - the file exists in `specialists/agents/` → `ok`, except `personal-assistant` with Chrome
80
+ down, which keeps today's `degraded — chrome not running`;
81
+ - otherwise the name is in the store → `disabled (operator)`;
82
+ - otherwise → `missing file`.
83
+
84
+ The header becomes `<n> registered, missing=<x> disabled=<y>`. An unreadable store is
85
+ tolerated the same way the bootstrap tolerates it: no names classify as disabled, and the
86
+ absent file stays `missing file`, which is the honest answer when the store cannot be read.
87
+
88
+ With surface 1 in place a disabled agent has no AGENTS.md line at all, so in the steady
89
+ state `disabled=0`. The classification earns its place in the drift window: an account
90
+ disabled before this change carries the stale line until a provisioning run or a
91
+ disable/enable call reconciles it, and that is exactly when the operator needs the line to
92
+ read `disabled` rather than `missing file`.
93
+
94
+ ## Surface 3 — the disable path reconciles immediately
95
+
96
+ `agents.ts` currently leaves AGENTS.md untouched, so prose is only correct after the next
97
+ provisioning run. After the file move and the store write, both handlers exec
98
+ `bash <PLATFORM_ROOT>/scripts/lib/agents-md-bootstrap.sh <accountDir>` with a timeout.
99
+ `platform/scripts` ships in the payload (`packages/create-maxy-code/scripts/bundle.js:318`
100
+ copies the whole `platform` tree minus the listed exclusions), and `claude-info.ts:33`
101
+ is the precedent for `execFileSync` in an admin route.
102
+
103
+ Enable needs no separate restore path. The file is live again by the time the script runs,
104
+ so the append loop puts the line back and the store no longer names it.
105
+
106
+ **Failure posture.** A failed exec does not turn the request into a 500: the file move and
107
+ the store write already happened, and dispatch is already stopped, so reporting the whole
108
+ disable as failed would be false. The handler logs
109
+ `[admin/agents] op=agents-md-reconcile accountId=<id8> slug=<slug> outcome=failed reason="…"`
110
+ and the response carries `routingReconciled: false`, so no caller is told the prose is
111
+ correct when it is not.
112
+
113
+ ## Tests
114
+
115
+ **Shell, `platform/scripts/__tests__/agents-md-bootstrap.test.sh`.** Against the existing
116
+ three-specialist temp account: a store naming one of them leaves no line for it and keeps
117
+ the other two, and the `op=specialist-lines` line reports `kept=2 withheld=<that name>`;
118
+ clearing the store restores the line on the next run; a malformed store removes nothing and
119
+ reports `withheld=store-unreadable`. The pre-existing cases run unchanged with no store on
120
+ disk.
121
+
122
+ **Route, `platform/ui/server/routes/admin/__tests__/agents.test.ts`.** The harness mocks
123
+ `PLATFORM_ROOT` to a temp tree; the real bootstrap script is copied into
124
+ `<platformRoot>/scripts/lib/` so the exec runs the shipped code rather than a stub. A
125
+ disable leaves no `specialists:<name>` line in the account's AGENTS.md; the following
126
+ enable puts it back.
127
+
128
+ **MCP, `platform/plugins/admin/mcp/src/__tests__/specialist-registry.test.ts`
129
+ (`node:test`).** A temp account with three AGENTS.md entries: one live file, one named by
130
+ the store, one neither. Asserts `ok`, `disabled (operator)`, `missing file`, and
131
+ `missing=1 disabled=1`. A `missing file` line for a name in the store is a failure.
132
+
133
+ ## Out of scope
134
+
135
+ - The disable mechanism, the quarantine directory, and the store format (Task 1996).
136
+ - What AGENTS.md is for, and its append-only posture for anything that is not a specialist
137
+ routing line.
138
+ - The `op=agent-parity` check, which already covers store-versus-directory drift.
139
+ - `AGENTS.md` write permissions, which is Task 2057.
@@ -21,6 +21,7 @@ import QRCode from "qrcode";
21
21
  import { getSession, closeDriver } from "./lib/neo4j.js";
22
22
  import { findSkillOwners, searchSkills, computePluginReadHint, loadSkill, parseRequiredInputs, adminAuthoringSkillBlock, skillSlugFromReadArgs, AUTHORING_DISPATCH_HINT } from "./skill-resolution.js";
23
23
  import { storeSkillToAccount } from "./store-skill-core.js";
24
+ import { classifySpecialists } from "./specialist-registry.js";
24
25
  import { resolvePublicHostname } from "./lib/public-hostname.js";
25
26
  import { resolveAdminHostname } from "./lib/admin-hostname.js";
26
27
  import { publishSite } from "./tools/publish-site.js";
@@ -434,25 +435,22 @@ eagerTool(server, "system-status", "Check health of all SiteDesk platform servic
434
435
  }
435
436
  else {
436
437
  const content = readFileSync(agentsMdPath, "utf-8");
437
- const entries = content.match(/^- \*\*specialists:([^*]+)\*\*/gm);
438
- if (!entries || entries.length === 0) {
438
+ // Task 2016 — `missing` and `disabled` are different answers. An agent
439
+ // the operator switched off has no file here by design, and reporting
440
+ // that as a fault trains the operator to ignore the real ones.
441
+ const report = classifySpecialists({
442
+ agentsMd: content,
443
+ specialistsAgentsDir,
444
+ accountDir,
445
+ chromeUp,
446
+ });
447
+ if (report.registered === 0) {
439
448
  checks.specialists = "not configured (AGENTS.md empty)";
440
449
  }
441
450
  else {
442
- const lines = [];
443
- for (const entry of entries) {
444
- const nameMatch = entry.match(/specialists:([^*]+)/);
445
- if (!nameMatch)
446
- continue;
447
- const name = nameMatch[1];
448
- const fileExists = existsSync(join(specialistsAgentsDir, `${name}.md`));
449
- let status = fileExists ? "ok" : "missing file";
450
- if (name === "personal-assistant" && !chromeUp) {
451
- status = "degraded — chrome not running";
452
- }
453
- lines.push(` ${name}: ${status}`);
454
- }
455
- checks.specialists = `${entries.length} registered\n${lines.join("\n")}`;
451
+ checks.specialists =
452
+ `${report.registered} registered, missing=${report.missing} disabled=${report.disabled}\n` +
453
+ report.lines.join("\n");
456
454
  }
457
455
  }
458
456
  }