@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.
- package/dist/migrations/manifests/0.6.39.json +2 -2
- package/dist/migrations/manifests/0.6.40.json +9 -0
- package/dist/templates/opencode/lib/trellis-context.js +50 -0
- 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 +29 -0
- package/dist/templates/trellis/scripts/task.py +5 -0
- package/dist/templates/trellis/workflow.md +8 -0
- 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
|
+
}
|
|
@@ -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.
|
|
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.
|
|
37
|
+
"@pennixrv/trellis-core": "0.6.40"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@eslint/js": "^9.18.0",
|