induscode 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.
- induscode/__init__.py +56 -0
- induscode/addons/__init__.py +176 -0
- induscode/addons/contract.py +923 -0
- induscode/addons/dispatch/__init__.py +43 -0
- induscode/addons/dispatch/event_dispatcher.py +348 -0
- induscode/addons/dispatch/tool_interceptor.py +349 -0
- induscode/addons/host.py +469 -0
- induscode/addons/loader.py +314 -0
- induscode/addons/manifest.py +232 -0
- induscode/addons/surface.py +199 -0
- induscode/boot/__init__.py +108 -0
- induscode/boot/auth_vault.py +323 -0
- induscode/boot/boot.py +210 -0
- induscode/boot/contract.py +223 -0
- induscode/boot/invocation.py +117 -0
- induscode/boot/runners/__init__.py +42 -0
- induscode/boot/runners/link_runner.py +82 -0
- induscode/boot/runners/oneshot_runner.py +85 -0
- induscode/boot/runners/registry.py +46 -0
- induscode/boot/runners/repl_runner.py +340 -0
- induscode/boot/runners/session.py +549 -0
- induscode/boot/stages.py +198 -0
- induscode/boot/upgrade/__init__.py +36 -0
- induscode/boot/upgrade/apply.py +125 -0
- induscode/boot/upgrade/upgrades.py +136 -0
- induscode/briefing/__init__.py +115 -0
- induscode/briefing/compose.py +414 -0
- induscode/briefing/contract.py +528 -0
- induscode/briefing/macros.py +721 -0
- induscode/briefing/skills.py +417 -0
- induscode/capability_deck/__init__.py +233 -0
- induscode/capability_deck/bridge_ledger/__init__.py +66 -0
- induscode/capability_deck/bridge_ledger/key.py +181 -0
- induscode/capability_deck/bridge_ledger/ledger.py +276 -0
- induscode/capability_deck/bridge_ledger/network.py +336 -0
- induscode/capability_deck/builtin_bridge.py +358 -0
- induscode/capability_deck/cards/__init__.py +116 -0
- induscode/capability_deck/cards/bg_process.py +482 -0
- induscode/capability_deck/cards/memory.py +226 -0
- induscode/capability_deck/cards/saas.py +280 -0
- induscode/capability_deck/cards/task.py +256 -0
- induscode/capability_deck/cards/todo.py +312 -0
- induscode/capability_deck/contract.py +450 -0
- induscode/capability_deck/manifest.py +126 -0
- induscode/capability_deck/provision.py +217 -0
- induscode/channels/__init__.py +146 -0
- induscode/channels/contract.py +585 -0
- induscode/channels/framer.py +132 -0
- induscode/channels/link/__init__.py +50 -0
- induscode/channels/link/dialog.py +246 -0
- induscode/channels/link/driver.py +308 -0
- induscode/channels/link/server.py +217 -0
- induscode/channels/oneshot.py +178 -0
- induscode/channels/ops.py +140 -0
- induscode/channels/session_ops.py +172 -0
- induscode/conductor/__init__.py +240 -0
- induscode/conductor/catalog.py +309 -0
- induscode/conductor/conductor.py +1084 -0
- induscode/conductor/contract.py +1035 -0
- induscode/conductor/matcher.py +291 -0
- induscode/conductor/serialize.py +575 -0
- induscode/conductor/signal_hub.py +382 -0
- induscode/conductor/skill_parse.py +294 -0
- induscode/conductor/transcript_store.py +449 -0
- induscode/console/__init__.py +236 -0
- induscode/console/app.py +1677 -0
- induscode/console/components/__init__.py +62 -0
- induscode/console/components/banner.py +499 -0
- induscode/console/components/banner_sweep.py +188 -0
- induscode/console/components/emblem.py +181 -0
- induscode/console/components/status_bar.py +102 -0
- induscode/console/contract.py +836 -0
- induscode/console/input/__init__.py +107 -0
- induscode/console/input/chord.py +197 -0
- induscode/console/input/dir_reader.py +113 -0
- induscode/console/input/intents.py +258 -0
- induscode/console/input/providers.py +469 -0
- induscode/console/mount.py +137 -0
- induscode/console/overlays/__init__.py +94 -0
- induscode/console/overlays/auth.py +503 -0
- induscode/console/overlays/pickers.py +526 -0
- induscode/console/overlays/router.py +129 -0
- induscode/console/overlays/sessions.py +232 -0
- induscode/console/reducer.py +145 -0
- induscode/console/resume_picker.py +156 -0
- induscode/console/slash_commands/__init__.py +78 -0
- induscode/console/slash_commands/builtins.py +254 -0
- induscode/console/slash_commands/dynamic.py +217 -0
- induscode/console/slash_commands/integrations.py +949 -0
- induscode/console/slash_commands/transcript.py +404 -0
- induscode/console/slash_commands/workbench.py +430 -0
- induscode/console/startup.py +434 -0
- induscode/console/theme/__init__.py +44 -0
- induscode/console/theme/adapter.py +168 -0
- induscode/console/theme/palette.py +128 -0
- induscode/console/theme/resolve.py +123 -0
- induscode/console/theme/tokens.py +185 -0
- induscode/console_slash/__init__.py +111 -0
- induscode/console_slash/contract.py +185 -0
- induscode/console_slash/registry.py +140 -0
- induscode/console_slash/resolve.py +194 -0
- induscode/console_slash/shared.py +172 -0
- induscode/entry.py +108 -0
- induscode/insight/__init__.py +153 -0
- induscode/insight/collector.py +73 -0
- induscode/insight/replay.py +305 -0
- induscode/insight/wrapper.py +1115 -0
- induscode/kit/__init__.py +82 -0
- induscode/kit/clipboard_image.py +215 -0
- induscode/kit/external_editor.py +120 -0
- induscode/kit/image.py +188 -0
- induscode/kit/shell.py +89 -0
- induscode/kit/tool_fetch.py +288 -0
- induscode/launch/__init__.py +224 -0
- induscode/launch/catalog.py +310 -0
- induscode/launch/contract.py +569 -0
- induscode/launch/credentials.py +852 -0
- induscode/launch/invocation/__init__.py +39 -0
- induscode/launch/invocation/attachments.py +281 -0
- induscode/launch/invocation/flags.py +210 -0
- induscode/launch/invocation/read.py +369 -0
- induscode/launch/invocation/usage.py +110 -0
- induscode/launch/oauth.py +808 -0
- induscode/launch/packages.py +299 -0
- induscode/launch/pickers.py +291 -0
- induscode/py.typed +0 -0
- induscode/runtime_bridge/__init__.py +166 -0
- induscode/runtime_bridge/bridges/__init__.py +66 -0
- induscode/runtime_bridge/bridges/_drive.py +268 -0
- induscode/runtime_bridge/bridges/builtins.py +177 -0
- induscode/runtime_bridge/bridges/claude_cli.py +198 -0
- induscode/runtime_bridge/bridges/codex_cli.py +203 -0
- induscode/runtime_bridge/bridges/indusagi_cli.py +217 -0
- induscode/runtime_bridge/broker.py +397 -0
- induscode/runtime_bridge/contract.py +734 -0
- induscode/runtime_bridge/sink.py +351 -0
- induscode/sessions/__init__.py +25 -0
- induscode/sessions/contract.py +119 -0
- induscode/sessions/library.py +350 -0
- induscode/settings/__init__.py +47 -0
- induscode/settings/contract.py +313 -0
- induscode/settings/manager.py +268 -0
- induscode/transcript_export/__init__.py +109 -0
- induscode/transcript_export/contract.py +522 -0
- induscode/transcript_export/publish.py +455 -0
- induscode/transcript_export/sgr.py +566 -0
- induscode/transcript_export/template.py +319 -0
- induscode/transcript_export/theme_bridge.py +325 -0
- induscode/window_budget/__init__.py +76 -0
- induscode/window_budget/budget/__init__.py +26 -0
- induscode/window_budget/budget/estimate.py +273 -0
- induscode/window_budget/budget/gate.py +60 -0
- induscode/window_budget/budget/slice.py +145 -0
- induscode/window_budget/condenser.py +170 -0
- induscode/window_budget/contract.py +329 -0
- induscode/window_budget/summarize/__init__.py +33 -0
- induscode/window_budget/summarize/condense.py +212 -0
- induscode/window_budget/summarize/prompt.py +241 -0
- induscode/workspace/__init__.py +30 -0
- induscode/workspace/brand.py +96 -0
- induscode/workspace/locator.py +269 -0
- induscode-0.1.0.dist-info/METADATA +97 -0
- induscode-0.1.0.dist-info/RECORD +167 -0
- induscode-0.1.0.dist-info/WHEEL +4 -0
- induscode-0.1.0.dist-info/entry_points.txt +3 -0
- induscode-0.1.0.dist-info/licenses/CREDITS.md +22 -0
- induscode-0.1.0.dist-info/licenses/NOTICE +7 -0
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
"""Capability-card loader — ``SKILL.md`` discovery, validation, and parsing.
|
|
2
|
+
|
|
3
|
+
A capability card is *documentation the model reads*: a markdown file in the
|
|
4
|
+
Agent-Skills format whose frontmatter names a skill and one-line-describes
|
|
5
|
+
when it applies, with a body of on-demand instructions. The model decides from
|
|
6
|
+
the ``description`` whether a task matches, then loads the file's ``location``
|
|
7
|
+
to read the full body. This module turns a set of directory roots into
|
|
8
|
+
validated :class:`~induscode.briefing.contract.SkillCard` records plus a
|
|
9
|
+
diagnostic stream.
|
|
10
|
+
|
|
11
|
+
Discovery follows the format's two shapes, with a generic directory walk
|
|
12
|
+
rather than a special-cased recursion:
|
|
13
|
+
|
|
14
|
+
- At a *root* level, a direct ``*.md`` child is a single-file skill.
|
|
15
|
+
- In any *subdirectory*, a ``SKILL.md`` file is a packaged skill whose name is
|
|
16
|
+
expected to match its enclosing directory.
|
|
17
|
+
|
|
18
|
+
The walk yields candidate file descriptors; a separate validation stage turns
|
|
19
|
+
each candidate into one :class:`~induscode.briefing.contract.SkillDiagnostic`
|
|
20
|
+
(and, on success, one :class:`~induscode.briefing.contract.SkillCard`).
|
|
21
|
+
Validation prose and field policy are the briefing's own; only the *format*
|
|
22
|
+
(frontmatter keys, the ≤64 / ≤1024 limits, lowercase-hyphen names) is the
|
|
23
|
+
shared public spec. Names are deduped across roots — the first card to claim a
|
|
24
|
+
name wins and later claimants are reported as collisions.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import os
|
|
30
|
+
import re
|
|
31
|
+
import stat as _stat
|
|
32
|
+
from collections.abc import Iterator, Mapping, Sequence
|
|
33
|
+
from dataclasses import dataclass
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
from typing import Final
|
|
36
|
+
|
|
37
|
+
from .contract import (
|
|
38
|
+
SKILL_DESCRIPTION_LIMIT,
|
|
39
|
+
SKILL_NAME_LIMIT,
|
|
40
|
+
MacroOrigin,
|
|
41
|
+
SkillCard,
|
|
42
|
+
SkillDiagnostic,
|
|
43
|
+
SkillFrontmatter,
|
|
44
|
+
SkillLoad,
|
|
45
|
+
SkillOutcomeKind,
|
|
46
|
+
)
|
|
47
|
+
from .macros import split_frontmatter
|
|
48
|
+
|
|
49
|
+
__all__ = [
|
|
50
|
+
"SkillRoot",
|
|
51
|
+
"gather_skill_cards",
|
|
52
|
+
"load_skill_cards",
|
|
53
|
+
"model_invocable_cards",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
#: The packaged-skill manifest filename (compared case-sensitively).
|
|
57
|
+
SKILL_MANIFEST: Final[str] = "SKILL.md"
|
|
58
|
+
|
|
59
|
+
#: Directory names the walk never descends into.
|
|
60
|
+
PRUNED_DIRS: Final[frozenset[str]] = frozenset({"node_modules", ".git"})
|
|
61
|
+
|
|
62
|
+
#: The frontmatter keys this loader understands. A key outside this set marks
|
|
63
|
+
#: the document as malformed — the format is closed, so a stray key is far
|
|
64
|
+
#: more likely a typo than an intentional extension.
|
|
65
|
+
KNOWN_FRONTMATTER_KEYS: Final[frozenset[str]] = frozenset(
|
|
66
|
+
{
|
|
67
|
+
"name",
|
|
68
|
+
"description",
|
|
69
|
+
"license",
|
|
70
|
+
"compatibility",
|
|
71
|
+
"metadata",
|
|
72
|
+
"allowed-tools",
|
|
73
|
+
"disable-model-invocation",
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
#: The Agent-Skills naming rule: lowercase ASCII words joined by single
|
|
78
|
+
#: hyphens.
|
|
79
|
+
_NAME_PATTERN: Final[re.Pattern[str]] = re.compile(r"[a-z0-9]+(?:-[a-z0-9]+)*")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
# Discovery
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
88
|
+
class _Candidate:
|
|
89
|
+
"""One file the walk has identified as a skill candidate."""
|
|
90
|
+
|
|
91
|
+
# Absolute path of the candidate markdown file.
|
|
92
|
+
path: str
|
|
93
|
+
# The name the file's enclosing directory implies (for packaged skills).
|
|
94
|
+
dir_name: str
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _walk_candidates(root: str, seen: set[str]) -> Iterator[_Candidate]:
|
|
98
|
+
"""Walk a root directory, yielding every skill candidate beneath it.
|
|
99
|
+
|
|
100
|
+
Generator-driven so the validation stage can consume candidates lazily. At
|
|
101
|
+
the root the walk treats direct ``*.md`` files as single-file skills; in
|
|
102
|
+
every subdirectory it treats a ``SKILL.md`` as a packaged skill. Dotfiles,
|
|
103
|
+
dot-dirs, and the pruned directory set are skipped, and physical paths are
|
|
104
|
+
not revisited (symlink loops and shared targets resolve to one realpath).
|
|
105
|
+
|
|
106
|
+
:param root: the directory to walk
|
|
107
|
+
:param seen: realpaths already visited, to break symlink cycles
|
|
108
|
+
"""
|
|
109
|
+
yield from _walk_level(root, True, seen)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _walk_level(directory: str, at_root: bool, seen: set[str]) -> Iterator[_Candidate]:
|
|
113
|
+
"""Recursive level walk. ``at_root`` distinguishes the top level (where
|
|
114
|
+
loose ``*.md`` files count) from nested levels (where only ``SKILL.md``
|
|
115
|
+
counts)."""
|
|
116
|
+
try:
|
|
117
|
+
entries = sorted(os.listdir(directory))
|
|
118
|
+
except OSError:
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
for entry in entries:
|
|
122
|
+
if entry.startswith("."):
|
|
123
|
+
continue
|
|
124
|
+
full = os.path.join(directory, entry)
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
stats = os.stat(full) # follows symlinks
|
|
128
|
+
except OSError:
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
if _stat.S_ISDIR(stats.st_mode):
|
|
132
|
+
if entry in PRUNED_DIRS:
|
|
133
|
+
continue
|
|
134
|
+
try:
|
|
135
|
+
real = os.path.realpath(full, strict=True)
|
|
136
|
+
except OSError:
|
|
137
|
+
continue
|
|
138
|
+
if real in seen:
|
|
139
|
+
continue
|
|
140
|
+
seen.add(real)
|
|
141
|
+
yield from _walk_level(full, False, seen)
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
if not _stat.S_ISREG(stats.st_mode):
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
if at_root:
|
|
148
|
+
if entry.lower().endswith(".md"):
|
|
149
|
+
yield _Candidate(path=full, dir_name=os.path.basename(os.path.dirname(full)))
|
|
150
|
+
elif entry == SKILL_MANIFEST:
|
|
151
|
+
yield _Candidate(path=full, dir_name=os.path.basename(os.path.dirname(full)))
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# ---------------------------------------------------------------------------
|
|
155
|
+
# Validation
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
# A validation step's verdict: either a problem string or ``None`` for "ok".
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _check_name(name: str, dir_name: str, from_dir: bool) -> str | None:
|
|
162
|
+
"""Validate a skill name against the Agent-Skills naming rule: lowercase
|
|
163
|
+
ASCII letters, digits, and single internal hyphens, no longer than the
|
|
164
|
+
format limit, and — for packaged skills — matching the enclosing
|
|
165
|
+
directory.
|
|
166
|
+
|
|
167
|
+
:param name: the candidate name (from frontmatter or the directory)
|
|
168
|
+
:param dir_name: the enclosing directory's name, for the match check
|
|
169
|
+
:param from_dir: whether the name was inferred from the directory (skips
|
|
170
|
+
the match check, which would be trivially true)
|
|
171
|
+
:returns: a problem description, or ``None`` when the name is valid
|
|
172
|
+
"""
|
|
173
|
+
if not name:
|
|
174
|
+
return "a skill must declare a non-empty name"
|
|
175
|
+
if len(name) > SKILL_NAME_LIMIT:
|
|
176
|
+
return f"the name exceeds the {SKILL_NAME_LIMIT}-character limit"
|
|
177
|
+
if _NAME_PATTERN.fullmatch(name) is None:
|
|
178
|
+
return "the name must be lowercase words joined by single hyphens"
|
|
179
|
+
if not from_dir and name != dir_name:
|
|
180
|
+
return f'the declared name "{name}" does not match its directory "{dir_name}"'
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _check_description(description: str) -> str | None:
|
|
185
|
+
"""Validate a description: present and within the format's length limit.
|
|
186
|
+
|
|
187
|
+
:param description: the candidate description
|
|
188
|
+
:returns: a problem description, or ``None`` when valid
|
|
189
|
+
"""
|
|
190
|
+
if not description:
|
|
191
|
+
return "a skill must declare a description so the model can gate it"
|
|
192
|
+
if len(description) > SKILL_DESCRIPTION_LIMIT:
|
|
193
|
+
return f"the description exceeds the {SKILL_DESCRIPTION_LIMIT}-character limit"
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _check_frontmatter_keys(frontmatter: Mapping[str, object]) -> str | None:
|
|
198
|
+
"""Validate the frontmatter's key set: every key must be one the format
|
|
199
|
+
defines.
|
|
200
|
+
|
|
201
|
+
:param frontmatter: the raw parsed frontmatter map
|
|
202
|
+
:returns: a problem description naming the first stray key, or ``None``
|
|
203
|
+
"""
|
|
204
|
+
for key in frontmatter:
|
|
205
|
+
if key not in KNOWN_FRONTMATTER_KEYS:
|
|
206
|
+
return f'unrecognised frontmatter key "{key}"'
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# ---------------------------------------------------------------------------
|
|
211
|
+
# Frontmatter projection
|
|
212
|
+
# ---------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _project_frontmatter(raw: Mapping[str, object]) -> SkillFrontmatter:
|
|
216
|
+
"""Project a raw frontmatter map onto the typed
|
|
217
|
+
:class:`~induscode.briefing.contract.SkillFrontmatter`.
|
|
218
|
+
|
|
219
|
+
Translates the format's kebab-case keys (``allowed-tools``,
|
|
220
|
+
``disable-model-invocation``) to the snake_case fields, parses the tool
|
|
221
|
+
list from a comma-or-whitespace-separated scalar, and coerces the
|
|
222
|
+
invocation flag. Keys the projection does not name are dropped (the
|
|
223
|
+
key-set check has already vetted them).
|
|
224
|
+
"""
|
|
225
|
+
kwargs: dict[str, object] = {}
|
|
226
|
+
|
|
227
|
+
name = raw.get("name")
|
|
228
|
+
if isinstance(name, str):
|
|
229
|
+
kwargs["name"] = name.strip()
|
|
230
|
+
description = raw.get("description")
|
|
231
|
+
if isinstance(description, str):
|
|
232
|
+
kwargs["description"] = description.strip()
|
|
233
|
+
license_tag = raw.get("license")
|
|
234
|
+
if isinstance(license_tag, str):
|
|
235
|
+
kwargs["license"] = license_tag.strip()
|
|
236
|
+
compatibility = raw.get("compatibility")
|
|
237
|
+
if isinstance(compatibility, str):
|
|
238
|
+
kwargs["compatibility"] = compatibility.strip()
|
|
239
|
+
|
|
240
|
+
metadata = raw.get("metadata")
|
|
241
|
+
if isinstance(metadata, Mapping):
|
|
242
|
+
kwargs["metadata"] = metadata
|
|
243
|
+
|
|
244
|
+
tools = raw.get("allowed-tools")
|
|
245
|
+
if isinstance(tools, str):
|
|
246
|
+
items = tuple(t.strip() for t in re.split(r"[\s,]+", tools) if t.strip())
|
|
247
|
+
if items:
|
|
248
|
+
kwargs["allowed_tools"] = items
|
|
249
|
+
elif isinstance(tools, (list, tuple)):
|
|
250
|
+
kwargs["allowed_tools"] = tuple(str(t) for t in tools)
|
|
251
|
+
|
|
252
|
+
flag = raw.get("disable-model-invocation")
|
|
253
|
+
if flag is True or flag == "true":
|
|
254
|
+
kwargs["disable_model_invocation"] = True
|
|
255
|
+
|
|
256
|
+
return SkillFrontmatter(**kwargs) # type: ignore[arg-type]
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# ---------------------------------------------------------------------------
|
|
260
|
+
# Parsing one candidate
|
|
261
|
+
# ---------------------------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _diag(kind: SkillOutcomeKind, location: str, detail: str) -> SkillDiagnostic:
|
|
265
|
+
"""Build a diagnostic record."""
|
|
266
|
+
return SkillDiagnostic(kind=kind, location=location, detail=detail)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _parse_candidate(
|
|
270
|
+
candidate: _Candidate, origin: MacroOrigin
|
|
271
|
+
) -> tuple[SkillCard | None, SkillDiagnostic]:
|
|
272
|
+
"""Read and validate one candidate file into a
|
|
273
|
+
:class:`~induscode.briefing.contract.SkillCard` (or a failure diagnostic).
|
|
274
|
+
Never raises: an unreadable file becomes an ``invalid`` diagnostic.
|
|
275
|
+
|
|
276
|
+
:param candidate: the file to parse
|
|
277
|
+
:param origin: where the candidate's root was classified
|
|
278
|
+
:returns: the parsed card (or ``None``) plus its diagnostic
|
|
279
|
+
"""
|
|
280
|
+
path, dir_name = candidate.path, candidate.dir_name
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
text = Path(path).read_text(encoding="utf-8")
|
|
284
|
+
except Exception as cause:
|
|
285
|
+
detail = f"could not read the skill file ({_describe_cause(cause)})"
|
|
286
|
+
return None, _diag("invalid", path, detail)
|
|
287
|
+
|
|
288
|
+
split = split_frontmatter(text)
|
|
289
|
+
|
|
290
|
+
key_problem = _check_frontmatter_keys(split.frontmatter)
|
|
291
|
+
if key_problem is not None:
|
|
292
|
+
return None, _diag("invalid", path, key_problem)
|
|
293
|
+
|
|
294
|
+
fm = _project_frontmatter(split.frontmatter)
|
|
295
|
+
|
|
296
|
+
declared_name = fm.name or ""
|
|
297
|
+
from_dir = not declared_name
|
|
298
|
+
name = dir_name if from_dir else declared_name
|
|
299
|
+
name_problem = _check_name(name, dir_name, from_dir)
|
|
300
|
+
if name_problem is not None:
|
|
301
|
+
return None, _diag("invalid", path, name_problem)
|
|
302
|
+
|
|
303
|
+
description = fm.description or ""
|
|
304
|
+
desc_problem = _check_description(description)
|
|
305
|
+
if desc_problem is not None:
|
|
306
|
+
return None, _diag("invalid", path, desc_problem)
|
|
307
|
+
|
|
308
|
+
card = SkillCard(
|
|
309
|
+
name=name,
|
|
310
|
+
description=description,
|
|
311
|
+
body=split.body,
|
|
312
|
+
location=path,
|
|
313
|
+
origin=origin,
|
|
314
|
+
frontmatter=fm,
|
|
315
|
+
)
|
|
316
|
+
return card, _diag("loaded", path, f'loaded skill "{name}"')
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _describe_cause(cause: object) -> str:
|
|
320
|
+
"""Render a caught value into a short human phrase for a diagnostic."""
|
|
321
|
+
return str(cause)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# ---------------------------------------------------------------------------
|
|
325
|
+
# Aggregation
|
|
326
|
+
# ---------------------------------------------------------------------------
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
330
|
+
class SkillRoot:
|
|
331
|
+
"""One root directory to load skill cards from, with its origin tag."""
|
|
332
|
+
|
|
333
|
+
# The directory to walk.
|
|
334
|
+
dir: str
|
|
335
|
+
# How cards discovered under this root are tagged.
|
|
336
|
+
origin: MacroOrigin
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def load_skill_cards(
|
|
340
|
+
directory: str | os.PathLike[str], origin: MacroOrigin = "path"
|
|
341
|
+
) -> SkillLoad:
|
|
342
|
+
"""Load and validate the capability cards under one directory root.
|
|
343
|
+
|
|
344
|
+
Walks the root for candidates, parses each, and accumulates the
|
|
345
|
+
diagnostics. Name deduplication is *not* applied here (a single root
|
|
346
|
+
rarely collides with itself in a way worth reporting); use
|
|
347
|
+
:func:`gather_skill_cards` to merge several roots with collision
|
|
348
|
+
reporting. Symlink cycles within the root are broken by realpath tracking.
|
|
349
|
+
|
|
350
|
+
:param directory: the root directory to scan
|
|
351
|
+
:param origin: the origin tag for cards found here
|
|
352
|
+
:returns: the cards and diagnostics produced from this root
|
|
353
|
+
"""
|
|
354
|
+
cards: list[SkillCard] = []
|
|
355
|
+
diagnostics: list[SkillDiagnostic] = []
|
|
356
|
+
seen: set[str] = set()
|
|
357
|
+
|
|
358
|
+
for candidate in _walk_candidates(os.fspath(directory), seen):
|
|
359
|
+
card, diagnostic = _parse_candidate(candidate, origin)
|
|
360
|
+
diagnostics.append(diagnostic)
|
|
361
|
+
if card is not None:
|
|
362
|
+
cards.append(card)
|
|
363
|
+
|
|
364
|
+
return SkillLoad(cards=tuple(cards), diagnostics=tuple(diagnostics))
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def gather_skill_cards(roots: Sequence[SkillRoot]) -> SkillLoad:
|
|
368
|
+
"""Load and merge capability cards from several roots, deduping by name.
|
|
369
|
+
|
|
370
|
+
Roots are processed in order; the first card to claim a name wins, and any
|
|
371
|
+
later card with the same name is dropped with a ``collision`` diagnostic
|
|
372
|
+
(its own per-root ``loaded`` line is rewritten to the collision outcome).
|
|
373
|
+
The merged card list is the deduped survivors, in discovery order.
|
|
374
|
+
|
|
375
|
+
:param roots: the roots to load, in precedence order (earliest wins)
|
|
376
|
+
:returns: the merged cards and the combined diagnostic stream
|
|
377
|
+
"""
|
|
378
|
+
cards: list[SkillCard] = []
|
|
379
|
+
diagnostics: list[SkillDiagnostic] = []
|
|
380
|
+
claimed: dict[str, str] = {} # name → winning location
|
|
381
|
+
|
|
382
|
+
for root in roots:
|
|
383
|
+
loaded = load_skill_cards(root.dir, root.origin)
|
|
384
|
+
for d in loaded.diagnostics:
|
|
385
|
+
if d.kind != "loaded":
|
|
386
|
+
diagnostics.append(d)
|
|
387
|
+
# `loaded` diagnostics are re-emitted below alongside the card so
|
|
388
|
+
# a collision can rewrite them; non-loaded ones pass through
|
|
389
|
+
# as-is.
|
|
390
|
+
for card in loaded.cards:
|
|
391
|
+
winner = claimed.get(card.name)
|
|
392
|
+
if winner is not None:
|
|
393
|
+
diagnostics.append(
|
|
394
|
+
_diag(
|
|
395
|
+
"collision",
|
|
396
|
+
card.location,
|
|
397
|
+
f'the name "{card.name}" was already claimed by {winner}; '
|
|
398
|
+
"this card is dropped",
|
|
399
|
+
)
|
|
400
|
+
)
|
|
401
|
+
continue
|
|
402
|
+
claimed[card.name] = card.location
|
|
403
|
+
cards.append(card)
|
|
404
|
+
diagnostics.append(_diag("loaded", card.location, f'loaded skill "{card.name}"'))
|
|
405
|
+
|
|
406
|
+
return SkillLoad(cards=tuple(cards), diagnostics=tuple(diagnostics))
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def model_invocable_cards(cards: Sequence[SkillCard]) -> list[SkillCard]:
|
|
410
|
+
"""Filter cards down to those the model may auto-invoke (i.e. that are not
|
|
411
|
+
marked ``disable-model-invocation``). The hidden cards remain available as
|
|
412
|
+
explicit commands but do not appear in the briefing's skill block.
|
|
413
|
+
|
|
414
|
+
:param cards: the full card set
|
|
415
|
+
:returns: the subset eligible for model invocation
|
|
416
|
+
"""
|
|
417
|
+
return [c for c in cards if c.frontmatter.disable_model_invocation is not True]
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""Capability-deck subsystem — public barrel.
|
|
2
|
+
|
|
3
|
+
Re-exports the FROZEN tooling-layer contract: the :data:`Capability` alias
|
|
4
|
+
over the framework ``AgentTool``, the branded :data:`CapabilityId`, the
|
|
5
|
+
catalog row (:class:`CapabilityCard`) that is the deck's single source of
|
|
6
|
+
truth, the :data:`DeckProfile` table and :class:`DeckContext` injection bag,
|
|
7
|
+
the event-sourced MCP enrollment model (:class:`BridgeEntry`,
|
|
8
|
+
:data:`BridgeOp`, :class:`LedgerSnapshot` + the pure :func:`reduce_ledger`
|
|
9
|
+
fold), the assembled :class:`ToolDeck` the conductor consumes, and the typed
|
|
10
|
+
:class:`DeckFault` — plus every behavior module:
|
|
11
|
+
|
|
12
|
+
- the **builtin bridge** — the single seam re-exposing the framework's native
|
|
13
|
+
tools (read/write/edit/ls/grep/find/bash/process/checklist/web) as
|
|
14
|
+
capabilities, one 12-row descriptor table;
|
|
15
|
+
- the **manifest catalog** — the single ``CAPABILITY_CARDS`` source of truth
|
|
16
|
+
plus the lookups/profile filters derived from it;
|
|
17
|
+
- the **app-novel cards** — the in-house tools the deck builds itself
|
|
18
|
+
(checklist, background-process proxy, delegate/sub-agent, SaaS connector,
|
|
19
|
+
working memory) plus their builders, stores, and injection-handle keys;
|
|
20
|
+
- the **bridge ledger** — content-hash / ULID key minting, the immutable
|
|
21
|
+
:class:`BridgeLedger` with its pure enroll/retire/withdraw transitions and
|
|
22
|
+
live projections, and the side-effecting mount/adapt/detach network
|
|
23
|
+
operations;
|
|
24
|
+
- **deck provisioning** — the single data-driven :func:`provision_deck`
|
|
25
|
+
assembler over the (inversion-preserving) profile table, plus the
|
|
26
|
+
profile-to-cards selection it walks.
|
|
27
|
+
|
|
28
|
+
Consumers import the deck surface from ``induscode.capability_deck`` rather
|
|
29
|
+
than reaching into individual modules.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
from .bridge_ledger import (
|
|
35
|
+
AttachResult,
|
|
36
|
+
BridgeLedger,
|
|
37
|
+
EnrollRequest,
|
|
38
|
+
attach_bridge_capabilities,
|
|
39
|
+
bridge_box_to_capabilities,
|
|
40
|
+
bridge_capability_card,
|
|
41
|
+
bridge_config,
|
|
42
|
+
bridge_content_key,
|
|
43
|
+
bridge_ledger_from_log,
|
|
44
|
+
bridge_ulid_key,
|
|
45
|
+
detach_bridge,
|
|
46
|
+
empty_bridge_ledger,
|
|
47
|
+
enroll_bridge_card,
|
|
48
|
+
live_capabilities,
|
|
49
|
+
live_capabilities_for_server,
|
|
50
|
+
qualify_bridge_name,
|
|
51
|
+
retire,
|
|
52
|
+
withdraw_server,
|
|
53
|
+
)
|
|
54
|
+
from .builtin_bridge import (
|
|
55
|
+
BUILTIN_BRIDGE,
|
|
56
|
+
BUILTIN_IDS,
|
|
57
|
+
BUILTIN_PROFILES,
|
|
58
|
+
BridgeBuilder,
|
|
59
|
+
BuiltinDescriptor,
|
|
60
|
+
build_builtin,
|
|
61
|
+
build_builtins_for_profile,
|
|
62
|
+
builtin_descriptors,
|
|
63
|
+
)
|
|
64
|
+
from .cards import (
|
|
65
|
+
APP_NOVEL_CARDS,
|
|
66
|
+
DELEGATE_HANDLE_KEY,
|
|
67
|
+
DaemonDetails,
|
|
68
|
+
DaemonState,
|
|
69
|
+
DaemonTable,
|
|
70
|
+
DelegateRequest,
|
|
71
|
+
DelegateResult,
|
|
72
|
+
DelegateRunner,
|
|
73
|
+
InMemoryStore,
|
|
74
|
+
MEMORY_HANDLE_KEY,
|
|
75
|
+
MemoryDetails,
|
|
76
|
+
MemoryStore,
|
|
77
|
+
RemoteExecution,
|
|
78
|
+
RemoteToolSummary,
|
|
79
|
+
SAAS_GATEWAY_KEY,
|
|
80
|
+
SaasDetails,
|
|
81
|
+
SaasGatewayPort,
|
|
82
|
+
TaskDetails,
|
|
83
|
+
TodoDetails,
|
|
84
|
+
TodoItem,
|
|
85
|
+
TodoLedger,
|
|
86
|
+
TodoState,
|
|
87
|
+
TodoWeight,
|
|
88
|
+
build_daemon_capability,
|
|
89
|
+
build_memory_capability,
|
|
90
|
+
build_saas_capability,
|
|
91
|
+
build_task_capability,
|
|
92
|
+
build_todo_capability,
|
|
93
|
+
daemon_card,
|
|
94
|
+
memory_card,
|
|
95
|
+
saas_card,
|
|
96
|
+
task_card,
|
|
97
|
+
todo_card,
|
|
98
|
+
)
|
|
99
|
+
from .contract import (
|
|
100
|
+
AgentTool,
|
|
101
|
+
AgentToolResult,
|
|
102
|
+
AnyCapability,
|
|
103
|
+
BridgeEntry,
|
|
104
|
+
BridgeKey,
|
|
105
|
+
BridgeOp,
|
|
106
|
+
Capability,
|
|
107
|
+
CapabilityCard,
|
|
108
|
+
CapabilityId,
|
|
109
|
+
CardProfiles,
|
|
110
|
+
DeckBox,
|
|
111
|
+
DeckContext,
|
|
112
|
+
DeckFault,
|
|
113
|
+
DeckFaultKind,
|
|
114
|
+
DeckFrameworkHandles,
|
|
115
|
+
DeckFsBackend,
|
|
116
|
+
DeckProfile,
|
|
117
|
+
DeckShellBackend,
|
|
118
|
+
LedgerSnapshot,
|
|
119
|
+
Schema,
|
|
120
|
+
ToolBox,
|
|
121
|
+
ToolDeck,
|
|
122
|
+
bridge_key,
|
|
123
|
+
capability_id,
|
|
124
|
+
deck_fault,
|
|
125
|
+
reduce_ledger,
|
|
126
|
+
)
|
|
127
|
+
from .manifest import (
|
|
128
|
+
CAPABILITY_CARDS,
|
|
129
|
+
CAPABILITY_INDEX,
|
|
130
|
+
CARD_PROFILES,
|
|
131
|
+
capability_ids,
|
|
132
|
+
cards_for_profile,
|
|
133
|
+
find_card,
|
|
134
|
+
has_capability,
|
|
135
|
+
)
|
|
136
|
+
from .provision import cards_for_deck_profile, provision_deck
|
|
137
|
+
|
|
138
|
+
__all__ = [
|
|
139
|
+
"APP_NOVEL_CARDS",
|
|
140
|
+
"AgentTool",
|
|
141
|
+
"AgentToolResult",
|
|
142
|
+
"AnyCapability",
|
|
143
|
+
"AttachResult",
|
|
144
|
+
"BUILTIN_BRIDGE",
|
|
145
|
+
"BUILTIN_IDS",
|
|
146
|
+
"BUILTIN_PROFILES",
|
|
147
|
+
"BridgeBuilder",
|
|
148
|
+
"BridgeEntry",
|
|
149
|
+
"BridgeKey",
|
|
150
|
+
"BridgeLedger",
|
|
151
|
+
"BridgeOp",
|
|
152
|
+
"BuiltinDescriptor",
|
|
153
|
+
"CAPABILITY_CARDS",
|
|
154
|
+
"CAPABILITY_INDEX",
|
|
155
|
+
"CARD_PROFILES",
|
|
156
|
+
"Capability",
|
|
157
|
+
"CapabilityCard",
|
|
158
|
+
"CapabilityId",
|
|
159
|
+
"CardProfiles",
|
|
160
|
+
"DELEGATE_HANDLE_KEY",
|
|
161
|
+
"DaemonDetails",
|
|
162
|
+
"DaemonState",
|
|
163
|
+
"DaemonTable",
|
|
164
|
+
"DeckBox",
|
|
165
|
+
"DeckContext",
|
|
166
|
+
"DeckFault",
|
|
167
|
+
"DeckFaultKind",
|
|
168
|
+
"DeckFrameworkHandles",
|
|
169
|
+
"DeckFsBackend",
|
|
170
|
+
"DeckProfile",
|
|
171
|
+
"DeckShellBackend",
|
|
172
|
+
"DelegateRequest",
|
|
173
|
+
"DelegateResult",
|
|
174
|
+
"DelegateRunner",
|
|
175
|
+
"EnrollRequest",
|
|
176
|
+
"InMemoryStore",
|
|
177
|
+
"LedgerSnapshot",
|
|
178
|
+
"MEMORY_HANDLE_KEY",
|
|
179
|
+
"MemoryDetails",
|
|
180
|
+
"MemoryStore",
|
|
181
|
+
"RemoteExecution",
|
|
182
|
+
"RemoteToolSummary",
|
|
183
|
+
"SAAS_GATEWAY_KEY",
|
|
184
|
+
"SaasDetails",
|
|
185
|
+
"SaasGatewayPort",
|
|
186
|
+
"Schema",
|
|
187
|
+
"TaskDetails",
|
|
188
|
+
"TodoDetails",
|
|
189
|
+
"TodoItem",
|
|
190
|
+
"TodoLedger",
|
|
191
|
+
"TodoState",
|
|
192
|
+
"TodoWeight",
|
|
193
|
+
"ToolBox",
|
|
194
|
+
"ToolDeck",
|
|
195
|
+
"attach_bridge_capabilities",
|
|
196
|
+
"bridge_box_to_capabilities",
|
|
197
|
+
"bridge_capability_card",
|
|
198
|
+
"bridge_config",
|
|
199
|
+
"bridge_content_key",
|
|
200
|
+
"bridge_key",
|
|
201
|
+
"bridge_ledger_from_log",
|
|
202
|
+
"bridge_ulid_key",
|
|
203
|
+
"build_builtin",
|
|
204
|
+
"build_builtins_for_profile",
|
|
205
|
+
"build_daemon_capability",
|
|
206
|
+
"build_memory_capability",
|
|
207
|
+
"build_saas_capability",
|
|
208
|
+
"build_task_capability",
|
|
209
|
+
"build_todo_capability",
|
|
210
|
+
"builtin_descriptors",
|
|
211
|
+
"capability_id",
|
|
212
|
+
"capability_ids",
|
|
213
|
+
"cards_for_deck_profile",
|
|
214
|
+
"cards_for_profile",
|
|
215
|
+
"daemon_card",
|
|
216
|
+
"deck_fault",
|
|
217
|
+
"detach_bridge",
|
|
218
|
+
"empty_bridge_ledger",
|
|
219
|
+
"enroll_bridge_card",
|
|
220
|
+
"find_card",
|
|
221
|
+
"has_capability",
|
|
222
|
+
"live_capabilities",
|
|
223
|
+
"live_capabilities_for_server",
|
|
224
|
+
"memory_card",
|
|
225
|
+
"provision_deck",
|
|
226
|
+
"qualify_bridge_name",
|
|
227
|
+
"reduce_ledger",
|
|
228
|
+
"retire",
|
|
229
|
+
"saas_card",
|
|
230
|
+
"task_card",
|
|
231
|
+
"todo_card",
|
|
232
|
+
"withdraw_server",
|
|
233
|
+
]
|