@pennixrv/trellis 0.6.41 → 0.6.43
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.42.json +9 -0
- package/dist/migrations/manifests/0.6.43.json +9 -0
- package/dist/templates/codex/hooks/session-start.py +12 -1
- package/dist/templates/opencode/lib/trellis-context.js +10 -2
- package/dist/templates/opencode/plugins/inject-workflow-state.js +16 -3
- package/dist/templates/shared-hooks/inject-subagent-context.py +6 -2
- package/dist/templates/shared-hooks/inject-workflow-state.py +7 -1
- package/dist/templates/trellis/scripts/common/active_task.py +22 -18
- package/dist/templates/trellis/scripts/common/continuation_record.py +1 -1
- package/dist/templates/trellis/scripts/task.py +18 -0
- package/dist/templates/trellis/workflow.md +13 -0
- package/package.json +2 -2
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "0.6.42",
|
|
3
|
+
"description": "Isolates unbound task resolution from unrelated session fallback inference.",
|
|
4
|
+
"breaking": false,
|
|
5
|
+
"recommendMigrate": false,
|
|
6
|
+
"changelog": "**Fix:**\n- Stop the default task resolver from borrowing a task from a sole unrelated session file, while preserving read-only unbound task and ambiguity projections.",
|
|
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.43",
|
|
3
|
+
"description": "Ships the isolated active-task resolver to the dogfood script tree.",
|
|
4
|
+
"breaking": false,
|
|
5
|
+
"recommendMigrate": false,
|
|
6
|
+
"changelog": "**Fix:**\n- Keep the dogfood and published active-task resolver scripts synchronized so isolated session fallback behavior is identical in development and installed projects.",
|
|
7
|
+
"migrations": [],
|
|
8
|
+
"notes": "No project migration is required. Existing projects can run the normal trellis update flow."
|
|
9
|
+
}
|
|
@@ -229,6 +229,14 @@ def _resolve_task_dir(trellis_dir: Path, task_ref: str) -> Path:
|
|
|
229
229
|
|
|
230
230
|
def _get_task_status(trellis_dir: Path, hook_input: dict) -> str:
|
|
231
231
|
active = _resolve_active_task(trellis_dir, hook_input)
|
|
232
|
+
if active.source_type == "unbound_ambiguous":
|
|
233
|
+
candidates = "\n".join(f"- {candidate}" for candidate in active.candidate_paths)
|
|
234
|
+
return (
|
|
235
|
+
"Status: TASK BINDING AMBIGUOUS\n"
|
|
236
|
+
f"Candidates:\n{candidates}\n"
|
|
237
|
+
"Next: Review the candidates and run `python3 ./.trellis/scripts/task.py start <task>` "
|
|
238
|
+
"with the intended task once a direct session identity is available."
|
|
239
|
+
)
|
|
232
240
|
if not active.task_path:
|
|
233
241
|
return (
|
|
234
242
|
"Status: NO ACTIVE TASK\n"
|
|
@@ -383,7 +391,10 @@ def _build_compact_current_state(
|
|
|
383
391
|
lines.append(_format_git_state(repo_root))
|
|
384
392
|
|
|
385
393
|
active = _resolve_active_task(trellis_dir, hook_input)
|
|
386
|
-
if active.
|
|
394
|
+
if active.source_type == "unbound_ambiguous":
|
|
395
|
+
candidates = ", ".join(active.candidate_paths)
|
|
396
|
+
lines.append(f"Current task: ambiguous; candidates={candidates}; bind explicitly.")
|
|
397
|
+
elif active.task_path:
|
|
387
398
|
task_dir = _resolve_task_dir(trellis_dir, active.task_path)
|
|
388
399
|
status = "unknown"
|
|
389
400
|
task_json = task_dir / "task.json"
|
|
@@ -498,8 +498,16 @@ export class TrellisContext {
|
|
|
498
498
|
return []
|
|
499
499
|
}
|
|
500
500
|
})
|
|
501
|
-
if (candidates.length
|
|
502
|
-
|
|
501
|
+
if (candidates.length === 0) return null
|
|
502
|
+
if (candidates.length === 1) {
|
|
503
|
+
return { taskPath: candidates[0], source: "unbound", stale: false }
|
|
504
|
+
}
|
|
505
|
+
return {
|
|
506
|
+
taskPath: null,
|
|
507
|
+
source: "unbound_ambiguous",
|
|
508
|
+
stale: false,
|
|
509
|
+
candidatePaths: candidates.sort(),
|
|
510
|
+
}
|
|
503
511
|
}
|
|
504
512
|
|
|
505
513
|
getCurrentTask(platformInput = null) {
|
|
@@ -143,6 +143,14 @@ function loadBreadcrumbs(directory) {
|
|
|
143
143
|
*/
|
|
144
144
|
function getActiveTask(ctx, platformInput = null) {
|
|
145
145
|
const active = ctx.getActiveTask(platformInput)
|
|
146
|
+
if (active.source === "unbound_ambiguous") {
|
|
147
|
+
return {
|
|
148
|
+
id: null,
|
|
149
|
+
status: "unbound_ambiguous",
|
|
150
|
+
source: active.source,
|
|
151
|
+
candidates: active.candidatePaths || [],
|
|
152
|
+
}
|
|
153
|
+
}
|
|
146
154
|
const taskRef = active.taskPath
|
|
147
155
|
if (!taskRef) return null
|
|
148
156
|
const taskDir = ctx.resolveTaskDir(taskRef)
|
|
@@ -170,12 +178,17 @@ function getActiveTask(ctx, platformInput = null) {
|
|
|
170
178
|
* "Refer to workflow.md for current step." line
|
|
171
179
|
* - no_task pseudo-status (id === null) → header omits task info
|
|
172
180
|
*/
|
|
173
|
-
function buildBreadcrumb(id, status, templates) {
|
|
181
|
+
function buildBreadcrumb(id, status, templates, candidates = []) {
|
|
174
182
|
let body = templates[status]
|
|
175
183
|
if (body === undefined) {
|
|
176
184
|
body = "Refer to workflow.md for current step."
|
|
177
185
|
}
|
|
178
|
-
let header
|
|
186
|
+
let header
|
|
187
|
+
if (status === "unbound_ambiguous") {
|
|
188
|
+
header = `Status: ${status}\nCandidates: ${candidates.join(", ")}`
|
|
189
|
+
} else {
|
|
190
|
+
header = id === null ? `Status: ${status}` : `Task: ${id} (${status})`
|
|
191
|
+
}
|
|
179
192
|
return `<workflow-state>\n${header}\n${body}\n</workflow-state>`
|
|
180
193
|
}
|
|
181
194
|
|
|
@@ -218,7 +231,7 @@ export default async ({ directory }) => {
|
|
|
218
231
|
const templates = loadBreadcrumbs(directory)
|
|
219
232
|
const task = getActiveTask(ctx, platformInput)
|
|
220
233
|
const breadcrumb = task
|
|
221
|
-
? buildBreadcrumb(task.id, task.status, templates, task.
|
|
234
|
+
? buildBreadcrumb(task.id, task.status, templates, task.candidates)
|
|
222
235
|
: buildBreadcrumb(null, "no_task", templates)
|
|
223
236
|
|
|
224
237
|
prependEphemeralText(messages, breadcrumb)
|
|
@@ -149,7 +149,7 @@ def get_current_task(
|
|
|
149
149
|
input_data: dict,
|
|
150
150
|
*,
|
|
151
151
|
platform: str | None = None,
|
|
152
|
-
allow_single_session_fallback: bool =
|
|
152
|
+
allow_single_session_fallback: bool = False,
|
|
153
153
|
allow_environment_context: bool = True,
|
|
154
154
|
require_existing: bool = False,
|
|
155
155
|
) -> str | None:
|
|
@@ -740,7 +740,11 @@ def main():
|
|
|
740
740
|
sys.exit(0)
|
|
741
741
|
|
|
742
742
|
# Get current task directory (research doesn't require it)
|
|
743
|
-
task_dir = get_current_task(
|
|
743
|
+
task_dir = get_current_task(
|
|
744
|
+
repo_root,
|
|
745
|
+
input_data,
|
|
746
|
+
allow_single_session_fallback=True,
|
|
747
|
+
)
|
|
744
748
|
|
|
745
749
|
# implement/check need task directory
|
|
746
750
|
if subagent_type in AGENTS_REQUIRE_TASK:
|
|
@@ -169,6 +169,9 @@ def get_active_task(
|
|
|
169
169
|
prompt.
|
|
170
170
|
"""
|
|
171
171
|
active = _resolve_active_task(root, input_data)
|
|
172
|
+
if active.source_type == "unbound_ambiguous":
|
|
173
|
+
candidates = ", ".join(active.candidate_paths)
|
|
174
|
+
return candidates, "unbound_ambiguous", active.source
|
|
172
175
|
if not active.task_path:
|
|
173
176
|
return None
|
|
174
177
|
|
|
@@ -379,7 +382,10 @@ def build_breadcrumb(
|
|
|
379
382
|
body = templates.get(status)
|
|
380
383
|
if body is None:
|
|
381
384
|
body = "Refer to workflow.md for current step."
|
|
382
|
-
|
|
385
|
+
if status == "unbound_ambiguous":
|
|
386
|
+
header = f"Status: {status}\nCandidates: {task_id}"
|
|
387
|
+
else:
|
|
388
|
+
header = f"Status: {status}" if task_id is None else f"Task: {task_id} ({status})"
|
|
383
389
|
return f"<workflow-state>\n{header}\n{body}\n</workflow-state>"
|
|
384
390
|
|
|
385
391
|
|
|
@@ -168,6 +168,7 @@ class ActiveTask:
|
|
|
168
168
|
source_type: str
|
|
169
169
|
context_key: str | None = None
|
|
170
170
|
stale: bool = False
|
|
171
|
+
candidate_paths: tuple[str, ...] = ()
|
|
171
172
|
|
|
172
173
|
@property
|
|
173
174
|
def source(self) -> str:
|
|
@@ -648,18 +649,17 @@ def resolve_active_task(
|
|
|
648
649
|
platform_input: dict[str, Any] | None = None,
|
|
649
650
|
platform: str | None = None,
|
|
650
651
|
*,
|
|
651
|
-
allow_single_session_fallback: bool =
|
|
652
|
+
allow_single_session_fallback: bool = False,
|
|
652
653
|
allow_environment_context: bool = True,
|
|
653
654
|
) -> ActiveTask:
|
|
654
655
|
"""Resolve the active task from session runtime state only.
|
|
655
656
|
|
|
656
|
-
A stale session task is returned as stale.
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
ActiveTask(None) — refuses to guess across windows.
|
|
657
|
+
A stale session task is returned as stale. Missing or unmatched session
|
|
658
|
+
identity does not infer ownership from the number of session files.
|
|
659
|
+
A unique developer-owned task, or an explicit ambiguous projection, may
|
|
660
|
+
still be exposed without writing a binding. Only the legacy inference from
|
|
661
|
+
one unrelated session file is opt-in; pull-based child agents use that
|
|
662
|
+
compatibility opt-in when they cannot inherit a parent session identity.
|
|
663
663
|
"""
|
|
664
664
|
context_key = resolve_context_key(
|
|
665
665
|
platform_input,
|
|
@@ -672,10 +672,9 @@ def resolve_active_task(
|
|
|
672
672
|
active = _active_from_ref(task_ref, repo_root, "session", context_key)
|
|
673
673
|
if active:
|
|
674
674
|
return active
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
return unbound
|
|
675
|
+
unbound = _resolve_unbound_task(repo_root)
|
|
676
|
+
if unbound is not None:
|
|
677
|
+
return unbound
|
|
679
678
|
return ActiveTask(None, "none", context_key)
|
|
680
679
|
|
|
681
680
|
if allow_single_session_fallback:
|
|
@@ -683,9 +682,9 @@ def resolve_active_task(
|
|
|
683
682
|
if fallback is not None:
|
|
684
683
|
return fallback
|
|
685
684
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
685
|
+
unbound = _resolve_unbound_task(repo_root)
|
|
686
|
+
if unbound is not None:
|
|
687
|
+
return unbound
|
|
689
688
|
|
|
690
689
|
return ActiveTask(None, "none", context_key)
|
|
691
690
|
|
|
@@ -735,11 +734,16 @@ def _resolve_unbound_task(repo_root: Path) -> ActiveTask | None:
|
|
|
735
734
|
for task in iter_active_tasks(get_tasks_dir(repo_root))
|
|
736
735
|
if task.assignee == developer and task.status in {"planning", "in_progress", "review"}
|
|
737
736
|
]
|
|
738
|
-
if len(candidates)
|
|
737
|
+
if len(candidates) == 0:
|
|
739
738
|
return None
|
|
740
739
|
|
|
741
|
-
|
|
742
|
-
|
|
740
|
+
task_paths = tuple(sorted(
|
|
741
|
+
task.directory.relative_to(repo_root).as_posix()
|
|
742
|
+
for task in candidates
|
|
743
|
+
))
|
|
744
|
+
if len(task_paths) == 1:
|
|
745
|
+
return ActiveTask(task_paths[0], "unbound", None)
|
|
746
|
+
return ActiveTask(None, "unbound_ambiguous", None, candidate_paths=task_paths)
|
|
743
747
|
|
|
744
748
|
|
|
745
749
|
def _utc_now() -> str:
|
|
@@ -125,7 +125,7 @@ def _file_digest(path: Path, maximum: int = MAX_EVIDENCE_BYTES) -> tuple[str, in
|
|
|
125
125
|
|
|
126
126
|
|
|
127
127
|
def _task_source(root: Path) -> dict[str, Any]:
|
|
128
|
-
active = resolve_active_task(root)
|
|
128
|
+
active = resolve_active_task(root, allow_single_session_fallback=True)
|
|
129
129
|
if not active.task_path:
|
|
130
130
|
raise NoDirectCurrentTask("no_direct_current_task")
|
|
131
131
|
if active.source_type == "session-fallback":
|
|
@@ -312,6 +312,11 @@ def cmd_finish(args: argparse.Namespace) -> int:
|
|
|
312
312
|
print(colored("Task exists but no direct session is bound; run task.py start first", Colors.YELLOW))
|
|
313
313
|
print(f"Task: {current}")
|
|
314
314
|
return 0
|
|
315
|
+
if active.source_type == "unbound_ambiguous":
|
|
316
|
+
print(colored("Multiple tasks exist but no direct session is bound; choose one before finishing", Colors.YELLOW))
|
|
317
|
+
for candidate in active.candidate_paths:
|
|
318
|
+
print(f"Candidate: {candidate}")
|
|
319
|
+
return 0
|
|
315
320
|
|
|
316
321
|
try:
|
|
317
322
|
assert_task_mutation_allowed(repo_root, repo_root / current)
|
|
@@ -367,6 +372,8 @@ def cmd_current(args: argparse.Namespace) -> int:
|
|
|
367
372
|
"source": active.source,
|
|
368
373
|
"stale": active.stale,
|
|
369
374
|
}
|
|
375
|
+
if active.candidate_paths:
|
|
376
|
+
payload["candidates"] = list(active.candidate_paths)
|
|
370
377
|
# Only present when the read failed, so the healthy shape is unchanged.
|
|
371
378
|
if read_error:
|
|
372
379
|
payload["error"] = read_error
|
|
@@ -374,6 +381,12 @@ def cmd_current(args: argparse.Namespace) -> int:
|
|
|
374
381
|
return 0 if active.task_path else 1
|
|
375
382
|
|
|
376
383
|
if args.source:
|
|
384
|
+
if active.source_type == "unbound_ambiguous":
|
|
385
|
+
print("Current task: (ambiguous)")
|
|
386
|
+
print("Source: unbound_ambiguous")
|
|
387
|
+
for candidate in active.candidate_paths:
|
|
388
|
+
print(f"Candidate: {candidate}")
|
|
389
|
+
return 1
|
|
377
390
|
print(f"Current task: {active.task_path or '(none)'}")
|
|
378
391
|
print(f"Source: {active.source}")
|
|
379
392
|
if active.stale:
|
|
@@ -384,6 +397,11 @@ def cmd_current(args: argparse.Namespace) -> int:
|
|
|
384
397
|
print(active.task_path)
|
|
385
398
|
return 0
|
|
386
399
|
|
|
400
|
+
if active.source_type == "unbound_ambiguous":
|
|
401
|
+
print("Multiple active tasks require explicit binding:")
|
|
402
|
+
for candidate in active.candidate_paths:
|
|
403
|
+
print(candidate)
|
|
404
|
+
|
|
387
405
|
return 1
|
|
388
406
|
|
|
389
407
|
|
|
@@ -123,6 +123,8 @@ python3 ./.trellis/scripts/get_context.py --mode phase --step <X.Y> # detailed
|
|
|
123
123
|
TAG ↔ PHASE scoping:
|
|
124
124
|
[workflow-state:no_task] → no active task; before Phase 1
|
|
125
125
|
[workflow-state:unbound_task] → one developer-owned task exists without a session pointer
|
|
126
|
+
[workflow-state:unbound_ambiguous] → multiple developer-owned tasks exist without a session pointer
|
|
127
|
+
[workflow-state:unbound_ambiguous-inline] → Codex inline variant of unbound_ambiguous
|
|
126
128
|
[workflow-state:task_error] → active task record is unreadable; repair it before continuing
|
|
127
129
|
[workflow-state:planning] → all of Phase 1 (status='planning')
|
|
128
130
|
[workflow-state:planning-inline] → Codex inline variant of Phase 1
|
|
@@ -196,6 +198,16 @@ Complex task: ask the user if you can create a Trellis task and enter the planni
|
|
|
196
198
|
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
199
|
[/workflow-state:unbound_task]
|
|
198
200
|
|
|
201
|
+
<!-- Per-turn breadcrumb shown when multiple resumable tasks exist without a direct session binding. -->
|
|
202
|
+
|
|
203
|
+
[workflow-state:unbound_ambiguous]
|
|
204
|
+
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.
|
|
205
|
+
[/workflow-state:unbound_ambiguous]
|
|
206
|
+
|
|
207
|
+
[workflow-state:unbound_ambiguous-inline]
|
|
208
|
+
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.
|
|
209
|
+
[/workflow-state:unbound_ambiguous-inline]
|
|
210
|
+
|
|
199
211
|
<!-- Per-turn breadcrumb: shown when the active task record cannot be read. -->
|
|
200
212
|
|
|
201
213
|
[workflow-state:task_error]
|
|
@@ -702,6 +714,7 @@ All tag blocks live in the `## Phase Index` section above, immediately after eac
|
|
|
702
714
|
|---|---|
|
|
703
715
|
| No active task (before Phase 1) | `[workflow-state:no_task]` (after the Phase Index ASCII art) |
|
|
704
716
|
| One developer-owned task without a session binding | `[workflow-state:unbound_task]` (bind before lifecycle writes) |
|
|
717
|
+
| Multiple developer-owned tasks without a session binding | `[workflow-state:unbound_ambiguous]` / `[workflow-state:unbound_ambiguous-inline]` (choose and bind explicitly) |
|
|
705
718
|
| Active task record unreadable | `[workflow-state:task_error]` (repair the existing task before continuing) |
|
|
706
719
|
| All of Phase 1 (task created → ready for implementation) | `[workflow-state:planning]` (after Phase 1 summary) |
|
|
707
720
|
| 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.43",
|
|
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.43"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@eslint/js": "^9.18.0",
|