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,247 @@
1
+ """
2
+ Skill loading — `SKILL.md` files the agent pulls in when it needs them.
3
+
4
+ A skill is a folder with a `SKILL.md`: YAML frontmatter naming it and saying when it
5
+ applies, then markdown instructions, then whatever they reference.
6
+
7
+ ~/.agents/skills/pdf/
8
+ SKILL.md <- frontmatter + instructions
9
+ reference.md <- read on demand, from inside the instructions
10
+
11
+ **Progressive disclosure is the whole design.** Discovery reads only the frontmatter;
12
+ the body loads on `skill(name)`, its references only if the agent follows them. Nine
13
+ skills cost ~400 tokens of catalogue instead of ~90,000 of instructions.
14
+
15
+ The format and default path (`~/.agents/skills`) are the cross-agent convention.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import os
21
+ from dataclasses import dataclass, field
22
+ from pathlib import Path
23
+ from typing import Iterator
24
+
25
+ SKILL_FILENAMES = ("SKILL.md", "skill.md")
26
+ USER_SKILLS_DIR = Path.home() / ".agents" / "skills"
27
+ PROJECT_SKILLS_DIRNAME = ".agents/skills"
28
+ SKILLS_PATH_ENV = "STCODE_SKILLS_PATH"
29
+ """Colon-separated extra directories, searched before the conventional ones."""
30
+
31
+ MAX_SKILL_BYTES = 256 * 1024
32
+ """A SKILL.md larger than this is a mistake — long material belongs in a file the
33
+ instructions point at, so it is read only when it is actually needed."""
34
+
35
+
36
+ @dataclass
37
+ class Skill:
38
+ """One discovered skill. `body` is None until someone asks for it."""
39
+
40
+ name: str
41
+ description: str
42
+ path: Path
43
+ """The `SKILL.md` itself. `path.parent` is the skill's root, which is what relative
44
+ references inside the body resolve against."""
45
+
46
+ source: str = "user"
47
+ metadata: dict[str, str | list[str]] = field(default_factory=dict)
48
+ body: str | None = None
49
+
50
+ @property
51
+ def root(self) -> Path:
52
+ return self.path.parent
53
+
54
+ @property
55
+ def allowed_tools(self) -> list[str]:
56
+ """Tools the skill declares it needs, if it declares any.
57
+
58
+ Advisory, not a sandbox: it narrows what a sub-agent is *given* when a skill is
59
+ the reason it was spawned. A skill cannot grant itself a tool the session does
60
+ not already have.
61
+ """
62
+ raw = self.metadata.get("allowed-tools") or self.metadata.get("allowed_tools") or []
63
+ return raw if isinstance(raw, list) else [item.strip() for item in raw.split(",") if item.strip()]
64
+
65
+ def load(self) -> str:
66
+ """Read and cache the instructions."""
67
+ if self.body is None:
68
+ _, self.body = parse_skill_file(self.path)
69
+ return self.body
70
+
71
+ def catalogue_line(self) -> str:
72
+ return f"- **{self.name}** — {self.description}"
73
+
74
+
75
+ def parse_frontmatter(text: str) -> tuple[dict[str, str | list[str]], str]:
76
+ """Split `---`-delimited frontmatter from the body.
77
+
78
+ A deliberately small YAML subset — scalars, quoted scalars, block lists, inline
79
+ lists, and indented continuations — rather than a YAML dependency. Skill
80
+ frontmatter is two or three keys in practice, and the failure mode of a partial
81
+ parser here is a skill with a missing field, which `discover` reports and skips.
82
+ """
83
+ metadata: dict[str, str | list[str]] = {}
84
+ if not text.startswith("---"):
85
+ return metadata, text
86
+
87
+ lines = text.splitlines()
88
+ try:
89
+ end = next(i for i in range(1, len(lines)) if lines[i].strip() == "---")
90
+ except StopIteration:
91
+ return metadata, text # unterminated block: it is body text, not frontmatter
92
+
93
+ key: str | None = None
94
+ for line in lines[1:end]:
95
+ if not line.strip() or line.lstrip().startswith("#"):
96
+ continue
97
+
98
+ if line.lstrip().startswith("- ") and key:
99
+ existing = metadata.get(key)
100
+ item = _unquote(line.lstrip()[2:].strip())
101
+ metadata[key] = [*existing, item] if isinstance(existing, list) else [item]
102
+ continue
103
+
104
+ if line[0].isspace() and key:
105
+ # A wrapped scalar. Join with a space; the newline was formatting.
106
+ previous = metadata.get(key)
107
+ if isinstance(previous, str):
108
+ metadata[key] = f"{previous} {line.strip()}"
109
+ continue
110
+
111
+ name, separator, value = line.partition(":")
112
+ if not separator:
113
+ continue
114
+ key = name.strip()
115
+ value = value.strip()
116
+ if value.startswith("[") and value.endswith("]"):
117
+ metadata[key] = [_unquote(part.strip()) for part in value[1:-1].split(",") if part.strip()]
118
+ else:
119
+ metadata[key] = _unquote(value)
120
+
121
+ return metadata, "\n".join(lines[end + 1 :]).lstrip("\n")
122
+
123
+
124
+ def _unquote(value: str) -> str:
125
+ if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'":
126
+ return value[1:-1]
127
+ return value
128
+
129
+
130
+ def parse_skill_file(path: Path) -> tuple[dict[str, str | list[str]], str]:
131
+ text = path.read_text(encoding="utf-8", errors="replace")
132
+ if len(text.encode("utf-8")) > MAX_SKILL_BYTES:
133
+ text = text[:MAX_SKILL_BYTES]
134
+ return parse_frontmatter(text)
135
+
136
+
137
+ def skill_search_paths(cwd: Path | None = None) -> list[Path]:
138
+ """Where skills are looked for, most specific first.
139
+
140
+ A project's own `.agents/skills` beats the user's, so a repository can ship a skill
141
+ that overrides a personal one of the same name — the repository is the more
142
+ specific statement about how work in it should be done.
143
+ """
144
+ paths: list[Path] = []
145
+ for entry in os.environ.get(SKILLS_PATH_ENV, "").split(os.pathsep):
146
+ if entry.strip():
147
+ paths.append(Path(entry.strip()).expanduser())
148
+ paths.append((cwd or Path.cwd()) / PROJECT_SKILLS_DIRNAME)
149
+ paths.append(USER_SKILLS_DIR)
150
+ return paths
151
+
152
+
153
+ def _skill_files(directory: Path) -> Iterator[Path]:
154
+ if not directory.is_dir():
155
+ return
156
+ for entry in sorted(directory.iterdir()):
157
+ if entry.is_dir():
158
+ for filename in SKILL_FILENAMES:
159
+ candidate = entry / filename
160
+ if candidate.is_file():
161
+ yield candidate
162
+ break
163
+ elif entry.suffix == ".md" and entry.name not in SKILL_FILENAMES:
164
+ # A bare `<name>.md` is a skill with no bundled files. Common enough for
165
+ # short, self-contained instructions that rejecting it would be pedantry.
166
+ yield entry
167
+
168
+
169
+ class SkillRegistry:
170
+ """The skills available this session, indexed by name.
171
+
172
+ Discovery is frontmatter-only and happens once at startup: it stats a handful of
173
+ directories and reads a few hundred bytes from each `SKILL.md`. Bodies are read
174
+ lazily, so the cost of having fifty skills installed and using none is negligible.
175
+ """
176
+
177
+ def __init__(self, skills: dict[str, Skill] | None = None) -> None:
178
+ self._skills: dict[str, Skill] = skills or {}
179
+ self.problems: list[str] = []
180
+ """Skills that were found but could not be read. Surfaced rather than swallowed —
181
+ a skill that silently fails to load looks identical to one that was never
182
+ written, and the user has no way to tell which."""
183
+
184
+ def __len__(self) -> int:
185
+ return len(self._skills)
186
+
187
+ def __contains__(self, name: str) -> bool:
188
+ return name in self._skills
189
+
190
+ def __iter__(self) -> Iterator[Skill]:
191
+ return iter(self._skills.values())
192
+
193
+ def get(self, name: str) -> Skill | None:
194
+ return self._skills.get(name)
195
+
196
+ def names(self) -> list[str]:
197
+ return sorted(self._skills)
198
+
199
+ def add(self, skill: Skill) -> None:
200
+ # First writer wins: `skill_search_paths` is ordered most-specific-first, so a
201
+ # project skill must not be replaced by the user-global one behind it.
202
+ self._skills.setdefault(skill.name, skill)
203
+
204
+ def catalogue(self) -> str:
205
+ """The listing that goes in the system prompt — names and descriptions only."""
206
+ if not self._skills:
207
+ return ""
208
+ return "\n".join(skill.catalogue_line() for skill in sorted(self._skills.values(), key=lambda s: s.name))
209
+
210
+ @classmethod
211
+ def discover(cls, cwd: Path | None = None, paths: list[Path] | None = None) -> "SkillRegistry":
212
+ registry = cls()
213
+ project_dir = (cwd or Path.cwd()) / PROJECT_SKILLS_DIRNAME
214
+ for directory in paths or skill_search_paths(cwd):
215
+ # Compared as a path, not a substring: `~/.agents/skills` also *contains*
216
+ # ".agents/skills", so a substring test labels every user skill "project".
217
+ source = "project" if directory == project_dir else "user"
218
+ for path in _skill_files(directory):
219
+ try:
220
+ metadata, _ = parse_skill_file(path)
221
+ except OSError as exc:
222
+ registry.problems.append(f"{path}: {exc.strerror}")
223
+ continue
224
+
225
+ name = str(metadata.get("name") or path.parent.name if path.name in SKILL_FILENAMES else path.stem)
226
+ description = str(metadata.get("description") or "")
227
+ if not description:
228
+ registry.problems.append(f"{path}: no `description` in its frontmatter, skipped")
229
+ continue
230
+ registry.add(
231
+ Skill(name=name, description=description, path=path, source=source, metadata=metadata)
232
+ )
233
+ return registry
234
+
235
+
236
+ __all__ = [
237
+ "MAX_SKILL_BYTES",
238
+ "PROJECT_SKILLS_DIRNAME",
239
+ "SKILLS_PATH_ENV",
240
+ "SKILL_FILENAMES",
241
+ "USER_SKILLS_DIR",
242
+ "Skill",
243
+ "SkillRegistry",
244
+ "parse_frontmatter",
245
+ "parse_skill_file",
246
+ "skill_search_paths",
247
+ ]
@@ -0,0 +1,111 @@
1
+ """
2
+ Built-in tools, and the named sets an agent is spawned with.
3
+
4
+ The sets matter as much as the tools:
5
+
6
+ * `MAIN_TOOLS` — a top-level agent's allowance.
7
+ * `WORKER_TOOLS` — what `task` hands a sub-agent. No `task` and no `repl`: one that can
8
+ spawn is one for which `max_depth` bounds nothing, and one holding a persistent
9
+ namespace is doing the parent's job without the parent's oversight.
10
+ * `READ_ONLY_TOOLS` — derived from declared permissions, so it cannot drift.
11
+
12
+ `web_search` is in both. Without `TAVILY_API_KEY` it says so on the first call rather
13
+ than being missing with no explanation.
14
+ """
15
+
16
+ from stcode.core.harness.tools.base import (
17
+ DEFAULT_MAX_OUTPUT,
18
+ ApprovalRequest,
19
+ Question,
20
+ Runtime,
21
+ Tool,
22
+ ToolCancelled,
23
+ ToolDenied,
24
+ ToolError,
25
+ ToolForbidden,
26
+ ToolPermission,
27
+ ToolResult,
28
+ ToolTimeout,
29
+ current_runtime,
30
+ to_tool_definition,
31
+ tool,
32
+ )
33
+ from stcode.core.harness.tools.files import edit, read, write
34
+ from stcode.core.harness.tools.interact import ask_user_question
35
+ from stcode.core.harness.tools.registry import ToolRegistry
36
+ from stcode.core.harness.tools.repl import repl
37
+ from stcode.core.harness.tools.schema import schema_from_signature
38
+ from stcode.core.harness.tools.search import glob, grep, ls
39
+ from stcode.core.harness.tools.shell import bash, bash_output
40
+ from stcode.core.harness.tools.skill import skill
41
+ from stcode.core.harness.tools.todo import todo_write
42
+ from stcode.core.harness.tools.web import web_search
43
+
44
+ BUILTIN_TOOLS: tuple[Tool[object], ...] = (
45
+ read,
46
+ write,
47
+ edit,
48
+ bash,
49
+ bash_output,
50
+ glob,
51
+ grep,
52
+ ls,
53
+ todo_write,
54
+ web_search,
55
+ ask_user_question,
56
+ skill,
57
+ repl,
58
+ )
59
+
60
+ WORKER_TOOLS: tuple[str, ...] = (
61
+ "read", "write", "edit", "bash", "bash_output", "glob", "grep", "ls",
62
+ "todo_write", "ask_user_question", "skill", "web_search",
63
+ )
64
+ """What a sub-agent gets. No `task`, no `repl` — see the module docstring."""
65
+
66
+ MAIN_TOOLS: tuple[str, ...] = (*WORKER_TOOLS, "repl")
67
+ """What a top-level agent gets. `task` is added on top by `core/agent/` at construction:
68
+ it cannot live here, because a tool that spawns an `Agent` would point `core/harness` at
69
+ its own caller."""
70
+
71
+ READ_ONLY_TOOLS: tuple[str, ...] = tuple(
72
+ builtin.name for builtin in BUILTIN_TOOLS if builtin.permission is ToolPermission.READ
73
+ )
74
+ """Derived from the declared permissions rather than hand-listed, so it cannot drift."""
75
+
76
+ __all__ = [
77
+ "BUILTIN_TOOLS",
78
+ "DEFAULT_MAX_OUTPUT",
79
+ "MAIN_TOOLS",
80
+ "READ_ONLY_TOOLS",
81
+ "WORKER_TOOLS",
82
+ "ApprovalRequest",
83
+ "Question",
84
+ "Runtime",
85
+ "Tool",
86
+ "ToolCancelled",
87
+ "ToolDenied",
88
+ "ToolError",
89
+ "ToolForbidden",
90
+ "ToolPermission",
91
+ "ToolRegistry",
92
+ "ToolResult",
93
+ "ToolTimeout",
94
+ "ask_user_question",
95
+ "bash",
96
+ "bash_output",
97
+ "current_runtime",
98
+ "edit",
99
+ "glob",
100
+ "grep",
101
+ "ls",
102
+ "read",
103
+ "repl",
104
+ "schema_from_signature",
105
+ "skill",
106
+ "to_tool_definition",
107
+ "todo_write",
108
+ "tool",
109
+ "web_search",
110
+ "write",
111
+ ]