@rubytech/create-sitedesk-code 0.1.507 → 0.1.508

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 (78) hide show
  1. package/package.json +2 -2
  2. package/payload/platform/config/brand.json +12 -0
  3. package/payload/platform/plugins/admin/PLUGIN.md +10 -5
  4. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +91 -13
  5. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +7 -0
  6. package/payload/platform/plugins/docs/references/admin-ui.md +90 -12
  7. package/payload/platform/plugins/memory/PLUGIN.md +6 -3
  8. package/payload/platform/scripts/__tests__/provision-honours-disabled-agents.test.sh +84 -0
  9. package/payload/platform/scripts/check-risk-class.mjs +7 -2
  10. package/payload/platform/scripts/check-specialist-tool-surface.mjs +5 -2
  11. package/payload/platform/scripts/lib/canonical-tool-names.mjs +7 -4
  12. package/payload/platform/scripts/lib/provision-account-dir.sh +43 -0
  13. package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.d.ts +1 -1
  14. package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.d.ts.map +1 -1
  15. package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.js +56 -3
  16. package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.js.map +1 -1
  17. package/payload/platform/services/claude-session-manager/dist/tool-surface.d.ts +19 -5
  18. package/payload/platform/services/claude-session-manager/dist/tool-surface.d.ts.map +1 -1
  19. package/payload/platform/services/claude-session-manager/dist/tool-surface.js +50 -11
  20. package/payload/platform/services/claude-session-manager/dist/tool-surface.js.map +1 -1
  21. package/payload/server/public/activity.html +5 -5
  22. package/payload/server/public/agents.html +4 -4
  23. package/payload/server/public/assets/{AdminLoginScreens-YT7jsh_H.js → AdminLoginScreens-DKeCt1uf.js} +1 -1
  24. package/payload/server/public/assets/{AdminShell-Ckht9hmU.js → AdminShell-HeELCCjJ.js} +1 -1
  25. package/payload/server/public/assets/{activity-DH3Wgc3O.js → activity-BEIDAxDA.js} +1 -1
  26. package/payload/server/public/assets/admin-C0-WU1sf.js +1 -0
  27. package/payload/server/public/assets/agents-DjUvMFTj.js +1 -0
  28. package/payload/server/public/assets/{browser-Bym8QhfA.js → browser-GkVTGqlb.js} +1 -1
  29. package/payload/server/public/assets/{calendar-CFnufJOy.js → calendar-CSqJeFEg.js} +1 -1
  30. package/payload/server/public/assets/chat-C0Ps84U5.js +1 -0
  31. package/payload/server/public/assets/chevron-left-DQ2uNlu9.js +1 -0
  32. package/payload/server/public/assets/chevron-right-C4QCiRGD.js +1 -0
  33. package/payload/server/public/assets/clock-BEeQ7JxX.js +1 -0
  34. package/payload/server/public/assets/data-CGuNfym7.js +1 -0
  35. package/payload/server/public/assets/{file-text-zch0E1GJ.js → file-text-DiscM2pP.js} +1 -1
  36. package/payload/server/public/assets/{graph-BavHLzpV.js → graph-CRP5DM8C.js} +1 -1
  37. package/payload/server/public/assets/{graph-labels-CBwh4g-6.js → graph-labels-Du-6KoBE.js} +1 -1
  38. package/payload/server/public/assets/{maximize-2-bTn_Ozpi.js → maximize-2-D-H_uIV-.js} +1 -1
  39. package/payload/server/public/assets/{operator-a4IOwf62.js → operator-D5rA76YY.js} +1 -1
  40. package/payload/server/public/assets/{page-3T2mE63b.js → page-CHK1415C.js} +1 -1
  41. package/payload/server/public/assets/{page-D6M1CmPe.js → page-DZE3H_n4.js} +1 -1
  42. package/payload/server/public/assets/{public-B3y_YU1G.js → public-DFHXVkdD.js} +1 -1
  43. package/payload/server/public/assets/{rotate-ccw-BGvzNB1b.js → rotate-ccw-CesEJsuO.js} +1 -1
  44. package/payload/server/public/assets/{routines-C7Dr16am.js → routines-DYMYY_r8.js} +1 -1
  45. package/payload/server/public/assets/{skills-DFeb3XO3.js → skills-67jtJ16D.js} +1 -1
  46. package/payload/server/public/assets/{tasks-CSfnCKJe.js → tasks-D2C2a-uk.js} +1 -1
  47. package/payload/server/public/assets/{time-entry-format-BgZqOOqG.js → time-entry-format-BixTGQAd.js} +1 -1
  48. package/payload/server/public/assets/{triangle-alert-DBJ7xz_t.js → triangle-alert-Co9RWb9q.js} +1 -1
  49. package/payload/server/public/assets/{useCopyFeedback-BFOujMl-.js → useCopyFeedback-7zwf9_w4.js} +1 -1
  50. package/payload/server/public/assets/useSubAccountSwitcher-Bg72hdQ8.js +9 -0
  51. package/payload/server/public/assets/useSubAccountSwitcher-DE4v5_Fz.css +1 -0
  52. package/payload/server/public/assets/{useVoiceRecorder-BP6CpYQD.js → useVoiceRecorder-CZDDiwMD.js} +1 -1
  53. package/payload/server/public/assets/{wrench-DdaRhfzS.js → wrench-CjQqdj4P.js} +1 -1
  54. package/payload/server/public/brand/fonts/inter-600.woff2 +0 -0
  55. package/payload/server/public/brand/fonts/inter-700.woff2 +0 -0
  56. package/payload/server/public/brand/fonts/inter-800.woff2 +0 -0
  57. package/payload/server/public/brand-defaults.css +22 -1
  58. package/payload/server/public/browser.html +4 -4
  59. package/payload/server/public/calendar.html +7 -7
  60. package/payload/server/public/chat.html +13 -13
  61. package/payload/server/public/data.html +11 -11
  62. package/payload/server/public/graph.html +9 -9
  63. package/payload/server/public/index.html +14 -14
  64. package/payload/server/public/operator.html +14 -14
  65. package/payload/server/public/public.html +13 -13
  66. package/payload/server/public/routines.html +6 -6
  67. package/payload/server/public/skills.html +5 -5
  68. package/payload/server/public/tasks.html +6 -6
  69. package/payload/server/server.js +882 -251
  70. package/payload/server/public/assets/admin-DetvulAu.js +0 -1
  71. package/payload/server/public/assets/agents-X_IvDHLZ.js +0 -1
  72. package/payload/server/public/assets/chat-wrckyIpw.js +0 -1
  73. package/payload/server/public/assets/chevron-left-Dtk3jEK5.js +0 -1
  74. package/payload/server/public/assets/chevron-right-Dih6IZ8B.js +0 -1
  75. package/payload/server/public/assets/clock-iJCrrmWM.js +0 -1
  76. package/payload/server/public/assets/data-D76--TYs.js +0 -1
  77. package/payload/server/public/assets/useSubAccountSwitcher-0PfRrN6i.js +0 -9
  78. package/payload/server/public/assets/useSubAccountSwitcher-BkQ5tA8A.css +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-sitedesk-code",
3
- "version": "0.1.507",
3
+ "version": "0.1.508",
4
4
  "description": "Install SiteDesk — automated back office for independent building contractors",
5
5
  "bin": {
6
6
  "create-sitedesk-code": "./dist/index.js"
@@ -13,7 +13,7 @@
13
13
  "build": "tsc",
14
14
  "bundle": "node scripts/bundle.js",
15
15
  "test": "npm run build && node --test 'dist/__tests__/*.test.js'",
16
- "prepublishOnly": "bash ../../platform/scripts/verify-skill-tool-surface.sh && node ../../platform/scripts/check-plugin-tools-mcp-consistency.mjs && node ../../platform/scripts/check-risk-class.mjs && node ../../platform/scripts/check-specialist-tool-surface.mjs && node ../../platform/scripts/check-agent-contract.mjs && node ../../platform/scripts/check-routing-prose-bash.mjs && node ../../platform/scripts/check-no-task-id-leaks.mjs && node ../../platform/scripts/check-no-esm-require.mjs && node ../../platform/scripts/check-no-raw-mcp-registrations.mjs && node ../../platform/scripts/check-skill-load-coverage.mjs && node ../../platform/scripts/check-architecture-skill-no-drift.mjs && node ../../platform/ui/scripts/check-route-wiring.mjs && node ../../platform/ui/scripts/check-edge-admin-routes.mjs && npm run build && node --test 'dist/__tests__/*.test.js' && chmod +x dist/index.js && npm run bundle && bash ../../platform/scripts/smoke-boot-services.sh && node ../../platform/ui/scripts/check-bundle-node-imports.mjs --dir=./payload/server/public/assets"
16
+ "prepublishOnly": "bash ../../platform/scripts/verify-skill-tool-surface.sh && node ../../platform/scripts/check-plugin-tools-mcp-consistency.mjs && node ../../platform/scripts/check-risk-class.mjs && node ../../platform/scripts/check-specialist-tool-surface.mjs && node ../../platform/scripts/check-agent-contract.mjs && node ../../platform/scripts/check-routing-prose-bash.mjs && node ../../platform/scripts/check-no-task-id-leaks.mjs && node ../../platform/scripts/check-no-esm-require.mjs && node ../../platform/scripts/check-no-raw-mcp-registrations.mjs && node ../../platform/scripts/check-skill-load-coverage.mjs && node ../../platform/scripts/check-architecture-skill-no-drift.mjs && node ../../platform/ui/scripts/check-brand-fonts.mjs && node ../../platform/ui/scripts/check-route-wiring.mjs && node ../../platform/ui/scripts/check-edge-admin-routes.mjs && npm run build && node --test 'dist/__tests__/*.test.js' && chmod +x dist/index.js && npm run bundle && bash ../../platform/scripts/smoke-boot-services.sh && node ../../platform/ui/scripts/check-bundle-node-imports.mjs --dir=./payload/server/public/assets"
17
17
  },
18
18
  "files": [
19
19
  "dist",
@@ -140,6 +140,18 @@
140
140
  {
141
141
  "weight": 500,
142
142
  "file": "inter-500.woff2"
143
+ },
144
+ {
145
+ "weight": 600,
146
+ "file": "inter-600.woff2"
147
+ },
148
+ {
149
+ "weight": 700,
150
+ "file": "inter-700.woff2"
151
+ },
152
+ {
153
+ "weight": 800,
154
+ "file": "inter-800.woff2"
143
155
  }
144
156
  ]
145
157
  },
@@ -148,11 +148,16 @@ tools:
148
148
  adminAllowlist: true
149
149
  riskClass: write_local
150
150
  hidden:
151
- - remote-auth-status
152
- - remote-auth-set-password
153
- - remote-auth-revoke-password
154
- - qr-generate
155
- - wifi
151
+ - name: remote-auth-status
152
+ riskClass: read
153
+ - name: remote-auth-set-password
154
+ riskClass: write_local
155
+ - name: remote-auth-revoke-password
156
+ riskClass: write_local
157
+ - name: qr-generate
158
+ riskClass: write_local
159
+ - name: wifi
160
+ riskClass: exec
156
161
  always: true
157
162
  embed: false
158
163
  mcp:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: platform-architecture
3
3
  description: Use when grounding any documented-surface claim about what SiteDesk ships — plugins, skills, specialists, install/deploy flows, internals. This is the install catalogue, not evidence of what is enabled on the current account. For install state on this account, call `capabilities-here`; for documented surface, cite the `Source:` URL inline.
4
- content-hash: sha256:314794449900df77759bdbbc4bbeadb2afb0e8d6479371d935e7030a18d47485
4
+ content-hash: sha256:b15d0e491d957f75523ecf7056f8004bafcdf448767c0c3dd64b40a73926b58b
5
5
  brand: sitedesk-code
6
6
  product-name: SiteDesk
7
7
  ---
@@ -3104,8 +3104,11 @@ not-found|rm-error>`.
3104
3104
 
3105
3105
  ### Agents
3106
3106
 
3107
- The Agents surface (`/agents`) is the admin view of the account's agents, of two
3108
- kinds, each card tagged `Public` or `Specialist`:
3107
+ The Agents surface (`/agents`) is the admin view of the account's agents. Each
3108
+ card is tagged by **origin**, which says who put the agent there and therefore
3109
+ what the operator may do with it. Origin is a different question to `kind`, which
3110
+ says what shape the agent is: a shipped specialist and a user-created one are
3111
+ both `kind: 'specialist'` and differ only by origin.
3109
3112
 
3110
3113
  - **Public agents** — the per-agent directories under `<accountDir>/agents/<slug>/`,
3111
3114
  each carrying a `config.json` (`displayName`, `status`, `model`,
@@ -3116,19 +3119,63 @@ kinds, each card tagged `Public` or `Specialist`:
3116
3119
  to the public URL (computed client-side: an `admin.` host maps to the `public.`
3117
3120
  host), plus a **delete** behind a confirm that runs the loud-fail `DELETE`
3118
3121
  (graph projection cleanup precedes file removal).
3119
- - **User-created specialists** — the agent files under
3122
+ - **User-created specialists** (`origin: 'specialist'`) — the agent files under
3120
3123
  `<accountDir>/plugins/<plugin>/agents/<plugin>--<name>.md` (written by
3121
- `agent-builder`). `<accountDir>/plugins` is the user-created tree, so only
3122
- user-created specialists list here; shipped premium and core specialists (whose
3123
- plugins live under `PLATFORM_ROOT`) do not. The specialist modal shows the
3124
- model, tools, the owning plugin's skills, and the system-prompt body. Specialists
3125
- are authored, edited, and deleted through chat (`agent-builder` /
3126
- `specialist-management`), so the specialist modal offers no delete or open-agent
3127
- link.
3124
+ `agent-builder`). The specialist modal shows the model, tools, the owning
3125
+ plugin's skills, and the system-prompt body. Specialists are authored, edited,
3126
+ and deleted through chat (`agent-builder` / `specialist-management`), so the
3127
+ specialist modal offers no delete or open-agent link.
3128
+ - **Shipped specialists** (`origin: 'shipped'`) the union of
3129
+ `<accountDir>/specialists/agents/`, `<accountDir>/specialists/agents-disabled/`
3130
+ and `PLATFORM_ROOT/templates/specialists/agents/`, in that precedence order.
3131
+ `sidebar-artefacts.ts` reads the same union minus the quarantine directory, so
3132
+ a disabled agent still appears there through its bundled-template row. The
3133
+ route once walked only the account plugins tree, so the twelve
3134
+ highest-capability agents on the box were the ones the operator could not see.
3135
+ These are the only agents that can be disabled, and the route enforces that
3136
+ rather than relying on the UI to withhold the control.
3128
3137
 
3129
3138
  The former "Public" flyout toggle in the account menu was removed; this page is
3130
3139
  the sole agent surface.
3131
3140
 
3141
+ **Risk class.** Every classified card carries a green/amber/red badge derived
3142
+ from the agent's declared `tools:` line: green when every tool is `read`, amber
3143
+ when the worst is `write_local`, red on any `exec` or `external`, and red when
3144
+ any tool resolves to no class at all. Resolution has two sources, because
3145
+ frontmatter names tools in Claude Code's namespace: a plugin tool is a direct
3146
+ lookup in `ToolSurface.riskByTool`, which is keyed by the same
3147
+ canonical `mcp__plugin_<p>_<p>__<tool>` string frontmatter writes, and the
3148
+ eleven built-ins carry their own table in `server/lib/agent-risk.ts`. An empty
3149
+ tool list is red: no `tools:` line means the full surface, not none of it.
3150
+
3151
+ A public agent carries `risk: null` and shows no badge. Its tool surface
3152
+ resolves through `ADMIN_CORE_TOOLS` plus the plugin allowlists rather than a
3153
+ `tools:` line, and classifying its empty list would paint every public agent red
3154
+ on a premise nothing established.
3155
+
3156
+ The detail modal renders one chip per declared tool carrying **that tool's own**
3157
+ class, so a `read` tool inside a red agent still reads as harmless; a tool with
3158
+ no class is dashed and uncoloured rather than painted as if it were understood.
3159
+ Badge colours come from the shared semantic status set: `-solid` with white text
3160
+ for the badge, `-tint` with `-textOnTint` for a chip, never white.
3161
+
3162
+ **Disable.** `<accountDir>/specialists/agents/` is the live dispatchable set —
3163
+ `spawn-context.ts` reads it to build the spawn manifest — so disabling **moves**
3164
+ the agent file to `<accountDir>/specialists/agents-disabled/` and records the
3165
+ basename in a 0600 `<accountDir>/agents-disabled.json`. A flag alone would leave
3166
+ the agent running while the card claimed otherwise.
3167
+
3168
+ The file is moved, never deleted: a premium `--` agent and an operator-edited
3169
+ override each exist only in the account dir, so deleting one and later restoring
3170
+ from the bundled template would hand back a different agent than the operator
3171
+ switched off. A disabled agent stays listed, marked `Disabled`, because an agent
3172
+ that vanished from the surface that disabled it could never be switched back on.
3173
+
3174
+ `provision-account-dir.sh` re-reads the store after its core-specialist recopy
3175
+ and removes anything named in it, so an upgrade does not silently re-deliver a
3176
+ disabled agent. An unreadable store withholds nothing and says so: a corrupt
3177
+ file is not evidence that an agent was disabled.
3178
+
3132
3179
  **Account scope.** The route (`server/routes/admin/agents.ts`) never infers an
3133
3180
  account from device state. The two reads resolve from the caller's admin session
3134
3181
  (`requireAdminSession` + the shared `accountDirForSession` in
@@ -3142,8 +3189,10 @@ explicit `?plugin=` parameter, not inferred from `--` in the slug.
3142
3189
 
3143
3190
  | Route | Behaviour |
3144
3191
  |-------|-----------|
3145
- | `GET /api/admin/agents` | Session-scoped. Lists the session account's public `agents/*/` dirs (never the `admin` agent) plus its user-created specialists from `plugins/*/agents/*.md`, each row tagged `kind`. Returns `{agents, accountId, skipped, specialistsSkipped}``accountId` names the account resolved (the client sends it back on a delete); `skipped` counts public dirs whose `config.json` failed to parse; `specialistsSkipped` counts specialist `.md` files with no parseable `name`. 401 when the session maps to no account. |
3146
- | `GET /api/admin/agents/:slug` | Session-scoped. Without `?plugin=`, returns a public agent's config fields + four owned docs + a `present` map (a missing or unreadable doc is `''`/`present.<role>=false`, never a 500). With `?plugin=`, returns that plugin's specialist `{kind, model, tools, systemPrompt, skills}`. Unknown 404s, 401 as above. |
3192
+ | `GET /api/admin/agents` | Session-scoped. Lists the session account's public `agents/*/` dirs (never the `admin` agent), its user-created specialists from `plugins/*/agents/*.md`, and its shipped specialists, each row tagged `kind` and `origin` and carrying `risk`, `worstTool`, `unresolved` and `disabled`. Returns `{agents, accountId, skipped, specialistsSkipped, shippedSkipped}`. 401 when the session maps to no account. |
3193
+ | `GET /api/admin/agents/:slug` | Session-scoped. Without `?plugin=` or `?origin=`, returns a public agent's config fields + four owned docs + a `present` map (a missing or unreadable doc is `''`/`present.<role>=false`, never a 500). With `?plugin=`, that plugin's user-created specialist; with `?origin=shipped`, the shipped one. Both specialist shapes add `{risk, worstTool, unresolved, byTool, disabled}`, where `byTool` is one class per declared tool. Selection is explicit rather than inferred, because a premium file and a user-created specialist under a plugin of the same name produce the same slug. Unknown 404s, 401 as above. |
3194
+ | `POST /api/admin/agents/:slug/disable?accountId=` | Shipped agents only, enforced here: a slug present in none of the three shipped directories 404s. Moves the file from `specialists/agents/` to `specialists/agents-disabled/` and records the basename in the 0600 store. Returns `{ok, moved}`; `moved:false` means the agent existed only as a bundled template, so nothing was there to move and the store entry is what stops the next provisioning run delivering it. The store is read strictly before anything moves, so an unreadable store 500s with the file untouched rather than rewriting the file whole from an empty set and dropping every other disabled agent. Same `accountId` contract as delete. |
3195
+ | `POST /api/admin/agents/:slug/enable?accountId=` | Covers both of disable's outcomes. Returns `{ok, restored}`: a quarantined file moves back (`restored:true`); a bundled-only agent has nothing to move, so clearing the store entry is the whole job (`restored:false`), which is what stops provisioning withholding it. Treating that second case as "nothing to restore" made disable a one-way door. 404 only when neither directory nor store knows the agent. |
3147
3196
  | `DELETE /api/admin/agents/:slug?accountId=` | Public agents only, on the named validated account. Removes the dir after `deleteAgentProjection`; refuses the `admin` slug (403) and a missing/unknown `accountId` (400) with no write. Loud-fail: a graph-cleanup throw aborts the file removal. |
3148
3197
  | `POST /api/admin/agents/:slug/project?accountId=` | Re-projects the named account's on-disk agent into the graph. Same `accountId` contract as delete. |
3149
3198
 
@@ -3170,6 +3219,35 @@ Agents page survived undiagnosed on a multi-account install.
3170
3219
  - `op=delete accountId=<id8> slug=<…> outcome=<ok|failed>` with
3171
3220
  `reason=<graph-cleanup-failed|rm-error>` on failure (files preserved on a
3172
3221
  graph-cleanup throw).
3222
+ - `op=classify account=<id8> agent=<name> origin=<shipped|public|specialist>
3223
+ risk=<green|amber|red> worstTool=<tool> tools=<n> unresolved=<comma-list|none>
3224
+ disabled=<bool>` — one line per classified agent per listing, so it fires on
3225
+ every page load. `worstTool` makes a wrong class diagnosable without
3226
+ re-deriving it. **A non-empty `unresolved=` is the one to act on**: the agent
3227
+ is forced red, and the cause is that the built-in table or the registry
3228
+ mapping has drifted, not that the agent changed.
3229
+ - `op=risk-surface status=load-failed reason=<msg>` — the plugin registry did
3230
+ not parse, so every row is forced red and the listing returns
3231
+ `riskSurfaceFailed: true`. Without the forced red, every plugin tool would
3232
+ land in `unresolved` and a registry outage would read as agent drift across
3233
+ every card at once; without the response field, a red row would be
3234
+ indistinguishable from a genuinely dangerous agent, because nothing resolved
3235
+ so `unresolved` is empty and the per-tool explanation cannot fire.
3236
+ - `op=disabled-store status=parse-failed reason=<msg>` — the listing then treats
3237
+ every agent as enabled, which is what they actually are. A read surface shows
3238
+ the true dispatchable set rather than a comforting one.
3239
+ - `op=<disable|enable> accountId=<id8> slug=<…> outcome=<ok|failed>`, with
3240
+ `moved=<bool>` on disable and `reason=nothing-quarantined` on a failed enable.
3241
+
3242
+ **Disable is a no-event failure, so it needs a standing check.** If the store
3243
+ names an agent whose file is still in `specialists/agents/`, nothing throws, the
3244
+ card reads disabled, and the agent keeps being dispatched. The five-minute
3245
+ `account-dir-schema-reconcile` pass reports it as
3246
+ `[fs-reconcile] op=agent-parity account=<id8> disabled=<n> stillOnDisk=<list>`,
3247
+ fired only on disagreement. The per-cycle heartbeat is `agent-drift=0` on that
3248
+ pass's summary line, which is what makes the absence of a per-account line
3249
+ readable as "clean" rather than "never audited" — grep the summary line, not the
3250
+ per-account one, to confirm the check is running.
3173
3251
 
3174
3252
  ### Graph
3175
3253
 
@@ -9,6 +9,13 @@ 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-07-25 (0.1.508)
13
+
14
+ - SiteDesk's screens now use a single, cleaner typeface throughout, and the old olive-green colour is gone.
15
+ - You can now disable an agent from the Agents page, and each one shows its risk level.
16
+ - Dashboard sections appear as soon as each one loads, instead of the whole page waiting on the slowest one.
17
+ - Real Agent's icons and imagery were redrawn to match its colour palette.
18
+
12
19
  ## 2026-07-25 (0.1.507)
13
20
 
14
21
  - Chat now updates the moment a message or permission prompt arrives, instead of checking every couple of seconds.
@@ -151,8 +151,11 @@ not-found|rm-error>`.
151
151
 
152
152
  ### Agents
153
153
 
154
- The Agents surface (`/agents`) is the admin view of the account's agents, of two
155
- kinds, each card tagged `Public` or `Specialist`:
154
+ The Agents surface (`/agents`) is the admin view of the account's agents. Each
155
+ card is tagged by **origin**, which says who put the agent there and therefore
156
+ what the operator may do with it. Origin is a different question to `kind`, which
157
+ says what shape the agent is: a shipped specialist and a user-created one are
158
+ both `kind: 'specialist'` and differ only by origin.
156
159
 
157
160
  - **Public agents** — the per-agent directories under `<accountDir>/agents/<slug>/`,
158
161
  each carrying a `config.json` (`displayName`, `status`, `model`,
@@ -163,19 +166,63 @@ kinds, each card tagged `Public` or `Specialist`:
163
166
  to the public URL (computed client-side: an `admin.` host maps to the `public.`
164
167
  host), plus a **delete** behind a confirm that runs the loud-fail `DELETE`
165
168
  (graph projection cleanup precedes file removal).
166
- - **User-created specialists** — the agent files under
169
+ - **User-created specialists** (`origin: 'specialist'`) — the agent files under
167
170
  `<accountDir>/plugins/<plugin>/agents/<plugin>--<name>.md` (written by
168
- `agent-builder`). `<accountDir>/plugins` is the user-created tree, so only
169
- user-created specialists list here; shipped premium and core specialists (whose
170
- plugins live under `PLATFORM_ROOT`) do not. The specialist modal shows the
171
- model, tools, the owning plugin's skills, and the system-prompt body. Specialists
172
- are authored, edited, and deleted through chat (`agent-builder` /
173
- `specialist-management`), so the specialist modal offers no delete or open-agent
174
- link.
171
+ `agent-builder`). The specialist modal shows the model, tools, the owning
172
+ plugin's skills, and the system-prompt body. Specialists are authored, edited,
173
+ and deleted through chat (`agent-builder` / `specialist-management`), so the
174
+ specialist modal offers no delete or open-agent link.
175
+ - **Shipped specialists** (`origin: 'shipped'`) the union of
176
+ `<accountDir>/specialists/agents/`, `<accountDir>/specialists/agents-disabled/`
177
+ and `PLATFORM_ROOT/templates/specialists/agents/`, in that precedence order.
178
+ `sidebar-artefacts.ts` reads the same union minus the quarantine directory, so
179
+ a disabled agent still appears there through its bundled-template row. The
180
+ route once walked only the account plugins tree, so the twelve
181
+ highest-capability agents on the box were the ones the operator could not see.
182
+ These are the only agents that can be disabled, and the route enforces that
183
+ rather than relying on the UI to withhold the control.
175
184
 
176
185
  The former "Public" flyout toggle in the account menu was removed; this page is
177
186
  the sole agent surface.
178
187
 
188
+ **Risk class.** Every classified card carries a green/amber/red badge derived
189
+ from the agent's declared `tools:` line: green when every tool is `read`, amber
190
+ when the worst is `write_local`, red on any `exec` or `external`, and red when
191
+ any tool resolves to no class at all. Resolution has two sources, because
192
+ frontmatter names tools in Claude Code's namespace: a plugin tool is a direct
193
+ lookup in `ToolSurface.riskByTool`, which is keyed by the same
194
+ canonical `mcp__plugin_<p>_<p>__<tool>` string frontmatter writes, and the
195
+ eleven built-ins carry their own table in `server/lib/agent-risk.ts`. An empty
196
+ tool list is red: no `tools:` line means the full surface, not none of it.
197
+
198
+ A public agent carries `risk: null` and shows no badge. Its tool surface
199
+ resolves through `ADMIN_CORE_TOOLS` plus the plugin allowlists rather than a
200
+ `tools:` line, and classifying its empty list would paint every public agent red
201
+ on a premise nothing established.
202
+
203
+ The detail modal renders one chip per declared tool carrying **that tool's own**
204
+ class, so a `read` tool inside a red agent still reads as harmless; a tool with
205
+ no class is dashed and uncoloured rather than painted as if it were understood.
206
+ Badge colours come from the shared semantic status set: `-solid` with white text
207
+ for the badge, `-tint` with `-textOnTint` for a chip, never white.
208
+
209
+ **Disable.** `<accountDir>/specialists/agents/` is the live dispatchable set —
210
+ `spawn-context.ts` reads it to build the spawn manifest — so disabling **moves**
211
+ the agent file to `<accountDir>/specialists/agents-disabled/` and records the
212
+ basename in a 0600 `<accountDir>/agents-disabled.json`. A flag alone would leave
213
+ the agent running while the card claimed otherwise.
214
+
215
+ The file is moved, never deleted: a premium `--` agent and an operator-edited
216
+ override each exist only in the account dir, so deleting one and later restoring
217
+ from the bundled template would hand back a different agent than the operator
218
+ switched off. A disabled agent stays listed, marked `Disabled`, because an agent
219
+ that vanished from the surface that disabled it could never be switched back on.
220
+
221
+ `provision-account-dir.sh` re-reads the store after its core-specialist recopy
222
+ and removes anything named in it, so an upgrade does not silently re-deliver a
223
+ disabled agent. An unreadable store withholds nothing and says so: a corrupt
224
+ file is not evidence that an agent was disabled.
225
+
179
226
  **Account scope.** The route (`server/routes/admin/agents.ts`) never infers an
180
227
  account from device state. The two reads resolve from the caller's admin session
181
228
  (`requireAdminSession` + the shared `accountDirForSession` in
@@ -189,8 +236,10 @@ explicit `?plugin=` parameter, not inferred from `--` in the slug.
189
236
 
190
237
  | Route | Behaviour |
191
238
  |-------|-----------|
192
- | `GET /api/admin/agents` | Session-scoped. Lists the session account's public `agents/*/` dirs (never the `admin` agent) plus its user-created specialists from `plugins/*/agents/*.md`, each row tagged `kind`. Returns `{agents, accountId, skipped, specialistsSkipped}``accountId` names the account resolved (the client sends it back on a delete); `skipped` counts public dirs whose `config.json` failed to parse; `specialistsSkipped` counts specialist `.md` files with no parseable `name`. 401 when the session maps to no account. |
193
- | `GET /api/admin/agents/:slug` | Session-scoped. Without `?plugin=`, returns a public agent's config fields + four owned docs + a `present` map (a missing or unreadable doc is `''`/`present.<role>=false`, never a 500). With `?plugin=`, returns that plugin's specialist `{kind, model, tools, systemPrompt, skills}`. Unknown 404s, 401 as above. |
239
+ | `GET /api/admin/agents` | Session-scoped. Lists the session account's public `agents/*/` dirs (never the `admin` agent), its user-created specialists from `plugins/*/agents/*.md`, and its shipped specialists, each row tagged `kind` and `origin` and carrying `risk`, `worstTool`, `unresolved` and `disabled`. Returns `{agents, accountId, skipped, specialistsSkipped, shippedSkipped}`. 401 when the session maps to no account. |
240
+ | `GET /api/admin/agents/:slug` | Session-scoped. Without `?plugin=` or `?origin=`, returns a public agent's config fields + four owned docs + a `present` map (a missing or unreadable doc is `''`/`present.<role>=false`, never a 500). With `?plugin=`, that plugin's user-created specialist; with `?origin=shipped`, the shipped one. Both specialist shapes add `{risk, worstTool, unresolved, byTool, disabled}`, where `byTool` is one class per declared tool. Selection is explicit rather than inferred, because a premium file and a user-created specialist under a plugin of the same name produce the same slug. Unknown 404s, 401 as above. |
241
+ | `POST /api/admin/agents/:slug/disable?accountId=` | Shipped agents only, enforced here: a slug present in none of the three shipped directories 404s. Moves the file from `specialists/agents/` to `specialists/agents-disabled/` and records the basename in the 0600 store. Returns `{ok, moved}`; `moved:false` means the agent existed only as a bundled template, so nothing was there to move and the store entry is what stops the next provisioning run delivering it. The store is read strictly before anything moves, so an unreadable store 500s with the file untouched rather than rewriting the file whole from an empty set and dropping every other disabled agent. Same `accountId` contract as delete. |
242
+ | `POST /api/admin/agents/:slug/enable?accountId=` | Covers both of disable's outcomes. Returns `{ok, restored}`: a quarantined file moves back (`restored:true`); a bundled-only agent has nothing to move, so clearing the store entry is the whole job (`restored:false`), which is what stops provisioning withholding it. Treating that second case as "nothing to restore" made disable a one-way door. 404 only when neither directory nor store knows the agent. |
194
243
  | `DELETE /api/admin/agents/:slug?accountId=` | Public agents only, on the named validated account. Removes the dir after `deleteAgentProjection`; refuses the `admin` slug (403) and a missing/unknown `accountId` (400) with no write. Loud-fail: a graph-cleanup throw aborts the file removal. |
195
244
  | `POST /api/admin/agents/:slug/project?accountId=` | Re-projects the named account's on-disk agent into the graph. Same `accountId` contract as delete. |
196
245
 
@@ -217,6 +266,35 @@ Agents page survived undiagnosed on a multi-account install.
217
266
  - `op=delete accountId=<id8> slug=<…> outcome=<ok|failed>` with
218
267
  `reason=<graph-cleanup-failed|rm-error>` on failure (files preserved on a
219
268
  graph-cleanup throw).
269
+ - `op=classify account=<id8> agent=<name> origin=<shipped|public|specialist>
270
+ risk=<green|amber|red> worstTool=<tool> tools=<n> unresolved=<comma-list|none>
271
+ disabled=<bool>` — one line per classified agent per listing, so it fires on
272
+ every page load. `worstTool` makes a wrong class diagnosable without
273
+ re-deriving it. **A non-empty `unresolved=` is the one to act on**: the agent
274
+ is forced red, and the cause is that the built-in table or the registry
275
+ mapping has drifted, not that the agent changed.
276
+ - `op=risk-surface status=load-failed reason=<msg>` — the plugin registry did
277
+ not parse, so every row is forced red and the listing returns
278
+ `riskSurfaceFailed: true`. Without the forced red, every plugin tool would
279
+ land in `unresolved` and a registry outage would read as agent drift across
280
+ every card at once; without the response field, a red row would be
281
+ indistinguishable from a genuinely dangerous agent, because nothing resolved
282
+ so `unresolved` is empty and the per-tool explanation cannot fire.
283
+ - `op=disabled-store status=parse-failed reason=<msg>` — the listing then treats
284
+ every agent as enabled, which is what they actually are. A read surface shows
285
+ the true dispatchable set rather than a comforting one.
286
+ - `op=<disable|enable> accountId=<id8> slug=<…> outcome=<ok|failed>`, with
287
+ `moved=<bool>` on disable and `reason=nothing-quarantined` on a failed enable.
288
+
289
+ **Disable is a no-event failure, so it needs a standing check.** If the store
290
+ names an agent whose file is still in `specialists/agents/`, nothing throws, the
291
+ card reads disabled, and the agent keeps being dispatched. The five-minute
292
+ `account-dir-schema-reconcile` pass reports it as
293
+ `[fs-reconcile] op=agent-parity account=<id8> disabled=<n> stillOnDisk=<list>`,
294
+ fired only on disagreement. The per-cycle heartbeat is `agent-drift=0` on that
295
+ pass's summary line, which is what makes the absence of a per-account line
296
+ readable as "clean" rather than "never audited" — grep the summary line, not the
297
+ per-account one, to confirm the check is running.
220
298
 
221
299
  ### Graph
222
300
 
@@ -168,9 +168,12 @@ tools:
168
168
  adminAllowlist: true
169
169
  riskClass: read
170
170
  hidden:
171
- - session-compact
172
- - session-compact-status
173
- - image-fetch
171
+ - name: session-compact
172
+ riskClass: write_local
173
+ - name: session-compact-status
174
+ riskClass: read
175
+ - name: image-fetch
176
+ riskClass: read
174
177
  skills:
175
178
  - skills/conversational-memory/SKILL.md
176
179
  - skills/preference-audit/SKILL.md
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env bash
2
+ # Task 1996 — an upgrade must not re-deliver an agent the operator switched off.
3
+ #
4
+ # provision_account_dir wipes every core specialist and re-copies it from the
5
+ # bundled templates on every run. Without this, disabling an agent lapses on the
6
+ # next publish: the file reappears in the dispatchable directory, nothing logs,
7
+ # and the card still reads "disabled". That is the no-event failure the task
8
+ # names, so it is pinned here.
9
+ set -uo pipefail
10
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
+ PLAT_SCRIPTS="$SCRIPT_DIR/.."
12
+ PROJECT_DIR="$(cd "$PLAT_SCRIPTS/.." && pwd)"
13
+ TEMPLATES_DIR="$PROJECT_DIR/templates"
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 TEMPLATES_DIR PROJECT_DIR
21
+ export SCRIPT_DIR="$PLAT_SCRIPTS"
22
+ export USERS_FILE="/nonexistent/users.json"
23
+
24
+ # A real shipped specialist, so the test exercises the same wipe-and-recopy loop
25
+ # a publish runs rather than a synthetic filename.
26
+ DISABLED_AGENT="coding-assistant.md"
27
+
28
+ T1=$(mktemp -d); A1="$T1/acct"
29
+ provision_account_dir "$A1" "dddddddd-dddd-dddd-dddd-dddddddddddd" "house" >/dev/null 2>&1
30
+ assert_present "$A1/specialists/agents/$DISABLED_AGENT" "baseline-agent-delivered"
31
+
32
+ # The operator disables it: the file moves to quarantine and the store names it.
33
+ mkdir -p "$A1/specialists/agents-disabled"
34
+ mv "$A1/specialists/agents/$DISABLED_AGENT" "$A1/specialists/agents-disabled/$DISABLED_AGENT"
35
+ printf '{"disabled":["%s"]}\n' "$DISABLED_AGENT" > "$A1/agents-disabled.json"
36
+
37
+ # The upgrade runs again.
38
+ OUT="$(provision_account_dir "$A1" "dddddddd-dddd-dddd-dddd-dddddddddddd" "house" 2>&1)"
39
+
40
+ assert_absent "$A1/specialists/agents/$DISABLED_AGENT" "disabled-agent-not-redelivered"
41
+ assert_present "$A1/specialists/agents-disabled/$DISABLED_AGENT" "quarantined-copy-survives-upgrade"
42
+ # Every other core specialist is still delivered: the guard removes what the
43
+ # operator named, not the whole set.
44
+ assert_present "$A1/specialists/agents/librarian.md" "other-agents-still-delivered"
45
+ # The removal is reported, so an operator reading the publish output can see
46
+ # which agents provisioning deliberately withheld.
47
+ assert_eq "$(printf '%s' "$OUT" | grep -c "agents-withheld=1")" "1" "withheld-count-logged"
48
+ rm -rf "$T1"
49
+
50
+ # No store, no behaviour change: every core specialist is delivered.
51
+ T2=$(mktemp -d); A2="$T2/acct"
52
+ provision_account_dir "$A2" "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee" "house" >/dev/null 2>&1
53
+ assert_present "$A2/specialists/agents/$DISABLED_AGENT" "no-store-delivers-everything"
54
+ rm -rf "$T2"
55
+
56
+ # A malformed store withholds nothing rather than withholding everything: an
57
+ # unreadable file is not evidence that the operator disabled an agent.
58
+ T3=$(mktemp -d); A3="$T3/acct"
59
+ provision_account_dir "$A3" "ffffffff-ffff-ffff-ffff-ffffffffffff" "house" >/dev/null 2>&1
60
+ printf '{ not json\n' > "$A3/agents-disabled.json"
61
+ OUT3="$(provision_account_dir "$A3" "ffffffff-ffff-ffff-ffff-ffffffffffff" "house" 2>&1)"
62
+ assert_present "$A3/specialists/agents/$DISABLED_AGENT" "malformed-store-withholds-nothing"
63
+ assert_eq "$(printf '%s' "$OUT3" | grep -c 'agents-disabled-store-unreadable')" "1" "malformed-store-reported"
64
+ rm -rf "$T3"
65
+
66
+ # Review finding: the withhold used `rm -f` unconditionally. In the drift state
67
+ # the reconcile check exists to catch — store names it, file is live, quarantine
68
+ # empty — that deletes the only copy of a premium `--` agent or an operator-edited
69
+ # override, because the replace loop skips `*--*` and the copy loop only restores
70
+ # templates. Withholding must move, never destroy.
71
+ T4=$(mktemp -d); A4="$T4/acct"
72
+ provision_account_dir "$A4" "aaaa1111-aaaa-1111-aaaa-111111111111" "house" >/dev/null 2>&1
73
+ mkdir -p "$A4/specialists/agents"
74
+ printf -- '---\nname: quoter\n---\n\nOperator edited this premium agent.\n' > "$A4/specialists/agents/sitedesk--quoter.md"
75
+ printf '{"disabled":["sitedesk--quoter.md"]}\n' > "$A4/agents-disabled.json"
76
+ provision_account_dir "$A4" "aaaa1111-aaaa-1111-aaaa-111111111111" "house" >/dev/null 2>&1
77
+
78
+ assert_absent "$A4/specialists/agents/sitedesk--quoter.md" "premium-withheld-from-dispatch"
79
+ assert_present "$A4/specialists/agents-disabled/sitedesk--quoter.md" "premium-preserved-not-destroyed"
80
+ assert_eq "$(grep -c 'Operator edited this premium agent' "$A4/specialists/agents-disabled/sitedesk--quoter.md" 2>/dev/null || echo 0)" "1" "premium-content-intact"
81
+ rm -rf "$T4"
82
+
83
+ echo "PASS=$PASS FAIL=$FAIL"
84
+ if [ "$FAIL" -gt 0 ]; then printf '%s\n' "${FAILED[@]}"; exit 1; fi
@@ -92,13 +92,18 @@ function collectPluginDirs() {
92
92
  }
93
93
 
94
94
  /** Independent of tool-surface.ts on purpose: a gate that shares the parser it
95
- * is checking cannot detect that parser drifting. */
95
+ * is checking cannot detect that parser drifting.
96
+ *
97
+ * Task 1998 — `hidden:` entries carry the same name+riskClass shape as
98
+ * `tools:` entries and are served by the same MCP server, so a hidden tool
99
+ * can contradict its own name exactly as a visible one can. Scanning only
100
+ * `tools:` would leave eight declarations outside the gate. */
96
101
  function declaredTools(manifestPath) {
97
102
  const out = []
98
103
  let current = null
99
104
  let inTools = false
100
105
  for (const line of readFileSync(manifestPath, 'utf8').split(/\r?\n/)) {
101
- if (/^tools:\s*$/.test(line)) { inTools = true; continue }
106
+ if (/^(tools|hidden):\s*$/.test(line)) { inTools = true; current = null; continue }
102
107
  if (inTools && /^[A-Za-z_]/.test(line)) inTools = false
103
108
  if (!inTools) continue
104
109
  const name = line.match(/^\s+-\s+name:\s*(.+?)\s*$/)
@@ -88,8 +88,11 @@ function parsePluginManifest(rawManifest) {
88
88
  if (nameLine) { tools.push(nameLine[1]); continue; }
89
89
  }
90
90
  if (mode === 'hidden') {
91
- const item = line.match(/^\s+-\s+(.+?)\s*$/);
92
- if (item) hidden.push(item[1]);
91
+ // Task 1998 — hidden entries carry the same `- name:` + `riskClass:`
92
+ // shape as tools entries. Names only here; the gate checks membership
93
+ // of the `all` surface, not consequence.
94
+ const nameLine = line.match(/^\s+-\s+name:\s*(.+?)\s*$/);
95
+ if (nameLine) hidden.push(nameLine[1]);
93
96
  }
94
97
  }
95
98
  return { tools, hidden, hasTools };
@@ -36,9 +36,12 @@ function extractFrontmatter(md) {
36
36
  return md.slice(3, end)
37
37
  }
38
38
 
39
- // Collect `- name: <x>` entries under a bare `tools:` block and `- <x>` entries
40
- // under a bare `hidden:` block. Both contribute names a specialist may list in
41
- // its `tools:` frontmatter, so both belong to the live set.
39
+ // Collect `- name: <x>` entries under a bare `tools:` block and under a bare
40
+ // `hidden:` block. Both contribute names a specialist may list in its `tools:`
41
+ // frontmatter, so both belong to the live set. Task 1998 gave the hidden block
42
+ // the same name+riskClass shape as tools, so the two branches now read alike;
43
+ // this parser stays separate from tool-surface.ts on purpose, so a drift in one
44
+ // shows up as a diff rather than as agreement between a parser and itself.
42
45
  function parseToolNames(frontmatter) {
43
46
  const lines = frontmatter.split('\n')
44
47
  const names = []
@@ -57,7 +60,7 @@ function parseToolNames(frontmatter) {
57
60
  const m = line.match(/^\s+-\s+name:\s*(.+?)\s*$/)
58
61
  if (m) names.push(m[1])
59
62
  } else if (mode === 'hidden') {
60
- const m = line.match(/^\s+-\s+(.+?)\s*$/)
63
+ const m = line.match(/^\s+-\s+name:\s*(.+?)\s*$/)
61
64
  if (m) names.push(m[1])
62
65
  }
63
66
  }
@@ -251,6 +251,49 @@ SETTINGS_EOF
251
251
  [ -f "$specialist" ] && cp "$specialist" "$ACCOUNT_DIR/specialists/agents/$(basename "$specialist")"
252
252
  done
253
253
 
254
+ # Task 1996 — the copy loop above re-delivers every core specialist, which
255
+ # would silently undo a disable on the next publish. specialists/agents/ is
256
+ # the dispatchable set (spawn-context.ts reads it), so an agent the operator
257
+ # switched off is removed again here. The quarantined copy under
258
+ # specialists/agents-disabled/ is never touched: it is the operator's own
259
+ # file, and for a premium or edited agent it is the only copy.
260
+ #
261
+ # An unreadable store withholds NOTHING. A corrupt file is not evidence that
262
+ # an agent was disabled, and withholding the whole set on a parse error would
263
+ # turn a bad byte into a fleet-wide loss of specialists.
264
+ if [ -f "$ACCOUNT_DIR/agents-disabled.json" ]; then
265
+ local _disabled_names _withheld=0
266
+ if _disabled_names="$(python3 -c '
267
+ import json, sys
268
+ with open(sys.argv[1]) as fh:
269
+ names = json.load(fh)["disabled"]
270
+ if not isinstance(names, list):
271
+ raise SystemExit(1)
272
+ for n in names:
273
+ if isinstance(n, str) and n.endswith(".md") and "/" not in n and ".." not in n:
274
+ print(n)
275
+ ' "$ACCOUNT_DIR/agents-disabled.json" 2>/dev/null)"; then
276
+ local _name
277
+ while IFS= read -r _name; do
278
+ [ -n "$_name" ] || continue
279
+ if [ -e "$ACCOUNT_DIR/specialists/agents/$_name" ]; then
280
+ # MOVE, never delete. In the drift state the standing parity check
281
+ # exists to catch (store names it, file is live, quarantine empty) a
282
+ # delete would destroy the only copy of a premium `--` agent or an
283
+ # operator-edited override: the replace loop above skips `*--*` and
284
+ # the copy loop only restores templates, so nothing re-delivers it.
285
+ mkdir -p "$ACCOUNT_DIR/specialists/agents-disabled"
286
+ mv -f "$ACCOUNT_DIR/specialists/agents/$_name" \
287
+ "$ACCOUNT_DIR/specialists/agents-disabled/$_name"
288
+ _withheld=$((_withheld + 1))
289
+ fi
290
+ done <<< "$_disabled_names"
291
+ echo " specialists agents-withheld=$_withheld (disabled by operator)"
292
+ else
293
+ echo " specialists agents-disabled-store-unreadable — withholding nothing"
294
+ fi
295
+ fi
296
+
254
297
  rm -f "$ACCOUNT_DIR/.claude/agents/"*.md 2>/dev/null || true
255
298
 
256
299
  # SOUL.md — user-controlled. Only create if missing. Never overwrite.
@@ -15,7 +15,7 @@ export interface ReconcileResult extends ReconcileCounts {
15
15
  /** The single per-run summary line body. A non-zero count is the standing signal
16
16
  * that an agent authored folders the hard guards could not prevent. Pure
17
17
  * formatter so the emitter and its test share one wording. */
18
- export declare function formatReconcileLine(c: ReconcileCounts, homeMissing?: number, homeUnaudited?: number): string;
18
+ export declare function formatReconcileLine(c: ReconcileCounts, homeMissing?: number, homeUnaudited?: number, agentDrift?: number): string;
19
19
  /** Parse the fenced ```allowed-top-level block from an account's SCHEMA.md —
20
20
  * the same structured set fs-schema-guard.sh parses with awk. Returns the
21
21
  * entry names (blank lines dropped). An absent block yields an empty set. */
@@ -1 +1 @@
1
- {"version":3,"file":"account-dir-schema-reconcile.d.ts","sourceRoot":"","sources":["../src/account-dir-schema-reconcile.ts"],"names":[],"mappings":"AAsFA,MAAM,WAAW,eAAe;IAC9B,aAAa,EAAE,MAAM,CAAA;IACrB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,eAAgB,SAAQ,eAAe;IACtD;;;;;0CAKsC;IACtC,UAAU,EAAE,MAAM,EAAE,CAAA;CACrB;AAED;;+DAE+D;AAC/D,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,eAAe,EAAE,WAAW,SAAI,EAAE,aAAa,SAAI,GAAG,MAAM,CAGlG;AAED;;8EAE8E;AAC9E,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAcpE;AA+ED;;;8EAG8E;AAC9E,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,eAAe,CAoCpE;AAED,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAA;IACpB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CAC/B;AAyBD;;;;;;;;;;;;;iCAaiC;AACjC,wBAAgB,YAAY,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,CA+CtD;AAED,MAAM,WAAW,yBAAyB;IACxC,KAAK,IAAI,IAAI,CAAA;IACb,IAAI,IAAI,IAAI,CAAA;CACb;AAED;oDACoD;AACpD,wBAAgB,+BAA+B,CAC7C,IAAI,EAAE,aAAa,GAAG;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,GAC3C,yBAAyB,CAiB3B"}
1
+ {"version":3,"file":"account-dir-schema-reconcile.d.ts","sourceRoot":"","sources":["../src/account-dir-schema-reconcile.ts"],"names":[],"mappings":"AA2FA,MAAM,WAAW,eAAe;IAC9B,aAAa,EAAE,MAAM,CAAA;IACrB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,eAAgB,SAAQ,eAAe;IACtD;;;;;0CAKsC;IACtC,UAAU,EAAE,MAAM,EAAE,CAAA;CACrB;AAED;;+DAE+D;AAC/D,wBAAgB,mBAAmB,CACjC,CAAC,EAAE,eAAe,EAClB,WAAW,SAAI,EACf,aAAa,SAAI,EACjB,UAAU,SAAI,GACb,MAAM,CAGR;AAED;;8EAE8E;AAC9E,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAcpE;AA+ED;;;8EAG8E;AAC9E,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,eAAe,CAoCpE;AAED,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAA;IACpB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CAC/B;AA0DD;;;;;;;;;;;;;iCAaiC;AACjC,wBAAgB,YAAY,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,CA8DtD;AAED,MAAM,WAAW,yBAAyB;IACxC,KAAK,IAAI,IAAI,CAAA;IACb,IAAI,IAAI,IAAI,CAAA;CACb;AAED;oDACoD;AACpD,wBAAgB,+BAA+B,CAC7C,IAAI,EAAE,aAAa,GAAG;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,GAC3C,yBAAyB,CAiB3B"}