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,528 @@
1
+ """Briefing contract — the FROZEN type surface of the prompt layer.
2
+
3
+ This module is the single typed seam between the coding-agent's runtime state
4
+ and the LLM-facing **briefing** (the system prompt). It declares *only* shapes
5
+ plus a handful of tiny inert helpers — no filesystem walks, no string
6
+ assembly — so every later module (the section pipeline, the macro scanner, and
7
+ the capability-card loader) is written against the names declared here. The
8
+ file is intentionally small, append-mostly, and stable.
9
+
10
+ Design stance (ported from TS ``src/briefing/contract.ts``):
11
+
12
+ - The briefing is **declarative, not a string template**. A :data:`Briefing`
13
+ is an ordered list of :class:`BriefingSection` descriptors, each a small
14
+ record that decides whether it applies to a :class:`BriefingContext` and
15
+ renders its own fragment. :func:`~induscode.briefing.compose.compose_briefing`
16
+ folds the enabled sections into the final prompt; there is no mega-literal
17
+ with ``{{TOKEN}}`` placeholders.
18
+ - Macros are a **single-pass ``$arg`` model**. A :class:`Macro` carries a body
19
+ and a source label; expansion is one left-to-right scan over the body that
20
+ resolves ``$1`` / ``$@`` / ``$ARGUMENTS`` / ``${@:N:L}`` against a
21
+ :class:`MacroScope`. The contract names the token kinds and the scope; it
22
+ does not run regexes.
23
+ - A :class:`SkillCard` is a CapabilityCard parsed from a ``SKILL.md`` document.
24
+ The Agent-Skills *format* (frontmatter keys, name/description limits) is a
25
+ public spec and is kept; the validation prose and field policy are the
26
+ briefing's own.
27
+
28
+ Port note — transcript-export types relocated
29
+ ---------------------------------------------
30
+ The TS ``briefing/contract.ts`` additionally hosted the shared types of the
31
+ HTML transcript exporter: the SGR machine (``SgrState`` / ``SgrToken`` /
32
+ ``SgrMutation`` / ``SGR_INITIAL_STATE``), the export palette (``ExportTheme``
33
+ / ``FALLBACK_EXPORT_THEME`` / ``Rgb`` / ``LuminanceLut`` / ``ThemeMode`` /
34
+ ``ThemeBridge``), and the publish surface (``TranscriptPart`` /
35
+ ``WidgetRender`` / ``PublishOptions`` / ``SHELL_SLOTS`` / ``ShellSlot``).
36
+ In the Python build those types are owned by
37
+ ``induscode.transcript_export.contract`` (ported separately) — this module
38
+ holds only the briefing-owned vocabulary: sections, macros, skills,
39
+ :class:`ContextDoc`, and :class:`BriefingFault` (which transcript-export
40
+ imports from here, fault kinds ``publish`` / ``theme`` included, so the closed
41
+ fault set stays in one place).
42
+
43
+ Framework anchors (all from the ``indusagi`` package — the sibling rebuilt
44
+ framework this app targets):
45
+
46
+ - :class:`AgentState`, :class:`AgentTool` ← ``indusagi.agent``
47
+ - :class:`TextContent`, :class:`ImageContent` ← ``indusagi.ai``
48
+
49
+ The briefing never re-declares these; it composes them.
50
+ """
51
+
52
+ from __future__ import annotations
53
+
54
+ from collections.abc import Callable, Mapping, Sequence
55
+ from dataclasses import dataclass
56
+ from datetime import datetime
57
+ from typing import ClassVar, Final, Literal, TypeAlias
58
+
59
+ from indusagi.agent import AgentState, AgentTool
60
+ from indusagi.ai import ImageContent, TextContent
61
+
62
+ __all__ = [
63
+ "AgentState",
64
+ "AgentTool",
65
+ "AllToken",
66
+ "Briefing",
67
+ "BriefingContext",
68
+ "BriefingFault",
69
+ "BriefingFaultKind",
70
+ "BriefingInputs",
71
+ "BriefingSection",
72
+ "ContextDoc",
73
+ "ImageContent",
74
+ "LiteralToken",
75
+ "Macro",
76
+ "MacroOrigin",
77
+ "MacroScope",
78
+ "MacroToken",
79
+ "MacroTokenKind",
80
+ "PositionalToken",
81
+ "SKILL_DESCRIPTION_LIMIT",
82
+ "SKILL_NAME_LIMIT",
83
+ "SkillCard",
84
+ "SkillDiagnostic",
85
+ "SkillFrontmatter",
86
+ "SkillLoad",
87
+ "SkillOutcomeKind",
88
+ "SliceToken",
89
+ "SubagentBrief",
90
+ "TextContent",
91
+ "briefing_fault",
92
+ ]
93
+
94
+
95
+ # ---------------------------------------------------------------------------
96
+ # Briefing sections (declarative system-prompt pipeline)
97
+ # ---------------------------------------------------------------------------
98
+
99
+
100
+ @dataclass(frozen=True, slots=True, kw_only=True)
101
+ class SubagentBrief:
102
+ """A named delegate role advertised in the subagent section of the briefing.
103
+
104
+ The minimum a section needs to list a delegate the primary agent may hand
105
+ work to: a stable invocation ``name``, a one-line ``purpose``, and an
106
+ optional ``when`` hint describing the situations the role is suited to.
107
+ """
108
+
109
+ # Stable name the primary agent uses to delegate to this role.
110
+ name: str
111
+ # One-line description of what the role is for.
112
+ purpose: str
113
+ # Optional hint describing when delegating to this role is appropriate.
114
+ when: str | None = None
115
+
116
+
117
+ @dataclass(frozen=True, slots=True, kw_only=True)
118
+ class ContextDoc:
119
+ """A project-context document inlined into the briefing.
120
+
121
+ Project files (AGENTS-style guidance) are surfaced under their own section
122
+ so the model can read repository conventions. ``path`` labels the source;
123
+ ``body`` is the verbatim text to inline.
124
+ """
125
+
126
+ # Source path of the document, used as its heading label.
127
+ path: str
128
+ # Verbatim document text to inline into the briefing.
129
+ body: str
130
+
131
+
132
+ @dataclass(frozen=True, slots=True, kw_only=True)
133
+ class BriefingContext:
134
+ """The working context a :class:`BriefingSection` reads when it decides
135
+ whether to apply and what to render.
136
+
137
+ This is the data-driven replacement for the old string-template's
138
+ placeholder bag. Every field is optional so a section that does not need a
139
+ given input simply ignores it, and a caller can compose a partial briefing
140
+ (e.g. one with no skills, or no project docs) without populating the whole
141
+ shape. A section is pure with respect to this context: it reads, it never
142
+ mutates.
143
+
144
+ - ``workspace`` — absolute working directory the session is scoped to; the
145
+ footer section surfaces it and skills resolve relative paths against it.
146
+ - ``tools`` — the capabilities advertised this turn; the tools/guidelines
147
+ sections gate themselves on which ids are present.
148
+ - ``cwd`` — alias kept distinct from ``workspace`` for sections that want
149
+ the *display* path (may be relativized) versus the absolute root.
150
+ - ``skills`` — the :class:`SkillCard` records eligible for model
151
+ invocation; the skills section renders these into its block.
152
+ - ``subagents`` — the named delegate roles to advertise in the delegate
153
+ block.
154
+ - ``context_docs`` — project context documents (AGENTS-style files) to
155
+ inline.
156
+ - ``now`` — the timestamp the footer stamps; injectable for determinism.
157
+ - ``extras`` — an open bag for app-novel sections to read bespoke inputs
158
+ without widening this contract on every addition.
159
+ """
160
+
161
+ # Absolute working directory the session's briefing is scoped to.
162
+ workspace: str | None = None
163
+ # The capabilities advertised this turn, by their wire-facing descriptors.
164
+ tools: Sequence[AgentTool] | None = None
165
+ # Display path for the working directory (may be relativized).
166
+ cwd: str | None = None
167
+ # Capability cards eligible for model invocation, for the skills block.
168
+ skills: Sequence[SkillCard] | None = None
169
+ # Named delegate roles to advertise in the subagent/delegate section.
170
+ subagents: Sequence[SubagentBrief] | None = None
171
+ # Project context documents to inline under a project-context section.
172
+ context_docs: Sequence[ContextDoc] | None = None
173
+ # Timestamp the footer stamps; injectable so renders are deterministic.
174
+ now: datetime | None = None
175
+ # Open bag for app-novel sections to read bespoke inputs.
176
+ extras: Mapping[str, object] | None = None
177
+
178
+
179
+ @dataclass(frozen=True, slots=True, kw_only=True)
180
+ class BriefingSection:
181
+ """One declarative section of the briefing.
182
+
183
+ The unit of the data-driven pipeline that replaces the old string
184
+ template: an ordered array of these is composed by
185
+ :func:`~induscode.briefing.compose.compose_briefing`, which renders each
186
+ applicable section and joins the fragments. A section owns its own gating
187
+ — via the optional :attr:`applies` predicate — and its own rendering, so
188
+ adding a section is adding one descriptor to the array, not editing a
189
+ central literal.
190
+
191
+ - ``id`` — stable identifier for ordering, dedup, and selective inclusion.
192
+ - ``title`` — optional human-facing heading the section may emit.
193
+ - ``applies`` — optional predicate deciding whether this section
194
+ contributes to a given context; when ``None`` the section is treated as
195
+ always applicable.
196
+ - ``render`` — produce the section's fragment for a context. Pure: reads
197
+ the context, returns a string, performs no I/O and mutates nothing.
198
+ Returning an empty string is allowed and is dropped from the composed
199
+ output.
200
+ """
201
+
202
+ # Stable identifier for ordering, dedup, and selective inclusion.
203
+ id: str
204
+ # Optional human-facing heading the section may emit.
205
+ title: str | None = None
206
+ # Optional predicate; when None the section always renders.
207
+ applies: Callable[[BriefingContext], bool] | None = None
208
+ # Render this section's fragment for a context (pure).
209
+ render: Callable[[BriefingContext], str]
210
+
211
+
212
+ #: An ordered briefing recipe: the sections, in render order.
213
+ #:
214
+ #: ``compose_briefing`` walks this list, skips sections whose
215
+ #: :attr:`BriefingSection.applies` returns ``False``, renders the rest, and
216
+ #: joins the non-empty fragments. The recipe is data; swapping the section set
217
+ #: (or its order) reconfigures the whole prompt without touching the composer.
218
+ Briefing: TypeAlias = Sequence[BriefingSection]
219
+
220
+
221
+ @dataclass(frozen=True, slots=True, kw_only=True)
222
+ class BriefingInputs:
223
+ """Inputs handed to ``compose_briefing`` to assemble a full briefing string.
224
+
225
+ Pairs the :data:`Briefing` recipe with the :class:`BriefingContext` to
226
+ render it against, plus two override hooks that mirror the legacy
227
+ custom-prompt path: ``prelude`` text prepended ahead of the sections, and
228
+ ``append`` text added after them. Both are optional; the common case
229
+ passes only ``sections`` and ``context``.
230
+ """
231
+
232
+ # The ordered section recipe to render.
233
+ sections: Briefing
234
+ # The context the sections render against.
235
+ context: BriefingContext
236
+ # Optional text prepended ahead of the rendered sections.
237
+ prelude: str | None = None
238
+ # Optional text appended after the rendered sections.
239
+ append: str | None = None
240
+
241
+
242
+ # ---------------------------------------------------------------------------
243
+ # Macros (single-pass $arg model)
244
+ # ---------------------------------------------------------------------------
245
+
246
+ #: Where a :class:`Macro` was discovered, for labelling and precedence.
247
+ #:
248
+ #: - ``user`` — the per-user macro directory.
249
+ #: - ``project`` — the project-local macro directory.
250
+ #: - ``path`` — an explicitly supplied path outside the standard roots.
251
+ #: - ``builtin`` — a macro shipped with the app.
252
+ MacroOrigin: TypeAlias = Literal["user", "project", "path", "builtin"]
253
+
254
+
255
+ @dataclass(frozen=True, slots=True, kw_only=True)
256
+ class Macro:
257
+ """A user-defined slash macro: a named prompt body with ``$arg`` placeholders.
258
+
259
+ Discovered from ``*.md`` files (frontmatter + body) under the
260
+ user/project/path roots. Invoking ``/<name> args…`` resolves the body
261
+ against a :class:`MacroScope` built from the args, substituting the
262
+ ``$arg`` tokens in one pass. The field names are the briefing's own; the
263
+ placeholder *syntax* is the shared cross-tool convention and is preserved.
264
+
265
+ - ``name`` — the invocation name (without the leading slash).
266
+ - ``description`` — one-line summary for command listings; derived from
267
+ frontmatter or the body's opening, with an ``origin``-derived label.
268
+ - ``body`` — the prompt text containing the ``$arg`` placeholders.
269
+ - ``origin`` — where the macro was found, for labelling and precedence.
270
+ - ``source`` — absolute path of the file the macro was loaded from.
271
+ """
272
+
273
+ # Invocation name, without the leading slash.
274
+ name: str
275
+ # One-line description for command listings.
276
+ description: str
277
+ # Prompt body containing `$arg` placeholders.
278
+ body: str
279
+ # Where the macro was discovered.
280
+ origin: MacroOrigin
281
+ # Absolute path of the file the macro was loaded from.
282
+ source: str
283
+
284
+
285
+ @dataclass(frozen=True, slots=True, kw_only=True)
286
+ class MacroScope:
287
+ """The argument environment a :class:`Macro` body is resolved against.
288
+
289
+ Built from a parsed invocation: ``args`` is the positional vector (1-based
290
+ in the ``$N`` syntax, 0-based in this tuple), ``all`` is the original
291
+ joined argument string the ``$@`` / ``$ARGUMENTS`` tokens expand to, and
292
+ ``raw`` is the untouched text after the command name (kept for diagnostics
293
+ and for tokens that want the pre-split form). A token references this
294
+ scope; the scanner reads it.
295
+ """
296
+
297
+ # Positional arguments, in order; `$1` maps to index 0.
298
+ args: tuple[str, ...]
299
+ # The joined argument string `$@` / `$ARGUMENTS` expand to.
300
+ all: str
301
+ # The verbatim text following the command name, before splitting.
302
+ raw: str
303
+
304
+
305
+ #: The kinds of unit the single-pass ``$arg`` scanner emits.
306
+ #:
307
+ #: - ``literal`` — a run of ordinary text copied through unchanged.
308
+ #: - ``positional`` — a ``$N`` reference to one positional argument.
309
+ #: - ``all`` — a ``$@`` or ``$ARGUMENTS`` reference to the joined arguments.
310
+ #: - ``slice`` — a ``${@:N}`` / ``${@:N:L}`` reference to an argument
311
+ #: sub-range.
312
+ MacroTokenKind: TypeAlias = Literal["literal", "positional", "all", "slice"]
313
+
314
+
315
+ @dataclass(frozen=True, slots=True)
316
+ class LiteralToken:
317
+ """A run of ordinary text copied through unchanged."""
318
+
319
+ kind: ClassVar[Literal["literal"]] = "literal"
320
+ text: str
321
+
322
+
323
+ @dataclass(frozen=True, slots=True)
324
+ class PositionalToken:
325
+ """A ``$N`` reference carrying the 1-based ``index`` of its argument."""
326
+
327
+ kind: ClassVar[Literal["positional"]] = "positional"
328
+ index: int
329
+
330
+
331
+ @dataclass(frozen=True, slots=True)
332
+ class AllToken:
333
+ """A ``$@`` / ``$ARGUMENTS`` reference; carries nothing beyond its kind."""
334
+
335
+ kind: ClassVar[Literal["all"]] = "all"
336
+
337
+
338
+ @dataclass(frozen=True, slots=True)
339
+ class SliceToken:
340
+ """A ``${@:N}`` / ``${@:N:L}`` reference carrying ``start`` and an
341
+ optional ``length``."""
342
+
343
+ kind: ClassVar[Literal["slice"]] = "slice"
344
+ start: int
345
+ length: int | None = None
346
+
347
+
348
+ #: One unit produced by the single-pass macro scanner.
349
+ #:
350
+ #: The body is scanned left-to-right into a flat token stream; resolving the
351
+ #: stream against a :class:`MacroScope` and concatenating yields the expanded
352
+ #: text. This replaces the legacy multi-pass regex substitution with one scan
353
+ #: emitting these discriminated tokens.
354
+ MacroToken: TypeAlias = LiteralToken | PositionalToken | AllToken | SliceToken
355
+
356
+
357
+ # ---------------------------------------------------------------------------
358
+ # Skill cards (CapabilityCard loaded from a SKILL.md)
359
+ # ---------------------------------------------------------------------------
360
+
361
+
362
+ @dataclass(frozen=True, slots=True, kw_only=True)
363
+ class SkillFrontmatter:
364
+ """The parsed frontmatter of a ``SKILL.md`` document.
365
+
366
+ The Agent-Skills format is a public spec, so these keys are kept; the
367
+ policy over them (which are required, which are allowed) is the briefing's
368
+ own. Only ``name`` and ``description`` carry semantics for the prompt; the
369
+ rest are metadata the loader preserves but does not interpret.
370
+
371
+ - ``name`` — the skill's invocation name; must match its directory and use
372
+ lowercase hyphenated segments.
373
+ - ``description`` — required, single-line summary the model reads to
374
+ decide when the skill applies.
375
+ - ``license`` — optional SPDX-style license tag.
376
+ - ``compatibility`` — optional free-form compatibility note.
377
+ - ``metadata`` — optional open key/value bag.
378
+ - ``allowed_tools`` — optional restriction on which tools the skill may
379
+ use while active (the format's kebab-case ``allowed-tools`` key).
380
+ - ``disable_model_invocation`` — when true the skill is hidden from the
381
+ briefing and exposed only as an explicit ``/skill:<name>`` command (the
382
+ format's ``disable-model-invocation`` key).
383
+ """
384
+
385
+ # Invocation name; must match the parent directory, lowercase-hyphenated.
386
+ name: str | None = None
387
+ # Required single-line summary the model reads to gate the skill.
388
+ description: str | None = None
389
+ # Optional SPDX-style license tag.
390
+ license: str | None = None
391
+ # Optional free-form compatibility note.
392
+ compatibility: str | None = None
393
+ # Optional open metadata bag.
394
+ metadata: Mapping[str, object] | None = None
395
+ # Optional restriction on tools the skill may use while active.
396
+ allowed_tools: tuple[str, ...] | None = None
397
+ # Hide from the briefing; expose only as an explicit command.
398
+ disable_model_invocation: bool | None = None
399
+
400
+
401
+ @dataclass(frozen=True, slots=True, kw_only=True)
402
+ class SkillCard:
403
+ """A capability card loaded from a ``SKILL.md`` document.
404
+
405
+ The resolved, validated form of one skill: its ``name``, the
406
+ ``description`` the model reads, the markdown ``body`` (instructions
407
+ loaded on demand), and the on-disk ``location`` of the file. ``origin``
408
+ records where it was discovered and ``frontmatter`` retains the parsed
409
+ header for introspection. Unlike a deck capability, a skill card is
410
+ *documentation the model reads*, not a callable — the model loads
411
+ ``location`` with the reader when a task matches ``description``.
412
+ """
413
+
414
+ # Validated invocation name.
415
+ name: str
416
+ # Single-line summary surfaced in the briefing.
417
+ description: str
418
+ # Markdown instruction text below the frontmatter.
419
+ body: str
420
+ # Absolute path of the `SKILL.md` file.
421
+ location: str
422
+ # Where the card was discovered.
423
+ origin: MacroOrigin
424
+ # Parsed frontmatter, retained for introspection.
425
+ frontmatter: SkillFrontmatter
426
+
427
+
428
+ #: Outcome categories the skill loader can attach to a candidate file.
429
+ #:
430
+ #: - ``loaded`` — a valid card was produced.
431
+ #: - ``skipped`` — the file was ignored by policy (e.g. not a ``SKILL.md``).
432
+ #: - ``invalid`` — the file was a skill but failed validation.
433
+ #: - ``collision`` — a card with the same name already won; this one was
434
+ #: dropped.
435
+ SkillOutcomeKind: TypeAlias = Literal["loaded", "skipped", "invalid", "collision"]
436
+
437
+
438
+ @dataclass(frozen=True, slots=True, kw_only=True)
439
+ class SkillDiagnostic:
440
+ """A single diagnostic the skill loader emits for one candidate file.
441
+
442
+ Replaces the legacy parallel ``{skills, diagnostics}`` arrays with one
443
+ tagged record per candidate: ``kind`` is the outcome, ``location`` the
444
+ file it concerns, and ``detail`` a human-readable explanation written in
445
+ the briefing's own voice.
446
+ """
447
+
448
+ # Outcome category for the candidate.
449
+ kind: SkillOutcomeKind
450
+ # Absolute path of the candidate file the diagnostic concerns.
451
+ location: str
452
+ # Human-readable explanation of the outcome.
453
+ detail: str
454
+
455
+
456
+ @dataclass(frozen=True, slots=True, kw_only=True)
457
+ class SkillLoad:
458
+ """The aggregate result of loading skills from a set of roots.
459
+
460
+ ``cards`` are the validated, deduped cards (first writer wins per name);
461
+ ``diagnostics`` is the flat stream of per-candidate outcomes for surfacing
462
+ in a load report. A consumer renders ``cards`` into the briefing and may
463
+ show ``diagnostics`` to the user.
464
+ """
465
+
466
+ # Validated, deduped cards in discovery order.
467
+ cards: tuple[SkillCard, ...]
468
+ # Per-candidate outcomes accumulated during the load.
469
+ diagnostics: tuple[SkillDiagnostic, ...]
470
+
471
+
472
+ #: Maximum length of a validated skill name, per the Agent-Skills format.
473
+ SKILL_NAME_LIMIT: Final[int] = 64
474
+
475
+ #: Maximum length of a validated skill description, per the format.
476
+ SKILL_DESCRIPTION_LIMIT: Final[int] = 1024
477
+
478
+
479
+ # ---------------------------------------------------------------------------
480
+ # Faults
481
+ # ---------------------------------------------------------------------------
482
+
483
+ #: The closed set of failure categories this layer can surface.
484
+ #:
485
+ #: - ``skill_invalid`` — a ``SKILL.md`` failed format validation.
486
+ #: - ``macro_invalid`` — a macro file could not be parsed.
487
+ #: - ``publish`` — building or writing the HTML transcript failed (raised by
488
+ #: ``induscode.transcript_export``, which shares this fault type).
489
+ #: - ``theme`` — a theme color could not be parsed or derived (ditto).
490
+ BriefingFaultKind: TypeAlias = Literal["skill_invalid", "macro_invalid", "publish", "theme"]
491
+
492
+
493
+ class BriefingFault(Exception):
494
+ """A typed, discriminated failure raised by the briefing/transcript layer.
495
+
496
+ ``kind`` selects the category, ``message`` is a human-readable summary,
497
+ and the optional ``cause`` carries the underlying error for logging
498
+ without forcing consumers to parse the message. Construct one with
499
+ :func:`briefing_fault`.
500
+
501
+ Port note: the TS shape was a plain frozen record ``throw``-n as a value;
502
+ Python requires raisables to be exceptions, so the same three fields ride
503
+ on an :class:`Exception` subclass.
504
+ """
505
+
506
+ def __init__(
507
+ self, kind: BriefingFaultKind, message: str, cause: object | None = None
508
+ ) -> None:
509
+ super().__init__(message)
510
+ # Failure category — the discriminant consumers switch on.
511
+ self.kind: BriefingFaultKind = kind
512
+ # Human-readable, single-line summary of what went wrong.
513
+ self.message: str = message
514
+ # Underlying error or structured detail, if any.
515
+ self.cause: object | None = cause
516
+
517
+
518
+ def briefing_fault(
519
+ kind: BriefingFaultKind, message: str, cause: object | None = None
520
+ ) -> BriefingFault:
521
+ """Construct a :class:`BriefingFault`. The single sanctioned way to mint
522
+ one, so the shape stays uniform across every producer.
523
+
524
+ :param kind: the failure category
525
+ :param message: a human-readable, single-line summary
526
+ :param cause: optional underlying error or structured detail
527
+ """
528
+ return BriefingFault(kind, message, cause)