total-recall-brain 3.40.6 → 3.41.0

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 (170) hide show
  1. package/bin/total-recall-memory.mjs +0 -0
  2. package/bin/total-recall.mjs +9 -40
  3. package/docs/reference/cli-reference.md +65 -1
  4. package/frontend/dist/assets/{index-AGUes-VG.css → index-BG9NkVUy.css} +1 -1
  5. package/frontend/dist/assets/{index-Bt5iVPZj.js → index-BtuiZqwP.js} +20 -7
  6. package/frontend/dist/index.html +2 -2
  7. package/frontend/src/components/plugins/PluginPanel.tsx +31 -5
  8. package/frontend/src/components/plugins/card/CardEngine.tsx +15 -23
  9. package/frontend/src/components/plugins/card/CardScenes.tsx +52 -23
  10. package/frontend/src/components/plugins/card/card.css +2 -5
  11. package/frontend/src/components/plugins/card/contract.ts +13 -19
  12. package/frontend/src/components/plugins/card/format.ts +8 -1
  13. package/frontend/src/components/plugins/card/widgets.tsx +4 -35
  14. package/frontend/src/components/plugins/plugin-frame.ts +30 -3
  15. package/frontend/src/pages/PluginsPage.tsx +3 -2
  16. package/package.json +1 -1
  17. package/scaffold/.agent/skills/plugin/SKILL.md +110 -0
  18. package/scaffold/.agent/skills/plugin/evals/README.md +53 -0
  19. package/scaffold/.agent/skills/plugin/evals/cases/01-fabricated-seed.mjs +42 -0
  20. package/scaffold/.agent/skills/plugin/evals/cases/02-reassuring-default.mjs +39 -0
  21. package/scaffold/.agent/skills/plugin/evals/cases/03-declared-template.mjs +32 -0
  22. package/scaffold/.agent/skills/plugin/evals/cases/04-card-collects-credential.mjs +46 -0
  23. package/scaffold/.agent/skills/plugin/evals/cases/05-host-leak-in-scaffold.mjs +55 -0
  24. package/scaffold/.agent/skills/plugin/evals/cases/06-correct-plugin.mjs +92 -0
  25. package/scaffold/.agent/skills/plugin/evals/run.mjs +118 -0
  26. package/scaffold/.agent/skills/plugin/prompts/01-function.md +29 -0
  27. package/scaffold/.agent/skills/plugin/prompts/02-surfaces.md +43 -0
  28. package/scaffold/.agent/skills/plugin/prompts/03-scaffold.md +31 -0
  29. package/scaffold/.agent/skills/plugin/prompts/04-cli.md +49 -0
  30. package/scaffold/.agent/skills/plugin/prompts/05-credentials.md +41 -0
  31. package/scaffold/.agent/skills/plugin/prompts/06-card.md +46 -0
  32. package/scaffold/.agent/skills/plugin/prompts/07-scaffold.md +52 -0
  33. package/scaffold/.agent/skills/plugin/prompts/08-verify.md +43 -0
  34. package/scaffold/.agent/skills/plugin/references/anti-patterns.md +123 -0
  35. package/scaffold/.agent/skills/plugin/references/card.md +90 -0
  36. package/scaffold/.agent/skills/plugin/references/scaffold.md +120 -0
  37. package/scaffold/.agent/skills/plugin/references/surfaces.md +206 -0
  38. package/scaffold/.agent/skills/plugin/scripts/record.mjs +360 -0
  39. package/scaffold/.agent/skills/plugin/subagents/card-designer.md +60 -0
  40. package/scaffold/.agent/skills/plugin/subagents/plugin-reviewer.md +55 -0
  41. package/scaffold/.agent/skills/plugin/subagents/scaffold-extractor.md +71 -0
  42. package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +71 -1441
  43. package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +7 -5
  44. package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +3 -44
  45. package/scaffold/.agent/skills/total-recall/plugins/auth/scripts/agent-login-pty.py +185 -0
  46. package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +20 -1
  47. package/scaffold/.agent/skills/total-recall/plugins/auth/src/host-secrets-store.mjs +15 -0
  48. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-env-export.mjs +6 -4
  49. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-remote-deploy.mjs +1 -0
  50. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/plugin.json +0 -24
  51. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +4 -4
  52. package/scaffold/.agent/skills/total-recall/plugins/code-quality/plugin.json +0 -21
  53. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +4 -4
  54. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +0 -32
  55. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +4 -4
  56. package/scaffold/.agent/skills/total-recall/plugins/embeddings/plugin.json +11 -0
  57. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/README.md +5 -2
  58. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +4 -4
  59. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/plugin.json +1 -28
  60. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/ui/DESIGN.md +95 -13
  61. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/ui/card.js +553 -340
  62. package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +0 -36
  63. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/card.js +383 -400
  64. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +4 -4
  65. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +0 -1
  66. package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +0 -28
  67. package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +4 -4
  68. package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +0 -32
  69. package/scaffold/.agent/skills/total-recall/references/auth.md +3 -1
  70. package/scaffold/.agent/skills/total-recall/references/memory.md +4 -0
  71. package/scaffold/.agent/skills/total-recall/references/openwiki.md +2 -0
  72. package/scaffold/.agent/skills/total-recall/references/operations.md +12 -0
  73. package/src/cli/brain.mjs +4 -2
  74. package/src/cli/context-stats.mjs +104 -0
  75. package/src/cli/context.mjs +8 -0
  76. package/src/cli/daemon.mjs +10 -1
  77. package/src/cli/export.mjs +3 -2
  78. package/src/cli/ingest-okf.mjs +12 -15
  79. package/src/cli/init.mjs +45 -85
  80. package/src/cli/lint.mjs +3 -2
  81. package/src/cli/plugin/copies.mjs +68 -0
  82. package/src/cli/plugin/create.mjs +22 -24
  83. package/src/cli/plugin/index.mjs +49 -3
  84. package/src/cli/plugin/search.mjs +19 -2
  85. package/src/cli/rebuild.mjs +2 -0
  86. package/src/cli/remember.mjs +23 -0
  87. package/src/cli/status.mjs +10 -3
  88. package/src/cli/update.mjs +3 -2
  89. package/src/core/action-predictor.mjs +161 -0
  90. package/src/core/app-deploy/runtime-cli/node.mjs.tmpl +20 -3
  91. package/src/core/app-deploy/source.mjs +16 -9
  92. package/src/core/automatic-brain-updates.mjs +6 -3
  93. package/src/core/brain-registry.mjs +19 -2
  94. package/src/core/brain-state.mjs +23 -0
  95. package/src/core/cli-commands.mjs +1 -0
  96. package/src/core/cli-maintenance.mjs +1 -1
  97. package/src/core/cli-semantic-index.mjs +1 -1
  98. package/src/core/command-surface.mjs +3 -3
  99. package/src/core/composed-command-resolution.mjs +21 -0
  100. package/src/core/config.mjs +84 -0
  101. package/src/core/confined-memory.mjs +93 -0
  102. package/src/core/context-telemetry.mjs +214 -0
  103. package/src/core/daemon-control.mjs +14 -43
  104. package/src/core/fast-recall.mjs +1 -1
  105. package/src/core/memory-brain.mjs +2 -0
  106. package/src/core/plugin-activation.mjs +38 -6
  107. package/src/core/plugin-bundle.mjs +54 -0
  108. package/src/core/plugin-catalog.mjs +1 -1
  109. package/src/core/plugin-context.mjs +1 -1
  110. package/src/core/plugin-contracts/card.mjs +69 -37
  111. package/src/core/plugin-loader.mjs +5 -6
  112. package/src/core/plugin-runner.mjs +13 -6
  113. package/src/core/plugin-runtime.mjs +5 -4
  114. package/src/core/plugin-setup.mjs +5 -3
  115. package/src/core/plugin-state-store.mjs +12 -1
  116. package/src/core/plugin-state.mjs +46 -5
  117. package/src/core/plugin-store.mjs +78 -3
  118. package/src/core/plugin-tasks.mjs +3 -1
  119. package/src/core/plugin-updates.mjs +117 -0
  120. package/src/core/plugin-vendor.mjs +10 -4
  121. package/src/core/project-brain.mjs +14 -41
  122. package/src/core/project-id.mjs +4 -1
  123. package/src/core/required-plugins.mjs +151 -13
  124. package/src/core/skills-registry.mjs +27 -3
  125. package/src/core/ssss-kernel-bridge.mjs +3 -1
  126. package/src/core/startup-health.mjs +66 -16
  127. package/src/core/surface.mjs +51 -44
  128. package/src/core/testing/plugin-fixture.mjs +69 -0
  129. package/src/core/vault-files.mjs +13 -0
  130. package/src/core/vault.mjs +4 -30
  131. package/src/server/index.mjs +13 -1
  132. package/src/server/invoke-decision-child.mjs +49 -0
  133. package/src/server/invoke-execution.mjs +87 -0
  134. package/src/server/invoke-policy.mjs +64 -0
  135. package/src/server/invoke-read-child.mjs +26 -0
  136. package/src/server/invoke-targets.mjs +49 -0
  137. package/src/server/invoke-write-child.mjs +19 -0
  138. package/src/server/routes/invoke.mjs +314 -0
  139. package/src/server/routes/plugins.mjs +1 -1
  140. package/scaffold/.agent/skills/plugins/SKILL.md +0 -57
  141. package/scaffold/.agent/skills/plugins/references/card-design.md +0 -55
  142. package/scaffold/.agent/skills/plugins/references/distribution.md +0 -23
  143. package/scaffold/.agent/skills/plugins/references/manifest-contract.md +0 -90
  144. package/scaffold/.agent/skills/plugins/references/optimized/index.md +0 -9
  145. package/scaffold/.agent/skills/plugins/references/optimized/manifest.json +0 -35
  146. package/scaffold/.agent/skills/plugins/references/optimized/original.md +0 -58
  147. package/scaffold/.agent/skills/plugins/references/optimized/section-01.md +0 -5
  148. package/scaffold/.agent/skills/plugins/references/optimized/section-02.md +0 -25
  149. package/scaffold/.agent/skills/plugins/references/optimized/section-03.md +0 -12
  150. package/scaffold/.agent/skills/plugins/references/optimized/section-04.md +0 -7
  151. package/scaffold/.agent/skills/plugins/references/optimized/section-05.md +0 -5
  152. package/scaffold/.agent/skills/total-recall/openwiki/README.md +0 -10
  153. package/scaffold/.agent/skills/total-recall/openwiki/architecture.md +0 -5
  154. package/scaffold/.agent/skills/total-recall/openwiki/ide.md +0 -10
  155. package/scaffold/.agent/skills/total-recall/openwiki/memory.md +0 -11
  156. package/scaffold/.agent/skills/total-recall/openwiki/secrets.md +0 -7
  157. package/scaffold/.agent/skills/total-recall/openwiki/skills.md +0 -7
  158. package/scaffold/openwiki/README.md +0 -10
  159. package/scaffold/openwiki/architecture.md +0 -5
  160. package/scaffold/openwiki/ide.md +0 -10
  161. package/scaffold/openwiki/memory.md +0 -11
  162. package/scaffold/openwiki/secrets.md +0 -7
  163. package/scaffold/openwiki/skills.md +0 -7
  164. package/src/core/okf-adapter.mjs +0 -559
  165. package/templates/openwiki/README.md +0 -10
  166. package/templates/openwiki/architecture.md +0 -5
  167. package/templates/openwiki/ide.md +0 -10
  168. package/templates/openwiki/memory.md +0 -11
  169. package/templates/openwiki/secrets.md +0 -7
  170. 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.