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.
- ai_code_engineer/__init__.py +2 -0
- ai_code_engineer/catalog.py +143 -0
- ai_code_engineer/chat.py +181 -0
- ai_code_engineer/cli.py +384 -0
- ai_code_engineer/config.py +405 -0
- ai_code_engineer/engine.py +1282 -0
- ai_code_engineer/errors.py +27 -0
- ai_code_engineer/git_integration.py +443 -0
- ai_code_engineer/gui.py +2646 -0
- ai_code_engineer/host.py +81 -0
- ai_code_engineer/ignore.py +269 -0
- ai_code_engineer/intent.py +222 -0
- ai_code_engineer/labels.py +871 -0
- ai_code_engineer/memory.py +91 -0
- ai_code_engineer/modes.py +156 -0
- ai_code_engineer/overrides.py +540 -0
- ai_code_engineer/planbook.py +192 -0
- ai_code_engineer/providers.py +404 -0
- ai_code_engineer/redaction.py +54 -0
- ai_code_engineer/repair.py +564 -0
- ai_code_engineer/report.py +352 -0
- ai_code_engineer/runner.py +854 -0
- ai_code_engineer/setup.py +386 -0
- ai_code_engineer/symbols.py +1286 -0
- ai_code_engineer/verification.py +218 -0
- ai_code_engineer/webapp/__init__.py +1 -0
- ai_code_engineer/webapp/__main__.py +45 -0
- ai_code_engineer/webapp/contract.py +36 -0
- ai_code_engineer/webapp/controller.py +3556 -0
- ai_code_engineer/webapp/fake.py +1141 -0
- ai_code_engineer/webapp/launch.py +108 -0
- ai_code_engineer/webapp/server.py +349 -0
- ai_code_engineer/webapp/static/app.css +780 -0
- ai_code_engineer/webapp/static/app.js +2118 -0
- ai_code_engineer/webapp/static/boot.js +19 -0
- ai_code_engineer/webapp/static/index.html +89 -0
- ai_code_engineer/webapp/static/tokens.css +173 -0
- ai_code_engineer/workspace.py +385 -0
- ai_code_engineer-0.1.0.dist-info/METADATA +7 -0
- ai_code_engineer-0.1.0.dist-info/RECORD +44 -0
- ai_code_engineer-0.1.0.dist-info/WHEEL +5 -0
- ai_code_engineer-0.1.0.dist-info/entry_points.txt +2 -0
- ai_code_engineer-0.1.0.dist-info/licenses/LICENSE +21 -0
- 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)
|