devmemory-cli 0.1.0.dev0__py3-none-any.whl
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.
- devmemory/__about__.py +3 -0
- devmemory/__init__.py +14 -0
- devmemory/__main__.py +6 -0
- devmemory/adapters/__init__.py +6 -0
- devmemory/adapters/databricks.py +346 -0
- devmemory/adapters/entire.py +444 -0
- devmemory/adapters/git.py +408 -0
- devmemory/adapters/graph.py +251 -0
- devmemory/adapters/metrics.py +150 -0
- devmemory/adapters/tests.py +227 -0
- devmemory/analysis/__init__.py +19 -0
- devmemory/analysis/base.py +128 -0
- devmemory/analysis/chain.py +53 -0
- devmemory/analysis/llm.py +236 -0
- devmemory/analysis/rules.py +110 -0
- devmemory/api/__init__.py +10 -0
- devmemory/api/app.py +390 -0
- devmemory/api/mappers.py +187 -0
- devmemory/api/schemas.py +201 -0
- devmemory/cli/__init__.py +1 -0
- devmemory/cli/_errors.py +36 -0
- devmemory/cli/_render.py +79 -0
- devmemory/cli/analytics.py +136 -0
- devmemory/cli/analyze.py +58 -0
- devmemory/cli/app.py +163 -0
- devmemory/cli/checkpoint.py +199 -0
- devmemory/cli/compare.py +104 -0
- devmemory/cli/doctor.py +151 -0
- devmemory/cli/history.py +56 -0
- devmemory/cli/impact.py +95 -0
- devmemory/cli/init.py +91 -0
- devmemory/cli/mcp.py +66 -0
- devmemory/cli/memory.py +70 -0
- devmemory/cli/restore.py +91 -0
- devmemory/cli/search.py +48 -0
- devmemory/cli/serve.py +64 -0
- devmemory/cli/show.py +139 -0
- devmemory/cli/status.py +72 -0
- devmemory/cli/task.py +333 -0
- devmemory/config.py +302 -0
- devmemory/domain/__init__.py +5 -0
- devmemory/domain/enums.py +151 -0
- devmemory/domain/errors.py +188 -0
- devmemory/domain/models.py +452 -0
- devmemory/domain/taskloop.py +212 -0
- devmemory/environment.py +67 -0
- devmemory/logging.py +148 -0
- devmemory/mcp/__init__.py +12 -0
- devmemory/mcp/server.py +225 -0
- devmemory/paths.py +112 -0
- devmemory/pipeline/__init__.py +7 -0
- devmemory/pipeline/checkpoint.py +443 -0
- devmemory/pipeline/feature_detect.py +53 -0
- devmemory/pipeline/regression.py +141 -0
- devmemory/pipeline/runlog.py +73 -0
- devmemory/pipeline/status_rules.py +44 -0
- devmemory/py.typed +0 -0
- devmemory/services/__init__.py +9 -0
- devmemory/services/agent_context.py +287 -0
- devmemory/services/analysis.py +116 -0
- devmemory/services/analytics.py +328 -0
- devmemory/services/brief.py +53 -0
- devmemory/services/context.py +88 -0
- devmemory/services/databricks_sync.py +121 -0
- devmemory/services/features.py +85 -0
- devmemory/services/impact.py +47 -0
- devmemory/services/memory.py +212 -0
- devmemory/services/projects.py +226 -0
- devmemory/services/restore.py +194 -0
- devmemory/services/taskloop/__init__.py +39 -0
- devmemory/services/taskloop/collectors.py +263 -0
- devmemory/services/taskloop/engine.py +426 -0
- devmemory/services/taskloop/requirements.py +358 -0
- devmemory/services/trace.py +152 -0
- devmemory/services/versions.py +287 -0
- devmemory/storage/__init__.py +9 -0
- devmemory/storage/artifacts.py +113 -0
- devmemory/storage/db.py +205 -0
- devmemory/storage/graph_impacts.py +63 -0
- devmemory/storage/migrations/0001_init.sql +15 -0
- devmemory/storage/migrations/0002_versions.sql +210 -0
- devmemory/storage/migrations/0003_graph.sql +14 -0
- devmemory/storage/migrations/0004_taskloop.sql +82 -0
- devmemory/storage/migrations/0005_project_brief.sql +12 -0
- devmemory/storage/repositories.py +286 -0
- devmemory/storage/tasks.py +342 -0
- devmemory/storage/versions.py +604 -0
- devmemory/web/static/assets/index-CbV5njRH.js +78 -0
- devmemory/web/static/assets/index-DD-7ceZx.css +1 -0
- devmemory/web/static/index.html +18 -0
- devmemory_cli-0.1.0.dev0.dist-info/METADATA +174 -0
- devmemory_cli-0.1.0.dev0.dist-info/RECORD +95 -0
- devmemory_cli-0.1.0.dev0.dist-info/WHEEL +4 -0
- devmemory_cli-0.1.0.dev0.dist-info/entry_points.txt +3 -0
- devmemory_cli-0.1.0.dev0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""The state-aware coding loop.
|
|
2
|
+
|
|
3
|
+
``engine`` is the entry point: create a task, then ``refresh_state`` /
|
|
4
|
+
``get_state`` drive the loop. Collectors aggregate evidence (Git, Entire, tests,
|
|
5
|
+
optional Graph); ``requirements`` normalizes and evaluates requirements. Nothing
|
|
6
|
+
here decides *how* code is written - that stays with Antigravity.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from devmemory.services.taskloop.engine import (
|
|
10
|
+
TaskNotFoundError,
|
|
11
|
+
create_task,
|
|
12
|
+
get_checkpoint,
|
|
13
|
+
get_state,
|
|
14
|
+
get_task,
|
|
15
|
+
latest_task,
|
|
16
|
+
list_snapshots,
|
|
17
|
+
list_tasks,
|
|
18
|
+
mark_complete,
|
|
19
|
+
refresh_state,
|
|
20
|
+
report_issue,
|
|
21
|
+
resolve_issue,
|
|
22
|
+
set_requirement_status,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"TaskNotFoundError",
|
|
27
|
+
"create_task",
|
|
28
|
+
"get_checkpoint",
|
|
29
|
+
"get_state",
|
|
30
|
+
"get_task",
|
|
31
|
+
"latest_task",
|
|
32
|
+
"list_snapshots",
|
|
33
|
+
"list_tasks",
|
|
34
|
+
"mark_complete",
|
|
35
|
+
"refresh_state",
|
|
36
|
+
"report_issue",
|
|
37
|
+
"resolve_issue",
|
|
38
|
+
"set_requirement_status",
|
|
39
|
+
]
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Evidence collectors for one state refresh.
|
|
2
|
+
|
|
3
|
+
Each collector wraps an existing DevMemory adapter and returns a normalized
|
|
4
|
+
state sub-model. Every one fails *gracefully* (§21): on any error it returns a
|
|
5
|
+
model with ``available=False`` / a ``NOT_RUN``/``ERROR`` status and a ``reason`` -
|
|
6
|
+
it never raises for "the tool just isn't here", and it never fabricates a count
|
|
7
|
+
or a checkpoint id.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
import time
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from devmemory.adapters.tests import TestAdapter
|
|
17
|
+
from devmemory.domain.enums import TestRunStatus
|
|
18
|
+
from devmemory.domain.models import TestOutcome
|
|
19
|
+
from devmemory.domain.taskloop import StateCheckpoint, StateGit, StateImpact, StateTests
|
|
20
|
+
from devmemory.logging import get_logger
|
|
21
|
+
from devmemory.services.context import ProjectContext
|
|
22
|
+
|
|
23
|
+
_log = get_logger(__name__)
|
|
24
|
+
|
|
25
|
+
# Build-tool / test-runner droppings. The engine runs the test command on every
|
|
26
|
+
# refresh, so these churn constantly - they are never meaningful task work and
|
|
27
|
+
# must not keep a task out of READY (whether untracked or accidentally tracked).
|
|
28
|
+
_JUNK_PATH = re.compile(
|
|
29
|
+
r"(^|/)(__pycache__|\.pytest_cache|\.ruff_cache|\.mypy_cache|node_modules"
|
|
30
|
+
r"|\.tox|\.nox|htmlcov|\.coverage)(/|$)|\.pyc$|\.pyo$|(^|/)\.DS_Store$"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _meaningful(paths: list[str]) -> list[str]:
|
|
35
|
+
return [p for p in paths if not _JUNK_PATH.search(p)]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# --- git --------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def collect_git(ctx: ProjectContext, *, base_commit: str | None) -> StateGit:
|
|
42
|
+
"""Branch, HEAD, cumulative diff since the task's base commit, tree cleanliness.
|
|
43
|
+
|
|
44
|
+
Git is the source of truth for committed code. If git is unavailable the
|
|
45
|
+
engine treats the task as BLOCKED (§21).
|
|
46
|
+
"""
|
|
47
|
+
git = ctx.git
|
|
48
|
+
try:
|
|
49
|
+
if not git.is_repository():
|
|
50
|
+
return StateGit(available=False, reason="not a git repository")
|
|
51
|
+
|
|
52
|
+
wt = git.working_tree_state()
|
|
53
|
+
head = git.head_sha()
|
|
54
|
+
subject = git.commit("HEAD").subject if head else None
|
|
55
|
+
|
|
56
|
+
# "clean" for the READY gate ignores test-runner droppings (the engine
|
|
57
|
+
# itself creates __pycache__/.pytest_cache every refresh).
|
|
58
|
+
dirty = _meaningful(wt.staged) + _meaningful(wt.unstaged) + _meaningful(wt.untracked)
|
|
59
|
+
tree_clean = not dirty
|
|
60
|
+
|
|
61
|
+
files_changed = lines_added = lines_deleted = 0
|
|
62
|
+
if head:
|
|
63
|
+
# Cumulative task progress: base_commit..working-tree. Falls back to
|
|
64
|
+
# the last commit's own diff when we have no base.
|
|
65
|
+
diff_base = base_commit or git.parent_sha("HEAD")
|
|
66
|
+
try:
|
|
67
|
+
stat = git.diff_stat(diff_base, "HEAD")
|
|
68
|
+
files_changed = stat.files_changed
|
|
69
|
+
lines_added = stat.additions
|
|
70
|
+
lines_deleted = stat.deletions
|
|
71
|
+
except Exception as exc:
|
|
72
|
+
_log.warning("taskloop.git.diffstat_failed", error=str(exc))
|
|
73
|
+
|
|
74
|
+
_log.info("taskloop.collector.git", branch=wt.branch, clean=tree_clean)
|
|
75
|
+
return StateGit(
|
|
76
|
+
branch=wt.branch or "(detached)",
|
|
77
|
+
commit_sha=head,
|
|
78
|
+
commit_subject=subject,
|
|
79
|
+
files_changed=files_changed,
|
|
80
|
+
lines_added=lines_added,
|
|
81
|
+
lines_deleted=lines_deleted,
|
|
82
|
+
working_tree_clean=tree_clean,
|
|
83
|
+
available=True,
|
|
84
|
+
)
|
|
85
|
+
except Exception as exc:
|
|
86
|
+
_log.warning("taskloop.collector.git_failed", error=str(exc))
|
|
87
|
+
return StateGit(available=False, reason=f"git error: {exc}")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# --- entire ----------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def collect_checkpoint(ctx: ProjectContext, *, head_sha: str | None) -> StateCheckpoint:
|
|
94
|
+
"""Latest relevant checkpoint + the one tied to HEAD, plus a session id.
|
|
95
|
+
|
|
96
|
+
Never fabricates a checkpoint id (§21). If Entire is absent/disabled the
|
|
97
|
+
result is an empty :class:`StateCheckpoint` and the rest of the state still
|
|
98
|
+
works (§21).
|
|
99
|
+
"""
|
|
100
|
+
entire = ctx.entire
|
|
101
|
+
out = StateCheckpoint()
|
|
102
|
+
try:
|
|
103
|
+
if not entire.is_installed():
|
|
104
|
+
return out
|
|
105
|
+
|
|
106
|
+
# The checkpoint associated with the current commit (trailer / heuristic).
|
|
107
|
+
if head_sha:
|
|
108
|
+
ref = entire.resolve_for_commit(head_sha)
|
|
109
|
+
if ref is not None:
|
|
110
|
+
out.last_committed_id = ref.checkpoint_id
|
|
111
|
+
out.commit_sha = ref.commit_sha
|
|
112
|
+
out.association = ref.association_method.value
|
|
113
|
+
for sess in ref.sessions:
|
|
114
|
+
if sess.session_id:
|
|
115
|
+
out.session_id = sess.session_id
|
|
116
|
+
break
|
|
117
|
+
|
|
118
|
+
# The newest checkpoint on the branch (may be ahead of HEAD, uncommitted).
|
|
119
|
+
checkpoints = entire.list_checkpoints(limit=1)
|
|
120
|
+
if checkpoints:
|
|
121
|
+
cid = checkpoints[0].get("id") or checkpoints[0].get("checkpoint_id")
|
|
122
|
+
if isinstance(cid, str):
|
|
123
|
+
out.current_id = cid
|
|
124
|
+
out.current_id = out.current_id or out.last_committed_id
|
|
125
|
+
|
|
126
|
+
if out.session_id is None:
|
|
127
|
+
for row in entire.list_sessions(limit=5):
|
|
128
|
+
sid = row.get("session_id")
|
|
129
|
+
if isinstance(sid, str) and row.get("status") == "active":
|
|
130
|
+
out.session_id = sid
|
|
131
|
+
break
|
|
132
|
+
|
|
133
|
+
_log.info(
|
|
134
|
+
"taskloop.collector.entire",
|
|
135
|
+
current=out.current_id,
|
|
136
|
+
committed=out.last_committed_id,
|
|
137
|
+
)
|
|
138
|
+
return out
|
|
139
|
+
except Exception as exc:
|
|
140
|
+
_log.warning("taskloop.collector.entire_failed", error=str(exc))
|
|
141
|
+
return out # empty, not fabricated
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# --- tests ---------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def collect_tests(
|
|
148
|
+
ctx: ProjectContext,
|
|
149
|
+
*,
|
|
150
|
+
command: str | None,
|
|
151
|
+
output_dir: Path,
|
|
152
|
+
) -> StateTests:
|
|
153
|
+
"""Run the configured test command and normalize the result.
|
|
154
|
+
|
|
155
|
+
Supports pytest, ``npm test`` / ``npm run test`` (jest/mocha via the generic
|
|
156
|
+
parser), and go. Parsing failures yield ``FAILED_TO_PARSE`` rather than
|
|
157
|
+
invented counts (§9). A command that cannot start yields ``ERROR`` (§21).
|
|
158
|
+
"""
|
|
159
|
+
if not command:
|
|
160
|
+
return StateTests(status=TestRunStatus.NOT_RUN)
|
|
161
|
+
|
|
162
|
+
cfg = ctx.config.tests
|
|
163
|
+
adapter = TestAdapter(ctx.paths.repo_root)
|
|
164
|
+
try:
|
|
165
|
+
outcome = adapter.run(
|
|
166
|
+
command,
|
|
167
|
+
parser=cfg.parser,
|
|
168
|
+
junit_xml=cfg.junit_xml,
|
|
169
|
+
timeout=cfg.timeout_seconds,
|
|
170
|
+
)
|
|
171
|
+
except Exception as exc:
|
|
172
|
+
_log.warning("taskloop.collector.tests_error", command=command, error=str(exc))
|
|
173
|
+
return StateTests(command=command, status=TestRunStatus.ERROR)
|
|
174
|
+
|
|
175
|
+
output_path: str | None = None
|
|
176
|
+
if outcome.output:
|
|
177
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
178
|
+
fname = f"tests-{int(time.time())}.log"
|
|
179
|
+
(output_dir / fname).write_text(outcome.output, encoding="utf-8")
|
|
180
|
+
output_path = str((output_dir / fname).relative_to(ctx.paths.repo_root))
|
|
181
|
+
|
|
182
|
+
status = _classify_tests(outcome)
|
|
183
|
+
_log.info(
|
|
184
|
+
"taskloop.collector.tests",
|
|
185
|
+
command=command,
|
|
186
|
+
status=status.value,
|
|
187
|
+
passed=outcome.passed,
|
|
188
|
+
failed=outcome.failed,
|
|
189
|
+
)
|
|
190
|
+
return StateTests(
|
|
191
|
+
command=command,
|
|
192
|
+
passed=outcome.passed,
|
|
193
|
+
failed=outcome.failed,
|
|
194
|
+
skipped=outcome.skipped,
|
|
195
|
+
status=status,
|
|
196
|
+
exit_code=outcome.exit_code,
|
|
197
|
+
output_path=output_path,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _classify_tests(outcome: TestOutcome) -> TestRunStatus:
|
|
202
|
+
total = outcome.passed + outcome.failed + outcome.skipped
|
|
203
|
+
# pytest exit 5 = "no tests were collected". The TestAdapter synthesizes
|
|
204
|
+
# errors=1 for any non-zero exit, so check the code before the count.
|
|
205
|
+
if outcome.exit_code == 5 and outcome.failed == 0:
|
|
206
|
+
return TestRunStatus.FAILED_TO_PARSE
|
|
207
|
+
if outcome.failed > 0 or outcome.errors > 0:
|
|
208
|
+
return TestRunStatus.FAILED
|
|
209
|
+
if total == 0:
|
|
210
|
+
# Command ran, nothing parsed. A clean exit means an unknown format;
|
|
211
|
+
# a non-zero exit is a real failure.
|
|
212
|
+
if outcome.exit_code in (0, None):
|
|
213
|
+
return TestRunStatus.FAILED_TO_PARSE
|
|
214
|
+
return TestRunStatus.FAILED
|
|
215
|
+
if outcome.exit_code in (0, None):
|
|
216
|
+
return TestRunStatus.PASSED
|
|
217
|
+
return TestRunStatus.FAILED
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# --- graph (optional) --------------------------------------------------
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def collect_impact(ctx: ProjectContext, *, head_sha: str | None) -> StateImpact:
|
|
224
|
+
"""A small, useful change-impact count from the Entire ``graph`` plugin.
|
|
225
|
+
|
|
226
|
+
Not a visualization, not a graph DB (§8). If the plugin is unavailable the
|
|
227
|
+
result is ``available=False`` with a reason and the state still works.
|
|
228
|
+
"""
|
|
229
|
+
graph = ctx.graph
|
|
230
|
+
try:
|
|
231
|
+
if not graph.is_available:
|
|
232
|
+
probe = graph.probe()
|
|
233
|
+
return StateImpact(available=False, reason=probe.detail or "graph plugin not installed")
|
|
234
|
+
if not head_sha:
|
|
235
|
+
return StateImpact(available=False, reason="no commit to analyze")
|
|
236
|
+
|
|
237
|
+
impact = graph.commit_impact(head_sha)
|
|
238
|
+
if impact is None:
|
|
239
|
+
return StateImpact(available=False, reason="graph returned no impact for this commit")
|
|
240
|
+
|
|
241
|
+
affected_files = len({e.path for e in impact.entities})
|
|
242
|
+
affected_tests = sum(
|
|
243
|
+
1
|
|
244
|
+
for e in impact.entities
|
|
245
|
+
if "test" in e.path.lower() or e.path.lower().endswith("_test.go")
|
|
246
|
+
)
|
|
247
|
+
details = [
|
|
248
|
+
f"{e.change_type} {e.kind} {e.name} ({e.dependents_count} dependents)"
|
|
249
|
+
for e in impact.hotspots[:8]
|
|
250
|
+
]
|
|
251
|
+
_log.info("taskloop.collector.graph", affected_files=affected_files)
|
|
252
|
+
return StateImpact(
|
|
253
|
+
affected_files=affected_files,
|
|
254
|
+
affected_tests=affected_tests,
|
|
255
|
+
available=True,
|
|
256
|
+
details=details,
|
|
257
|
+
)
|
|
258
|
+
except Exception as exc:
|
|
259
|
+
_log.warning("taskloop.collector.graph_failed", error=str(exc))
|
|
260
|
+
return StateImpact(available=False, reason=f"graph error: {exc}")
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
__all__ = ["collect_checkpoint", "collect_git", "collect_impact", "collect_tests"]
|