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,385 @@
1
+ """Conservative file access. Not a replacement for OS sandboxing."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import os
6
+ from pathlib import Path, PurePosixPath
7
+ import re
8
+ import stat
9
+ import tempfile
10
+
11
+ from . import ignore, runner, symbols
12
+ from .errors import MissingFileError, PolicyError
13
+
14
+ MAX_FILE_BYTES = 128 * 1024
15
+ TEXT_SUFFIXES = {".py", ".java", ".kt", ".kts", ".xml", ".gradle", ".md", ".txt",
16
+ ".json", ".yaml", ".yml", ".toml", ".properties", ".sql", ".js",
17
+ ".ts", ".tsx", ".jsx", ".mjs", ".cjs", ".go", ".rs", ".css", ".html",
18
+ ".sh", ".bat", ".cmd"}
19
+ # The suffix gate is a text-or-binary check, and a few real text files have no suffix at all.
20
+ # Refusing them does not protect anything: it stops an agent that scaffolds projects from
21
+ # writing the one file that keeps its own `target/` and `.venv/` out of the first commit.
22
+ # Credentials stay refused by the gates that run before this one (`ignore.refused_dir`: protected paths,
23
+ # credential names, NTFS aliases), and none of these names is in AGENT_RULE_FILES, so none is a hook an
24
+ # agent could leave behind.
25
+ TEXT_NAMES = {".gitignore", ".gitattributes", ".editorconfig", ".dockerignore",
26
+ "dockerfile", "makefile", "jenkinsfile", "license", "readme"}
27
+ # Instruction, rules and auto-launch config for the agents and editors that may open this
28
+ # folder next. A model that writes one gains a hook that outlives the session — read back
29
+ # as instructions, or executed on open — so none of them is ever writable. They are readable
30
+ # only where the suffix itself is one of the text kinds above: `mcp.json` yes, `cursorrules`
31
+ # not, and that asymmetry is the suffix gate doing its job, not this list's promise.
32
+ AGENT_RULE_FILES = {"agents.md", "agent.md", "skill.md", "skills.md", "qoder.md",
33
+ "claude.md", "cursor.md", "cursorrules", ".cursorrules",
34
+ "windsurfrules", ".windsurfrules", "gemini.md", "devin.md",
35
+ "aider.conf.yml", "codex.md",
36
+ "copilot-instructions.md", "mcp.json", ".mcp.json", "opencode.json"}
37
+ # Directories whose contents tools act on: .vscode/tasks.json runs programs,
38
+ # .mvn/jvm.config hands JVM options (including -javaagent) to every later build. Every one of
39
+ # these names begins with a dot, because that is what makes it a tool's own folder rather than
40
+ # a package somebody wrote — see ROOT_POLICY_DIRS for the plain-spelled half of the rule.
41
+ AGENT_CONFIG_DIRS = {".github", ".gitlab", ".circleci", ".vscode", ".zed", ".trae",
42
+ ".qoder", ".claude", ".cursor", ".gemini", ".devin", ".aider",
43
+ ".factory", ".continue", ".junie", ".kiro", ".opencode", ".agent",
44
+ ".githooks", ".mvn"}
45
+ # An agent's own policy and skill folders are spelled plainly and sit at the top of the
46
+ # workspace. Anywhere deeper they are somebody's package: `com.acme.policies` is source code,
47
+ # and refusing to write it would refuse the task.
48
+ ROOT_POLICY_DIRS = {"policies", "skills"}
49
+
50
+
51
+ def digest(raw: bytes) -> str:
52
+ return hashlib.sha256(raw).hexdigest()
53
+
54
+
55
+ # Parsed declarations for files that have not changed since the last map, keyed by the
56
+ # resolved root so two projects never share rows. Advisory only: the map is context for a
57
+ # model, and the bytes on disk stay the authority for every hash check.
58
+ INDEX_CACHE: dict[tuple[str, str], tuple[int, int, dict]] = {}
59
+ INDEX_CACHE_LIMIT = symbols.MAX_FILES
60
+
61
+
62
+ def clear_index_cache(root: Path | str | None = None) -> int:
63
+ """Forget cached rows, for one project or for all of them. Returns how many went."""
64
+ key = str(Path(root).resolve()) if root is not None else None
65
+ doomed = [item for item in INDEX_CACHE if key is None or item[0] == key]
66
+ for item in doomed:
67
+ del INDEX_CACHE[item]
68
+ return len(doomed)
69
+
70
+
71
+ def is_link(path: Path) -> bool:
72
+ info = path.lstat()
73
+ return stat.S_ISLNK(info.st_mode) or bool(getattr(info, "st_file_attributes", 0) & 0x400)
74
+
75
+
76
+ def ensure_project_dir(path: Path | str) -> Path:
77
+ """Create the empty folder the user just chose for a new project.
78
+
79
+ Only a missing or empty folder qualifies, so this can never become a way to
80
+ start writing into an existing project without looking at it first.
81
+ """
82
+ candidate = Path(path)
83
+ if not candidate.is_absolute():
84
+ raise PolicyError("Choose an absolute project folder.")
85
+ if candidate.parent == candidate or len(str(candidate).rstrip("\\/")) <= 3:
86
+ raise PolicyError("Choose a folder inside a drive, not the drive itself.")
87
+ if candidate.exists():
88
+ if not candidate.is_dir():
89
+ raise PolicyError("That path is a file. Choose a folder instead.")
90
+ if any(candidate.iterdir()):
91
+ raise PolicyError("That folder already contains files. Use Browse… to open it as an existing project.")
92
+ try:
93
+ candidate.mkdir(parents=True, exist_ok=True)
94
+ except OSError as exc:
95
+ raise PolicyError("Could not create the project folder: " + str(exc)[:160]) from None
96
+ return candidate.resolve()
97
+
98
+
99
+ class Workspace:
100
+ def __init__(self, root: Path):
101
+ self.root = root.resolve(strict=True)
102
+ if not self.root.is_dir():
103
+ raise PolicyError("Workspace must be a directory.")
104
+ self._cache_key = str(self.root)
105
+ # What the last walk stopped at, counted by `files()` and spoken by `map_note()`. Zero until a
106
+ # walk has run, which is the honest answer to "what did you not show me".
107
+ self.skipped_generated = 0
108
+ self.skipped_ignored = 0
109
+
110
+ def path(self, relative: str, *, writable: bool = False) -> Path:
111
+ if not isinstance(relative, str) or not relative or len(relative) > 400:
112
+ raise PolicyError("Invalid workspace path.")
113
+ if "\\" in relative or ":" in relative or "\x00" in relative:
114
+ raise PolicyError("Use a relative path with forward slashes.")
115
+ parts = relative.split("/")
116
+ if PurePosixPath(relative).is_absolute() or any(
117
+ p in {"", ".", ".."} or p.rstrip(" .") != p for p in parts
118
+ ):
119
+ raise PolicyError("Absolute paths, traversal and ambiguous paths are blocked.")
120
+ if any(ignore.refused_dir(p) for p in parts):
121
+ raise PolicyError("Protected path.")
122
+ if any(re.fullmatch(r"(?i)(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?", p) for p in parts):
123
+ raise PolicyError("Device paths are blocked.")
124
+ current = self.root
125
+ for part in parts:
126
+ current = current / part
127
+ if current.exists() or current.is_symlink():
128
+ if is_link(current):
129
+ raise PolicyError("Symlinks and reparse points are blocked.")
130
+ resolved = current.resolve()
131
+ if not resolved.is_relative_to(self.root):
132
+ raise PolicyError("Path escapes workspace.")
133
+ # The name the model typed and the name NTFS finally opens are not always the
134
+ # same (case, aliases, mount points), so the resolved form is checked too.
135
+ if any(ignore.refused_dir(p) for p in resolved.relative_to(self.root).parts):
136
+ raise PolicyError("Protected path.")
137
+ if (current.suffix.lower() not in TEXT_SUFFIXES
138
+ and current.name.casefold() not in TEXT_NAMES):
139
+ # The name goes into the sentence because the model wrote it seconds earlier and is
140
+ # about to be told only that something was refused. Without it, a small model repeats
141
+ # the same action — measured twice in the ecommerce run, where two 5-minute turns
142
+ # ended BLOCKED on a path this gate had rejected by spelling.
143
+ raise PolicyError("Only supported text files are accessible: " + relative)
144
+ if writable and (current.name.casefold() in AGENT_RULE_FILES or
145
+ any(p.casefold() in AGENT_CONFIG_DIRS for p in parts) or
146
+ parts[0].casefold() in ROOT_POLICY_DIRS):
147
+ raise PolicyError("Instructions and policy files are read-only.")
148
+ return current
149
+
150
+ def read(self, relative: str) -> dict:
151
+ path = self.path(relative)
152
+ try:
153
+ info = path.stat()
154
+ except FileNotFoundError:
155
+ raise MissingFileError("File does not exist: " + relative) from None
156
+ if not stat.S_ISREG(info.st_mode):
157
+ raise PolicyError("Path is not a regular file: " + relative)
158
+ if info.st_size > MAX_FILE_BYTES:
159
+ raise PolicyError("File exceeds the 128 KiB read limit: " + relative)
160
+ raw = path.read_bytes()
161
+ if len(raw) > MAX_FILE_BYTES or b"\x00" in raw:
162
+ raise PolicyError("File is too large or binary.")
163
+ try:
164
+ content = raw.decode("utf-8")
165
+ except UnicodeDecodeError as exc:
166
+ # The offset is the point: "not UTF-8" alone is not something anyone can act on, and a
167
+ # cp1252 `messages_ar.properties` is one byte per line away from a fix. There is no
168
+ # fallback decode here deliberately — this content can come back as a write, and
169
+ # re-encoding a file nobody chose to convert rewrites every non-ASCII byte in it.
170
+ raise PolicyError(
171
+ f"File '{relative}' is not valid UTF-8 (invalid byte 0x{raw[exc.start]:02x} at "
172
+ f"offset {exc.start}). Convert it to UTF-8, or keep it out of the task.") from exc
173
+ return {"path": relative, "sha256": digest(raw), "content": content}
174
+
175
+ def files(self, limit: int = 2000) -> list[str]:
176
+ """Every path this tool is willing to look at, in one walk.
177
+
178
+ Three things decide together here, and they must keep deciding together: the directory names
179
+ `ignore` owns, the repository's own `.gitignore` (including nested ones, for the directories the
180
+ walk actually reaches), and the policy gate in `path()`. The map, the search and the composer's
181
+ context count all read this list, so a file that is invisible to one is invisible to all three —
182
+ which is the only version of that story a person can live with.
183
+
184
+ What was hidden as noise is counted and reported by `repo_map()`, because "where did my file
185
+ go" is a question a silent filter makes unanswerable. Credentials and policy paths stay
186
+ uncounted and unmentioned: they are refusals, not tidiness.
187
+ """
188
+ found = []
189
+ self.skipped_generated = 0
190
+ self.skipped_ignored = 0
191
+ root_rules = ignore.Rules.load(self.root)
192
+ in_force = {"": root_rules} # rules for a directory, keyed by its own relative path
193
+ for base, dirs, names in os.walk(self.root, followlinks=False):
194
+ walked = Path(base).relative_to(self.root).as_posix()
195
+ own = "" if walked == "." else walked
196
+ rules = in_force.get(own.rsplit("/", 1)[0] if "/" in own else "", root_rules)
197
+ prefix = own + "/" if own else ""
198
+ if ".gitignore" in names and own:
199
+ try:
200
+ rules = rules.child((Path(base) / ".gitignore").read_text(
201
+ encoding="utf-8", errors="replace"))
202
+ except OSError:
203
+ pass
204
+ in_force[own] = rules
205
+ # A link is dropped here rather than in `ignore`: recognising one needs this root, and the
206
+ # decision "do not follow it out of the workspace" belongs to the gate, not to tidiness.
207
+ kept, hidden, skipped = ignore.walk_prune(
208
+ [name for name in dirs if not is_link(Path(base) / name)], rules)
209
+ self.skipped_generated += hidden
210
+ self.skipped_ignored += skipped
211
+ dirs[:] = kept
212
+ for name in sorted(names):
213
+ relative = prefix + name
214
+ if rules and rules.ignores(relative, False):
215
+ self.skipped_ignored += 1
216
+ continue
217
+ if ignore.generated_file(name):
218
+ self.skipped_generated += 1
219
+ continue
220
+ try:
221
+ self.path(relative)
222
+ except PolicyError:
223
+ continue
224
+ found.append(relative)
225
+ if len(found) >= limit:
226
+ return found
227
+ return found
228
+
229
+ def search(self, query: str) -> list[dict]:
230
+ if not isinstance(query, str) or not 1 <= len(query) <= 200:
231
+ raise PolicyError("Search query must contain 1–200 characters.")
232
+ results = []
233
+ for name in self.files():
234
+ try:
235
+ content = self.read(name)["content"]
236
+ except PolicyError:
237
+ continue
238
+ for line, value in enumerate(content.splitlines(), 1):
239
+ if query.casefold() in value.casefold():
240
+ results.append({"path": name, "line": line, "text": value[:300]})
241
+ if len(results) >= 40:
242
+ return results
243
+ return results
244
+
245
+ def repo_map(self) -> str:
246
+ files, rows = self.index()
247
+ # A folder with several builds in it gets its map spread across them: the budget is the same
248
+ # 12 000 characters either way, and in a reactor alphabetical order spends all of it on the
249
+ # first three modules.
250
+ return symbols.render(rows, files, spread_files=len(runner.projects(self.root)) > 1,
251
+ note=self.map_note())
252
+
253
+ def index(self) -> tuple[list[str], list[dict]]:
254
+ """The visible files and the parsed declarations for the code among them.
255
+
256
+ Split out of `repo_map()` because a symbol query needs the rows without the prose: rendering
257
+ costs 12 000 characters of text, and `find_symbol` wants the structures behind them. The cache
258
+ in `_index_row` makes the second call in a turn nearly free, which is the whole reason both
259
+ surfaces can afford to ask.
260
+ """
261
+ files = self.files(limit=symbols.MAX_FILES)
262
+ rows = []
263
+ indexed = set()
264
+ for name in files:
265
+ # Code gets its declarations; the handful of configuration files that describe the project
266
+ # get their facts. Everything else stays a line in the list, as it always was.
267
+ if not (symbols.indexable(name) or symbols.noteworthy(name)):
268
+ continue
269
+ row = self._index_row(name)
270
+ if row is not None:
271
+ indexed.add(name)
272
+ rows.append(row)
273
+ for stale in [key for key in list(INDEX_CACHE)
274
+ if key[0] == self._cache_key and key[1] not in indexed]:
275
+ del INDEX_CACHE[stale]
276
+ return files, rows
277
+
278
+ def sources(self, rows: list[dict]) -> list[tuple[str, str]]:
279
+ """The text of every indexed file — code, and only code.
280
+
281
+ A reference search over the whole workspace would answer `register` with a Spring key in
282
+ `application.yml` and spend the hit budget on configuration; `search_code` is the verb for text,
283
+ this one is for names. A file the gates refuse (too large, not UTF-8) is left out rather than
284
+ failing the query, because a partial answer about references is still the answer being asked.
285
+ """
286
+ out = []
287
+ for row in rows:
288
+ if row.get("kind") == "config":
289
+ continue
290
+ try:
291
+ out.append((row["path"], self.read(row["path"])["content"]))
292
+ except PolicyError:
293
+ continue
294
+ return out
295
+
296
+ def map_note(self) -> str:
297
+ """One line on what the walk left out, so an empty result is not mistaken for an empty project.
298
+
299
+ The counts come from the same walk that produced the list, so they cannot drift from it; a model
300
+ that knows 400 generated files were skipped will read a path by name instead of concluding the
301
+ code it was asked about does not exist.
302
+ """
303
+ hidden = []
304
+ if self.skipped_generated:
305
+ hidden.append(str(self.skipped_generated) + " built or generated")
306
+ if self.skipped_ignored:
307
+ hidden.append(str(self.skipped_ignored) + " listed in .gitignore")
308
+ if not hidden:
309
+ return ""
310
+ return ("(not shown: " + " and ".join(hidden) + " path(s) the walk stopped at. "
311
+ "Name a path and read it if you need one.)")
312
+
313
+ def _index_row(self, relative: str) -> dict | None:
314
+ """This file's declarations, reusing the last parse when the file still fits.
315
+
316
+ mtime and size are a heuristic: an equal-size edit inside one clock tick can be
317
+ missed. A stale row only makes the map slightly out of date — every write is still
318
+ checked against the hash of the bytes on disk.
319
+ """
320
+ key = (self._cache_key, relative)
321
+ try:
322
+ info = (self.root / relative).stat()
323
+ except OSError:
324
+ return None
325
+ if len(INDEX_CACHE) >= INDEX_CACHE_LIMIT:
326
+ INDEX_CACHE.clear() # bounded convenience, not an LRU
327
+ cached = INDEX_CACHE.get(key)
328
+ if cached and cached[:2] == (info.st_mtime_ns, info.st_size):
329
+ return cached[2]
330
+ try:
331
+ content = self.read(relative)["content"]
332
+ except PolicyError:
333
+ return None
334
+ row = (symbols.parse(relative, content) if symbols.indexable(relative)
335
+ else symbols.config_row(relative, content))
336
+ if row is not None:
337
+ INDEX_CACHE[key] = (info.st_mtime_ns, info.st_size, row)
338
+ return row
339
+
340
+ def write(self, relative: str, content: str, expected: str | None) -> str:
341
+ path = self.path(relative, writable=True)
342
+ if not isinstance(content, str) or len(content.encode("utf-8")) > MAX_FILE_BYTES:
343
+ raise PolicyError("Invalid or oversized replacement.")
344
+ actual = self.read(relative)["sha256"] if path.exists() else None
345
+ if actual != expected:
346
+ raise PolicyError("File changed since review; regenerate the proposal.")
347
+ path.parent.mkdir(parents=True, exist_ok=True)
348
+ path = self.path(relative, writable=True)
349
+ raw = content.encode("utf-8")
350
+ fd, tmp = tempfile.mkstemp(prefix=".agent-write-", dir=path.parent)
351
+ try:
352
+ with os.fdopen(fd, "wb") as handle:
353
+ handle.write(raw)
354
+ handle.flush()
355
+ os.fsync(handle.fileno())
356
+ if path.exists():
357
+ os.chmod(tmp, path.stat().st_mode)
358
+ # Recheck before replacement; concurrent hostile filesystem access still
359
+ # requires OS isolation, and is outside this developer-local MVP.
360
+ self.path(relative, writable=True)
361
+ latest = self.read(relative)["sha256"] if path.exists() else None
362
+ if latest != expected:
363
+ raise PolicyError("Concurrent edit detected; replacement cancelled.")
364
+ os.replace(tmp, path)
365
+ finally:
366
+ if os.path.exists(tmp):
367
+ os.unlink(tmp)
368
+ return digest(raw)
369
+
370
+ def remove(self, relative: str, expected: str | None) -> None:
371
+ """Delete one file a proposal named, under the same guards `write` runs.
372
+
373
+ The hash check is the reason this is a workspace verb and not an `unlink()` call: removing a
374
+ file that changed since review is the same race a write refuses, and the bytes nobody looked
375
+ at would be gone either way.
376
+ """
377
+ path = self.path(relative, writable=True)
378
+ if not path.exists():
379
+ raise MissingFileError("File does not exist: " + relative)
380
+ if self.read(relative)["sha256"] != expected:
381
+ raise PolicyError("File changed since review; regenerate the proposal.")
382
+ self.path(relative, writable=True) # re-run the guards after the race window
383
+ if self.read(relative)["sha256"] != expected:
384
+ raise PolicyError("Concurrent edit detected; removal cancelled.")
385
+ path.unlink()
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: ai-code-engineer
3
+ Version: 0.1.0
4
+ Summary: A local-first, review-first developer agent for Ollama and OpenRouter
5
+ Requires-Python: >=3.11
6
+ License-File: LICENSE
7
+ Dynamic: license-file
@@ -0,0 +1,44 @@
1
+ ai_code_engineer/__init__.py,sha256=FmPQKfgbe4hLH7k7yY_tFWi7f4zvmcVfQTqVdHgmoKQ,99
2
+ ai_code_engineer/catalog.py,sha256=uXyNmTHoBOUUzpnEcBdBkq9P_nDCHAvIeiHlStwM0rI,7007
3
+ ai_code_engineer/chat.py,sha256=f-_izcLRD6jk-GcIDaJMSQtrHLQI6zVB0gb7mIbVQD4,8652
4
+ ai_code_engineer/cli.py,sha256=ueHIboIqf3Ts02oYaQIXx8jL4xAozoywt2rjCBVT_RM,20179
5
+ ai_code_engineer/config.py,sha256=m_5CAq7267zP-O9PnHJvY2dG1vDG6RnPDiQSpIXmpnI,20040
6
+ ai_code_engineer/engine.py,sha256=p2RkcCpitNGpp8aCRE-EkhCf5h_6KDvt2GztoBd1KEE,77632
7
+ ai_code_engineer/errors.py,sha256=qntzsZyp_h-Dg9AkInhFNvQXjUo1D4scHpOaQ2VvW1g,848
8
+ ai_code_engineer/git_integration.py,sha256=eqngEmDXaKe4C4NHYeZf0uYYc5m28QqSLfOCDxPfehc,20627
9
+ ai_code_engineer/gui.py,sha256=ujBF7_mU29IymYiFdUcQx8qJLl9AtyG0n6On6X3QllY,140349
10
+ ai_code_engineer/host.py,sha256=U57qftFUYKTMaM1hiGLduVMfGZ8kHE7jBc54XvnWh5I,4782
11
+ ai_code_engineer/ignore.py,sha256=J5xwsYUyKExmTbV0XANX7ITQQLi5tGG9c74rWryligo,13022
12
+ ai_code_engineer/intent.py,sha256=JopMi6FvbPA4JFOhk7TYbyxY_MfJr1wnRTflopZxbPU,12993
13
+ ai_code_engineer/labels.py,sha256=KQA0CEceUQ0LyegFoGIgPgIQ8j_Q-SI-8_sBVkzVSYE,55981
14
+ ai_code_engineer/memory.py,sha256=3meGtsyYI8ovKuWF034xy48lTjN_0sA6FlxA4arUXco,3566
15
+ ai_code_engineer/modes.py,sha256=L0Z3lCb3IqfveT5iNP4j0CKW8HIyxlSu62aY-X8T6qU,6542
16
+ ai_code_engineer/overrides.py,sha256=qxhhWrWwlzl_fogIKiMgGM7LFVvRtjwui2-OZUYA6sY,27181
17
+ ai_code_engineer/planbook.py,sha256=oaoVsKsZHqw8QFJr8G-7tv0KMLmhduHZh4czZnIHqIA,8033
18
+ ai_code_engineer/providers.py,sha256=ORJT1kK-f-GKkHwxHotzYqDLHjrYclOA0u1IyK2W7o8,21394
19
+ ai_code_engineer/redaction.py,sha256=0TqLPaLzArsqUqsoRFbz--zXukrVrLL6LD_7fWHqCSk,2921
20
+ ai_code_engineer/repair.py,sha256=oOUzWoc3JMpMNtZyEdsnyBdo1mFcpVLyBnhK06htqTc,29155
21
+ ai_code_engineer/report.py,sha256=B2tlgsx-5kmDmCaFyQZ6OND_QysK00uH2c-uE7dCaLg,19270
22
+ ai_code_engineer/runner.py,sha256=nz8HJABDSVDkNs_p7xBN32BElHb69Sf5PLqoDQN1Z-c,39886
23
+ ai_code_engineer/setup.py,sha256=zRWWncBE5VXHrPWCPG0v8RyAnz3xpu0yMxx8fEk5r1A,23339
24
+ ai_code_engineer/symbols.py,sha256=a9by4ZwQmgtmOiudcoh8eCTvy5W2xy18gwIbL_Y3N2g,62398
25
+ ai_code_engineer/verification.py,sha256=lIhhMytnDEuA7NMFGUFDEttGdDFtCbVH1jhBcLug5QU,9737
26
+ ai_code_engineer/workspace.py,sha256=m1IrW3YIK4LnlfBlRFoFXiMw3e4fRigN-LkVBO_NzHw,19967
27
+ ai_code_engineer/webapp/__init__.py,sha256=luFGr3RhX3birwA2LS0PEkwVVqW3g3Owrpj5P9ZKHWs,87
28
+ ai_code_engineer/webapp/__main__.py,sha256=cuMPrBiFuWqVW9eU_VOyrAraznFUG1-9LBwcCE6vhsU,1274
29
+ ai_code_engineer/webapp/contract.py,sha256=WR-y7bCGfSQIk0IttyOE4qKZdc6XMUWAdLtHW9zn6Is,1491
30
+ ai_code_engineer/webapp/controller.py,sha256=uIQAP45DEt8nDdnOVYNXRwJTF47JcKHDJIHQJyJEMlg,194631
31
+ ai_code_engineer/webapp/fake.py,sha256=uO3Gh6OoSWxFcCR58TaQo13QKE5eP8m13Dn-F6rDBGs,65968
32
+ ai_code_engineer/webapp/launch.py,sha256=WvQPzyOeFgMGY9147MKZ-bztTqskgny1TyHzPohc-_A,4390
33
+ ai_code_engineer/webapp/server.py,sha256=lU7u5hn9GBLNpOdwcOw7u3tnELXdb9TzsQdxRtNZXoA,16084
34
+ ai_code_engineer/webapp/static/app.css,sha256=OMYKaETWLLTU9FQu3DB5dehkr8fqoB07WLcKmK69PKQ,48160
35
+ ai_code_engineer/webapp/static/app.js,sha256=bbo4xywf1ArnyDRrUSKE7UdAwjzGOOkwZVeSJbgldZA,109470
36
+ ai_code_engineer/webapp/static/boot.js,sha256=OXGEPpzdi8TOMbw8359SHyaYz4eOd4Z9E7-Iyf3-EVk,1094
37
+ ai_code_engineer/webapp/static/index.html,sha256=ujPrWY_6FqIDaWA4qHGEdBBGAeRGP-RLfriqGw4uceM,3933
38
+ ai_code_engineer/webapp/static/tokens.css,sha256=mdue-1-pylxWjS5tPIF_cNDilxlE2QLzN5tFo84nS_w,10444
39
+ ai_code_engineer-0.1.0.dist-info/licenses/LICENSE,sha256=lmENOq1hWBTypWzFHalhT64yQ5eMBozcMuyUF_xByR0,1086
40
+ ai_code_engineer-0.1.0.dist-info/METADATA,sha256=7pCvYo9De17VTEP-cDxOhCjjqGtD1T4Wi5FtVFQlPx4,214
41
+ ai_code_engineer-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
42
+ ai_code_engineer-0.1.0.dist-info/entry_points.txt,sha256=UH2Bu59Ay1Su2zXfqc13aDetckIyUK5vh27pEiEMLW4,52
43
+ ai_code_engineer-0.1.0.dist-info/top_level.txt,sha256=tBDPUjzxAE0YjqHx8WsTVssvXM4lo7Nwzy4DeEXHdDU,17
44
+ ai_code_engineer-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ agent = ai_code_engineer.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mohamed Saad (mohamedsaad208)
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.
@@ -0,0 +1 @@
1
+ ai_code_engineer