bmsdna-devtools 0.28.0__tar.gz → 0.28.1__tar.gz

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 (76) hide show
  1. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/PKG-INFO +40 -1
  2. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/README.md +39 -0
  3. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/bmsdna/devtools/cli.py +26 -0
  4. bmsdna_devtools-0.28.1/bmsdna/devtools/issue_do.py +71 -0
  5. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/pyproject.toml +1 -1
  6. bmsdna_devtools-0.28.1/tests/test_issue_do.py +20 -0
  7. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/uv.lock +1 -1
  8. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/.github/workflows/auto-release.yml +0 -0
  9. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/.github/workflows/python-publish.yml +0 -0
  10. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/.github/workflows/python-test.yml +0 -0
  11. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/.gitignore +0 -0
  12. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/.python-version +0 -0
  13. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/bmsdna/devtools/__init__.py +0 -0
  14. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/bmsdna/devtools/ado_auth.py +0 -0
  15. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/bmsdna/devtools/ado_issue.py +0 -0
  16. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/bmsdna/devtools/app_service_logs.py +0 -0
  17. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/bmsdna/devtools/bdt_config.py +0 -0
  18. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/bmsdna/devtools/cli_tools.py +0 -0
  19. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/bmsdna/devtools/commit.py +0 -0
  20. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/bmsdna/devtools/env_config.py +0 -0
  21. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/bmsdna/devtools/find_injection.py +0 -0
  22. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/bmsdna/devtools/find_repo.py +0 -0
  23. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/bmsdna/devtools/gh_issue.py +0 -0
  24. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/bmsdna/devtools/gh_pr.py +0 -0
  25. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/bmsdna/devtools/gitrepo.py +0 -0
  26. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/bmsdna/devtools/injection_frontend.py +0 -0
  27. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/bmsdna/devtools/injection_python.py +0 -0
  28. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/bmsdna/devtools/lint.py +0 -0
  29. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/bmsdna/devtools/lint_findings.py +0 -0
  30. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/bmsdna/devtools/lint_models.py +0 -0
  31. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/bmsdna/devtools/lint_sql.py +0 -0
  32. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/bmsdna/devtools/lint_tooling.py +0 -0
  33. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/bmsdna/devtools/lint_typescript.py +0 -0
  34. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/bmsdna/devtools/logs.py +0 -0
  35. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/bmsdna/devtools/pr_build.py +0 -0
  36. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/bmsdna/devtools/pr_issue_link.py +0 -0
  37. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/bmsdna/devtools/pr_labels.py +0 -0
  38. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/bmsdna/devtools/pr_markdown.py +0 -0
  39. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/bmsdna/devtools/pull.py +0 -0
  40. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/bmsdna/devtools/testdb.py +0 -0
  41. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/bmsdna/devtools/worktree.py +0 -0
  42. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/justfile +0 -0
  43. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/skills/bmsdna-devtools/SKILL.md +0 -0
  44. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/tests/conftest.py +0 -0
  45. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/tests/test_ado_auth.py +0 -0
  46. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/tests/test_ado_issue.py +0 -0
  47. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/tests/test_ado_issue_flow.py +0 -0
  48. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/tests/test_app_service_logs.py +0 -0
  49. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/tests/test_bdt_config.py +0 -0
  50. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/tests/test_cli_cleanup.py +0 -0
  51. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/tests/test_cli_commit.py +0 -0
  52. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/tests/test_cli_issue.py +0 -0
  53. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/tests/test_cli_pr_create.py +0 -0
  54. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/tests/test_cli_pr_id.py +0 -0
  55. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/tests/test_cli_pull.py +0 -0
  56. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/tests/test_cli_tools.py +0 -0
  57. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/tests/test_commit.py +0 -0
  58. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/tests/test_env_config.py +0 -0
  59. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/tests/test_find_injection.py +0 -0
  60. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/tests/test_find_repo.py +0 -0
  61. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/tests/test_gh_issue.py +0 -0
  62. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/tests/test_gh_pr.py +0 -0
  63. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/tests/test_gitrepo.py +0 -0
  64. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/tests/test_lint.py +0 -0
  65. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/tests/test_lint_models.py +0 -0
  66. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/tests/test_lint_sql.py +0 -0
  67. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/tests/test_lint_tooling.py +0 -0
  68. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/tests/test_lint_typescript.py +0 -0
  69. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/tests/test_pr_build.py +0 -0
  70. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/tests/test_pr_build_flow.py +0 -0
  71. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/tests/test_pr_issue_link.py +0 -0
  72. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/tests/test_pr_labels.py +0 -0
  73. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/tests/test_pr_markdown.py +0 -0
  74. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/tests/test_pull.py +0 -0
  75. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/tests/test_testdb.py +0 -0
  76. {bmsdna_devtools-0.28.0 → bmsdna_devtools-0.28.1}/tests/test_worktree_clean.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: bmsdna-devtools
3
- Version: 0.28.0
3
+ Version: 0.28.1
4
4
  Summary: Shared Azure DevOps / GitHub / git / Azure Monitor developer tooling for BMS projects
5
5
  Requires-Python: >=3.14
6
6
  Requires-Dist: pgdevkit[db]>=0.7.1
@@ -287,6 +287,17 @@ CLI flag beats `[tool.bdt.github].board` in `pyproject.toml`:
287
287
  board = "Roadmap"
288
288
  ```
289
289
 
290
+ ## `bdt issue do`
291
+
292
+ ```bash
293
+ bdt issue do 60 [--agent claude] [--dry-run] [extra agent args...]
294
+ ```
295
+
296
+ Hands an issue (GitHub) or work item (Azure DevOps) to a coding agent. Fetches the title and
297
+ description and runs `claude -p "<prompt>" --name "60: <title>"`, so the session is easy to find
298
+ with `claude --resume`. `--agent` picks another executable (it then gets just the prompt as its
299
+ last argument); extra args are passed through; `--dry-run` prints the command.
300
+
290
301
  ## `bdt worktree`
291
302
 
292
303
  ```bash
@@ -297,6 +308,34 @@ Creates `.worktrees/<name>` branched from `--base`, initializes submodules
297
308
  (unless `--no-submodules`), and copies an env file into the new worktree as
298
309
  `.env` (auto-detects `.local_env` then `.env` if `--env-file` isn't given).
299
310
 
311
+ ## `bdt find-repo`
312
+
313
+ ```bash
314
+ bdt find-repo my-repo [--root ~/projects] [--org MYORG] [--github-org MYORG] [--yes] [--pat PAT]
315
+ ```
316
+
317
+ Finds a repo by name: first among the local clones under the work dir, then -- only if there is
318
+ no local match -- in an Azure DevOps org and/or a GitHub org. Names match exactly
319
+ (case-insensitive) if any repo has that name, otherwise by substring. Local matches are printed
320
+ as paths. A single remote-only match is offered for cloning (`--yes` skips the prompt; without a
321
+ TTY it never prompts and just tells you to pass `--yes`); several matches are listed and nothing
322
+ is cloned.
323
+
324
+ Clone destinations: `<root>/<project>/<repo>` for Azure DevOps, `<root>/github/<repo>` for GitHub.
325
+
326
+ Configuration (flags override env vars):
327
+
328
+ | Setting | Flag | Env vars | Default |
329
+ | --- | --- | --- | --- |
330
+ | Local work dir | `--root` | `AZDO_WORK_DIR`, `BMS_WORK_DIR` | `~/projects` (`C:/Projects` on Windows) |
331
+ | Azure DevOps org | `--org` | `AZDO_ORG`, `BMS_ORG` | none (ADO not searched) |
332
+ | GitHub org | `--github-org` | `GITHUB_ORG`, `BMS_GITHUB_ORG` | none (GitHub not searched) |
333
+ | ADO PAT for cloning | `--pat` | `AZURE_DEVOPS_EXT_PAT`, `AZURE_DEVOPS_PAT` | falls back to `az` login |
334
+
335
+ Set the org env vars in your shell profile to make `bdt find-repo <name>` work anywhere. It replaces
336
+ the `cross-repo-discovery` skill's `ALL_REPOS.md` sync. Needs `az` (ADO) / `gh` (GitHub) only when
337
+ those orgs are searched.
338
+
300
339
  ## `bdt cleanup worktrees` / `bdt cleanup orphaned-dbs` / `bdt cleanup db` / `bdt cleanup worktree`
301
340
 
302
341
  ```bash
@@ -274,6 +274,17 @@ CLI flag beats `[tool.bdt.github].board` in `pyproject.toml`:
274
274
  board = "Roadmap"
275
275
  ```
276
276
 
277
+ ## `bdt issue do`
278
+
279
+ ```bash
280
+ bdt issue do 60 [--agent claude] [--dry-run] [extra agent args...]
281
+ ```
282
+
283
+ Hands an issue (GitHub) or work item (Azure DevOps) to a coding agent. Fetches the title and
284
+ description and runs `claude -p "<prompt>" --name "60: <title>"`, so the session is easy to find
285
+ with `claude --resume`. `--agent` picks another executable (it then gets just the prompt as its
286
+ last argument); extra args are passed through; `--dry-run` prints the command.
287
+
277
288
  ## `bdt worktree`
278
289
 
279
290
  ```bash
@@ -284,6 +295,34 @@ Creates `.worktrees/<name>` branched from `--base`, initializes submodules
284
295
  (unless `--no-submodules`), and copies an env file into the new worktree as
285
296
  `.env` (auto-detects `.local_env` then `.env` if `--env-file` isn't given).
286
297
 
298
+ ## `bdt find-repo`
299
+
300
+ ```bash
301
+ bdt find-repo my-repo [--root ~/projects] [--org MYORG] [--github-org MYORG] [--yes] [--pat PAT]
302
+ ```
303
+
304
+ Finds a repo by name: first among the local clones under the work dir, then -- only if there is
305
+ no local match -- in an Azure DevOps org and/or a GitHub org. Names match exactly
306
+ (case-insensitive) if any repo has that name, otherwise by substring. Local matches are printed
307
+ as paths. A single remote-only match is offered for cloning (`--yes` skips the prompt; without a
308
+ TTY it never prompts and just tells you to pass `--yes`); several matches are listed and nothing
309
+ is cloned.
310
+
311
+ Clone destinations: `<root>/<project>/<repo>` for Azure DevOps, `<root>/github/<repo>` for GitHub.
312
+
313
+ Configuration (flags override env vars):
314
+
315
+ | Setting | Flag | Env vars | Default |
316
+ | --- | --- | --- | --- |
317
+ | Local work dir | `--root` | `AZDO_WORK_DIR`, `BMS_WORK_DIR` | `~/projects` (`C:/Projects` on Windows) |
318
+ | Azure DevOps org | `--org` | `AZDO_ORG`, `BMS_ORG` | none (ADO not searched) |
319
+ | GitHub org | `--github-org` | `GITHUB_ORG`, `BMS_GITHUB_ORG` | none (GitHub not searched) |
320
+ | ADO PAT for cloning | `--pat` | `AZURE_DEVOPS_EXT_PAT`, `AZURE_DEVOPS_PAT` | falls back to `az` login |
321
+
322
+ Set the org env vars in your shell profile to make `bdt find-repo <name>` work anywhere. It replaces
323
+ the `cross-repo-discovery` skill's `ALL_REPOS.md` sync. Needs `az` (ADO) / `gh` (GitHub) only when
324
+ those orgs are searched.
325
+
287
326
  ## `bdt cleanup worktrees` / `bdt cleanup orphaned-dbs` / `bdt cleanup db` / `bdt cleanup worktree`
288
327
 
289
328
  ```bash
@@ -15,6 +15,7 @@ from pgdevkit.testdb import constants as pgdevkit_constants
15
15
  from . import ado_issue, app_service_logs, commit as commit_mod
16
16
  from . import env_config
17
17
  from . import find_repo as find_repo_mod
18
+ from . import issue_do as issue_do_mod
18
19
  from . import gh_issue, gh_pr
19
20
  from . import find_injection as find_injection_mod
20
21
  from . import lint as lint_mod
@@ -694,6 +695,31 @@ def issue_search(
694
695
  )
695
696
 
696
697
 
698
+ @issue_app.command("do", context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
699
+ def issue_do(
700
+ ctx: typer.Context,
701
+ number: int = typer.Argument(..., help="Issue number (GitHub) or work item ID (Azure DevOps)"),
702
+ agent: str = typer.Option("claude", "--agent", help="Agent executable to start (claude runs headless: `claude -p ... --name '<number>: <title>'`)"),
703
+ dry_run: bool = typer.Option(False, "--dry-run", help="Print the agent command instead of running it"),
704
+ pat: str | None = typer.Option(
705
+ None,
706
+ "--pat",
707
+ envvar=["AZURE_DEVOPS_EXT_PAT", "AZURE_DEVOPS_PAT"],
708
+ help="Azure DevOps PAT (else falls back to `az` login)",
709
+ ),
710
+ ) -> None:
711
+ """Start a coding agent on an issue / work item, with the session named '<number>: <title>'.
712
+ Extra args after the number are passed through to the agent."""
713
+ remote = current_remote()
714
+ if isinstance(remote, GitHubRemote):
715
+ title, body = issue_do_mod.fetch_github(require_gh(), number)
716
+ else:
717
+ session = requests.Session()
718
+ session.headers.update(auth_header(pat))
719
+ title, body = issue_do_mod.fetch_ado(session, remote, number)
720
+ issue_do_mod.run(number, title, body, agent=agent, extra=list(ctx.args), dry_run=dry_run)
721
+
722
+
697
723
  @issue_app.command("update")
698
724
  def issue_update(
699
725
  number: int = typer.Argument(..., help="Issue number (GitHub) or work item ID (Azure DevOps)"),
@@ -0,0 +1,71 @@
1
+ """`bdt issue do <number>`: hand an issue / work item to a coding agent.
2
+
3
+ Fetches the issue's title and description (GitHub via `gh`, Azure DevOps via the REST API)
4
+ and starts the agent headless (`claude -p`), naming the session `<number>: <title>` so it is
5
+ easy to find again with `claude --resume`.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import re
12
+ import shutil
13
+ import subprocess
14
+ import sys
15
+
16
+ import requests
17
+
18
+ from .ado_issue import _base_url
19
+ from .gitrepo import AdoRemote
20
+
21
+ _HTML_TAG_RE = re.compile(r"<[^>]+>")
22
+
23
+
24
+ def fetch_github(gh: str, number: int) -> tuple[str, str]:
25
+ result = subprocess.run([gh, "issue", "view", str(number), "--json", "title,body"], capture_output=True, text=True, check=False)
26
+ if result.returncode != 0:
27
+ sys.exit(f"gh issue view {number} failed:\n{result.stderr.strip()}")
28
+ data = json.loads(result.stdout)
29
+ return data["title"], data.get("body") or ""
30
+
31
+
32
+ def fetch_ado(session: requests.Session, remote: AdoRemote, number: int) -> tuple[str, str]:
33
+ r = session.get(
34
+ f"{_base_url(remote)}/_apis/wit/workitems/{number}",
35
+ params={"fields": "System.Title,System.Description", "api-version": "7.1"},
36
+ )
37
+ r.raise_for_status()
38
+ fields = r.json()["fields"]
39
+ # ADO descriptions are HTML; the agent only needs the text.
40
+ description = _HTML_TAG_RE.sub("", (fields.get("System.Description") or "").replace("<br>", "\n").replace("</div>", "\n"))
41
+ return fields["System.Title"], description.strip()
42
+
43
+
44
+ def session_name(number: int, title: str) -> str:
45
+ return f"{number}: {title}"
46
+
47
+
48
+ def build_prompt(number: int, title: str, body: str) -> str:
49
+ return (
50
+ f"Work on issue {number}: {title}\n\n{body.strip() or '(no description)'}\n\n"
51
+ "Follow the dev-workflow skill: worktree, draft PR early, implement, test, publish the PR "
52
+ f"and reference issue {number} in it. Do not merge."
53
+ )
54
+
55
+
56
+ def build_command(agent: str, number: int, title: str, body: str, extra: list[str]) -> list[str]:
57
+ prompt = build_prompt(number, title, body)
58
+ if agent == "claude":
59
+ return [agent, "-p", prompt, "--name", session_name(number, title), *extra]
60
+ # Other agents: no known naming flag, so just hand over the prompt.
61
+ return [agent, *extra, prompt]
62
+
63
+
64
+ def run(number: int, title: str, body: str, *, agent: str, extra: list[str], dry_run: bool) -> None:
65
+ cmd = build_command(agent, number, title, body, extra)
66
+ if dry_run:
67
+ print(" ".join(json.dumps(c) if " " in c or "\n" in c else c for c in cmd))
68
+ return
69
+ if shutil.which(agent) is None:
70
+ sys.exit(f"'{agent}' is required for this command but wasn't found on PATH.")
71
+ raise SystemExit(subprocess.run(cmd, check=False).returncode)
@@ -10,7 +10,7 @@ packages = ["bmsdna"]
10
10
 
11
11
  [project]
12
12
  name = "bmsdna-devtools"
13
- version = "0.28.0"
13
+ version = "0.28.1"
14
14
  description = "Shared Azure DevOps / GitHub / git / Azure Monitor developer tooling for BMS projects"
15
15
  readme = "README.md"
16
16
  requires-python = ">=3.14" # pgdevkit>=0.7.1 requires 3.14; was >=3.11 before adding it as a dependency
@@ -0,0 +1,20 @@
1
+ from bmsdna.devtools import issue_do
2
+
3
+
4
+ def test_claude_command_names_session_and_is_headless() -> None:
5
+ cmd = issue_do.build_command("claude", 60, "add command", "body text", ["--model", "opus"])
6
+ assert cmd[:2] == ["claude", "-p"]
7
+ assert "body text" in cmd[2]
8
+ assert cmd[3:5] == ["--name", "60: add command"]
9
+ assert cmd[-2:] == ["--model", "opus"]
10
+
11
+
12
+ def test_other_agent_gets_prompt_last() -> None:
13
+ cmd = issue_do.build_command("codex", 7, "t", "", ["exec"])
14
+ assert cmd[:2] == ["codex", "exec"]
15
+ assert "(no description)" in cmd[2]
16
+
17
+
18
+ def test_dry_run_prints(capsys) -> None:
19
+ issue_do.run(1, "x", "y", agent="claude", extra=[], dry_run=True)
20
+ assert '"1: x"' in capsys.readouterr().out
@@ -22,7 +22,7 @@ wheels = [
22
22
 
23
23
  [[package]]
24
24
  name = "bmsdna-devtools"
25
- version = "0.28.0"
25
+ version = "0.28.1"
26
26
  source = { editable = "." }
27
27
  dependencies = [
28
28
  { name = "pgdevkit", extra = ["db"] },