stcode 0.0.1a0__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 (111) hide show
  1. stcode/__init__.py +10 -0
  2. stcode/cli/__init__.py +9 -0
  3. stcode/cli/bootstrap.py +70 -0
  4. stcode/cli/labels.py +621 -0
  5. stcode/cli/logic/__init__.py +0 -0
  6. stcode/cli/logic/commands.py +36 -0
  7. stcode/cli/logic/completion.py +43 -0
  8. stcode/cli/logic/connection.py +46 -0
  9. stcode/cli/logic/requests.py +18 -0
  10. stcode/cli/logic/sessions.py +67 -0
  11. stcode/cli/logic/settings.py +35 -0
  12. stcode/cli/logic/state.py +43 -0
  13. stcode/cli/logic/workspace.py +17 -0
  14. stcode/cli/main.py +290 -0
  15. stcode/cli/models.py +61 -0
  16. stcode/cli/services/__init__.py +0 -0
  17. stcode/cli/services/client.py +393 -0
  18. stcode/cli/services/launcher.py +159 -0
  19. stcode/cli/services/paths.py +18 -0
  20. stcode/cli/services/preferences.py +107 -0
  21. stcode/cli/ui/__init__.py +0 -0
  22. stcode/cli/ui/app.py +136 -0
  23. stcode/cli/ui/app.tcss +3 -0
  24. stcode/cli/ui/components/__init__.py +0 -0
  25. stcode/cli/ui/components/banner.py +78 -0
  26. stcode/cli/ui/components/cards.py +229 -0
  27. stcode/cli/ui/components/prompt.py +225 -0
  28. stcode/cli/ui/components/status.py +95 -0
  29. stcode/cli/ui/components/transcript/__init__.py +51 -0
  30. stcode/cli/ui/components/transcript/entries.py +232 -0
  31. stcode/cli/ui/components/transcript/formatting.py +188 -0
  32. stcode/cli/ui/components/transcript/view.py +101 -0
  33. stcode/cli/ui/screens/__init__.py +0 -0
  34. stcode/cli/ui/screens/chat/__init__.py +0 -0
  35. stcode/cli/ui/screens/chat/chat.tcss +12 -0
  36. stcode/cli/ui/screens/chat/presenter.py +187 -0
  37. stcode/cli/ui/screens/chat/screen.py +682 -0
  38. stcode/cli/ui/screens/connect.py +126 -0
  39. stcode/cli/ui/screens/connect.tcss +49 -0
  40. stcode/cli/ui/screens/dialog.tcss +40 -0
  41. stcode/cli/ui/screens/sessions.py +105 -0
  42. stcode/cli/ui/screens/settings.py +137 -0
  43. stcode/cli/ui/screens/settings.tcss +69 -0
  44. stcode/cli/ui/screens/trust.py +50 -0
  45. stcode/cli/ui/theme.py +221 -0
  46. stcode/core/__main__.py +7 -0
  47. stcode/core/agent/__init__.py +41 -0
  48. stcode/core/agent/agent.py +651 -0
  49. stcode/core/agent/events.py +97 -0
  50. stcode/core/agent/supervisor.py +226 -0
  51. stcode/core/agent/task.py +120 -0
  52. stcode/core/common/__init__.py +21 -0
  53. stcode/core/common/compat.py +27 -0
  54. stcode/core/common/ids.py +43 -0
  55. stcode/core/common/tools.py +50 -0
  56. stcode/core/common/trace.py +406 -0
  57. stcode/core/common/truncate.py +76 -0
  58. stcode/core/configs.py +490 -0
  59. stcode/core/daemon/__init__.py +32 -0
  60. stcode/core/daemon/autonomy.py +71 -0
  61. stcode/core/daemon/main.py +126 -0
  62. stcode/core/daemon/protocol.py +499 -0
  63. stcode/core/daemon/runner.py +359 -0
  64. stcode/core/daemon/server.py +682 -0
  65. stcode/core/daemon/workspace.py +88 -0
  66. stcode/core/harness/__init__.py +81 -0
  67. stcode/core/harness/approvals.py +85 -0
  68. stcode/core/harness/context.py +260 -0
  69. stcode/core/harness/errors.py +40 -0
  70. stcode/core/harness/harness.py +417 -0
  71. stcode/core/harness/mcp.py +530 -0
  72. stcode/core/harness/outputs.py +99 -0
  73. stcode/core/harness/prompts/__init__.py +138 -0
  74. stcode/core/harness/prompts/execute.py +51 -0
  75. stcode/core/harness/prompts/plan.py +57 -0
  76. stcode/core/harness/prompts/sections.py +238 -0
  77. stcode/core/harness/skills/__init__.py +30 -0
  78. stcode/core/harness/skills/loader.py +247 -0
  79. stcode/core/harness/tools/__init__.py +111 -0
  80. stcode/core/harness/tools/base.py +640 -0
  81. stcode/core/harness/tools/files.py +276 -0
  82. stcode/core/harness/tools/interact.py +72 -0
  83. stcode/core/harness/tools/registry.py +113 -0
  84. stcode/core/harness/tools/repl.py +97 -0
  85. stcode/core/harness/tools/schema.py +186 -0
  86. stcode/core/harness/tools/search.py +356 -0
  87. stcode/core/harness/tools/shell.py +372 -0
  88. stcode/core/harness/tools/skill.py +66 -0
  89. stcode/core/harness/tools/todo.py +62 -0
  90. stcode/core/harness/tools/web.py +163 -0
  91. stcode/core/providers/__init__.py +31 -0
  92. stcode/core/providers/anthropic_claude.py +243 -0
  93. stcode/core/providers/base.py +73 -0
  94. stcode/core/providers/gateway.py +267 -0
  95. stcode/core/providers/google_gemini.py +225 -0
  96. stcode/core/providers/openai_gpt.py +250 -0
  97. stcode/core/providers/registry.py +46 -0
  98. stcode/core/providers/types.py +148 -0
  99. stcode/core/repl/__init__.py +19 -0
  100. stcode/core/repl/_worker.py +179 -0
  101. stcode/core/repl/client.py +374 -0
  102. stcode/core/session/__init__.py +39 -0
  103. stcode/core/session/session.py +493 -0
  104. stcode/core/team/__init__.py +38 -0
  105. stcode/core/team/mailbox.py +249 -0
  106. stcode/core/team/tools.py +145 -0
  107. stcode-0.0.1a0.dist-info/METADATA +229 -0
  108. stcode-0.0.1a0.dist-info/RECORD +111 -0
  109. stcode-0.0.1a0.dist-info/WHEEL +4 -0
  110. stcode-0.0.1a0.dist-info/entry_points.txt +3 -0
  111. stcode-0.0.1a0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,417 @@
1
+ """
2
+ Harness — the one object the agent loop talks to.
3
+
4
+ harness = await Harness.create(cwd=project_root, approval_mode="auto-edit")
5
+ system = harness.system_prompt()
6
+ tools = harness.tool_definitions()
7
+ result = await harness.invoke("read", {"path": "src/main.py"}, tool_call_id=call.id)
8
+
9
+ Assembles everything under `core/harness/`: tools, skills, MCP servers, the prompt, and
10
+ the `Runtime` that carries them into a call. The agent loop needs two verbs from here —
11
+ "what do I advertise" and "run this" — and nothing about how either is built.
12
+
13
+ A `Harness` is **per agent**, not per process: its context holds a cwd, a write scope
14
+ and an approval mode, and two sub-agents must be able to have different scopes at once.
15
+ `for_subagent()` makes the narrowed copy; mutating one harness between spawns is the
16
+ bug that rule exists to prevent.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import asyncio
22
+ import contextlib
23
+ from pathlib import Path
24
+ from typing import Any, Iterator, MutableMapping, Sequence
25
+
26
+ from stcode.core.common import trace
27
+ from stcode.core.common.tools import ToolDefinition, ToolResult
28
+ from stcode.core.harness.approvals import DEFAULT_APPROVAL_MODE, ApprovalMode
29
+ from stcode.core.harness.context import HarnessContext, git_context
30
+ from stcode.core.harness.mcp import (
31
+ MCP_CODE_DIRNAME,
32
+ MCPManager,
33
+ generate_server_code,
34
+ load_mcp_config,
35
+ )
36
+ from stcode.core.harness.outputs import OutputStore
37
+ from stcode.core.harness.prompts import (
38
+ PromptMode,
39
+ build_system_prompt,
40
+ mode_for,
41
+ )
42
+ from stcode.core.harness.skills import SkillRegistry
43
+ from stcode.core.harness.tools import BUILTIN_TOOLS, MAIN_TOOLS, WORKER_TOOLS, ToolRegistry
44
+ from stcode.core.harness.tools.base import (
45
+ ApprovalFn,
46
+ AskFn,
47
+ EventFn,
48
+ ProgressFn,
49
+ Runtime,
50
+ Tool,
51
+ current_runtime,
52
+ )
53
+ from stcode.core.repl import PyREPL
54
+
55
+ PROJECT_INSTRUCTION_FILES = ("CLAUDE.md", "AGENTS.md", ".stcode/instructions.md")
56
+ """Read in order; first hit wins. `AGENTS.md` is the cross-agent convention, and a repo
57
+ that has one has already written down how it wants to be worked in."""
58
+
59
+
60
+ class Harness:
61
+ """One agent's capabilities: its tools, its skills, its prompt, its permissions."""
62
+
63
+ def __init__(
64
+ self,
65
+ context: HarnessContext | None = None,
66
+ *,
67
+ tools: Sequence[Tool[Any]] | None = None,
68
+ allowed: Sequence[str] | None = None,
69
+ approval_mode: ApprovalMode = DEFAULT_APPROVAL_MODE,
70
+ session_id: str = "",
71
+ agent_name: str = "main",
72
+ depth: int = 0,
73
+ project_instructions: str = "",
74
+ extra_prompt: str = "",
75
+ role: str = "",
76
+ outputs: MutableMapping[str, Any] | None = None,
77
+ mcp_catalogue: str = "",
78
+ on_progress: ProgressFn | None = None,
79
+ on_approval: ApprovalFn | None = None,
80
+ on_ask: AskFn | None = None,
81
+ on_event: EventFn | None = None,
82
+ mcp: MCPManager | None = None,
83
+ cancel: asyncio.Event | None = None,
84
+ ) -> None:
85
+ self.context = context if context is not None else HarnessContext()
86
+ self.registry = ToolRegistry(tools if tools is not None else BUILTIN_TOOLS)
87
+ # An explicit `tools=` list is already the caller's selection, so allow all of
88
+ # it. The built-in set is not — `MAIN_TOOLS` is the narrower default.
89
+ if allowed is not None:
90
+ self.allowed: list[str] | None = list(allowed)
91
+ elif tools is not None:
92
+ self.allowed = None
93
+ else:
94
+ self.allowed = list(MAIN_TOOLS)
95
+ self.approval_mode = approval_mode
96
+ self.session_id = session_id
97
+ self.agent_name = agent_name
98
+ self.depth = depth
99
+ self.project_instructions = project_instructions
100
+ self.extra_prompt = extra_prompt
101
+ self.role = role
102
+ """The agent's prompt body, already resolved. A string, not a name: `Harness`
103
+ should not know where agent profiles live on disk."""
104
+
105
+ self.mcp = mcp
106
+ self.mcp_catalogue = mcp_catalogue
107
+ """Server and tool *names*, for the prompt. Empty in `tools` mode, where the
108
+ definitions are advertised instead."""
109
+
110
+ self.mcp_servers: dict[str, list[str]] = {}
111
+ """Server -> tool names, for a client that wants to show what is connected.
112
+
113
+ Kept because in `code` mode the manager is closed as soon as it has said what
114
+ each server offers: the connection is gone by then, the facts are not, and
115
+ `/mcp` still has to be able to answer."""
116
+ # Whatever `elide` cuts is parked here; `_share_output` copies it into the
117
+ # REPL's `tool_out` so the model can reach it. Bounded: nothing else would ever
118
+ # remove an entry, and a long session would hold every large result it made.
119
+ self.outputs: MutableMapping[str, Any] = outputs if outputs is not None else OutputStore()
120
+ self.on_progress = on_progress
121
+ self.on_approval = on_approval
122
+ self.on_ask = on_ask
123
+ self.on_event = on_event
124
+ # One event, shared by every `Runtime` this harness hands out, so a single
125
+ # `set()` reaches every tool in flight. A per-call event nobody holds a
126
+ # reference to could never be set at all.
127
+ self.cancel = cancel if cancel is not None else asyncio.Event()
128
+
129
+ # ---- construction ----
130
+
131
+ @classmethod
132
+ async def create(
133
+ cls,
134
+ cwd: str | Path | None = None,
135
+ *,
136
+ approval_mode: ApprovalMode = DEFAULT_APPROVAL_MODE,
137
+ load_skills: bool = True,
138
+ load_mcp: bool = True,
139
+ load_git: bool = True,
140
+ load_repl: bool = True,
141
+ role: str = "",
142
+ agent_prompt: str = "",
143
+ mcp_expose: str = "code",
144
+ mcp_config: str | Path | None = None,
145
+ **kwargs: Any,
146
+ ) -> "Harness":
147
+ """Build a harness with skills discovered and MCP servers connected.
148
+
149
+ Async because connecting to MCP servers is. Both discovery steps fail soft — a
150
+ broken skill or unreachable server is reported, not raised. Neither is a reason
151
+ a coding session cannot start.
152
+ """
153
+ root = Path(cwd).expanduser() if cwd else Path.cwd()
154
+ if not root.is_dir():
155
+ root = Path.cwd()
156
+ context = kwargs.pop("context", None) or HarnessContext(cwd=root)
157
+ context.cwd = root
158
+ if load_skills and context.skills is None:
159
+ context.skills = SkillRegistry.discover(root)
160
+ if load_git and not context.git:
161
+ context.git = await git_context(root)
162
+ if load_repl and context.repl is None:
163
+ # Constructed, not started: the first cell spawns the subprocess. Most
164
+ # sessions never run one.
165
+ context.repl = PyREPL(cwd=root, env=context.env)
166
+
167
+ kwargs.setdefault("role", agent_prompt.strip())
168
+ kwargs.setdefault("project_instructions", read_project_instructions(root))
169
+ harness = cls(context, approval_mode=approval_mode, **kwargs)
170
+
171
+ if load_mcp:
172
+ servers = load_mcp_config(mcp_config, cwd=root)
173
+ if servers:
174
+ manager = MCPManager()
175
+ await manager.open(servers)
176
+ harness.mcp_servers = {
177
+ server: sorted(tools) for server, tools in manager.schemas().items()
178
+ }
179
+ if mcp_expose == "tools":
180
+ harness.mcp = manager
181
+ harness.registry.extend(manager.tools.values(), replace=True)
182
+ # Registering is not advertising: `allowed` is a fixed list, so an
183
+ # MCP tool nobody adds to it stays connected and invisible.
184
+ harness.allow(*manager.tools)
185
+ else:
186
+ # Code mode: we connected only to ask what each server offers. The
187
+ # stubs open their own connection inside the REPL, so holding this
188
+ # one would mean two per server.
189
+ generate_server_code(root, manager.schemas())
190
+ harness.mcp_catalogue = manager.catalogue()
191
+ await manager.aclose()
192
+ return harness
193
+
194
+ def allow(self, *names: str) -> None:
195
+ """Add tools to what this agent may see. No-op when it may already see everything."""
196
+ if self.allowed is None:
197
+ return
198
+ self.allowed += [name for name in names if name not in self.allowed]
199
+
200
+ def for_subagent(
201
+ self,
202
+ name: str,
203
+ *,
204
+ tools: Sequence[str] | None = None,
205
+ scope: str | Sequence[str] | None = None,
206
+ approval_mode: ApprovalMode | None = None,
207
+ ) -> "Harness":
208
+ """A narrowed harness for a child agent.
209
+
210
+ Shares the parent's tool registry, MCP connections and output store — those are
211
+ session-wide and expensive — but gets its own `HarnessContext`, so its write
212
+ scope, read tracking and todos are genuinely separate. A shared context would
213
+ scope-check against the parent's paths and make the rule unenforceable.
214
+ """
215
+ scopes = (scope,) if isinstance(scope, str) else tuple(scope or ())
216
+ child_context = HarnessContext(
217
+ cwd=self.context.cwd,
218
+ scope=tuple(self.context.resolve(entry) for entry in scopes),
219
+ skills=self.context.skills,
220
+ session_ctx=self.context.session_ctx,
221
+ env=dict(self.context.env),
222
+ tool_config=self.context.tool_config.model_copy(deep=True),
223
+ )
224
+ child = Harness(
225
+ child_context,
226
+ allowed=list(tools) if tools is not None else list(WORKER_TOOLS),
227
+ approval_mode=approval_mode or self.approval_mode,
228
+ session_id=self.session_id,
229
+ agent_name=name,
230
+ depth=self.depth + 1,
231
+ project_instructions=self.project_instructions,
232
+ extra_prompt=self.extra_prompt,
233
+ role=self.role,
234
+ outputs=self.outputs,
235
+ # No `mcp_catalogue`: a sub-agent has no `repl`, so this would spend
236
+ # tokens advertising a dead end.
237
+ on_progress=self.on_progress,
238
+ on_approval=self.on_approval,
239
+ on_ask=self.on_ask,
240
+ # A sub-agent gets no `task`, so it never emits; passed anyway so the
241
+ # arrow is one-way and a future nested case does not silently go dark.
242
+ on_event=self.on_event,
243
+ mcp=self.mcp,
244
+ # Interrupting the parent must reach its children: a sub-agent still
245
+ # editing files after Esc is exactly the orphan to avoid.
246
+ cancel=self.cancel,
247
+ )
248
+ child.registry = self.registry
249
+ return child
250
+
251
+ # ---- what the model sees ----
252
+
253
+ def tool_names(self) -> list[str]:
254
+ return [entry.name for entry in self.registry.select(self.allowed, approval_mode=self.approval_mode)]
255
+
256
+ def tool_definitions(self, names: Sequence[str] | None = None) -> list[ToolDefinition]:
257
+ """The tool list for a turn, filtered by this agent's allowance and its mode."""
258
+ return self.registry.definitions(
259
+ names if names is not None else self.allowed, approval_mode=self.approval_mode
260
+ )
261
+
262
+ def system_prompt(self, *, mode: PromptMode | None = None, task: str = "") -> str:
263
+ """This agent's system prompt.
264
+
265
+ `subagent` is derived from `depth`, not passed in: a harness from
266
+ `for_subagent()` is a sub-agent by construction, and a caller allowed to say
267
+ otherwise will eventually get it wrong.
268
+ """
269
+ skills = self.context.skills
270
+ return build_system_prompt(
271
+ mode or mode_for(self.approval_mode),
272
+ subagent=self.depth > 0,
273
+ cwd=str(self.context.cwd),
274
+ approval_mode=self.approval_mode,
275
+ tool_names=self.tool_names(),
276
+ skill_catalogue=skills.catalogue() if skills else "",
277
+ role=self.role,
278
+ mcp_catalogue=self.mcp_catalogue,
279
+ mcp_directory=MCP_CODE_DIRNAME,
280
+ project_instructions=self.project_instructions,
281
+ write_scope=", ".join(str(path) for path in self.context.scope),
282
+ todos=self.context.render_todos() if self.context.todos else "",
283
+ git=self.context.git,
284
+ extra="\n\n".join(part for part in (self.extra_prompt, task) if part),
285
+ )
286
+
287
+ # ---- running a tool ----
288
+
289
+ def runtime(self) -> Runtime[HarnessContext]:
290
+ """A fresh runtime carrying this harness's policy and callbacks.
291
+
292
+ Per call, not cached: a `Runtime` holds an `execution_id` and a start time, and
293
+ sharing one across concurrent calls mislabels every log line they produce.
294
+ """
295
+ return Runtime(
296
+ context=self.context,
297
+ cancel=self.cancel,
298
+ session_id=self.session_id,
299
+ agent_name=self.agent_name,
300
+ depth=self.depth,
301
+ approval_mode=self.approval_mode,
302
+ outputs=self.outputs,
303
+ # Decided in exactly one place: an elision may only name `tool_out[...]`
304
+ # if there is a REPL actually holding it.
305
+ outputs_reachable=getattr(self.context, "repl", None) is not None,
306
+ on_progress=self.on_progress,
307
+ on_approval=self.on_approval,
308
+ on_ask=self.on_ask,
309
+ on_event=self.on_event,
310
+ )
311
+
312
+ async def invoke(
313
+ self, name: str, arguments: dict[str, Any] | None = None, *, tool_call_id: str = ""
314
+ ) -> ToolResult:
315
+ """Run one model-issued tool call. Never raises for a tool-level failure.
316
+
317
+ An unknown or disallowed name comes back as an error result too — the model
318
+ chose that name, so the model is who needs to hear about it.
319
+ """
320
+ allowed = {entry.name for entry in self.registry.select(self.allowed, approval_mode=self.approval_mode)}
321
+ if name not in allowed:
322
+ available = ", ".join(sorted(allowed))
323
+ return ToolResult.error(
324
+ f"`{name}` is not available to this agent. You have: {available}.", tool=name
325
+ )
326
+ # `execute_tool <name>` — the GenAI convention's name for this span, so a
327
+ # platform files it next to the model call that asked for it.
328
+ with trace.span(
329
+ f"execute_tool {name}",
330
+ attributes={
331
+ "gen_ai.operation.name": "execute_tool",
332
+ "gen_ai.tool.name": name,
333
+ "gen_ai.tool.call.id": tool_call_id,
334
+ "stcode.agent": self.agent_name,
335
+ },
336
+ ) as recorder:
337
+ if trace.records_content():
338
+ recorder.set(stcode__input=arguments)
339
+ result = await self.registry[name].invoke(
340
+ arguments, self.runtime(), tool_call_id=tool_call_id
341
+ )
342
+ recorder.set(**{"stcode.tool.ok": not result.is_error})
343
+ if trace.records_content():
344
+ recorder.set(stcode__output=result.content)
345
+ if result.is_error:
346
+ recorder.fail(RuntimeError(result.content))
347
+ await self._share_output(result)
348
+ return result
349
+
350
+ async def _share_output(self, result: ToolResult) -> None:
351
+ """Push an elided result's full payload into the REPL's `tool_out`.
352
+
353
+ This is what makes eliding non-lossy: the model was just told the rest is in
354
+ `tool_out["..."]`, and this puts it there. A dead REPL costs a hint, not a turn.
355
+ """
356
+ repl = getattr(self.context, "repl", None)
357
+ if repl is None or not result.output_id:
358
+ return
359
+ with contextlib.suppress(Exception):
360
+ await repl.inject({result.output_id: self.outputs.get(result.output_id)})
361
+
362
+ # ---- ambient runtime ----
363
+
364
+ @contextlib.contextmanager
365
+ def bind(self) -> Iterator[Runtime[HarnessContext]]:
366
+ """Make this harness's runtime the ambient one for the block.
367
+
368
+ Lets in-process code — a test, a cell in this interpreter — call
369
+ `await read("x.py")` without threading a runtime through.
370
+ """
371
+ runtime = self.runtime()
372
+ token = current_runtime.set(runtime)
373
+ try:
374
+ yield runtime
375
+ finally:
376
+ current_runtime.reset(token)
377
+
378
+ # ---- lifecycle ----
379
+
380
+ async def aclose(self) -> None:
381
+ """Release MCP connections and kill anything still running in the background.
382
+
383
+ Ctrl-C with sub-agents in flight must leave no orphans; background shells are
384
+ this layer's share of that.
385
+ """
386
+ for shell in list(self.context.shells.values()):
387
+ if shell.running:
388
+ with contextlib.suppress(ProcessLookupError, OSError):
389
+ shell.process.kill()
390
+ if shell.pump is not None:
391
+ shell.pump.cancel()
392
+ self.context.shells.clear()
393
+ if self.context.repl is not None:
394
+ await self.context.repl.aclose()
395
+ if self.mcp is not None:
396
+ await self.mcp.aclose()
397
+
398
+ async def __aenter__(self) -> "Harness":
399
+ return self
400
+
401
+ async def __aexit__(self, *_exc_info: object) -> None:
402
+ await self.aclose()
403
+
404
+
405
+ def read_project_instructions(root: Path) -> str:
406
+ """The repository's own guidance for agents working in it, if it has any."""
407
+ for name in PROJECT_INSTRUCTION_FILES:
408
+ candidate = root / name
409
+ if candidate.is_file():
410
+ try:
411
+ return candidate.read_text(encoding="utf-8", errors="replace")
412
+ except OSError:
413
+ continue
414
+ return ""
415
+
416
+
417
+ __all__ = ["PROJECT_INSTRUCTION_FILES", "Harness", "read_project_instructions"]