ai-code-engineer 0.1.0__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.
Files changed (44) hide show
  1. ai_code_engineer/__init__.py +2 -0
  2. ai_code_engineer/catalog.py +143 -0
  3. ai_code_engineer/chat.py +181 -0
  4. ai_code_engineer/cli.py +384 -0
  5. ai_code_engineer/config.py +405 -0
  6. ai_code_engineer/engine.py +1282 -0
  7. ai_code_engineer/errors.py +27 -0
  8. ai_code_engineer/git_integration.py +443 -0
  9. ai_code_engineer/gui.py +2646 -0
  10. ai_code_engineer/host.py +81 -0
  11. ai_code_engineer/ignore.py +269 -0
  12. ai_code_engineer/intent.py +222 -0
  13. ai_code_engineer/labels.py +871 -0
  14. ai_code_engineer/memory.py +91 -0
  15. ai_code_engineer/modes.py +156 -0
  16. ai_code_engineer/overrides.py +540 -0
  17. ai_code_engineer/planbook.py +192 -0
  18. ai_code_engineer/providers.py +404 -0
  19. ai_code_engineer/redaction.py +54 -0
  20. ai_code_engineer/repair.py +564 -0
  21. ai_code_engineer/report.py +352 -0
  22. ai_code_engineer/runner.py +854 -0
  23. ai_code_engineer/setup.py +386 -0
  24. ai_code_engineer/symbols.py +1286 -0
  25. ai_code_engineer/verification.py +218 -0
  26. ai_code_engineer/webapp/__init__.py +1 -0
  27. ai_code_engineer/webapp/__main__.py +45 -0
  28. ai_code_engineer/webapp/contract.py +36 -0
  29. ai_code_engineer/webapp/controller.py +3556 -0
  30. ai_code_engineer/webapp/fake.py +1141 -0
  31. ai_code_engineer/webapp/launch.py +108 -0
  32. ai_code_engineer/webapp/server.py +349 -0
  33. ai_code_engineer/webapp/static/app.css +780 -0
  34. ai_code_engineer/webapp/static/app.js +2118 -0
  35. ai_code_engineer/webapp/static/boot.js +19 -0
  36. ai_code_engineer/webapp/static/index.html +89 -0
  37. ai_code_engineer/webapp/static/tokens.css +173 -0
  38. ai_code_engineer/workspace.py +385 -0
  39. ai_code_engineer-0.1.0.dist-info/METADATA +7 -0
  40. ai_code_engineer-0.1.0.dist-info/RECORD +44 -0
  41. ai_code_engineer-0.1.0.dist-info/WHEEL +5 -0
  42. ai_code_engineer-0.1.0.dist-info/entry_points.txt +2 -0
  43. ai_code_engineer-0.1.0.dist-info/licenses/LICENSE +21 -0
  44. ai_code_engineer-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,91 @@
1
+ """Per-project notes the user writes, so a decision survives the task that created it.
2
+
3
+ A small local model re-derives package names, Java versions and naming rules from scratch
4
+ on every task, and the user re-types them in the message box. This module keeps those
5
+ decisions in one file per project, *outside* the approved folder: a proposal can never
6
+ edit the notes it is being held to, which is the same reason the plan ledger lives outside.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ import os
12
+ from pathlib import Path
13
+ import re
14
+ import tempfile
15
+
16
+ from .errors import PolicyError
17
+
18
+ MAX_MEMORY = 4000
19
+ LABEL = ("Project notes the user wrote for this folder. These are the user's own standing "
20
+ "instructions, not model output and not observed code. Follow them unless the "
21
+ "current task overrides one; say so in the summary if a note blocks you:\n")
22
+
23
+
24
+ def key_for(root: str) -> str:
25
+ """A name that is readable in the folder and unique per resolved project root.
26
+
27
+ The same folder typed with different separators or case must land on one file, or a
28
+ note about "never rename this package" would silently apply to only one spelling.
29
+ """
30
+ try:
31
+ resolved = Path(root).expanduser().resolve()
32
+ except OSError:
33
+ resolved = Path(root)
34
+ return re.sub(r"[^A-Za-z0-9._-]", "-", resolved.name) + "-" + \
35
+ hashlib.sha256(str(resolved).casefold().encode("utf-8")).hexdigest()[:16]
36
+
37
+
38
+ def path_for(memory_dir: Path, root: str) -> Path:
39
+ if not str(root).strip():
40
+ raise PolicyError("Choose a project folder before editing its notes.")
41
+ return Path(memory_dir) / (key_for(root) + ".md")
42
+
43
+
44
+ def read(memory_dir: Path, root: str) -> str:
45
+ try:
46
+ return path_for(memory_dir, root).read_text(encoding="utf-8")
47
+ except FileNotFoundError:
48
+ return ""
49
+ except OSError as exc:
50
+ raise PolicyError("Could not read the project notes: " + str(exc)[:160]) from None
51
+
52
+
53
+ def write(memory_dir: Path, root: str, text: str) -> Path:
54
+ """Replace the notes, or clear the file when the box is emptied."""
55
+ if len(text) > MAX_MEMORY:
56
+ raise PolicyError(f"Project notes must stay within {MAX_MEMORY} characters "
57
+ f"(this is {len(text)}).")
58
+ path = path_for(memory_dir, root)
59
+ path.parent.mkdir(parents=True, exist_ok=True)
60
+ if not text.strip():
61
+ try:
62
+ path.unlink()
63
+ except FileNotFoundError:
64
+ pass
65
+ return path
66
+ # Written through a temporary file in the same folder, so a crash mid-save cannot
67
+ # leave half a note that the next task would still be told to obey.
68
+ handle, staged = tempfile.mkstemp(dir=path.parent, suffix=".tmp")
69
+ try:
70
+ with os.fdopen(handle, "w", encoding="utf-8", newline="\n") as stream:
71
+ stream.write(text)
72
+ os.replace(staged, path)
73
+ except OSError as exc:
74
+ try:
75
+ os.unlink(staged)
76
+ except OSError:
77
+ pass
78
+ raise PolicyError("Could not save the project notes: " + str(exc)[:160]) from None
79
+ return path
80
+
81
+
82
+ def block(text: str) -> str:
83
+ """The labelled context a planning turn receives, empty when there are no notes."""
84
+ trimmed = (text or "").strip()
85
+ return (LABEL + trimmed[:MAX_MEMORY]) if trimmed else ""
86
+
87
+
88
+ def record(session: dict, text: str) -> None:
89
+ """Keep the delivered notes on the session for audit, outside the proposal hash."""
90
+ session["memory"] = text.strip()[:MAX_MEMORY]
91
+ session["memory_sha256"] = hashlib.sha256(session["memory"].encode("utf-8")).hexdigest()
@@ -0,0 +1,156 @@
1
+ """The position a folder keeps when no window is looking at it.
2
+
3
+ `intent.py` owns what the three positions mean and every sentence they say. This module owns the one
4
+ place the answer to "what was this folder told to be" is *kept*, so that a terminal process which never
5
+ opens a window can be refused by the same fact the window obeys.
6
+
7
+ It used to live inside a window's own preference block. That block is rebuilt from a list of named keys
8
+ by whichever surface saved last, so the desktop window's next save deleted the web window's map of
9
+ per-folder positions — and a folder with no stored row is granted on **Change**. Choosing Read-only for
10
+ a folder and then opening the other window once was enough to put that folder back on the writable
11
+ path, silently. A promise one surface can delete by opening is not a promise, so the declaration moved
12
+ out to a file with one purpose and one writer.
13
+
14
+ Nothing here decides policy. It answers the question and records who answered it.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import os
20
+ import time
21
+ from pathlib import Path
22
+
23
+ from . import intent
24
+ from .engine import atomic_json, project_key
25
+
26
+ FILE = ".agent-modes.json"
27
+ # Written into the file as a code and spoken by `intent.source`, so the same row reads right in either
28
+ # language.
29
+ WEB, DESKTOP, TERMINAL, SAVED = "web", "gui", "cli", "saved"
30
+
31
+ # (mtime_ns, size) -> the parsed block, the same idiom the session cache uses. A write gate asks this on
32
+ # every snapshot, and a snapshot goes out on every streamed log line, so it must not re-read the disk.
33
+ _cache: dict[str, tuple[tuple, dict]] = {}
34
+
35
+
36
+ def path(app_dir) -> Path:
37
+ return Path(app_dir) / FILE
38
+
39
+
40
+ def _kept(value) -> str:
41
+ """The position this row states, defaulting to the one that writes nothing.
42
+
43
+ A file edited by hand, a value from a version that used other words, a row whose mode is a number:
44
+ each of those used to answer "nothing was declared", which is the direction that lets a write
45
+ through. Over-sealing costs the operator one deliberate click to lift it; under-sealing costs a file.
46
+ """
47
+ text = str(value or "").strip().lower()
48
+ return text if text in intent.MODES else intent.READ
49
+
50
+
51
+ def _rows(app_dir) -> dict:
52
+ """Every declared position, keyed by project key. A file nobody can read answers as nothing was
53
+ ever declared, which is the direction that keeps the tool working on a folder it cannot see."""
54
+ file = path(app_dir)
55
+ stamp = os.path.normcase(str(file))
56
+ try:
57
+ info = file.stat()
58
+ except OSError:
59
+ _cache.pop(stamp, None)
60
+ return {}
61
+ key = (info.st_mtime_ns, info.st_size)
62
+ cached = _cache.get(stamp)
63
+ if cached and cached[0] == key:
64
+ return cached[1]
65
+ try:
66
+ raw = json.loads(file.read_text(encoding="utf-8"))
67
+ except (OSError, ValueError):
68
+ raw = None
69
+ rows: dict = {}
70
+ stored = raw.get("modes") if isinstance(raw, dict) else None
71
+ if isinstance(stored, dict):
72
+ for name, row in stored.items():
73
+ # A row that exists but states no readable position is read as sealed, not as absent.
74
+ value = row.get("mode") if isinstance(row, dict) else row
75
+ blank = {} if not isinstance(row, dict) else row
76
+ rows[str(name)] = {"mode": _kept(value),
77
+ "by": str(blank.get("by", ""))[:40],
78
+ "at": str(blank.get("at", ""))[:32],
79
+ "path": str(blank.get("path", ""))[:400]}
80
+ _cache[stamp] = (key, rows)
81
+ return rows
82
+
83
+
84
+ def _write(app_dir, rows: dict) -> None:
85
+ atomic_json(path(app_dir), {"modes": rows})
86
+ _cache.pop(os.path.normcase(str(path(app_dir))), None)
87
+
88
+
89
+ def folder_key(folder) -> str:
90
+ """The project key for a folder, or "" for no folder. `project_key` resolves an empty path to the
91
+ current directory, and a declaration must never land on whatever folder the process happened to be
92
+ started in."""
93
+ return project_key(folder) if str(folder or "").strip() else ""
94
+
95
+
96
+ def mode_for(app_dir, folder) -> str:
97
+ """The position this folder was told to keep, or "" when nothing was ever said about it."""
98
+ key = folder_key(folder)
99
+ if not key:
100
+ return ""
101
+ row = _rows(app_dir).get(key)
102
+ return row["mode"] if row else ""
103
+
104
+
105
+ def sealed(app_dir, folder) -> bool:
106
+ return mode_for(app_dir, folder) == intent.READ
107
+
108
+
109
+ def row_for(app_dir, folder) -> dict:
110
+ key = folder_key(folder)
111
+ return dict(_rows(app_dir).get(key) or {})
112
+
113
+
114
+ def refusal(app_dir, folder, what: str = "Apply", *, arabic: bool = False, badge: str = "") -> str:
115
+ """The line a write gate prints, chosen by *where the promise came from*.
116
+
117
+ A window holding the position itself gets the badge's own refusal — the operator knows who set it,
118
+ they just did. A folder carrying a declaration the window never made gets the sentence naming the
119
+ surface and the minute, because "refused" without a who reads like a bug and sends the operator
120
+ looking for a switch that is not in this window.
121
+ """
122
+ row = row_for(app_dir, folder)
123
+ if row.get("mode") == intent.READ and not intent.read_only(badge):
124
+ return intent.declared(by=row.get("by", ""), at=row.get("at", ""), arabic=arabic,
125
+ project=Path(folder).name if str(folder or "").strip() else "")
126
+ return intent.no_write(what, arabic=arabic)
127
+
128
+
129
+ def declare(app_dir, folder, mode, by="") -> dict:
130
+ """Record the position for this folder and return the row that was written."""
131
+ key = folder_key(folder)
132
+ if not key:
133
+ return {}
134
+ rows = dict(_rows(app_dir))
135
+ # The key is normalised for matching — on Windows that means lower-cased — so the row also carries
136
+ # the path as the file system spells it, because a listing is read by a person.
137
+ row = {"mode": _kept(mode), "by": str(by or "")[:40], "at": time.strftime("%Y-%m-%d %H:%M"),
138
+ "path": str(Path(folder).resolve())[:400]}
139
+ rows[key] = row
140
+ _write(app_dir, rows)
141
+ return row
142
+
143
+
144
+ def forget(app_dir, folder) -> None:
145
+ """Stop declaring anything: the folder goes back to how a new folder is opened."""
146
+ key = folder_key(folder)
147
+ rows = dict(_rows(app_dir))
148
+ if key in rows:
149
+ rows.pop(key)
150
+ _write(app_dir, rows)
151
+
152
+
153
+ def listed(app_dir) -> list[dict]:
154
+ """Every declaration, newest first, for a person who wants to know what is sealed."""
155
+ return sorted(({**row, "folder": key} for key, row in _rows(app_dir).items()),
156
+ key=lambda row: row["at"], reverse=True)