pixie-lab-dibs 0.2.1__tar.gz → 0.3.0__tar.gz

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 (32) hide show
  1. {pixie_lab_dibs-0.2.1 → pixie_lab_dibs-0.3.0}/CHANGELOG.md +21 -0
  2. {pixie_lab_dibs-0.2.1 → pixie_lab_dibs-0.3.0}/PKG-INFO +22 -15
  3. {pixie_lab_dibs-0.2.1 → pixie_lab_dibs-0.3.0}/README.md +21 -14
  4. {pixie_lab_dibs-0.2.1 → pixie_lab_dibs-0.3.0}/src/dibs/__init__.py +1 -1
  5. {pixie_lab_dibs-0.2.1 → pixie_lab_dibs-0.3.0}/src/dibs/_store.py +85 -29
  6. {pixie_lab_dibs-0.2.1 → pixie_lab_dibs-0.3.0}/src/dibs/app.py +52 -11
  7. {pixie_lab_dibs-0.2.1 → pixie_lab_dibs-0.3.0}/src/dibs/cli.py +48 -2
  8. pixie_lab_dibs-0.3.0/src/dibs/skill/SKILL.md +86 -0
  9. {pixie_lab_dibs-0.2.1 → pixie_lab_dibs-0.3.0}/tests/test_app.py +12 -5
  10. {pixie_lab_dibs-0.2.1 → pixie_lab_dibs-0.3.0}/tests/test_cli.py +44 -0
  11. {pixie_lab_dibs-0.2.1 → pixie_lab_dibs-0.3.0}/tests/test_store.py +64 -9
  12. pixie_lab_dibs-0.2.1/src/dibs/skill/SKILL.md +0 -65
  13. {pixie_lab_dibs-0.2.1 → pixie_lab_dibs-0.3.0}/.github/workflows/format.yml +0 -0
  14. {pixie_lab_dibs-0.2.1 → pixie_lab_dibs-0.3.0}/.github/workflows/publish.yml +0 -0
  15. {pixie_lab_dibs-0.2.1 → pixie_lab_dibs-0.3.0}/.github/workflows/test.yml +0 -0
  16. {pixie_lab_dibs-0.2.1 → pixie_lab_dibs-0.3.0}/.gitignore +0 -0
  17. {pixie_lab_dibs-0.2.1 → pixie_lab_dibs-0.3.0}/.pre-commit-config.yaml +0 -0
  18. {pixie_lab_dibs-0.2.1 → pixie_lab_dibs-0.3.0}/LICENSE +0 -0
  19. {pixie_lab_dibs-0.2.1 → pixie_lab_dibs-0.3.0}/LICENSES/BSD-3-Clause.txt +0 -0
  20. {pixie_lab_dibs-0.2.1 → pixie_lab_dibs-0.3.0}/Makefile +0 -0
  21. {pixie_lab_dibs-0.2.1 → pixie_lab_dibs-0.3.0}/REUSE.toml +0 -0
  22. {pixie_lab_dibs-0.2.1 → pixie_lab_dibs-0.3.0}/pyproject.toml +0 -0
  23. {pixie_lab_dibs-0.2.1 → pixie_lab_dibs-0.3.0}/scripts/service-smoke.sh +0 -0
  24. {pixie_lab_dibs-0.2.1 → pixie_lab_dibs-0.3.0}/src/dibs/_skill.py +0 -0
  25. {pixie_lab_dibs-0.2.1 → pixie_lab_dibs-0.3.0}/src/dibs/_sources.py +0 -0
  26. {pixie_lab_dibs-0.2.1 → pixie_lab_dibs-0.3.0}/src/dibs/_sshsig.py +0 -0
  27. {pixie_lab_dibs-0.2.1 → pixie_lab_dibs-0.3.0}/src/dibs/_users.py +0 -0
  28. {pixie_lab_dibs-0.2.1 → pixie_lab_dibs-0.3.0}/tests/__init__.py +0 -0
  29. {pixie_lab_dibs-0.2.1 → pixie_lab_dibs-0.3.0}/tests/conftest.py +0 -0
  30. {pixie_lab_dibs-0.2.1 → pixie_lab_dibs-0.3.0}/tests/test_sources.py +0 -0
  31. {pixie_lab_dibs-0.2.1 → pixie_lab_dibs-0.3.0}/tests/test_users.py +0 -0
  32. {pixie_lab_dibs-0.2.1 → pixie_lab_dibs-0.3.0}/uv.lock +0 -0
@@ -10,6 +10,27 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+ ## [0.3.0] - 2026-10-27
14
+
15
+ ### Added
16
+
17
+ - Agent sessions: `DIBS_SESSION` tags the claims of one agent session,
18
+ with a random tag from `dibs session`,
19
+ and claims read `... assisted by 'Claude Code:<model>' (session <tag>) ...`. `release --mine` releases only that session's claims.
20
+ - `dibs whoami` shows who claims are made as, and fails when nobody has
21
+ logged in, so an agent can tell setup is missing.
22
+ - `/agents` and the skill now say who does what: a person sets up once
23
+ per workstation, and every agent there uses that login, never
24
+ registering itself.
25
+
26
+ ### Changed
27
+
28
+ - A claim belongs to its actor, the user with their agent and session,
29
+ rather than to the user alone. Two agents of one person, or a person
30
+ and their agent, now conflict when claiming the same thing, instead of
31
+ the second silently taking over. A person claiming by hand can still
32
+ release any of their agents' claims without `--force`.
33
+
13
34
  ## [0.2.1] - 2026-10-27
14
35
 
15
36
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pixie-lab-dibs
3
- Version: 0.2.1
3
+ Version: 0.3.0
4
4
  Summary: Call dibs on shared lab machines: see who is using what, claim it, release it
5
5
  Project-URL: Homepage, https://github.com/safl/pixie-lab-dibs
6
6
  Author-email: "Simon A. F. Lund" <os@safl.dk>
@@ -180,24 +180,31 @@ and it applies whenever the agent is about to use a lab machine:
180
180
  dibs skill install
181
181
  ```
182
182
 
183
- `dibs skill print` shows it, to adapt for another agent. An agent claims
184
- as its human, and names itself the way an `Assisted-by:` trailer does:
183
+ `dibs skill print` shows it, to adapt for another agent.
184
+
185
+ An agent never registers or logs in. It claims with its human's login,
186
+ names itself the way an `Assisted-by:` trailer does, and tags its
187
+ session with a tag from `dibs session`, so that two agents of one person
188
+ do not take or release each other's claims. The login is shared; the
189
+ tag is not, because each agent passes its own on its commands:
185
190
 
186
191
  ```
187
- export DIBS_ASSISTED_BY="Claude Code:claude-opus-5-5"
192
+ DIBS_ASSISTED_BY="Claude Code:claude-opus-5-5" DIBS_SESSION=s-3f2a9c41 dibs claim epyc-02 --for "ring test"
188
193
  ```
189
194
 
190
- The claim then reads `'alice' assisted by 'Claude Code:claude-opus-5-5' has claimed ...`.
191
-
192
- If you are an agent reading this: before you run anything on a lab
193
- machine, run `dibs list` and claim the machine with
194
- `dibs claim <machine> --for <purpose>`. Power-cycling, the console,
195
- reflashing or anything else through the BMC needs the whole machine,
196
- never just a part. Never use a machine someone else has claimed, and
197
- never pass `--force`; ask your human instead. Release what you claimed
198
- with `dibs release --mine` when the work is done, and also when it fails
199
- or is abandoned. That releases only what you claimed, not your human's
200
- own claims.
195
+ The claim then reads `'alice' assisted by 'Claude Code:claude-opus-5-5' (session s-3f2a9c41) has claimed ...`. Another session cannot claim
196
+ over it or release it without `--force`. The human, claiming by hand,
197
+ can release any of their agents' claims, but needs `--force` to claim
198
+ over one.
199
+
200
+ If you are an agent reading this: run `dibs whoami` first; if it fails,
201
+ stop and tell your human, never register yourself. Before you run
202
+ anything on a lab machine, run `dibs list` and claim the machine.
203
+ Power-cycling, the console, reflashing or anything else through the BMC
204
+ needs the whole machine, never just a part. Never use a machine someone
205
+ else has claimed, and never pass `--force`; ask your human instead.
206
+ Release with `dibs release --mine` when the work is done, and also when
207
+ it fails or is abandoned. That releases only your session's claims.
201
208
 
202
209
  ### Automation-runners
203
210
 
@@ -158,24 +158,31 @@ and it applies whenever the agent is about to use a lab machine:
158
158
  dibs skill install
159
159
  ```
160
160
 
161
- `dibs skill print` shows it, to adapt for another agent. An agent claims
162
- as its human, and names itself the way an `Assisted-by:` trailer does:
161
+ `dibs skill print` shows it, to adapt for another agent.
162
+
163
+ An agent never registers or logs in. It claims with its human's login,
164
+ names itself the way an `Assisted-by:` trailer does, and tags its
165
+ session with a tag from `dibs session`, so that two agents of one person
166
+ do not take or release each other's claims. The login is shared; the
167
+ tag is not, because each agent passes its own on its commands:
163
168
 
164
169
  ```
165
- export DIBS_ASSISTED_BY="Claude Code:claude-opus-5-5"
170
+ DIBS_ASSISTED_BY="Claude Code:claude-opus-5-5" DIBS_SESSION=s-3f2a9c41 dibs claim epyc-02 --for "ring test"
166
171
  ```
167
172
 
168
- The claim then reads `'alice' assisted by 'Claude Code:claude-opus-5-5' has claimed ...`.
169
-
170
- If you are an agent reading this: before you run anything on a lab
171
- machine, run `dibs list` and claim the machine with
172
- `dibs claim <machine> --for <purpose>`. Power-cycling, the console,
173
- reflashing or anything else through the BMC needs the whole machine,
174
- never just a part. Never use a machine someone else has claimed, and
175
- never pass `--force`; ask your human instead. Release what you claimed
176
- with `dibs release --mine` when the work is done, and also when it fails
177
- or is abandoned. That releases only what you claimed, not your human's
178
- own claims.
173
+ The claim then reads `'alice' assisted by 'Claude Code:claude-opus-5-5' (session s-3f2a9c41) has claimed ...`. Another session cannot claim
174
+ over it or release it without `--force`. The human, claiming by hand,
175
+ can release any of their agents' claims, but needs `--force` to claim
176
+ over one.
177
+
178
+ If you are an agent reading this: run `dibs whoami` first; if it fails,
179
+ stop and tell your human, never register yourself. Before you run
180
+ anything on a lab machine, run `dibs list` and claim the machine.
181
+ Power-cycling, the console, reflashing or anything else through the BMC
182
+ needs the whole machine, never just a part. Never use a machine someone
183
+ else has claimed, and never pass `--force`; ask your human instead.
184
+ Release with `dibs release --mine` when the work is done, and also when
185
+ it fails or is abandoned. That releases only your session's claims.
179
186
 
180
187
  ### Automation-runners
181
188
 
@@ -12,4 +12,4 @@ everyone who is on a machine, and it never stops anyone from logging
12
12
  in.
13
13
  """
14
14
 
15
- __version__ = "0.2.1"
15
+ __version__ = "0.3.0"
@@ -17,6 +17,13 @@ claim on a thing conflicts with another user's claim on any of its parts,
17
17
  and a claim on a part conflicts with another user's claim on that part
18
18
  or on the thing.
19
19
 
20
+ A claim is made by an **actor**: the user, and when an agent claims for
21
+ them, the agent (``assisted_by``) and its session. One actor cannot claim
22
+ over another's claim, even of the same user, so two agents of one person
23
+ do not collide unnoticed. Releasing is the same, except that the user
24
+ claiming by hand, without agent or session, may release any claim made
25
+ under their name: an agent answers to its human.
26
+
20
27
  Every surface (``dibs``, the web page, the login hook on a machine)
21
28
  prints a claim as the same sentence, from :func:`sentence`.
22
29
  """
@@ -47,6 +54,9 @@ THING_NAME_RE = re.compile(rf"^{_LABEL}(\.{_LABEL})?$")
47
54
  PURPOSE_MAX = 128
48
55
  ASSISTED_BY_MAX = 64
49
56
 
57
+ # Short, and plain, since it is typed into every command of a session.
58
+ SESSION_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,31}$")
59
+
50
60
  _SCHEMA = """
51
61
  CREATE TABLE IF NOT EXISTS things (
52
62
  name TEXT PRIMARY KEY,
@@ -61,7 +71,8 @@ CREATE TABLE IF NOT EXISTS claims (
61
71
  assisted_by TEXT NOT NULL DEFAULT '',
62
72
  purpose TEXT NOT NULL,
63
73
  claimed_at TEXT NOT NULL,
64
- expires_at TEXT NOT NULL DEFAULT ''
74
+ expires_at TEXT NOT NULL DEFAULT '',
75
+ session TEXT NOT NULL DEFAULT ''
65
76
  );
66
77
  """
67
78
 
@@ -117,6 +128,14 @@ def validate_assisted_by(raw: str) -> str:
117
128
  return _one_line("assisted_by", raw, ASSISTED_BY_MAX) if (raw or "").strip() else ""
118
129
 
119
130
 
131
+ def validate_session(raw: str) -> str:
132
+ """Optional, so blank passes."""
133
+ value = (raw or "").strip()
134
+ if value and not SESSION_RE.match(value):
135
+ raise ValueError(f"session {value!r} must be letters, digits, . _ or -, at most 32 chars")
136
+ return value
137
+
138
+
120
139
  @dataclass
121
140
  class Thing:
122
141
  name: str
@@ -139,6 +158,12 @@ class Claim:
139
158
  an ``Assisted-by:`` trailer; blank for a claim made by hand."""
140
159
  expires_at: str = ""
141
160
  """Blank for a claim that never expires."""
161
+ session: str = ""
162
+ """Which of the holder's agent sessions made it; blank by hand."""
163
+
164
+ @property
165
+ def actor(self) -> tuple[str, str, str]:
166
+ return (self.holder, self.assisted_by, self.session)
142
167
 
143
168
  def is_live(self, now: datetime) -> bool:
144
169
  """A claim past its expiry counts as no claim: the thing is free
@@ -153,13 +178,15 @@ class Claim:
153
178
  }
154
179
  if self.assisted_by:
155
180
  out["assisted_by"] = self.assisted_by
181
+ if self.session:
182
+ out["session"] = self.session
156
183
  if self.expires_at:
157
184
  out["expires_at"] = self.expires_at
158
185
  return out
159
186
 
160
187
 
161
188
  class ClaimConflict(ValueError):
162
- """One or more things are claimed by another user."""
189
+ """One or more things are claimed by another actor."""
163
190
 
164
191
  def __init__(self, blockers: Sequence[Claim], now: datetime) -> None:
165
192
  self.blockers = list(blockers)
@@ -188,6 +215,8 @@ def sentence(name: str, claim: Claim | None, now: datetime) -> str:
188
215
  who = f"'{claim.holder}'"
189
216
  if claim.assisted_by:
190
217
  who += f" assisted by '{claim.assisted_by}'"
218
+ if claim.session:
219
+ who += f" (session {claim.session})"
191
220
  return (
192
221
  f"{who} has claimed '{claim.thing}' for '{claim.purpose}' "
193
222
  f"expires in: '{remaining(claim.expires_at, now)}'"
@@ -222,6 +251,9 @@ class Store:
222
251
  self.db_path = Path(db_path)
223
252
  with self._conn() as conn:
224
253
  conn.executescript(_SCHEMA)
254
+ cols = {r[1] for r in conn.execute("PRAGMA table_info(claims)")}
255
+ if "session" not in cols: # a database from before sessions
256
+ conn.execute("ALTER TABLE claims ADD COLUMN session TEXT NOT NULL DEFAULT ''")
225
257
 
226
258
  @contextlib.contextmanager
227
259
  def _conn(self) -> Generator[sqlite3.Connection]:
@@ -283,23 +315,25 @@ class Store:
283
315
  holder: str,
284
316
  purpose: str,
285
317
  assisted_by: str = "",
318
+ session: str = "",
286
319
  expires_in: int | None = None,
287
320
  force: bool = False,
288
321
  now: datetime | None = None,
289
322
  ) -> builtins.list[tuple[Claim | None, Claim]]:
290
- """Claim every thing in ``names`` for ``holder``, or none of them:
291
- if any holds a live claim by another user (and ``force`` is off)
292
- this raises :class:`ClaimConflict` and changes nothing.
293
- Ownership is the user's: claiming again as the same holder, with
294
- or without an agent, updates the purpose, expiry and
295
- ``assisted_by`` and keeps ``claimed_at``. ``expires_in`` is
296
- seconds from now; ``None`` never expires. Returns ``(before,
297
- after)`` pairs so the caller can log what a ``force`` took over;
298
- a forced claim on a thing also takes over other users' claims on
299
- its parts, and those appear as pairs too. Callers check that the
300
- things exist."""
323
+ """Claim every thing in ``names`` for the actor ``holder`` /
324
+ ``assisted_by`` / ``session``, or none of them: if any holds a live
325
+ claim by another actor (and ``force`` is off) this raises
326
+ :class:`ClaimConflict` and changes nothing. Claiming again as the
327
+ same actor updates the purpose and expiry and keeps
328
+ ``claimed_at``. ``expires_in`` is seconds from now; ``None`` never
329
+ expires. Returns ``(before, after)`` pairs so the caller can log
330
+ what a ``force`` took over; a forced claim on a thing also takes
331
+ over other actors' claims on its parts, and those appear as pairs
332
+ too. Callers check that the things exist."""
301
333
  purpose = validate_purpose(purpose)
302
334
  assisted_by = validate_assisted_by(assisted_by)
335
+ session = validate_session(session)
336
+ me = (holder, assisted_by, session)
303
337
  if expires_in is not None and expires_in <= 0:
304
338
  raise ValueError("expires_in must be a positive number of seconds")
305
339
  at = now or datetime.now(UTC)
@@ -308,7 +342,7 @@ class Store:
308
342
  with _WRITE_LOCK, self._conn() as conn:
309
343
  before = {n: self._claim_row(conn, n) for n in unique}
310
344
  live = {n: c for n, c in before.items() if c is not None and c.is_live(at)}
311
- others = [c for c in self._live_rows(conn, at) if c.holder != holder]
345
+ others = [c for c in self._live_rows(conn, at) if c.actor != me]
312
346
  blockers = [c for c in others if any(overlaps(c.thing, n) for n in unique)]
313
347
  if blockers and not force:
314
348
  raise ClaimConflict(blockers, at)
@@ -316,13 +350,21 @@ class Store:
316
350
  afters: dict[str, Claim] = {}
317
351
  for n in unique:
318
352
  prior = live.get(n)
319
- since = prior.claimed_at if prior and prior.holder == holder else ts(at)
320
- after = afters[n] = Claim(n, holder, purpose, since, assisted_by, expires_at)
353
+ since = prior.claimed_at if prior and prior.actor == me else ts(at)
354
+ after = afters[n] = Claim(
355
+ thing=n,
356
+ holder=holder,
357
+ purpose=purpose,
358
+ claimed_at=since,
359
+ assisted_by=assisted_by,
360
+ expires_at=expires_at,
361
+ session=session,
362
+ )
321
363
  conn.execute(
322
364
  "INSERT OR REPLACE INTO claims "
323
- "(thing, holder, assisted_by, purpose, claimed_at, expires_at) "
324
- "VALUES (?, ?, ?, ?, ?, ?)",
325
- (n, holder, assisted_by, purpose, since, expires_at),
365
+ "(thing, holder, assisted_by, purpose, claimed_at, expires_at, session) "
366
+ "VALUES (?, ?, ?, ?, ?, ?, ?)",
367
+ (n, holder, assisted_by, purpose, since, expires_at, session),
326
368
  )
327
369
  out.append((prior, after))
328
370
  for b in blockers:
@@ -337,34 +379,48 @@ class Store:
337
379
  names: Sequence[str],
338
380
  *,
339
381
  holder: str,
382
+ assisted_by: str = "",
383
+ session: str = "",
340
384
  force: bool = False,
341
385
  now: datetime | None = None,
342
386
  ) -> builtins.list[Claim]:
343
387
  """Release the claims on ``names``, all or nothing like
344
- :meth:`claim`: releasing another user's live claim needs
345
- ``force``. Expired claims are cleared whoever holds them. Returns
346
- the live claims released."""
388
+ :meth:`claim`. Without ``force`` an actor may release its own
389
+ claims, and the user by hand (no agent, no session) may also
390
+ release its agents'. Expired claims are cleared whoever holds
391
+ them. Returns the live claims released."""
347
392
  at = now or datetime.now(UTC)
348
393
  unique = list(dict.fromkeys(names))
394
+ me = (holder, assisted_by.strip(), session.strip())
395
+ by_hand = not me[1] and not me[2]
396
+
397
+ def mine(c: Claim) -> bool:
398
+ return c.actor == me or (by_hand and c.holder == holder)
399
+
349
400
  with _WRITE_LOCK, self._conn() as conn:
350
401
  before = [c for c in (self._claim_row(conn, n) for n in unique) if c is not None]
351
- blockers = [c for c in before if c.is_live(at) and c.holder != holder]
402
+ blockers = [c for c in before if c.is_live(at) and not mine(c)]
352
403
  if blockers and not force:
353
404
  raise ClaimConflict(blockers, at)
354
405
  conn.executemany("DELETE FROM claims WHERE thing = ?", [(c.thing,) for c in before])
355
406
  return [c for c in before if c.is_live(at)]
356
407
 
357
408
  def claimed_by(
358
- self, holder: str, assisted_by: str = "", *, now: datetime | None = None
409
+ self,
410
+ holder: str,
411
+ assisted_by: str = "",
412
+ session: str = "",
413
+ *,
414
+ now: datetime | None = None,
359
415
  ) -> builtins.list[Claim]:
360
- """Live claims held by ``holder`` through exactly ``assisted_by``
361
- (blank: claimed by hand), so an agent's ``release --mine`` leaves
362
- its human's own claims alone."""
416
+ """Live claims of exactly this actor, so an agent's ``release
417
+ --mine`` leaves its human's and its other sessions' claims alone."""
363
418
  at = now or datetime.now(UTC)
364
419
  with self._conn() as conn:
365
420
  rows = conn.execute(
366
- "SELECT * FROM claims WHERE holder = ? AND assisted_by = ? ORDER BY thing",
367
- (holder, assisted_by.strip()),
421
+ "SELECT * FROM claims WHERE holder = ? AND assisted_by = ? AND session = ? "
422
+ "ORDER BY thing",
423
+ (holder, assisted_by.strip(), session.strip()),
368
424
  ).fetchall()
369
425
  return [c for c in (Claim(**dict(r)) for r in rows) if c.is_live(at)]
370
426
 
@@ -42,6 +42,7 @@ class ClaimBody(BaseModel):
42
42
  things: list[str] = Field(..., min_length=1)
43
43
  purpose: str
44
44
  assisted_by: str = Field(default="", description="Agent:model, as in an Assisted-by trailer")
45
+ session: str = Field(default="", description="Which agent session of the user is claiming")
45
46
  expires_in: int | None = Field(default=None, description="Seconds; null never expires")
46
47
  force: bool = False
47
48
 
@@ -49,7 +50,10 @@ class ClaimBody(BaseModel):
49
50
  class ReleaseBody(BaseModel):
50
51
  things: list[str] = Field(default_factory=list)
51
52
  assisted_by: str = ""
52
- mine: bool = Field(default=False, description="Every live claim you hold through assisted_by")
53
+ session: str = ""
54
+ mine: bool = Field(
55
+ default=False, description="Every live claim of this user, assisted_by and session"
56
+ )
53
57
  force: bool = False
54
58
 
55
59
 
@@ -77,7 +81,11 @@ def current_user(request: Request) -> str:
77
81
  """Dependency: the user a bearer token belongs to, else 401."""
78
82
  user = request.app.state.users.user_for_token(_bearer(request))
79
83
  if user is None:
80
- raise HTTPException(status_code=401, detail="not logged in; run: dibs login <user>")
84
+ raise HTTPException(
85
+ status_code=401,
86
+ detail="not logged in; a person sets up with dibs register and dibs login "
87
+ "(agents use their person's login)",
88
+ )
81
89
  return str(user)
82
90
 
83
91
 
@@ -207,6 +215,7 @@ def create_app(
207
215
  holder=user,
208
216
  purpose=body.purpose,
209
217
  assisted_by=body.assisted_by,
218
+ session=body.session,
210
219
  expires_in=body.expires_in,
211
220
  force=body.force,
212
221
  now=now,
@@ -230,11 +239,20 @@ def create_app(
230
239
  if body.mine == bool(body.things):
231
240
  raise HTTPException(status_code=422, detail="name things to release, or set mine")
232
241
  if body.mine:
233
- names = [c.thing for c in store.claimed_by(user, body.assisted_by, now=now)]
242
+ names = [
243
+ c.thing for c in store.claimed_by(user, body.assisted_by, body.session, now=now)
244
+ ]
234
245
  else:
235
246
  names = [n.strip().lower() for n in body.things]
236
247
  try:
237
- released = store.release(names, holder=user, force=body.force, now=now)
248
+ released = store.release(
249
+ names,
250
+ holder=user,
251
+ assisted_by=body.assisted_by,
252
+ session=body.session,
253
+ force=body.force,
254
+ now=now,
255
+ )
238
256
  except ClaimConflict as exc:
239
257
  raise _conflict(exc) from exc
240
258
  things = {t.name: t for t in store.list_things()}
@@ -352,24 +370,47 @@ AGENTS_PAGE = """\
352
370
  # dibs on this lab
353
371
 
354
372
  The machines on this lab are shared by people, ai-agents and
355
- automation-runners, and claimed with dibs at {base}. Read this before you
356
- touch any of them.
373
+ automation-runners. Each is claimed with dibs, at {base}, before anyone
374
+ touches it. Read the part for you.
357
375
 
358
- ## Set up, once per machine you work from
376
+ ## If you are a person: set up once per workstation
359
377
 
360
378
  ```
361
379
  uv tool install pixie-lab-dibs
362
- export DIBS_URL={base}
363
- dibs register <your name> # then ask the lab's admin to approve you
380
+ echo 'export DIBS_URL={base}' >> ~/.bashrc
381
+ dibs register <name> # then ask the lab's admin to approve you
364
382
  dibs login
383
+ dibs skill install # Claude Code agents then follow the rules below by themselves
365
384
  ```
366
385
 
367
- An agent uses its human's login and needs none of its own. For Claude
368
- Code, `dibs skill install` makes the rules below load by themselves.
386
+ Every agent you run on this workstation uses this login. Agents never
387
+ register or log in themselves.
388
+
389
+ ## If you are an agent: before anything else
390
+
391
+ 1. Check that your human has set up this workstation:
392
+
393
+ ```
394
+ DIBS_URL={base} dibs whoami
395
+ ```
396
+
397
+ It prints your human's name. If it fails, stop, and give your human the
398
+ setup above. Do not register or log in yourself.
399
+
400
+ 1. Get a session tag for this conversation, once, with `dibs session`, and
401
+ pass it, with who you are, on every dibs command:
402
+
403
+ ```
404
+ DIBS_URL={base} DIBS_ASSISTED_BY="Claude Code:<model>" DIBS_SESSION=<tag> dibs ...
405
+ ```
406
+
407
+ Your claims then say which agent made them, and no other agent of the
408
+ same person can take or release them without `--force`.
369
409
 
370
410
  ## Right now
371
411
 
372
412
  {now}
413
+
373
414
  """
374
415
 
375
416
  PAGE = """<!doctype html>
@@ -14,7 +14,9 @@ and ``dibs list``, ``info``, ``claim`` and ``release`` use it. The client
14
14
  finds the server with ``--url`` or ``DIBS_URL``, keeps its token per URL
15
15
  in ``$XDG_CONFIG_HOME/dibs/client.json`` (``DIBS_TOKEN`` overrides it, for
16
16
  runners), and an agent sets ``DIBS_ASSISTED_BY`` (``Agent:model``, as in
17
- an ``Assisted-by:`` trailer) so its claims say so.
17
+ an ``Assisted-by:`` trailer) and ``DIBS_SESSION`` (a tag for this agent
18
+ session) so its claims say which agent made them and other agents of the
19
+ same person leave them alone.
18
20
 
19
21
  Exit status: 0 done, 1 refused because a thing is claimed by someone
20
22
  else, 2 anything else (usage, unknown thing, server unreachable).
@@ -137,6 +139,10 @@ def assisted_by() -> str:
137
139
  return os.environ.get("DIBS_ASSISTED_BY", "").strip()
138
140
 
139
141
 
142
+ def session() -> str:
143
+ return os.environ.get("DIBS_SESSION", "").strip()
144
+
145
+
140
146
  def default_key() -> Path:
141
147
  for name in _DEFAULT_KEYS:
142
148
  path = Path.home() / ".ssh" / name
@@ -312,6 +318,7 @@ def cmd_claim(args: argparse.Namespace) -> None:
312
318
  "things": args.things,
313
319
  "purpose": args.purpose,
314
320
  "assisted_by": assisted_by(),
321
+ "session": session(),
315
322
  "expires_in": args.expires,
316
323
  "force": args.force,
317
324
  }
@@ -325,6 +332,7 @@ def cmd_release(args: argparse.Namespace) -> None:
325
332
  body = {
326
333
  "things": args.things,
327
334
  "assisted_by": assisted_by(),
335
+ "session": session(),
328
336
  "mine": args.mine,
329
337
  "force": args.force,
330
338
  }
@@ -363,6 +371,37 @@ def cmd_login(args: argparse.Namespace) -> None:
363
371
  print(f"Logged in as '{answer['user']}'.")
364
372
 
365
373
 
374
+ def cmd_whoami(args: argparse.Namespace) -> None:
375
+ """Who this workstation's dibs claims as: the user from the login,
376
+ and the agent and session from the environment. Fails when nobody
377
+ has logged in, which is how an agent learns setup is missing."""
378
+ url = _url(args)
379
+ me: dict[str, str] = {"user": _request(url, "GET", "/auth/whoami")["user"]}
380
+ if assisted_by():
381
+ me["assisted_by"] = assisted_by()
382
+ if session():
383
+ me["session"] = session()
384
+ if args.format:
385
+ sys.stdout.write(dump(me, args.format))
386
+ return
387
+ line = f"'{me['user']}'"
388
+ if "assisted_by" in me:
389
+ line += f" assisted by '{me['assisted_by']}'"
390
+ if "session" in me:
391
+ line += f" (session {me['session']})"
392
+ print(line)
393
+
394
+
395
+ def cmd_session(args: argparse.Namespace) -> None:
396
+ """A fresh session tag for an agent to pass as ``DIBS_SESSION`` for
397
+ the rest of its conversation. Random, so two agents of the same
398
+ person never pick the same one; nothing is stored, here or on the
399
+ server, until a claim carries it."""
400
+ import secrets
401
+
402
+ print(f"s-{secrets.token_hex(4)}")
403
+
404
+
366
405
  def cmd_logout(args: argparse.Namespace) -> None:
367
406
  url = _url(args)
368
407
  if token(url):
@@ -639,7 +678,8 @@ def build_parser() -> argparse.ArgumentParser:
639
678
  epilog=(
640
679
  "The client finds the server via --url or DIBS_URL. Claiming needs a login: "
641
680
  "register, have the admin approve you, then login. Agents set "
642
- "DIBS_ASSISTED_BY; runners may pass a token in DIBS_TOKEN. Exit status: 0 done, "
681
+ "DIBS_ASSISTED_BY and DIBS_SESSION; runners may pass a token in DIBS_TOKEN. "
682
+ "Exit status: 0 done, "
643
683
  "1 claimed by someone else, 2 other errors."
644
684
  ),
645
685
  )
@@ -696,6 +736,12 @@ def build_parser() -> argparse.ArgumentParser:
696
736
  p.add_argument("-i", "--key", help="SSH public key file (default: the one you registered with)")
697
737
  p.set_defaults(func=cmd_login)
698
738
 
739
+ p = sub.add_parser("whoami", parents=[data], help="show who your claims are made as")
740
+ p.set_defaults(func=cmd_whoami)
741
+
742
+ p = sub.add_parser("session", help="print a fresh session tag for an agent")
743
+ p.set_defaults(func=cmd_session)
744
+
699
745
  p = sub.add_parser("logout", help="drop your token for this server")
700
746
  p.set_defaults(func=cmd_logout)
701
747
 
@@ -0,0 +1,86 @@
1
+ ---
2
+ name: dibs
3
+ description: Claim a shared lab machine with dibs before using it, and release it after. Use before you SSH into, run tests on, flash, reboot, power-cycle, or open the BMC or console of any lab machine (e.g. epyc-01, epyc-02, ryzen-01), including through cijoe, ipmitool or Redfish.
4
+ ---
5
+
6
+ <!--
7
+ SPDX-FileCopyrightText: Simon A. F. Lund <os@safl.dk>
8
+
9
+ SPDX-License-Identifier: BSD-3-Clause
10
+ -->
11
+
12
+ # Claim lab machines with dibs
13
+
14
+ Lab machines are shared with people, other agents and CI. dibs says who
15
+ is on which. A claim is a note, not a lock: nothing stops you from
16
+ logging in to a machine someone else holds, so it is on you not to.
17
+
18
+ ## Once per conversation
19
+
20
+ 1. Check that your human has set up this workstation:
21
+
22
+ ```
23
+ dibs whoami
24
+ ```
25
+
26
+ It prints your human's name. If it fails, or `dibs` or `DIBS_URL` is
27
+ missing, stop and tell your human to read `$DIBS_URL/agents`, or the
28
+ dibs README. Never run `dibs register` or `dibs login` yourself: you
29
+ claim with your human's login.
30
+
31
+ 1. Get a session tag for this conversation, once, with `dibs session`.
32
+ It prints a random tag such as `s-3f2a9c41`. Do not make one up: two
33
+ agents could pick the same. Pass it, and who you are, on every dibs
34
+ command, inline, so every call agrees:
35
+
36
+ ```
37
+ DIBS_ASSISTED_BY="Claude Code:<your model id>" DIBS_SESSION=<tag> dibs ...
38
+ ```
39
+
40
+ Your claims then say which agent made them, and another agent of the
41
+ same person cannot take or release them without `--force`; nor can you
42
+ theirs.
43
+
44
+ ## Before you touch a machine
45
+
46
+ 1. Look: `dibs list --json`, and read the JSON rather than the text.
47
+
48
+ 1. Claim it, saying what for:
49
+
50
+ ```
51
+ DIBS_ASSISTED_BY=... DIBS_SESSION=... dibs claim <machine> --for "<what you are doing>"
52
+ ```
53
+
54
+ 1. Exit status 1 means someone else holds it, maybe another agent of
55
+ your human's. Do not use the machine and do not pass `--force`. Tell
56
+ your human who holds it (the error says) and ask what to do.
57
+
58
+ Anything that goes through the BMC or affects the whole box needs the
59
+ whole machine: power-cycling, the serial or graphical console, booting
60
+ or reflashing it, firmware, and kernel or driver changes. Claim
61
+ `<machine>` for those.
62
+
63
+ Some machines list parts in `dibs list`, named `<machine>.<part>`. Claim
64
+ a part only when your work stays inside it and cannot disturb the rest
65
+ of the machine; claiming the machine takes all of its parts. Claim
66
+ several things in one command; it is all or nothing.
67
+
68
+ ## When you are done
69
+
70
+ Release what you claimed, and also when the work fails, is abandoned, or
71
+ waits on your human for long:
72
+
73
+ ```
74
+ DIBS_ASSISTED_BY=... DIBS_SESSION=... dibs release --mine
75
+ ```
76
+
77
+ This releases only this session's claims: not your human's, and not
78
+ other agents'.
79
+
80
+ ## Never
81
+
82
+ - Use a machine claimed by someone else, or one you have not claimed.
83
+ - Pass `--force`, or release a claim this session did not make.
84
+ - Register or log in to dibs yourself.
85
+ - Power-cycle, reflash, reboot or use the BMC of a machine when you hold
86
+ only a part of it.
@@ -67,11 +67,15 @@ def test_conflict_unknown_and_invalid(client: TestClient, tokens) -> None:
67
67
  assert _claim(client, tokens, "ryzen-01", purpose="").status_code == 422
68
68
 
69
69
 
70
- def test_release_mine_is_scoped_to_assisted_by(client: TestClient, tokens) -> None:
70
+ def test_release_mine_is_scoped_to_the_session(client: TestClient, tokens) -> None:
71
71
  _claim(client, tokens, "epyc-02")
72
- r = _claim(client, tokens, "warp", assisted_by=AGENT)
73
- assert r.json()["things"][0]["sentence"].startswith(f"'alice' assisted by '{AGENT}'")
74
- released = _release(client, tokens, mine=True, assisted_by=AGENT).json()["things"]
72
+ r = _claim(client, tokens, "warp", assisted_by=AGENT, session="s1")
73
+ assert r.json()["things"][0]["sentence"].startswith(
74
+ f"'alice' assisted by '{AGENT}' (session s1)"
75
+ )
76
+ assert r.json()["things"][0]["claim"]["session"] == "s1"
77
+ _claim(client, tokens, "ryzen-01", assisted_by=AGENT, session="s2")
78
+ released = _release(client, tokens, mine=True, assisted_by=AGENT, session="s1").json()["things"]
75
79
  assert [t["name"] for t in released] == ["warp"]
76
80
  assert client.get("/things/epyc-02").json()["claim"]["holder"] == "alice"
77
81
  assert _release(client, tokens).status_code == 422
@@ -185,7 +189,10 @@ def test_agents_page_is_set_up_for_this_server(client: TestClient, tokens) -> No
185
189
  r = client.get("/agents")
186
190
  assert r.headers["content-type"].startswith("text/plain")
187
191
  page = r.text
188
- assert "export DIBS_URL=http://testserver\n" in page
192
+ assert "echo 'export DIBS_URL=http://testserver' >> ~/.bashrc" in page
193
+ assert "DIBS_URL=http://testserver dibs whoami" in page
194
+ assert "If you are a person" in page and "If you are an agent" in page
195
+ assert "Agents never\nregister" in page
189
196
  assert "- 'alice' has claimed 'warp' for 'xnvme' expires in: 'never'" in page
190
197
  assert page.endswith(skill_body())
191
198
  # The skill's Claude Code frontmatter and licence comment are not for this reader.
@@ -293,3 +293,47 @@ def test_skill_body_drops_only_the_preamble() -> None:
293
293
  body = skill_body()
294
294
  assert body.startswith("# Claim lab machines with dibs\n")
295
295
  assert skill_text().endswith(body)
296
+
297
+
298
+ def test_whoami_shows_the_actor(run: Run, monkeypatch: pytest.MonkeyPatch, capsys) -> None:
299
+ assert run("whoami") == 0
300
+ assert capsys.readouterr().out == "'alice'\n"
301
+ monkeypatch.setenv("DIBS_ASSISTED_BY", "Claude Code:claude-opus-5-5")
302
+ monkeypatch.setenv("DIBS_SESSION", "s1")
303
+ assert run("whoami") == 0
304
+ assert (
305
+ capsys.readouterr().out
306
+ == "'alice' assisted by 'Claude Code:claude-opus-5-5' (session s1)\n"
307
+ )
308
+ assert run("whoami", "--json") == 0
309
+ assert json.loads(capsys.readouterr().out) == {
310
+ "user": "alice",
311
+ "assisted_by": "Claude Code:claude-opus-5-5",
312
+ "session": "s1",
313
+ }
314
+ # With nobody logged in it fails, which is how an agent finds out.
315
+ assert run("whoami", who=None) == cli.EXIT_ERROR
316
+ assert "agents use their person's login" in capsys.readouterr().err
317
+
318
+
319
+ def test_claims_carry_the_session(run: Run, monkeypatch: pytest.MonkeyPatch, capsys) -> None:
320
+ monkeypatch.setenv("DIBS_ASSISTED_BY", "Claude Code:claude-opus-5-5")
321
+ monkeypatch.setenv("DIBS_SESSION", "s1")
322
+ assert run("claim", "warp", "--for", "x") == 0
323
+ assert "(session s1) has claimed 'warp'" in capsys.readouterr().out
324
+ monkeypatch.setenv("DIBS_SESSION", "s2")
325
+ assert run("claim", "warp", "--for", "y") == cli.EXIT_REFUSED
326
+ assert run("release", "--mine") == 0
327
+ assert capsys.readouterr().out.endswith("nothing to release\n")
328
+
329
+
330
+ def test_session_tags_are_fresh_and_valid(capsys) -> None:
331
+ from dibs._store import validate_session
332
+
333
+ tags = set()
334
+ for _ in range(20):
335
+ assert cli.main(["session"]) == 0
336
+ tag = capsys.readouterr().out.strip()
337
+ assert validate_session(tag) == tag
338
+ tags.add(tag)
339
+ assert len(tags) == 20
@@ -29,8 +29,12 @@ def test_sentence_free_claimed_and_assisted(store: Store) -> None:
29
29
  assert sentence("node-01", c, T0) == (
30
30
  "'alice' has claimed 'node-01' for 'xnvme' expires in: 'never'"
31
31
  )
32
- [(_, c)] = store.claim(["node-01"], holder="alice", purpose="x", assisted_by=AGENT, now=T0)
33
- assert sentence("node-01", c, T0).startswith(f"'alice' assisted by '{AGENT}' has claimed")
32
+ [(_, c)] = store.claim(
33
+ ["node-02"], holder="alice", purpose="x", assisted_by=AGENT, session="s1", now=T0
34
+ )
35
+ assert sentence("node-02", c, T0).startswith(
36
+ f"'alice' assisted by '{AGENT}' (session s1) has claimed 'node-02'"
37
+ )
34
38
 
35
39
 
36
40
  @pytest.mark.parametrize(
@@ -66,11 +70,61 @@ def test_claim_is_all_or_nothing(store: Store) -> None:
66
70
  assert store.get_claim("a") is None
67
71
 
68
72
 
69
- def test_an_agent_and_its_human_do_not_block_each_other(store: Store) -> None:
73
+ def test_two_agent_sessions_of_one_user_conflict(store: Store) -> None:
74
+ store.claim(["a"], holder="alice", purpose="x", assisted_by=AGENT, session="s1", now=T0)
75
+ with pytest.raises(ClaimConflict) as exc:
76
+ store.claim(["a"], holder="alice", purpose="y", assisted_by=AGENT, session="s2", now=T0)
77
+ assert "(session s1)" in exc.value.sentences[0]
78
+ with pytest.raises(ClaimConflict):
79
+ store.release(["a"], holder="alice", assisted_by=AGENT, session="s2", now=T0)
80
+ # The same session claiming again just updates its claim.
81
+ store.claim(["a"], holder="alice", purpose="z", assisted_by=AGENT, session="s1", now=T0)
82
+ assert store.get_claim("a").purpose == "z" # type: ignore[union-attr]
83
+
84
+
85
+ def test_an_agent_cannot_take_or_release_its_humans_claim(store: Store) -> None:
70
86
  store.claim(["a"], holder="alice", purpose="x", now=T0)
71
- store.claim(["a"], holder="alice", purpose="y", assisted_by=AGENT, now=T0)
72
- store.release(["a"], holder="alice", now=T0)
73
- assert store.get_claim("a") is None
87
+ with pytest.raises(ClaimConflict):
88
+ store.claim(["a"], holder="alice", purpose="y", assisted_by=AGENT, session="s1", now=T0)
89
+ with pytest.raises(ClaimConflict):
90
+ store.release(["a"], holder="alice", assisted_by=AGENT, session="s1", now=T0)
91
+
92
+
93
+ def test_the_human_may_release_their_agents_claims(store: Store) -> None:
94
+ store.claim(["a"], holder="alice", purpose="x", assisted_by=AGENT, session="s1", now=T0)
95
+ # Claiming over it still needs force, so nothing is overwritten unnoticed...
96
+ with pytest.raises(ClaimConflict):
97
+ store.claim(["a"], holder="alice", purpose="y", now=T0)
98
+ # ...but cleaning up after one's own agent does not.
99
+ assert [c.thing for c in store.release(["a"], holder="alice", now=T0)] == ["a"]
100
+
101
+
102
+ def test_another_users_human_may_not_release_my_agents_claim(store: Store) -> None:
103
+ store.claim(["a"], holder="alice", purpose="x", assisted_by=AGENT, session="s1", now=T0)
104
+ with pytest.raises(ClaimConflict):
105
+ store.release(["a"], holder="bob", now=T0)
106
+
107
+
108
+ def test_old_database_gains_sessions(tmp_path: Path) -> None:
109
+ import sqlite3
110
+
111
+ db = tmp_path / "old.db"
112
+ conn = sqlite3.connect(str(db))
113
+ conn.executescript(
114
+ "CREATE TABLE claims (thing TEXT PRIMARY KEY, holder TEXT NOT NULL, "
115
+ "assisted_by TEXT NOT NULL DEFAULT '', purpose TEXT NOT NULL, claimed_at TEXT NOT NULL, "
116
+ "expires_at TEXT NOT NULL DEFAULT '');"
117
+ "INSERT INTO claims (thing, holder, purpose, claimed_at) VALUES ('a', 'alice', 'x', 'y');"
118
+ )
119
+ conn.commit()
120
+ conn.close()
121
+ assert Store(db).get_claim("a").session == "" # type: ignore[union-attr]
122
+
123
+
124
+ @pytest.mark.parametrize("raw", ["-x", "a b", "x" * 33, "a/b"])
125
+ def test_bad_sessions_are_refused(store: Store, raw: str) -> None:
126
+ with pytest.raises(ValueError):
127
+ store.claim(["a"], holder="alice", purpose="x", session=raw, now=T0)
74
128
 
75
129
 
76
130
  def test_force_takeover_reports_previous_holder(store: Store) -> None:
@@ -99,12 +153,13 @@ def test_release_rules(store: Store) -> None:
99
153
  assert store.get_claim("b") is None
100
154
 
101
155
 
102
- def test_claimed_by_is_scoped_to_assisted_by(store: Store) -> None:
156
+ def test_claimed_by_is_scoped_to_the_actor(store: Store) -> None:
103
157
  store.claim(["a"], holder="alice", purpose="x", now=T0)
104
- store.claim(["b"], holder="alice", purpose="y", assisted_by=AGENT, now=T0)
158
+ store.claim(["b"], holder="alice", purpose="y", assisted_by=AGENT, session="s1", now=T0)
159
+ store.claim(["d"], holder="alice", purpose="y", assisted_by=AGENT, session="s2", now=T0)
105
160
  store.claim(["c"], holder="alice", purpose="z", expires_in=60, now=T0 - timedelta(hours=1))
106
161
  assert [c.thing for c in store.claimed_by("alice", now=T0)] == ["a"]
107
- assert [c.thing for c in store.claimed_by("alice", AGENT, now=T0)] == ["b"]
162
+ assert [c.thing for c in store.claimed_by("alice", AGENT, "s1", now=T0)] == ["b"]
108
163
 
109
164
 
110
165
  @pytest.mark.parametrize(
@@ -1,65 +0,0 @@
1
- ---
2
- name: dibs
3
- description: Claim a shared lab machine with dibs before using it, and release it after. Use before you SSH into, run tests on, flash, reboot, power-cycle, or open the BMC or console of any lab machine (e.g. epyc-01, epyc-02, ryzen-01), including through cijoe, ipmitool or Redfish.
4
- ---
5
-
6
- <!--
7
- SPDX-FileCopyrightText: Simon A. F. Lund <os@safl.dk>
8
-
9
- SPDX-License-Identifier: BSD-3-Clause
10
- -->
11
-
12
- # Claim lab machines with dibs
13
-
14
- Lab machines are shared with people, other agents and CI. dibs says who
15
- is on which. A claim is a note, not a lock: nothing stops you from
16
- logging in to a machine someone else holds, so it is on you not to.
17
-
18
- ## Before you touch a machine
19
-
20
- 1. Look: `dibs list`. Use `dibs list --json` to read it, not the text.
21
-
22
- 1. Claim it, naming yourself as an agent and saying what for:
23
-
24
- ```
25
- DIBS_ASSISTED_BY="Claude Code:<your model id>" dibs claim <machine> --for "<what you are doing>"
26
- ```
27
-
28
- Set `DIBS_ASSISTED_BY` on every dibs command, inline, so each one
29
- agrees on who you are. It uses your human's login; you need no login
30
- of your own.
31
-
32
- 1. Exit status 1 means someone else holds it. Do not use the machine and
33
- do not pass `--force`. Tell your human who holds it (the error says)
34
- and ask what to do.
35
-
36
- Anything that goes through the BMC or affects the whole box needs the
37
- whole machine: power-cycling, the serial or graphical console, booting
38
- or reflashing it, firmware, and kernel or driver changes. Claim
39
- `<machine>` for those.
40
-
41
- Some machines list parts in `dibs list`, named `<machine>.<part>`. Claim
42
- a part only when your work stays inside it and cannot disturb the rest
43
- of the machine; claiming the machine takes all of its parts. Claim
44
- several things in one command; it is all or nothing.
45
-
46
- ## When you are done
47
-
48
- Release what you claimed, and also when the work fails, is abandoned, or
49
- waits on your human for long:
50
-
51
- ```
52
- DIBS_ASSISTED_BY="Claude Code:<your model id>" dibs release --mine
53
- ```
54
-
55
- This releases only what you claimed, not your human's own claims.
56
-
57
- ## Never
58
-
59
- - Use a machine claimed by someone else, or one you have not claimed.
60
- - Pass `--force`, or release a claim you did not make.
61
- - Power-cycle, reflash, reboot or use the BMC of a machine when you hold
62
- only a part of it.
63
-
64
- If `dibs` is not installed or `DIBS_URL` is not set, stop and tell your
65
- human rather than going ahead without a claim.
File without changes
File without changes
File without changes