@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.
- package/package.json +1 -1
- package/payload/platform/docs/superpowers/plans/2026-07-27-task-2016-disabled-agent-routing.md +624 -0
- package/payload/platform/docs/superpowers/specs/2026-07-27-task-2016-disabled-agent-routing-design.md +139 -0
- package/payload/platform/plugins/admin/mcp/dist/index.js +14 -16
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/admin/mcp/dist/specialist-registry.d.ts +14 -0
- package/payload/platform/plugins/admin/mcp/dist/specialist-registry.d.ts.map +1 -0
- package/payload/platform/plugins/admin/mcp/dist/specialist-registry.js +57 -0
- package/payload/platform/plugins/admin/mcp/dist/specialist-registry.js.map +1 -0
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +15 -3
- package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +6 -0
- package/payload/platform/plugins/docs/references/admin-ui.md +14 -2
- package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +2 -2
- package/payload/platform/scripts/__tests__/agents-md-bootstrap.test.sh +47 -0
- package/payload/platform/scripts/lib/agents-md-bootstrap.sh +58 -1
- package/payload/platform/services/claude-session-manager/dist/config.d.ts +48 -12
- package/payload/platform/services/claude-session-manager/dist/config.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/config.js +65 -15
- package/payload/platform/services/claude-session-manager/dist/config.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/index.js +7 -4
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/rootless-client-audit.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/rootless-client-audit.js +17 -2
- package/payload/platform/services/claude-session-manager/dist/rootless-client-audit.js.map +1 -1
- 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
|
-
|
|
438
|
-
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
}
|