total-recall-brain 3.40.7 → 3.41.1
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/bin/total-recall-memory.mjs +0 -0
- package/bin/total-recall.mjs +9 -40
- package/docs/reference/cli-reference.md +65 -1
- package/frontend/dist/assets/{index-AGUes-VG.css → index-BG9NkVUy.css} +1 -1
- package/frontend/dist/assets/{index-Bt5iVPZj.js → index-BtuiZqwP.js} +20 -7
- package/frontend/dist/index.html +2 -2
- package/frontend/src/components/plugins/PluginPanel.tsx +31 -5
- package/frontend/src/components/plugins/card/CardEngine.tsx +15 -23
- package/frontend/src/components/plugins/card/CardScenes.tsx +52 -23
- package/frontend/src/components/plugins/card/card.css +2 -5
- package/frontend/src/components/plugins/card/contract.ts +13 -19
- package/frontend/src/components/plugins/card/format.ts +8 -1
- package/frontend/src/components/plugins/card/widgets.tsx +4 -35
- package/frontend/src/components/plugins/plugin-frame.ts +30 -3
- package/frontend/src/pages/PluginsPage.tsx +3 -2
- package/package.json +1 -1
- package/scaffold/.agent/skills/meta-harness/references/optimized/original.md +1 -1
- package/scaffold/.agent/skills/plugin/SKILL.md +110 -0
- package/scaffold/.agent/skills/plugin/evals/README.md +53 -0
- package/scaffold/.agent/skills/plugin/evals/cases/01-fabricated-seed.mjs +42 -0
- package/scaffold/.agent/skills/plugin/evals/cases/02-reassuring-default.mjs +39 -0
- package/scaffold/.agent/skills/plugin/evals/cases/03-declared-template.mjs +32 -0
- package/scaffold/.agent/skills/plugin/evals/cases/04-card-collects-credential.mjs +46 -0
- package/scaffold/.agent/skills/plugin/evals/cases/05-host-leak-in-scaffold.mjs +55 -0
- package/scaffold/.agent/skills/plugin/evals/cases/06-correct-plugin.mjs +92 -0
- package/scaffold/.agent/skills/plugin/evals/run.mjs +118 -0
- package/scaffold/.agent/skills/plugin/prompts/01-function.md +29 -0
- package/scaffold/.agent/skills/plugin/prompts/02-surfaces.md +43 -0
- package/scaffold/.agent/skills/plugin/prompts/03-scaffold.md +31 -0
- package/scaffold/.agent/skills/plugin/prompts/04-cli.md +49 -0
- package/scaffold/.agent/skills/plugin/prompts/05-credentials.md +41 -0
- package/scaffold/.agent/skills/plugin/prompts/06-card.md +46 -0
- package/scaffold/.agent/skills/plugin/prompts/07-scaffold.md +52 -0
- package/scaffold/.agent/skills/plugin/prompts/08-verify.md +43 -0
- package/scaffold/.agent/skills/plugin/references/anti-patterns.md +123 -0
- package/scaffold/.agent/skills/plugin/references/card.md +90 -0
- package/scaffold/.agent/skills/plugin/references/scaffold.md +120 -0
- package/scaffold/.agent/skills/plugin/references/surfaces.md +206 -0
- package/scaffold/.agent/skills/plugin/scripts/record.mjs +360 -0
- package/scaffold/.agent/skills/plugin/subagents/card-designer.md +60 -0
- package/scaffold/.agent/skills/plugin/subagents/plugin-reviewer.md +55 -0
- package/scaffold/.agent/skills/plugin/subagents/scaffold-extractor.md +71 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +71 -1441
- package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +7 -5
- package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +3 -44
- package/scaffold/.agent/skills/total-recall/plugins/auth/scripts/agent-login-pty.py +185 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +20 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/host-secrets-store.mjs +15 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-env-export.mjs +6 -4
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-remote-deploy.mjs +1 -0
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/plugin.json +0 -24
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/plugin.json +0 -21
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +0 -32
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/plugin.json +11 -0
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/README.md +5 -2
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/plugin.json +1 -28
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/ui/DESIGN.md +95 -13
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/ui/card.js +553 -340
- package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +0 -36
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/card.js +383 -400
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +0 -1
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +0 -28
- package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +0 -32
- package/scaffold/.agent/skills/total-recall/references/auth.md +3 -1
- package/scaffold/.agent/skills/total-recall/references/memory.md +4 -0
- package/scaffold/.agent/skills/total-recall/references/openwiki.md +2 -0
- package/scaffold/.agent/skills/total-recall/references/operations.md +12 -0
- package/src/cli/brain.mjs +72 -2
- package/src/cli/connect.mjs +10 -39
- package/src/cli/context-stats.mjs +104 -0
- package/src/cli/context.mjs +8 -0
- package/src/cli/daemon.mjs +10 -1
- package/src/cli/export.mjs +3 -2
- package/src/cli/ingest-okf.mjs +12 -15
- package/src/cli/init.mjs +0 -85
- package/src/cli/lint.mjs +3 -2
- package/src/cli/plugin/copies.mjs +68 -0
- package/src/cli/plugin/create.mjs +22 -24
- package/src/cli/plugin/index.mjs +49 -3
- package/src/cli/plugin/search.mjs +19 -2
- package/src/cli/rebuild.mjs +2 -0
- package/src/cli/remember.mjs +23 -0
- package/src/cli/status.mjs +10 -3
- package/src/cli/update.mjs +3 -2
- package/src/core/action-predictor.mjs +161 -0
- package/src/core/app-deploy/runtime-cli/node.mjs.tmpl +20 -3
- package/src/core/app-deploy/source.mjs +16 -9
- package/src/core/automatic-brain-updates.mjs +6 -3
- package/src/core/brain-registry.mjs +19 -2
- package/src/core/brain-state.mjs +23 -0
- package/src/core/cli-commands.mjs +1 -0
- package/src/core/cli-maintenance.mjs +1 -1
- package/src/core/cli-semantic-index.mjs +1 -1
- package/src/core/command-surface.mjs +3 -3
- package/src/core/composed-command-resolution.mjs +21 -0
- package/src/core/config.mjs +84 -0
- package/src/core/confined-memory.mjs +93 -0
- package/src/core/context-telemetry.mjs +214 -0
- package/src/core/daemon-control.mjs +14 -43
- package/src/core/fast-recall.mjs +1 -1
- package/src/core/memory-brain.mjs +2 -0
- package/src/core/plugin-activation.mjs +38 -6
- package/src/core/plugin-bundle.mjs +54 -0
- package/src/core/plugin-catalog.mjs +1 -1
- package/src/core/plugin-context.mjs +1 -1
- package/src/core/plugin-contracts/card.mjs +69 -37
- package/src/core/plugin-loader.mjs +5 -6
- package/src/core/plugin-runner.mjs +13 -6
- package/src/core/plugin-runtime.mjs +5 -4
- package/src/core/plugin-setup.mjs +5 -3
- package/src/core/plugin-state-store.mjs +12 -1
- package/src/core/plugin-state.mjs +46 -5
- package/src/core/plugin-store.mjs +78 -3
- package/src/core/plugin-tasks.mjs +3 -1
- package/src/core/plugin-updates.mjs +117 -0
- package/src/core/plugin-vendor.mjs +10 -4
- package/src/core/project-brain.mjs +14 -41
- package/src/core/project-id.mjs +4 -1
- package/src/core/required-plugins.mjs +151 -13
- package/src/core/skills-registry.mjs +27 -3
- package/src/core/ssss-kernel-bridge.mjs +3 -1
- package/src/core/startup-health.mjs +66 -16
- package/src/core/surface.mjs +53 -46
- package/src/core/testing/plugin-fixture.mjs +69 -0
- package/src/core/vault-files.mjs +13 -0
- package/src/core/vault.mjs +4 -30
- package/src/server/index.mjs +13 -1
- package/src/server/invoke-decision-child.mjs +49 -0
- package/src/server/invoke-execution.mjs +87 -0
- package/src/server/invoke-policy.mjs +64 -0
- package/src/server/invoke-read-child.mjs +26 -0
- package/src/server/invoke-targets.mjs +49 -0
- package/src/server/invoke-write-child.mjs +19 -0
- package/src/server/routes/invoke.mjs +314 -0
- package/src/server/routes/plugins.mjs +1 -1
- package/scaffold/.agent/skills/plugins/SKILL.md +0 -57
- package/scaffold/.agent/skills/plugins/references/card-design.md +0 -55
- package/scaffold/.agent/skills/plugins/references/distribution.md +0 -23
- package/scaffold/.agent/skills/plugins/references/manifest-contract.md +0 -90
- package/scaffold/.agent/skills/plugins/references/optimized/index.md +0 -9
- package/scaffold/.agent/skills/plugins/references/optimized/manifest.json +0 -35
- package/scaffold/.agent/skills/plugins/references/optimized/original.md +0 -58
- package/scaffold/.agent/skills/plugins/references/optimized/section-01.md +0 -5
- package/scaffold/.agent/skills/plugins/references/optimized/section-02.md +0 -25
- package/scaffold/.agent/skills/plugins/references/optimized/section-03.md +0 -12
- package/scaffold/.agent/skills/plugins/references/optimized/section-04.md +0 -7
- package/scaffold/.agent/skills/plugins/references/optimized/section-05.md +0 -5
- package/scaffold/.agent/skills/total-recall/openwiki/README.md +0 -10
- package/scaffold/.agent/skills/total-recall/openwiki/architecture.md +0 -5
- package/scaffold/.agent/skills/total-recall/openwiki/ide.md +0 -10
- package/scaffold/.agent/skills/total-recall/openwiki/memory.md +0 -11
- package/scaffold/.agent/skills/total-recall/openwiki/secrets.md +0 -7
- package/scaffold/.agent/skills/total-recall/openwiki/skills.md +0 -7
- package/scaffold/openwiki/README.md +0 -10
- package/scaffold/openwiki/architecture.md +0 -5
- package/scaffold/openwiki/ide.md +0 -10
- package/scaffold/openwiki/memory.md +0 -11
- package/scaffold/openwiki/secrets.md +0 -7
- package/scaffold/openwiki/skills.md +0 -7
- package/src/core/okf-adapter.mjs +0 -559
- package/templates/openwiki/README.md +0 -10
- package/templates/openwiki/architecture.md +0 -5
- package/templates/openwiki/ide.md +0 -10
- package/templates/openwiki/memory.md +0 -11
- package/templates/openwiki/secrets.md +0 -7
- package/templates/openwiki/skills.md +0 -7
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# Surfaces
|
|
2
|
+
|
|
3
|
+
Every surface a plugin can declare, with the exact fields for each.
|
|
4
|
+
|
|
5
|
+
**Declare only what the capability needs.** An unused surface is noise; a missing one hides the capability. Identity is required; everything else is optional and independent.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Identity — always required
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"id": "host-telemetry",
|
|
14
|
+
"name": "Host Telemetry",
|
|
15
|
+
"version": "1.0.0",
|
|
16
|
+
"description": "One concrete sentence saying what this adds.",
|
|
17
|
+
"author": "Greg",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"use_cases": ["operations", "self-hosting"]
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
| Field | Rule |
|
|
24
|
+
| --- | --- |
|
|
25
|
+
| `id` | lowercase kebab-case, matching the repository `tr-plugin-<id>` |
|
|
26
|
+
| `name` | plain and descriptive; never an invented brand for a wrapper of someone else's service |
|
|
27
|
+
| `version` | semver |
|
|
28
|
+
| `description` | at least five characters, one sentence, no filler |
|
|
29
|
+
| `author`, `license` | authorship and license, as shipped |
|
|
30
|
+
| `use_cases` | free-form tags used to filter discovery |
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## CLI
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
"cli": {
|
|
38
|
+
"command": "host-telemetry",
|
|
39
|
+
"handler": "./cli.mjs",
|
|
40
|
+
"subcommands": [
|
|
41
|
+
{ "name": "sample", "description": "One telemetry reading from this machine",
|
|
42
|
+
"effect": "read", "output": "json",
|
|
43
|
+
"args": [ { "name": "json", "flag": "--json", "label": "JSON output", "type": "boolean" } ] },
|
|
44
|
+
{ "name": "charge", "description": "Create a payment", "effect": "send", "output": "json", "timeout_ms": 30000,
|
|
45
|
+
"args": [
|
|
46
|
+
{ "name": "amount", "flag": "--amount", "label": "Amount", "type": "number", "required": true },
|
|
47
|
+
{ "name": "currency", "flag": "--currency", "label": "Currency", "type": "choice", "options": ["usd", "eur"] },
|
|
48
|
+
{ "name": "secretKey", "label": "Secret key", "type": "string", "secret": true }
|
|
49
|
+
] }
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
| Field | Required | Rule |
|
|
55
|
+
| --- | --- | --- |
|
|
56
|
+
| `name` | yes | kebab-case, unique, declared once |
|
|
57
|
+
| `description` | yes | what the operation does |
|
|
58
|
+
| `effect` | yes | `read`, `write`, `destructive`, `send` |
|
|
59
|
+
| `args[].name` | yes | camelCase |
|
|
60
|
+
| `args[].label` | yes | human label |
|
|
61
|
+
| `args[].type` | yes | `string`, `number`, `boolean`, `choice` |
|
|
62
|
+
| `args[].flag` | for non-secret | `--flag-name` |
|
|
63
|
+
| `args[].positional` | alternative to flag | `true` |
|
|
64
|
+
| `args[].required` | no | `true` when the operation cannot run without it |
|
|
65
|
+
| `args[].options` | for `choice` | non-empty array |
|
|
66
|
+
| `args[].secret` | for credentials | `true`; the value travels over **stdin**, never `argv`. Exactly one secret argument per command, a string, with no flag |
|
|
67
|
+
| `output` | no | `json` (default) or `text` |
|
|
68
|
+
| `timeout_ms` | no | 1–600000, for slow operations |
|
|
69
|
+
|
|
70
|
+
The handler must emit JSON under `--json` on **every** subcommand, exit non-zero on failure with the real error on stderr, and answer `--help` without resolving credentials.
|
|
71
|
+
|
|
72
|
+
## Memory
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
"ssss_schemas": { "categories": [ { "name": "host-readings", "description": "…", "node_type": "memory" } ] }
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Adds a category to the vault. Use when the capability contributes a new kind of knowledge.
|
|
79
|
+
|
|
80
|
+
## Context
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
"compile": { "generator": "./generate.mjs", "watch": ["data"] }
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`generator` is a safe relative path; `watch` lists plugin-owned directories the compiler re-reads. Use when the capability must change what agents read at the start of a task.
|
|
87
|
+
|
|
88
|
+
## Skills
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
"skills": [ { "id": "host-telemetry", "path": "./skills/host-telemetry/SKILL.md" } ]
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Instruction an agent loads on demand.
|
|
95
|
+
|
|
96
|
+
## Tasks
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
"tasks": [ { "intent": "Sample every node", "schedule": "*/15 * * * *", "command": "fleet", "placement": "selected-node" } ]
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Five-field cron. `command` must be a declared subcommand.
|
|
103
|
+
|
|
104
|
+
## Runtime
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
"runtime": { "modules": { "sampler": "./runtime/sampler.mjs" }, "routers": ["./routes/readings.mjs"] }
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Live behaviour rather than a command. Module keys are lowercase kebab-case.
|
|
111
|
+
|
|
112
|
+
## UI — the card face
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
"ui": {
|
|
116
|
+
"design_tokens": "ui/DESIGN.md",
|
|
117
|
+
"elements": [
|
|
118
|
+
{ "id": "card", "kind": "card", "tag": "tr-host-telemetry-card", "module": "ui/card.js",
|
|
119
|
+
"description": "Fleet telemetry board" }
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Every plugin has a card**: exactly one element of kind `card`, and **no `card_layout`** — a declared template is an error, not a fallback. `description` says what the element is. See [card](card.md).
|
|
125
|
+
|
|
126
|
+
## Deploy
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
"deploy": { "targets": ["ssss-app", "nextjs"], "required_ssss_version": ">=0.9.3",
|
|
130
|
+
"access_grants": ["ssss:vault:read"], "resources": { "db": "sqlite" } }
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Use when the capability is deployed into other applications.
|
|
134
|
+
|
|
135
|
+
## Distribution and monetization
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
"distribution": { "tier": "official", "license": "open" },
|
|
139
|
+
"monetization": { "model": "metered", "price_cents": 2, "currency": "usd", "period": "per-use",
|
|
140
|
+
"license_key_secret": "SERVICE_LICENSE_KEY", "checkout_url": "https://…" }
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`tier`: `core`, `official`, `personal`, `community`. `license`: `open` or `closed`. A closed plugin is an operated service behind a thin client; never rely on a local license check.
|
|
144
|
+
|
|
145
|
+
## Composition
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
"requires": [ { "id": "auth", "range": "^1.0.0" } ]
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Builds on other bricks instead of starting from zero.
|
|
152
|
+
|
|
153
|
+
## Credentials
|
|
154
|
+
|
|
155
|
+
```json
|
|
156
|
+
"secrets": [ { "key": "SERVICE_API_KEY", "description": "Reads and creates charges", "required": true } ]
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Uppercase key. Resolved only through `total-recall auth secret`. No store of the plugin's own, and no ambient environment variable.
|
|
160
|
+
|
|
161
|
+
## Settings
|
|
162
|
+
|
|
163
|
+
```json
|
|
164
|
+
"configuration": { "fields": [ { "key": "region", "label": "Region", "type": "string", "default": "us-east" } ] }
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Keys are camelCase and may not contain `password`, `secret`, `apiKey` or `token` — those belong in `secrets`.
|
|
168
|
+
|
|
169
|
+
## Status
|
|
170
|
+
|
|
171
|
+
```json
|
|
172
|
+
"status": { "command": "status", "refresh_seconds": 30 }
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`command` must be a declared `read` subcommand. `refresh_seconds` is 5–86400.
|
|
176
|
+
|
|
177
|
+
## Branding
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
"branding": {
|
|
181
|
+
"name": "Host Telemetry", "color": "#059669", "accent": "#10b981",
|
|
182
|
+
"font": "jetbrains-mono", "density": "compact", "radius": "sm", "header": "ledger-rule",
|
|
183
|
+
"badge": "Telemetry", "logo": "ui/logo.svg",
|
|
184
|
+
"provider": { "name": "Stripe", "url": "https://stripe.com" }
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
| Field | Values |
|
|
189
|
+
| --- | --- |
|
|
190
|
+
| `color`, `accent` | `#rrggbb` |
|
|
191
|
+
| `font` | `system`, `inter`, `ibm-plex`, `space-grotesk`, `jetbrains-mono`, `dm-sans`, `outfit`, `fraunces`, `newsreader`, `chivo-mono` |
|
|
192
|
+
| `density` | `compact`, `comfortable`, `spacious` |
|
|
193
|
+
| `radius` | `none`, `sm`, `md`, `lg`, `pill` |
|
|
194
|
+
| `header` | `plain`, `ledger-rule`, `band`, `tab-strip`, `badge-left` |
|
|
195
|
+
| `logo` | relative `.svg` or `.png` inside the plugin; a real mark, never an emoji or generic icon |
|
|
196
|
+
| `provider` | for a wrapper of someone else's API: set it and keep that provider's name and brand rather than inventing your own |
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Validating
|
|
201
|
+
|
|
202
|
+
```
|
|
203
|
+
total-recall plugin info <id>
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
No errors and no warnings before the plugin is done. `scripts/record.mjs` runs the same validation plus the checks the validator cannot make.
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* record.mjs — the plugin creator's verification battery.
|
|
4
|
+
*
|
|
5
|
+
* Runs the checks the recipe asks for, against a plugin repository. Exits
|
|
6
|
+
* non-zero on any failure and prints the real reason. Nothing here trusts a
|
|
7
|
+
* declaration: every check reads the actual files or runs the actual command.
|
|
8
|
+
*
|
|
9
|
+
* node record.mjs <pluginDir> [--manifest|--card|--secrets|--scaffold|--json]
|
|
10
|
+
*
|
|
11
|
+
* Checks are marked PASS, FAIL or WARN. A WARN is a judgement call the agent
|
|
12
|
+
* must resolve; a FAIL is a rejection.
|
|
13
|
+
*/
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { execFileSync } from 'node:child_process';
|
|
17
|
+
import { pathToFileURL } from 'node:url';
|
|
18
|
+
|
|
19
|
+
const args = process.argv.slice(2);
|
|
20
|
+
const json = args.includes('--json');
|
|
21
|
+
const only = ['--manifest', '--card', '--secrets', '--scaffold'].filter((f) => args.includes(f));
|
|
22
|
+
const root = path.resolve(args.find((a) => !a.startsWith('-')) || '.');
|
|
23
|
+
const want = (section) => only.length === 0 || only.includes(section);
|
|
24
|
+
|
|
25
|
+
const results = [];
|
|
26
|
+
const record = (section, level, check, detail) => results.push({ section, level, check, detail });
|
|
27
|
+
|
|
28
|
+
/* ── helpers ─────────────────────────────────────────────────────────────── */
|
|
29
|
+
|
|
30
|
+
const read = (rel) => {
|
|
31
|
+
try { return fs.readFileSync(path.join(root, rel), 'utf8'); } catch { return ''; }
|
|
32
|
+
};
|
|
33
|
+
const exists = (rel) => fs.existsSync(path.join(root, rel));
|
|
34
|
+
|
|
35
|
+
/** Files the plugin owns, excluding dependencies and the brain. */
|
|
36
|
+
function walk(dir, out = [], base = root) {
|
|
37
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
38
|
+
if (entry.name.startsWith('.') || ['node_modules', 'bricks'].includes(entry.name)) continue;
|
|
39
|
+
const abs = path.join(dir, entry.name);
|
|
40
|
+
if (entry.isSymbolicLink()) continue;
|
|
41
|
+
if (entry.isDirectory()) walk(abs, out, base);
|
|
42
|
+
else out.push(path.relative(base, abs));
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* ── manifest ────────────────────────────────────────────────────────────── */
|
|
48
|
+
|
|
49
|
+
let manifest = null;
|
|
50
|
+
if (want('--manifest')) {
|
|
51
|
+
const file = path.join(root, 'plugin.json');
|
|
52
|
+
if (!fs.existsSync(file)) {
|
|
53
|
+
record('manifest', 'FAIL', 'plugin.json exists', 'no plugin.json in ' + root);
|
|
54
|
+
} else {
|
|
55
|
+
try {
|
|
56
|
+
manifest = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
57
|
+
record('manifest', 'PASS', 'plugin.json parses', `${manifest.id} v${manifest.version}`);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
record('manifest', 'FAIL', 'plugin.json parses', err.message);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Validate with the real contract when this skill is inside a Total Recall checkout.
|
|
65
|
+
if (manifest) {
|
|
66
|
+
const loaderPath = path.resolve(import.meta.dirname, '../../../../src/core/plugin-loader.mjs');
|
|
67
|
+
if (fs.existsSync(loaderPath)) {
|
|
68
|
+
try {
|
|
69
|
+
const { validatePluginManifest } = await import(pathToFileURL(loaderPath).href);
|
|
70
|
+
const v = validatePluginManifest(manifest);
|
|
71
|
+
for (const e of v.errors || []) record('manifest', 'FAIL', 'contract', e);
|
|
72
|
+
for (const w of v.warnings || []) record('manifest', 'WARN', 'contract', w);
|
|
73
|
+
if (!(v.errors || []).length && !(v.warnings || []).length) {
|
|
74
|
+
record('manifest', 'PASS', 'contract', 'no errors, no warnings');
|
|
75
|
+
}
|
|
76
|
+
} catch (err) {
|
|
77
|
+
record('manifest', 'WARN', 'contract', `validator unavailable: ${err.message}`);
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
record('manifest', 'WARN', 'contract', 'validator not found; structural checks only');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Card declaration
|
|
84
|
+
const elements = manifest.ui?.elements || [];
|
|
85
|
+
const faces = elements.filter((e) => e && (e.kind === 'card' || e.kind === 'panel'));
|
|
86
|
+
if (manifest.card_layout !== undefined) {
|
|
87
|
+
record('manifest', 'FAIL', 'no template', "'card_layout' is declared; a face is bespoke owner code");
|
|
88
|
+
} else {
|
|
89
|
+
record('manifest', 'PASS', 'no template', 'no card_layout');
|
|
90
|
+
}
|
|
91
|
+
if (faces.length === 0) {
|
|
92
|
+
record('manifest', 'FAIL', 'card declared', 'every plugin has a card; ui.elements declares none');
|
|
93
|
+
} else if (faces.length > 1) {
|
|
94
|
+
record('manifest', 'FAIL', 'card declared', `${faces.length} faces declared; exactly one is allowed`);
|
|
95
|
+
} else {
|
|
96
|
+
const face = faces[0];
|
|
97
|
+
record('manifest', 'PASS', 'card declared', `<${face.tag}> → ${face.module}`);
|
|
98
|
+
if (!exists(face.module)) record('manifest', 'FAIL', 'card module resolves', `${face.module} does not exist`);
|
|
99
|
+
else record('manifest', 'PASS', 'card module resolves', face.module);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Declared effects and typed arguments on the CLI surface.
|
|
103
|
+
for (const sub of manifest.cli?.subcommands || []) {
|
|
104
|
+
if (!['read', 'write', 'destructive', 'send'].includes(sub.effect)) {
|
|
105
|
+
record('manifest', 'FAIL', 'effect declared', `'${sub.name}' has no valid effect`);
|
|
106
|
+
}
|
|
107
|
+
for (const arg of sub.args || []) {
|
|
108
|
+
if (arg.secret === true && arg.flag !== undefined) {
|
|
109
|
+
record('manifest', 'FAIL', 'secret over stdin', `'${sub.name}.${arg.name}' is secret but carries a flag`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Status must bind to a real read command.
|
|
115
|
+
const status = manifest.status;
|
|
116
|
+
if (status?.command) {
|
|
117
|
+
const sub = (manifest.cli?.subcommands || []).find((s) => s.name === status.command);
|
|
118
|
+
if (!sub) record('manifest', 'FAIL', 'status binds', `status.command '${status.command}' is not declared`);
|
|
119
|
+
else if (sub.effect && sub.effect !== 'read') record('manifest', 'FAIL', 'status binds', `status.command '${status.command}' is not a read`);
|
|
120
|
+
else record('manifest', 'PASS', 'status binds', status.command);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* ── card ────────────────────────────────────────────────────────────────── */
|
|
125
|
+
|
|
126
|
+
if (want('--card') && manifest) {
|
|
127
|
+
const face = (manifest.ui?.elements || []).find((e) => e && e.kind === 'card');
|
|
128
|
+
const src = face ? read(face.module) : '';
|
|
129
|
+
if (!src) {
|
|
130
|
+
record('card', 'FAIL', 'card source readable', 'no card module to read');
|
|
131
|
+
} else {
|
|
132
|
+
const lines = src.split('\n');
|
|
133
|
+
|
|
134
|
+
// A credential must never be collectable from the face.
|
|
135
|
+
if (/type\s*=\s*["']password["']/.test(src) || /input\.type\s*=\s*["']password["']/.test(src)) {
|
|
136
|
+
record('card', 'FAIL', 'never collects a credential', 'a password input exists in the card');
|
|
137
|
+
} else {
|
|
138
|
+
record('card', 'PASS', 'never collects a credential', 'no password input');
|
|
139
|
+
}
|
|
140
|
+
if (/save-token|dispatchEvent\(\s*new CustomEvent\(\s*["']save-/.test(src)) {
|
|
141
|
+
record('card', 'FAIL', 'never transmits a credential', 'the card dispatches a credential event');
|
|
142
|
+
} else {
|
|
143
|
+
record('card', 'PASS', 'never transmits a credential', 'no credential event');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Seeded values: a literal that looks like a reading, not a read of one.
|
|
147
|
+
// Each pattern requires an actual literal (a digit or a quoted string), so
|
|
148
|
+
// a ternary such as `? h.loadAvg : []` is not mistaken for a seed.
|
|
149
|
+
const seedPatterns = [
|
|
150
|
+
[/\bthis\.(sample|reading|status|telemetry|gateway|data)\s*=\s*\{[^}]*\b(hostname|loadAvg|usedPercent|uptimeSeconds)\b/s, 'a seeded reading object'],
|
|
151
|
+
[/hostname\s*:\s*["'][^"']+["']/, 'a literal hostname'],
|
|
152
|
+
[/usedPercent\s*:\s*\d/, 'a literal usedPercent'],
|
|
153
|
+
[/loadAvg\s*:\s*\[\s*\d/, 'a literal load average'],
|
|
154
|
+
[/\bvalue\s*=\s*["']\d+(\.\d+)?["']/, 'a seeded input value'],
|
|
155
|
+
];
|
|
156
|
+
let seeded = [];
|
|
157
|
+
for (const [re, what] of seedPatterns) if (re.test(src)) seeded.push(what);
|
|
158
|
+
if (seeded.length) record('card', 'FAIL', 'no seeded values', seeded.join('; '));
|
|
159
|
+
else record('card', 'PASS', 'no seeded values', 'no literal reading found');
|
|
160
|
+
|
|
161
|
+
// A reassuring default is a *fallback* for an unknown — `x || 'Up to date'`
|
|
162
|
+
// or `x ?? 'Synced'`. The same word chosen by a ternary between two honest
|
|
163
|
+
// labels (`ok ? 'Connected' : 'Unconfigured'`) is not a default, so only a
|
|
164
|
+
// fallback position is flagged.
|
|
165
|
+
const reassuringWords = ['Up to date', 'Synced', 'Repository Active', 'ONLINE', 'Healthy', 'Connected', 'All good', 'OK'];
|
|
166
|
+
const reassuranceFallbacks = [];
|
|
167
|
+
for (const word of reassuringWords) {
|
|
168
|
+
for (const quote of ["'", '"']) {
|
|
169
|
+
const literal = `${quote}${word}${quote}`;
|
|
170
|
+
const index = src.indexOf(literal);
|
|
171
|
+
if (index === -1) continue;
|
|
172
|
+
// Look back for the operator that makes it a default.
|
|
173
|
+
const before = src.slice(Math.max(0, index - 40), index);
|
|
174
|
+
if (/\|\||\?\?\s*$/.test(before) || /\?\?\s*$/.test(src.slice(Math.max(0, index - 6), index))) {
|
|
175
|
+
reassuranceFallbacks.push(word);
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
// A default inside a catch branch is a fallback too.
|
|
179
|
+
const sinceCatch = src.slice(src.lastIndexOf('catch', index) === -1 ? 0 : src.lastIndexOf('catch', index), index);
|
|
180
|
+
if (src.lastIndexOf('catch', index) !== -1 && sinceCatch.length < 200 && !/}\s*(else|finally)/.test(sinceCatch)) {
|
|
181
|
+
reassuranceFallbacks.push(`${word} (in a catch)`);
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (reassuranceFallbacks.length) record('card', 'FAIL', 'no reassuring default', reassuranceFallbacks.join(', '));
|
|
187
|
+
else record('card', 'PASS', 'no reassuring default', 'none found');
|
|
188
|
+
|
|
189
|
+
// Randomness is how fabricated ids appear.
|
|
190
|
+
if (/Math\.random\(/.test(src)) record('card', 'FAIL', 'no invented identifiers', 'Math.random in the card');
|
|
191
|
+
else record('card', 'PASS', 'no invented identifiers', 'no Math.random');
|
|
192
|
+
|
|
193
|
+
// Every read must go through the host, and failures must be states.
|
|
194
|
+
if (!/host\.run|this\.host/.test(src)) {
|
|
195
|
+
record('card', 'WARN', 'reads through the host', 'no host.run reference; does it render real data?');
|
|
196
|
+
} else {
|
|
197
|
+
record('card', 'PASS', 'reads through the host', 'host.run is used');
|
|
198
|
+
}
|
|
199
|
+
if (!/catch/.test(src)) record('card', 'WARN', 'failures are states', 'no catch; can the card show a failed read?');
|
|
200
|
+
else record('card', 'PASS', 'failures are states', 'a catch exists');
|
|
201
|
+
|
|
202
|
+
// A timer left running after disconnect.
|
|
203
|
+
if (/setInterval/.test(src) && !/disconnectedCallback/.test(src)) {
|
|
204
|
+
record('card', 'FAIL', 'timers cleared', 'setInterval without disconnectedCallback');
|
|
205
|
+
} else {
|
|
206
|
+
record('card', 'PASS', 'timers cleared', 'no leaked interval');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Raw hex outside a token block suggests a hard-coded palette.
|
|
210
|
+
const hex = [...src.matchAll(/#[0-9a-fA-F]{6}\b/g)];
|
|
211
|
+
const tokenBlockHex = (src.match(/--face-[a-z-]+:\s*#[0-9a-fA-F]{3,8}/g) || []).length;
|
|
212
|
+
if (hex.length - tokenBlockHex > 6) {
|
|
213
|
+
record('card', 'WARN', 'tokens only', `${hex.length - tokenBlockHex} raw hex values outside token declarations`);
|
|
214
|
+
} else {
|
|
215
|
+
record('card', 'PASS', 'tokens only', 'colour is declared as tokens');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// The host's tokens must actually be used.
|
|
219
|
+
for (const token of ['--text-primary', '--border']) {
|
|
220
|
+
if (!src.includes(token)) record('card', 'WARN', 'host tokens used', `${token} is not referenced; the face may not adapt to the dashboard`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Accessibility floor.
|
|
224
|
+
if (!/min-height:\s*(4[4-9]|[5-9]\d)px/.test(src)) {
|
|
225
|
+
record('card', 'WARN', 'touch targets', 'no control declares a >=44px minimum height');
|
|
226
|
+
} else {
|
|
227
|
+
record('card', 'PASS', 'touch targets', 'controls reach 44px');
|
|
228
|
+
}
|
|
229
|
+
if (!/prefers-reduced-motion/.test(src)) {
|
|
230
|
+
record('card', 'WARN', 'reduced motion', 'prefers-reduced-motion is not respected');
|
|
231
|
+
} else {
|
|
232
|
+
record('card', 'PASS', 'reduced motion', 'respected');
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/* ── secrets ─────────────────────────────────────────────────────────────── */
|
|
238
|
+
|
|
239
|
+
if (want('--secrets') && manifest) {
|
|
240
|
+
const files = walk(root).filter((f) => /\.(mjs|js)$/.test(f));
|
|
241
|
+
|
|
242
|
+
// A declared credential must not be read from the ambient environment.
|
|
243
|
+
for (const secret of manifest.secrets || []) {
|
|
244
|
+
const key = secret.key;
|
|
245
|
+
let envRead = null;
|
|
246
|
+
for (const f of files) {
|
|
247
|
+
if (f.startsWith('test/')) continue;
|
|
248
|
+
const src = read(f);
|
|
249
|
+
if (new RegExp(`process\\.env(\\.${key}|\\[\\s*["']${key}["']\\s*\\])`).test(src)) { envRead = f; break; }
|
|
250
|
+
}
|
|
251
|
+
if (envRead) record('secrets', 'FAIL', 'declared secret is not ambient', `${key} read from process.env in ${envRead}`);
|
|
252
|
+
else record('secrets', 'PASS', 'declared secret is not ambient', key);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// The card may name a credential's identifier — it must, so the operator
|
|
256
|
+
// knows which key to set. What it must never do is receive or render the
|
|
257
|
+
// value. The only channel a value could travel on is stdin, so that is the
|
|
258
|
+
// thing to forbid.
|
|
259
|
+
const faceSrc = read(manifest.ui?.elements?.find?.((e) => e.kind === 'card')?.module || '');
|
|
260
|
+
if (/stdin\s*:/.test(faceSrc)) {
|
|
261
|
+
record('secrets', 'FAIL', 'card never receives a credential', 'the card passes stdin to host.run');
|
|
262
|
+
} else {
|
|
263
|
+
record('secrets', 'PASS', 'card never receives a credential', 'no stdin channel from the face');
|
|
264
|
+
}
|
|
265
|
+
const names = (manifest.secrets || []).filter((s) => faceSrc.includes(s.key));
|
|
266
|
+
if (names.length) {
|
|
267
|
+
record('secrets', 'PASS', 'card names the identifier only', `${names.map((s) => s.key).join(', ')} — so the operator can fix it`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Credential access must go through auth.
|
|
271
|
+
const authUse = files.some((f) => /auth['"],\s*['"]secret|auth secret/.test(read(f)));
|
|
272
|
+
if ((manifest.secrets || []).length && !authUse) {
|
|
273
|
+
record('secrets', 'FAIL', 'credentials resolve through auth', 'no auth secret call found');
|
|
274
|
+
} else if ((manifest.secrets || []).length) {
|
|
275
|
+
record('secrets', 'PASS', 'credentials resolve through auth', 'auth secret is used');
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/* ── scaffold ────────────────────────────────────────────────────────────── */
|
|
280
|
+
|
|
281
|
+
if (want('--scaffold')) {
|
|
282
|
+
const capDir = path.join(root, 'capability');
|
|
283
|
+
if (!fs.existsSync(capDir)) {
|
|
284
|
+
record('scaffold', 'WARN', 'scaffold present', 'no capability/ — fine if another app would not want this capability');
|
|
285
|
+
} else {
|
|
286
|
+
const capEntry = manifest?.capability?.entry || 'capability/index.mjs';
|
|
287
|
+
const capSrc = read(capEntry);
|
|
288
|
+
if (!capSrc) {
|
|
289
|
+
record('scaffold', 'FAIL', 'entry resolves', `${capEntry} does not exist`);
|
|
290
|
+
} else {
|
|
291
|
+
record('scaffold', 'PASS', 'entry resolves', capEntry);
|
|
292
|
+
|
|
293
|
+
const hostRefs = capSrc.match(/total-recall|TOTAL_RECALL_BIN|auth secret/g) || [];
|
|
294
|
+
if (hostRefs.length) record('scaffold', 'FAIL', 'no Total Recall dependency', hostRefs.join(', '));
|
|
295
|
+
else record('scaffold', 'PASS', 'no Total Recall dependency', 'none reachable from the capability');
|
|
296
|
+
|
|
297
|
+
const placeholders = capSrc.match(/example\.com|example\.org|localhost:\d+/g) || [];
|
|
298
|
+
if (placeholders.length) record('scaffold', 'FAIL', 'no placeholder values', placeholders.join(', '));
|
|
299
|
+
else record('scaffold', 'PASS', 'no placeholder values', 'none');
|
|
300
|
+
|
|
301
|
+
if (!/export\s+(async\s+)?function|export\s+const/.test(capSrc)) {
|
|
302
|
+
record('scaffold', 'FAIL', 'exports the capability', 'no exported function; a builder cannot import it');
|
|
303
|
+
} else {
|
|
304
|
+
record('scaffold', 'PASS', 'exports the capability', 'exported functions present');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (/process\.argv|process\.exitCode/.test(capSrc)) {
|
|
308
|
+
record('scaffold', 'FAIL', 'no CLI concerns', 'argv or exitCode inside the capability');
|
|
309
|
+
} else {
|
|
310
|
+
record('scaffold', 'PASS', 'no CLI concerns', 'no argv or exitCode');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (fs.existsSync(path.join(capDir, 'ui'))) {
|
|
314
|
+
record('scaffold', 'FAIL', 'no UI in the scaffold', 'capability/ui exists');
|
|
315
|
+
} else {
|
|
316
|
+
record('scaffold', 'PASS', 'no UI in the scaffold', 'no ui under capability/');
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Prove standalone operation: import it with Total Recall unavailable.
|
|
321
|
+
try {
|
|
322
|
+
const env = { ...process.env };
|
|
323
|
+
delete env.TOTAL_RECALL_BIN;
|
|
324
|
+
env.PATH = '/nonexistent';
|
|
325
|
+
execFileSync(process.execPath, ['--input-type=module', '-e',
|
|
326
|
+
`await import(${JSON.stringify(pathToFileURL(path.join(root, capEntry)).href)})`],
|
|
327
|
+
{ env, stdio: 'pipe', timeout: 15000 });
|
|
328
|
+
record('scaffold', 'PASS', 'loads with Total Recall absent', 'imported successfully');
|
|
329
|
+
} catch (err) {
|
|
330
|
+
record('scaffold', 'FAIL', 'loads with Total Recall absent', String(err.stderr || err.message).trim().split('\n').slice(-2).join(' '));
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/* ── report ──────────────────────────────────────────────────────────────── */
|
|
336
|
+
|
|
337
|
+
const fails = results.filter((r) => r.level === 'FAIL');
|
|
338
|
+
const warns = results.filter((r) => r.level === 'WARN');
|
|
339
|
+
|
|
340
|
+
if (json) {
|
|
341
|
+
process.stdout.write(JSON.stringify({
|
|
342
|
+
plugin: manifest?.id || path.basename(root),
|
|
343
|
+
root,
|
|
344
|
+
pass: results.filter((r) => r.level === 'PASS').length,
|
|
345
|
+
fail: fails.length,
|
|
346
|
+
warn: warns.length,
|
|
347
|
+
results,
|
|
348
|
+
}, null, 2) + '\n');
|
|
349
|
+
} else {
|
|
350
|
+
const tag = { PASS: ' ok ', FAIL: ' FAIL ', WARN: ' warn ' };
|
|
351
|
+
let section = '';
|
|
352
|
+
for (const r of results) {
|
|
353
|
+
if (r.section !== section) { section = r.section; console.log(`\n${section.toUpperCase()}`); }
|
|
354
|
+
console.log(`[${tag[r.level]}] ${r.check} — ${r.detail}`);
|
|
355
|
+
}
|
|
356
|
+
console.log(`\n${results.filter((r) => r.level === 'PASS').length} passed, ${fails.length} failed, ${warns.length} warnings`);
|
|
357
|
+
if (warns.length) console.log('\nA warning is a judgement call to resolve, not a pass.');
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
process.exitCode = fails.length ? 1 : 0;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: card-designer
|
|
3
|
+
inputs: a capability brief — users, tasks, what must be visible without a click, dependencies, brand
|
|
4
|
+
returns: ui/DESIGN.md reasoning and a first ui/card.js, or the questions that cannot be answered yet
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Subagent — card designer
|
|
8
|
+
|
|
9
|
+
Design one plugin's card face. You are **not** filling in a layout: you are designing the interface for this capability, and it must not resemble any other plugin's.
|
|
10
|
+
|
|
11
|
+
Read [card](../references/card.md) first. It is the specification.
|
|
12
|
+
|
|
13
|
+
## Ask before you draw
|
|
14
|
+
|
|
15
|
+
Given the brief, you must be able to answer:
|
|
16
|
+
|
|
17
|
+
1. Who uses it, and what are they in the middle of doing?
|
|
18
|
+
2. What must be visible without a single click?
|
|
19
|
+
3. What is the one primary action, and what is destructive?
|
|
20
|
+
4. What is the domain's own vocabulary?
|
|
21
|
+
5. What is the worst thing that happens if the operator misreads this screen?
|
|
22
|
+
|
|
23
|
+
**If you cannot answer 1, 2 or 4 from the brief, stop and return the questions.** A face designed around a guessed function is a template, which is the failure this exists to prevent. Do not invent users or tasks to proceed.
|
|
24
|
+
|
|
25
|
+
## Design
|
|
26
|
+
|
|
27
|
+
1. Write `ui/DESIGN.md`: who uses it, the tasks, what is visible without a click, the vocabulary, the chosen arrangement and why it fits this plugin in its own terms, the `tokens:` block, and the state list.
|
|
28
|
+
2. Author `ui/card.js`: one Web Component, the single element of kind `card`, screens the plugin defines and orders by task frequency, wide not a column.
|
|
29
|
+
|
|
30
|
+
## Hard rules
|
|
31
|
+
|
|
32
|
+
- **No template, no `card_layout`, no region or widget vocabulary.**
|
|
33
|
+
- **Every value comes from `host.run`.** Before the first read, frames show em dashes. Never a seed, never a placeholder, never an example default.
|
|
34
|
+
- **A failed read clears the figures** rather than retaining a previous one as current.
|
|
35
|
+
- **Unknown is an em dash with its reason.** `0` and `unknown` are different facts.
|
|
36
|
+
- **Name the driver of a combined severity**, or a red tile beside a green figure reads as a contradiction.
|
|
37
|
+
- **The host's tokens, tuned for a dark surface.** The dashboard is dark; verify on its real values, not on white.
|
|
38
|
+
- **Severity never by colour alone.**
|
|
39
|
+
- **44 px controls, keyboard reachable, visible focus, `prefers-reduced-motion`.**
|
|
40
|
+
- **Never render a secret, never collect one.**
|
|
41
|
+
- **Name each screen for the task it serves**, not for a command.
|
|
42
|
+
|
|
43
|
+
## Before returning
|
|
44
|
+
|
|
45
|
+
- Render at 360, 768 and 1280 px on the dark tokens. No horizontal overflow.
|
|
46
|
+
- Render every state: present, missing, unreachable, rejected, empty, stale.
|
|
47
|
+
- Put it beside another plugin's face. If the two are interchangeable, redesign.
|
|
48
|
+
|
|
49
|
+
## Return
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
DESIGN.md: <the file, or the blocking questions>
|
|
53
|
+
card.js: <the file>
|
|
54
|
+
screens: <each screen and the task it serves>
|
|
55
|
+
states rendered: <which you exercised and what you saw>
|
|
56
|
+
distinguishable from: <which plugin you compared against, and how they differ>
|
|
57
|
+
unverified: <what you could not check>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Never return a face you have not rendered and looked at.
|