@pennixrv/trellis 0.6.34 → 0.6.35

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.
@@ -0,0 +1,9 @@
1
+ {
2
+ "version": "0.6.34",
3
+ "description": "Records the published 0.6.34 release before the sealed-handoff lifecycle release.",
4
+ "breaking": false,
5
+ "recommendMigrate": false,
6
+ "changelog": "**Internal:**\n- Restores the published 0.6.34 release manifest so adjacent-version update paths remain complete.",
7
+ "migrations": [],
8
+ "notes": "No project migration is required. Existing projects can run the normal `trellis update` flow."
9
+ }
@@ -781,6 +781,22 @@ def clear_active_task(
781
781
  return previous
782
782
 
783
783
 
784
+ def clear_active_task_for_context(
785
+ context_key: str,
786
+ task_path: str,
787
+ repo_root: Path,
788
+ ) -> str:
789
+ """Clear one exact session pointer, refusing an unexpected replacement."""
790
+ context_path = _context_path(repo_root, context_key)
791
+ context = _read_json(context_path)
792
+ if context is None or not _string_value(context.get("current_task")):
793
+ return "absent"
794
+ current = _string_value(context.get("current_task"))
795
+ if not _task_refs_match(current, task_path, repo_root):
796
+ return "changed"
797
+ return "cleared" if context_path.is_file() and _remove_file(context_path) else "absent"
798
+
799
+
784
800
  def clear_task_from_sessions(task_path: str, repo_root: Path) -> int:
785
801
  """Delete all session runtime files that point at a task."""
786
802
  target = _canonical_task_ref(task_path, repo_root) or normalize_task_ref(task_path)
@@ -10,7 +10,7 @@ from datetime import datetime, timezone
10
10
  from pathlib import Path
11
11
  from typing import Any
12
12
 
13
- from .active_task import clear_active_task, resolve_active_task, resolve_context_key, set_active_task
13
+ from .active_task import clear_active_task, clear_active_task_for_context, resolve_active_task, resolve_context_key, set_active_task
14
14
  from .continuation_record import (
15
15
  ContinuationError,
16
16
  SAFE_ID,
@@ -290,6 +290,32 @@ def retire(root: Path, task_id: str, handoff_id: str, core_digest: str, expected
290
290
  return _public(record) | {"record_digest": digest}
291
291
 
292
292
 
293
+ def retire_handoff(root: Path, task_id: str, handoff_id: str, core_digest: str) -> dict[str, Any]:
294
+ """Release one sealed handoff without requiring the departed source context."""
295
+ path, record = _load(root, task_id, handoff_id)
296
+ with _record_lock(path):
297
+ record = _decode(path)
298
+ if record["core_digest"] != core_digest:
299
+ raise OwnershipError("core_digest does not match")
300
+ if record["state"] == "ready":
301
+ return _public(record)
302
+ if record["state"] not in {"sealed", "retiring"}:
303
+ raise OwnershipError("ownership record is not a sealed handoff")
304
+ if record["state"] == "sealed":
305
+ record["previous_event_digest"] = record["integrity"]["record_digest"]
306
+ record["state"] = "retiring"
307
+ record["generation"] += 1
308
+ _write(path, record)
309
+ cleared = clear_active_task_for_context(record["source_context_key"], record["task"]["path"], root)
310
+ if cleared == "changed":
311
+ raise OwnershipError("source task pointer changed during handoff retirement")
312
+ record["previous_event_digest"] = record["integrity"]["record_digest"]
313
+ record["state"] = "ready"
314
+ record["generation"] += 1
315
+ digest = _write(path, record)
316
+ return _public(record) | {"record_digest": digest}
317
+
318
+
293
319
  def claim(root: Path, task_id: str, task_path: str, handoff_id: str, core_digest: str, expected_generation: int) -> dict[str, Any]:
294
320
  actor, active = _direct_context(root)
295
321
  path, record = _load(root, task_id, handoff_id)
@@ -410,5 +436,4 @@ def assert_task_mutation_allowed(root: Path, task_path: Path) -> None:
410
436
  raise OwnershipError("ownership record task path does not match current task")
411
437
  if record.get("consumer_session_id") == actor:
412
438
  continue
413
- if record["source_session_id"] == actor or record["state"] in {"retiring", "ready", "claiming", "claimed", "consumed", "archived"}:
414
- raise OwnershipError("fencing_conflict: task is owned by another handoff session")
439
+ raise OwnershipError("fencing_conflict: task is owned by another handoff session")
@@ -90,6 +90,7 @@ from common.ownership_record import (
90
90
  consume as consume_ownership,
91
91
  quiesce as quiesce_ownership,
92
92
  retire as retire_ownership,
93
+ retire_handoff as retire_handoff_ownership,
93
94
  seal as seal_ownership,
94
95
  status as ownership_status,
95
96
  )
@@ -436,6 +437,10 @@ def cmd_ownership(args: argparse.Namespace) -> int:
436
437
  repo_root, args.task_id, args.handoff_id, args.core_digest,
437
438
  args.expected_generation, args.archive_observation,
438
439
  )
440
+ elif command == "retire-handoff":
441
+ result = retire_handoff_ownership(
442
+ repo_root, args.task_id, args.handoff_id, args.core_digest,
443
+ )
439
444
  elif command == "claim":
440
445
  result = claim_ownership(
441
446
  repo_root, args.task_id, args.task, args.handoff_id, args.core_digest, args.expected_generation
@@ -840,6 +845,9 @@ def main() -> int:
840
845
  ownership_retire.add_argument("--archive-observation", required=True, choices=("not_required", "observed"))
841
846
  add_ownership_common(ownership_retire, expected=True)
842
847
 
848
+ ownership_retire_handoff = ownership_sub.add_parser("retire-handoff", help="Release one sealed handoff by exact id")
849
+ add_ownership_common(ownership_retire_handoff)
850
+
843
851
  ownership_claim = ownership_sub.add_parser("claim", help="Claim a ready handoff as this session")
844
852
  ownership_claim.add_argument("--task", required=True, help="Task path to bind to this session")
845
853
  add_ownership_common(ownership_claim, expected=True)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pennixrv/trellis",
3
- "version": "0.6.34",
3
+ "version": "0.6.35",
4
4
  "description": "AI capabilities grow like ivy — Trellis provides the structure to guide them along a disciplined path",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -34,7 +34,7 @@
34
34
  "inquirer": "^9.3.7",
35
35
  "undici": "^6.21.0",
36
36
  "zod": "^4.4.2",
37
- "@pennixrv/trellis-core": "0.6.34"
37
+ "@pennixrv/trellis-core": "0.6.35"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@eslint/js": "^9.18.0",