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.
Files changed (167) hide show
  1. induscode/__init__.py +56 -0
  2. induscode/addons/__init__.py +176 -0
  3. induscode/addons/contract.py +923 -0
  4. induscode/addons/dispatch/__init__.py +43 -0
  5. induscode/addons/dispatch/event_dispatcher.py +348 -0
  6. induscode/addons/dispatch/tool_interceptor.py +349 -0
  7. induscode/addons/host.py +469 -0
  8. induscode/addons/loader.py +314 -0
  9. induscode/addons/manifest.py +232 -0
  10. induscode/addons/surface.py +199 -0
  11. induscode/boot/__init__.py +108 -0
  12. induscode/boot/auth_vault.py +323 -0
  13. induscode/boot/boot.py +210 -0
  14. induscode/boot/contract.py +223 -0
  15. induscode/boot/invocation.py +117 -0
  16. induscode/boot/runners/__init__.py +42 -0
  17. induscode/boot/runners/link_runner.py +82 -0
  18. induscode/boot/runners/oneshot_runner.py +85 -0
  19. induscode/boot/runners/registry.py +46 -0
  20. induscode/boot/runners/repl_runner.py +340 -0
  21. induscode/boot/runners/session.py +549 -0
  22. induscode/boot/stages.py +198 -0
  23. induscode/boot/upgrade/__init__.py +36 -0
  24. induscode/boot/upgrade/apply.py +125 -0
  25. induscode/boot/upgrade/upgrades.py +136 -0
  26. induscode/briefing/__init__.py +115 -0
  27. induscode/briefing/compose.py +414 -0
  28. induscode/briefing/contract.py +528 -0
  29. induscode/briefing/macros.py +721 -0
  30. induscode/briefing/skills.py +417 -0
  31. induscode/capability_deck/__init__.py +233 -0
  32. induscode/capability_deck/bridge_ledger/__init__.py +66 -0
  33. induscode/capability_deck/bridge_ledger/key.py +181 -0
  34. induscode/capability_deck/bridge_ledger/ledger.py +276 -0
  35. induscode/capability_deck/bridge_ledger/network.py +336 -0
  36. induscode/capability_deck/builtin_bridge.py +358 -0
  37. induscode/capability_deck/cards/__init__.py +116 -0
  38. induscode/capability_deck/cards/bg_process.py +482 -0
  39. induscode/capability_deck/cards/memory.py +226 -0
  40. induscode/capability_deck/cards/saas.py +280 -0
  41. induscode/capability_deck/cards/task.py +256 -0
  42. induscode/capability_deck/cards/todo.py +312 -0
  43. induscode/capability_deck/contract.py +450 -0
  44. induscode/capability_deck/manifest.py +126 -0
  45. induscode/capability_deck/provision.py +217 -0
  46. induscode/channels/__init__.py +146 -0
  47. induscode/channels/contract.py +585 -0
  48. induscode/channels/framer.py +132 -0
  49. induscode/channels/link/__init__.py +50 -0
  50. induscode/channels/link/dialog.py +246 -0
  51. induscode/channels/link/driver.py +308 -0
  52. induscode/channels/link/server.py +217 -0
  53. induscode/channels/oneshot.py +178 -0
  54. induscode/channels/ops.py +140 -0
  55. induscode/channels/session_ops.py +172 -0
  56. induscode/conductor/__init__.py +240 -0
  57. induscode/conductor/catalog.py +309 -0
  58. induscode/conductor/conductor.py +1084 -0
  59. induscode/conductor/contract.py +1035 -0
  60. induscode/conductor/matcher.py +291 -0
  61. induscode/conductor/serialize.py +575 -0
  62. induscode/conductor/signal_hub.py +382 -0
  63. induscode/conductor/skill_parse.py +294 -0
  64. induscode/conductor/transcript_store.py +449 -0
  65. induscode/console/__init__.py +236 -0
  66. induscode/console/app.py +1677 -0
  67. induscode/console/components/__init__.py +62 -0
  68. induscode/console/components/banner.py +499 -0
  69. induscode/console/components/banner_sweep.py +188 -0
  70. induscode/console/components/emblem.py +181 -0
  71. induscode/console/components/status_bar.py +102 -0
  72. induscode/console/contract.py +836 -0
  73. induscode/console/input/__init__.py +107 -0
  74. induscode/console/input/chord.py +197 -0
  75. induscode/console/input/dir_reader.py +113 -0
  76. induscode/console/input/intents.py +258 -0
  77. induscode/console/input/providers.py +469 -0
  78. induscode/console/mount.py +137 -0
  79. induscode/console/overlays/__init__.py +94 -0
  80. induscode/console/overlays/auth.py +503 -0
  81. induscode/console/overlays/pickers.py +526 -0
  82. induscode/console/overlays/router.py +129 -0
  83. induscode/console/overlays/sessions.py +232 -0
  84. induscode/console/reducer.py +145 -0
  85. induscode/console/resume_picker.py +156 -0
  86. induscode/console/slash_commands/__init__.py +78 -0
  87. induscode/console/slash_commands/builtins.py +254 -0
  88. induscode/console/slash_commands/dynamic.py +217 -0
  89. induscode/console/slash_commands/integrations.py +949 -0
  90. induscode/console/slash_commands/transcript.py +404 -0
  91. induscode/console/slash_commands/workbench.py +430 -0
  92. induscode/console/startup.py +434 -0
  93. induscode/console/theme/__init__.py +44 -0
  94. induscode/console/theme/adapter.py +168 -0
  95. induscode/console/theme/palette.py +128 -0
  96. induscode/console/theme/resolve.py +123 -0
  97. induscode/console/theme/tokens.py +185 -0
  98. induscode/console_slash/__init__.py +111 -0
  99. induscode/console_slash/contract.py +185 -0
  100. induscode/console_slash/registry.py +140 -0
  101. induscode/console_slash/resolve.py +194 -0
  102. induscode/console_slash/shared.py +172 -0
  103. induscode/entry.py +108 -0
  104. induscode/insight/__init__.py +153 -0
  105. induscode/insight/collector.py +73 -0
  106. induscode/insight/replay.py +305 -0
  107. induscode/insight/wrapper.py +1115 -0
  108. induscode/kit/__init__.py +82 -0
  109. induscode/kit/clipboard_image.py +215 -0
  110. induscode/kit/external_editor.py +120 -0
  111. induscode/kit/image.py +188 -0
  112. induscode/kit/shell.py +89 -0
  113. induscode/kit/tool_fetch.py +288 -0
  114. induscode/launch/__init__.py +224 -0
  115. induscode/launch/catalog.py +310 -0
  116. induscode/launch/contract.py +569 -0
  117. induscode/launch/credentials.py +852 -0
  118. induscode/launch/invocation/__init__.py +39 -0
  119. induscode/launch/invocation/attachments.py +281 -0
  120. induscode/launch/invocation/flags.py +210 -0
  121. induscode/launch/invocation/read.py +369 -0
  122. induscode/launch/invocation/usage.py +110 -0
  123. induscode/launch/oauth.py +808 -0
  124. induscode/launch/packages.py +299 -0
  125. induscode/launch/pickers.py +291 -0
  126. induscode/py.typed +0 -0
  127. induscode/runtime_bridge/__init__.py +166 -0
  128. induscode/runtime_bridge/bridges/__init__.py +66 -0
  129. induscode/runtime_bridge/bridges/_drive.py +268 -0
  130. induscode/runtime_bridge/bridges/builtins.py +177 -0
  131. induscode/runtime_bridge/bridges/claude_cli.py +198 -0
  132. induscode/runtime_bridge/bridges/codex_cli.py +203 -0
  133. induscode/runtime_bridge/bridges/indusagi_cli.py +217 -0
  134. induscode/runtime_bridge/broker.py +397 -0
  135. induscode/runtime_bridge/contract.py +734 -0
  136. induscode/runtime_bridge/sink.py +351 -0
  137. induscode/sessions/__init__.py +25 -0
  138. induscode/sessions/contract.py +119 -0
  139. induscode/sessions/library.py +350 -0
  140. induscode/settings/__init__.py +47 -0
  141. induscode/settings/contract.py +313 -0
  142. induscode/settings/manager.py +268 -0
  143. induscode/transcript_export/__init__.py +109 -0
  144. induscode/transcript_export/contract.py +522 -0
  145. induscode/transcript_export/publish.py +455 -0
  146. induscode/transcript_export/sgr.py +566 -0
  147. induscode/transcript_export/template.py +319 -0
  148. induscode/transcript_export/theme_bridge.py +325 -0
  149. induscode/window_budget/__init__.py +76 -0
  150. induscode/window_budget/budget/__init__.py +26 -0
  151. induscode/window_budget/budget/estimate.py +273 -0
  152. induscode/window_budget/budget/gate.py +60 -0
  153. induscode/window_budget/budget/slice.py +145 -0
  154. induscode/window_budget/condenser.py +170 -0
  155. induscode/window_budget/contract.py +329 -0
  156. induscode/window_budget/summarize/__init__.py +33 -0
  157. induscode/window_budget/summarize/condense.py +212 -0
  158. induscode/window_budget/summarize/prompt.py +241 -0
  159. induscode/workspace/__init__.py +30 -0
  160. induscode/workspace/brand.py +96 -0
  161. induscode/workspace/locator.py +269 -0
  162. induscode-0.1.0.dist-info/METADATA +97 -0
  163. induscode-0.1.0.dist-info/RECORD +167 -0
  164. induscode-0.1.0.dist-info/WHEEL +4 -0
  165. induscode-0.1.0.dist-info/entry_points.txt +3 -0
  166. induscode-0.1.0.dist-info/licenses/CREDITS.md +22 -0
  167. 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
+ ]