@pennixrv/trellis 0.7.1-beta.11 → 0.7.1-beta.13
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/teamwork/kill.d.ts +2 -0
- package/dist/commands/teamwork/kill.d.ts.map +1 -1
- package/dist/commands/teamwork/kill.js +79 -38
- package/dist/commands/teamwork/kill.js.map +1 -1
- package/dist/commands/teamwork/messages.d.ts.map +1 -1
- package/dist/commands/teamwork/messages.js +3 -0
- package/dist/commands/teamwork/messages.js.map +1 -1
- package/dist/commands/teamwork/process-identity.d.ts +3 -0
- package/dist/commands/teamwork/process-identity.d.ts.map +1 -0
- package/dist/commands/teamwork/process-identity.js +24 -0
- package/dist/commands/teamwork/process-identity.js.map +1 -0
- package/dist/commands/teamwork/rm.d.ts.map +1 -1
- package/dist/commands/teamwork/rm.js +57 -64
- package/dist/commands/teamwork/rm.js.map +1 -1
- package/dist/commands/teamwork/run.d.ts.map +1 -1
- package/dist/commands/teamwork/run.js +6 -3
- package/dist/commands/teamwork/run.js.map +1 -1
- package/dist/commands/teamwork/store/lock.js +28 -4
- package/dist/commands/teamwork/store/lock.js.map +1 -1
- package/dist/commands/teamwork/supervisor/stdout.d.ts.map +1 -1
- package/dist/commands/teamwork/supervisor/stdout.js +1 -2
- package/dist/commands/teamwork/supervisor/stdout.js.map +1 -1
- package/dist/commands/teamwork/supervisor/warning.d.ts.map +1 -1
- package/dist/commands/teamwork/supervisor/warning.js +2 -3
- package/dist/commands/teamwork/supervisor/warning.js.map +1 -1
- package/dist/commands/teamwork/supervisor.d.ts.map +1 -1
- package/dist/commands/teamwork/supervisor.js +18 -12
- package/dist/commands/teamwork/supervisor.js.map +1 -1
- package/dist/migrations/manifests/0.7.1-beta.12.json +9 -0
- package/dist/migrations/manifests/0.7.1-beta.13.json +9 -0
- package/dist/templates/codex/hooks/session-start.py +14 -11
- package/dist/templates/common/bundled-skills/trellis-solution-design-and-convergence/workflow.md +2 -0
- package/dist/templates/common/bundled-skills/trellis-task-implementation/workflow.md +2 -0
- package/dist/templates/copilot/hooks/session-start.py +14 -11
- package/dist/templates/shared-hooks/session-start.py +15 -11
- package/dist/templates/trellis/scripts/common/task_continuity.py +17 -8
- package/dist/templates/trellis/scripts/common/task_workflow.py +12 -2
- package/package.json +2 -2
|
@@ -429,8 +429,7 @@ def _get_task_status(trellis_dir: Path, input_data: dict) -> str:
|
|
|
429
429
|
return (
|
|
430
430
|
"Status: NO ACTIVE TASK\n"
|
|
431
431
|
"Next-Action: Classify the current turn before creating any Trellis task. "
|
|
432
|
-
"
|
|
433
|
-
"Complex task asks whether task creation and planning are allowed."
|
|
432
|
+
"Route through trellis-request-intake using the existing authorization and taskless contract."
|
|
434
433
|
)
|
|
435
434
|
|
|
436
435
|
task_ref = active.task_path
|
|
@@ -443,12 +442,10 @@ def _get_task_status(trellis_dir: Path, input_data: dict) -> str:
|
|
|
443
442
|
)
|
|
444
443
|
|
|
445
444
|
task_json_path = task_dir / "task.json"
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
except (json.JSONDecodeError, PermissionError):
|
|
451
|
-
pass # Optional task metadata; fall back to generic status.
|
|
445
|
+
from common.io import read_json_checked # type: ignore[import-not-found]
|
|
446
|
+
task_data, _reason = read_json_checked(task_json_path)
|
|
447
|
+
if task_data is None or task_data.get("status") not in ("planning", "in_progress", "completed", "cancelled"):
|
|
448
|
+
return f"Status: TASK_ERROR\nTask: {task_ref}\nNext-Action: Repair the existing task record through its owner before continuing; do not create a replacement task."
|
|
452
449
|
|
|
453
450
|
task_title = task_data.get("title", task_ref)
|
|
454
451
|
task_status = task_data.get("status", "unknown")
|
|
@@ -698,13 +695,15 @@ def _build_compact_current_state(
|
|
|
698
695
|
active = _resolve_active_task(trellis_dir, input_data)
|
|
699
696
|
if active.task_path:
|
|
700
697
|
task_dir = _resolve_task_dir(trellis_dir, active.task_path)
|
|
701
|
-
status = "
|
|
698
|
+
status = "task_error"
|
|
702
699
|
task_json = task_dir / "task.json"
|
|
703
700
|
if task_json.is_file():
|
|
704
701
|
try:
|
|
705
702
|
data = json.loads(task_json.read_text(encoding="utf-8"))
|
|
706
703
|
if isinstance(data, dict):
|
|
707
|
-
|
|
704
|
+
value = data.get("status")
|
|
705
|
+
if value in ("planning", "in_progress", "completed", "cancelled"):
|
|
706
|
+
status = value
|
|
708
707
|
except (json.JSONDecodeError, OSError):
|
|
709
708
|
pass # Optional task metadata; fall back to generic status.
|
|
710
709
|
lines.append(f"Current task: {_repo_relative(repo_root, task_dir)}; status={status}.")
|
|
@@ -859,7 +858,12 @@ def main():
|
|
|
859
858
|
project_dir = Path(_normalize_windows_shell_path(val)).resolve()
|
|
860
859
|
break
|
|
861
860
|
if project_dir is None:
|
|
862
|
-
|
|
861
|
+
raw_cwd = hook_input.get("cwd")
|
|
862
|
+
project_dir = Path(_normalize_windows_shell_path(raw_cwd if isinstance(raw_cwd, str) and raw_cwd else ".")).resolve()
|
|
863
|
+
|
|
864
|
+
project_dir = next((candidate for candidate in (project_dir, *project_dir.parents) if (candidate / ".trellis").is_dir()), None)
|
|
865
|
+
if project_dir is None or not (project_dir / ".trellis" / "scripts" / "common" / "active_task.py").is_file():
|
|
866
|
+
return
|
|
863
867
|
|
|
864
868
|
trellis_dir = project_dir / ".trellis"
|
|
865
869
|
context_key = _resolve_context_key(trellis_dir, hook_input)
|
|
@@ -313,6 +313,10 @@ def transfer_task_ownership(args: argparse.Namespace) -> int:
|
|
|
313
313
|
root = get_repo_root()
|
|
314
314
|
operation = args.transfer_action
|
|
315
315
|
try:
|
|
316
|
+
package_bound = args.package_id is not None or args.package_digest is not None
|
|
317
|
+
if operation != "status" or package_bound:
|
|
318
|
+
if not isinstance(args.package_id, str) or not PACKAGE_ID_RE.fullmatch(args.package_id) or not isinstance(args.package_digest, str) or not PACKAGE_DIGEST_RE.fullmatch(args.package_digest):
|
|
319
|
+
raise ContinuityError("TASK_TRANSFER_INVALID_INPUT", "session package identity and SHA-256 digest are required")
|
|
316
320
|
task_dir = resolve_task_dir(args.task_path, root)
|
|
317
321
|
if task_dir is None or not is_within_tasks_dir(task_dir, root):
|
|
318
322
|
raise ContinuityError("TASK_TRANSFER_WITHHELD", "task path is unavailable or unsafe")
|
|
@@ -328,8 +332,10 @@ def transfer_task_ownership(args: argparse.Namespace) -> int:
|
|
|
328
332
|
path = _ownership_path(root, task_path)
|
|
329
333
|
with _record_lock(path):
|
|
330
334
|
record = _load_owner(path)
|
|
335
|
+
if record and (record["task_id"] != task_id or record["task_path"] != task_path):
|
|
336
|
+
raise ContinuityError("TASK_TRANSFER_WITHHELD", "ownership record does not match the requested task")
|
|
331
337
|
if operation == "status":
|
|
332
|
-
if record and (record["package_id"] != args.package_id or record["package_digest"] != args.package_digest):
|
|
338
|
+
if record and package_bound and (record["package_id"] != args.package_id or record["package_digest"] != args.package_digest):
|
|
333
339
|
raise ContinuityError("TASK_TRANSFER_STALE_PACKAGE", "session package identity or digest changed")
|
|
334
340
|
result = record or {"status": "absent", "generation": 0, "task_id": task_id}
|
|
335
341
|
if args.include_current:
|
|
@@ -342,8 +348,6 @@ def transfer_task_ownership(args: argparse.Namespace) -> int:
|
|
|
342
348
|
if args.expected_generation is None or args.expected_generation < 0:
|
|
343
349
|
raise ContinuityError("TASK_TRANSFER_INVALID_INPUT", "a non-negative expected generation is required")
|
|
344
350
|
if operation == "quiesce":
|
|
345
|
-
if not PACKAGE_ID_RE.fullmatch(args.package_id) or not PACKAGE_DIGEST_RE.fullmatch(args.package_digest):
|
|
346
|
-
raise ContinuityError("TASK_TRANSFER_INVALID_INPUT", "session package identity or SHA-256 digest is invalid")
|
|
347
351
|
context_key, current_path = _direct_pointer(root, task_path)
|
|
348
352
|
if current_path is None:
|
|
349
353
|
raise ContinuityError("TASK_TRANSFER_WITHHELD", "source session has no current task")
|
|
@@ -360,13 +364,18 @@ def transfer_task_ownership(args: argparse.Namespace) -> int:
|
|
|
360
364
|
result = record
|
|
361
365
|
print(json.dumps(result, ensure_ascii=False, sort_keys=True))
|
|
362
366
|
return 0
|
|
363
|
-
|
|
364
|
-
|
|
367
|
+
if record["status"] != "consumed" or record["generation"] != args.expected_generation:
|
|
368
|
+
raise ContinuityError("TASK_TRANSFER_STALE_GENERATION", "task already has an ownership record at a different state or generation")
|
|
369
|
+
if record["target_session_id"] != context_key:
|
|
370
|
+
raise ContinuityError("TASK_TRANSFER_WITHHELD", "only the consumed target can start another handoff")
|
|
371
|
+
if record["package_id"] == args.package_id:
|
|
372
|
+
raise ContinuityError("TASK_TRANSFER_STALE_PACKAGE", "another handoff requires a new session package")
|
|
373
|
+
elif args.expected_generation != 0:
|
|
365
374
|
raise ContinuityError("TASK_TRANSFER_STALE_GENERATION", "initial ownership generation must be 0")
|
|
366
375
|
record = {
|
|
367
376
|
"schema_version": 1, "task_id": task_id, "task_path": task_path,
|
|
368
377
|
"package_id": args.package_id, "package_digest": args.package_digest,
|
|
369
|
-
"generation": 1, "status": "quiescing", "source_session_id": context_key,
|
|
378
|
+
"generation": args.expected_generation + 1, "status": "quiescing", "source_session_id": context_key,
|
|
370
379
|
"target_session_id": None, "continuity_digest": continuity_digest,
|
|
371
380
|
"route": _route_snapshot(data),
|
|
372
381
|
}
|
|
@@ -491,8 +500,8 @@ def add_continuity_commands(subparsers) -> None:
|
|
|
491
500
|
transfer.add_argument("transfer_action", choices=("status", "quiesce", "seal", "retire", "claim", "consume", "release"))
|
|
492
501
|
transfer.add_argument("--task-id", required=True)
|
|
493
502
|
transfer.add_argument("--task-path", required=True)
|
|
494
|
-
transfer.add_argument("--package-id",
|
|
495
|
-
transfer.add_argument("--package-digest",
|
|
503
|
+
transfer.add_argument("--package-id", help="Required with --package-digest for mutations or package-bound status")
|
|
504
|
+
transfer.add_argument("--package-digest", help="Required with --package-id for mutations or package-bound status")
|
|
496
505
|
transfer.add_argument("--expected-continuity-digest", help="Expected live projection for sealed-source claim recovery")
|
|
497
506
|
transfer.add_argument("--expected-generation", type=int)
|
|
498
507
|
transfer.add_argument("--include-current", action="store_true", help="Include the live task projection and its digest in transfer status")
|
|
@@ -133,6 +133,10 @@ def implementation_start_ready(data: dict, task_dir: Path) -> bool:
|
|
|
133
133
|
phase_ids = state.get("phase_ids")
|
|
134
134
|
if not isinstance(phase_ids, list) or PHASE_IDS[3] not in phase_ids:
|
|
135
135
|
return False
|
|
136
|
+
phases = state.get("phases")
|
|
137
|
+
implementation = phases.get(PHASE_IDS[3]) if isinstance(phases, dict) else None
|
|
138
|
+
if not isinstance(implementation, dict) or implementation.get("status") != "active":
|
|
139
|
+
return False
|
|
136
140
|
if state.get("task_kind") not in {"implementation", "combined"}:
|
|
137
141
|
return False
|
|
138
142
|
authorization = state.get("implementation_authorization")
|
|
@@ -460,11 +464,12 @@ def _update(args: argparse.Namespace, task_dir: Path, data: dict, state: dict) -
|
|
|
460
464
|
current, record = PHASE_IDS[4], state["phases"][PHASE_IDS[4]]
|
|
461
465
|
else:
|
|
462
466
|
current, record = _phase(state)
|
|
467
|
+
accepted_design_return = action == "return" and current == PHASE_IDS[2] and record.get("status") == "accepted"
|
|
463
468
|
returning_to_requirements = action == "return" and args.target_phase == PHASE_IDS[0]
|
|
464
469
|
rerouting_to_requirements = action == "route" and args.target_phase == PHASE_IDS[0]
|
|
465
470
|
if current != PHASE_IDS[0] and not returning_to_requirements and not rerouting_to_requirements:
|
|
466
471
|
_require_current_requirements_confirmation(task_dir, state)
|
|
467
|
-
if action not in {"show", "resume", "route", "authorize", "admission"} and record.get("status") != "active" and not reviewed_delivery:
|
|
472
|
+
if action not in {"show", "resume", "route", "authorize", "admission"} and record.get("status") != "active" and not reviewed_delivery and not accepted_design_return:
|
|
468
473
|
raise TaskWorkflowError("TASK_PHASE_INVALID_TRANSITION", "only the active phase can be changed")
|
|
469
474
|
|
|
470
475
|
if action == "artifact":
|
|
@@ -565,7 +570,7 @@ def _update(args: argparse.Namespace, task_dir: Path, data: dict, state: dict) -
|
|
|
565
570
|
target = _required_text(args.target_phase, "--target-phase")
|
|
566
571
|
if target not in state["phase_ids"] or (
|
|
567
572
|
state["phase_ids"].index(target) >= state["phase_ids"].index(current)
|
|
568
|
-
and not (reviewed_delivery and target == current)
|
|
573
|
+
and not ((reviewed_delivery or accepted_design_return) and target == current)
|
|
569
574
|
):
|
|
570
575
|
raise TaskWorkflowError("TASK_PHASE_INVALID_TRANSITION", "return target must be an earlier phase in the current route")
|
|
571
576
|
reason = _required_text(args.reason, "--reason")
|
|
@@ -760,6 +765,11 @@ def cmd_delegate_start(args: argparse.Namespace) -> int:
|
|
|
760
765
|
child_data, child_meta = _load(child_dir)
|
|
761
766
|
if child_data.get("parent") != parent_dir.name or child_dir.name not in parent_data.get("children", []):
|
|
762
767
|
raise TaskWorkflowError("TASK_DELEGATE_PARENT_LINK_INVALID", "parent and child task links do not match")
|
|
768
|
+
from .task_continuity import assert_task_mutation_allowed
|
|
769
|
+
|
|
770
|
+
assert_task_mutation_allowed(repo_root, parent_dir, parent_data)
|
|
771
|
+
if child_data.get("status") not in {"planning", "in_progress"}:
|
|
772
|
+
raise TaskWorkflowError("TASK_DELEGATE_CHILD_TERMINAL", "only a planning or in-progress child task can be delegated")
|
|
763
773
|
if parent_data.get("status") != "in_progress" or not implementation_start_ready(parent_data, parent_dir):
|
|
764
774
|
raise TaskWorkflowError("TASK_DELEGATE_IMPLEMENTATION_NOT_AUTHORIZED", "parent task is not at an authorized Task Implementation and Test Verification Phase entry")
|
|
765
775
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pennixrv/trellis",
|
|
3
|
-
"version": "0.7.1-beta.
|
|
3
|
+
"version": "0.7.1-beta.13",
|
|
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.1-beta.
|
|
37
|
+
"@pennixrv/trellis-core": "0.7.1-beta.13"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@eslint/js": "^9.18.0",
|