@pennixrv/trellis 0.6.25 → 0.6.26

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.25",
3
+ "description": "Prevents a known target session from inheriting a foreign active-task pointer.",
4
+ "breaking": false,
5
+ "recommendMigrate": false,
6
+ "changelog": "**Bug fixes:**\n- Keeps session-scoped active-task resolution isolated when a known session has no usable pointer.\n- Preserves identity-less single-session fallback for compatible sub-agent environments.\n\n**Internal:**\n- Restores the missing release manifest so adjacent-version `trellis update` paths remain complete.",
7
+ "migrations": [],
8
+ "notes": "No project migration is required. Existing projects can run the normal `trellis update` flow."
9
+ }
@@ -36,6 +36,7 @@ export declare const commonTasks: string;
36
36
  export declare const commonTaskContext: string;
37
37
  export declare const commonTaskStore: string;
38
38
  export declare const commonSessionContext: string;
39
+ export declare const commonContinuationRecord: string;
39
40
  export declare const commonPackagesContext: string;
40
41
  export declare const commonWorkflowPhase: string;
41
42
  export declare const commonTrellisConfig: string;
@@ -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,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,CAuCnD;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,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"}
@@ -46,6 +46,7 @@ export const commonTasks = readTemplate("scripts/common/tasks.py");
46
46
  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
+ export const commonContinuationRecord = readTemplate("scripts/common/continuation_record.py");
49
50
  export const commonPackagesContext = readTemplate("scripts/common/packages_context.py");
50
51
  export const commonWorkflowPhase = readTemplate("scripts/common/workflow_phase.py");
51
52
  export const commonTrellisConfig = readTemplate("scripts/common/trellis_config.py");
@@ -95,6 +96,7 @@ export function getAllScripts() {
95
96
  scripts.set("common/task_context.py", commonTaskContext);
96
97
  scripts.set("common/task_store.py", commonTaskStore);
97
98
  scripts.set("common/session_context.py", commonSessionContext);
99
+ scripts.set("common/continuation_record.py", commonContinuationRecord);
98
100
  scripts.set("common/packages_context.py", commonPackagesContext);
99
101
  scripts.set("common/workflow_phase.py", commonWorkflowPhase);
100
102
  scripts.set("common/trellis_config.py", commonTrellisConfig);
@@ -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,439 @@
1
+ """Bounded, task-scoped Continuation Record runtime projection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import os
8
+ import re
9
+ import tempfile
10
+ from contextlib import contextmanager
11
+ from datetime import datetime, timezone
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from .active_task import resolve_active_task
16
+ from .git import run_git
17
+ from .io import read_json_checked
18
+ from .paths import FILE_TASK_JSON, get_repo_root
19
+
20
+
21
+ SCHEMA_VERSION = 1
22
+ KIND = "trellis-continuation-record"
23
+ STATUS_ABSENT = "absent"
24
+ STATUS_READY = "ready"
25
+ STATUS_STALE = "stale"
26
+ STATUS_WITHHELD = "withheld"
27
+ MAX_RECORD_BYTES = 128 * 1024
28
+ MAX_REQUEST_BYTES = 64 * 1024
29
+ MAX_TEXT_BYTES = 4096
30
+ MAX_LIST = 32
31
+ MAX_EVIDENCE = 32
32
+ MAX_EVIDENCE_BYTES = 2 * 1024 * 1024
33
+ MAX_TASK_FILES = 512
34
+ MAX_TASK_FILE_BYTES = 2 * 1024 * 1024
35
+ SAFE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
36
+ SHA256 = re.compile(r"^sha256:[0-9a-f]{64}$")
37
+ SECRET = re.compile(
38
+ r"(?:-----BEGIN [A-Z0-9 ]+ PRIVATE KEY-----|bearer\s+[A-Za-z0-9._~+/=-]{20,}|"
39
+ r"\b(?:sk|th)-[A-Za-z0-9_-]{20,}|(?:api[_-]?key|password|secret)\s*[:=]\s*\S+)",
40
+ re.IGNORECASE,
41
+ )
42
+
43
+
44
+ class ContinuationError(ValueError):
45
+ """Raised when a Continuation Record cannot be safely read or written."""
46
+
47
+
48
+ class NoDirectCurrentTask(ContinuationError):
49
+ """Raised when the current session has no directly bound active task."""
50
+
51
+
52
+ def _canonical(value: Any) -> bytes:
53
+ return json.dumps(value, ensure_ascii=True, sort_keys=True, separators=(",", ":")).encode("utf-8")
54
+
55
+
56
+ def _digest(value: Any) -> str:
57
+ return "sha256:" + hashlib.sha256(_canonical(value)).hexdigest()
58
+
59
+
60
+ def _text(value: Any, label: str, maximum: int = MAX_TEXT_BYTES) -> str:
61
+ if not isinstance(value, str) or not value.strip() or len(value.encode("utf-8")) > maximum:
62
+ raise ContinuationError(f"{label} is invalid")
63
+ if any(ord(char) < 32 and char not in "\n\t" for char in value) or SECRET.search(value):
64
+ raise ContinuationError(f"{label} is unsafe")
65
+ return value
66
+
67
+
68
+ def _list(value: Any, label: str) -> list[str]:
69
+ if not isinstance(value, list) or len(value) > MAX_LIST:
70
+ raise ContinuationError(f"{label} is invalid")
71
+ return [_text(item, f"{label}[{index}]") for index, item in enumerate(value)]
72
+
73
+
74
+ def _safe_task_id(task_id: Any) -> str:
75
+ value = _text(task_id, "task.id", 128)
76
+ if not SAFE_ID.fullmatch(value) or "/" in value or "\\" in value or value in {".", ".."}:
77
+ raise ContinuationError("task.id is unsafe")
78
+ return value
79
+
80
+
81
+ def _record_path(root: Path, task_id: str) -> Path:
82
+ safe_id = _safe_task_id(task_id)
83
+ runtime = root / ".trellis" / ".runtime"
84
+ records = runtime / "continuation-records"
85
+ task_dir = records / safe_id
86
+ if runtime.is_symlink() or records.is_symlink() or task_dir.is_symlink():
87
+ raise ContinuationError("continuation record directory is unsafe")
88
+ return task_dir / "record.json"
89
+
90
+
91
+ def _regular(path: Path, label: str) -> Path:
92
+ if path.is_symlink() or not path.is_file():
93
+ raise ContinuationError(f"{label} is not a regular file")
94
+ return path
95
+
96
+
97
+ def _project_file(root: Path, value: Any, label: str) -> Path:
98
+ relative = _text(value, label, 1024)
99
+ candidate = Path(relative)
100
+ if candidate.is_absolute() or ".." in candidate.parts or relative.startswith(".trellis/.runtime/"):
101
+ raise ContinuationError(f"{label} must be project-relative")
102
+ cursor = root
103
+ for part in candidate.parts:
104
+ cursor /= part
105
+ if cursor.is_symlink():
106
+ raise ContinuationError(f"{label} contains a symbolic path")
107
+ resolved = (root / candidate).resolve()
108
+ try:
109
+ resolved.relative_to(root.resolve())
110
+ except ValueError as exc:
111
+ raise ContinuationError(f"{label} escapes project") from exc
112
+ return resolved
113
+
114
+
115
+ def _file_digest(path: Path, maximum: int = MAX_EVIDENCE_BYTES) -> tuple[str, int]:
116
+ hasher = hashlib.sha256()
117
+ size = 0
118
+ with _regular(path, "evidence file").open("rb") as handle:
119
+ while chunk := handle.read(256 * 1024):
120
+ size += len(chunk)
121
+ if size > maximum:
122
+ raise ContinuationError("source file is too large")
123
+ hasher.update(chunk)
124
+ return "sha256:" + hasher.hexdigest(), size
125
+
126
+
127
+ def _task_source(root: Path) -> dict[str, Any]:
128
+ active = resolve_active_task(root)
129
+ if not active.task_path:
130
+ raise NoDirectCurrentTask("no_direct_current_task")
131
+ if active.source_type == "session-fallback":
132
+ raise ContinuationError("session_fallback_untrusted")
133
+ if active.source_type != "session" or not active.context_key:
134
+ raise NoDirectCurrentTask("no_direct_current_task")
135
+ if active.stale:
136
+ raise ContinuationError("current_session_pointer_stale")
137
+ task_path = _project_file(root, active.task_path, "task path")
138
+ task_file = _regular(task_path / FILE_TASK_JSON, "task.json")
139
+ data, reason = read_json_checked(task_file)
140
+ if data is None:
141
+ raise ContinuationError(f"task.json is {reason}")
142
+ task_id = _safe_task_id(data.get("id") or data.get("name"))
143
+ entries: list[dict[str, Any]] = []
144
+ for item in sorted(task_path.rglob("*")):
145
+ if item.is_symlink() or not item.is_file():
146
+ continue
147
+ if len(entries) >= MAX_TASK_FILES:
148
+ raise ContinuationError("task has too many material files")
149
+ item_digest, size = _file_digest(item, MAX_TASK_FILE_BYTES)
150
+ entries.append({"path": item.relative_to(root).as_posix(), "bytes": size, "sha256": item_digest})
151
+ return {
152
+ "id": task_id,
153
+ "path": active.task_path,
154
+ "status": _text(data.get("status"), "task.status", 64),
155
+ "material_digest": _digest(entries),
156
+ "context_key": active.context_key,
157
+ }
158
+
159
+
160
+ def _git_source(root: Path) -> dict[str, Any]:
161
+ def run(*args: str) -> str:
162
+ rc, out, _ = run_git(list(args), cwd=root, timeout=20)
163
+ if rc:
164
+ raise ContinuationError("git source snapshot failed")
165
+ return out.strip()
166
+
167
+ dirty = [
168
+ line
169
+ for line in run("status", "--porcelain=v1").splitlines()
170
+ if line and ".trellis/.runtime/continuation-records/" not in line[3:]
171
+ ]
172
+ branch = run("branch", "--show-current")
173
+ head = run("rev-parse", "HEAD")
174
+ return {
175
+ "branch": branch or None,
176
+ "head": _text(head, "git.head", 128),
177
+ "worktree_state": "dirty" if dirty else "clean",
178
+ "dirty_paths_digest": _digest(dirty),
179
+ }
180
+
181
+
182
+ def _evidence(root: Path, values: Any) -> list[dict[str, Any]]:
183
+ if not isinstance(values, list) or len(values) > MAX_EVIDENCE:
184
+ raise ContinuationError("evidence must be a bounded list")
185
+ result = []
186
+ seen: set[str] = set()
187
+ for item in values:
188
+ path = _project_file(root, item, "evidence path")
189
+ relative = path.relative_to(root).as_posix()
190
+ if relative in seen:
191
+ raise ContinuationError("evidence paths must be unique")
192
+ seen.add(relative)
193
+ file_digest, size = _file_digest(path)
194
+ result.append({"path": relative, "bytes": size, "sha256": file_digest})
195
+ return result
196
+
197
+
198
+ def _source(root: Path, task: dict[str, Any], evidence: list[dict[str, Any]]) -> dict[str, Any]:
199
+ git = _git_source(root)
200
+ source = {
201
+ "task": {key: task[key] for key in ("id", "path", "status", "material_digest")},
202
+ "git": git,
203
+ "evidence": evidence,
204
+ }
205
+ source["source_digest"] = _digest(source)
206
+ return source
207
+
208
+
209
+ def _request(root: Path, path: Path) -> tuple[dict[str, Any], list[dict[str, Any]]]:
210
+ _regular(path, "continuity request")
211
+ if path.stat().st_size > MAX_REQUEST_BYTES:
212
+ raise ContinuationError("continuity request is too large")
213
+ try:
214
+ value = json.loads(path.read_text(encoding="utf-8"))
215
+ except (OSError, UnicodeError, json.JSONDecodeError) as exc:
216
+ raise ContinuationError("continuity request is invalid") from exc
217
+ if not isinstance(value, dict) or set(value) != {"objective", "decisions", "completed", "open_items", "blockers", "next_safe_action", "non_transferable_operations", "evidence"}:
218
+ raise ContinuationError("continuity request fields are invalid")
219
+ content = {key: _text(value[key], f"content.{key}") if key in {"objective", "next_safe_action"} else _list(value[key], f"content.{key}") for key in value if key != "evidence"}
220
+ return content, _evidence(root, value["evidence"])
221
+
222
+
223
+ def _request_path(root: Path, path: Path) -> Path:
224
+ try:
225
+ relative = path.relative_to(root)
226
+ except ValueError as exc:
227
+ raise ContinuationError("continuity request must be project-relative") from exc
228
+ return _project_file(root, relative.as_posix(), "continuity request")
229
+
230
+
231
+ def _expected_digest(value: str) -> str:
232
+ if value == "absent" or SHA256.fullmatch(value):
233
+ return value
234
+ raise ContinuationError("expected record digest is invalid")
235
+
236
+
237
+ def _ensure_record_parent(path: Path) -> None:
238
+ if path.parent.is_symlink() or path.parent.parent.is_symlink():
239
+ raise ContinuationError("continuation record directory is unsafe")
240
+ path.parent.mkdir(parents=True, exist_ok=True)
241
+ if path.parent.is_symlink() or path.parent.parent.is_symlink():
242
+ raise ContinuationError("continuation record directory is unsafe")
243
+ os.chmod(path.parent.parent, 0o700)
244
+ os.chmod(path.parent, 0o700)
245
+
246
+
247
+ @contextmanager
248
+ def _record_lock(path: Path):
249
+ _ensure_record_parent(path)
250
+ lock = path.parent / ".lock"
251
+ if lock.is_symlink():
252
+ raise ContinuationError("continuation record lock is unsafe")
253
+ # ponytail: one advisory lock per task record; a lock service is unnecessary unless distributed writers are introduced.
254
+ with lock.open("a+", encoding="utf-8") as handle:
255
+ try:
256
+ import fcntl
257
+ fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
258
+ except (ImportError, OSError) as exc:
259
+ raise ContinuationError("continuation record lock unavailable") from exc
260
+ try:
261
+ yield
262
+ finally:
263
+ fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
264
+
265
+
266
+ def _record_digest(record: dict[str, Any]) -> str:
267
+ return _digest({key: value for key, value in record.items() if key != "integrity"})
268
+
269
+
270
+ def _decode(path: Path) -> dict[str, Any]:
271
+ _regular(path, "continuation record")
272
+ if path.stat().st_size > MAX_RECORD_BYTES:
273
+ raise ContinuationError("continuation record is too large")
274
+ data, reason = read_json_checked(path)
275
+ if data is None:
276
+ raise ContinuationError(f"continuation record is {reason}")
277
+ expected = {"schema_version", "kind", "task", "source", "content", "revision", "updated_at", "integrity"}
278
+ if set(data) != expected or data["schema_version"] != SCHEMA_VERSION or data["kind"] != KIND:
279
+ raise ContinuationError("continuation record schema is unsupported")
280
+ task = data["task"]
281
+ if not isinstance(task, dict) or set(task) != {"id", "path", "status", "material_digest"}:
282
+ raise ContinuationError("continuation record task is invalid")
283
+ _safe_task_id(task["id"])
284
+ task_path = _text(task["path"], "task.path", 1024)
285
+ task_path_value = Path(task_path)
286
+ if task_path_value.is_absolute() or ".." in task_path_value.parts or not task_path.startswith(".trellis/tasks/"):
287
+ raise ContinuationError("task.path is unsafe")
288
+ _text(task["status"], "task.status", 64)
289
+ if not SHA256.fullmatch(task["material_digest"]):
290
+ raise ContinuationError("task.material_digest is invalid")
291
+ source = data["source"]
292
+ if not isinstance(source, dict) or set(source) != {"task", "git", "evidence", "source_digest"}:
293
+ raise ContinuationError("continuation record source is invalid")
294
+ if source["task"] != task:
295
+ raise ContinuationError("continuation record task source is invalid")
296
+ git = source["git"]
297
+ if not isinstance(git, dict) or set(git) != {"branch", "head", "worktree_state", "dirty_paths_digest"}:
298
+ raise ContinuationError("continuation record git source is invalid")
299
+ if git["branch"] is not None:
300
+ _text(git["branch"], "git.branch", 512)
301
+ _text(git["head"], "git.head", 128)
302
+ if git["worktree_state"] not in {"clean", "dirty"} or not SHA256.fullmatch(git["dirty_paths_digest"]):
303
+ raise ContinuationError("continuation record git source is invalid")
304
+ if not isinstance(source["evidence"], list) or len(source["evidence"]) > MAX_EVIDENCE:
305
+ raise ContinuationError("continuation record evidence is invalid")
306
+ for index, item in enumerate(source["evidence"]):
307
+ if not isinstance(item, dict) or set(item) != {"path", "bytes", "sha256"}:
308
+ raise ContinuationError("continuation record evidence item is invalid")
309
+ _text(item["path"], f"evidence[{index}].path", 1024)
310
+ if not isinstance(item["bytes"], int) or item["bytes"] < 0 or not SHA256.fullmatch(item["sha256"]):
311
+ raise ContinuationError("continuation record evidence item is invalid")
312
+ if not SHA256.fullmatch(source["source_digest"]):
313
+ raise ContinuationError("source digest is invalid")
314
+ if source["source_digest"] != _digest({key: value for key, value in source.items() if key != "source_digest"}):
315
+ raise ContinuationError("source digest does not match")
316
+ content = data["content"]
317
+ if not isinstance(content, dict) or set(content) != {"objective", "decisions", "completed", "open_items", "blockers", "next_safe_action", "non_transferable_operations"}:
318
+ raise ContinuationError("continuation record content is invalid")
319
+ _text(content["objective"], "content.objective")
320
+ _text(content["next_safe_action"], "content.next_safe_action")
321
+ for key in ("decisions", "completed", "open_items", "blockers", "non_transferable_operations"):
322
+ _list(content[key], f"content.{key}")
323
+ if not isinstance(data["revision"], int) or data["revision"] < 1:
324
+ raise ContinuationError("revision is invalid")
325
+ _text(data["updated_at"], "updated_at", 128)
326
+ integrity = data["integrity"]
327
+ if not isinstance(integrity, dict) or set(integrity) != {"record_digest"} or not SHA256.fullmatch(integrity["record_digest"]):
328
+ raise ContinuationError("record digest is invalid")
329
+ if integrity["record_digest"] != _record_digest(data):
330
+ raise ContinuationError("record digest does not match")
331
+ return data
332
+
333
+
334
+ def status(root: Path | None = None) -> dict[str, Any]:
335
+ root = (root or get_repo_root()).resolve()
336
+ try:
337
+ task = _task_source(root)
338
+ except NoDirectCurrentTask:
339
+ return {"status": STATUS_ABSENT, "reason": "no_direct_current_task"}
340
+ except ContinuationError as exc:
341
+ return {"status": STATUS_WITHHELD, "reason": str(exc)}
342
+ try:
343
+ path = _record_path(root, task["id"])
344
+ except ContinuationError as exc:
345
+ return {"status": STATUS_WITHHELD, "reason": str(exc)}
346
+ if not path.exists():
347
+ return {"status": STATUS_ABSENT, "task": {key: task[key] for key in ("id", "path", "status")}}
348
+ try:
349
+ record = _decode(path)
350
+ current = _source(root, task, _evidence(root, [item["path"] for item in record["source"]["evidence"]]))
351
+ state = STATUS_READY if record["source"] == current else STATUS_STALE
352
+ result = {"status": state, "task": record["task"], "revision": record["revision"], "record_digest": record["integrity"]["record_digest"]}
353
+ if state == STATUS_READY:
354
+ result["content"] = record["content"]
355
+ else:
356
+ result["reason"] = "source_drift"
357
+ return result
358
+ except (ContinuationError, OSError) as exc:
359
+ return {"status": STATUS_WITHHELD, "task": {key: task[key] for key in ("id", "path", "status")}, "reason": str(exc)}
360
+
361
+
362
+ def seal(root: Path, request_path: Path, expected: str) -> dict[str, Any]:
363
+ expected = _expected_digest(expected)
364
+ request_path = _request_path(root, request_path)
365
+ task = _task_source(root)
366
+ path = _record_path(root, task["id"])
367
+ with _record_lock(path):
368
+ task = _task_source(root)
369
+ current_digest: str | None = None
370
+ current: dict[str, Any] | None = None
371
+ if path.exists():
372
+ current = _decode(path)
373
+ current_digest = current["integrity"]["record_digest"]
374
+ current_source = _source(root, task, _evidence(root, [item["path"] for item in current["source"]["evidence"]]))
375
+ if current["source"] != current_source:
376
+ raise ContinuationError("existing continuation record source drifted")
377
+ if expected != "absent" and expected != current_digest:
378
+ raise ContinuationError("expected record digest does not match")
379
+ if expected == "absent" and current_digest is not None:
380
+ raise ContinuationError("record already exists")
381
+ content, evidence = _request(root, request_path)
382
+ source = _source(root, task, evidence)
383
+ current_task = _task_source(root)
384
+ if task != current_task or source != _source(root, current_task, _evidence(root, [item["path"] for item in evidence])):
385
+ raise ContinuationError("continuation source changed while sealing")
386
+ record: dict[str, Any] = {
387
+ "schema_version": SCHEMA_VERSION,
388
+ "kind": KIND,
389
+ "task": {key: task[key] for key in ("id", "path", "status", "material_digest")},
390
+ "source": source,
391
+ "content": content,
392
+ "revision": 1 if current is None else current["revision"] + 1,
393
+ "updated_at": datetime.now(timezone.utc).isoformat(timespec="microseconds").replace("+00:00", "Z"),
394
+ }
395
+ record["integrity"] = {"record_digest": _record_digest(record)}
396
+ encoded = json.dumps(record, ensure_ascii=True, indent=2, sort_keys=True) + "\n"
397
+ if len(encoded.encode("utf-8")) > MAX_RECORD_BYTES:
398
+ raise ContinuationError("continuation record is too large")
399
+ temporary: Path | None = None
400
+ try:
401
+ with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=path.parent, delete=False) as handle:
402
+ temporary = Path(handle.name)
403
+ handle.write(encoded)
404
+ handle.flush()
405
+ os.fsync(handle.fileno())
406
+ os.chmod(temporary, 0o600)
407
+ os.replace(temporary, path)
408
+ directory_fd = os.open(path.parent, os.O_RDONLY)
409
+ try:
410
+ os.fsync(directory_fd)
411
+ finally:
412
+ os.close(directory_fd)
413
+ except OSError as exc:
414
+ if temporary is not None:
415
+ temporary.unlink(missing_ok=True)
416
+ raise ContinuationError("continuation record atomic write failed") from exc
417
+ return {"status": STATUS_READY, "revision": record["revision"], "record_digest": record["integrity"]["record_digest"]}
418
+
419
+
420
+ def clear(root: Path, expected: str) -> dict[str, Any]:
421
+ expected = _expected_digest(expected)
422
+ task = _task_source(root)
423
+ path = _record_path(root, task["id"])
424
+ with _record_lock(path):
425
+ if not path.exists():
426
+ if expected == "absent":
427
+ return {"status": STATUS_ABSENT}
428
+ raise ContinuationError("record is absent")
429
+ record = _decode(path)
430
+ actual = record["integrity"]["record_digest"]
431
+ if actual != expected:
432
+ raise ContinuationError("expected record digest does not match")
433
+ path.unlink()
434
+ directory_fd = os.open(path.parent, os.O_RDONLY)
435
+ try:
436
+ os.fsync(directory_fd)
437
+ finally:
438
+ os.close(directory_fd)
439
+ return {"status": STATUS_ABSENT, "cleared": actual}
@@ -22,6 +22,7 @@ import sys
22
22
  from pathlib import Path
23
23
 
24
24
  from .active_task import resolve_context_key
25
+ from .continuation_record import status as continuity_status
25
26
  from .config import get_git_packages
26
27
  from .git import run_git
27
28
  from .packages_context import get_packages_section
@@ -558,6 +559,8 @@ def get_context_json(repo_root: Path | None = None) -> dict:
558
559
  if pkg_git_info:
559
560
  result["packageGit"] = pkg_git_info
560
561
 
562
+ result["continuity"] = continuity_status(repo_root)
563
+
561
564
  return result
562
565
 
563
566
 
@@ -644,6 +647,8 @@ def get_context_text(repo_root: Path | None = None) -> str:
644
647
  lines.append("[!] This task has prd.md - read it for task details")
645
648
  else:
646
649
  lines.append("(none)")
650
+ continuity = continuity_status(repo_root)
651
+ lines.append(f"Continuation Record: {continuity['status']}")
647
652
  lines.append("")
648
653
 
649
654
  # Active tasks
@@ -797,6 +802,8 @@ def get_context_record_json(repo_root: Path | None = None) -> dict:
797
802
  if pkg_git_info:
798
803
  result["packageGit"] = pkg_git_info
799
804
 
805
+ result["continuity"] = continuity_status(repo_root)
806
+
800
807
  return result
801
808
 
802
809
 
@@ -74,6 +74,13 @@ from common.task_context import (
74
74
  cmd_list_context,
75
75
  curated_entry_count,
76
76
  )
77
+ from common.continuation_record import (
78
+ STATUS_WITHHELD,
79
+ ContinuationError,
80
+ clear as clear_continuity,
81
+ seal as seal_continuity,
82
+ status as continuity_status,
83
+ )
77
84
 
78
85
 
79
86
  # =============================================================================
@@ -349,6 +356,32 @@ def cmd_current(args: argparse.Namespace) -> int:
349
356
  return 1
350
357
 
351
358
 
359
+ def cmd_continuity(args: argparse.Namespace) -> int:
360
+ """Read or explicitly mutate the current task's Continuation Record."""
361
+ repo_root = get_repo_root()
362
+ try:
363
+ if args.continuity_command == "status":
364
+ result = continuity_status(repo_root)
365
+ print(json.dumps(result, ensure_ascii=False, sort_keys=True))
366
+ return 0
367
+ if not getattr(args, "explicit_user_request", False):
368
+ print(colored("Error: continuity writes require --explicit-user-request", Colors.RED), file=sys.stderr)
369
+ return 2
370
+ if args.continuity_command == "seal":
371
+ request = Path(args.request)
372
+ if request.is_absolute() or ".." in request.parts:
373
+ print(colored("Error: continuity request must be project-relative", Colors.RED), file=sys.stderr)
374
+ return 2
375
+ result = seal_continuity(repo_root, repo_root / request, args.expected)
376
+ else:
377
+ result = clear_continuity(repo_root, args.expected)
378
+ print(json.dumps(result, ensure_ascii=False, sort_keys=True))
379
+ return 0
380
+ except (ContinuationError, OSError) as exc:
381
+ print(json.dumps({"status": STATUS_WITHHELD, "reason": str(exc)}, ensure_ascii=False, sort_keys=True))
382
+ return 2
383
+
384
+
352
385
  # =============================================================================
353
386
  # Command: list
354
387
  # =============================================================================
@@ -697,6 +730,19 @@ def main() -> int:
697
730
  p_current.add_argument("--json", action="store_true",
698
731
  help="Output machine-readable JSON")
699
732
 
733
+ # continuity
734
+ p_continuity = subparsers.add_parser("continuity", help="Read or explicitly update the current task Continuation Record")
735
+ continuity_sub = p_continuity.add_subparsers(dest="continuity_command", required=True)
736
+ continuity_status_parser = continuity_sub.add_parser("status", help="Show read-only Continuation Record status")
737
+ continuity_status_parser.add_argument("--json", action="store_true", help="Output machine-readable JSON")
738
+ continuity_seal = continuity_sub.add_parser("seal", help="Write a Continuation Record from an explicit request")
739
+ continuity_seal.add_argument("--request", required=True, type=Path, help="Project-relative bounded request JSON")
740
+ continuity_seal.add_argument("--expected", required=True, help="absent or the current record digest")
741
+ continuity_seal.add_argument("--explicit-user-request", action="store_true", help="Required write confirmation")
742
+ continuity_clear = continuity_sub.add_parser("clear", help="Clear the current Continuation Record")
743
+ continuity_clear.add_argument("--expected", required=True, help="The current record digest or absent")
744
+ continuity_clear.add_argument("--explicit-user-request", action="store_true", help="Required write confirmation")
745
+
700
746
  # finish
701
747
  subparsers.add_parser("finish", help="Clear active task")
702
748
 
@@ -777,6 +823,7 @@ def main() -> int:
777
823
  "list-context": cmd_list_context,
778
824
  "start": cmd_start,
779
825
  "current": cmd_current,
826
+ "continuity": cmd_continuity,
780
827
  "finish": cmd_finish,
781
828
  "set-branch": cmd_set_branch,
782
829
  "set-base-branch": cmd_set_base_branch,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pennixrv/trellis",
3
- "version": "0.6.25",
3
+ "version": "0.6.26",
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.25"
37
+ "@pennixrv/trellis-core": "0.6.26"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@eslint/js": "^9.18.0",