@pennixrv/trellis 0.6.39 → 0.6.41

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.
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "version": "0.6.39",
3
- "description": "Adds direct completion guidance for explicitly classified analysis-only tasks.",
3
+ "description": "Restores active-task visibility and adds direct completion guidance for analysis-only tasks.",
4
4
  "breaking": false,
5
5
  "recommendMigrate": false,
6
- "changelog": "**Feature:**\n- Analysis-only research, audit, and design tasks can complete declared evidence work directly from planning and archive without entering implementation.",
6
+ "changelog": "**Fixes:**\n- Projects one unique developer-owned resumable task as read-only workflow context when no session pointer exists.\n- Analysis-only research, audit, and design tasks can complete declared evidence work directly from planning and archive without entering implementation.",
7
7
  "migrations": [],
8
8
  "notes": "No project migration is required. Existing projects can run the normal trellis update flow."
9
9
  }
@@ -0,0 +1,9 @@
1
+ {
2
+ "version": "0.6.40",
3
+ "description": "Restores active-task visibility when a session pointer is unavailable.",
4
+ "breaking": false,
5
+ "recommendMigrate": false,
6
+ "changelog": "**Fix:**\n- Projects one unique developer-owned resumable task as read-only workflow context when no Trellis session pointer exists.",
7
+ "migrations": [],
8
+ "notes": "No project migration is required. Existing projects can run the normal trellis update flow."
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "version": "0.6.41",
3
+ "description": "Restores active-task visibility when a host context key has no persisted session file.",
4
+ "breaking": false,
5
+ "recommendMigrate": false,
6
+ "changelog": "**Fix:**\n- Projects one unique developer-owned resumable task as read-only workflow context when the host provides a context key whose session file is missing, while still refusing to guess when another session pointer exists.",
7
+ "migrations": [],
8
+ "notes": "No project migration is required. Existing projects can run the normal trellis update flow."
9
+ }
@@ -373,10 +373,13 @@ export class TrellisContext {
373
373
  *
374
374
  * Resolution order (mirrors Python `active_task.resolve_active_task`):
375
375
  * 1. Lookup the runtime file for the input-derived context key. A known
376
- * key is authoritative, so a missing or empty context returns no task.
376
+ * key is authoritative when it contains a task; a missing or empty
377
+ * context may use the guarded unbound-task projection.
377
378
  * 2. If no context key is available and exactly one session runtime file
378
379
  * exists locally, use it (`_resolveSingleSessionFallback`). Refuses to
379
380
  * guess when 0 or ≥2 files exist so multi-window isolation holds.
381
+ * 3. If no session file exists, expose one resumable task assigned to the
382
+ * current developer as a read-only unbound projection.
380
383
  */
381
384
  getActiveTask(platformInput = null) {
382
385
  const contextKey = this.getContextKey(platformInput)
@@ -391,6 +394,10 @@ export class TrellisContext {
391
394
  stale: !taskDir || !existsSync(taskDir),
392
395
  }
393
396
  }
397
+ const unbound = this._resolveUnboundTask()
398
+ if (unbound) {
399
+ return unbound
400
+ }
394
401
  return { taskPath: null, source: "none", stale: false }
395
402
  }
396
403
 
@@ -399,6 +406,11 @@ export class TrellisContext {
399
406
  return fallback
400
407
  }
401
408
 
409
+ const unbound = this._resolveUnboundTask()
410
+ if (unbound) {
411
+ return unbound
412
+ }
413
+
402
414
  return { taskPath: null, source: "none", stale: false }
403
415
  }
404
416
 
@@ -440,6 +452,56 @@ export class TrellisContext {
440
452
  }
441
453
  }
442
454
 
455
+ _resolveUnboundTask() {
456
+ const sessionsDir = join(this.directory, ".trellis", ".runtime", "sessions")
457
+ if (existsSync(sessionsDir)) {
458
+ let sessionFiles
459
+ try {
460
+ sessionFiles = readdirSync(sessionsDir).filter(name => name.endsWith(".json"))
461
+ } catch {
462
+ return null
463
+ }
464
+ if (sessionFiles.some(file => {
465
+ try {
466
+ const context = JSON.parse(readFileSync(join(sessionsDir, file), "utf-8"))
467
+ return Boolean(this.normalizeTaskRef(context?.current_task || ""))
468
+ } catch {
469
+ return false
470
+ }
471
+ })) return null
472
+ }
473
+
474
+ const developer = (process.env.TRELLIS_DEVELOPER || "").trim() || (() => {
475
+ try {
476
+ const content = readFileSync(join(this.directory, ".trellis", ".developer"), "utf-8")
477
+ return content.split(/\r?\n/).find(line => line.startsWith("name="))?.slice(5).trim() || ""
478
+ } catch {
479
+ return ""
480
+ }
481
+ })()
482
+ if (!developer) return null
483
+
484
+ const tasksDir = join(this.directory, ".trellis", "tasks")
485
+ let taskNames
486
+ try {
487
+ taskNames = readdirSync(tasksDir).filter(name => name !== "archive")
488
+ } catch {
489
+ return null
490
+ }
491
+
492
+ const candidates = taskNames.flatMap(name => {
493
+ try {
494
+ const data = JSON.parse(readFileSync(join(tasksDir, name, "task.json"), "utf-8"))
495
+ if (data.assignee !== developer || !["planning", "in_progress", "review"].includes(data.status)) return []
496
+ return [`.trellis/tasks/${name}`]
497
+ } catch {
498
+ return []
499
+ }
500
+ })
501
+ if (candidates.length !== 1) return null
502
+ return { taskPath: candidates[0], source: "unbound", stale: false }
503
+ }
504
+
443
505
  getCurrentTask(platformInput = null) {
444
506
  return this.getActiveTask(platformInput).taskPath
445
507
  }
@@ -156,6 +156,7 @@ function getActiveTask(ctx, platformInput = null) {
156
156
  const status = typeof data.status === "string" ? data.status : ""
157
157
  if (!status) return null
158
158
  const id = data.id || taskRef.split("/").pop()
159
+ if (active.source === "unbound") return { id, status: "unbound_task", source: active.source }
159
160
  return { id, status, source: active.source }
160
161
  } catch {
161
162
  return null
@@ -192,6 +192,8 @@ def get_active_task(
192
192
  status = data.get("status", "")
193
193
  if not isinstance(status, str) or not status:
194
194
  return task_dir.name, "task_error", active.source
195
+ if active.source_type == "unbound":
196
+ return task_id, "unbound_task", active.source
195
197
  return task_id, status, active.source
196
198
 
197
199
 
@@ -2,8 +2,8 @@
2
2
  """Session-scoped active task resolution.
3
3
 
4
4
  The user-facing concept is a single "active task". Trellis stores that pointer
5
- per AI session/window under `.trellis/.runtime/sessions/`; without a stable
6
- session key there is no active task.
5
+ per AI session/window under `.trellis/.runtime/sessions/`; when the pointer is
6
+ missing, a unique developer-owned resumable task may be exposed read-only.
7
7
  """
8
8
 
9
9
  from __future__ import annotations
@@ -654,9 +654,10 @@ def resolve_active_task(
654
654
  """Resolve the active task from session runtime state only.
655
655
 
656
656
  A stale session task is returned as stale. A known context identity is
657
- authoritative: a missing or empty context for that identity returns no
658
- task. When context identity is unavailable, single-session inference may
659
- cover pull-based platform sub-agents (copilot, gemini, qoder) that don't
657
+ authoritative when it contains a task reference; a missing or empty
658
+ context may use the guarded unbound-task projection. When context identity
659
+ is unavailable, single-session inference may cover pull-based platform
660
+ sub-agents (copilot, gemini, qoder) that don't
660
661
  inherit the parent's session id. ≥2 files or 0 files yield
661
662
  ActiveTask(None) — refuses to guess across windows.
662
663
  """
@@ -671,6 +672,10 @@ def resolve_active_task(
671
672
  active = _active_from_ref(task_ref, repo_root, "session", context_key)
672
673
  if active:
673
674
  return active
675
+ if allow_single_session_fallback:
676
+ unbound = _resolve_unbound_task(repo_root)
677
+ if unbound is not None:
678
+ return unbound
674
679
  return ActiveTask(None, "none", context_key)
675
680
 
676
681
  if allow_single_session_fallback:
@@ -678,6 +683,10 @@ def resolve_active_task(
678
683
  if fallback is not None:
679
684
  return fallback
680
685
 
686
+ unbound = _resolve_unbound_task(repo_root)
687
+ if unbound is not None:
688
+ return unbound
689
+
681
690
  return ActiveTask(None, "none", context_key)
682
691
 
683
692
 
@@ -706,6 +715,33 @@ def _resolve_single_session_fallback(repo_root: Path) -> ActiveTask | None:
706
715
  return _active_from_ref(task_ref, repo_root, "session-fallback", fallback_key)
707
716
 
708
717
 
718
+ def _resolve_unbound_task(repo_root: Path) -> ActiveTask | None:
719
+ """Expose one developer-owned task when no session pointer exists."""
720
+ sessions_dir = _runtime_sessions_dir(repo_root)
721
+ if sessions_dir.is_dir():
722
+ session_files = sorted(sessions_dir.glob("*.json"))
723
+ if any(_string_value((_read_json(session) or {}).get("current_task")) for session in session_files):
724
+ return None
725
+
726
+ from .paths import get_developer, get_tasks_dir
727
+ from .tasks import iter_active_tasks
728
+
729
+ developer = get_developer(repo_root)
730
+ if not developer:
731
+ return None
732
+
733
+ candidates = [
734
+ task
735
+ for task in iter_active_tasks(get_tasks_dir(repo_root))
736
+ if task.assignee == developer and task.status in {"planning", "in_progress", "review"}
737
+ ]
738
+ if len(candidates) != 1:
739
+ return None
740
+
741
+ task_path = candidates[0].directory.relative_to(repo_root).as_posix()
742
+ return ActiveTask(task_path, "unbound", None)
743
+
744
+
709
745
  def _utc_now() -> str:
710
746
  return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
711
747
 
@@ -308,6 +308,11 @@ def cmd_finish(args: argparse.Namespace) -> int:
308
308
  print(colored("No current task set", Colors.YELLOW))
309
309
  return 0
310
310
 
311
+ if active.source_type == "unbound":
312
+ print(colored("Task exists but no direct session is bound; run task.py start first", Colors.YELLOW))
313
+ print(f"Task: {current}")
314
+ return 0
315
+
311
316
  try:
312
317
  assert_task_mutation_allowed(repo_root, repo_root / current)
313
318
  except (OwnershipError, OSError) as exc:
@@ -76,7 +76,7 @@ python3 ./.trellis/scripts/task.py create-pr [name] [--dry-run]
76
76
 
77
77
  > Run `python3 ./.trellis/scripts/task.py --help` to see the authoritative, up-to-date list.
78
78
 
79
- **Current-task mechanism**: `task.py create` creates the task directory and (when session identity is available) auto-sets the per-session active-task pointer so the planning breadcrumb fires immediately. `task.py start` writes the same pointer (idempotent if already set) and flips `task.json.status` from `planning` to `in_progress`. State is stored under `.trellis/.runtime/sessions/`. If no context key is available from hook input, `TRELLIS_CONTEXT_ID`, or a platform-native session environment variable, there is no active task and `task.py start` fails with a session identity hint. `task.py finish` deletes the current session file (status unchanged). `task.py archive <task>` writes `status=completed`, moves the directory to `archive/`, and deletes any runtime session files that still point at the archived task.
79
+ **Current-task mechanism**: `task.py create` creates the task directory and (when session identity is available) auto-sets the per-session active-task pointer so the planning breadcrumb fires immediately. `task.py start` writes the same pointer (idempotent if already set) and flips `task.json.status` from `planning` to `in_progress`. State is stored under `.trellis/.runtime/sessions/`. If no context key is available from hook input, `TRELLIS_CONTEXT_ID`, or a platform-native session environment variable, `task.py start` fails with a session identity hint because it cannot persist a session pointer; read-only context may still project one unique developer-owned resumable task. `task.py finish` deletes the current session file (status unchanged). `task.py archive <task>` writes `status=completed`, moves the directory to `archive/`, and deletes any runtime session files that still point at the archived task.
80
80
 
81
81
  ### Workspace System
82
82
 
@@ -122,6 +122,7 @@ python3 ./.trellis/scripts/get_context.py --mode phase --step <X.Y> # detailed
122
122
 
123
123
  TAG ↔ PHASE scoping:
124
124
  [workflow-state:no_task] → no active task; before Phase 1
125
+ [workflow-state:unbound_task] → one developer-owned task exists without a session pointer
125
126
  [workflow-state:task_error] → active task record is unreadable; repair it before continuing
126
127
  [workflow-state:planning] → all of Phase 1 (status='planning')
127
128
  [workflow-state:planning-inline] → Codex inline variant of Phase 1
@@ -189,6 +190,12 @@ Simple conversation / small task: ask only whether this turn should create a Tre
189
190
  Complex task: ask the user if you can create a Trellis task and enter the planning phase. If the user says no, explain, clarify scope, or suggest a smaller split.
190
191
  [/workflow-state:no_task]
191
192
 
193
+ <!-- Per-turn breadcrumb: shown when one resumable task exists without a direct session binding. -->
194
+
195
+ [workflow-state:unbound_task]
196
+ 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.
197
+ [/workflow-state:unbound_task]
198
+
192
199
  <!-- Per-turn breadcrumb: shown when the active task record cannot be read. -->
193
200
 
194
201
  [workflow-state:task_error]
@@ -694,6 +701,7 @@ All tag blocks live in the `## Phase Index` section above, immediately after eac
694
701
  | Scope | Corresponding tag |
695
702
  |---|---|
696
703
  | No active task (before Phase 1) | `[workflow-state:no_task]` (after the Phase Index ASCII art) |
704
+ | One developer-owned task without a session binding | `[workflow-state:unbound_task]` (bind before lifecycle writes) |
697
705
  | Active task record unreadable | `[workflow-state:task_error]` (repair the existing task before continuing) |
698
706
  | All of Phase 1 (task created → ready for implementation) | `[workflow-state:planning]` (after Phase 1 summary) |
699
707
  | Codex inline Phase 1 | `[workflow-state:planning-inline]` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pennixrv/trellis",
3
- "version": "0.6.39",
3
+ "version": "0.6.41",
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.39"
37
+ "@pennixrv/trellis-core": "0.6.41"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@eslint/js": "^9.18.0",