dna-cli 0.1.0__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 (66) hide show
  1. dna_cli-0.1.0/.gitignore +23 -0
  2. dna_cli-0.1.0/LICENSE +21 -0
  3. dna_cli-0.1.0/PKG-INFO +57 -0
  4. dna_cli-0.1.0/README.md +31 -0
  5. dna_cli-0.1.0/dna_cli/__init__.py +87 -0
  6. dna_cli-0.1.0/dna_cli/_active_story.py +134 -0
  7. dna_cli-0.1.0/dna_cli/_banner.py +61 -0
  8. dna_cli-0.1.0/dna_cli/_ctx.py +433 -0
  9. dna_cli-0.1.0/dna_cli/_git_symbiosis.py +167 -0
  10. dna_cli-0.1.0/dna_cli/_github_bridge.py +294 -0
  11. dna_cli-0.1.0/dna_cli/_methodology_gates.py +164 -0
  12. dna_cli-0.1.0/dna_cli/data/git-hooks/prepare-commit-msg +132 -0
  13. dna_cli-0.1.0/dna_cli/doc_cmd.py +842 -0
  14. dna_cli-0.1.0/dna_cli/docs_cmd.py +79 -0
  15. dna_cli-0.1.0/dna_cli/eval_cmd.py +274 -0
  16. dna_cli-0.1.0/dna_cli/hooks_cmd.py +146 -0
  17. dna_cli-0.1.0/dna_cli/install_cmd.py +496 -0
  18. dna_cli-0.1.0/dna_cli/issue_bridge_cmd.py +305 -0
  19. dna_cli-0.1.0/dna_cli/kind_cmd.py +84 -0
  20. dna_cli-0.1.0/dna_cli/memory_cmd.py +298 -0
  21. dna_cli-0.1.0/dna_cli/pr_cmd.py +251 -0
  22. dna_cli-0.1.0/dna_cli/recall_cmd.py +161 -0
  23. dna_cli-0.1.0/dna_cli/research_cmd.py +284 -0
  24. dna_cli-0.1.0/dna_cli/scope_cmd.py +129 -0
  25. dna_cli-0.1.0/dna_cli/sdlc_cmd.py +6549 -0
  26. dna_cli-0.1.0/dna_cli/source_cmd.py +437 -0
  27. dna_cli-0.1.0/dna_cli/testkit_cmd.py +352 -0
  28. dna_cli-0.1.0/pyproject.toml +68 -0
  29. dna_cli-0.1.0/tests/conftest.py +98 -0
  30. dna_cli-0.1.0/tests/test_changelog_cmd.py +81 -0
  31. dna_cli-0.1.0/tests/test_doc_apply_bundle_entries.py +179 -0
  32. dna_cli-0.1.0/tests/test_doc_apply_prompt_budget.py +196 -0
  33. dna_cli-0.1.0/tests/test_docs_cmd.py +130 -0
  34. dna_cli-0.1.0/tests/test_eval_cmd.py +226 -0
  35. dna_cli-0.1.0/tests/test_focus_gates_cli.py +218 -0
  36. dna_cli-0.1.0/tests/test_focus_gates_guards.py +118 -0
  37. dna_cli-0.1.0/tests/test_git_symbiosis_hook.py +211 -0
  38. dna_cli-0.1.0/tests/test_hooks_cli.py +109 -0
  39. dna_cli-0.1.0/tests/test_install_cmd.py +279 -0
  40. dna_cli-0.1.0/tests/test_install_github_real.py +53 -0
  41. dna_cli-0.1.0/tests/test_issue_bridge_cli.py +473 -0
  42. dna_cli-0.1.0/tests/test_issue_comment_cli.py +104 -0
  43. dna_cli-0.1.0/tests/test_kaizen_event.py +317 -0
  44. dna_cli-0.1.0/tests/test_kaizen_transitions.py +183 -0
  45. dna_cli-0.1.0/tests/test_memory_cmd.py +91 -0
  46. dna_cli-0.1.0/tests/test_plan_from_file.py +89 -0
  47. dna_cli-0.1.0/tests/test_produces_cmd.py +52 -0
  48. dna_cli-0.1.0/tests/test_recall_cmd.py +112 -0
  49. dna_cli-0.1.0/tests/test_research_cmd.py +95 -0
  50. dna_cli-0.1.0/tests/test_scope_detect.py +70 -0
  51. dna_cli-0.1.0/tests/test_sdlc_brief.py +149 -0
  52. dna_cli-0.1.0/tests/test_sdlc_correctness.py +189 -0
  53. dna_cli-0.1.0/tests/test_sdlc_ergonomics.py +21 -0
  54. dna_cli-0.1.0/tests/test_sdlc_scope_default.py +178 -0
  55. dna_cli-0.1.0/tests/test_sdlc_workitem_cli.py +376 -0
  56. dna_cli-0.1.0/tests/test_source_url_resolution.py +43 -0
  57. dna_cli-0.1.0/tests/test_spike_cli.py +196 -0
  58. dna_cli-0.1.0/tests/test_story_check.py +176 -0
  59. dna_cli-0.1.0/tests/test_story_commits_backref.py +150 -0
  60. dna_cli-0.1.0/tests/test_story_done_guard.py +61 -0
  61. dna_cli-0.1.0/tests/test_story_pr_cli.py +297 -0
  62. dna_cli-0.1.0/tests/test_story_review_pr_guard.py +98 -0
  63. dna_cli-0.1.0/tests/test_story_show.py +78 -0
  64. dna_cli-0.1.0/tests/test_story_start_gate.py +40 -0
  65. dna_cli-0.1.0/tests/test_tenant_write_note.py +59 -0
  66. dna_cli-0.1.0/tests/test_testkit_cmd.py +196 -0
@@ -0,0 +1,23 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ node_modules/
7
+ *.tsbuildinfo
8
+ dist/
9
+ coverage/
10
+ .DS_Store
11
+ .env
12
+ .env.*
13
+ !.env.example
14
+ # resolver cache — created when a demo/example resolves github: deps
15
+ .dna-cache/
16
+ # MkDocs build output — the docs site is built in CI / on Pages, never committed
17
+ site/
18
+ # per-workstation pointer written by `dna sdlc story start` (read by the
19
+ # prepare-commit-msg hook) — never source-of-truth, never committed
20
+ .dna/active-story.txt
21
+
22
+ # Local hybrid-search index store (dna recall/search) — s-record-search-sqlite
23
+ .dna-search/
dna_cli-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DNA contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
dna_cli-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,57 @@
1
+ Metadata-Version: 2.4
2
+ Name: dna-cli
3
+ Version: 0.1.0
4
+ Summary: DNA command-line interface — the `dna` binary for the declarative SDLC + document CRUD (kernel-local, no service required)
5
+ Project-URL: Homepage, https://ruinosus.github.io/dna/
6
+ Project-URL: Repository, https://github.com/ruinosus/dna
7
+ Project-URL: Documentation, https://ruinosus.github.io/dna/reference/cli/
8
+ Project-URL: Changelog, https://github.com/ruinosus/dna/blob/main/CHANGELOG.md
9
+ Project-URL: Issues, https://github.com/ruinosus/dna/issues
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: agents,cli,declarative,dna,manifests,sdlc
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Requires-Python: <3.14,>=3.12
18
+ Requires-Dist: click>=8.1.0
19
+ Requires-Dist: dna-sdk<0.2,>=0.1
20
+ Requires-Dist: pyyaml>=6.0
21
+ Requires-Dist: rich>=13.0
22
+ Provides-Extra: dev
23
+ Requires-Dist: dna-sdk[search-sqlite]; extra == 'dev'
24
+ Requires-Dist: pytest>=8.0; extra == 'dev'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # dna-cli
28
+
29
+ The `dna` command-line interface for **DNA — Domain Notation of Anything**:
30
+ document CRUD (`dna doc`, `dna kind`, `dna scope`, `dna source`), semantic
31
+ recall and memory (`dna recall`, `dna memory`), research syntheses
32
+ (`dna research`), and a declarative, story-first SDLC (`dna sdlc`) — all
33
+ kernel-local against your filesystem or SQL source, no service required.
34
+
35
+ ## Install
36
+
37
+ ```bash
38
+ pip install dna-cli # or: uv tool install dna-cli
39
+ ```
40
+
41
+ Pre-release / exact-pin alternative — from the repo:
42
+
43
+ ```bash
44
+ cd packages/sdk-py && uv venv && uv pip install -e ".[dev]" -e ../cli
45
+ ```
46
+
47
+ ## Quick taste
48
+
49
+ ```console
50
+ $ dna scope list
51
+ $ dna doc show Agent greeter --scope hello-genome
52
+ $ dna sdlc story create s-my-story --title "..." --ac "..." --dod "..."
53
+ $ dna recall "reciprocal rank fusion" --kind Story -k 1
54
+ ```
55
+
56
+ Full CLI reference: <https://ruinosus.github.io/dna/reference/cli/> ·
57
+ Repository: <https://github.com/ruinosus/dna>
@@ -0,0 +1,31 @@
1
+ # dna-cli
2
+
3
+ The `dna` command-line interface for **DNA — Domain Notation of Anything**:
4
+ document CRUD (`dna doc`, `dna kind`, `dna scope`, `dna source`), semantic
5
+ recall and memory (`dna recall`, `dna memory`), research syntheses
6
+ (`dna research`), and a declarative, story-first SDLC (`dna sdlc`) — all
7
+ kernel-local against your filesystem or SQL source, no service required.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install dna-cli # or: uv tool install dna-cli
13
+ ```
14
+
15
+ Pre-release / exact-pin alternative — from the repo:
16
+
17
+ ```bash
18
+ cd packages/sdk-py && uv venv && uv pip install -e ".[dev]" -e ../cli
19
+ ```
20
+
21
+ ## Quick taste
22
+
23
+ ```console
24
+ $ dna scope list
25
+ $ dna doc show Agent greeter --scope hello-genome
26
+ $ dna sdlc story create s-my-story --title "..." --ac "..." --dod "..."
27
+ $ dna recall "reciprocal rank fusion" --kind Story -k 1
28
+ ```
29
+
30
+ Full CLI reference: <https://ruinosus.github.io/dna/reference/cli/> ·
31
+ Repository: <https://github.com/ruinosus/dna>
@@ -0,0 +1,87 @@
1
+ """``dna`` — user-facing CLI for the DNA kernel.
2
+
3
+ Boots a local Kernel against ``DNA_SOURCE_URL`` / ``DNA_BASE_DIR``
4
+ (filesystem source) and runs one command per invocation. No service
5
+ required — the kernel IS the backend.
6
+
7
+ Output formatting:
8
+ - ``--json`` for machine-readable (default for CRUD writes).
9
+ - rich tables for ``list`` / ``show`` (default).
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import os
14
+
15
+
16
+ import click
17
+
18
+ from dna_cli import (
19
+ doc_cmd,
20
+ docs_cmd,
21
+ eval_cmd,
22
+ install_cmd,
23
+ kind_cmd,
24
+ memory_cmd,
25
+ recall_cmd,
26
+ research_cmd,
27
+ scope_cmd,
28
+ sdlc_cmd,
29
+ source_cmd,
30
+ )
31
+
32
+
33
+ class _BannerGroup(click.Group):
34
+ """Click Group that prints the gradient banner before ``--help`` output."""
35
+
36
+ def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
37
+ from dna_cli._banner import render_banner
38
+ banner = render_banner()
39
+ if banner:
40
+ formatter.write(banner)
41
+ formatter.write_paragraph()
42
+ super().format_help(ctx, formatter)
43
+
44
+
45
+ @click.group(
46
+ cls=_BannerGroup,
47
+ help=(
48
+ "DNA — declarative lifecycle + document CLI.\n\n"
49
+ "Boots a local kernel via DNA_SOURCE_URL / DNA_BASE_DIR "
50
+ "(filesystem source). Run `dna kind list` to start exploring, "
51
+ "`dna sdlc --help` for the lifecycle verbs."
52
+ ),
53
+ context_settings={"help_option_names": ["-h", "--help"]},
54
+ )
55
+ @click.version_option(version="0.1.0", prog_name="dna")
56
+ def main() -> None:
57
+ """Top-level group — subcommands attached below."""
58
+
59
+
60
+ main.add_command(kind_cmd.kind)
61
+ main.add_command(doc_cmd.doc)
62
+ main.add_command(scope_cmd.scope)
63
+ main.add_command(docs_cmd.docs_)
64
+ main.add_command(sdlc_cmd.sdlc)
65
+ main.add_command(research_cmd.research)
66
+ main.add_command(recall_cmd.recall)
67
+ main.add_command(recall_cmd.search)
68
+ main.add_command(memory_cmd.memory)
69
+ # Importing testkit_cmd registers `sdlc test-guide` + `sdlc test-run` on the
70
+ # sdlc group via its decorators (TESTS as first-class SDLC).
71
+ from dna_cli import testkit_cmd as _testkit_cmd # noqa: E402,F401
72
+ # Importing hooks_cmd registers `sdlc hooks install|uninstall|status` — the
73
+ # git↔SDLC symbiosis wiring (Work-Item trailers via prepare-commit-msg).
74
+ from dna_cli import hooks_cmd as _hooks_cmd # noqa: E402,F401
75
+ # Importing pr_cmd registers `sdlc story pr` + `sdlc pr-footer` — the PR
76
+ # half of the symbiosis (attribution footer, PR born from the Story).
77
+ from dna_cli import pr_cmd as _pr_cmd # noqa: E402,F401
78
+ # Importing issue_bridge_cmd registers `sdlc issue publish|import|sync` —
79
+ # the GitHub Issues side of the symbiosis (bridge with provenance).
80
+ from dna_cli import issue_bridge_cmd as _issue_bridge_cmd # noqa: E402,F401
81
+ main.add_command(source_cmd.source)
82
+ main.add_command(install_cmd.install)
83
+ main.add_command(eval_cmd.eval_)
84
+
85
+
86
+ if __name__ == "__main__":
87
+ main()
@@ -0,0 +1,134 @@
1
+ """Active-Story pointer — `.dna/active-story.txt` at the repo root.
2
+
3
+ Written by ``dna sdlc story start <name>`` so that out-of-band tools
4
+ (Claude Code hooks, IDE plugins, dashboards) can attribute their
5
+ events to the Story the human-or-agent is currently working on.
6
+
7
+ File format (single line, plain text)::
8
+
9
+ <scope>:<story-name>
10
+
11
+ Examples::
12
+
13
+ dna-development:s-foo
14
+ hr-screening:s-bar
15
+
16
+ A trailing newline is optional. The file is gitignored — it is per-
17
+ workstation state, not source-of-truth, so a fresh clone has no
18
+ active Story until ``story start`` is invoked.
19
+
20
+ Discovery walks up from ``cwd`` looking for ``.git`` to anchor the
21
+ ``.dna/`` directory; this matches the convention used by source
22
+ replicas (``.dna-replicas.yaml`` discovery in ``source_replicas.py``).
23
+ Override via ``DNA_ACTIVE_STORY_PATH`` for tests.
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import os
28
+ from pathlib import Path
29
+
30
+ _ENV = "DNA_ACTIVE_STORY_PATH"
31
+ _FILENAME = ".dna/active-story.txt"
32
+
33
+
34
+ def _find_repo_root(start: Path | None = None) -> Path | None:
35
+ """Walk up from ``start`` looking for ``.git`` (file or dir).
36
+
37
+ Returns ``None`` when no repo root is found. Symmetric with the
38
+ discovery used in ``source_replicas.py``.
39
+ """
40
+ cur = (start or Path.cwd()).resolve()
41
+ for parent in (cur, *cur.parents):
42
+ if (parent / ".git").exists():
43
+ return parent
44
+ return None
45
+
46
+
47
+ def get_active_story_path(*, start: Path | None = None) -> Path:
48
+ """Resolve the canonical pointer path. Order of precedence:
49
+
50
+ 1. ``DNA_ACTIVE_STORY_PATH`` env var (tests + advanced setups)
51
+ 2. ``<repo-root>/.dna/active-story.txt`` if walking up finds ``.git``
52
+ 3. ``<cwd>/.dna/active-story.txt`` as fallback (loose-tree mode)
53
+ """
54
+ env = os.environ.get(_ENV)
55
+ if env:
56
+ return Path(env)
57
+ root = _find_repo_root(start)
58
+ if root is not None:
59
+ return root / _FILENAME
60
+ return (start or Path.cwd()).resolve() / _FILENAME
61
+
62
+
63
+ def write_active_story(scope: str, name: str, *, start: Path | None = None) -> Path:
64
+ """Stamp ``<scope>:<name>`` to the pointer file. Creates the
65
+ parent directory if missing. Returns the resolved path. Atomic
66
+ via tmpfile + rename.
67
+ """
68
+ if not scope or not name:
69
+ raise ValueError("scope and name must both be non-empty")
70
+ if ":" in scope or ":" in name:
71
+ # Reserve `:` as the field separator. Story slugs use `s-` /
72
+ # `f-` / `e-` prefixes by convention so this never collides
73
+ # in practice; defensive guard just in case.
74
+ raise ValueError("scope/name must not contain ':'")
75
+ path = get_active_story_path(start=start)
76
+ path.parent.mkdir(parents=True, exist_ok=True)
77
+ tmp = path.with_suffix(path.suffix + ".tmp")
78
+ tmp.write_text(f"{scope}:{name}\n", encoding="utf-8")
79
+ tmp.replace(path)
80
+ return path
81
+
82
+
83
+ def read_active_story(
84
+ *, start: Path | None = None
85
+ ) -> tuple[str, str] | None:
86
+ """Read the pointer; returns ``(scope, name)`` or ``None`` when
87
+ absent / empty / malformed. Non-fatal — callers (hooks) treat
88
+ ``None`` as "no active story; broadcast as ephemeral".
89
+ """
90
+ path = get_active_story_path(start=start)
91
+ try:
92
+ raw = path.read_text(encoding="utf-8").strip()
93
+ except FileNotFoundError:
94
+ return None
95
+ except OSError:
96
+ return None
97
+ if not raw:
98
+ return None
99
+ if ":" not in raw:
100
+ return None
101
+ scope, _, name = raw.partition(":")
102
+ scope = scope.strip()
103
+ name = name.strip()
104
+ if not scope or not name:
105
+ return None
106
+ return scope, name
107
+
108
+
109
+ def clear_active_story(*, start: Path | None = None) -> bool:
110
+ """Remove the pointer file. Returns True if a file was removed,
111
+ False if it was already absent. Idempotent.
112
+ """
113
+ path = get_active_story_path(start=start)
114
+ try:
115
+ path.unlink()
116
+ return True
117
+ except FileNotFoundError:
118
+ return False
119
+
120
+
121
+ def clear_if_matches(
122
+ scope: str, name: str, *, start: Path | None = None
123
+ ) -> bool:
124
+ """Clear the pointer ONLY when it currently points at the given
125
+ Story. Used by ``dna sdlc story done|block|cancel`` so that
126
+ closing some other Story (not the active one) doesn't blank the
127
+ pointer. Returns True when cleared.
128
+ """
129
+ cur = read_active_story(start=start)
130
+ if cur is None:
131
+ return False
132
+ if cur != (scope, name):
133
+ return False
134
+ return clear_active_story(start=start)
@@ -0,0 +1,61 @@
1
+ """ASCII banner with cobre/orange gradient, shown on ``dna --help``.
2
+
3
+ Inspired by ``claude-code-templates`` ``cct`` banner. Renders only when
4
+ stdout is a TTY (skip in pipes / CI). Set ``DNA_NO_BANNER=1`` to disable.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ import sys
10
+
11
+ import click
12
+ from rich.console import Console
13
+ from rich.text import Text
14
+
15
+ # Cobre → laranja → pêssego gradient (same palette aitmpl.com uses).
16
+ _GRADIENT = ["#EA580C", "#F97316", "#FB923C", "#FDBA74", "#FED7AA", "#FFE7CC"]
17
+
18
+ # Compact 5-line block. Stencil-ish (block characters) for a "Claude Code SDK"
19
+ # feel without being noisy. Width ~50 cols → fits any terminal.
20
+ _LOGO = r"""
21
+ █████ █████ ██████
22
+ ██ ██ ██ ██ ██ ██
23
+ ███████ ███████ ██████
24
+ ██ ██ ██ ██ ██
25
+ ██ ██ ██ ██ ██ declarative agent platform
26
+ """
27
+
28
+
29
+ def _gradient_text(line: str) -> Text:
30
+ """Apply per-character color gradient across visible glyphs."""
31
+ t = Text()
32
+ n = len(_GRADIENT)
33
+ for i, ch in enumerate(line):
34
+ if ch in (" ", "\n", "\t"):
35
+ t.append(ch)
36
+ else:
37
+ t.append(ch, style=_GRADIENT[i % n])
38
+ return t
39
+
40
+
41
+ def render_banner() -> str:
42
+ """Return colorized banner string, or empty if banner suppressed."""
43
+ if os.environ.get("DNA_NO_BANNER") == "1":
44
+ return ""
45
+ if not sys.stdout.isatty():
46
+ return ""
47
+ lines = _LOGO.strip("\n").splitlines()
48
+ console = Console(file=sys.stdout, force_terminal=True)
49
+ # Build the colored block via Rich's capture so we can return it as a string
50
+ # (Click's HelpFormatter writes strings, not Rich objects).
51
+ with console.capture() as cap:
52
+ for line in lines:
53
+ console.print(_gradient_text(line))
54
+ return cap.get()
55
+
56
+
57
+ def print_banner() -> None:
58
+ """Print banner to stdout when appropriate."""
59
+ out = render_banner()
60
+ if out:
61
+ click.echo(out)