@pennixrv/trellis 0.7.0-beta.32 → 0.7.0-beta.34
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/commands/channel/index.d.ts.map +1 -1
- package/dist/commands/channel/index.js +15 -11
- package/dist/commands/channel/index.js.map +1 -1
- package/dist/commands/channel/spawn.d.ts.map +1 -1
- package/dist/commands/channel/spawn.js +16 -11
- package/dist/commands/channel/spawn.js.map +1 -1
- package/dist/migrations/manifests/0.7.0-beta.33.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.34.json +9 -0
- package/dist/templates/common/bundled-skills/trellis-channel/SKILL.md +3 -1
- package/dist/templates/common/bundled-skills/trellis-channel/references/command-reference.md +11 -10
- package/dist/templates/common/bundled-skills/trellis-channel/references/forum.md +4 -12
- package/dist/templates/common/bundled-skills/trellis-channel/references/multi-target-dispatch.md +113 -0
- package/dist/templates/common/bundled-skills/trellis-channel/references/progress-debugging.md +4 -4
- package/dist/templates/common/bundled-skills/trellis-channel/references/subnode-work.md +32 -7
- package/dist/templates/common/bundled-skills/trellis-channel/references/workers.md +2 -3
- package/dist/templates/common/bundled-skills/trellis-channel/references/workflows.md +11 -12
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-task-lifecycle.md +6 -2
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/task-system.md +6 -1
- package/dist/templates/common/commands/continue.md +3 -2
- package/dist/templates/common/commands/start.md +1 -0
- package/dist/templates/common/skills/brainstorm.md +14 -1
- package/dist/templates/shared-hooks/inject-workflow-state.py +2 -1
- package/dist/templates/trellis/index.d.ts +1 -0
- package/dist/templates/trellis/index.d.ts.map +1 -1
- package/dist/templates/trellis/index.js +2 -0
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/scripts/common/task_planning.py +133 -0
- package/dist/templates/trellis/scripts/common/task_store.py +20 -1
- package/dist/templates/trellis/scripts/subnode_artifact.py +32 -0
- package/dist/templates/trellis/scripts/task.py +96 -2
- package/dist/templates/trellis/workflow.md +39 -2
- package/package.json +2 -2
|
@@ -294,6 +294,12 @@ def _parse_meta_pairs(pairs: list[str] | None) -> dict[str, str] | None:
|
|
|
294
294
|
file=sys.stderr,
|
|
295
295
|
)
|
|
296
296
|
return None
|
|
297
|
+
try:
|
|
298
|
+
from .task_planning import validate_meta_value
|
|
299
|
+
validate_meta_value(key, value)
|
|
300
|
+
except ValueError as exc:
|
|
301
|
+
print(colored(f"Error: {exc}", Colors.RED), file=sys.stderr)
|
|
302
|
+
return None
|
|
297
303
|
meta[key] = value
|
|
298
304
|
return meta
|
|
299
305
|
|
|
@@ -662,7 +668,7 @@ def cmd_create(args: argparse.Namespace) -> int:
|
|
|
662
668
|
if getattr(args, "no_start", False):
|
|
663
669
|
print(
|
|
664
670
|
colored(
|
|
665
|
-
"Skipped session activation (--no-start); run task.py start when
|
|
671
|
+
"Skipped session activation (--no-start); run task.py select to plan, start only when authorized.",
|
|
666
672
|
Colors.YELLOW,
|
|
667
673
|
),
|
|
668
674
|
file=sys.stderr,
|
|
@@ -1934,6 +1940,12 @@ def cmd_set_meta(args: argparse.Namespace) -> int:
|
|
|
1934
1940
|
return 1
|
|
1935
1941
|
key = args.key
|
|
1936
1942
|
value = args.value
|
|
1943
|
+
from .task_planning import invalidate_plan, validate_meta_value
|
|
1944
|
+
try:
|
|
1945
|
+
validate_meta_value(key, value)
|
|
1946
|
+
except ValueError as exc:
|
|
1947
|
+
print(colored(f"Error: {exc}", Colors.RED), file=sys.stderr)
|
|
1948
|
+
return 1
|
|
1937
1949
|
|
|
1938
1950
|
if not key:
|
|
1939
1951
|
print(colored("Error: Missing arguments", Colors.RED))
|
|
@@ -1959,6 +1971,13 @@ def cmd_set_meta(args: argparse.Namespace) -> int:
|
|
|
1959
1971
|
meta = data.get("meta")
|
|
1960
1972
|
if not isinstance(meta, dict):
|
|
1961
1973
|
meta = {}
|
|
1974
|
+
if key in {"execution_class", "delivery_mode"} and meta.get(key) != value and "planning" in meta:
|
|
1975
|
+
try:
|
|
1976
|
+
invalidate_plan(data)
|
|
1977
|
+
except ValueError as exc:
|
|
1978
|
+
print(colored(f"Error: {exc}", Colors.RED), file=sys.stderr)
|
|
1979
|
+
return 1
|
|
1980
|
+
meta = data["meta"]
|
|
1962
1981
|
meta[key] = value
|
|
1963
1982
|
data["meta"] = meta
|
|
1964
1983
|
if not write_json(task_json, data):
|
|
@@ -632,6 +632,29 @@ def _validate_disposition_data(
|
|
|
632
632
|
_require_text(disposition.get("decided_at"), "disposition.decided_at", max_len=128)
|
|
633
633
|
|
|
634
634
|
|
|
635
|
+
def _validate_unit_plan(brief: dict[str, Any], task_dir: Path, repo_root: Path) -> None:
|
|
636
|
+
"""New dispatch admission only; historical report readers remain compatible."""
|
|
637
|
+
plan = brief.get("unit_plan")
|
|
638
|
+
if not isinstance(plan, dict):
|
|
639
|
+
_fail("new evidence unit requires unit_plan")
|
|
640
|
+
reference = _require_text(plan.get("plan_ref"), "unit_plan.plan_ref", max_len=1024)
|
|
641
|
+
locator = reference.partition("#")[0]
|
|
642
|
+
relative = Path(locator)
|
|
643
|
+
if relative.is_absolute() or ".." in relative.parts or "\\" in locator:
|
|
644
|
+
_fail("unit_plan.plan_ref must be a repository-relative task document")
|
|
645
|
+
document = repo_root / relative
|
|
646
|
+
lexical_task = _task_lexical_from_resolved(task_dir, repo_root)
|
|
647
|
+
_assert_no_subpath_symlinks(lexical_task, document)
|
|
648
|
+
if not document.is_file() or document.suffix != ".md":
|
|
649
|
+
_fail("unit_plan.plan_ref must identify a regular document in this task")
|
|
650
|
+
if document.stat().st_size > MAX_REPORT_BYTES:
|
|
651
|
+
_fail("unit_plan.plan_ref document exceeds the byte limit")
|
|
652
|
+
_require_text(document.read_text(encoding="utf-8"), "unit_plan.plan_ref document", max_len=MAX_REPORT_BYTES)
|
|
653
|
+
_require_text(plan.get("sizing_rationale"), "unit_plan.sizing_rationale")
|
|
654
|
+
if len(brief["scope"]) > 1:
|
|
655
|
+
_require_text(plan.get("grouping_rationale"), "unit_plan.grouping_rationale")
|
|
656
|
+
|
|
657
|
+
|
|
635
658
|
def _init(args: argparse.Namespace) -> None:
|
|
636
659
|
repo_root = get_repo_root()
|
|
637
660
|
task_dir = resolve_task_dir(args.task, repo_root)
|
|
@@ -659,6 +682,7 @@ def _init(args: argparse.Namespace) -> None:
|
|
|
659
682
|
brief["role_id"] = "subnode"
|
|
660
683
|
brief["report_path"] = _relative_to_repo(node_dir / "report.json", repo_root)
|
|
661
684
|
_validate_brief_data(brief, task_dir, node_dir, repo_root)
|
|
685
|
+
_validate_unit_plan(brief, task_dir, repo_root)
|
|
662
686
|
for directory in (task_dir / "subnodes", task_dir / "subnodes" / work_id, node_dir):
|
|
663
687
|
if directory.is_symlink():
|
|
664
688
|
_fail(f"refusing symlinked artifact directory: {directory}")
|
|
@@ -709,6 +733,7 @@ def _queue_init(args: argparse.Namespace) -> None:
|
|
|
709
733
|
subnode_id = _require_id(brief.get("subnode_id"), "brief.subnode_id")
|
|
710
734
|
if brief_task_dir != task_dir:
|
|
711
735
|
_fail(f"queue item {subnode_id} brief belongs to another task")
|
|
736
|
+
_validate_unit_plan(brief, task_dir, repo_root)
|
|
712
737
|
if brief.get("work_id") != work_id:
|
|
713
738
|
_fail(f"queue item {subnode_id} brief belongs to another work")
|
|
714
739
|
if subnode_id in seen:
|
|
@@ -764,6 +789,13 @@ def _queue_claim(args: argparse.Namespace) -> None:
|
|
|
764
789
|
item = next((candidate for candidate in items if candidate["subnode_id"] == subnode_id), None)
|
|
765
790
|
if item is None:
|
|
766
791
|
_fail(f"subnode is not listed in the queue: {subnode_id}")
|
|
792
|
+
for previous in items:
|
|
793
|
+
if previous is item:
|
|
794
|
+
break
|
|
795
|
+
previous_claim = task_dir / "subnodes" / args.work_id / previous["subnode_id"] / "dispatch-claim.json"
|
|
796
|
+
_assert_no_subpath_symlinks(task_dir, previous_claim)
|
|
797
|
+
if not previous_claim.is_file():
|
|
798
|
+
_fail(f"FIFO requires claiming earlier unit first: {previous['subnode_id']}")
|
|
767
799
|
claim_path = task_dir / "subnodes" / args.work_id / subnode_id / "dispatch-claim.json"
|
|
768
800
|
_assert_no_subpath_symlinks(task_dir, claim_path)
|
|
769
801
|
claim = {
|
|
@@ -8,7 +8,10 @@ Usage:
|
|
|
8
8
|
python3 task.py add-context <dir> <file> <path> [reason] # Add jsonl entry
|
|
9
9
|
python3 task.py validate <dir> # Validate jsonl files
|
|
10
10
|
python3 task.py list-context <dir> # List jsonl entries
|
|
11
|
-
python3 task.py
|
|
11
|
+
python3 task.py select <dir> # Select an existing task without starting it
|
|
12
|
+
python3 task.py plan seal <dir> # Seal the current material plan
|
|
13
|
+
python3 task.py plan approve <dir> --revision N --basis "approval"
|
|
14
|
+
python3 task.py start <dir> # Start an approved task, record current branch
|
|
12
15
|
python3 task.py replan <dir> "<reason>" # Return an in-progress task to planning
|
|
13
16
|
python3 task.py current [--source] [--json] # Show active task
|
|
14
17
|
python3 task.py ownership <operation> ... # Manage formal handoff task ownership
|
|
@@ -59,7 +62,10 @@ from common.io import (
|
|
|
59
62
|
read_json_checked,
|
|
60
63
|
write_json,
|
|
61
64
|
)
|
|
62
|
-
from common.task_utils import resolve_task_dir, run_task_hooks
|
|
65
|
+
from common.task_utils import is_within_tasks_dir, resolve_task_dir, run_task_hooks
|
|
66
|
+
from common.task_planning import (
|
|
67
|
+
PlanningError, approve_plan, invalidate_plan, require_start_approval, seal_plan,
|
|
68
|
+
)
|
|
63
69
|
from common.tasks import iter_active_tasks, children_progress
|
|
64
70
|
from common.workflow_selection import WORKFLOW_ID_RE, workflow_md_for_task
|
|
65
71
|
|
|
@@ -194,6 +200,58 @@ def _record_start_state(
|
|
|
194
200
|
)
|
|
195
201
|
|
|
196
202
|
|
|
203
|
+
def _active_task_target(task_input: str, repo_root: Path) -> tuple[Path, dict]:
|
|
204
|
+
"""Resolve a live task and enforce the native mutation boundary."""
|
|
205
|
+
target = resolve_task_dir(task_input, repo_root)
|
|
206
|
+
if target is None or not is_within_tasks_dir(target, repo_root):
|
|
207
|
+
raise PlanningError("target must be a direct active task directory")
|
|
208
|
+
task_json = target / FILE_TASK_JSON
|
|
209
|
+
if task_json.is_symlink():
|
|
210
|
+
raise PlanningError("refusing symlinked task.json")
|
|
211
|
+
data, reason = read_json_checked(task_json)
|
|
212
|
+
if data is None:
|
|
213
|
+
raise PlanningError(describe_json_read_failure(task_json, reason)[0])
|
|
214
|
+
if data.get("status") not in {"planning", "in_progress"}:
|
|
215
|
+
raise PlanningError("target task must be planning or in_progress")
|
|
216
|
+
assert_task_mutation_allowed(repo_root, target)
|
|
217
|
+
return target, data
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def cmd_select(args: argparse.Namespace) -> int:
|
|
221
|
+
"""Bind context only; Channel drain is owned by the coordinator procedure."""
|
|
222
|
+
repo_root = get_repo_root()
|
|
223
|
+
try:
|
|
224
|
+
target, _data = _active_task_target(args.dir, repo_root)
|
|
225
|
+
active = set_active_task(target.relative_to(repo_root.resolve()).as_posix(), repo_root)
|
|
226
|
+
if not active:
|
|
227
|
+
raise PlanningError("failed to select current task")
|
|
228
|
+
except (PlanningError, OwnershipError, OSError, ValueError) as exc:
|
|
229
|
+
print(colored(f"Error: {exc}", Colors.RED), file=sys.stderr)
|
|
230
|
+
return 1
|
|
231
|
+
print(colored(f"✓ Task selected without starting: {active.task_path}", Colors.GREEN))
|
|
232
|
+
return 0
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def cmd_plan(args: argparse.Namespace) -> int:
|
|
236
|
+
repo_root = get_repo_root()
|
|
237
|
+
try:
|
|
238
|
+
target, data = _active_task_target(args.dir, repo_root)
|
|
239
|
+
if data.get("status") != "planning":
|
|
240
|
+
raise PlanningError("plan seal/approve requires planning; use replan for a material change")
|
|
241
|
+
if args.plan_command == "seal":
|
|
242
|
+
revision = seal_plan(data, target)
|
|
243
|
+
else:
|
|
244
|
+
approve_plan(data, target, args.revision, args.basis)
|
|
245
|
+
revision = args.revision
|
|
246
|
+
if not write_json(target / FILE_TASK_JSON, data):
|
|
247
|
+
raise PlanningError("could not write planning record")
|
|
248
|
+
except (PlanningError, OwnershipError, OSError, ValueError) as exc:
|
|
249
|
+
print(colored(f"Error: {exc}", Colors.RED), file=sys.stderr)
|
|
250
|
+
return 1
|
|
251
|
+
print(colored(f"✓ Plan {args.plan_command}: revision {revision}", Colors.GREEN))
|
|
252
|
+
return 0
|
|
253
|
+
|
|
254
|
+
|
|
197
255
|
def cmd_start(args: argparse.Namespace) -> int:
|
|
198
256
|
"""Set active task."""
|
|
199
257
|
repo_root = get_repo_root()
|
|
@@ -223,6 +281,19 @@ def cmd_start(args: argparse.Namespace) -> int:
|
|
|
223
281
|
print(colored(f"Error: {exc}", Colors.RED), file=sys.stderr)
|
|
224
282
|
return 2
|
|
225
283
|
|
|
284
|
+
# Reject before writing either the session pointer or task status. The
|
|
285
|
+
# manifest override below never supplies implementation authorization.
|
|
286
|
+
try:
|
|
287
|
+
if not is_within_tasks_dir(full_path, repo_root) or (full_path / FILE_TASK_JSON).is_symlink():
|
|
288
|
+
raise PlanningError("start requires a direct active task with regular task.json")
|
|
289
|
+
task_data, reason = read_json_checked(full_path / FILE_TASK_JSON)
|
|
290
|
+
if task_data is None:
|
|
291
|
+
raise PlanningError(describe_json_read_failure(full_path / FILE_TASK_JSON, reason)[0])
|
|
292
|
+
require_start_approval(task_data, full_path)
|
|
293
|
+
except (PlanningError, OSError, ValueError) as exc:
|
|
294
|
+
print(colored(f"Error: {exc}", Colors.RED), file=sys.stderr)
|
|
295
|
+
return 1
|
|
296
|
+
|
|
226
297
|
# Context-manifest gate (#573): a seeded-but-uncurated implement/check
|
|
227
298
|
# manifest means every sub-agent dispatched for this task runs with zero
|
|
228
299
|
# spec context, and nothing downstream surfaces that to the main session.
|
|
@@ -340,6 +411,12 @@ def cmd_replan(args: argparse.Namespace) -> int:
|
|
|
340
411
|
)
|
|
341
412
|
return 1
|
|
342
413
|
|
|
414
|
+
try:
|
|
415
|
+
invalidate_plan(data)
|
|
416
|
+
except PlanningError as exc:
|
|
417
|
+
print(colored(f"Error: {exc}", Colors.RED), file=sys.stderr)
|
|
418
|
+
return 1
|
|
419
|
+
|
|
343
420
|
event = {
|
|
344
421
|
"timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
|
345
422
|
"task": full_path.relative_to(repo_root).as_posix(),
|
|
@@ -803,6 +880,9 @@ def show_usage() -> None:
|
|
|
803
880
|
|
|
804
881
|
Usage:
|
|
805
882
|
python3 task.py create <title> --description <desc> Create new task directory (both required, non-empty)
|
|
883
|
+
python3 task.py select <dir> Select context without starting
|
|
884
|
+
python3 task.py plan seal <dir> Seal the current material plan
|
|
885
|
+
python3 task.py plan approve <dir> --revision N --basis "approval"
|
|
806
886
|
python3 task.py create <title> --description <desc> --package <pkg> Create task for a specific package
|
|
807
887
|
python3 task.py create <title> --description <desc> --parent <dir> Create task as child of parent
|
|
808
888
|
python3 task.py create <title> --description <desc> --no-start Create without making it active in this session
|
|
@@ -964,6 +1044,18 @@ def main() -> int:
|
|
|
964
1044
|
p_listctx = subparsers.add_parser("list-context", help="List context entries")
|
|
965
1045
|
p_listctx.add_argument("dir", help="Task directory")
|
|
966
1046
|
|
|
1047
|
+
p_select = subparsers.add_parser("select", help="Select a task without changing its phase")
|
|
1048
|
+
p_select.add_argument("dir", help="Existing active task directory")
|
|
1049
|
+
|
|
1050
|
+
p_plan = subparsers.add_parser("plan", help="Seal or record later approval of a material plan")
|
|
1051
|
+
plan_sub = p_plan.add_subparsers(dest="plan_command", required=True)
|
|
1052
|
+
p_seal = plan_sub.add_parser("seal", help="Seal the current material plan revision")
|
|
1053
|
+
p_seal.add_argument("dir", help="Planning task directory")
|
|
1054
|
+
p_approve = plan_sub.add_parser("approve", help="Record actual later user approval")
|
|
1055
|
+
p_approve.add_argument("dir", help="Planning task directory")
|
|
1056
|
+
p_approve.add_argument("--revision", type=int, required=True)
|
|
1057
|
+
p_approve.add_argument("--basis", required=True, help="Short non-sensitive basis for real approval")
|
|
1058
|
+
|
|
967
1059
|
# start
|
|
968
1060
|
p_start = subparsers.add_parser("start", help="Set active task")
|
|
969
1061
|
p_start.add_argument("dir", help="Task directory")
|
|
@@ -1124,6 +1216,8 @@ def main() -> int:
|
|
|
1124
1216
|
"add-context": cmd_add_context,
|
|
1125
1217
|
"validate": cmd_validate,
|
|
1126
1218
|
"list-context": cmd_list_context,
|
|
1219
|
+
"select": cmd_select,
|
|
1220
|
+
"plan": cmd_plan,
|
|
1127
1221
|
"start": cmd_start,
|
|
1128
1222
|
"replan": cmd_replan,
|
|
1129
1223
|
"current": cmd_current,
|
|
@@ -22,6 +22,36 @@ target or explicit inspection. Native protected writes retain owner identity
|
|
|
22
22
|
and ownership checks. Never repeat or re-answer consumed pre-compaction user
|
|
23
23
|
input; handle genuinely new input normally. No recovery cache or extra waiter.
|
|
24
24
|
|
|
25
|
+
## Plan Approval And Task Selection
|
|
26
|
+
|
|
27
|
+
Classify task meta explicitly as `execution_class=direct|planned` and
|
|
28
|
+
`delivery_mode=change_bearing|analysis_only`; an unclassified old planning task
|
|
29
|
+
needs this once before start. Direct small work and eligible analysis-only work
|
|
30
|
+
retain their existing shortest paths. Existing in-progress tasks are not reset.
|
|
31
|
+
|
|
32
|
+
For planned/change-bearing work, close decisions and required task artifacts,
|
|
33
|
+
run native `task.py plan seal <task>`, present that material plan, and stop before
|
|
34
|
+
implementation. Only a later explicit user approval for THIS task's CURRENT
|
|
35
|
+
sealed revision authorizes `task.py plan approve <task> --revision <n> --basis
|
|
36
|
+
"<short non-sensitive actual approval basis>"`, then `task.py start`. Initial
|
|
37
|
+
delivery requests, parent-task approval, and design answers do not qualify.
|
|
38
|
+
`--allow-empty-context` overrides only context manifests, never approval.
|
|
39
|
+
The native record enforces structure; chat authenticity remains agent-owned.
|
|
40
|
+
|
|
41
|
+
Material scope, owner, risk, public behavior, or acceptance changes require
|
|
42
|
+
`task.py replan` and a newly sealed, presented, subsequently approved revision.
|
|
43
|
+
Resealing a sealed planning task declares a new material revision. Wording,
|
|
44
|
+
formatting, progress, and execution evidence do not automatically invalidate it.
|
|
45
|
+
Generic create/set-meta cannot prefill the reserved `meta.planning` record.
|
|
46
|
+
|
|
47
|
+
Use native `task.py select <task>` for context-only selection: no phase/branch
|
|
48
|
+
change, after_start hook, or implementation authority. `create --no-start`
|
|
49
|
+
deliberately keeps the prior pointer. Save the old checkpoint before switching;
|
|
50
|
+
with live Channel work, pause refills and drain all already dispatched units and
|
|
51
|
+
reservations according to `trellis-channel`'s multi-target procedure, preserving
|
|
52
|
+
pending units and the old phase. Do not kill/retry or automatically restart a
|
|
53
|
+
user-stopped task. Ordinary continue/compaction retains the checkpoint fast path.
|
|
54
|
+
|
|
25
55
|
## Trellis System
|
|
26
56
|
|
|
27
57
|
### Developer Identity
|
|
@@ -54,7 +84,10 @@ Every task has its own directory under `.trellis/tasks/{MM-DD-name}/` holding `t
|
|
|
54
84
|
```bash
|
|
55
85
|
# Task lifecycle
|
|
56
86
|
python3 ./.trellis/scripts/task.py create "<title>" [--slug <name>] [--parent <dir>]
|
|
57
|
-
python3 ./.trellis/scripts/task.py
|
|
87
|
+
python3 ./.trellis/scripts/task.py select <name> # context only, preserve phase
|
|
88
|
+
python3 ./.trellis/scripts/task.py plan seal <name> # current material plan
|
|
89
|
+
python3 ./.trellis/scripts/task.py plan approve <name> --revision <n> --basis "<actual approval>"
|
|
90
|
+
python3 ./.trellis/scripts/task.py start <name> # begin only when start gates pass
|
|
58
91
|
python3 ./.trellis/scripts/task.py replan <name> "<reason>" # return material ambiguity to planning
|
|
59
92
|
python3 ./.trellis/scripts/task.py current --source # show active task and source
|
|
60
93
|
python3 ./.trellis/scripts/task.py finish # clear active task (triggers after_finish hooks)
|
|
@@ -247,6 +280,7 @@ If `task.json.meta.delivery_mode = "analysis_only"` exactly, complete the declar
|
|
|
247
280
|
Lightweight: `prd.md` can be enough. Complex: finish `prd.md`, `design.md`, and `implement.md`; run the Planning Seal closure pass before asking for review. If `decision-needed` items or an unsealed decision graph remain, load `pennix-decision-grill`, batch only independent frontier questions, and stay in planning. Answers returned by the current continuation must be persisted and fed back into the same planning loop.
|
|
248
281
|
Multi-deliverable scope: consider a parent task plus independently verifiable child tasks; dependencies must be written in child artifacts, not implied by tree position.
|
|
249
282
|
Sub-agent mode: curate `implement.jsonl` and `check.jsonl` as spec/research manifests before start.
|
|
283
|
+
Planned/change-bearing start requires native plan seal and matching later approval of this task's current material revision. Selecting context does not approve it.
|
|
250
284
|
[/workflow-state:planning]
|
|
251
285
|
|
|
252
286
|
<!-- Per-turn breadcrumb: shown throughout Phase 1 when Codex uses its default
|
|
@@ -261,6 +295,7 @@ If `task.json.meta.delivery_mode = "analysis_only"` exactly, complete the declar
|
|
|
261
295
|
Lightweight: `prd.md` can be enough. Complex: finish `prd.md`, `design.md`, and `implement.md`; run the Planning Seal closure pass before asking for review. If `decision-needed` items or an unsealed decision graph remain, load `pennix-decision-grill`, batch only independent frontier questions, and stay in planning. Answers returned by the current continuation must be persisted and fed back into the same planning loop.
|
|
262
296
|
Multi-deliverable scope: consider a parent task plus independently verifiable child tasks; dependencies must be written in child artifacts, not implied by tree position.
|
|
263
297
|
Inline mode: skip jsonl curation; Phase 2 reads artifacts/specs via `trellis-before-dev`.
|
|
298
|
+
Planned/change-bearing start requires native plan seal and matching later approval of this task's current material revision. Selecting context does not approve it.
|
|
264
299
|
[/workflow-state:planning-inline]
|
|
265
300
|
|
|
266
301
|
### Phase 2: Execute
|
|
@@ -294,6 +329,7 @@ Dispatch prompt starts with `Active task: <task path from task.py current>`. Rea
|
|
|
294
329
|
Flow: `trellis-before-dev` -> edit -> `trellis-check` -> validation -> `trellis-update-spec` -> commit (Phase 3.4) -> `/trellis:finish-work`.
|
|
295
330
|
If implementation discovers a material unresolved decision, record `decision-needed`, run `task.py replan <task> "<reason>"`, and return to the planning frontier; native questions are planning-only.
|
|
296
331
|
Do not dispatch implement/check sub-agents in inline mode.
|
|
332
|
+
Explicit Channel independent-evidence subnodes remain available under the selected workflow; required independent evidence cannot be replaced by main-session pass claims.
|
|
297
333
|
Read context: `prd.md` -> `design.md if present` -> `implement.md if present`, plus relevant spec/research loaded by skills.
|
|
298
334
|
[/workflow-state:in_progress-inline]
|
|
299
335
|
|
|
@@ -506,7 +542,8 @@ Skip this step. Context is loaded directly by the `trellis-before-dev` skill in
|
|
|
506
542
|
|
|
507
543
|
This step applies only to change-bearing tasks. An eligible analysis-only task stays in `planning` and, after its evidence work is complete, continues directly to Phase 3.3 without running `task.py start`.
|
|
508
544
|
|
|
509
|
-
After the Planning Seal closure pass
|
|
545
|
+
After the Planning Seal closure pass, artifact review, and applicable native
|
|
546
|
+
plan seal/approve gates described above, flip the task status to `in_progress`:
|
|
510
547
|
|
|
511
548
|
```bash
|
|
512
549
|
python3 ./.trellis/scripts/task.py start <task-dir>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pennixrv/trellis",
|
|
3
|
-
"version": "0.7.0-beta.
|
|
3
|
+
"version": "0.7.0-beta.34",
|
|
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.7.0-beta.
|
|
37
|
+
"@pennixrv/trellis-core": "0.7.0-beta.34"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@eslint/js": "^9.18.0",
|