devlyn-cli 4.3.25 → 4.3.28

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 (41) hide show
  1. package/AGENTS.md +1 -1
  2. package/CLAUDE.md +1 -1
  3. package/README.md +27 -27
  4. package/bin/devlyn.js +42 -94
  5. package/bin/harness-activation.js +4 -8
  6. package/bin/harness-context.js +395 -0
  7. package/bin/harness-run.js +148 -0
  8. package/bin/instruction-templates.json +2 -0
  9. package/bin/instructions.js +71 -159
  10. package/bin/skill-history.json +8 -0
  11. package/config/skills/_shared/runtime-principles.md +1 -1
  12. package/config/skills/_shared/storage-checkout.py +1 -0
  13. package/config/skills/_shared/storage-client.py +349 -14
  14. package/config/skills/_shared/task-complete.py +99 -24
  15. package/config/skills/_shared/task-completion.md +7 -3
  16. package/config/skills/devlyn-ideate/SKILL.md +2 -2
  17. package/config/skills/devlyn-ideate/references/loop.md +2 -2
  18. package/config/skills/devlyn-ideate/scripts/queue.py +10 -3
  19. package/harness/AGENTS.md +2 -2
  20. package/harness/CLAUDE.md +2 -2
  21. package/native/claude/.claude-plugin/plugin.json +1 -1
  22. package/native/claude/skills/_shared/runtime-principles.md +1 -1
  23. package/native/claude/skills/_shared/storage-checkout.py +1 -0
  24. package/native/claude/skills/_shared/storage-client.py +349 -14
  25. package/native/claude/skills/_shared/task-complete.py +99 -24
  26. package/native/claude/skills/_shared/task-completion.md +7 -3
  27. package/native/claude/skills/devlyn-ideate/SKILL.md +2 -2
  28. package/native/claude/skills/devlyn-ideate/references/loop.md +2 -2
  29. package/native/claude/skills/devlyn-ideate/scripts/queue.py +10 -3
  30. package/native/codex/.agents/plugins/marketplace.json +1 -1
  31. package/native/codex/plugins/devlyn/.codex-plugin/plugin.json +1 -1
  32. package/native/codex/plugins/devlyn/skills/_shared/runtime-principles.md +1 -1
  33. package/native/codex/plugins/devlyn/skills/_shared/storage-checkout.py +1 -0
  34. package/native/codex/plugins/devlyn/skills/_shared/storage-client.py +349 -14
  35. package/native/codex/plugins/devlyn/skills/_shared/task-complete.py +99 -24
  36. package/native/codex/plugins/devlyn/skills/_shared/task-completion.md +7 -3
  37. package/native/codex/plugins/devlyn/skills/devlyn-ideate/SKILL.md +2 -2
  38. package/native/codex/plugins/devlyn/skills/devlyn-ideate/references/loop.md +2 -2
  39. package/native/codex/plugins/devlyn/skills/devlyn-ideate/scripts/queue.py +10 -3
  40. package/package.json +2 -2
  41. package/release.json +51 -41
@@ -316,15 +316,21 @@ def ignore_task_records(work):
316
316
  f"{directory} records are not ignored or .devlyn is dirty; repair the starting commit's .devlyn paths and ignore rules before allocating")
317
317
 
318
318
 
319
- def storage_call(name, *args, adapter=False, receipt_path=None, **kwargs):
319
+ def storage_call(name, *args, adapter=False, receipt_path=None, resume=None, **kwargs):
320
320
  module = shared("storage-checkout" if adapter else "storage-client")
321
321
  try:
322
322
  return module[name](*args, **kwargs)
323
323
  except module["StorageRefusal"] as exc:
324
324
  request = args[2] if adapter and len(args) == 3 else kwargs.get("receipt", {}).get("storage", {}).get("request", {})
325
325
  path = receipt_path or request.get("external_receipt")
326
- resume = exc.resume or (storage_resume(path, for_removal=name in ('enrollment', 'recovery_registration')) if path else None)
327
- error = CompletionError(str(exc) + ('; resume ' + resume if resume else '; repair Devlyn enrollment and retry'))
326
+ resume = exc.resume or resume or (storage_resume(path, for_removal=name in ('enrollment', 'recovery_registration')) if path else None)
327
+ message = str(exc)
328
+ if resume:
329
+ if resume not in message:
330
+ message += '; resume ' + resume
331
+ else:
332
+ message += '; repair Devlyn enrollment and retry'
333
+ error = CompletionError(message)
328
334
  error.storage_error = exc.result(receipt=path, resume=resume)
329
335
  raise error from exc
330
336
 
@@ -339,10 +345,15 @@ def managed_registration(receipt, *, recovery=False):
339
345
  require('storage' in receipt, 'managed receipt binding missing; retain and repair enrollment')
340
346
  saved = receipt['storage']['decision']['registration']
341
347
  if recovery:
348
+ storage_call('retirement_refusal')
342
349
  current = storage_call('recovery_registration', receipt_path=receipt['storage'].get('request', {}).get('external_receipt'))
350
+ if authority_binding(current) != authority_binding(saved):
351
+ refuse_moved(receipt)
343
352
  require(authority_binding(current) == authority_binding(saved),
344
353
  'recovery pins authority binding changed; retain and repair enrollment')
345
354
  return {**current, 'helper_path': current['retained_helper']['path']}
355
+ if decision['mode'] != 'managed' or authority_binding(decision['registration']) != authority_binding(saved):
356
+ refuse_moved(receipt)
346
357
  require(decision['mode'] == 'managed', 'managed enrollment missing')
347
358
  require(authority_binding(decision['registration']) == authority_binding(saved),
348
359
  'enrollment authority binding changed; retain original recovery binding')
@@ -487,11 +498,61 @@ def storage_rpc(registration, request, path=None):
487
498
  raise error from exc
488
499
 
489
500
 
501
+ def refuse_moved(receipt, path=None):
502
+ if receipt['storage'].get('moved') or storage_call('moved_operation', receipt['storage']['decision']['registration']):
503
+ path = Path(receipt['storage']['request']['external_receipt']) if path is None else path
504
+ resume = receipt_resume('recover-storage', path, receipt)
505
+ error = CompletionError('This task moved with Devlyn; release it and keep its files: ' + resume)
506
+ error.storage_error = {'receipt': str(path), 'resume': resume}
507
+ raise error
508
+
509
+
510
+ def release_moved(receipt, path, operation):
511
+ state = receipt['storage']
512
+ if 'moved' not in state:
513
+ registration = storage_call('recovery_registration', receipt_path=str(path),
514
+ resume=receipt_resume('recover-storage', path, receipt))
515
+ registration = {**registration, 'helper_path': registration['retained_helper']['path']}
516
+ storage_call('replay_retirement', registration, receipt_path=str(path))
517
+ result = None
518
+ if path_present(path.with_name('storage-allocation.json')):
519
+ if 'removal_request' not in state:
520
+ work = Path(receipt['worktree'])
521
+ state['removal_request'] = {'adapter_protocol': 1,
522
+ 'allocation_operation_id': state['request']['operation_id'], 'external_receipt': str(path),
523
+ 'removal_operation_id': storage_call('operation_id'),
524
+ 'worktree_identity': storage_call('persistent_identity', work) if path_present(work) else None}
525
+ atomic_json(path, receipt)
526
+ result = storage_call('call', registration, 'reconcile-for-removal', state['removal_request'], adapter=True)
527
+ require(result['operation_id'] == state['removal_request']['removal_operation_id'], 'removal fence ID changed')
528
+ require(result['closure_result']['established_by'] != 'legacy_unreserved', 'moved release lacks positive closure')
529
+ state['moved'] = {'operation_id': operation, 'removal_result': result}
530
+ atomic_json(path, receipt)
531
+ root = Path(receipt['common_gitdir']) / 'devlyn-completion'
532
+ moved = root / 'moved'
533
+ moved.mkdir(exist_ok=True)
534
+ require(moved.resolve() == moved and moved.is_dir(), 'moved receipt directory redirected; retain')
535
+ destination = moved / (receipt['id'] + '.' + state['request']['operation_id'])
536
+ if path.parent != destination:
537
+ require(not path_present(destination), 'moved receipt destination already exists; retain both')
538
+ path.parent.rename(destination)
539
+ # Also retry the retired source barrier after a rename whose reply was lost.
540
+ if path_present(root / 'retired'):
541
+ storage_call('sync_directory', root / 'retired')
542
+ storage_call('sync_directory', moved)
543
+ storage_call('sync_directory', root)
544
+ return {'status': 'STORAGE_MOVED', 'receipt': str(destination / 'receipt.json'), **state['moved']}
545
+
546
+
490
547
  def recover_storage(args):
491
548
  path = resolve_receipt(Path(args.receipt).absolute(), operation_id=getattr(args, "operation_id", None))
492
- with locked_receipt(path, operation_id=getattr(args, "operation_id", None), resume=True) as receipt:
549
+ with locked_receipt(path, operation_id=getattr(args, "operation_id", None), resume=True, allow_moved=True) as receipt:
493
550
  args.operation_id = receipt.get("storage", {}).get("request", {}).get("operation_id")
494
551
  require(storage_call("managed_receipt", receipt), "receipt is not managed")
552
+ operation = receipt['storage'].get('moved', {}).get('operation_id') or storage_call(
553
+ 'moved_operation', receipt['storage']['decision']['registration'])
554
+ if operation:
555
+ return release_moved(receipt, path, operation)
495
556
  if args.for_removal:
496
557
  accepted = receipt.get('acceptance') or receipt.get('pushed') or receipt.get('pr_number')
497
558
  if accepted and receipt.get('pr_number') and receipt.get('delivery') not in {'COMPLETE', 'CLOSED'}:
@@ -1457,32 +1518,41 @@ def resolve_receipt(path, *, operation_id=None):
1457
1518
  if operation_id is None or read_json(path).get('storage', {}).get('request', {}).get('operation_id') == operation_id:
1458
1519
  return path
1459
1520
  require(path.parent.parent.name == 'devlyn-completion', 'receipt operation ID differs from saved resume')
1521
+ kinds = ('retired', 'moved')
1522
+ if path.parent.parent.name == 'retired' and path.parent.parent.parent.name == 'devlyn-completion':
1523
+ key, separator, archived_operation = path.parent.name.partition('.')
1524
+ require(separator and (operation_id is None or operation_id == archived_operation),
1525
+ 'receipt operation ID differs from saved resume')
1526
+ operation_id = archived_operation
1527
+ path = path.parent.parent.parent / key / 'receipt.json'
1528
+ kinds = ('moved',)
1460
1529
  require(path.parent.parent.name == 'devlyn-completion', 'receipt locator is missing')
1461
1530
  candidates = []
1462
- retired = path.parent.parent / 'retired'
1463
- if path_present(retired):
1464
- require(retired == retired.resolve() and retired.is_dir(), 'retired receipt directory redirected')
1465
- for candidate in retired.glob(path.parent.name + '.*/receipt.json'):
1466
- if operation_id is not None and candidate.parent.name != path.parent.name + '.' + operation_id:
1467
- continue
1468
- saved = read_json(candidate)
1469
- operation = saved.get('storage', {}).get('request', {}).get('operation_id')
1470
- require(candidate == candidate.resolve() and saved.get('id') == path.parent.name
1471
- and Path(saved['common_gitdir']) / 'devlyn-completion' / saved['id'] / 'receipt.json' == path
1472
- and saved.get('storage', {}).get('request', {}).get('external_receipt') == str(path)
1473
- and candidate.parent.name == saved['id'] + '.' + str(operation)
1474
- and managed_terminal(saved),
1475
- 'retired receipt locator/operation binding changed; retain')
1476
- if operation_id is None or operation == operation_id:
1477
- candidates.append(candidate)
1478
- require(len(candidates) == 1, 'receipt operation/locator missing or ambiguous; use the operation-specific retired receipt: '
1531
+ for kind in kinds:
1532
+ retired = path.parent.parent / kind
1533
+ if path_present(retired):
1534
+ require(retired == retired.resolve() and retired.is_dir(), f'{kind} receipt directory redirected')
1535
+ for candidate in retired.glob(path.parent.name + '.*/receipt.json'):
1536
+ if operation_id is not None and candidate.parent.name != path.parent.name + '.' + operation_id:
1537
+ continue
1538
+ saved = read_json(candidate)
1539
+ operation = saved.get('storage', {}).get('request', {}).get('operation_id')
1540
+ require(candidate == candidate.resolve() and saved.get('id') == path.parent.name
1541
+ and Path(saved['common_gitdir']) / 'devlyn-completion' / saved['id'] / 'receipt.json' == path
1542
+ and saved.get('storage', {}).get('request', {}).get('external_receipt') == str(path)
1543
+ and candidate.parent.name == saved['id'] + '.' + str(operation)
1544
+ and (managed_terminal(saved) if kind == 'retired' else bool(saved.get('storage', {}).get('moved'))),
1545
+ f'{kind} receipt locator/operation binding changed; retain')
1546
+ if operation_id is None or operation == operation_id:
1547
+ candidates.append(candidate)
1548
+ require(len(candidates) == 1, 'receipt operation/locator missing or ambiguous; use the operation-specific archived receipt: '
1479
1549
  + ', '.join(map(str, candidates)))
1480
1550
  return candidates[0]
1481
1551
 
1482
1552
 
1483
1553
  @contextlib.contextmanager
1484
- def locked_receipt(path, *, blocking=True, operation_id=None, resume=False):
1485
- require(path.name == "receipt.json" and (path.parent.parent.name == "devlyn-completion" or path.parent.parent.name == "retired" and path.parent.parent.parent.name == "devlyn-completion") and path == path.resolve(), "receipt must be its original external nonsymlink path")
1554
+ def locked_receipt(path, *, blocking=True, operation_id=None, resume=False, allow_moved=False):
1555
+ require(path.name == "receipt.json" and (path.parent.parent.name == "devlyn-completion" or path.parent.parent.name in {"retired", "moved"} and path.parent.parent.parent.name == "devlyn-completion") and path == path.resolve(), "receipt must be its original external nonsymlink path")
1486
1556
  preview = read_json(path)
1487
1557
  if storage_call('managed_receipt', preview):
1488
1558
  root = path.parent.parent if path.parent.parent.name == 'devlyn-completion' else path.parent.parent.parent
@@ -1504,7 +1574,8 @@ def locked_receipt(path, *, blocking=True, operation_id=None, resume=False):
1504
1574
  require(operation_id is None or actual == operation_id, 'receipt operation ID differs from saved resume; retain both operations')
1505
1575
  original = Path(receipt['common_gitdir']) / 'devlyn-completion' / receipt['id'] / 'receipt.json'
1506
1576
  archived = original.parent.parent / 'retired' / (receipt['id'] + '.' + receipt.get('storage', {}).get('request', {}).get('operation_id', '')) / 'receipt.json'
1507
- require(path == original or path == archived, 'receipt/common Gitdir binding mismatch')
1577
+ moved = archived.parent.parent.parent / 'moved' / archived.parent.name / 'receipt.json'
1578
+ require(path in (original, archived, moved), 'receipt/common Gitdir binding mismatch')
1508
1579
  require(receipt["id"] == hashlib.sha256(receipt["branch"].encode()).hexdigest()[:24] and receipt["branch"] not in {receipt["base"], "main", "master"}, "receipt branch ownership changed")
1509
1580
  if storage_call("managed_receipt", receipt):
1510
1581
  require(receipt.get("storage", {}).get("decision", {}).get("registration") and receipt.get("storage", {}).get("request"), "Open Devlyn and repair this managed receipt binding before resuming")
@@ -1513,6 +1584,10 @@ def locked_receipt(path, *, blocking=True, operation_id=None, resume=False):
1513
1584
  require(receipt["allocation"] == "owned", "allocation was interrupted; no historical adoption is permitted")
1514
1585
  if path == archived:
1515
1586
  require(managed_terminal(receipt), 'retired receipt is not terminal')
1587
+ if path == moved:
1588
+ require(receipt.get('storage', {}).get('moved'), 'moved receipt lacks disposition')
1589
+ if storage_call('managed_receipt', receipt) and not allow_moved:
1590
+ refuse_moved(receipt, path)
1516
1591
  yield receipt
1517
1592
 
1518
1593
 
@@ -106,8 +106,10 @@ New allocation and warming modes come from enrollment state alone:
106
106
  | absent | present | verified `retired: true` | standalone (unenrollment complete) |
107
107
  | absent | present | absent, unreadable or incomplete | refuse; Open Devlyn to repair its storage registration |
108
108
 
109
- The journal `storage-client-unenroll.json` is read no-follow only on the last two
110
- rows. Schema 1, canonical UUIDv7 operation, permanent `storage-client-fence.json`,
109
+ An unfinished retirement takes precedence over enrollment; follow its
110
+ operation-specific retained-helper resume to reconcile or cancel as named.
111
+ The journal `storage-client-unenroll.json` is read no-follow for retirement
112
+ recovery and the last two rows. Schema 1, canonical UUIDv7 operation, permanent `storage-client-fence.json`,
111
113
  matching operation/root/lock identities and Rust's archive ordering/path
112
114
  validation are required. If native root/lock identities or the home path changed,
113
115
  completed retirement permits standalone only when `storage`, `control`,
@@ -119,7 +121,9 @@ completed unenrollment; a leftover managed key retains its own recovery command.
119
121
  Existing operations keep their managed identity. Receipts pin the control
120
122
  identity; current enrollment verifies the helper before allocation. Recovery uses
121
123
  the current verified `storage-client-recovery/pins.json`, including after uninstall
122
- or unenrollment, and requires its authority binding to match the receipt. Missing
124
+ or unenrollment, and requires its authority binding to match the receipt, except
125
+ for a proven moved task: use its unflagged, operation-specific `recover-storage`
126
+ resume to release it, retain its files, and archive its receipt under `moved/`. Missing
123
127
  or invalid pins retain resources and require repair; saved artifacts are never a
124
128
  fallback. Enrolled checkout preserves
125
129
  `managed_pending`/`managed_owned` and Rust's `storage-allocation.json` subreceipt.
@@ -3,7 +3,7 @@ name: devlyn-ideate
3
3
  description: Loop designer and intent queue. Turns an intent or a document into a validated loop package (a meta-prompt plus self-contained task contracts), queues its tasks, reports queue status, and drains the queue serially with evidence-derived acceptance, recovery and delivery. Operations — plan, add, status, drain; a bare intent is planned and, when the request authorizes implementation, continues through add and drain; no arguments shows status. Use when the user wants an idea, goal or document planned into tasks, wants to stack work ("queue this", "큐에 넣어줘"), asks what is queued, or wants unattended execution ("drain the queue", "큐 드레인 시작", "밤새 돌려줘").
4
4
  ---
5
5
 
6
- Ideate owns planning and durable serial execution. Each task runs under the installed methodology, the CLAUDE.md/AGENTS.md instruction block; ideate adds no phase graph, reviewer quota, engine router or restart cycle.
6
+ Ideate owns planning and durable serial execution. Each task runs under the installed methodology, the Devlyn defaults and project instructions; ideate adds no phase graph, reviewer quota, engine router or restart cycle.
7
7
 
8
8
  <ideate_args>
9
9
  $ARGUMENTS
@@ -94,7 +94,7 @@ A removed flag stops with its instruction and selects no other behavior.
94
94
 
95
95
  An absolute path to a package's `meta.md` is added as it is; anything else is planned first. Add with `python3 "$DEVLYN_SKILL_DIR/scripts/queue.py" add '<absolute meta.md>'`, the only writer of a loop's queue file `docs/specs/<loop-id>/queue.md`; never edit one by hand. In every delivery mode add makes no commit on the checked-out branch and leaves the branch, the index and tracked files as they are: it holds the package and its rows at `refs/devlyn/captures/<loop-id>`, records the add and removes the package copies equal to that capture. Report the capture ref and its `git show` command, any `cleanup` entry, and that a revision needs a new loop id. A retry of the same add reports it as added, also once the copies are gone. Tasks start from committed state, a local loop from the branch it was added on and `auto`/`pr` from the remote base, so commit, and for `auto`/`pr` push, whatever the plan depends on. A pending legacy raw-intent row of `docs/specs/queue.md` is replaced in place by a package whose `## Intent` reproduces the row verbatim, added with `--materialize <line>`.
96
96
 
97
- Release-backed queues retain their verified release in `<git-common-dir>/devlyn-loops/harness.json` until all tasks, recovery and delivery settle. A different release refuses with the exact recovery argv; use it. Existing unfinished unbound queues and standalone installations remain legacy.
97
+ Release-backed queues retain their verified release in `<git-common-dir>/devlyn-loops/harness.json` until all tasks, recovery and delivery settle. A different release refuses: use the exact recovery argv when it gives one; otherwise do what it says (Drain in Devlyn, the tasks to finish first, or a repair). Never build the bound queue's path yourself. Existing unfinished unbound queues and standalone installations remain legacy.
98
98
 
99
99
  ## status
100
100
 
@@ -55,9 +55,9 @@ Package cleanup, parent pruning and report failures appear as diagnostics under
55
55
 
56
56
  ## Release binding and read-only consumers
57
57
 
58
- `<git-common-dir>/devlyn-loops/harness.json` is authoritative across linked worktrees: `{schemaVersion: 1, release: {package, version, digest, root}, defaults: {path, sha256}}`. Paths are absolute and hashes cover exact bytes. The queue lock covers binding before the first capture. An interrupted first add, unfinished tasks, recovery conflicts and pending delivery retain it; settlement removes it. A different release refuses with `recovery argv` as a JSON argv array, preserving the caller’s arguments. No caller substitutes another implementation.
58
+ `<git-common-dir>/devlyn-loops/harness.json` is authoritative across linked worktrees: `{schemaVersion: 1, release: {package, version, digest, root}, defaults: {path, sha256}}`. Paths are absolute and hashes cover exact bytes. The queue lock covers binding before the first capture. An interrupted first add, unfinished tasks, recovery conflicts and pending delivery retain it; settlement removes it. A different release refuses: use the exact recovery argv when it gives one (a JSON argv array preserving the caller’s arguments); otherwise do what it says (Drain in Devlyn, the tasks to finish first, or a repair). Never build the bound queue’s path yourself. No caller substitutes another implementation.
59
59
 
60
- `queue.py inspect --repo <dir> --json` reads without cleanup, locks or writes. It returns the status summary plus `binding` (null for legacy) and `compatibility`: `compatible`, `legacy`, `reasons`, `recoveryArgv`, `taskPacket` ranges and `queueBinding` ranges. Incompatible or unreadable state is reported, never migrated. Apps should use this reader instead of `status`, which still reconciles cleanup. For a mismatch, `recoveryArgv` names the bound queue’s inspect invocation; use the same bound queue entry point for authorized add/drain operations.
60
+ `queue.py inspect --repo <dir> --json` reads without cleanup, locks or writes. It returns the status summary plus `binding` (null for legacy) and `compatibility`: `compatible`, `legacy`, `reasons`, `recoveryArgv`, `taskPacket` ranges and `queueBinding` ranges. Incompatible or unreadable state is reported, never migrated. Apps should use this reader instead of `status`, which still reconciles cleanup. For a mismatch, `recoveryArgv` names the bound queue’s inspect invocation when handoff is allowed, and is null when the refusal names Drain in Devlyn, tasks to finish first, or a repair. Use an exact recovery argv when provided; otherwise follow the refusal.
61
61
 
62
62
  Codex projection writers place `.devlyn-harness-projection.json` at the copied plugin root, beside `skills` and `.codex-plugin`: `{schemaVersion: 1, release: <ReleaseRef>, source: "native/codex/plugins/devlyn"}`. The reader verifies the named release and every projected file’s bytes/mode against that subtree (excluding only the marker). Symlinked skill invocations resolve through their real path. Standalone installs and existing unfinished unbound queues retain legacy behavior; new legacy packets use v2 with a null release, and their `defaults` name the invoking installation's own `_shared/runtime-principles.md` (`source: "skill"`). No engine applicability is inferred from instruction filenames: bound queues use verified release defaults with native instructions taking precedence, except a Devlyn managed block, which the defaults replace. Worker and runner paths point to the bound release, independent of a projection cache’s lifetime.
63
63
 
@@ -188,8 +188,11 @@ def enforce_binding(common, anchor=None):
188
188
  if binding and anchor is not None and release_settled_binding(anchor, common, interrupted_add=True):
189
189
  return enforce_binding(common)
190
190
  if binding:
191
- argv = harness_call("recovery_argv", binding, sys.argv[1:])
192
- raise LoopError(f"{exc}; recovery argv: {json.dumps(argv, ensure_ascii=False)}") from exc
191
+ line = shared("storage-client")["queue_handoff"](common, binding)
192
+ argv = harness_call("recovery_argv", binding, sys.argv[1:]) if line is None else None
193
+ error = LoopError(f"{exc}; {line}" if line is not None else f"{exc}; recovery argv: {json.dumps(argv, ensure_ascii=False)}")
194
+ error.handoff = line
195
+ raise error from exc
193
196
  raise
194
197
  return binding
195
198
 
@@ -1661,7 +1664,11 @@ def inspect(args):
1661
1664
  compatibility.update(compatible=False, reasons=[str(exc)])
1662
1665
  try:
1663
1666
  valid = harness_call("read_binding", common)
1664
- if valid:
1667
+ if hasattr(exc, "handoff"):
1668
+ line = exc.handoff
1669
+ else:
1670
+ line = shared("storage-client")["queue_handoff"](common, valid) if valid else None
1671
+ if valid and line is None:
1665
1672
  compatibility["recoveryArgv"] = harness_call("recovery_argv", valid, ["inspect", "--repo", str(anchor), "--json"])
1666
1673
  except (LoopError, OSError):
1667
1674
  pass # The diagnostic above describes the malformed binding; no safe argv can be inferred.
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "devlyn-harness-v4-3-25",
2
+ "name": "devlyn-harness-v4-3-28",
3
3
  "plugins": [
4
4
  {
5
5
  "name": "devlyn",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devlyn",
3
- "version": "4.3.25",
3
+ "version": "4.3.28",
4
4
  "description": "Devlyn harness skills",
5
5
  "skills": "./skills/"
6
6
  }
@@ -17,7 +17,7 @@ Seven rules govern every change. Cite them by name when a decision touches one.
17
17
  3. **No guesswork** — verify with the actual files, logs, diffs, and run output before forming conclusions. State the falsifiable prediction BEFORE the experiment; record raw results AFTER. Retroactive prediction edits are dishonest.
18
18
  4. **Worldclass** — zero CRITICAL, zero HIGH security/design findings on the shippable path. Any review is one pass in which every reviewer, including each one the user requires, reads the source the checks ran on; fix each finding or reject it with a reason, re-check the fixes and the invariants they touch, and repeat the full review only while a CRITICAL or HIGH finding remains.
19
19
  5. **Best practice** — idiomatic for the language and framework. Use standard primitives; do not hand-roll what the library already provides.
20
- 6. **Optimized** — use efficient code and avoid unnecessary work without sacrificing correctness or the user's requirements.
20
+ 6. **Optimized** — efficient on the resource that matters (the user's time, tokens, attention, runtime) without weakening correctness or requirements. An extra check or process step must beat the simpler path.
21
21
  7. **Production ready** — error states are explicit and visible; behavior under failure is what the user expects, not silent corruption.
22
22
 
23
23
  Three discipline rules govern HOW the principles are applied:
@@ -52,6 +52,7 @@ def validate_closure(value):
52
52
  require(type(value) is dict, 'positive reservation closure missing')
53
53
  tag = value.get('established_by')
54
54
  shapes = {'released': {'established_by': ('released',), 'reservation': 'reservation'},
55
+ 'authority_migrated': {'established_by': ('authority_migrated',), 'operation_id': 'str'},
55
56
  'authority_expired': {'established_by': ('authority_expired',), 'operation_id': 'str'},
56
57
  'never_admitted': {'established_by': ('never_admitted',), 'operation_id': 'str'},
57
58
  'legacy_unreserved': {'established_by': ('legacy_unreserved',), 'receipt_version': 'u32'}}