@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.
- package/dist/migrations/manifests/0.6.39.json +2 -2
- package/dist/migrations/manifests/0.6.40.json +9 -0
- package/dist/migrations/manifests/0.6.41.json +9 -0
- package/dist/templates/opencode/lib/trellis-context.js +63 -1
- package/dist/templates/opencode/plugins/inject-workflow-state.js +1 -0
- package/dist/templates/shared-hooks/inject-workflow-state.py +2 -0
- package/dist/templates/trellis/scripts/common/active_task.py +41 -5
- package/dist/templates/trellis/scripts/task.py +5 -0
- package/dist/templates/trellis/workflow.md +9 -1
- package/package.json +2 -2
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": "0.6.39",
|
|
3
|
-
"description": "
|
|
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": "**
|
|
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
|
|
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/`;
|
|
6
|
-
|
|
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
|
|
658
|
-
task. When context identity
|
|
659
|
-
|
|
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,
|
|
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.
|
|
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.
|
|
37
|
+
"@pennixrv/trellis-core": "0.6.41"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@eslint/js": "^9.18.0",
|