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,414 @@
1
+ """Briefing composer — the declarative system-prompt pipeline.
2
+
3
+ The system prompt is assembled from an ordered list of
4
+ :class:`~induscode.briefing.contract.BriefingSection` descriptors, not a string
5
+ template with ``{{TOKEN}}`` holes. Each section decides for itself whether it
6
+ contributes to a given :class:`~induscode.briefing.contract.BriefingContext`
7
+ (its ``applies`` predicate) and renders its own fragment (its ``render``). The
8
+ composer is a small reducer: it walks the recipe in order, keeps the
9
+ applicable sections, renders them, drops empties, and joins with blank-line
10
+ gaps. Adding, removing, or reordering a section is a data edit to
11
+ :data:`BRIEFING_SECTIONS`, never a change to the composer.
12
+
13
+ Every guideline, tool note, and heading below is authored for this rebuild
14
+ (the prose is copied verbatim from the TS ``src/briefing/compose.ts``). The
15
+ *set* of sections (role, tools, working guidance, task tracking, delegates,
16
+ connectors, project context, skills, footer) follows the well-known coding-
17
+ agent shape; the wording is the briefing's own.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from collections.abc import Sequence
23
+ from datetime import datetime, timezone
24
+ from typing import Final, Mapping
25
+
26
+ from .contract import (
27
+ AgentTool,
28
+ Briefing,
29
+ BriefingContext,
30
+ BriefingInputs,
31
+ BriefingSection,
32
+ )
33
+
34
+ __all__ = [
35
+ "BRIEFING_SECTIONS",
36
+ "compose_briefing",
37
+ ]
38
+
39
+
40
+ # ---------------------------------------------------------------------------
41
+ # Small render helpers
42
+ # ---------------------------------------------------------------------------
43
+
44
+
45
+ def _join_blocks(parts: Sequence[str]) -> str:
46
+ """Join non-empty fragments with a blank line between them."""
47
+ return "\n\n".join(p for p in parts if p.strip())
48
+
49
+
50
+ def _bullets(lines: Sequence[str]) -> str:
51
+ """Render a markdown bullet list from already-formatted line bodies."""
52
+ return "\n".join(f"- {line}" for line in lines)
53
+
54
+
55
+ def _escape_xml(value: str) -> str:
56
+ """Escape the five XML metacharacters for the skills block's tag values."""
57
+ out = ""
58
+ for ch in value:
59
+ if ch == "&":
60
+ out += "&"
61
+ elif ch == "<":
62
+ out += "&lt;"
63
+ elif ch == ">":
64
+ out += "&gt;"
65
+ elif ch == '"':
66
+ out += "&quot;"
67
+ elif ch == "'":
68
+ out += "&#39;"
69
+ else:
70
+ out += ch
71
+ return out
72
+
73
+
74
+ def _has_tool(ctx: BriefingContext, tool_id: str) -> bool:
75
+ """True when the context advertises a tool whose name matches ``tool_id``."""
76
+ return any(t.name == tool_id for t in ctx.tools or ())
77
+
78
+
79
+ def _has_tool_prefix(ctx: BriefingContext, prefix: str) -> bool:
80
+ """True when the context advertises any tool whose name begins with
81
+ ``prefix``."""
82
+ return any(t.name.startswith(prefix) for t in ctx.tools or ())
83
+
84
+
85
+ def _iso_utc(moment: datetime) -> str:
86
+ """Format a timestamp the way the TS footer did (``Date.toISOString()``):
87
+ UTC, millisecond precision, trailing ``Z``. A naive datetime is treated as
88
+ already-UTC."""
89
+ if moment.tzinfo is not None:
90
+ moment = moment.astimezone(timezone.utc)
91
+ return moment.strftime("%Y-%m-%dT%H:%M:%S.") + f"{moment.microsecond // 1000:03d}Z"
92
+
93
+
94
+ # ---------------------------------------------------------------------------
95
+ # Tool descriptions (re-authored one-liners)
96
+ # ---------------------------------------------------------------------------
97
+
98
+ #: One-line summaries for the built-in tools, keyed by tool name. The briefing
99
+ #: surfaces the summary for each advertised tool that has one; tools without
100
+ #: an entry fall back to their own ``description`` from the
101
+ #: :class:`~induscode.briefing.contract.AgentTool` record.
102
+ #:
103
+ #: The phrasing here is the briefing's own.
104
+ TOOL_SUMMARIES: Final[Mapping[str, str]] = {
105
+ "read": "Open a file's contents for inspection.",
106
+ "write": "Create a new file or overwrite an existing one wholesale.",
107
+ "edit": "Apply a precise in-place change by matching exact existing text.",
108
+ "bash": "Run a shell command in the workspace.",
109
+ "grep": "Search file contents by pattern across the tree.",
110
+ "find": "Locate files and directories by name or glob.",
111
+ "ls": "List the entries of a directory.",
112
+ "task": "Hand a self-contained sub-task to a delegate agent.",
113
+ "todoread": "Read back the current task checklist.",
114
+ "todowrite": "Record or revise the task checklist.",
115
+ "webfetch": "Retrieve and read the contents of a URL.",
116
+ "websearch": "Query the web for current information.",
117
+ }
118
+
119
+
120
+ def _describe_tool(tool: AgentTool) -> str:
121
+ """Render a single tool's advertised line: name + best available summary."""
122
+ summary = TOOL_SUMMARIES.get(tool.name)
123
+ if summary is None:
124
+ description = getattr(tool, "description", None)
125
+ summary = description.strip() if isinstance(description, str) else ""
126
+ return f"`{tool.name}` — {summary}" if summary else f"`{tool.name}`"
127
+
128
+
129
+ # ---------------------------------------------------------------------------
130
+ # Sections
131
+ # ---------------------------------------------------------------------------
132
+
133
+
134
+ def _render_role(_ctx: BriefingContext) -> str:
135
+ return _join_blocks(
136
+ [
137
+ "You are a terminal-based software engineering assistant. You operate inside a real workspace and make progress by reading code, running commands, and editing files directly — not by describing what someone else should do.",
138
+ "Work toward the user's actual goal. Take the initiative to gather the context you need, but stay within the scope of what was asked: do not redesign, rename, or refactor beyond the request unless the user invites it.",
139
+ ]
140
+ )
141
+
142
+
143
+ #: Role / setup. Always present. Establishes the agent's purpose and the
144
+ #: stance the rest of the briefing assumes.
145
+ ROLE_SECTION: Final[BriefingSection] = BriefingSection(id="role", render=_render_role)
146
+
147
+
148
+ def _tools_applies(ctx: BriefingContext) -> bool:
149
+ return len(ctx.tools or ()) > 0
150
+
151
+
152
+ def _render_tools(ctx: BriefingContext) -> str:
153
+ lines = [_describe_tool(t) for t in ctx.tools or ()]
154
+ return _join_blocks(
155
+ [
156
+ "# Tools",
157
+ "These capabilities are available to you this turn. Reach for the most specific one for the job.",
158
+ _bullets(lines),
159
+ ]
160
+ )
161
+
162
+
163
+ #: Available tools. Renders only when the context advertises at least one
164
+ #: tool. Lists each advertised tool with its re-authored one-liner.
165
+ TOOLS_SECTION: Final[BriefingSection] = BriefingSection(
166
+ id="tools", title="Tools", applies=_tools_applies, render=_render_tools
167
+ )
168
+
169
+
170
+ def _render_guidelines(ctx: BriefingContext) -> str:
171
+ items: list[str] = []
172
+
173
+ if _has_tool(ctx, "read"):
174
+ items.append(
175
+ "Always inspect a file with the reader before you change it. Do not shell out to `cat`, `head`, or `sed` to view a file when the reader is available."
176
+ )
177
+ if _has_tool(ctx, "edit"):
178
+ items.append(
179
+ "For a targeted change, prefer the editor: it replaces an exact span of existing text, so the old text you supply must match the file byte-for-byte."
180
+ )
181
+ if _has_tool(ctx, "write"):
182
+ items.append(
183
+ "Reserve the writer for creating a new file or replacing one in full; for anything smaller, edit in place rather than rewriting the whole file."
184
+ )
185
+ if _has_tool(ctx, "grep") or _has_tool(ctx, "find") or _has_tool(ctx, "ls"):
186
+ items.append(
187
+ "Explore the tree with the dedicated search and listing tools rather than shell equivalents — they are quicker and already skip ignored paths."
188
+ )
189
+ if _has_tool(ctx, "bash"):
190
+ items.append(
191
+ "Use the shell for builds, tests, and other commands that no purpose-built tool covers. Keep commands scoped and avoid destructive operations unless the user asked for them."
192
+ )
193
+
194
+ items.append(
195
+ "Keep your replies short and to the point. Lead with the result; add only the explanation the user needs to act on it."
196
+ )
197
+ items.append(
198
+ "When a change is non-trivial, verify it — run the build or the relevant tests — before you report it as done."
199
+ )
200
+
201
+ return _join_blocks(["# Working guidance", _bullets(items)])
202
+
203
+
204
+ #: Working guidance. The behavioral guidelines — every sentence authored
205
+ #: fresh. Each guideline is gated on the relevant tool being present so the
206
+ #: briefing never instructs the model to use a capability it was not given.
207
+ GUIDELINES_SECTION: Final[BriefingSection] = BriefingSection(
208
+ id="guidelines", title="Working guidance", render=_render_guidelines
209
+ )
210
+
211
+
212
+ def _tasks_applies(ctx: BriefingContext) -> bool:
213
+ return _has_tool(ctx, "todowrite") or _has_tool(ctx, "todoread")
214
+
215
+
216
+ def _render_tasks(_ctx: BriefingContext) -> str:
217
+ return _join_blocks(
218
+ [
219
+ "# Task tracking",
220
+ "For work that spans several steps, maintain a checklist with the task tools. Break the job into concrete items, mark exactly one in progress at a time, and tick items off as you finish them so the user can follow along.",
221
+ ]
222
+ )
223
+
224
+
225
+ #: Task tracking. Present when a todo tool is advertised. Explains how the
226
+ #: model should keep the shared checklist current.
227
+ TASK_SECTION: Final[BriefingSection] = BriefingSection(
228
+ id="tasks", title="Task tracking", applies=_tasks_applies, render=_render_tasks
229
+ )
230
+
231
+
232
+ def _subagents_applies(ctx: BriefingContext) -> bool:
233
+ return len(ctx.subagents or ()) > 0
234
+
235
+
236
+ def _render_subagents(ctx: BriefingContext) -> str:
237
+ lines = []
238
+ for s in ctx.subagents or ():
239
+ when = f" Use it when {s.when}" if s.when else ""
240
+ lines.append(f"**{s.name}** — {s.purpose}.{when}")
241
+ return _join_blocks(
242
+ [
243
+ "# Delegates",
244
+ "You can offload a focused, self-contained piece of work to one of these delegates with the task tool. Hand off when a sub-problem is well-scoped enough to be solved without your full conversation context.",
245
+ _bullets(lines),
246
+ ]
247
+ )
248
+
249
+
250
+ #: Delegates / subagents. Present when the context lists delegate roles.
251
+ #: Renders each role the primary agent may hand work to.
252
+ SUBAGENTS_SECTION: Final[BriefingSection] = BriefingSection(
253
+ id="subagents", title="Delegates", applies=_subagents_applies, render=_render_subagents
254
+ )
255
+
256
+
257
+ def _connectors_applies(ctx: BriefingContext) -> bool:
258
+ return _has_tool_prefix(ctx, "connector_") or _has_tool_prefix(ctx, "saas_")
259
+
260
+
261
+ def _render_connectors(_ctx: BriefingContext) -> str:
262
+ return _join_blocks(
263
+ [
264
+ "# Connectors",
265
+ _bullets(
266
+ [
267
+ "When a task needs an external service (issue trackers, calendars, docs, and the like), prefer the connector tools over scraping or guessing.",
268
+ "Confirm the connection is authorized before you rely on it, and surface a clear next step if it is not.",
269
+ "Page through large result sets deliberately rather than assuming the first response is complete.",
270
+ ]
271
+ ),
272
+ ]
273
+ )
274
+
275
+
276
+ #: Connector guidance. Present when SaaS-connector tools (prefixed) are
277
+ #: advertised. Authored fresh; no carried-over vendor prose.
278
+ CONNECTORS_SECTION: Final[BriefingSection] = BriefingSection(
279
+ id="connectors", title="Connectors", applies=_connectors_applies, render=_render_connectors
280
+ )
281
+
282
+
283
+ def _project_context_applies(ctx: BriefingContext) -> bool:
284
+ return len(ctx.context_docs or ()) > 0
285
+
286
+
287
+ def _render_project_context(ctx: BriefingContext) -> str:
288
+ docs = [f"## {d.path}\n\n{d.body.strip()}" for d in ctx.context_docs or ()]
289
+ return _join_blocks(
290
+ [
291
+ "# Project context",
292
+ "The following project documents describe conventions for this repository. Treat them as standing instructions.",
293
+ *docs,
294
+ ]
295
+ )
296
+
297
+
298
+ #: Project context. Present when the context carries project documents.
299
+ #: Inlines each under its own sub-heading so repository conventions are in
300
+ #: view.
301
+ PROJECT_CONTEXT_SECTION: Final[BriefingSection] = BriefingSection(
302
+ id="project-context",
303
+ title="Project context",
304
+ applies=_project_context_applies,
305
+ render=_render_project_context,
306
+ )
307
+
308
+
309
+ def _skills_applies(ctx: BriefingContext) -> bool:
310
+ return len(ctx.skills or ()) > 0
311
+
312
+
313
+ def _render_skills(ctx: BriefingContext) -> str:
314
+ entries = []
315
+ for card in ctx.skills or ():
316
+ entries.append(
317
+ "\n".join(
318
+ [
319
+ " <skill>",
320
+ f" <name>{_escape_xml(card.name)}</name>",
321
+ f" <description>{_escape_xml(card.description)}</description>",
322
+ f" <location>{_escape_xml(card.location)}</location>",
323
+ " </skill>",
324
+ ]
325
+ )
326
+ )
327
+ joined = "\n".join(entries)
328
+ return _join_blocks(
329
+ [
330
+ "# Skills",
331
+ "Each skill below is a set of task-specific instructions stored on disk. When a task lines up with a skill's description, load its file with the reader and follow it. Paths a skill mentions are relative to that skill's own directory.",
332
+ f"<available_skills>\n{joined}\n</available_skills>",
333
+ ]
334
+ )
335
+
336
+
337
+ #: Skills block. Present when the context carries model-invocable cards.
338
+ #: Renders the cards into an ``<available_skills>`` block (the format's shape)
339
+ #: with a re-authored lead-in.
340
+ SKILLS_SECTION: Final[BriefingSection] = BriefingSection(
341
+ id="skills", title="Skills", applies=_skills_applies, render=_render_skills
342
+ )
343
+
344
+
345
+ def _render_footer(ctx: BriefingContext) -> str:
346
+ where = ctx.cwd if ctx.cwd is not None else ctx.workspace
347
+ when = _iso_utc(ctx.now if ctx.now is not None else datetime.now(timezone.utc))
348
+ lines: list[str] = []
349
+ if where:
350
+ lines.append(f"Working directory: {where}")
351
+ lines.append(f"Current time: {when}")
352
+ return "\n".join(lines)
353
+
354
+
355
+ #: Footer. Always present. Stamps the working directory and the current time
356
+ #: so the model has its bearings.
357
+ FOOTER_SECTION: Final[BriefingSection] = BriefingSection(id="footer", render=_render_footer)
358
+
359
+
360
+ #: The default briefing recipe, in render order. Swap or reorder entries to
361
+ #: reshape the prompt without touching :func:`compose_briefing`.
362
+ BRIEFING_SECTIONS: Final[Briefing] = (
363
+ ROLE_SECTION,
364
+ TOOLS_SECTION,
365
+ GUIDELINES_SECTION,
366
+ TASK_SECTION,
367
+ SUBAGENTS_SECTION,
368
+ CONNECTORS_SECTION,
369
+ PROJECT_CONTEXT_SECTION,
370
+ SKILLS_SECTION,
371
+ FOOTER_SECTION,
372
+ )
373
+
374
+
375
+ # ---------------------------------------------------------------------------
376
+ # Composer
377
+ # ---------------------------------------------------------------------------
378
+
379
+
380
+ def compose_briefing(value: BriefingContext | BriefingInputs) -> str:
381
+ """Fold a section recipe and a context into the final briefing string.
382
+
383
+ Accepts either a plain :class:`~induscode.briefing.contract.BriefingContext`
384
+ (using the default :data:`BRIEFING_SECTIONS` recipe) or a full
385
+ :class:`~induscode.briefing.contract.BriefingInputs` bundle that names its
386
+ own sections and optional ``prelude`` / ``append`` text. The composer
387
+ keeps each section whose ``applies`` predicate is satisfied (or absent),
388
+ renders it, discards empty fragments, and joins the rest with blank-line
389
+ gaps. Optional ``prelude`` and ``append`` bracket the rendered sections.
390
+
391
+ :param value: either the render context, or a full inputs bundle
392
+ :returns: the assembled system-prompt string
393
+ """
394
+ inputs = (
395
+ value
396
+ if isinstance(value, BriefingInputs)
397
+ else BriefingInputs(sections=BRIEFING_SECTIONS, context=value)
398
+ )
399
+
400
+ rendered: list[str] = []
401
+ if inputs.prelude is not None and inputs.prelude.strip():
402
+ rendered.append(inputs.prelude.strip())
403
+
404
+ for section in inputs.sections:
405
+ if section.applies is not None and not section.applies(inputs.context):
406
+ continue
407
+ fragment = section.render(inputs.context)
408
+ if fragment.strip():
409
+ rendered.append(fragment.strip())
410
+
411
+ if inputs.append is not None and inputs.append.strip():
412
+ rendered.append(inputs.append.strip())
413
+
414
+ return _join_blocks(rendered)