@codyswann/lisa 2.328.4 → 2.328.5

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 (72) hide show
  1. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  2. package/dist/core/upstream-evidence-manifest.js +10 -2
  3. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  4. package/package.json +1 -1
  5. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  6. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  7. package/plugins/lisa/.codex-plugin/skills/lisa-detect-tooling/SKILL.md +32 -2
  8. package/plugins/lisa/.codex-plugin/skills/lisa-detect-tooling/scripts/commands.mjs +479 -0
  9. package/plugins/lisa/.codex-plugin/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +170 -4
  10. package/plugins/lisa/skills/lisa-detect-tooling/SKILL.md +33 -3
  11. package/plugins/lisa/skills/lisa-detect-tooling/scripts/commands.mjs +479 -0
  12. package/plugins/lisa/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +170 -4
  13. package/plugins/lisa-agy/plugin.json +1 -1
  14. package/plugins/lisa-agy/skills/lisa-detect-tooling/SKILL.md +33 -3
  15. package/plugins/lisa-agy/skills/lisa-detect-tooling/scripts/commands.mjs +479 -0
  16. package/plugins/lisa-agy/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +170 -4
  17. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  18. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  19. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  20. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  23. package/plugins/lisa-copilot/skills/lisa-detect-tooling/SKILL.md +33 -3
  24. package/plugins/lisa-copilot/skills/lisa-detect-tooling/scripts/commands.mjs +479 -0
  25. package/plugins/lisa-copilot/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +170 -4
  26. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-cursor/skills/lisa-detect-tooling/SKILL.md +33 -3
  28. package/plugins/lisa-cursor/skills/lisa-detect-tooling/scripts/commands.mjs +479 -0
  29. package/plugins/lisa-cursor/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +170 -4
  30. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  32. package/plugins/lisa-expo-agy/plugin.json +1 -1
  33. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  37. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  38. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  42. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  43. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  47. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  48. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  52. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  53. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  57. package/plugins/lisa-rails-agy/plugin.json +1 -1
  58. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  62. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  63. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  67. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  68. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  70. package/plugins/src/base/skills/lisa-detect-tooling/SKILL.md +33 -3
  71. package/plugins/src/base/skills/lisa-detect-tooling/scripts/commands.mjs +479 -0
  72. package/plugins/src/base/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +170 -4
@@ -22,9 +22,11 @@
22
22
  * @module detect-tooling
23
23
  */
24
24
 
25
- import { existsSync, readFileSync, statSync } from "node:fs";
25
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
26
26
  import { join } from "node:path";
27
27
 
28
+ import { commandsIn } from "./commands.mjs";
29
+
28
30
  /** The tool named by the Expo template's scripts and its flows directory. */
29
31
  const MAESTRO = "maestro";
30
32
 
@@ -109,6 +111,128 @@ export function toolsFromScripts(pkg) {
109
111
  return found;
110
112
  }
111
113
 
114
+ /** Where git hooks live, strongest signal first. */
115
+ const HOOK_DIRECTORIES = [".husky", join(".git", "hooks")];
116
+
117
+ /**
118
+ * Coding agents, which a PROJECT manifest must never try to provision.
119
+ *
120
+ * A hook that shells out to `claude` is real evidence of a real dependency, but
121
+ * `remoteEnv.tools` is the wrong place to answer it: agents belong to the
122
+ * machine, not the checkout, and each is installed by its vendor's own method
123
+ * into a version directory it manages itself. Pinning one as a release archive
124
+ * would fight the self-updater that owns that directory.
125
+ *
126
+ * That layer is `lisa-setup-workstation`. Named here rather than imported from
127
+ * it, because a detector that stops working when a sibling skill is absent is
128
+ * worse than one carrying six strings.
129
+ *
130
+ * Deliberately NOT extended to `gh`, `bws`, `aws` or `sonar`: those genuinely
131
+ * belong in a project manifest, and `gh` going undeclared is the incident this
132
+ * skill was written after.
133
+ */
134
+ const WORKSTATION_PROVISIONED = new Set([
135
+ "claude",
136
+ "codex",
137
+ "cursor-agent",
138
+ "opencode",
139
+ "agy",
140
+ "copilot",
141
+ ]);
142
+
143
+ /**
144
+ * Tools a git hook runs — the strongest signal a project can give.
145
+ *
146
+ * Stronger than an npm script, because a hook runs on EVERY commit and push
147
+ * whether anyone asked or not. A container missing one of these cannot commit at
148
+ * all; the failure arrives mid-task, unattended, at the moment of use.
149
+ *
150
+ * The worst shape is the guarded one. This repository's pre-commit hook wraps
151
+ * its secret scan in `command -v gitleaks`, so on a machine without gitleaks the
152
+ * scan is SKIPPED SILENTLY — the hook passes, the commit succeeds, and nothing
153
+ * was scanned. A guard makes the absence invisible exactly where it matters
154
+ * most, which is why a probed name counts as evidence rather than being read as
155
+ * "optional".
156
+ * @param {string} cwd Project root.
157
+ * @returns {Map<string, string>} Tool name to the hook that proves it.
158
+ */
159
+ export function toolsFromGitHooks(cwd) {
160
+ const found = new Map();
161
+ for (const directory of HOOK_DIRECTORIES) {
162
+ const full = join(cwd, directory);
163
+ let names = [];
164
+ try {
165
+ names = readdirSync(full);
166
+ } catch {
167
+ continue;
168
+ }
169
+ for (const name of names.sort()) {
170
+ // `.sample` files ship with git and run nothing.
171
+ if (name.endsWith(".sample")) continue;
172
+ let body;
173
+ try {
174
+ body = readFileSync(join(full, name), "utf8");
175
+ } catch {
176
+ continue;
177
+ }
178
+ for (const tool of commandsIn(body)) {
179
+ if (!found.has(tool)) found.set(tool, `git hook ${directory}/${name}`);
180
+ }
181
+ }
182
+ }
183
+ return found;
184
+ }
185
+
186
+ /**
187
+ * Tools an npm script invokes, discovered rather than matched against a list.
188
+ *
189
+ * `toolsFromScripts` answers "which of the tools I already know about appear
190
+ * here". This answers the open question — what does this project actually RUN —
191
+ * which is the one that finds a tool nobody thought to add. An Expo project
192
+ * invoking `eas` from eight scripts produced nothing under the old shape,
193
+ * because `eas` was not in `KNOWN_TOOLS`.
194
+ * @param {object|null} pkg Parsed package.json.
195
+ * @returns {Map<string, string>} Tool name to the script that proves it.
196
+ */
197
+ export function toolsDiscoveredInScripts(pkg) {
198
+ const found = new Map();
199
+ for (const [name, body] of Object.entries(pkg?.scripts ?? {})) {
200
+ if (typeof body !== "string") continue;
201
+ for (const tool of commandsIn(body)) {
202
+ if (found.has(tool)) continue;
203
+ found.set(tool, `npm script "${name}": ${body.trim().slice(0, 60)}`);
204
+ }
205
+ }
206
+ return found;
207
+ }
208
+
209
+ /**
210
+ * Whether the package manager already puts this binary on the local path.
211
+ *
212
+ * A dependency's binary lands in `node_modules/.bin`, where npm scripts resolve
213
+ * it without it ever being on PATH. Proposing a manifest entry for one is noise,
214
+ * and noise is how a detector teaches people to skim it.
215
+ *
216
+ * Only meaningful once dependencies are installed, which is why
217
+ * `dependenciesInstalled` exists rather than this quietly returning false on a
218
+ * fresh clone and over-proposing every dev tool in the project.
219
+ * @param {string} cwd Project root.
220
+ * @param {string} tool Tool name.
221
+ * @returns {boolean} Whether npm already provides it.
222
+ */
223
+ export function providedByNodeModules(cwd, tool) {
224
+ return existsSync(join(cwd, "node_modules", ".bin", tool));
225
+ }
226
+
227
+ /**
228
+ * Whether `node_modules` is populated, so its absence can be reported.
229
+ * @param {string} cwd Project root.
230
+ * @returns {boolean} Whether dependencies are installed.
231
+ */
232
+ export function dependenciesInstalled(cwd) {
233
+ return existsSync(join(cwd, "node_modules", ".bin"));
234
+ }
235
+
112
236
  /**
113
237
  * MCP servers that are access substrates rather than evidence for a CLI.
114
238
  *
@@ -330,8 +454,14 @@ export function detectTooling(cwd = process.cwd()) {
330
454
  const notes = readJson(join(cwd, ".lisa", "secret-notes.json"));
331
455
 
332
456
  const declared = declaredTools(config);
457
+ // Curated signals carry a vetted `why`. Discovered ones carry only the fact
458
+ // that the project runs the thing — weaker prose, identical standing as
459
+ // evidence, and the only kind that can find a tool nobody listed.
460
+ const hooks = toolsFromGitHooks(cwd);
333
461
  const signals = [
462
+ hooks,
334
463
  toolsFromScripts(pkg),
464
+ toolsDiscoveredInScripts(pkg),
335
465
  toolsFromMcp(mcp),
336
466
  toolsFromSecretNotes(notes),
337
467
  toolsFromQuality(config, pkg, cwd),
@@ -343,8 +473,11 @@ export function detectTooling(cwd = process.cwd()) {
343
473
  if (declared.has(tool)) continue;
344
474
  // Already provided by the package manager, so there is nothing to pin.
345
475
  if (satisfiedByNpm(pkg, tool)) continue;
476
+ if (providedByNodeModules(cwd, tool)) continue;
477
+ // Real dependency, wrong manifest — the workstation layer owns it.
478
+ if (WORKSTATION_PROVISIONED.has(tool)) continue;
346
479
  const entry = merged.get(tool) ?? { evidence: [] };
347
- entry.evidence.push(evidence);
480
+ if (!entry.evidence.includes(evidence)) entry.evidence.push(evidence);
348
481
  merged.set(tool, entry);
349
482
  }
350
483
  }
@@ -352,12 +485,35 @@ export function detectTooling(cwd = process.cwd()) {
352
485
  return [...merged.entries()]
353
486
  .map(([name, entry]) => ({
354
487
  name,
355
- why: KNOWN_TOOLS[name]?.why ?? "",
488
+ why: KNOWN_TOOLS[name]?.why ?? discoveredWhy(name, hooks.has(name)),
356
489
  evidence: entry.evidence,
490
+ // Says which vocabulary the `why` came from, so a reader can weigh it.
491
+ // A curated entry has been thought about; a discovered one has only been
492
+ // observed.
493
+ source: Object.hasOwn(KNOWN_TOOLS, name) ? "curated" : "discovered",
357
494
  }))
358
495
  .sort((a, b) => a.name.localeCompare(b.name));
359
496
  }
360
497
 
498
+ /**
499
+ * The `why` for a tool nobody wrote prose for.
500
+ *
501
+ * Every proposal must carry one — a proposal without a reason is an assertion,
502
+ * and this program's whole boundary is that it asserts nothing. So a discovered
503
+ * tool states the observation plainly rather than borrowing confidence it has
504
+ * not earned.
505
+ * @param {string} name Tool name.
506
+ * @param {boolean} fromHook Whether a git hook runs it.
507
+ * @returns {string} A reason a reader can check.
508
+ */
509
+ export function discoveredWhy(name, fromHook) {
510
+ return fromHook
511
+ ? `A git hook runs \`${name}\` on every commit or push, and nothing ` +
512
+ `declares it — a machine without it fails at commit time, or skips the ` +
513
+ `check silently if the hook guards it.`
514
+ : `An npm script invokes \`${name}\`, and nothing puts it on PATH.`;
515
+ }
516
+
361
517
  /**
362
518
  * The manifest entries an operator can paste, with the parts only they supply.
363
519
  *
@@ -438,8 +594,18 @@ if (process.argv[1] && import.meta.url === `file://${process.argv[1]}`) {
438
594
  console.log(
439
595
  `${proposals.length} tool(s) look required but are not declared:\n`
440
596
  );
597
+ if (!dependenciesInstalled(process.cwd())) {
598
+ // Without node_modules there is no way to tell a tool that needs PATH
599
+ // from one npm already provides, so every dev dependency looks missing.
600
+ // Said out loud rather than silently over-proposing.
601
+ console.log(
602
+ "note: dependencies are not installed, so binaries npm would provide " +
603
+ "cannot be\n excluded. Run the install first for a shorter, " +
604
+ "more accurate list.\n"
605
+ );
606
+ }
441
607
  for (const proposal of proposals) {
442
- console.log(` ${proposal.name}`);
608
+ console.log(` ${proposal.name} [${proposal.source}]`);
443
609
  console.log(` why: ${proposal.why}`);
444
610
  for (const evidence of proposal.evidence) {
445
611
  console.log(` evidence: ${evidence}`);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.328.4",
3
+ "version": "2.328.5",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.328.4",
3
+ "version": "2.328.5",
4
4
  "description": "AWS CDK-specific Lisa plugin.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.328.4",
3
+ "version": "2.328.5",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.328.4",
3
+ "version": "2.328.5",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.328.4",
3
+ "version": "2.328.5",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.328.4",
3
+ "version": "2.328.5",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: lisa-detect-tooling
3
- description: "Find the command-line tools a project needs but never declares, and propose pinned manifest entries for them. Reads npm scripts, MCP servers, credential usage notes and quality configuration, subtracts whatever remoteEnv.tools already covers, and prints proposals with evidence. Writes nothing and installs nothing — a tool reaches a machine only when a human has reviewed a pinned, checksummed entry. Invoked by lisa-setup-remote-env before provisioning, and runnable on its own."
3
+ description: "Find the command-line tools a project needs but never declares, and propose pinned manifest entries for them. Reads git hooks and npm scripts for what the project actually RUNS — discovering tools no list mentions — plus MCP servers, credential usage notes and quality configuration. Subtracts whatever remoteEnv.tools, node_modules and the workstation layer already cover, and prints proposals with evidence. Writes nothing and installs nothing — a tool reaches a machine only when a human has reviewed a pinned, checksummed entry. Invoked by lisa-setup-remote-env before provisioning, and runnable on its own."
4
4
  allowed-tools: ["Bash", "Read", "AskUserQuestion"]
5
5
  ---
6
6
 
@@ -20,15 +20,45 @@ Nothing populates that last row. So a project can ship scripts that invoke `maes
20
20
 
21
21
  That is the same failure this repository has now paid for twice: `gh` was declared nowhere and a cloud session could not commit; `tar` was needed by an install method and asserted by nothing.
22
22
 
23
+ ## Discovery, not recognition
24
+
25
+ The first version of this could only find tools it had been told about: it matched script bodies against a fixed `KNOWN_TOOLS` list. That makes the detector useful for exactly the set that needs it least — the tools someone already thought of.
26
+
27
+ An Expo project invoking `eas` from **eight** npm scripts and six CI steps produced no proposal, because `eas` was not on the list. `gitleaks`, which runs on every commit in every one of these repositories, produced no proposal either.
28
+
29
+ So the question asked of executable sources is now the open one — *what does this project actually run* — and everything already provided is subtracted afterwards:
30
+
31
+ - what `remoteEnv.tools` declares
32
+ - what `node_modules/.bin` provides (a dependency's binary never needs to be on PATH)
33
+ - what `lisa-setup-workstation` owns (coding agents belong to the machine, not the checkout)
34
+
35
+ The curated list stays, because a vetted `why` is worth more than an observed one — proposals say which they are, `[curated]` or `[discovered]`.
36
+
23
37
  ## What it does
24
38
 
25
- Reads four signals, subtracts what `remoteEnv.tools` already declares, and prints what is left with the evidence for each:
39
+ Reads six signals, subtracts what is already covered, and prints what is left with the evidence for each:
40
+
41
+ - **git hooks** (`.husky/*`, `.git/hooks/*`) that invoke a binary. The **strongest** signal available, stronger than an npm script: a hook runs on every commit and push whether anyone asked or not, so a machine without the tool cannot commit at all.
26
42
 
27
- - **npm scripts** that invoke a binary. The strongest signal there is a script running `maestro test` is the project stating a dependency in executable form. Matched on the script *body*, not its name, because the name is a label.
43
+ A `command -v` guard counts as evidence rather than as "optional", because the guarded shape is the worse one. Lisa's pre-commit hook wraps its secret scan in `command -v gitleaks`, so on a machine without gitleaks the scan is **skipped silently** — the hook passes, the commit succeeds, and nothing was scanned. A guard makes the absence invisible precisely where it matters.
44
+
45
+ - **npm scripts** that invoke a binary — a script running `maestro test` is the project stating a dependency in executable form. Read from the script *body*, not its name, because the name is a label.
28
46
  - **MCP servers with a CLI equivalent.** An MCP server is not a substitute for the binary: several authenticate by browser OAuth, which a container cannot do, so a project relying on one remotely has *no* integration rather than a degraded one. Do not infer a CLI for access layers that already define their own headless substrate; Linear is handled by `lisa-linear-access` through `LINEAR_API_KEY` + GraphQL when MCP OAuth is unavailable.
29
47
  - **Credential usage notes.** A note explaining what a token is for usually names the program that consumes it. `lisa-secrets-access` already exposes these without touching a value, which makes them a first-class input rather than a trick.
30
48
  - **Quality configuration**, where a threshold implies the tool that produces it.
31
49
 
50
+ ## Reading shell is the hard part
51
+
52
+ Discovery only works if "what does this run" can be answered from shell text without inventing commands. Three things had to be right, each found by running it against real repositories:
53
+
54
+ **Only the first word of a command position counts.** Word-scanning this repo's own hooks proposed `ltrimstr`, `map` and `select` (jq filter internals), `console.log` and `process.exit` (embedded JavaScript), and `load_audit_cves` (a function the hook defines three lines up). Reading only where a command can actually start removes all of them without a denylist entry for any.
55
+
56
+ **Quoting and commenting nest, so neither can be stripped independently.** The comment on line 9 of `pre-push.local` ends `...the script's exit code)`. That lone apostrophe pairs with the next real quote 28 lines later, blanks everything between, and leaks a `node -e` payload's JavaScript into the results. Stripping comments first does not fix it, because a `#` inside a quoted string is not a comment. One character scanner tracking both is the only correct shape.
57
+
58
+ **`$( )` restarts the quoting context.** In `"$(printf '%s' "$JSON" | node -e '…')"`, quotes inside the substitution pair among themselves. A flat scanner falls out of phase on the first one and starts reporting the payload's own string literals.
59
+
60
+ Against Lisa and the three TunnlAI repositories, what survives is `gitleaks`, `jq`, `gtimeout` and `eas` — every one a real, undeclared invocation, with no false positives.
61
+
32
62
  ## What it will not do
33
63
 
34
64
  **It writes nothing and installs nothing.** Output is a proposal with `<pin>`, `<release url>` and `<sha256>` left for a human.