@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.
Files changed (38) hide show
  1. package/dist/commands/teamwork/kill.d.ts +2 -0
  2. package/dist/commands/teamwork/kill.d.ts.map +1 -1
  3. package/dist/commands/teamwork/kill.js +79 -38
  4. package/dist/commands/teamwork/kill.js.map +1 -1
  5. package/dist/commands/teamwork/messages.d.ts.map +1 -1
  6. package/dist/commands/teamwork/messages.js +3 -0
  7. package/dist/commands/teamwork/messages.js.map +1 -1
  8. package/dist/commands/teamwork/process-identity.d.ts +3 -0
  9. package/dist/commands/teamwork/process-identity.d.ts.map +1 -0
  10. package/dist/commands/teamwork/process-identity.js +24 -0
  11. package/dist/commands/teamwork/process-identity.js.map +1 -0
  12. package/dist/commands/teamwork/rm.d.ts.map +1 -1
  13. package/dist/commands/teamwork/rm.js +57 -64
  14. package/dist/commands/teamwork/rm.js.map +1 -1
  15. package/dist/commands/teamwork/run.d.ts.map +1 -1
  16. package/dist/commands/teamwork/run.js +6 -3
  17. package/dist/commands/teamwork/run.js.map +1 -1
  18. package/dist/commands/teamwork/store/lock.js +28 -4
  19. package/dist/commands/teamwork/store/lock.js.map +1 -1
  20. package/dist/commands/teamwork/supervisor/stdout.d.ts.map +1 -1
  21. package/dist/commands/teamwork/supervisor/stdout.js +1 -2
  22. package/dist/commands/teamwork/supervisor/stdout.js.map +1 -1
  23. package/dist/commands/teamwork/supervisor/warning.d.ts.map +1 -1
  24. package/dist/commands/teamwork/supervisor/warning.js +2 -3
  25. package/dist/commands/teamwork/supervisor/warning.js.map +1 -1
  26. package/dist/commands/teamwork/supervisor.d.ts.map +1 -1
  27. package/dist/commands/teamwork/supervisor.js +18 -12
  28. package/dist/commands/teamwork/supervisor.js.map +1 -1
  29. package/dist/migrations/manifests/0.7.1-beta.12.json +9 -0
  30. package/dist/migrations/manifests/0.7.1-beta.13.json +9 -0
  31. package/dist/templates/codex/hooks/session-start.py +14 -11
  32. package/dist/templates/common/bundled-skills/trellis-solution-design-and-convergence/workflow.md +2 -0
  33. package/dist/templates/common/bundled-skills/trellis-task-implementation/workflow.md +2 -0
  34. package/dist/templates/copilot/hooks/session-start.py +14 -11
  35. package/dist/templates/shared-hooks/session-start.py +15 -11
  36. package/dist/templates/trellis/scripts/common/task_continuity.py +17 -8
  37. package/dist/templates/trellis/scripts/common/task_workflow.py +12 -2
  38. 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
- "Simple conversation / small task asks only whether this turn should create a Trellis task. "
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
- task_data = {}
447
- if task_json_path.is_file():
448
- try:
449
- task_data = json.loads(task_json_path.read_text(encoding="utf-8"))
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 = "unknown"
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
- status = str(data.get("status") or "unknown")
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
- project_dir = Path(_normalize_windows_shell_path(hook_input.get("cwd", "."))).resolve()
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
- raise ContinuityError("TASK_TRANSFER_STALE_GENERATION", "task already has an ownership record")
364
- if args.expected_generation != 0:
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", required=True)
495
- transfer.add_argument("--package-digest", required=True)
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.11",
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.11"
37
+ "@pennixrv/trellis-core": "0.7.1-beta.13"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@eslint/js": "^9.18.0",