@pennixrv/trellis 0.6.27 → 0.6.29

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/dist/commands/channel/agent-loader.d.ts +3 -1
  2. package/dist/commands/channel/agent-loader.d.ts.map +1 -1
  3. package/dist/commands/channel/agent-loader.js +32 -1
  4. package/dist/commands/channel/agent-loader.js.map +1 -1
  5. package/dist/commands/channel/guard.d.ts +21 -2
  6. package/dist/commands/channel/guard.d.ts.map +1 -1
  7. package/dist/commands/channel/guard.js +105 -4
  8. package/dist/commands/channel/guard.js.map +1 -1
  9. package/dist/commands/channel/index.d.ts.map +1 -1
  10. package/dist/commands/channel/index.js +22 -5
  11. package/dist/commands/channel/index.js.map +1 -1
  12. package/dist/commands/channel/run.d.ts.map +1 -1
  13. package/dist/commands/channel/run.js +15 -9
  14. package/dist/commands/channel/run.js.map +1 -1
  15. package/dist/commands/channel/spawn.d.ts +4 -0
  16. package/dist/commands/channel/spawn.d.ts.map +1 -1
  17. package/dist/commands/channel/spawn.js +59 -7
  18. package/dist/commands/channel/spawn.js.map +1 -1
  19. package/dist/commands/channel/workers.d.ts +8 -0
  20. package/dist/commands/channel/workers.d.ts.map +1 -0
  21. package/dist/commands/channel/workers.js +30 -0
  22. package/dist/commands/channel/workers.js.map +1 -0
  23. package/dist/migrations/manifests/0.6.27.json +9 -0
  24. package/dist/migrations/manifests/0.6.28.json +9 -0
  25. package/dist/migrations/manifests/0.6.29.json +9 -0
  26. package/dist/templates/common/bundled-skills/trellis-channel/references/command-reference.md +23 -4
  27. package/dist/templates/common/bundled-skills/trellis-channel/references/subnode-work.md +37 -8
  28. package/dist/templates/common/bundled-skills/trellis-channel/references/workers.md +25 -6
  29. package/dist/templates/common/bundled-skills/trellis-meta/SKILL.md +2 -2
  30. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/multi-agent-channel.md +2 -1
  31. package/dist/templates/trellis/agents/subnode.env +4 -0
  32. package/dist/templates/trellis/agents/subnode.md +5 -0
  33. package/dist/templates/trellis/config.yaml +9 -1
  34. package/dist/templates/trellis/index.d.ts +5 -3
  35. package/dist/templates/trellis/index.d.ts.map +1 -1
  36. package/dist/templates/trellis/index.js +7 -3
  37. package/dist/templates/trellis/index.js.map +1 -1
  38. package/dist/templates/trellis/scripts/common/ownership_record.py +414 -0
  39. package/dist/templates/trellis/scripts/subnode_artifact.py +100 -1
  40. package/dist/templates/trellis/scripts/task.py +114 -1
  41. package/package.json +2 -2
@@ -85,7 +85,8 @@ max_journal_lines: 2000
85
85
  # continuously idle past `idle_timeout`).
86
86
  #
87
87
  # Precedence: CLI flag > env var (TRELLIS_CHANNEL_WORKER_IDLE_TIMEOUT /
88
- # TRELLIS_CHANNEL_MAX_LIVE_WORKERS) > this config > built-in default.
88
+ # TRELLIS_CHANNEL_MAX_LIVE_WORKERS) > role config (`channel.subnode` for
89
+ # subnodes, `channel.worker_guard` otherwise) > built-in default.
89
90
  #
90
91
  # `idle_timeout: 0` disables idle cleanup (workers can sit idle forever
91
92
  # unless explicitly killed or given `--timeout`).
@@ -103,6 +104,13 @@ channel:
103
104
  worker_guard:
104
105
  idle_timeout: 5m
105
106
  max_live_workers: 6
107
+ # Defaults for `trellis channel spawn --agent subnode`. Explicit spawn
108
+ # flags still override these values; ordinary workers use worker_guard.
109
+ subnode:
110
+ idle_timeout: 5m
111
+ max_live_workers: 8
112
+ timeout: 30m
113
+ warn_before: 5m
106
114
  # trusted_context_dirs:
107
115
  # - /work/user/trellis_workspace
108
116
  # auto_trust_trellis_symlinks: false
@@ -10,8 +10,8 @@
10
10
  * │ ├── __init__.py
11
11
  * │ ├── common/ # Shared utilities (Python)
12
12
  * │ └── *.py # Main scripts (Python)
13
- * ├── agents/ # Channel runtime agent definitions
14
- * │ └── *.md # Loaded by `trellis channel spawn --agent <name>`
13
+ * ├── agents/ # Channel runtime agent definitions and assets
14
+ * │ └── *.md / *.env # Loaded by the channel runtime
15
15
  * ├── scripts-shell-archive/ # Archived shell scripts (for reference)
16
16
  * ├── workflow.md # Workflow guide
17
17
  * ├── config.yaml # Trellis configuration
@@ -37,6 +37,7 @@ export declare const commonTaskContext: string;
37
37
  export declare const commonTaskStore: string;
38
38
  export declare const commonSessionContext: string;
39
39
  export declare const commonContinuationRecord: string;
40
+ export declare const commonOwnershipRecord: string;
40
41
  export declare const commonPackagesContext: string;
41
42
  export declare const commonWorkflowPhase: string;
42
43
  export declare const commonTrellisConfig: string;
@@ -55,12 +56,13 @@ export declare const gitattributesTemplate: string;
55
56
  export declare const implementAgentTemplate: string;
56
57
  export declare const checkAgentTemplate: string;
57
58
  export declare const subnodeAgentTemplate: string;
59
+ export declare const subnodeEnvTemplate: string;
58
60
  /**
59
61
  * Get all script templates as a map of relative path to content
60
62
  */
61
63
  export declare function getAllScripts(): Map<string, string>;
62
64
  /**
63
- * Get all channel runtime agent definitions as a map of relative path
65
+ * Get all channel runtime agent assets as a map of relative path
64
66
  * (under `.trellis/agents/`) to content.
65
67
  *
66
68
  * Consumed by `trellis init` (to dispatch on first install) and by
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/templates/trellis/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAcH,eAAO,MAAM,WAAW,QAAsC,CAAC;AAG/D,eAAO,MAAM,UAAU,QAA6C,CAAC;AACrE,eAAO,MAAM,WAAW,QAA0C,CAAC;AACnE,eAAO,MAAM,eAAe,QAA8C,CAAC;AAC3E,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAC9E,eAAO,MAAM,eAAe,QAA+C,CAAC;AAC5E,eAAO,MAAM,eAAe,QAA+C,CAAC;AAC5E,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAC9E,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAC9E,eAAO,MAAM,YAAY,QAA2C,CAAC;AACrE,eAAO,MAAM,QAAQ,QAAuC,CAAC;AAC7D,eAAO,MAAM,SAAS,QAAwC,CAAC;AAC/D,eAAO,MAAM,SAAS,QAAwC,CAAC;AAC/D,eAAO,MAAM,WAAW,QAA0C,CAAC;AACnE,eAAO,MAAM,WAAW,QAA0C,CAAC;AACnE,eAAO,MAAM,iBAAiB,QAAiD,CAAC;AAChF,eAAO,MAAM,eAAe,QAA+C,CAAC;AAC5E,eAAO,MAAM,oBAAoB,QAEhC,CAAC;AACF,eAAO,MAAM,wBAAwB,QAEpC,CAAC;AACF,eAAO,MAAM,qBAAqB,QAEjC,CAAC;AACF,eAAO,MAAM,mBAAmB,QAE/B,CAAC;AACF,eAAO,MAAM,mBAAmB,QAE/B,CAAC;AACF,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAG9E,eAAO,MAAM,kBAAkB,QAA2C,CAAC;AAC3E,eAAO,MAAM,mBAAmB,QAA4C,CAAC;AAC7E,eAAO,MAAM,UAAU,QAAkC,CAAC;AAC1D,eAAO,MAAM,gBAAgB,QAAyC,CAAC;AACvE,eAAO,MAAM,gBAAgB,QAAyC,CAAC;AACvE,eAAO,MAAM,qBAAqB,QAEjC,CAAC;AACF,eAAO,MAAM,mBAAmB,QAA4C,CAAC;AAG7E,eAAO,MAAM,kBAAkB,QAA8B,CAAC;AAC9D,eAAO,MAAM,kBAAkB,QAA8B,CAAC;AAC9D,eAAO,MAAM,iBAAiB,QAAgC,CAAC;AAC/D,eAAO,MAAM,qBAAqB,QAAoC,CAAC;AAMvE,eAAO,MAAM,sBAAsB,QAAsC,CAAC;AAC1E,eAAO,MAAM,kBAAkB,QAAkC,CAAC;AAClE,eAAO,MAAM,oBAAoB,QAAoC,CAAC;AAEtE;;GAEG;AACH,wBAAgB,aAAa,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAwCnD;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAMlD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/templates/trellis/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAcH,eAAO,MAAM,WAAW,QAAsC,CAAC;AAG/D,eAAO,MAAM,UAAU,QAA6C,CAAC;AACrE,eAAO,MAAM,WAAW,QAA0C,CAAC;AACnE,eAAO,MAAM,eAAe,QAA8C,CAAC;AAC3E,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAC9E,eAAO,MAAM,eAAe,QAA+C,CAAC;AAC5E,eAAO,MAAM,eAAe,QAA+C,CAAC;AAC5E,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAC9E,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAC9E,eAAO,MAAM,YAAY,QAA2C,CAAC;AACrE,eAAO,MAAM,QAAQ,QAAuC,CAAC;AAC7D,eAAO,MAAM,SAAS,QAAwC,CAAC;AAC/D,eAAO,MAAM,SAAS,QAAwC,CAAC;AAC/D,eAAO,MAAM,WAAW,QAA0C,CAAC;AACnE,eAAO,MAAM,WAAW,QAA0C,CAAC;AACnE,eAAO,MAAM,iBAAiB,QAAiD,CAAC;AAChF,eAAO,MAAM,eAAe,QAA+C,CAAC;AAC5E,eAAO,MAAM,oBAAoB,QAEhC,CAAC;AACF,eAAO,MAAM,wBAAwB,QAEpC,CAAC;AACF,eAAO,MAAM,qBAAqB,QAEjC,CAAC;AACF,eAAO,MAAM,qBAAqB,QAEjC,CAAC;AACF,eAAO,MAAM,mBAAmB,QAE/B,CAAC;AACF,eAAO,MAAM,mBAAmB,QAE/B,CAAC;AACF,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAG9E,eAAO,MAAM,kBAAkB,QAA2C,CAAC;AAC3E,eAAO,MAAM,mBAAmB,QAA4C,CAAC;AAC7E,eAAO,MAAM,UAAU,QAAkC,CAAC;AAC1D,eAAO,MAAM,gBAAgB,QAAyC,CAAC;AACvE,eAAO,MAAM,gBAAgB,QAAyC,CAAC;AACvE,eAAO,MAAM,qBAAqB,QAEjC,CAAC;AACF,eAAO,MAAM,mBAAmB,QAA4C,CAAC;AAG7E,eAAO,MAAM,kBAAkB,QAA8B,CAAC;AAC9D,eAAO,MAAM,kBAAkB,QAA8B,CAAC;AAC9D,eAAO,MAAM,iBAAiB,QAAgC,CAAC;AAC/D,eAAO,MAAM,qBAAqB,QAAoC,CAAC;AAMvE,eAAO,MAAM,sBAAsB,QAAsC,CAAC;AAC1E,eAAO,MAAM,kBAAkB,QAAkC,CAAC;AAClE,eAAO,MAAM,oBAAoB,QAAoC,CAAC;AACtE,eAAO,MAAM,kBAAkB,QAAqC,CAAC;AAErE;;GAEG;AACH,wBAAgB,aAAa,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAyCnD;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAOlD"}
@@ -10,8 +10,8 @@
10
10
  * │ ├── __init__.py
11
11
  * │ ├── common/ # Shared utilities (Python)
12
12
  * │ └── *.py # Main scripts (Python)
13
- * ├── agents/ # Channel runtime agent definitions
14
- * │ └── *.md # Loaded by `trellis channel spawn --agent <name>`
13
+ * ├── agents/ # Channel runtime agent definitions and assets
14
+ * │ └── *.md / *.env # Loaded by the channel runtime
15
15
  * ├── scripts-shell-archive/ # Archived shell scripts (for reference)
16
16
  * ├── workflow.md # Workflow guide
17
17
  * ├── config.yaml # Trellis configuration
@@ -47,6 +47,7 @@ export const commonTaskContext = readTemplate("scripts/common/task_context.py");
47
47
  export const commonTaskStore = readTemplate("scripts/common/task_store.py");
48
48
  export const commonSessionContext = readTemplate("scripts/common/session_context.py");
49
49
  export const commonContinuationRecord = readTemplate("scripts/common/continuation_record.py");
50
+ export const commonOwnershipRecord = readTemplate("scripts/common/ownership_record.py");
50
51
  export const commonPackagesContext = readTemplate("scripts/common/packages_context.py");
51
52
  export const commonWorkflowPhase = readTemplate("scripts/common/workflow_phase.py");
52
53
  export const commonTrellisConfig = readTemplate("scripts/common/trellis_config.py");
@@ -71,6 +72,7 @@ export const gitattributesTemplate = readTemplate("gitattributes.txt");
71
72
  export const implementAgentTemplate = readTemplate("agents/implement.md");
72
73
  export const checkAgentTemplate = readTemplate("agents/check.md");
73
74
  export const subnodeAgentTemplate = readTemplate("agents/subnode.md");
75
+ export const subnodeEnvTemplate = readTemplate("agents/subnode.env");
74
76
  /**
75
77
  * Get all script templates as a map of relative path to content
76
78
  */
@@ -97,6 +99,7 @@ export function getAllScripts() {
97
99
  scripts.set("common/task_store.py", commonTaskStore);
98
100
  scripts.set("common/session_context.py", commonSessionContext);
99
101
  scripts.set("common/continuation_record.py", commonContinuationRecord);
102
+ scripts.set("common/ownership_record.py", commonOwnershipRecord);
100
103
  scripts.set("common/packages_context.py", commonPackagesContext);
101
104
  scripts.set("common/workflow_phase.py", commonWorkflowPhase);
102
105
  scripts.set("common/trellis_config.py", commonTrellisConfig);
@@ -112,7 +115,7 @@ export function getAllScripts() {
112
115
  return scripts;
113
116
  }
114
117
  /**
115
- * Get all channel runtime agent definitions as a map of relative path
118
+ * Get all channel runtime agent assets as a map of relative path
116
119
  * (under `.trellis/agents/`) to content.
117
120
  *
118
121
  * Consumed by `trellis init` (to dispatch on first install) and by
@@ -124,6 +127,7 @@ export function getAllAgents() {
124
127
  agents.set("implement.md", implementAgentTemplate);
125
128
  agents.set("check.md", checkAgentTemplate);
126
129
  agents.set("subnode.md", subnodeAgentTemplate);
130
+ agents.set("subnode.env", subnodeEnvTemplate);
127
131
  return agents;
128
132
  }
129
133
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/templates/trellis/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/templates/trellis/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,414 @@
1
+ """Task-bound ownership transfer for formal cross-session handoff."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import secrets
8
+ import tempfile
9
+ from datetime import datetime, timezone
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from .active_task import clear_active_task, resolve_active_task, resolve_context_key, set_active_task
14
+ from .continuation_record import (
15
+ ContinuationError,
16
+ SAFE_ID,
17
+ SHA256,
18
+ _digest,
19
+ _project_file,
20
+ _record_lock,
21
+ _safe_task_id,
22
+ _text,
23
+ )
24
+ from .io import read_json_checked
25
+ from .paths import FILE_TASK_JSON, get_repo_root
26
+
27
+
28
+ KIND = "trellis-handoff-ownership"
29
+ SCHEMA_VERSION = 1
30
+ STATES = {"quiescing", "sealed", "retiring", "ready", "claiming", "claimed", "consumed", "archived"}
31
+ ARCHIVE_OBSERVATIONS = {"not_required", "observed"}
32
+ MAX_RECORD_BYTES = 32 * 1024
33
+
34
+
35
+ class OwnershipError(ContinuationError):
36
+ """Raised when a handoff ownership transition is unsafe or stale."""
37
+
38
+
39
+ def _identity(value: Any, label: str) -> str:
40
+ value = _text(value, label, 160)
41
+ if not SAFE_ID.fullmatch(value):
42
+ raise OwnershipError(f"{label} is unsafe")
43
+ return value
44
+
45
+
46
+ def _record_path(root: Path, task_id: str, handoff_id: str) -> Path:
47
+ task_id = _safe_task_id(task_id)
48
+ handoff_id = _identity(handoff_id, "handoff_id")
49
+ runtime = root / ".trellis" / ".runtime"
50
+ records = runtime / "handoff-ownership"
51
+ task_dir = records / task_id
52
+ if runtime.is_symlink() or records.is_symlink() or task_dir.is_symlink():
53
+ raise OwnershipError("ownership record directory is unsafe")
54
+ return task_dir / f"{handoff_id}.json"
55
+
56
+
57
+ def _now() -> str:
58
+ return datetime.now(timezone.utc).isoformat(timespec="microseconds").replace("+00:00", "Z")
59
+
60
+
61
+ def _task_snapshot(root: Path, task_path: str) -> dict[str, str]:
62
+ path = _project_file(root, task_path, "task path")
63
+ relative = path.relative_to(root).as_posix()
64
+ if not relative.startswith(".trellis/tasks/"):
65
+ raise OwnershipError("task path is outside .trellis/tasks")
66
+ data, reason = read_json_checked(path / FILE_TASK_JSON)
67
+ if data is None:
68
+ raise OwnershipError(f"task.json is {reason}")
69
+ task_id = _safe_task_id(data.get("id") or data.get("name"))
70
+ return {"id": task_id, "path": relative, "status": _text(data.get("status"), "task.status", 64)}
71
+
72
+
73
+ def _direct_context(root: Path, require_task: bool = False) -> tuple[str, Any]:
74
+ context_key = resolve_context_key()
75
+ if not context_key:
76
+ raise OwnershipError("no_direct_session_identity")
77
+ active = resolve_active_task(
78
+ root,
79
+ allow_single_session_fallback=False,
80
+ allow_environment_context=True,
81
+ )
82
+ if active.source_type == "session-fallback":
83
+ raise OwnershipError("session_fallback_untrusted")
84
+ if active.context_key != context_key:
85
+ raise OwnershipError("direct_session_identity_unstable")
86
+ if require_task and not active.task_path:
87
+ raise OwnershipError("no_direct_current_task")
88
+ return context_key, active
89
+
90
+
91
+ def _new_record(task: dict[str, str], handoff_id: str, core_digest: str, source: str) -> dict[str, Any]:
92
+ if not SHA256.fullmatch(core_digest):
93
+ raise OwnershipError("core_digest is invalid")
94
+ return {
95
+ "schema_version": SCHEMA_VERSION,
96
+ "kind": KIND,
97
+ "handoff_id": _identity(handoff_id, "handoff_id"),
98
+ "core_digest": core_digest,
99
+ "task": task,
100
+ "source_session_id": source,
101
+ "source_context_key": source,
102
+ "consumer_session_id": None,
103
+ "consumer_context_key": None,
104
+ "state": "quiescing",
105
+ "generation": 0,
106
+ "fencing_token": secrets.token_hex(16),
107
+ "archive_observation": None,
108
+ "event_id": f"event-{secrets.token_hex(12)}",
109
+ "previous_event_digest": None,
110
+ "updated_at": _now(),
111
+ }
112
+
113
+
114
+ def _record_digest(record: dict[str, Any]) -> str:
115
+ return _digest({key: value for key, value in record.items() if key != "integrity"})
116
+
117
+
118
+ def _decode(path: Path) -> dict[str, Any]:
119
+ if path.is_symlink() or not path.is_file() or path.stat().st_size > MAX_RECORD_BYTES:
120
+ raise OwnershipError("ownership record is unsafe or too large")
121
+ data, reason = read_json_checked(path)
122
+ if data is None:
123
+ raise OwnershipError(f"ownership record is {reason}")
124
+ required = {
125
+ "schema_version", "kind", "handoff_id", "core_digest", "task",
126
+ "source_session_id", "source_context_key", "consumer_session_id",
127
+ "consumer_context_key", "state", "generation", "fencing_token",
128
+ "archive_observation", "event_id", "previous_event_digest", "updated_at", "integrity",
129
+ }
130
+ if set(data) != required or data["schema_version"] != SCHEMA_VERSION or data["kind"] != KIND:
131
+ raise OwnershipError("ownership record schema is unsupported")
132
+ _identity(data["handoff_id"], "handoff_id")
133
+ if not SHA256.fullmatch(data["core_digest"]):
134
+ raise OwnershipError("core_digest is invalid")
135
+ task = data["task"]
136
+ if not isinstance(task, dict) or set(task) != {"id", "path", "status"}:
137
+ raise OwnershipError("ownership task is invalid")
138
+ _safe_task_id(task["id"])
139
+ _text(task["path"], "task.path", 1024)
140
+ _text(task["status"], "task.status", 64)
141
+ for key in ("source_session_id", "source_context_key"):
142
+ _identity(data[key], key)
143
+ for key in ("consumer_session_id", "consumer_context_key"):
144
+ if data[key] is not None:
145
+ _identity(data[key], key)
146
+ if data["state"] not in STATES or not isinstance(data["generation"], int) or data["generation"] < 0:
147
+ raise OwnershipError("ownership state is invalid")
148
+ _identity(data["fencing_token"], "fencing_token")
149
+ if data["archive_observation"] is not None and data["archive_observation"] not in ARCHIVE_OBSERVATIONS:
150
+ raise OwnershipError("archive observation is invalid")
151
+ _identity(data["event_id"], "event_id")
152
+ if data["previous_event_digest"] is not None and not SHA256.fullmatch(data["previous_event_digest"]):
153
+ raise OwnershipError("previous event digest is invalid")
154
+ _text(data["updated_at"], "updated_at", 128)
155
+ integrity = data["integrity"]
156
+ if not isinstance(integrity, dict) or set(integrity) != {"record_digest"} or not SHA256.fullmatch(integrity["record_digest"]):
157
+ raise OwnershipError("ownership integrity is invalid")
158
+ if integrity["record_digest"] != _record_digest(data):
159
+ raise OwnershipError("ownership record digest does not match")
160
+ return data
161
+
162
+
163
+ def _write(path: Path, record: dict[str, Any]) -> str:
164
+ record["event_id"] = f"event-{secrets.token_hex(12)}"
165
+ record["updated_at"] = _now()
166
+ record["integrity"] = {"record_digest": _record_digest(record)}
167
+ encoded = json.dumps(record, ensure_ascii=True, indent=2, sort_keys=True) + "\n"
168
+ if len(encoded.encode("utf-8")) > MAX_RECORD_BYTES:
169
+ raise OwnershipError("ownership record is too large")
170
+ temporary: Path | None = None
171
+ try:
172
+ path.parent.mkdir(parents=True, exist_ok=True)
173
+ with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=path.parent, delete=False) as handle:
174
+ temporary = Path(handle.name)
175
+ handle.write(encoded)
176
+ handle.flush()
177
+ os.fsync(handle.fileno())
178
+ os.chmod(temporary, 0o600)
179
+ os.replace(temporary, path)
180
+ directory_fd = os.open(path.parent, os.O_RDONLY)
181
+ try:
182
+ os.fsync(directory_fd)
183
+ finally:
184
+ os.close(directory_fd)
185
+ except OSError as exc:
186
+ if temporary is not None:
187
+ temporary.unlink(missing_ok=True)
188
+ raise OwnershipError("ownership record atomic write failed") from exc
189
+ return record["integrity"]["record_digest"]
190
+
191
+
192
+ def _load(root: Path, task_id: str, handoff_id: str) -> tuple[Path, dict[str, Any]]:
193
+ path = _record_path(root, task_id, handoff_id)
194
+ if not path.exists():
195
+ raise OwnershipError("ownership record is absent")
196
+ return path, _decode(path)
197
+
198
+
199
+ def _check(record: dict[str, Any], core_digest: str, expected_generation: int, actor: str) -> None:
200
+ if record["core_digest"] != core_digest:
201
+ raise OwnershipError("core_digest does not match")
202
+ if record["generation"] != expected_generation:
203
+ raise OwnershipError("expected ownership generation does not match")
204
+ if record["source_session_id"] != actor and record.get("consumer_session_id") != actor:
205
+ raise OwnershipError("session is not an ownership actor")
206
+
207
+
208
+ def _public(record: dict[str, Any]) -> dict[str, Any]:
209
+ return {
210
+ "status": record["state"],
211
+ "handoff_id": record["handoff_id"],
212
+ "core_digest": record["core_digest"],
213
+ "task": record["task"],
214
+ "source_session_id": record["source_session_id"],
215
+ "consumer_session_id": record["consumer_session_id"],
216
+ "generation": record["generation"],
217
+ "archive_observation": record["archive_observation"],
218
+ "record_digest": record["integrity"]["record_digest"],
219
+ "updated_at": record["updated_at"],
220
+ }
221
+
222
+
223
+ def quiesce(root: Path, task_path: str, handoff_id: str, core_digest: str, source_session_id: str) -> dict[str, Any]:
224
+ actor, active = _direct_context(root, require_task=True)
225
+ task = _task_snapshot(root, task_path)
226
+ if active.source_type != "session" or active.context_key != actor or active.task_path != task["path"]:
227
+ raise OwnershipError("source task is not the direct current task")
228
+ if _identity(source_session_id, "source_session_id") != actor:
229
+ raise OwnershipError("source_session_id does not match direct session")
230
+ path = _record_path(root, task["id"], handoff_id)
231
+ with _record_lock(path):
232
+ if path.exists():
233
+ raise OwnershipError("ownership record already exists")
234
+ record = _new_record(task, handoff_id, core_digest, actor)
235
+ digest = _write(path, record)
236
+ return {"status": "quiescing", "generation": 0, "record_digest": digest}
237
+
238
+
239
+ def seal(root: Path, task_id: str, handoff_id: str, core_digest: str, expected_generation: int) -> dict[str, Any]:
240
+ actor, _ = _direct_context(root)
241
+ path, record = _load(root, task_id, handoff_id)
242
+ with _record_lock(path):
243
+ record = _decode(path)
244
+ _check(record, core_digest, expected_generation, actor)
245
+ if record["state"] != "quiescing" or record["source_session_id"] != actor:
246
+ raise OwnershipError("ownership record is not sealable by source")
247
+ record["previous_event_digest"] = record["integrity"]["record_digest"]
248
+ record["state"] = "sealed"
249
+ record["generation"] += 1
250
+ digest = _write(path, record)
251
+ return _public(record) | {"record_digest": digest}
252
+
253
+
254
+ def retire(root: Path, task_id: str, handoff_id: str, core_digest: str, expected_generation: int, archive_observation: str) -> dict[str, Any]:
255
+ if archive_observation not in ARCHIVE_OBSERVATIONS:
256
+ raise OwnershipError("archive observation is invalid")
257
+ actor, active = _direct_context(root)
258
+ path, record = _load(root, task_id, handoff_id)
259
+ with _record_lock(path):
260
+ record = _decode(path)
261
+ _check(record, core_digest, expected_generation, actor)
262
+ if record["source_session_id"] != actor or record["state"] not in {"sealed", "retiring"}:
263
+ raise OwnershipError("ownership record is not retireable by source")
264
+ if record["state"] == "sealed":
265
+ if active.task_path != record["task"]["path"]:
266
+ raise OwnershipError("source task is not bound for retirement")
267
+ record["previous_event_digest"] = record["integrity"]["record_digest"]
268
+ record["state"] = "retiring"
269
+ record["archive_observation"] = archive_observation
270
+ record["generation"] += 1
271
+ record["fencing_token"] = secrets.token_hex(16)
272
+ _write(path, record)
273
+ elif record["archive_observation"] != archive_observation:
274
+ raise OwnershipError("archive observation does not match retirement")
275
+
276
+ active = resolve_active_task(root, allow_single_session_fallback=False)
277
+ if active.context_key != actor:
278
+ raise OwnershipError("direct_session_identity_unstable")
279
+ if active.task_path == record["task"]["path"]:
280
+ clear_active_task(root)
281
+ elif active.task_path:
282
+ raise OwnershipError("source task pointer changed during retirement")
283
+ active = resolve_active_task(root, allow_single_session_fallback=False)
284
+ if active.context_key != actor or active.task_path:
285
+ raise OwnershipError("source task retirement could not be verified; recovery_required")
286
+ record["previous_event_digest"] = record["integrity"]["record_digest"]
287
+ record["state"] = "ready"
288
+ record["generation"] += 1
289
+ digest = _write(path, record)
290
+ return _public(record) | {"record_digest": digest}
291
+
292
+
293
+ def claim(root: Path, task_id: str, task_path: str, handoff_id: str, core_digest: str, expected_generation: int) -> dict[str, Any]:
294
+ actor, active = _direct_context(root)
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("expected ownership claim does not match")
300
+ if record["state"] == "claimed" and record.get("consumer_session_id") == actor:
301
+ if active.task_path != record["task"]["path"]:
302
+ raise OwnershipError("claimed consumer task pointer is missing")
303
+ return _public(record)
304
+ if record["state"] == "claiming" and record.get("consumer_session_id") == actor:
305
+ if active.task_path and active.task_path != record["task"]["path"]:
306
+ raise OwnershipError("target task pointer changed during claim; recovery_required")
307
+ if not active.task_path:
308
+ bound = set_active_task(record["task"]["path"], root)
309
+ if not bound:
310
+ raise OwnershipError("target task binding failed; recovery_required")
311
+ active = resolve_active_task(root, allow_single_session_fallback=False)
312
+ if active.context_key != actor or active.task_path != record["task"]["path"]:
313
+ raise OwnershipError("target task binding could not be verified; recovery_required")
314
+ record["previous_event_digest"] = record["integrity"]["record_digest"]
315
+ record["state"] = "claimed"
316
+ record["generation"] += 1
317
+ digest = _write(path, record)
318
+ return _public(record) | {"record_digest": digest}
319
+ if record["generation"] != expected_generation:
320
+ raise OwnershipError("expected ownership claim does not match")
321
+ if record["state"] != "ready":
322
+ raise OwnershipError("ownership record is not claimable")
323
+ if active.task_path:
324
+ raise OwnershipError("target session already has a direct task")
325
+ task = _task_snapshot(root, task_path)
326
+ if task != record["task"]:
327
+ raise OwnershipError("target task does not match ownership record")
328
+ record["previous_event_digest"] = record["integrity"]["record_digest"]
329
+ record["state"] = "claiming"
330
+ record["consumer_session_id"] = actor
331
+ record["consumer_context_key"] = actor
332
+ record["generation"] += 1
333
+ record["fencing_token"] = secrets.token_hex(16)
334
+ _write(path, record)
335
+ bound = set_active_task(task["path"], root)
336
+ if not bound:
337
+ raise OwnershipError("target task binding failed; recovery_required")
338
+ active = resolve_active_task(root, allow_single_session_fallback=False)
339
+ if active.context_key != actor or active.task_path != task["path"]:
340
+ raise OwnershipError("target task binding could not be verified; recovery_required")
341
+ record["previous_event_digest"] = record["integrity"]["record_digest"]
342
+ record["state"] = "claimed"
343
+ record["generation"] += 1
344
+ digest = _write(path, record)
345
+ return _public(record) | {"record_digest": digest}
346
+
347
+
348
+ def consume(root: Path, task_id: str, handoff_id: str, core_digest: str, expected_generation: int) -> dict[str, Any]:
349
+ actor, _ = _direct_context(root, require_task=True)
350
+ path, record = _load(root, task_id, handoff_id)
351
+ with _record_lock(path):
352
+ record = _decode(path)
353
+ if record["state"] == "consumed" and record.get("consumer_session_id") == actor:
354
+ return _public(record)
355
+ _check(record, core_digest, expected_generation, actor)
356
+ if record["state"] != "claimed" or record.get("consumer_session_id") != actor:
357
+ raise OwnershipError("ownership record is not consumable by current consumer")
358
+ record["previous_event_digest"] = record["integrity"]["record_digest"]
359
+ record["state"] = "consumed"
360
+ record["generation"] += 1
361
+ digest = _write(path, record)
362
+ return _public(record) | {"record_digest": digest}
363
+
364
+
365
+ def archive(root: Path, task_id: str, handoff_id: str, core_digest: str, expected_generation: int) -> dict[str, Any]:
366
+ actor, _ = _direct_context(root, require_task=True)
367
+ path, record = _load(root, task_id, handoff_id)
368
+ with _record_lock(path):
369
+ record = _decode(path)
370
+ if record["state"] == "archived" and record.get("consumer_session_id") == actor:
371
+ return _public(record)
372
+ _check(record, core_digest, expected_generation, actor)
373
+ if record["state"] != "consumed" or record.get("consumer_session_id") != actor:
374
+ raise OwnershipError("ownership record is not archiveable by current consumer")
375
+ record["previous_event_digest"] = record["integrity"]["record_digest"]
376
+ record["state"] = "archived"
377
+ record["generation"] += 1
378
+ digest = _write(path, record)
379
+ return _public(record) | {"record_digest": digest}
380
+
381
+
382
+ def status(root: Path | None, task_id: str, handoff_id: str, core_digest: str) -> dict[str, Any]:
383
+ root = (root or get_repo_root()).resolve()
384
+ _, record = _load(root, task_id, handoff_id)
385
+ if record["core_digest"] != core_digest:
386
+ raise OwnershipError("core_digest does not match")
387
+ return _public(record)
388
+
389
+
390
+ def assert_task_mutation_allowed(root: Path, task_path: Path) -> None:
391
+ """Reject ordinary task pointer changes that would bypass ownership fencing."""
392
+ actor = resolve_context_key()
393
+ if not actor:
394
+ return
395
+ try:
396
+ task_id = _safe_task_id(task_path.name)
397
+ except ContinuationError:
398
+ return
399
+ relative = task_path.relative_to(root).as_posix()
400
+ records_dir = root / ".trellis" / ".runtime" / "handoff-ownership" / task_id
401
+ if not records_dir.is_dir() or records_dir.is_symlink():
402
+ return
403
+ for record_path in sorted(records_dir.glob("*.json")):
404
+ if record_path.is_symlink():
405
+ raise OwnershipError("ownership record is unsafe")
406
+ record = _decode(record_path)
407
+ if record["state"] not in STATES:
408
+ continue
409
+ if record["task"]["path"] != relative:
410
+ raise OwnershipError("ownership record task path does not match current task")
411
+ if record.get("consumer_session_id") == actor:
412
+ 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")