@pennixrv/trellis 0.7.0-beta.24 → 0.7.0-beta.25

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.7.0-beta.25",
3
+ "description": "Preserve identity during unbound task recovery",
4
+ "breaking": false,
5
+ "recommendMigrate": false,
6
+ "changelog": "Unbound task projections retain resolved direct session identity. Formal ownership retirement and claim distinguish read-only candidates from direct bindings. Recovery guidance checks identity separately and preserves planning activation gates.",
7
+ "migrations": [],
8
+ "notes": "Run trellis update after upgrading. Refresh a separately selected Marketplace workflow using its explicit immutable ref. Candidate projections remain read-only; foreign-session isolation and ownership fencing remain enforced. No identity injection or automatic task selection is required."
9
+ }
@@ -8,10 +8,21 @@ Resume work on the current task — pick up at the right phase/step in `.trellis
8
8
 
9
9
  ```bash
10
10
  {{PYTHON_CMD}} ./.trellis/scripts/get_context.py
11
+ {{PYTHON_CMD}} ./.trellis/scripts/task.py current --json
11
12
  ```
12
13
 
13
14
  Confirms: current task, git state, recent commits.
14
15
 
16
+ Before routing by status, distinguish identity from task binding. A non-null
17
+ `session_source` confirms available identity; only `source=session:...` confirms
18
+ a direct task binding. For `unbound` / `unbound_ambiguous`, review existing task
19
+ artifacts and select by the user's explicit intent. Exit code 1 for ambiguity
20
+ does not mean identity is absent. Do not guess, duplicate tasks, invent identity,
21
+ or edit runtime pointers. Return a planning candidate to the planning gates below;
22
+ use native `task.py start <task>` only when its activation contract permits it.
23
+ An eligible `analysis_only` candidate remains in planning without start.
24
+ If `session_source` is null, report unavailable identity separately.
25
+
15
26
  ## Step 2: Load the Phase Index
16
27
 
17
28
  ```bash
@@ -694,7 +694,7 @@ def resolve_active_task(
694
694
  active = _active_from_ref(task_ref, repo_root, "session", context_key)
695
695
  if active:
696
696
  return active
697
- unbound = _resolve_unbound_task(repo_root)
697
+ unbound = _resolve_unbound_task(repo_root, context_key)
698
698
  if unbound is not None:
699
699
  return unbound
700
700
  return ActiveTask(None, "none", context_key)
@@ -736,8 +736,8 @@ def _resolve_single_session_fallback(repo_root: Path) -> ActiveTask | None:
736
736
  return _active_from_ref(task_ref, repo_root, "session-fallback", fallback_key)
737
737
 
738
738
 
739
- def _resolve_unbound_task(repo_root: Path) -> ActiveTask | None:
740
- """Expose one developer-owned task when no session pointer exists."""
739
+ def _resolve_unbound_task(repo_root: Path, context_key: str | None = None) -> ActiveTask | None:
740
+ """Expose read-only candidates without discarding a resolved identity."""
741
741
  sessions_dir = _runtime_sessions_dir(repo_root)
742
742
  if sessions_dir.is_dir():
743
743
  session_files = sorted(sessions_dir.glob("*.json"))
@@ -764,8 +764,8 @@ def _resolve_unbound_task(repo_root: Path) -> ActiveTask | None:
764
764
  for task in candidates
765
765
  ))
766
766
  if len(task_paths) == 1:
767
- return ActiveTask(task_paths[0], "unbound", None)
768
- return ActiveTask(None, "unbound_ambiguous", None, candidate_paths=task_paths)
767
+ return ActiveTask(task_paths[0], "unbound", context_key)
768
+ return ActiveTask(None, "unbound_ambiguous", context_key, candidate_paths=task_paths)
769
769
 
770
770
 
771
771
  def _utc_now() -> str:
@@ -834,7 +834,7 @@ def clear_active_task(
834
834
  return ActiveTask(None, "none")
835
835
 
836
836
  previous = resolve_active_task(repo_root, platform_input, platform)
837
- if not previous.task_path or not previous.context_key:
837
+ if previous.source_type != "session" or not previous.task_path or not previous.context_key:
838
838
  return previous
839
839
 
840
840
  context_path = _context_path(repo_root, previous.context_key)
@@ -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, clear_active_task_for_context, resolve_active_task, resolve_context_key, set_active_task
13
+ from .active_task import ActiveTask, 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,
@@ -83,6 +83,9 @@ def _direct_context(root: Path, require_task: bool = False) -> tuple[str, Any]:
83
83
  raise OwnershipError("session_fallback_untrusted")
84
84
  if active.context_key != context_key:
85
85
  raise OwnershipError("direct_session_identity_unstable")
86
+ if active.source_type in {"unbound", "unbound_ambiguous"}:
87
+ # Candidate paths are recovery context, never an owned session pointer.
88
+ active = ActiveTask(None, "none", context_key)
86
89
  if require_task and not active.task_path:
87
90
  raise OwnershipError("no_direct_current_task")
88
91
  return context_key, active
@@ -273,14 +276,14 @@ def retire(root: Path, task_id: str, handoff_id: str, core_digest: str, expected
273
276
  elif record["archive_observation"] != archive_observation:
274
277
  raise OwnershipError("archive observation does not match retirement")
275
278
 
276
- active = resolve_active_task(root, allow_single_session_fallback=False)
279
+ _, active = _direct_context(root)
277
280
  if active.context_key != actor:
278
281
  raise OwnershipError("direct_session_identity_unstable")
279
282
  if active.task_path == record["task"]["path"]:
280
283
  clear_active_task(root)
281
284
  elif active.task_path:
282
285
  raise OwnershipError("source task pointer changed during retirement")
283
- active = resolve_active_task(root, allow_single_session_fallback=False)
286
+ _, active = _direct_context(root)
284
287
  if active.context_key != actor or active.task_path:
285
288
  raise OwnershipError("source task retirement could not be verified; recovery_required")
286
289
  record["previous_event_digest"] = record["integrity"]["record_digest"]
@@ -334,7 +337,7 @@ def claim(root: Path, task_id: str, task_path: str, handoff_id: str, core_digest
334
337
  bound = set_active_task(record["task"]["path"], root)
335
338
  if not bound:
336
339
  raise OwnershipError("target task binding failed; recovery_required")
337
- active = resolve_active_task(root, allow_single_session_fallback=False)
340
+ _, active = _direct_context(root)
338
341
  if active.context_key != actor or active.task_path != record["task"]["path"]:
339
342
  raise OwnershipError("target task binding could not be verified; recovery_required")
340
343
  record["previous_event_digest"] = record["integrity"]["record_digest"]
@@ -361,7 +364,7 @@ def claim(root: Path, task_id: str, task_path: str, handoff_id: str, core_digest
361
364
  bound = set_active_task(task["path"], root)
362
365
  if not bound:
363
366
  raise OwnershipError("target task binding failed; recovery_required")
364
- active = resolve_active_task(root, allow_single_session_fallback=False)
367
+ _, active = _direct_context(root)
365
368
  if active.context_key != actor or active.task_path != task["path"]:
366
369
  raise OwnershipError("target task binding could not be verified; recovery_required")
367
370
  record["previous_event_digest"] = record["integrity"]["record_digest"]
@@ -200,17 +200,17 @@ Complex task: ask the user if you can create a Trellis task and enter the planni
200
200
  <!-- Per-turn breadcrumb: shown when one resumable task exists without a direct session binding. -->
201
201
 
202
202
  [workflow-state:unbound_task]
203
- An existing task is assigned to the current developer, but this shell has no direct Trellis session binding. Do not create a duplicate task. Continue reading and working from the existing task artifacts; before any lifecycle write or closure, run the native `task.py start <task>` command once a direct session identity is available. Never edit `.trellis/.runtime/sessions/` manually.
203
+ An existing developer-owned task has no direct session binding. Inspect native `task.py current --json`: a non-null `session_source` means direct identity is already available; an unbound source still means no task binding. Select the existing task from the user's explicit intent; do not guess or create a duplicate. Resume its planning/status gates first. Run native `task.py start <task>` only when its activation contract permits it, after the Planning Seal and implementation authorization for a planning change-bearing task. An eligible `analysis_only` task stays in planning without start. If identity is absent, report that separately; never invent an identity or edit runtime pointers.
204
204
  [/workflow-state:unbound_task]
205
205
 
206
206
  <!-- Per-turn breadcrumb shown when multiple resumable tasks exist without a direct session binding. -->
207
207
 
208
208
  [workflow-state:unbound_ambiguous]
209
- Multiple active tasks belong to the current developer, but this shell has no direct Trellis session binding. Do not guess or create a duplicate task. Review the listed candidates and run `python3 ./.trellis/scripts/task.py start <task>` with the intended task once a direct session identity is available.
209
+ Multiple developer-owned tasks have no direct session binding. Inspect native `task.py current --json`: a non-null `session_source` means direct identity is already available; an unbound source still means no task binding. Select the existing task from the user's explicit intent; do not guess or create a duplicate. Resume its planning/status gates first. Run native `task.py start <task>` only when its activation contract permits it, after the Planning Seal and implementation authorization for a planning change-bearing task. An eligible `analysis_only` task stays in planning without start. If identity is absent, report that separately; never invent an identity or edit runtime pointers.
210
210
  [/workflow-state:unbound_ambiguous]
211
211
 
212
212
  [workflow-state:unbound_ambiguous-inline]
213
- Multiple active tasks belong to the current developer, but this Codex session has no direct Trellis session binding. Do not guess or create a duplicate task. Review the listed candidates and run `python3 ./.trellis/scripts/task.py start <task>` with the intended task once a direct session identity is available.
213
+ Multiple developer-owned tasks have no direct session binding. Inspect native `task.py current --json`: a non-null `session_source` means direct identity is already available; an unbound source still means no task binding. Select the existing task from the user's explicit intent; do not guess or create a duplicate. Resume its planning/status gates first. Run native `task.py start <task>` only when its activation contract permits it, after the Planning Seal and implementation authorization for a planning change-bearing task. An eligible `analysis_only` task stays in planning without start. If identity is absent, report that separately; never invent an identity or edit runtime pointers.
214
214
  [/workflow-state:unbound_ambiguous-inline]
215
215
 
216
216
  <!-- Per-turn breadcrumb: shown when the active task record cannot be read. -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pennixrv/trellis",
3
- "version": "0.7.0-beta.24",
3
+ "version": "0.7.0-beta.25",
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.7.0-beta.24"
37
+ "@pennixrv/trellis-core": "0.7.0-beta.25"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@eslint/js": "^9.18.0",