@pennixrv/trellis 0.6.39 → 0.6.40

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
+ }
@@ -377,6 +377,8 @@ export class TrellisContext {
377
377
  * 2. If no context key is available and exactly one session runtime file
378
378
  * exists locally, use it (`_resolveSingleSessionFallback`). Refuses to
379
379
  * guess when 0 or ≥2 files exist so multi-window isolation holds.
380
+ * 3. If no session file exists, expose one resumable task assigned to the
381
+ * current developer as a read-only unbound projection.
380
382
  */
381
383
  getActiveTask(platformInput = null) {
382
384
  const contextKey = this.getContextKey(platformInput)
@@ -399,6 +401,11 @@ export class TrellisContext {
399
401
  return fallback
400
402
  }
401
403
 
404
+ const unbound = this._resolveUnboundTask()
405
+ if (unbound) {
406
+ return unbound
407
+ }
408
+
402
409
  return { taskPath: null, source: "none", stale: false }
403
410
  }
404
411
 
@@ -440,6 +447,49 @@ export class TrellisContext {
440
447
  }
441
448
  }
442
449
 
450
+ _resolveUnboundTask() {
451
+ const sessionsDir = join(this.directory, ".trellis", ".runtime", "sessions")
452
+ if (existsSync(sessionsDir)) {
453
+ let sessionFiles
454
+ try {
455
+ sessionFiles = readdirSync(sessionsDir).filter(name => name.endsWith(".json"))
456
+ } catch {
457
+ return null
458
+ }
459
+ if (sessionFiles.length > 0) return null
460
+ }
461
+
462
+ const developer = (process.env.TRELLIS_DEVELOPER || "").trim() || (() => {
463
+ try {
464
+ const content = readFileSync(join(this.directory, ".trellis", ".developer"), "utf-8")
465
+ return content.split(/\r?\n/).find(line => line.startsWith("name="))?.slice(5).trim() || ""
466
+ } catch {
467
+ return ""
468
+ }
469
+ })()
470
+ if (!developer) return null
471
+
472
+ const tasksDir = join(this.directory, ".trellis", "tasks")
473
+ let taskNames
474
+ try {
475
+ taskNames = readdirSync(tasksDir).filter(name => name !== "archive")
476
+ } catch {
477
+ return null
478
+ }
479
+
480
+ const candidates = taskNames.flatMap(name => {
481
+ try {
482
+ const data = JSON.parse(readFileSync(join(tasksDir, name, "task.json"), "utf-8"))
483
+ if (data.assignee !== developer || !["planning", "in_progress", "review"].includes(data.status)) return []
484
+ return [`.trellis/tasks/${name}`]
485
+ } catch {
486
+ return []
487
+ }
488
+ })
489
+ if (candidates.length !== 1) return null
490
+ return { taskPath: candidates[0], source: "unbound", stale: false }
491
+ }
492
+
443
493
  getCurrentTask(platformInput = null) {
444
494
  return this.getActiveTask(platformInput).taskPath
445
495
  }
@@ -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
 
@@ -678,6 +678,10 @@ def resolve_active_task(
678
678
  if fallback is not None:
679
679
  return fallback
680
680
 
681
+ unbound = _resolve_unbound_task(repo_root)
682
+ if unbound is not None:
683
+ return unbound
684
+
681
685
  return ActiveTask(None, "none", context_key)
682
686
 
683
687
 
@@ -706,6 +710,31 @@ def _resolve_single_session_fallback(repo_root: Path) -> ActiveTask | None:
706
710
  return _active_from_ref(task_ref, repo_root, "session-fallback", fallback_key)
707
711
 
708
712
 
713
+ def _resolve_unbound_task(repo_root: Path) -> ActiveTask | None:
714
+ """Expose one developer-owned task when no session pointer exists."""
715
+ sessions_dir = _runtime_sessions_dir(repo_root)
716
+ if sessions_dir.is_dir() and any(sessions_dir.glob("*.json")):
717
+ return None
718
+
719
+ from .paths import get_developer, get_tasks_dir
720
+ from .tasks import iter_active_tasks
721
+
722
+ developer = get_developer(repo_root)
723
+ if not developer:
724
+ return None
725
+
726
+ candidates = [
727
+ task
728
+ for task in iter_active_tasks(get_tasks_dir(repo_root))
729
+ if task.assignee == developer and task.status in {"planning", "in_progress", "review"}
730
+ ]
731
+ if len(candidates) != 1:
732
+ return None
733
+
734
+ task_path = candidates[0].directory.relative_to(repo_root).as_posix()
735
+ return ActiveTask(task_path, "unbound", None)
736
+
737
+
709
738
  def _utc_now() -> str:
710
739
  return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
711
740
 
@@ -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:
@@ -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.40",
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.40"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@eslint/js": "^9.18.0",