@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.
Files changed (32) hide show
  1. package/dist/commands/channel/index.d.ts.map +1 -1
  2. package/dist/commands/channel/index.js +15 -11
  3. package/dist/commands/channel/index.js.map +1 -1
  4. package/dist/commands/channel/spawn.d.ts.map +1 -1
  5. package/dist/commands/channel/spawn.js +16 -11
  6. package/dist/commands/channel/spawn.js.map +1 -1
  7. package/dist/migrations/manifests/0.7.0-beta.33.json +9 -0
  8. package/dist/migrations/manifests/0.7.0-beta.34.json +9 -0
  9. package/dist/templates/common/bundled-skills/trellis-channel/SKILL.md +3 -1
  10. package/dist/templates/common/bundled-skills/trellis-channel/references/command-reference.md +11 -10
  11. package/dist/templates/common/bundled-skills/trellis-channel/references/forum.md +4 -12
  12. package/dist/templates/common/bundled-skills/trellis-channel/references/multi-target-dispatch.md +113 -0
  13. package/dist/templates/common/bundled-skills/trellis-channel/references/progress-debugging.md +4 -4
  14. package/dist/templates/common/bundled-skills/trellis-channel/references/subnode-work.md +32 -7
  15. package/dist/templates/common/bundled-skills/trellis-channel/references/workers.md +2 -3
  16. package/dist/templates/common/bundled-skills/trellis-channel/references/workflows.md +11 -12
  17. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-task-lifecycle.md +6 -2
  18. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/task-system.md +6 -1
  19. package/dist/templates/common/commands/continue.md +3 -2
  20. package/dist/templates/common/commands/start.md +1 -0
  21. package/dist/templates/common/skills/brainstorm.md +14 -1
  22. package/dist/templates/shared-hooks/inject-workflow-state.py +2 -1
  23. package/dist/templates/trellis/index.d.ts +1 -0
  24. package/dist/templates/trellis/index.d.ts.map +1 -1
  25. package/dist/templates/trellis/index.js +2 -0
  26. package/dist/templates/trellis/index.js.map +1 -1
  27. package/dist/templates/trellis/scripts/common/task_planning.py +133 -0
  28. package/dist/templates/trellis/scripts/common/task_store.py +20 -1
  29. package/dist/templates/trellis/scripts/subnode_artifact.py +32 -0
  30. package/dist/templates/trellis/scripts/task.py +96 -2
  31. package/dist/templates/trellis/workflow.md +39 -2
  32. 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 ready.",
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 start <dir> # Set active task, record current branch
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 start <name> # set active task (session-scoped when available)
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 and artifact review, flip the task status to `in_progress`:
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.32",
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.32"
37
+ "@pennixrv/trellis-core": "0.7.0-beta.34"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@eslint/js": "^9.18.0",