windcode 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 (143) hide show
  1. windcode/__init__.py +6 -0
  2. windcode/__main__.py +4 -0
  3. windcode/auth/__init__.py +11 -0
  4. windcode/auth/store.py +90 -0
  5. windcode/cli.py +181 -0
  6. windcode/config/__init__.py +41 -0
  7. windcode/config/loader.py +117 -0
  8. windcode/config/models.py +203 -0
  9. windcode/config/writer.py +74 -0
  10. windcode/context/__init__.py +18 -0
  11. windcode/context/compactor.py +72 -0
  12. windcode/context/estimator.py +89 -0
  13. windcode/context/truncation.py +87 -0
  14. windcode/domain/__init__.py +1 -0
  15. windcode/domain/errors.py +33 -0
  16. windcode/domain/events.py +597 -0
  17. windcode/domain/messages.py +226 -0
  18. windcode/domain/models.py +73 -0
  19. windcode/domain/subagents.py +263 -0
  20. windcode/domain/tools.py +55 -0
  21. windcode/extensions/__init__.py +27 -0
  22. windcode/extensions/commands.py +25 -0
  23. windcode/extensions/discovery.py +139 -0
  24. windcode/extensions/events.py +58 -0
  25. windcode/extensions/hooks/__init__.py +4 -0
  26. windcode/extensions/hooks/dispatcher.py +107 -0
  27. windcode/extensions/hooks/executor.py +49 -0
  28. windcode/extensions/hooks/loader.py +101 -0
  29. windcode/extensions/hooks/models.py +109 -0
  30. windcode/extensions/mcp/__init__.py +10 -0
  31. windcode/extensions/mcp/adapter.py +101 -0
  32. windcode/extensions/mcp/catalog.py +153 -0
  33. windcode/extensions/mcp/client.py +273 -0
  34. windcode/extensions/mcp/runtime.py +144 -0
  35. windcode/extensions/mcp/tools.py +570 -0
  36. windcode/extensions/models.py +168 -0
  37. windcode/extensions/paths.py +71 -0
  38. windcode/extensions/plugins/__init__.py +3 -0
  39. windcode/extensions/plugins/installer.py +93 -0
  40. windcode/extensions/plugins/manifest.py +166 -0
  41. windcode/extensions/runtime.py +366 -0
  42. windcode/extensions/service.py +437 -0
  43. windcode/extensions/skills/__init__.py +3 -0
  44. windcode/extensions/skills/loader.py +67 -0
  45. windcode/extensions/skills/parser.py +57 -0
  46. windcode/extensions/skills/tools.py +213 -0
  47. windcode/extensions/snapshot.py +57 -0
  48. windcode/extensions/state.py +158 -0
  49. windcode/instructions/__init__.py +8 -0
  50. windcode/instructions/loader.py +50 -0
  51. windcode/memory/__init__.py +57 -0
  52. windcode/memory/extraction.py +83 -0
  53. windcode/memory/models.py +218 -0
  54. windcode/memory/refiner.py +189 -0
  55. windcode/memory/security.py +23 -0
  56. windcode/memory/service.py +179 -0
  57. windcode/memory/store.py +362 -0
  58. windcode/observability/__init__.py +4 -0
  59. windcode/observability/redaction.py +95 -0
  60. windcode/observability/trace.py +115 -0
  61. windcode/policy/__init__.py +22 -0
  62. windcode/policy/engine.py +137 -0
  63. windcode/policy/models.py +69 -0
  64. windcode/providers/__init__.py +29 -0
  65. windcode/providers/_utils.py +19 -0
  66. windcode/providers/anthropic.py +215 -0
  67. windcode/providers/base.py +46 -0
  68. windcode/providers/catalog.py +119 -0
  69. windcode/providers/errors.py +96 -0
  70. windcode/providers/openai_compat.py +231 -0
  71. windcode/providers/openai_responses.py +189 -0
  72. windcode/providers/registry.py +105 -0
  73. windcode/runtime/__init__.py +15 -0
  74. windcode/runtime/control.py +89 -0
  75. windcode/runtime/event_bus.py +67 -0
  76. windcode/runtime/loop.py +510 -0
  77. windcode/runtime/prompts.py +132 -0
  78. windcode/runtime/report.py +64 -0
  79. windcode/runtime/retry.py +73 -0
  80. windcode/runtime/scheduler.py +194 -0
  81. windcode/runtime/subagents/__init__.py +28 -0
  82. windcode/runtime/subagents/approvals.py +88 -0
  83. windcode/runtime/subagents/budgets.py +79 -0
  84. windcode/runtime/subagents/coordinator.py +714 -0
  85. windcode/runtime/subagents/factory.py +311 -0
  86. windcode/runtime/subagents/roles.py +74 -0
  87. windcode/runtime/subagents/verification.py +53 -0
  88. windcode/sandbox/__init__.py +3 -0
  89. windcode/sandbox/bwrap.py +79 -0
  90. windcode/sdk.py +1259 -0
  91. windcode/sessions/__init__.py +23 -0
  92. windcode/sessions/artifacts.py +69 -0
  93. windcode/sessions/models.py +104 -0
  94. windcode/sessions/store.py +167 -0
  95. windcode/sessions/tree.py +35 -0
  96. windcode/tools/__init__.py +10 -0
  97. windcode/tools/apply_patch.py +191 -0
  98. windcode/tools/ask_user.py +58 -0
  99. windcode/tools/builtins.py +44 -0
  100. windcode/tools/edit_file.py +54 -0
  101. windcode/tools/filesystem.py +77 -0
  102. windcode/tools/glob.py +35 -0
  103. windcode/tools/grep.py +66 -0
  104. windcode/tools/memory.py +400 -0
  105. windcode/tools/read_file.py +54 -0
  106. windcode/tools/registry.py +120 -0
  107. windcode/tools/shell.py +159 -0
  108. windcode/tools/subagents/__init__.py +31 -0
  109. windcode/tools/subagents/cancel.py +35 -0
  110. windcode/tools/subagents/integrate.py +54 -0
  111. windcode/tools/subagents/list.py +46 -0
  112. windcode/tools/subagents/spawn.py +86 -0
  113. windcode/tools/subagents/wait.py +74 -0
  114. windcode/tools/write_file.py +61 -0
  115. windcode/tui/__init__.py +3 -0
  116. windcode/tui/app.py +1015 -0
  117. windcode/tui/commands.py +90 -0
  118. windcode/tui/permission_display.py +24 -0
  119. windcode/tui/styles.tcss +591 -0
  120. windcode/tui/widgets/__init__.py +31 -0
  121. windcode/tui/widgets/approval.py +98 -0
  122. windcode/tui/widgets/command_menu.py +90 -0
  123. windcode/tui/widgets/extensions.py +48 -0
  124. windcode/tui/widgets/input.py +110 -0
  125. windcode/tui/widgets/memory.py +143 -0
  126. windcode/tui/widgets/messages.py +299 -0
  127. windcode/tui/widgets/models.py +565 -0
  128. windcode/tui/widgets/question.py +46 -0
  129. windcode/tui/widgets/sessions.py +68 -0
  130. windcode/tui/widgets/status.py +46 -0
  131. windcode/tui/widgets/subagents.py +195 -0
  132. windcode/tui/widgets/tools.py +66 -0
  133. windcode/tui/widgets/welcome.py +149 -0
  134. windcode/types.py +80 -0
  135. windcode/worktrees/__init__.py +24 -0
  136. windcode/worktrees/git.py +92 -0
  137. windcode/worktrees/manager.py +265 -0
  138. windcode/worktrees/models.py +62 -0
  139. windcode-0.1.0.dist-info/METADATA +207 -0
  140. windcode-0.1.0.dist-info/RECORD +143 -0
  141. windcode-0.1.0.dist-info/WHEEL +4 -0
  142. windcode-0.1.0.dist-info/entry_points.txt +2 -0
  143. windcode-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,213 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from collections.abc import Awaitable, Callable
5
+ from dataclasses import dataclass
6
+ from typing import cast
7
+
8
+ from pydantic import BaseModel, ConfigDict, Field
9
+
10
+ from windcode.domain.messages import SourcedContextMessage
11
+ from windcode.domain.tools import ToolContext, ToolEffect, ToolResult
12
+ from windcode.extensions.models import (
13
+ ActivationState,
14
+ CapabilityKind,
15
+ CapabilityRecord,
16
+ ExtensionSnapshot,
17
+ )
18
+ from windcode.extensions.skills.loader import SkillContent, SkillLoader
19
+ from windcode.extensions.skills.parser import SkillMetadata
20
+ from windcode.tools.registry import ToolRegistry
21
+
22
+
23
+ @dataclass(frozen=True, slots=True)
24
+ class SkillSearchResult:
25
+ capability_id: str
26
+ name: str
27
+ description: str
28
+ source_id: str
29
+ shadowed_by: str | None
30
+
31
+
32
+ @dataclass(frozen=True, slots=True)
33
+ class SkillActivationResult:
34
+ name: str
35
+ source_id: str
36
+ digest: str
37
+ loaded: bool
38
+
39
+
40
+ class SkillCatalog:
41
+ def __init__(self, snapshot: ExtensionSnapshot, loader: SkillLoader) -> None:
42
+ self.snapshot = snapshot
43
+ self.loader = loader
44
+
45
+ def search(self, query: str = "") -> tuple[SkillSearchResult, ...]:
46
+ needle = query.casefold().strip()
47
+ results: list[SkillSearchResult] = []
48
+ for record in self.snapshot.capabilities:
49
+ if record.kind is not CapabilityKind.SKILL:
50
+ continue
51
+ if (
52
+ not record.enabled
53
+ or not record.trusted
54
+ or record.shadowed_by is not None
55
+ or record.activation is ActivationState.FAILED
56
+ ):
57
+ continue
58
+ metadata = self.snapshot.definitions.get(record.capability_id)
59
+ if not isinstance(metadata, SkillMetadata):
60
+ continue
61
+ if (
62
+ needle
63
+ and needle not in metadata.name.casefold()
64
+ and needle not in metadata.description.casefold()
65
+ ):
66
+ continue
67
+ results.append(
68
+ SkillSearchResult(
69
+ record.capability_id,
70
+ metadata.name,
71
+ metadata.description,
72
+ record.source.source_id,
73
+ record.shadowed_by,
74
+ )
75
+ )
76
+ return tuple(results)
77
+
78
+ def load(self, selector: str) -> tuple[SkillContent, SourcedContextMessage]:
79
+ normalized = selector.removeprefix("$").casefold()
80
+ matches: list[tuple[CapabilityRecord, SkillMetadata]] = []
81
+ for record in self.snapshot.capabilities:
82
+ metadata = self.snapshot.definitions.get(record.capability_id)
83
+ if (
84
+ record.kind is CapabilityKind.SKILL
85
+ and record.enabled
86
+ and record.trusted
87
+ and record.shadowed_by is None
88
+ and isinstance(metadata, SkillMetadata)
89
+ and (record.capability_id == selector or metadata.name == normalized)
90
+ ):
91
+ matches.append((record, metadata))
92
+ if len(matches) != 1:
93
+ raise ValueError(f"Skill selector is missing or ambiguous: {selector}")
94
+ content = self.loader.load(*matches[0])
95
+ return content, SourcedContextMessage(content.source_id, content.content)
96
+
97
+
98
+ class SkillRuntime:
99
+ """Per-run Skill catalog, activation state, and pending sourced context."""
100
+
101
+ def __init__(self, catalog: SkillCatalog) -> None:
102
+ self.catalog = catalog
103
+ self._loaded: set[tuple[str, str]] = set()
104
+ self._pending_context: list[SourcedContextMessage] = []
105
+
106
+ def search(self, query: str = "") -> tuple[SkillSearchResult, ...]:
107
+ return self.catalog.search(query)
108
+
109
+ def activate(self, selector: str) -> SkillActivationResult:
110
+ content, message = self.catalog.load(selector)
111
+ key = (content.source_id, content.digest)
112
+ loaded = key not in self._loaded
113
+ if loaded:
114
+ self._loaded.add(key)
115
+ self._pending_context.append(message)
116
+ return SkillActivationResult(content.name, content.source_id, content.digest, loaded)
117
+
118
+ def drain_context(self) -> tuple[SourcedContextMessage, ...]:
119
+ messages = tuple(self._pending_context)
120
+ self._pending_context.clear()
121
+ return messages
122
+
123
+
124
+ class _StrictInput(BaseModel):
125
+ model_config = ConfigDict(extra="forbid")
126
+
127
+
128
+ class SearchSkillsInput(_StrictInput):
129
+ query: str = Field(default="", max_length=2_000)
130
+
131
+
132
+ class LoadSkillInput(_StrictInput):
133
+ name: str = Field(min_length=1, max_length=256)
134
+
135
+
136
+ SkillActivator = Callable[[str], Awaitable[SkillActivationResult]]
137
+
138
+
139
+ class SearchSkillsTool:
140
+ name = "search_skills"
141
+ description = (
142
+ "Search enabled and trusted Agent Skills by name or description. Results contain compact "
143
+ "metadata only. Use load_skill with an exact result name to load its instructions."
144
+ )
145
+ input_model = SearchSkillsInput
146
+ effects = frozenset[ToolEffect]()
147
+
148
+ def __init__(self, runtime: SkillRuntime) -> None:
149
+ self.runtime = runtime
150
+
151
+ async def execute(self, context: ToolContext, arguments: BaseModel) -> ToolResult:
152
+ del context
153
+ parsed = cast(SearchSkillsInput, arguments)
154
+ results = self.runtime.search(parsed.query)
155
+ data = {
156
+ "skills": [
157
+ {
158
+ "capability_id": item.capability_id,
159
+ "name": item.name,
160
+ "description": item.description,
161
+ "source_id": item.source_id,
162
+ }
163
+ for item in results
164
+ ]
165
+ }
166
+ return ToolResult(json.dumps(data, ensure_ascii=False, sort_keys=True), data=data)
167
+
168
+
169
+ class LoadSkillTool:
170
+ name = "load_skill"
171
+ description = (
172
+ "Load one enabled and trusted Agent Skill into the next model step. Pass an exact name "
173
+ "returned by search_skills. Repeated loads in one run do not duplicate instructions."
174
+ )
175
+ input_model = LoadSkillInput
176
+ effects = frozenset({ToolEffect.READ})
177
+
178
+ def __init__(self, activate: SkillActivator) -> None:
179
+ self.activate = activate
180
+
181
+ async def execute(self, context: ToolContext, arguments: BaseModel) -> ToolResult:
182
+ del context
183
+ parsed = cast(LoadSkillInput, arguments)
184
+ try:
185
+ result = await self.activate(parsed.name)
186
+ except ValueError as exc:
187
+ data = {
188
+ "error": "skill_unavailable",
189
+ "message": str(exc),
190
+ "name": parsed.name,
191
+ }
192
+ return ToolResult(
193
+ json.dumps(data, ensure_ascii=False, sort_keys=True),
194
+ is_error=True,
195
+ data=data,
196
+ )
197
+ data = {
198
+ "name": result.name,
199
+ "source_id": result.source_id,
200
+ "status": "loaded" if result.loaded else "already_loaded",
201
+ }
202
+ return ToolResult(json.dumps(data, ensure_ascii=False, sort_keys=True), data=data)
203
+
204
+
205
+ def register_skill_tools(
206
+ registry: ToolRegistry,
207
+ runtime: SkillRuntime,
208
+ activate: SkillActivator,
209
+ *,
210
+ replace: bool = False,
211
+ ) -> None:
212
+ registry.register(SearchSkillsTool(runtime), replace=replace)
213
+ registry.register(LoadSkillTool(activate), replace=replace)
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ import threading
6
+ from dataclasses import dataclass
7
+
8
+ from windcode.extensions.discovery import DiscoveryResult
9
+ from windcode.extensions.models import DiagnosticSeverity, ExtensionSnapshot
10
+
11
+
12
+ def config_fingerprint(value: object) -> str:
13
+ payload = json.dumps(value, sort_keys=True, separators=(",", ":"), default=str).encode()
14
+ return hashlib.sha256(payload).hexdigest()
15
+
16
+
17
+ @dataclass(frozen=True, slots=True)
18
+ class SnapshotCandidate:
19
+ snapshot: ExtensionSnapshot
20
+ publishable: bool
21
+
22
+
23
+ def build_candidate(
24
+ result: DiscoveryResult, *, generation: int, config: object
25
+ ) -> SnapshotCandidate:
26
+ snapshot = ExtensionSnapshot(
27
+ generation,
28
+ config_fingerprint(config),
29
+ result.records,
30
+ result.definitions,
31
+ result.diagnostics,
32
+ )
33
+ required_sources = {item.source.source_id for item in result.records if item.required}
34
+ blocked = any(
35
+ diagnostic.severity is DiagnosticSeverity.ERROR and diagnostic.source_id in required_sources
36
+ for diagnostic in result.diagnostics
37
+ )
38
+ return SnapshotCandidate(snapshot, not blocked)
39
+
40
+
41
+ class SnapshotPublisher:
42
+ def __init__(self, initial: ExtensionSnapshot | None = None) -> None:
43
+ self._current = initial or ExtensionSnapshot(0, config_fingerprint({}))
44
+ self._write_lock = threading.Lock()
45
+
46
+ @property
47
+ def current(self) -> ExtensionSnapshot:
48
+ return self._current
49
+
50
+ def publish(self, candidate: SnapshotCandidate) -> bool:
51
+ if not candidate.publishable:
52
+ return False
53
+ with self._write_lock:
54
+ if candidate.snapshot.generation <= self._current.generation:
55
+ raise ValueError("snapshot generation must increase")
56
+ self._current = candidate.snapshot
57
+ return True
@@ -0,0 +1,158 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ import os
6
+ from dataclasses import asdict, dataclass, field
7
+ from pathlib import Path
8
+ from typing import Any, cast
9
+ from uuid import uuid4
10
+
11
+ from windcode.extensions.models import Diagnostic, DiagnosticSeverity, DiagnosticStage
12
+
13
+
14
+ @dataclass(frozen=True, slots=True)
15
+ class InstalledPlugin:
16
+ plugin_id: str
17
+ version: str
18
+ digest: str
19
+ source_label: str
20
+ installed_at: str
21
+ enabled: bool = False
22
+
23
+
24
+ @dataclass(frozen=True, slots=True)
25
+ class WorkspaceTrust:
26
+ key: str
27
+ canonical_path: str
28
+ device: int
29
+ inode: int
30
+ trusted: bool
31
+
32
+
33
+ @dataclass(frozen=True, slots=True)
34
+ class ManagementAuditRecord:
35
+ event_id: str
36
+ action: str
37
+ generation: int
38
+ source_id: str
39
+ status: str
40
+ timestamp: str
41
+
42
+
43
+ @dataclass(frozen=True, slots=True)
44
+ class ExtensionState:
45
+ version: int = 1
46
+ plugins: dict[str, InstalledPlugin] = field(default_factory=dict[str, InstalledPlugin])
47
+ workspaces: dict[str, WorkspaceTrust] = field(default_factory=dict[str, WorkspaceTrust])
48
+ enabled: dict[str, bool] = field(default_factory=dict[str, bool])
49
+ config: dict[str, dict[str, str | int | float | bool]] = field(
50
+ default_factory=dict[str, dict[str, str | int | float | bool]]
51
+ )
52
+ audit: tuple[ManagementAuditRecord, ...] = ()
53
+
54
+
55
+ @dataclass(frozen=True, slots=True)
56
+ class StateLoadResult:
57
+ state: ExtensionState | None
58
+ diagnostics: tuple[Diagnostic, ...] = ()
59
+
60
+
61
+ def workspace_identity(workspace: Path) -> WorkspaceTrust:
62
+ canonical = workspace.expanduser().resolve(strict=True)
63
+ info = canonical.stat()
64
+ raw = f"{canonical}\0{info.st_dev}\0{info.st_ino}".encode()
65
+ key = hashlib.sha256(raw).hexdigest()
66
+ return WorkspaceTrust(key, str(canonical), info.st_dev, info.st_ino, trusted=False)
67
+
68
+
69
+ class ExtensionStateStore:
70
+ def __init__(self, path: Path) -> None:
71
+ self.path = path.expanduser().resolve()
72
+
73
+ def load(self) -> StateLoadResult:
74
+ if not self.path.exists():
75
+ return StateLoadResult(ExtensionState())
76
+ try:
77
+ raw = cast(dict[str, Any], json.loads(self.path.read_text(encoding="utf-8")))
78
+ plugins = {
79
+ key: InstalledPlugin(**cast(dict[str, Any], value))
80
+ for key, value in cast(dict[str, object], raw.get("plugins", {})).items()
81
+ }
82
+ workspaces = {
83
+ key: WorkspaceTrust(**cast(dict[str, Any], value))
84
+ for key, value in cast(dict[str, object], raw.get("workspaces", {})).items()
85
+ }
86
+ state = ExtensionState(
87
+ version=int(raw.get("version", 1)),
88
+ plugins=plugins,
89
+ workspaces=workspaces,
90
+ enabled={
91
+ str(key): bool(value)
92
+ for key, value in cast(dict[str, object], raw.get("enabled", {})).items()
93
+ },
94
+ config=cast(dict[str, dict[str, str | int | float | bool]], raw.get("config", {})),
95
+ audit=tuple(
96
+ ManagementAuditRecord(**cast(dict[str, Any], value))
97
+ for value in cast(list[object], raw.get("audit", []))
98
+ ),
99
+ )
100
+ return StateLoadResult(state)
101
+ except (OSError, UnicodeError, json.JSONDecodeError, TypeError, ValueError) as exc:
102
+ diagnostic = Diagnostic(
103
+ stage=DiagnosticStage.PARSE,
104
+ severity=DiagnosticSeverity.ERROR,
105
+ category="state_corrupt",
106
+ message=f"extension state could not be read: {exc}",
107
+ source_id="extension-state",
108
+ suggestion="Repair or restore the state file; Windcode did not overwrite it.",
109
+ )
110
+ return StateLoadResult(None, (diagnostic,))
111
+
112
+ def save(self, state: ExtensionState) -> None:
113
+ self.path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
114
+ os.chmod(self.path.parent, 0o700)
115
+ temporary = self.path.with_name(f".{self.path.name}.tmp-{uuid4().hex}")
116
+ payload = json.dumps(asdict(state), sort_keys=True, separators=(",", ":")) + "\n"
117
+ try:
118
+ descriptor = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
119
+ with os.fdopen(descriptor, "w", encoding="utf-8") as stream:
120
+ stream.write(payload)
121
+ stream.flush()
122
+ os.fsync(stream.fileno())
123
+ os.replace(temporary, self.path)
124
+ os.chmod(self.path, 0o600)
125
+ directory_fd = os.open(self.path.parent, os.O_RDONLY)
126
+ try:
127
+ os.fsync(directory_fd)
128
+ finally:
129
+ os.close(directory_fd)
130
+ finally:
131
+ temporary.unlink(missing_ok=True)
132
+
133
+ def set_workspace_trust(
134
+ self, state: ExtensionState, workspace: Path, trusted: bool
135
+ ) -> ExtensionState:
136
+ identity = workspace_identity(workspace)
137
+ record = WorkspaceTrust(
138
+ identity.key,
139
+ identity.canonical_path,
140
+ identity.device,
141
+ identity.inode,
142
+ trusted,
143
+ )
144
+ workspaces = dict(state.workspaces)
145
+ workspaces[record.key] = record
146
+ return ExtensionState(
147
+ state.version,
148
+ dict(state.plugins),
149
+ workspaces,
150
+ dict(state.enabled),
151
+ dict(state.config),
152
+ state.audit,
153
+ )
154
+
155
+ def is_workspace_trusted(self, state: ExtensionState, workspace: Path) -> bool:
156
+ identity = workspace_identity(workspace)
157
+ record = state.workspaces.get(identity.key)
158
+ return record is not None and record.trusted
@@ -0,0 +1,8 @@
1
+ from windcode.instructions.loader import (
2
+ INSTRUCTION_FILES,
3
+ InstructionBlock,
4
+ find_workspace_root,
5
+ load_instructions,
6
+ )
7
+
8
+ __all__ = ["INSTRUCTION_FILES", "InstructionBlock", "find_workspace_root", "load_instructions"]
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+ INSTRUCTION_FILES = ("AGENTS.md", "WINDCODE.md", "CLAUDE.md", "HERMES.md")
7
+
8
+
9
+ @dataclass(frozen=True, slots=True)
10
+ class InstructionBlock:
11
+ path: Path
12
+ content: str
13
+
14
+
15
+ def find_workspace_root(current_dir: Path, workspace_root: Path | None = None) -> Path:
16
+ current = current_dir.expanduser().resolve()
17
+ if workspace_root is not None:
18
+ root = workspace_root.expanduser().resolve()
19
+ if not current.is_relative_to(root):
20
+ raise ValueError(f"current directory {current} is outside workspace {root}")
21
+ return root
22
+
23
+ for candidate in (current, *current.parents):
24
+ if (candidate / ".git").exists():
25
+ return candidate
26
+ return current
27
+
28
+
29
+ def load_instructions(
30
+ current_dir: Path,
31
+ *,
32
+ workspace_root: Path | None = None,
33
+ ) -> tuple[InstructionBlock, ...]:
34
+ current = current_dir.expanduser().resolve()
35
+ root = find_workspace_root(current, workspace_root)
36
+ relative = current.relative_to(root)
37
+ directories = [root]
38
+ cursor = root
39
+ for part in relative.parts:
40
+ cursor /= part
41
+ directories.append(cursor)
42
+
43
+ blocks: list[InstructionBlock] = []
44
+ for directory in directories:
45
+ for filename in INSTRUCTION_FILES:
46
+ path = directory / filename
47
+ if path.is_file():
48
+ blocks.append(InstructionBlock(path=path, content=path.read_text(encoding="utf-8")))
49
+ break
50
+ return tuple(blocks)
@@ -0,0 +1,57 @@
1
+ from windcode.memory.extraction import (
2
+ classify_memory_intent,
3
+ explicitly_always_project_fact,
4
+ has_explicit_memory_intent,
5
+ is_project_fact,
6
+ is_stable_user_fact,
7
+ should_assess_experience,
8
+ )
9
+ from windcode.memory.models import (
10
+ MemoryActivation,
11
+ MemoryKind,
12
+ MemoryRecord,
13
+ MemoryScope,
14
+ MemorySearchResult,
15
+ MemorySource,
16
+ MemoryStatus,
17
+ default_memory_activation,
18
+ default_memory_priority,
19
+ )
20
+ from windcode.memory.refiner import (
21
+ ExperienceAssessment,
22
+ RefinedMemory,
23
+ assess_core_project_fact,
24
+ assess_experience,
25
+ refine_memory,
26
+ )
27
+ from windcode.memory.security import SensitiveMemoryError, contains_sensitive_data
28
+ from windcode.memory.service import MemoryService
29
+ from windcode.memory.store import MemoryStore, project_identifier
30
+
31
+ __all__ = [
32
+ "ExperienceAssessment",
33
+ "MemoryActivation",
34
+ "MemoryKind",
35
+ "MemoryRecord",
36
+ "MemoryScope",
37
+ "MemorySearchResult",
38
+ "MemoryService",
39
+ "MemorySource",
40
+ "MemoryStatus",
41
+ "MemoryStore",
42
+ "RefinedMemory",
43
+ "SensitiveMemoryError",
44
+ "assess_core_project_fact",
45
+ "assess_experience",
46
+ "classify_memory_intent",
47
+ "contains_sensitive_data",
48
+ "default_memory_activation",
49
+ "default_memory_priority",
50
+ "explicitly_always_project_fact",
51
+ "has_explicit_memory_intent",
52
+ "is_project_fact",
53
+ "is_stable_user_fact",
54
+ "project_identifier",
55
+ "refine_memory",
56
+ "should_assess_experience",
57
+ ]
@@ -0,0 +1,83 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ from windcode.memory.models import MemoryKind
6
+
7
+ _USER_FACT_PATTERNS = (
8
+ re.compile(r"(?:^|[,,。\s])我(?:喜欢|偏好|习惯|希望|通常|总是|不喜欢|不希望).+"), # noqa: RUF001
9
+ re.compile(r"(?:^|[,,。\s])我的(?:偏好|习惯|常用|工作方式|沟通方式).+(?:是|为).+"), # noqa: RUF001
10
+ re.compile(r"(?i)(?:^|[,.\s])I\s+(?:like|prefer|usually|always|dislike|want).+"),
11
+ re.compile(r"(?i)(?:^|[,.\s])my\s+(?:preference|workflow|habit).+\s+is\s+.+"),
12
+ )
13
+ _QUESTION_MARKERS = ("?", "?", "什么", "吗", "么", "why", "what", "how") # noqa: RUF001
14
+ _EXPERIENCE_MARKERS = ("经验", "lesson")
15
+ _REFERENCE_MARKERS = ("参考资料", "这份资料", "以下资料", "reference")
16
+ _SOP_MARKERS = ("sop", "标准操作流程", "操作规程", "执行流程", "工作流程")
17
+ _ALWAYS_PROJECT_MARKERS = ("每次都要记住", "始终适用", "永远适用", "always applies")
18
+
19
+
20
+ def is_stable_user_fact(text: str) -> bool:
21
+ normalized = " ".join(text.strip().split())
22
+ if not normalized or any(marker in normalized.casefold() for marker in _QUESTION_MARKERS):
23
+ return False
24
+ return any(pattern.search(normalized) is not None for pattern in _USER_FACT_PATTERNS)
25
+
26
+
27
+ def has_explicit_memory_intent(text: str) -> bool:
28
+ normalized = text.casefold()
29
+ return any(
30
+ marker in normalized
31
+ for marker in (
32
+ "记住",
33
+ "记下来",
34
+ "记录下来",
35
+ "以后都",
36
+ "写入长期记忆",
37
+ "加入长期记忆",
38
+ "remember",
39
+ )
40
+ )
41
+
42
+
43
+ def is_project_fact(text: str) -> bool:
44
+ normalized = text.casefold()
45
+ return any(
46
+ marker in normalized
47
+ for marker in ("这个项目", "本项目", "仓库", "代码库", "this project", "repository")
48
+ )
49
+
50
+
51
+ def classify_memory_intent(text: str) -> MemoryKind | None:
52
+ """Classify explicit memory requests before applying stable-fact heuristics."""
53
+ normalized = " ".join(text.strip().split()).casefold()
54
+ if has_explicit_memory_intent(normalized):
55
+ if any(marker in normalized for marker in _EXPERIENCE_MARKERS):
56
+ return MemoryKind.EXPERIENCE
57
+ if any(marker in normalized for marker in _SOP_MARKERS):
58
+ return MemoryKind.SOP
59
+ if any(marker in normalized for marker in _REFERENCE_MARKERS):
60
+ return MemoryKind.REFERENCE
61
+ if is_project_fact(normalized):
62
+ return MemoryKind.PROJECT_KNOWLEDGE
63
+ return MemoryKind.USER_PROFILE
64
+ if is_stable_user_fact(normalized):
65
+ return MemoryKind.USER_PROFILE
66
+ return None
67
+
68
+
69
+ def explicitly_always_project_fact(text: str) -> bool:
70
+ normalized = " ".join(text.strip().split()).casefold()
71
+ return is_project_fact(normalized) and any(
72
+ marker in normalized for marker in _ALWAYS_PROJECT_MARKERS
73
+ )
74
+
75
+
76
+ def should_assess_experience(
77
+ *,
78
+ status: str,
79
+ changed_files: tuple[str, ...],
80
+ verification: tuple[str, ...],
81
+ ) -> bool:
82
+ """Require a successful code change before spending a model call on experience extraction."""
83
+ return status == "completed" and bool(changed_files) and bool(verification)