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.
- windcode/__init__.py +6 -0
- windcode/__main__.py +4 -0
- windcode/auth/__init__.py +11 -0
- windcode/auth/store.py +90 -0
- windcode/cli.py +181 -0
- windcode/config/__init__.py +41 -0
- windcode/config/loader.py +117 -0
- windcode/config/models.py +203 -0
- windcode/config/writer.py +74 -0
- windcode/context/__init__.py +18 -0
- windcode/context/compactor.py +72 -0
- windcode/context/estimator.py +89 -0
- windcode/context/truncation.py +87 -0
- windcode/domain/__init__.py +1 -0
- windcode/domain/errors.py +33 -0
- windcode/domain/events.py +597 -0
- windcode/domain/messages.py +226 -0
- windcode/domain/models.py +73 -0
- windcode/domain/subagents.py +263 -0
- windcode/domain/tools.py +55 -0
- windcode/extensions/__init__.py +27 -0
- windcode/extensions/commands.py +25 -0
- windcode/extensions/discovery.py +139 -0
- windcode/extensions/events.py +58 -0
- windcode/extensions/hooks/__init__.py +4 -0
- windcode/extensions/hooks/dispatcher.py +107 -0
- windcode/extensions/hooks/executor.py +49 -0
- windcode/extensions/hooks/loader.py +101 -0
- windcode/extensions/hooks/models.py +109 -0
- windcode/extensions/mcp/__init__.py +10 -0
- windcode/extensions/mcp/adapter.py +101 -0
- windcode/extensions/mcp/catalog.py +153 -0
- windcode/extensions/mcp/client.py +273 -0
- windcode/extensions/mcp/runtime.py +144 -0
- windcode/extensions/mcp/tools.py +570 -0
- windcode/extensions/models.py +168 -0
- windcode/extensions/paths.py +71 -0
- windcode/extensions/plugins/__init__.py +3 -0
- windcode/extensions/plugins/installer.py +93 -0
- windcode/extensions/plugins/manifest.py +166 -0
- windcode/extensions/runtime.py +366 -0
- windcode/extensions/service.py +437 -0
- windcode/extensions/skills/__init__.py +3 -0
- windcode/extensions/skills/loader.py +67 -0
- windcode/extensions/skills/parser.py +57 -0
- windcode/extensions/skills/tools.py +213 -0
- windcode/extensions/snapshot.py +57 -0
- windcode/extensions/state.py +158 -0
- windcode/instructions/__init__.py +8 -0
- windcode/instructions/loader.py +50 -0
- windcode/memory/__init__.py +57 -0
- windcode/memory/extraction.py +83 -0
- windcode/memory/models.py +218 -0
- windcode/memory/refiner.py +189 -0
- windcode/memory/security.py +23 -0
- windcode/memory/service.py +179 -0
- windcode/memory/store.py +362 -0
- windcode/observability/__init__.py +4 -0
- windcode/observability/redaction.py +95 -0
- windcode/observability/trace.py +115 -0
- windcode/policy/__init__.py +22 -0
- windcode/policy/engine.py +137 -0
- windcode/policy/models.py +69 -0
- windcode/providers/__init__.py +29 -0
- windcode/providers/_utils.py +19 -0
- windcode/providers/anthropic.py +215 -0
- windcode/providers/base.py +46 -0
- windcode/providers/catalog.py +119 -0
- windcode/providers/errors.py +96 -0
- windcode/providers/openai_compat.py +231 -0
- windcode/providers/openai_responses.py +189 -0
- windcode/providers/registry.py +105 -0
- windcode/runtime/__init__.py +15 -0
- windcode/runtime/control.py +89 -0
- windcode/runtime/event_bus.py +67 -0
- windcode/runtime/loop.py +510 -0
- windcode/runtime/prompts.py +132 -0
- windcode/runtime/report.py +64 -0
- windcode/runtime/retry.py +73 -0
- windcode/runtime/scheduler.py +194 -0
- windcode/runtime/subagents/__init__.py +28 -0
- windcode/runtime/subagents/approvals.py +88 -0
- windcode/runtime/subagents/budgets.py +79 -0
- windcode/runtime/subagents/coordinator.py +714 -0
- windcode/runtime/subagents/factory.py +311 -0
- windcode/runtime/subagents/roles.py +74 -0
- windcode/runtime/subagents/verification.py +53 -0
- windcode/sandbox/__init__.py +3 -0
- windcode/sandbox/bwrap.py +79 -0
- windcode/sdk.py +1259 -0
- windcode/sessions/__init__.py +23 -0
- windcode/sessions/artifacts.py +69 -0
- windcode/sessions/models.py +104 -0
- windcode/sessions/store.py +167 -0
- windcode/sessions/tree.py +35 -0
- windcode/tools/__init__.py +10 -0
- windcode/tools/apply_patch.py +191 -0
- windcode/tools/ask_user.py +58 -0
- windcode/tools/builtins.py +44 -0
- windcode/tools/edit_file.py +54 -0
- windcode/tools/filesystem.py +77 -0
- windcode/tools/glob.py +35 -0
- windcode/tools/grep.py +66 -0
- windcode/tools/memory.py +400 -0
- windcode/tools/read_file.py +54 -0
- windcode/tools/registry.py +120 -0
- windcode/tools/shell.py +159 -0
- windcode/tools/subagents/__init__.py +31 -0
- windcode/tools/subagents/cancel.py +35 -0
- windcode/tools/subagents/integrate.py +54 -0
- windcode/tools/subagents/list.py +46 -0
- windcode/tools/subagents/spawn.py +86 -0
- windcode/tools/subagents/wait.py +74 -0
- windcode/tools/write_file.py +61 -0
- windcode/tui/__init__.py +3 -0
- windcode/tui/app.py +1015 -0
- windcode/tui/commands.py +90 -0
- windcode/tui/permission_display.py +24 -0
- windcode/tui/styles.tcss +591 -0
- windcode/tui/widgets/__init__.py +31 -0
- windcode/tui/widgets/approval.py +98 -0
- windcode/tui/widgets/command_menu.py +90 -0
- windcode/tui/widgets/extensions.py +48 -0
- windcode/tui/widgets/input.py +110 -0
- windcode/tui/widgets/memory.py +143 -0
- windcode/tui/widgets/messages.py +299 -0
- windcode/tui/widgets/models.py +565 -0
- windcode/tui/widgets/question.py +46 -0
- windcode/tui/widgets/sessions.py +68 -0
- windcode/tui/widgets/status.py +46 -0
- windcode/tui/widgets/subagents.py +195 -0
- windcode/tui/widgets/tools.py +66 -0
- windcode/tui/widgets/welcome.py +149 -0
- windcode/types.py +80 -0
- windcode/worktrees/__init__.py +24 -0
- windcode/worktrees/git.py +92 -0
- windcode/worktrees/manager.py +265 -0
- windcode/worktrees/models.py +62 -0
- windcode-0.1.0.dist-info/METADATA +207 -0
- windcode-0.1.0.dist-info/RECORD +143 -0
- windcode-0.1.0.dist-info/WHEEL +4 -0
- windcode-0.1.0.dist-info/entry_points.txt +2 -0
- 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,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)
|