@microck/raft 0.3.0 → 0.4.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/deploy/verify-features.py +36 -2
- package/docs/contract.md +2 -0
- package/docs/guides/scripting-ci.mdx +3 -2
- package/docs/operations.mdx +10 -10
- package/docs/reference/cli.md +26 -6
- package/docs/releases/0.4.0.md +15 -0
- package/package.json +1 -1
- package/raft.py +66 -7
- package/skills/raft-cli/SKILL.md +14 -1
|
@@ -196,7 +196,21 @@ def verify_location_notes():
|
|
|
196
196
|
]:
|
|
197
197
|
rejected = cli(*arguments)
|
|
198
198
|
assert rejected.returncode == 1 and error in rejected.stderr, rejected
|
|
199
|
-
|
|
199
|
+
# Box note mistakes are rejected before any host is contacted.
|
|
200
|
+
handle = "lab:rf-0123456789abcdef"
|
|
201
|
+
for arguments, error in [
|
|
202
|
+
(["note", "lab", "x", "--clear"], "TEXT or --clear"),
|
|
203
|
+
(["note", "lab"], "TEXT or --clear"),
|
|
204
|
+
(["note", "lab", "--purpose", "x"], "pass a box handle"),
|
|
205
|
+
(["note", handle, "x"], "--purpose, --owner and --next"),
|
|
206
|
+
(["note", handle], "--purpose (with optional"),
|
|
207
|
+
(["note", handle, "--purpose", "x", "--clear"], "--purpose (with optional"),
|
|
208
|
+
(["note", handle, "--owner", "agent"], "need --purpose"),
|
|
209
|
+
(["note", handle, "--purpose", " "], "--purpose cannot be empty"),
|
|
210
|
+
(["new", "--location", "lab", "--next", "later"], "need --purpose"),
|
|
211
|
+
]:
|
|
212
|
+
rejected = cli(*arguments)
|
|
213
|
+
assert rejected.returncode == 1 and error in rejected.stderr, (arguments, rejected)
|
|
200
214
|
# Concurrent edits to different locations must all survive.
|
|
201
215
|
many = {f"host{index}": {"ssh": f"host{index}"} for index in range(8)}
|
|
202
216
|
saved = config.read_text()
|
|
@@ -429,9 +443,27 @@ def verify(location, isolated_worker=False):
|
|
|
429
443
|
live.raft("destroy", handle)
|
|
430
444
|
fixtures.remove(handle)
|
|
431
445
|
print(f"{location}: noted new kept stdout to the handle and the note on stderr", flush=True)
|
|
432
|
-
box = create("--memory", "1GiB")
|
|
446
|
+
box = create("--memory", "1GiB", "--purpose", " Feature verification ", "--owner", "verify")
|
|
447
|
+
|
|
448
|
+
def listed_note(handle):
|
|
449
|
+
for line in live.raft("list", "--location", location).stdout.splitlines():
|
|
450
|
+
record = json.loads(line)
|
|
451
|
+
if record["box"] == handle:
|
|
452
|
+
return record["note"]
|
|
453
|
+
raise AssertionError(f"{handle} missing from list")
|
|
454
|
+
|
|
455
|
+
assert listed_note(box) == {"purpose": "Feature verification", "owner": "verify"}
|
|
456
|
+
# Each note edit replaces the whole note.
|
|
457
|
+
live.raft("note", box, "--purpose", "Feature verification", "--next", "Destroy at the end")
|
|
458
|
+
assert listed_note(box) == {"purpose": "Feature verification", "next": "Destroy at the end"}
|
|
459
|
+
live.raft("note", box, "--clear")
|
|
460
|
+
assert listed_note(box) is None
|
|
461
|
+
live.raft("note", box, "--purpose", "Feature verification")
|
|
433
462
|
live.wait_running(box)
|
|
434
463
|
_, name = parse_handle(box)
|
|
464
|
+
print(
|
|
465
|
+
f"{location}: box note template, listing, replacement and clearing passed", flush=True
|
|
466
|
+
)
|
|
435
467
|
with tempfile.TemporaryDirectory(prefix="raft-tree-test-") as work:
|
|
436
468
|
root = Path(work)
|
|
437
469
|
source = root / "source with spaces"
|
|
@@ -522,6 +554,7 @@ def verify(location, isolated_worker=False):
|
|
|
522
554
|
!= metadata(box)["config"]["volatile.eth0.hwaddr"]
|
|
523
555
|
)
|
|
524
556
|
assert metadata(template)["config"]["user.raft.disposable"] == "false"
|
|
557
|
+
assert listed_note(template) is None, "new --from inherited the source note"
|
|
525
558
|
assert metadata(template)["expanded_config"]["limits.cpu"] == "1"
|
|
526
559
|
live.raft("snapshot-delete", box, "prepared")
|
|
527
560
|
assert live.execute(template, "cat", "/workspace/proof") == "original"
|
|
@@ -536,6 +569,7 @@ def verify(location, isolated_worker=False):
|
|
|
536
569
|
fork = live.raft(*fork_args).stdout.strip()
|
|
537
570
|
fixtures.append(fork)
|
|
538
571
|
assert metadata(fork)["expanded_config"]["limits.memory"] == "1GiB"
|
|
572
|
+
assert listed_note(fork) == {"purpose": "Feature verification"}, "fork lost the note"
|
|
539
573
|
assert metadata(fork)["expanded_config"]["limits.cpu"] == "1"
|
|
540
574
|
assert metadata(box)["expanded_config"]["limits.memory"] == "4GiB"
|
|
541
575
|
if worker:
|
package/docs/contract.md
CHANGED
|
@@ -42,6 +42,8 @@ Raft manages persistent unprivileged native-architecture Incus system containers
|
|
|
42
42
|
|
|
43
43
|
`raft locations` lists configured locations as JSONL with `location`, `default` and `note` fields without contacting hosts. `raft note LOCATION TEXT` and `raft note LOCATION --clear` edit the optional `note` field in the controller configuration atomically, preserving location order and mode `0600`. Notes are advisory guidance; Raft shows them in `locations`, `limits` and on stderr from `new`, and never enforces them.
|
|
44
44
|
|
|
45
|
+
Boxes accept an optional note with a fixed template: `purpose` (required), `owner` and `next`, all nonempty strings. `raft new --purpose TEXT [--owner TEXT] [--next TEXT]` and `raft note BOX ...` store it as JSON in the box's `user.raft.note` Incus key; `raft note BOX --clear` removes it. Each edit replaces the whole note. `raft list` reports it as `note`, or `null`. `new` always writes the key, so `new --from` never inherits a snapshot's note; forks and recovered backups keep it. Box notes are advisory and never change lifecycle policy.
|
|
46
|
+
|
|
45
47
|
`raft limits [--location NAME] [--json]` inspects hosts without modifying state. It reports effective CPUs, total and available RAM, saved and active box counts, active resource limits, and disk space.
|
|
46
48
|
|
|
47
49
|
- **Reserve calculations**: Recommends running boxes after reserving 1 CPU and the larger of 2 GiB or 10% host RAM.
|
|
@@ -19,7 +19,7 @@ Raft does not have a global `--json` option. Parse each command's actual format:
|
|
|
19
19
|
| --- | --- |
|
|
20
20
|
| `new`, `fork`, `recover` | One location-qualified handle |
|
|
21
21
|
| `exec --detach` | One `rfcmd-` job ID |
|
|
22
|
-
| `list` | JSONL: one object per box, with `box`, `state`, `stopAt` |
|
|
22
|
+
| `list` | JSONL: one object per box, with `box`, `state`, `stopAt`, `note` |
|
|
23
23
|
| `info` | One pretty-printed native Incus metadata JSON object |
|
|
24
24
|
| `usage` | One JSON object containing running resource counters |
|
|
25
25
|
| `limits --json` | One JSON object per location (JSONL across multiple hosts) |
|
|
@@ -31,7 +31,8 @@ Raft does not have a global `--json` option. Parse each command's actual format:
|
|
|
31
31
|
| Other inspection/maintenance commands | Human-readable text or no output |
|
|
32
32
|
|
|
33
33
|
`list` emits nothing when there are no boxes. `stopAt` is the host Unix deadline
|
|
34
|
-
as a string, or an empty string when absent. `
|
|
34
|
+
as a string, or an empty string when absent. `note` is the box note object with
|
|
35
|
+
`purpose` and optional `owner` and `next`, or `null`. `info` uses native Incus keys rather
|
|
35
36
|
than a normalized Raft schema. `usage` includes `memoryBytes`, `memoryLimit`,
|
|
36
37
|
`cpuTime`, `effectiveCpus`, `filesystemUsedBytes`, `filesystemAvailableBytes`, and
|
|
37
38
|
`filesystemScope`; disk values describe the shared pool, not an individual box.
|
package/docs/operations.mdx
CHANGED
|
@@ -15,7 +15,7 @@ Install the CLI from npm on your Linux controller, confirm key-based SSH to the
|
|
|
15
15
|
host, then let `raft host add` do the remaining steps:
|
|
16
16
|
|
|
17
17
|
```sh title="CONTROLLER: Onboard a host"
|
|
18
|
-
npm install -g @microck/raft@0.
|
|
18
|
+
npm install -g @microck/raft@0.4.0
|
|
19
19
|
ssh ubuntu@192.0.2.10 true
|
|
20
20
|
raft host add lab --ssh ubuntu@192.0.2.10 --dry-run
|
|
21
21
|
raft host add lab --ssh ubuntu@192.0.2.10
|
|
@@ -62,7 +62,7 @@ sudo -n true && echo "Passwordless sudo is functional"
|
|
|
62
62
|
On your Linux controller with Python 3.11.8+, install the published CLI:
|
|
63
63
|
|
|
64
64
|
```sh title="CONTROLLER: Install published CLI"
|
|
65
|
-
npm install -g @microck/raft@0.
|
|
65
|
+
npm install -g @microck/raft@0.4.0
|
|
66
66
|
raft --version
|
|
67
67
|
```
|
|
68
68
|
|
|
@@ -70,7 +70,7 @@ Installation does not configure hosts or download images. Clone the matching
|
|
|
70
70
|
release to obtain the deployment scripts and example configuration:
|
|
71
71
|
|
|
72
72
|
```sh title="CONTROLLER: Obtain deployment scripts"
|
|
73
|
-
git clone --branch v0.
|
|
73
|
+
git clone --branch v0.4.0 https://github.com/Microck/raft.git raft
|
|
74
74
|
cd raft
|
|
75
75
|
```
|
|
76
76
|
|
|
@@ -159,16 +159,16 @@ Workspaces require an unprivileged Debian 13 development image matching the host
|
|
|
159
159
|
|
|
160
160
|
<Tab value="Download release image">
|
|
161
161
|
|
|
162
|
-
Download the release archive, checksum, and manifest from [Raft releases](https://github.com/Microck/raft/releases). These examples pin `v0.
|
|
162
|
+
Download the release archive, checksum, and manifest from [Raft releases](https://github.com/Microck/raft/releases). These examples pin `v0.4.0`, matching the CLI and deployment checkout above. The `sha256sum` check gates import so corrupted archives are rejected:
|
|
163
163
|
|
|
164
164
|
<Tabs items={["ARM64", "AMD64"]}>
|
|
165
165
|
|
|
166
166
|
<Tab value="ARM64">
|
|
167
167
|
|
|
168
168
|
```sh title="HOST (ARM64): Download, verify, and import release image"
|
|
169
|
-
curl -fsSL -O https://github.com/Microck/raft/releases/download/v0.
|
|
170
|
-
curl -fsSL -O https://github.com/Microck/raft/releases/download/v0.
|
|
171
|
-
curl -fsSL -O https://github.com/Microck/raft/releases/download/v0.
|
|
169
|
+
curl -fsSL -O https://github.com/Microck/raft/releases/download/v0.4.0/raft-dev-arm64.tar.gz
|
|
170
|
+
curl -fsSL -O https://github.com/Microck/raft/releases/download/v0.4.0/SHA256SUMS
|
|
171
|
+
curl -fsSL -O https://github.com/Microck/raft/releases/download/v0.4.0/manifest-arm64.json
|
|
172
172
|
|
|
173
173
|
sha256sum --ignore-missing -c SHA256SUMS && \
|
|
174
174
|
sudo incus --project raft image import ./raft-dev-arm64.tar.gz --alias raft-dev
|
|
@@ -179,9 +179,9 @@ sha256sum --ignore-missing -c SHA256SUMS && \
|
|
|
179
179
|
<Tab value="AMD64">
|
|
180
180
|
|
|
181
181
|
```sh title="HOST (AMD64): Download, verify, and import release image"
|
|
182
|
-
curl -fsSL -O https://github.com/Microck/raft/releases/download/v0.
|
|
183
|
-
curl -fsSL -O https://github.com/Microck/raft/releases/download/v0.
|
|
184
|
-
curl -fsSL -O https://github.com/Microck/raft/releases/download/v0.
|
|
182
|
+
curl -fsSL -O https://github.com/Microck/raft/releases/download/v0.4.0/raft-dev-amd64.tar.gz
|
|
183
|
+
curl -fsSL -O https://github.com/Microck/raft/releases/download/v0.4.0/SHA256SUMS
|
|
184
|
+
curl -fsSL -O https://github.com/Microck/raft/releases/download/v0.4.0/manifest-amd64.json
|
|
185
185
|
|
|
186
186
|
sha256sum --ignore-missing -c SHA256SUMS && \
|
|
187
187
|
sudo incus --project raft image import ./raft-dev-amd64.tar.gz --alias raft-dev
|
package/docs/reference/cli.md
CHANGED
|
@@ -36,7 +36,7 @@ Workspaces are referenced by location-qualified handles formatted as `<location>
|
|
|
36
36
|
|
|
37
37
|
Create and launch a new workspace container.
|
|
38
38
|
|
|
39
|
-
`raft new [--location LOCATION] [--ttl TTL] [--cpu {1,2}] [--memory {1GiB,2GiB,4GiB,8GiB}] [--disposable] [--from LOCATION:BOX/SNAPSHOT]`
|
|
39
|
+
`raft new [--location LOCATION] [--ttl TTL] [--cpu {1,2}] [--memory {1GiB,2GiB,4GiB,8GiB}] [--disposable] [--from LOCATION:BOX/SNAPSHOT] [--purpose TEXT [--owner TEXT] [--next TEXT]]`
|
|
40
40
|
|
|
41
41
|
- `--location LOCATION`: Target host location in `~/.config/raft/incus.json`. Defaults to first configured location.
|
|
42
42
|
- `--ttl TTL`: Lifetime in seconds before automatic stop (`60` to `2592000`). Default: `600`.
|
|
@@ -45,6 +45,7 @@ Create and launch a new workspace container.
|
|
|
45
45
|
|
|
46
46
|
- `--disposable`: Delete the box and snapshots on stop or expiry. Off by default.
|
|
47
47
|
- `--from LOCATION:BOX/SNAPSHOT`: Copy a named snapshot on its source host instead of the configured image. Normal creation sizing applies; the source can be running. Cannot target a different location.
|
|
48
|
+
- `--purpose TEXT`, `--owner TEXT`, `--next TEXT`: Record a [box note](#note). `--owner` and `--next` need `--purpose`. Without them the box starts with no note, including boxes created `--from` a noted snapshot.
|
|
48
49
|
|
|
49
50
|
Returns the qualified workspace handle. If the location has a note, `new` prints
|
|
50
51
|
it to stderr; stdout still contains only the handle.
|
|
@@ -291,14 +292,32 @@ Prints one JSON object per location (JSONL) in configuration order, with
|
|
|
291
292
|
|
|
292
293
|
### `note`
|
|
293
294
|
|
|
294
|
-
Set or clear
|
|
295
|
+
Set or clear the note of a location or a box.
|
|
295
296
|
|
|
296
297
|
`raft note LOCATION TEXT`
|
|
297
298
|
`raft note LOCATION --clear`
|
|
299
|
+
`raft note BOX --purpose TEXT [--owner TEXT] [--next TEXT]`
|
|
300
|
+
`raft note BOX --clear`
|
|
298
301
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
+
Location notes are operator guidance stored in `~/.config/raft/incus.json`. Use
|
|
303
|
+
them to record which host is preferred or what a host should not run. Raft shows
|
|
304
|
+
them in `locations`, `limits` and `new`.
|
|
305
|
+
|
|
306
|
+
Box notes tell other agents and people why a box exists. They follow one
|
|
307
|
+
template, stored on the box as JSON in the Incus key `user.raft.note`:
|
|
308
|
+
|
|
309
|
+
| Field | Meaning | Example |
|
|
310
|
+
| --- | --- | --- |
|
|
311
|
+
| `purpose` (required) | What the box is for | `Integration tests for PR 42` |
|
|
312
|
+
| `owner` | Who uses it | `codex session in ~/work/app` |
|
|
313
|
+
| `next` | What happens to it next | `Destroy after the tests pass` |
|
|
314
|
+
|
|
315
|
+
Each call replaces the whole note, on running or stopped boxes. `raft list`
|
|
316
|
+
shows it as `note`. Forks and recovered backups keep the source box's note;
|
|
317
|
+
update it if the copy has a different job.
|
|
318
|
+
|
|
319
|
+
Notes are advisory; Raft never enforces them. Empty values are rejected; use
|
|
320
|
+
`--clear` to remove a note.
|
|
302
321
|
|
|
303
322
|
## Host inspection and maintenance commands
|
|
304
323
|
|
|
@@ -325,7 +344,8 @@ List existing workspace containers across configured hosts.
|
|
|
325
344
|
|
|
326
345
|
`raft list [--location LOCATION]`
|
|
327
346
|
|
|
328
|
-
Prints one JSON object per box (JSONL), with `box`, `state`, and `
|
|
347
|
+
Prints one JSON object per box (JSONL), with `box`, `state`, `stopAt` and `note` fields.
|
|
348
|
+
`note` is the box note object, or `null` when the box has none.
|
|
329
349
|
An empty inventory prints no records. See [Scripting and CI](../guides/scripting-ci.mdx)
|
|
330
350
|
for output formats, exit codes, readiness, and failure handling.
|
|
331
351
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Raft 0.4.0
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Raft 0.4.0
|
|
6
|
+
|
|
7
|
+
- Boxes accept a note that says why they exist, so agents and people can tell which boxes are in use. The note follows one template: `purpose` (required), `owner` and `next`.
|
|
8
|
+
- `raft new --purpose TEXT [--owner TEXT] [--next TEXT]` notes a box when it is created. `raft note BOX --purpose ...` replaces a box's note and `raft note BOX --clear` removes it. `raft note LOCATION ...` still edits location notes.
|
|
9
|
+
- `raft list` adds a `note` field to each box: the note object, or `null`.
|
|
10
|
+
- Forks and recovered backups keep the source box's note. `new --from` starts without one.
|
|
11
|
+
- The `raft-cli` agent skill asks agents to note every box they create and to read notes before resuming, stopping, forking or destroying a box.
|
|
12
|
+
|
|
13
|
+
Install on Linux with Python 3.11.8+ using `npm install -g @microck/raft@0.4.0`, or install the attached Python wheel. Box notes live in each box's Incus configuration; host workers are unchanged and need no redeployment.
|
|
14
|
+
|
|
15
|
+
ARM64 and AMD64 development images accompany this release. Verify `SHA256SUMS` before importing an image for your host architecture.
|
package/package.json
CHANGED
package/raft.py
CHANGED
|
@@ -19,7 +19,7 @@ import uuid
|
|
|
19
19
|
|
|
20
20
|
import raft_files
|
|
21
21
|
|
|
22
|
-
__version__ = "0.
|
|
22
|
+
__version__ = "0.4.0"
|
|
23
23
|
|
|
24
24
|
CONFIG = Path.home() / ".config/raft/incus.json"
|
|
25
25
|
PROJECT = "raft"
|
|
@@ -30,6 +30,9 @@ GIB = 1024**3
|
|
|
30
30
|
# all derive from these lists, so adding a size here covers every command.
|
|
31
31
|
CPU_SIZES = [1, 2]
|
|
32
32
|
MEMORY_SIZES = ["1GiB", "2GiB", "4GiB", "8GiB"]
|
|
33
|
+
# Box notes follow one template so any agent can tell why a box exists, who uses
|
|
34
|
+
# it and what happens to it next. Each value is free text; purpose is required.
|
|
35
|
+
BOX_NOTE_FIELDS = ["purpose", "owner", "next"]
|
|
33
36
|
|
|
34
37
|
USAGE = """import json, shutil
|
|
35
38
|
from pathlib import Path
|
|
@@ -113,6 +116,26 @@ def set_note(location, text):
|
|
|
113
116
|
edit_configuration(change)
|
|
114
117
|
|
|
115
118
|
|
|
119
|
+
def box_note(args):
|
|
120
|
+
"""Build a box note from --purpose, --owner and --next; None when none is given."""
|
|
121
|
+
note = {}
|
|
122
|
+
for field in BOX_NOTE_FIELDS:
|
|
123
|
+
value = getattr(args, field, None)
|
|
124
|
+
if value is not None:
|
|
125
|
+
if not value.strip():
|
|
126
|
+
raise ValueError(f"--{field} cannot be empty")
|
|
127
|
+
note[field] = value.strip()
|
|
128
|
+
if note and "purpose" not in note:
|
|
129
|
+
raise ValueError("Box notes need --purpose")
|
|
130
|
+
return note or None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def read_box_note(box):
|
|
134
|
+
"""Return a box's note as a dict, or None. Raft writes it as JSON in user.raft.note."""
|
|
135
|
+
stored = box["config"].get("user.raft.note")
|
|
136
|
+
return json.loads(stored) if stored else None
|
|
137
|
+
|
|
138
|
+
|
|
116
139
|
def receive(command, destination, *, stdin=None):
|
|
117
140
|
"""Write streamed bytes in Python: SSH can exit zero after a local write error."""
|
|
118
141
|
with tempfile.TemporaryFile() as errors:
|
|
@@ -607,6 +630,7 @@ def sizing(args):
|
|
|
607
630
|
|
|
608
631
|
|
|
609
632
|
def new(args):
|
|
633
|
+
box_fields = box_note(args)
|
|
610
634
|
source = None
|
|
611
635
|
if args.template:
|
|
612
636
|
handle, separator, snapshot = args.template.partition("/")
|
|
@@ -658,6 +682,9 @@ def new(args):
|
|
|
658
682
|
"-c",
|
|
659
683
|
f"user.raft.disposable={'true' if args.disposable else 'false'}",
|
|
660
684
|
"-c",
|
|
685
|
+
# Always set, so a box created from a snapshot never inherits the source note.
|
|
686
|
+
"user.raft.note=" + (json.dumps(box_fields) if box_fields else ""),
|
|
687
|
+
"-c",
|
|
661
688
|
"volatile.eth0.hwaddr=",
|
|
662
689
|
*sizing(args),
|
|
663
690
|
)
|
|
@@ -849,6 +876,19 @@ def ttl(value):
|
|
|
849
876
|
return seconds
|
|
850
877
|
|
|
851
878
|
|
|
879
|
+
def note_options(command):
|
|
880
|
+
"""Add the box note template flags shared by new and note."""
|
|
881
|
+
command.add_argument(
|
|
882
|
+
"--purpose", help="Box note: what the box is for, e.g. 'Integration tests for PR 42'"
|
|
883
|
+
)
|
|
884
|
+
command.add_argument(
|
|
885
|
+
"--owner", help="Box note: who uses the box, e.g. 'codex session in ~/work/app'"
|
|
886
|
+
)
|
|
887
|
+
command.add_argument(
|
|
888
|
+
"--next", help="Box note: what happens next, e.g. 'Destroy after the tests pass'"
|
|
889
|
+
)
|
|
890
|
+
|
|
891
|
+
|
|
852
892
|
def parser():
|
|
853
893
|
cli = argparse.ArgumentParser(description=__doc__)
|
|
854
894
|
cli.add_argument("--version", action="version", version=__version__)
|
|
@@ -860,12 +900,13 @@ def parser():
|
|
|
860
900
|
create.add_argument("--memory", choices=MEMORY_SIZES, default="2GiB")
|
|
861
901
|
create.add_argument("--disposable", action="store_true", help="Delete on stop or expiry")
|
|
862
902
|
create.add_argument("--from", dest="template", metavar="LOCATION:BOX/SNAPSHOT")
|
|
903
|
+
note_options(create)
|
|
863
904
|
commands.add_parser("locations", help="List configured locations and their notes")
|
|
864
|
-
note = commands.add_parser("note", help="Set or clear
|
|
865
|
-
note.add_argument("location")
|
|
866
|
-
text =
|
|
867
|
-
|
|
868
|
-
|
|
905
|
+
note = commands.add_parser("note", help="Set or clear the note of a location or a box")
|
|
906
|
+
note.add_argument("target", metavar="LOCATION|BOX", help="A location name or a box handle")
|
|
907
|
+
note.add_argument("text", nargs="?", help="Location guidance, e.g. 'Preferred for builds'")
|
|
908
|
+
note.add_argument("--clear", action="store_true", help="Remove the note")
|
|
909
|
+
note_options(note)
|
|
869
910
|
host = commands.add_parser("host", help="Onboard workspace hosts")
|
|
870
911
|
host_commands = host.add_subparsers(dest="host_action", required=True)
|
|
871
912
|
onboard = host_commands.add_parser(
|
|
@@ -967,7 +1008,24 @@ def dispatch(args):
|
|
|
967
1008
|
host_add(args)
|
|
968
1009
|
return 0
|
|
969
1010
|
if args.action == "note":
|
|
970
|
-
|
|
1011
|
+
fields = box_note(args)
|
|
1012
|
+
# Location names cannot contain ":", so a colon always means a box handle.
|
|
1013
|
+
if ":" not in args.target:
|
|
1014
|
+
if fields is not None:
|
|
1015
|
+
raise ValueError("--purpose, --owner and --next describe boxes; pass a box handle")
|
|
1016
|
+
if args.clear == (args.text is not None):
|
|
1017
|
+
raise ValueError("Give the location note TEXT or --clear")
|
|
1018
|
+
set_note(args.target, None if args.clear else args.text)
|
|
1019
|
+
return 0
|
|
1020
|
+
if args.text is not None:
|
|
1021
|
+
raise ValueError("Describe boxes with --purpose, --owner and --next")
|
|
1022
|
+
if args.clear == (fields is not None):
|
|
1023
|
+
raise ValueError("Give --purpose (with optional --owner and --next) or --clear")
|
|
1024
|
+
location, name = parse_handle(args.target)
|
|
1025
|
+
instance(location, name)
|
|
1026
|
+
# Each call replaces the whole note; an empty value removes the key.
|
|
1027
|
+
value = "" if args.clear else json.dumps(fields)
|
|
1028
|
+
incus(location, "config", "set", name, "user.raft.note=" + value, locked=True)
|
|
971
1029
|
return 0
|
|
972
1030
|
if args.action == "locations":
|
|
973
1031
|
for index, (location, entry) in enumerate(configuration().items()):
|
|
@@ -1032,6 +1090,7 @@ def dispatch(args):
|
|
|
1032
1090
|
"box": location + ":" + box["name"],
|
|
1033
1091
|
"state": box["status"],
|
|
1034
1092
|
"stopAt": box["config"].get("user.raft.expires", ""),
|
|
1093
|
+
"note": read_box_note(box),
|
|
1035
1094
|
}
|
|
1036
1095
|
)
|
|
1037
1096
|
)
|
package/skills/raft-cli/SKILL.md
CHANGED
|
@@ -18,8 +18,21 @@ the default. Do not change notes unless the user asks; `raft note LOCATION TEXT`
|
|
|
18
18
|
sets one and `raft note LOCATION --clear` removes it. Keep the exact
|
|
19
19
|
location-qualified handle returned by `new`. There is no implicit current box.
|
|
20
20
|
|
|
21
|
+
Note every box you create. Box notes follow one template so the next agent can
|
|
22
|
+
tell whether a box is in use:
|
|
23
|
+
|
|
24
|
+
- `--purpose`: what the box is for (required), e.g. `Integration tests for PR 42`
|
|
25
|
+
- `--owner`: who uses it, e.g. `codex session in ~/work/app`
|
|
26
|
+
- `--next`: what happens next, e.g. `Destroy after the tests pass`
|
|
27
|
+
|
|
28
|
+
Pass them to `new`, and run `raft note "$box" --purpose ...` when the plan
|
|
29
|
+
changes; each call replaces the whole note. `raft list` shows every box's
|
|
30
|
+
`note`. Read it before you resume, stop, fork or destroy a box. Treat a box whose
|
|
31
|
+
note names another owner or a job unrelated to your task as not yours. Forks
|
|
32
|
+
keep the source note, so update a fork's note when its job differs.
|
|
33
|
+
|
|
21
34
|
```sh
|
|
22
|
-
box=$(raft new --location lab --ttl 600)
|
|
35
|
+
box=$(raft new --location lab --ttl 600 --purpose "Run the project tests" --owner "codex task 123" --next "Destroy when the tests finish")
|
|
23
36
|
raft exec "$box" -- timeout 240 bash -lc 'git clone <url> /workspace/project && cd /workspace/project && <test-command>'
|
|
24
37
|
raft download "$box" /workspace/project/report.json ./report.json
|
|
25
38
|
raft destroy "$box"
|