@gmickel/gno 1.38.0 → 1.39.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.
- package/assets/skill/README.md +2 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v1.38.0.zip → gno-browser-clipper-v1.39.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.39.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +208 -11
- package/spec/output-schemas/agents-mutation.schema.json +108 -0
- package/spec/output-schemas/agents-verify.schema.json +89 -0
- package/src/cli/commands/agents/block.ts +164 -0
- package/src/cli/commands/agents/commands.ts +413 -0
- package/src/cli/commands/agents/engine.ts +417 -0
- package/src/cli/commands/agents/harnesses.ts +298 -0
- package/src/cli/commands/agents/index.ts +35 -0
- package/src/cli/commands/completion/scripts.ts +5 -0
- package/src/cli/program.ts +104 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.38.0.zip.sha256 +0 -1
package/assets/skill/README.md
CHANGED
|
@@ -42,6 +42,8 @@ gno skill install --target hermes # Hermes
|
|
|
42
42
|
gno skill install --target all # All supported agents
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
As of their current releases, `--target claude` also serves Grok Build and Cursor via automatic `.claude/skills` discovery, `--target codex` is also read by Cursor, and other skill-capable clients can copy the files listed by `gno skill paths`.
|
|
46
|
+
|
|
45
47
|
Scope is configurable:
|
|
46
48
|
|
|
47
49
|
```bash
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
b3b2f2609cfe5c821eb9d22d76832b6ad329afa7496c0792e0dd4f5bd0f74fbc gno-browser-clipper-v1.39.0.zip
|
package/package.json
CHANGED
package/spec/cli.md
CHANGED
|
@@ -113,6 +113,10 @@ equivalent files fail closed as ambiguous.
|
|
|
113
113
|
| skill uninstall | yes | no | no | no | no | terminal |
|
|
114
114
|
| skill show | no | no | no | no | no | terminal |
|
|
115
115
|
| skill paths | yes | no | no | no | no | terminal |
|
|
116
|
+
| agents install | yes | no | no | no | no | terminal |
|
|
117
|
+
| agents update | yes | no | no | no | no | terminal |
|
|
118
|
+
| agents verify | yes | no | no | no | no | terminal |
|
|
119
|
+
| agents uninstall | yes | no | no | no | no | terminal |
|
|
116
120
|
| tags list | yes | no | no | yes | no | terminal |
|
|
117
121
|
| tags add | yes | no | no | no | no | terminal |
|
|
118
122
|
| tags rm | yes | no | no | no | no | terminal |
|
|
@@ -2582,6 +2586,8 @@ gno skill install [--scope <project|user>] [--target <claude|codex|opencode|open
|
|
|
2582
2586
|
3. Atomically installs skill directory (temp + rename)
|
|
2583
2587
|
4. Copies SKILL.md, reference files, and nested recipe files
|
|
2584
2588
|
|
|
2589
|
+
**Compatibility:** As of their current releases, Grok Build (the xAI grok CLI) automatically reads Claude skill directories (`.claude/skills/` and `~/.claude/skills/`) with no extra configuration, and Cursor loads agent skills from `.claude/skills/` and `.codex/skills/` (project and user scope) in addition to its own `.cursor/skills/`. `--target claude` therefore covers Grok Build and Cursor; `--target codex` is also read by Cursor. Other skill-capable clients can copy the installed files; `gno skill paths` prints their locations. Dedicated `grok` and `cursor` installer targets are not shipped: they would duplicate the claude/codex paths those tools already read. Revisit only if users keep `~/.grok/skills` or `~/.cursor/skills` separate and ask for first-class targets.
|
|
2590
|
+
|
|
2585
2591
|
**Output (JSON):**
|
|
2586
2592
|
|
|
2587
2593
|
```json
|
|
@@ -2623,7 +2629,7 @@ Remove GNO agent skill.
|
|
|
2623
2629
|
gno skill uninstall [--scope <project|user>] [--target <claude|codex|opencode|openclaw|hermes|all>] [--json]
|
|
2624
2630
|
```
|
|
2625
2631
|
|
|
2626
|
-
**Options:** Same as `skill install` (except `--force`)
|
|
2632
|
+
**Options:** Same as `skill install` (except `--force`).
|
|
2627
2633
|
|
|
2628
2634
|
**Safety Checks:**
|
|
2629
2635
|
|
|
@@ -2728,6 +2734,195 @@ gno skill paths [--scope <project|user>] [--target <claude|codex|opencode|opencl
|
|
|
2728
2734
|
|
|
2729
2735
|
---
|
|
2730
2736
|
|
|
2737
|
+
### gno agents install
|
|
2738
|
+
|
|
2739
|
+
Install the compact, versioned GNO protocol block into the global (user-scope)
|
|
2740
|
+
instruction files of every detected harness. The block is bounded by stable
|
|
2741
|
+
markers (`<!-- gno:agents:begin -->` / `<!-- gno:agents:end -->`); install and
|
|
2742
|
+
update touch ONLY the owned block — content outside the markers stays
|
|
2743
|
+
byte-identical. The block content is static (identical on every machine), so
|
|
2744
|
+
a block is current exactly when its stamp version and hash match the installed
|
|
2745
|
+
release.
|
|
2746
|
+
|
|
2747
|
+
**Synopsis:**
|
|
2748
|
+
|
|
2749
|
+
```bash
|
|
2750
|
+
gno agents install [--target <claude|codex|cursor|opencode|grok|hermes|openclaw|all>] [--extra-dir <path>]... [--dry-run] [--json]
|
|
2751
|
+
```
|
|
2752
|
+
|
|
2753
|
+
**Options:**
|
|
2754
|
+
|
|
2755
|
+
| Option | Type | Default | Description |
|
|
2756
|
+
| ------------- | ------- | ------- | ----------------------------------------------------------------------------------------------- |
|
|
2757
|
+
| `--target` | string | all | Harness to install for; `all` = every harness detected on this machine |
|
|
2758
|
+
| `--extra-dir` | string | — | Additional instruction dir (repeatable, for nonstandard/multi-instance layouts). Never guessed. |
|
|
2759
|
+
| `--dry-run` | boolean | false | Print per-target unified diffs; write nothing |
|
|
2760
|
+
|
|
2761
|
+
**Harness matrix (standard documented locations):**
|
|
2762
|
+
|
|
2763
|
+
| Target | Instruction file | Detection root | Notes |
|
|
2764
|
+
| ---------- | ------------------------------------------------------- | ----------------------- | ----------------------------------------------------------------------------------- |
|
|
2765
|
+
| `claude` | `$CLAUDE_CONFIG_DIR/CLAUDE.md` or `~/.claude/CLAUDE.md` | `~/.claude` | Honors `CLAUDE_CONFIG_DIR` |
|
|
2766
|
+
| `codex` | `$CODEX_HOME/AGENTS.md` or `~/.codex/AGENTS.md` | `~/.codex` | Honors `CODEX_HOME` |
|
|
2767
|
+
| `cursor` | `~/AGENTS.md` | `~/.cursor` | Cursor Agent walks cwd → home for AGENTS.md |
|
|
2768
|
+
| `opencode` | `~/.config/opencode/AGENTS.md` | `~/.config/opencode` | |
|
|
2769
|
+
| `grok` | — (import chain) | `~/.grok` | Reads the Claude global file; reported `covered via claude`, never double-installed |
|
|
2770
|
+
| `hermes` | `~/.hermes/SOUL.md` | `~/.hermes` | |
|
|
2771
|
+
| `openclaw` | `~/.openclaw/workspace/AGENTS.md` | `~/.openclaw/workspace` | |
|
|
2772
|
+
|
|
2773
|
+
**Behavior:**
|
|
2774
|
+
|
|
2775
|
+
1. Detects harnesses (config dir exists); undetected targets are reported
|
|
2776
|
+
`not-detected` and skipped — the installer never fabricates harness dirs
|
|
2777
|
+
(creating the instruction FILE inside a detected dir is fine). An explicit
|
|
2778
|
+
detected covered target (e.g. `--target grok`) also resolves its covering
|
|
2779
|
+
target so the file it actually reads converges; when the explicit target is
|
|
2780
|
+
absent, the covering chain never activates.
|
|
2781
|
+
2. Backup-first, atomic: an existing file is copied to
|
|
2782
|
+
`<file>.gno-agents.bak.<timestamp>` (same permission mode as the source),
|
|
2783
|
+
then the new content is written to a sibling temp file and renamed over the
|
|
2784
|
+
destination, so a failed write leaves the live file unchanged. A fresh
|
|
2785
|
+
install appends the block after one blank line (a file without a final
|
|
2786
|
+
newline gets that newline first).
|
|
2787
|
+
3. Idempotent: a current block is a `current` no-op (no write, no backup).
|
|
2788
|
+
4. Fail-closed: malformed or duplicate markers, a file that is not valid
|
|
2789
|
+
UTF-8, or any read/write failure produce a per-target `error` row, nothing
|
|
2790
|
+
is written to that file, and — for install/update — the command prints the
|
|
2791
|
+
complete block (`manualBlock` in JSON) so the operator can apply it by
|
|
2792
|
+
hand. Other targets in the same run still proceed. A leading UTF-8 BOM is
|
|
2793
|
+
preserved across every operation.
|
|
2794
|
+
5. Symlink-aware: writes go through the resolved real file (an operator's
|
|
2795
|
+
canonical-file-linked-everywhere scheme survives); targets resolving to the
|
|
2796
|
+
same real file are written once (`covered via <target> (same file)`).
|
|
2797
|
+
6. `GNO_AGENTS_HOME_OVERRIDE` overrides the home directory (testing/sandboxed
|
|
2798
|
+
verification); any home override suppresses harness config-dir env vars.
|
|
2799
|
+
|
|
2800
|
+
**Output (JSON):**
|
|
2801
|
+
|
|
2802
|
+
```json
|
|
2803
|
+
{
|
|
2804
|
+
"command": "install",
|
|
2805
|
+
"blockVersion": 2,
|
|
2806
|
+
"dryRun": false,
|
|
2807
|
+
"results": [
|
|
2808
|
+
{
|
|
2809
|
+
"target": "claude",
|
|
2810
|
+
"label": "Claude Code",
|
|
2811
|
+
"path": "/home/user/.claude/CLAUDE.md",
|
|
2812
|
+
"action": "install",
|
|
2813
|
+
"detected": true,
|
|
2814
|
+
"backup": null
|
|
2815
|
+
},
|
|
2816
|
+
{
|
|
2817
|
+
"target": "grok",
|
|
2818
|
+
"label": "Grok Build",
|
|
2819
|
+
"path": "/home/user/.claude/CLAUDE.md",
|
|
2820
|
+
"action": "covered",
|
|
2821
|
+
"detected": true,
|
|
2822
|
+
"via": "claude",
|
|
2823
|
+
"detail": "covered via claude"
|
|
2824
|
+
}
|
|
2825
|
+
]
|
|
2826
|
+
}
|
|
2827
|
+
```
|
|
2828
|
+
|
|
2829
|
+
`action` is one of `install`, `update`, `current`, `covered`, `not-detected`,
|
|
2830
|
+
`error` (and `remove`, `absent` for uninstall). A covered row's `path` is the
|
|
2831
|
+
file the harness actually reads (its covering target's file). With
|
|
2832
|
+
`--dry-run`, a `diffs` array of unified diffs is included. When
|
|
2833
|
+
any target failed on install/update, `manualBlock` carries the complete block
|
|
2834
|
+
text to paste. Schema: `spec/output-schemas/agents-mutation.schema.json`
|
|
2835
|
+
(shared by install, update, and uninstall).
|
|
2836
|
+
|
|
2837
|
+
**Exit Codes:**
|
|
2838
|
+
|
|
2839
|
+
- 0: Success (including no-op)
|
|
2840
|
+
- 1: Validation failure (unknown target, missing `--extra-dir`, malformed
|
|
2841
|
+
markers, non-UTF-8 file)
|
|
2842
|
+
- 2: Runtime failure — every failing target hit an I/O error (unreadable file,
|
|
2843
|
+
backup or write failed); the receipt still lists every target's outcome
|
|
2844
|
+
|
|
2845
|
+
---
|
|
2846
|
+
|
|
2847
|
+
### gno agents update
|
|
2848
|
+
|
|
2849
|
+
Refresh an installed block in place (block-version migration). Same options,
|
|
2850
|
+
output, and exit codes as `agents install`; both verbs converge the block to
|
|
2851
|
+
the current release — `update` exists for operator intent clarity.
|
|
2852
|
+
|
|
2853
|
+
```bash
|
|
2854
|
+
gno agents update [--target <...>] [--extra-dir <path>]... [--dry-run] [--json]
|
|
2855
|
+
```
|
|
2856
|
+
|
|
2857
|
+
---
|
|
2858
|
+
|
|
2859
|
+
### gno agents verify
|
|
2860
|
+
|
|
2861
|
+
Deterministic per-target verification of installed blocks. Fresh-session
|
|
2862
|
+
behavioral canaries are an operator practice, not automated here.
|
|
2863
|
+
|
|
2864
|
+
**Synopsis:**
|
|
2865
|
+
|
|
2866
|
+
```bash
|
|
2867
|
+
gno agents verify [--target <...>] [--extra-dir <path>]... [--json]
|
|
2868
|
+
```
|
|
2869
|
+
|
|
2870
|
+
**Checks per target:**
|
|
2871
|
+
|
|
2872
|
+
- Exactly one marker block (`malformed` otherwise)
|
|
2873
|
+
- Block stamp hash matches the block body (tamper detection)
|
|
2874
|
+
- Block version and content match the installed release (`outdated` otherwise)
|
|
2875
|
+
- The block carries no filesystem references by construction, so the spec's
|
|
2876
|
+
link-resolution check is vacuously satisfied
|
|
2877
|
+
- Targets resolving to the same real file are verified once, using install's
|
|
2878
|
+
ownership order; the rest report `covered via <target> (same file)`
|
|
2879
|
+
|
|
2880
|
+
**Output (JSON):**
|
|
2881
|
+
|
|
2882
|
+
```json
|
|
2883
|
+
{
|
|
2884
|
+
"command": "verify",
|
|
2885
|
+
"blockVersion": 2,
|
|
2886
|
+
"ok": true,
|
|
2887
|
+
"results": [
|
|
2888
|
+
{
|
|
2889
|
+
"target": "claude",
|
|
2890
|
+
"label": "Claude Code",
|
|
2891
|
+
"path": "/home/user/.claude/CLAUDE.md",
|
|
2892
|
+
"status": "ok",
|
|
2893
|
+
"detected": true,
|
|
2894
|
+
"blockVersion": 2,
|
|
2895
|
+
"hashOk": true
|
|
2896
|
+
}
|
|
2897
|
+
]
|
|
2898
|
+
}
|
|
2899
|
+
```
|
|
2900
|
+
|
|
2901
|
+
`status` is one of `ok`, `outdated`, `missing`, `malformed`, `error`,
|
|
2902
|
+
`covered`, `not-detected`. `error` means the file exists but could not be read
|
|
2903
|
+
— a "could not check", never a content verdict. Schema:
|
|
2904
|
+
`spec/output-schemas/agents-verify.schema.json`.
|
|
2905
|
+
|
|
2906
|
+
**Exit Codes:**
|
|
2907
|
+
|
|
2908
|
+
- 0: All verified targets ok (covered / not-detected do not fail)
|
|
2909
|
+
- 1: One or more targets `outdated`, `missing`, or `malformed`
|
|
2910
|
+
- 2: Every failing target is an `error` (I/O) row
|
|
2911
|
+
|
|
2912
|
+
---
|
|
2913
|
+
|
|
2914
|
+
### gno agents uninstall
|
|
2915
|
+
|
|
2916
|
+
Remove the block, its markers, and the blank line install added above it;
|
|
2917
|
+
everything else stays byte-identical. Same options, output shape
|
|
2918
|
+
(`action: remove|absent|...`), and exit codes as `agents install`.
|
|
2919
|
+
|
|
2920
|
+
```bash
|
|
2921
|
+
gno agents uninstall [--target <...>] [--extra-dir <path>]... [--dry-run] [--json]
|
|
2922
|
+
```
|
|
2923
|
+
|
|
2924
|
+
---
|
|
2925
|
+
|
|
2731
2926
|
### gno tags list
|
|
2732
2927
|
|
|
2733
2928
|
List all tags with document counts.
|
|
@@ -3643,16 +3838,18 @@ Write-lease contention on `index` / `update` / `embed` does not use the generic
|
|
|
3643
3838
|
|
|
3644
3839
|
## Environment Variables
|
|
3645
3840
|
|
|
3646
|
-
| Variable | Description
|
|
3647
|
-
| -------------------------- |
|
|
3648
|
-
| `GNO_CONFIG_DIR` | Override config directory
|
|
3649
|
-
| `GNO_DATA_DIR` | Override data directory (DB location)
|
|
3650
|
-
| `GNO_CACHE_DIR` | Override cache directory (models)
|
|
3651
|
-
| `NO_COLOR` | Disable colored output (standard)
|
|
3652
|
-
| `PAGER` | Pager for long output (default: less -R on Unix, built-in on Windows)
|
|
3653
|
-
| `GNO_SKILLS_HOME_OVERRIDE` | Override home dir for skill user scope (testing)
|
|
3654
|
-
| `CLAUDE_SKILLS_DIR` | Override Claude skills directory
|
|
3655
|
-
| `CODEX_SKILLS_DIR` | Override Codex skills directory
|
|
3841
|
+
| Variable | Description |
|
|
3842
|
+
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
3843
|
+
| `GNO_CONFIG_DIR` | Override config directory |
|
|
3844
|
+
| `GNO_DATA_DIR` | Override data directory (DB location) |
|
|
3845
|
+
| `GNO_CACHE_DIR` | Override cache directory (models) |
|
|
3846
|
+
| `NO_COLOR` | Disable colored output (standard) |
|
|
3847
|
+
| `PAGER` | Pager for long output (default: less -R on Unix, built-in on Windows) |
|
|
3848
|
+
| `GNO_SKILLS_HOME_OVERRIDE` | Override home dir for skill user scope (testing) |
|
|
3849
|
+
| `CLAUDE_SKILLS_DIR` | Override Claude skills directory |
|
|
3850
|
+
| `CODEX_SKILLS_DIR` | Override Codex skills directory |
|
|
3851
|
+
| `CLAUDE_CONFIG_DIR` | Claude Code config dir; `gno agents` resolves Claude's instruction file under it (suppressed by an explicit home override) |
|
|
3852
|
+
| `CODEX_HOME` | Codex config dir; same rule as `CLAUDE_CONFIG_DIR` |
|
|
3656
3853
|
|
|
3657
3854
|
---
|
|
3658
3855
|
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "gno://schemas/agents-mutation@1.0",
|
|
4
|
+
"title": "GNO Agents Mutation Receipt",
|
|
5
|
+
"description": "JSON receipt for `gno agents install|update|uninstall --json`",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["command", "blockVersion", "dryRun", "results"],
|
|
9
|
+
"allOf": [
|
|
10
|
+
{
|
|
11
|
+
"if": {
|
|
12
|
+
"properties": { "dryRun": { "const": false } },
|
|
13
|
+
"required": ["dryRun"]
|
|
14
|
+
},
|
|
15
|
+
"then": {
|
|
16
|
+
"not": { "properties": { "diffs": true }, "required": ["diffs"] }
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"properties": {
|
|
21
|
+
"command": {
|
|
22
|
+
"enum": ["install", "update", "uninstall"],
|
|
23
|
+
"description": "The mutation verb that produced this receipt"
|
|
24
|
+
},
|
|
25
|
+
"blockVersion": {
|
|
26
|
+
"type": "integer",
|
|
27
|
+
"minimum": 1,
|
|
28
|
+
"description": "Protocol block version of the installed release"
|
|
29
|
+
},
|
|
30
|
+
"dryRun": {
|
|
31
|
+
"type": "boolean",
|
|
32
|
+
"description": "True when nothing was written; diffs carries the planned changes"
|
|
33
|
+
},
|
|
34
|
+
"results": {
|
|
35
|
+
"type": "array",
|
|
36
|
+
"description": "One row per resolved target",
|
|
37
|
+
"items": {
|
|
38
|
+
"type": "object",
|
|
39
|
+
"additionalProperties": false,
|
|
40
|
+
"required": ["target", "label", "path", "action", "detected"],
|
|
41
|
+
"allOf": [
|
|
42
|
+
{
|
|
43
|
+
"if": {
|
|
44
|
+
"properties": { "action": { "const": "covered" } },
|
|
45
|
+
"required": ["action"]
|
|
46
|
+
},
|
|
47
|
+
"then": {
|
|
48
|
+
"properties": { "via": true, "detail": true },
|
|
49
|
+
"required": ["via", "detail"]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
"properties": {
|
|
54
|
+
"target": {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"description": "Harness id (claude, codex, cursor, ...) or extra-dir id"
|
|
57
|
+
},
|
|
58
|
+
"label": {
|
|
59
|
+
"type": "string",
|
|
60
|
+
"description": "Human-readable harness label"
|
|
61
|
+
},
|
|
62
|
+
"path": {
|
|
63
|
+
"type": "string",
|
|
64
|
+
"description": "Instruction file path for this target"
|
|
65
|
+
},
|
|
66
|
+
"action": {
|
|
67
|
+
"enum": [
|
|
68
|
+
"install",
|
|
69
|
+
"update",
|
|
70
|
+
"current",
|
|
71
|
+
"remove",
|
|
72
|
+
"absent",
|
|
73
|
+
"covered",
|
|
74
|
+
"not-detected",
|
|
75
|
+
"error"
|
|
76
|
+
],
|
|
77
|
+
"description": "What the run did (or would do) for this target"
|
|
78
|
+
},
|
|
79
|
+
"detected": {
|
|
80
|
+
"type": "boolean",
|
|
81
|
+
"description": "Whether the harness config dir exists on this machine"
|
|
82
|
+
},
|
|
83
|
+
"via": {
|
|
84
|
+
"type": "string",
|
|
85
|
+
"description": "Target id that covers this one (import chain or shared real file)"
|
|
86
|
+
},
|
|
87
|
+
"detail": {
|
|
88
|
+
"type": "string",
|
|
89
|
+
"description": "Human-readable detail (coverage reason, error guidance)"
|
|
90
|
+
},
|
|
91
|
+
"backup": {
|
|
92
|
+
"type": ["string", "null"],
|
|
93
|
+
"description": "Backup file path for a written target; null when no backup was made (new file or dry run). Omitted for no-op rows."
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
"diffs": {
|
|
99
|
+
"type": "array",
|
|
100
|
+
"description": "Unified diffs of planned writes; present only with dryRun",
|
|
101
|
+
"items": { "type": "string" }
|
|
102
|
+
},
|
|
103
|
+
"manualBlock": {
|
|
104
|
+
"type": "string",
|
|
105
|
+
"description": "The complete block to paste by hand; present on install/update when at least one target reported an error"
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "gno://schemas/agents-verify@1.0",
|
|
4
|
+
"title": "GNO Agents Verify Receipt",
|
|
5
|
+
"description": "JSON receipt for `gno agents verify --json`",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["command", "blockVersion", "ok", "results"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"command": { "const": "verify" },
|
|
11
|
+
"blockVersion": {
|
|
12
|
+
"type": "integer",
|
|
13
|
+
"minimum": 1,
|
|
14
|
+
"description": "Protocol block version of the installed release"
|
|
15
|
+
},
|
|
16
|
+
"ok": {
|
|
17
|
+
"type": "boolean",
|
|
18
|
+
"description": "True when no target is outdated, missing, or malformed (covered / not-detected do not fail)"
|
|
19
|
+
},
|
|
20
|
+
"results": {
|
|
21
|
+
"type": "array",
|
|
22
|
+
"description": "One row per resolved target",
|
|
23
|
+
"items": {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"additionalProperties": false,
|
|
26
|
+
"required": ["target", "label", "path", "status", "detected"],
|
|
27
|
+
"allOf": [
|
|
28
|
+
{
|
|
29
|
+
"if": {
|
|
30
|
+
"properties": { "status": { "const": "covered" } },
|
|
31
|
+
"required": ["status"]
|
|
32
|
+
},
|
|
33
|
+
"then": {
|
|
34
|
+
"properties": { "via": true, "detail": true },
|
|
35
|
+
"required": ["via", "detail"]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
"properties": {
|
|
40
|
+
"target": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"description": "Harness id (claude, codex, cursor, ...) or extra-dir id"
|
|
43
|
+
},
|
|
44
|
+
"label": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"description": "Human-readable harness label"
|
|
47
|
+
},
|
|
48
|
+
"path": {
|
|
49
|
+
"type": "string",
|
|
50
|
+
"description": "Instruction file path for this target"
|
|
51
|
+
},
|
|
52
|
+
"status": {
|
|
53
|
+
"enum": [
|
|
54
|
+
"ok",
|
|
55
|
+
"outdated",
|
|
56
|
+
"missing",
|
|
57
|
+
"malformed",
|
|
58
|
+
"error",
|
|
59
|
+
"covered",
|
|
60
|
+
"not-detected"
|
|
61
|
+
],
|
|
62
|
+
"description": "Verification outcome for this target (`error` = the file exists but could not be read)"
|
|
63
|
+
},
|
|
64
|
+
"detected": {
|
|
65
|
+
"type": "boolean",
|
|
66
|
+
"description": "Whether the harness config dir exists on this machine"
|
|
67
|
+
},
|
|
68
|
+
"via": {
|
|
69
|
+
"type": "string",
|
|
70
|
+
"description": "Target id whose row carries the actual block status (import chain or shared real file)"
|
|
71
|
+
},
|
|
72
|
+
"detail": {
|
|
73
|
+
"type": "string",
|
|
74
|
+
"description": "Human-readable detail (coverage reason, failure guidance)"
|
|
75
|
+
},
|
|
76
|
+
"blockVersion": {
|
|
77
|
+
"type": "integer",
|
|
78
|
+
"minimum": 0,
|
|
79
|
+
"description": "Version stamped on the found block (a legacy/pre-release stamp may read 0 — the `outdated` migration case); omitted when the stamp is unparseable"
|
|
80
|
+
},
|
|
81
|
+
"hashOk": {
|
|
82
|
+
"type": "boolean",
|
|
83
|
+
"description": "Block body matches its stamp hash (tamper detection)"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GNO agents protocol block: content, markers, hashing, extraction.
|
|
3
|
+
*
|
|
4
|
+
* One compact, versioned instruction block bounded by stable BEGIN/END
|
|
5
|
+
* markers. Install/update/uninstall touch ONLY the owned block; content
|
|
6
|
+
* outside the markers stays byte-identical. The block content is static —
|
|
7
|
+
* identical on every machine — so a block is current exactly when its stamp
|
|
8
|
+
* version matches the installed release and its hash matches its body.
|
|
9
|
+
*
|
|
10
|
+
* @module src/cli/commands/agents/block
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { CliError } from "../../errors.js";
|
|
14
|
+
|
|
15
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
16
|
+
// Constants
|
|
17
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
/** Version of the protocol block content. Bump on any content change. */
|
|
20
|
+
export const BLOCK_VERSION = 2;
|
|
21
|
+
|
|
22
|
+
/** Stable across block versions — never change these once shipped. */
|
|
23
|
+
export const BEGIN_MARKER = "<!-- gno:agents:begin -->";
|
|
24
|
+
export const END_MARKER = "<!-- gno:agents:end -->";
|
|
25
|
+
|
|
26
|
+
const STAMP_RE = /^<!-- gno-agents block v(\d+) sha256:([0-9a-f]{16}) /;
|
|
27
|
+
const HASH_PREFIX_LENGTH = 16;
|
|
28
|
+
|
|
29
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
30
|
+
// Rendering
|
|
31
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The protocol block body (between stamp line and end marker). Compact by
|
|
35
|
+
* design: the retrieval ladder + the writing contract. Detailed workflows live
|
|
36
|
+
* in the GNO skill; the block just points at it. No filesystem paths — the
|
|
37
|
+
* text is the same on every machine.
|
|
38
|
+
*/
|
|
39
|
+
export function renderBlockBody(): string {
|
|
40
|
+
return `## GNO knowledge retrieval
|
|
41
|
+
|
|
42
|
+
Local knowledge search over indexed collections. Source files are the truth; the GNO index is disposable, machine-local.
|
|
43
|
+
|
|
44
|
+
Ladder — scope to a collection first (\`--collection <name>\`):
|
|
45
|
+
|
|
46
|
+
1. Exact term/identifier/quote/error: \`gno search "<text>"\`
|
|
47
|
+
2. Entity or known document: \`gno query "<question>" --fast -n 10\`
|
|
48
|
+
3. Multi-document evidence for a goal: \`gno context build "<goal>" --budget 12000\`
|
|
49
|
+
4. Change/dependency questions: \`gno changes\` / \`gno diff <doc>\` / \`gno impact <doc>\`
|
|
50
|
+
5. Generated factual answer: \`gno ask "<question>" --verify\` (abstention is valid)
|
|
51
|
+
6. Expected document missing: reformulate + re-check collection scope (\`gno query diagnose "<query>" --target <doc>\`) before any grep fallback.
|
|
52
|
+
|
|
53
|
+
Writing: retrieve first — a question alone is read-only. Edit an existing canonical note in its source file; \`gno capture\` creates genuinely new notes (collection, title/path, source kind, provenance) — never an update API. After writes: reindex the collection, verify retrieval.
|
|
54
|
+
|
|
55
|
+
Cite with gno:// URIs. Advanced retrieval (structured queries, filters, backlinks, similar, capture recipes) lives in the \`gno\` skill: load it (\`/gno\`) when installed, otherwise run \`gno skill install --scope user\` first.`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** SHA-256 hex digest of the body, truncated for the stamp line. */
|
|
59
|
+
export function hashBlockBody(body: string): string {
|
|
60
|
+
const hasher = new Bun.CryptoHasher("sha256");
|
|
61
|
+
hasher.update(body);
|
|
62
|
+
return hasher.digest("hex").slice(0, HASH_PREFIX_LENGTH);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Render the stamp line carrying version + body hash. */
|
|
66
|
+
export function renderStampLine(body: string): string {
|
|
67
|
+
return `<!-- gno-agents block v${BLOCK_VERSION} sha256:${hashBlockBody(body)} — managed by \`gno agents\`; manual edits inside the markers are overwritten -->`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Render the complete block: BEGIN marker, stamp, body, END marker. */
|
|
71
|
+
export function renderBlock(): string {
|
|
72
|
+
const body = renderBlockBody();
|
|
73
|
+
return `${BEGIN_MARKER}\n${renderStampLine(body)}\n${body}\n${END_MARKER}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** True when the extracted stamp's hash matches the extracted body. */
|
|
77
|
+
export function stampAuthenticates(block: ExtractedBlock): boolean {
|
|
78
|
+
return block.stamp !== null && block.stamp.hash === hashBlockBody(block.body);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
82
|
+
// Extraction & Validation
|
|
83
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
export interface ExtractedBlock {
|
|
86
|
+
/** Offset of the BEGIN marker in the file content. */
|
|
87
|
+
start: number;
|
|
88
|
+
/** Offset just past the END marker. */
|
|
89
|
+
end: number;
|
|
90
|
+
/** Everything between the markers (stamp line + body), without markers. */
|
|
91
|
+
inner: string;
|
|
92
|
+
/** Body without the stamp line (equal to inner when no stamp present). */
|
|
93
|
+
body: string;
|
|
94
|
+
/** Parsed stamp, when present and well-formed. */
|
|
95
|
+
stamp: { version: number; hash: string } | null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export type BlockExtraction =
|
|
99
|
+
| { found: false }
|
|
100
|
+
| { found: true; block: ExtractedBlock };
|
|
101
|
+
|
|
102
|
+
function countOccurrences(haystack: string, needle: string): number {
|
|
103
|
+
let count = 0;
|
|
104
|
+
let idx = haystack.indexOf(needle);
|
|
105
|
+
while (idx !== -1) {
|
|
106
|
+
count += 1;
|
|
107
|
+
idx = haystack.indexOf(needle, idx + needle.length);
|
|
108
|
+
}
|
|
109
|
+
return count;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const MARKER_GUIDANCE =
|
|
113
|
+
"Fix or remove the markers manually (or restore the file from its .gno-agents.bak backup), then re-run.";
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Extract the managed block from file content.
|
|
117
|
+
* Fail-closed: malformed or duplicate markers throw with guidance — the
|
|
118
|
+
* installer never guesses or "repairs".
|
|
119
|
+
*/
|
|
120
|
+
export function extractBlock(
|
|
121
|
+
content: string,
|
|
122
|
+
filePath: string
|
|
123
|
+
): BlockExtraction {
|
|
124
|
+
const begins = countOccurrences(content, BEGIN_MARKER);
|
|
125
|
+
const ends = countOccurrences(content, END_MARKER);
|
|
126
|
+
|
|
127
|
+
if (begins === 0 && ends === 0) {
|
|
128
|
+
return { found: false };
|
|
129
|
+
}
|
|
130
|
+
if (begins !== 1 || ends !== 1) {
|
|
131
|
+
throw new CliError(
|
|
132
|
+
"VALIDATION",
|
|
133
|
+
`Malformed GNO agents markers in ${filePath}: found ${begins} BEGIN and ${ends} END marker(s), expected exactly one of each. ${MARKER_GUIDANCE}`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const start = content.indexOf(BEGIN_MARKER);
|
|
138
|
+
const endMarkerStart = content.indexOf(END_MARKER);
|
|
139
|
+
if (endMarkerStart < start) {
|
|
140
|
+
throw new CliError(
|
|
141
|
+
"VALIDATION",
|
|
142
|
+
`Malformed GNO agents markers in ${filePath}: END marker appears before BEGIN marker. ${MARKER_GUIDANCE}`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const end = endMarkerStart + END_MARKER.length;
|
|
147
|
+
const rawInner = content.slice(start + BEGIN_MARKER.length, endMarkerStart);
|
|
148
|
+
// Trim exactly the structural newlines install added around the inner text.
|
|
149
|
+
const inner = rawInner.replace(/^\n/, "").replace(/\n$/, "");
|
|
150
|
+
|
|
151
|
+
const newlineIdx = inner.indexOf("\n");
|
|
152
|
+
const firstLine = newlineIdx === -1 ? inner : inner.slice(0, newlineIdx);
|
|
153
|
+
const stampMatch = STAMP_RE.exec(firstLine);
|
|
154
|
+
const version = stampMatch ? Number(stampMatch[1]) : Number.NaN;
|
|
155
|
+
// An absurd version (not a safe integer) is an unparseable stamp, not a
|
|
156
|
+
// numeric verdict — it would otherwise serialize as null in JSON receipts.
|
|
157
|
+
const stamp =
|
|
158
|
+
stampMatch && Number.isSafeInteger(version)
|
|
159
|
+
? { version, hash: stampMatch[2] ?? "" }
|
|
160
|
+
: null;
|
|
161
|
+
const body = stamp && newlineIdx !== -1 ? inner.slice(newlineIdx + 1) : inner;
|
|
162
|
+
|
|
163
|
+
return { found: true, block: { start, end, inner, body, stamp } };
|
|
164
|
+
}
|