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,198 @@
1
+ """Boot stage pipeline — the ordered list of :class:`~.contract.Stage`
2
+ transforms that turn a bare :class:`~.contract.BootContext` (argv + workspace
3
+ + brand) into a fully-resolved one (parsed invocation, materialised
4
+ directories, applied upgrades, resolved startup resources, selected runner).
5
+
6
+ Port of TS ``src/boot/stages.ts``. The pipeline is data, not control flow:
7
+ :data:`STAGES` lists each step in order, and :func:`run_stages` folds an
8
+ *immutable* context through them — every stage receives a context and returns
9
+ its successor (built with :func:`dataclasses.replace`, never by mutating). A
10
+ stage may return an awaitable; the fold awaits each in turn so ordering and
11
+ side-effect sequencing are deterministic.
12
+
13
+ Stage order and intent:
14
+
15
+ 1. ``locate-workspace`` — materialise the resolved workspace on disk
16
+ (:func:`~induscode.workspace.ensure_dirs`). Pure path computation already
17
+ happened when the initial context was built; this stage only ``mkdir``\\ s.
18
+ 2. ``apply-upgrades`` — fold the idempotent
19
+ :func:`~.upgrade.apply_upgrades` registry over the workspace (a no-op on
20
+ an already-current profile).
21
+ 3. ``build-invocation`` — parse ``argv`` into the typed invocation.
22
+ 4. ``resolve-resources`` — best-effort construct the framework
23
+ settings/auth/model graph; degrade to a minimal object if a framework
24
+ piece is unavailable.
25
+ 5. ``select-runner`` — no-op transform that exists for symmetry and
26
+ tracing (the actual dispatch happens in :mod:`~.boot` after the pipeline,
27
+ so the selected runner can own the exit code).
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import inspect
33
+ from collections.abc import Mapping, Sequence
34
+ from dataclasses import replace
35
+ from typing import Final
36
+
37
+ from indusagi.shell_app import Settings
38
+
39
+ from induscode.conductor import CatalogSource, ModelCatalog
40
+ from induscode.workspace import ensure_dirs
41
+
42
+ from .contract import BootContext, Stage, StartupResources
43
+ from .invocation import tokenize_invocation
44
+ from .upgrade import apply_upgrades
45
+
46
+ __all__ = [
47
+ "STAGES",
48
+ "build_invocation",
49
+ "locate_workspace",
50
+ "resolve_resources",
51
+ "run_stages",
52
+ "select_runner_stage",
53
+ "upgrade_stage",
54
+ ]
55
+
56
+
57
+ # ---------------------------------------------------------------------------
58
+ # Stages
59
+ # ---------------------------------------------------------------------------
60
+
61
+
62
+ def _locate_workspace(ctx: BootContext) -> BootContext:
63
+ """Materialise the resolved workspace directories on disk. The workspace
64
+ record was already computed (pure) when the initial context was
65
+ assembled; this stage only ensures the directory subset exists. Returns
66
+ the same context — only the filesystem side effects happen here."""
67
+ ensure_dirs(ctx.workspace)
68
+ return ctx
69
+
70
+
71
+ #: Stage 1 — materialise the workspace directories.
72
+ locate_workspace: Final[Stage] = Stage(name="locate-workspace", apply=_locate_workspace)
73
+
74
+
75
+ async def _apply_upgrades(ctx: BootContext) -> BootContext:
76
+ """Run any pending one-time profile upgrades, idempotently. The driver is
77
+ non-fatal: a step that fails is reported and retried on a later launch,
78
+ never aborting boot. The context is returned unchanged (upgrades touch
79
+ the filesystem, not the context)."""
80
+ await apply_upgrades(ctx.workspace)
81
+ return ctx
82
+
83
+
84
+ #: Stage 2 — fold the upgrade registry over the workspace.
85
+ #:
86
+ #: Port note: TS exported this stage as ``upgrade``; in Python that name
87
+ #: would shadow the :mod:`induscode.boot.upgrade` subpackage attribute on the
88
+ #: parent package, so it is exported as ``upgrade_stage``.
89
+ upgrade_stage: Final[Stage] = Stage(name="apply-upgrades", apply=_apply_upgrades)
90
+
91
+
92
+ def _build_invocation(ctx: BootContext) -> BootContext:
93
+ """Parse ``argv`` into the typed invocation and thread it onto the
94
+ context. The initial context carries a pre-parsed invocation (the
95
+ bootstrapper parses once up front for the meta short-circuits); this
96
+ stage replaces it with a fresh parse of the same argv."""
97
+ return replace(ctx, invocation=tokenize_invocation(ctx.argv))
98
+
99
+
100
+ #: Stage 3 — re-parse the command line.
101
+ build_invocation: Final[Stage] = Stage(name="build-invocation", apply=_build_invocation)
102
+
103
+
104
+ def _load_default_settings() -> Settings | Mapping[str, object]:
105
+ """Resolve the framework's baseline settings. Prefers the framework's
106
+ published ``DEFAULT_SETTINGS``; a load failure degrades to an empty
107
+ mapping without failing boot (the *shape* is what later phases rely on)."""
108
+ try:
109
+ from indusagi.shell_app import DEFAULT_SETTINGS
110
+
111
+ return DEFAULT_SETTINGS
112
+ except Exception:
113
+ return {}
114
+
115
+
116
+ def _load_model_catalog() -> ModelCatalog:
117
+ """Resolve the model catalog: a fresh conductor
118
+ :class:`~induscode.conductor.ModelCatalog` over the live framework
119
+ registry (constructed fresh so per-process custom-model loading does not
120
+ leak across runs); degrades to an empty catalog when the framework
121
+ registry is unavailable."""
122
+ try:
123
+ return ModelCatalog()
124
+ except Exception:
125
+ return ModelCatalog(
126
+ CatalogSource(providers=lambda: [], models=lambda provider: [])
127
+ )
128
+
129
+
130
+ def _build_startup_resources() -> StartupResources:
131
+ """Construct the :class:`~.contract.StartupResources` graph, preferring
132
+ framework-owned pieces and falling back to minimal placeholders. The
133
+ credential graph stays an empty placeholder bag — the concrete vault is
134
+ consulted directly by the session assembly."""
135
+ return StartupResources(
136
+ settings=_load_default_settings(),
137
+ auth={},
138
+ models=_load_model_catalog(),
139
+ )
140
+
141
+
142
+ def _resolve_resources(ctx: BootContext) -> BootContext:
143
+ """Best-effort assembly of the startup resource graph."""
144
+ return replace(ctx, resources=_build_startup_resources())
145
+
146
+
147
+ #: Stage 4 — resolve the settings/auth/model graph (degrade-to-empty).
148
+ resolve_resources: Final[Stage] = Stage(name="resolve-resources", apply=_resolve_resources)
149
+
150
+
151
+ def _select_runner(ctx: BootContext) -> BootContext:
152
+ """Marker / tracing stage for runner selection. The real dispatch is
153
+ performed by :mod:`~.boot` after the pipeline so the chosen runner can
154
+ own the process exit code; this stage keeps the ordered pipeline a
155
+ faithful description of the launch sequence."""
156
+ return ctx
157
+
158
+
159
+ #: Stage 5 — tracing no-op for runner selection.
160
+ select_runner_stage: Final[Stage] = Stage(name="select-runner", apply=_select_runner)
161
+
162
+
163
+ # ---------------------------------------------------------------------------
164
+ # The pipeline + fold
165
+ # ---------------------------------------------------------------------------
166
+
167
+ #: The launch pipeline, in execution order. Folded by :func:`run_stages`.
168
+ STAGES: Final[tuple[Stage, ...]] = (
169
+ locate_workspace,
170
+ upgrade_stage,
171
+ build_invocation,
172
+ resolve_resources,
173
+ select_runner_stage,
174
+ )
175
+
176
+
177
+ async def run_stages(
178
+ initial: BootContext,
179
+ stages: Sequence[Stage] = STAGES,
180
+ ) -> BootContext:
181
+ """Fold an ordered list of :class:`~.contract.Stage` transforms over an
182
+ immutable :class:`~.contract.BootContext`.
183
+
184
+ Each stage receives the current context and returns its successor; the
185
+ result of one stage is the input to the next. Awaits every stage result
186
+ so async ordering is deterministic. The input ``initial`` is never
187
+ mutated — stages produce new contexts via :func:`dataclasses.replace`.
188
+
189
+ :param initial: the seed context (argv + workspace + brand + placeholder
190
+ fields)
191
+ :param stages: the ordered transforms to apply; defaults to :data:`STAGES`
192
+ :returns: the fully-resolved context after every stage has run
193
+ """
194
+ ctx = initial
195
+ for stage in stages:
196
+ result = stage.apply(ctx)
197
+ ctx = await result if inspect.isawaitable(result) else result
198
+ return ctx
@@ -0,0 +1,36 @@
1
+ """Upgrade subsystem — public barrel (port of TS ``src/boot/upgrade``).
2
+
3
+ Surfaces the ordered, idempotent profile-upgrade registry and its driver.
4
+ Boot consumers import :func:`apply_upgrades` to run pending one-time
5
+ migrations and the :data:`UPGRADES` registry / :class:`Upgrade` shape for
6
+ inspection and testing. The marker-file bookkeeping is an internal detail of
7
+ the driver and is not re-exported.
8
+
9
+ Port note: the TS barrel also exported ``projectTranscriptDirName`` (the
10
+ per-cwd transcript-dir encoder step 2 used). The Python steps are documented
11
+ no-ops on the fresh ``~/.pindusagi`` root (see :mod:`.upgrades`), so the
12
+ encoder has no consumer and is not ported.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from .apply import UpgradeReport, apply_upgrades
18
+ from .upgrades import (
19
+ UPGRADES,
20
+ Upgrade,
21
+ fold_credentials,
22
+ relocate_binaries,
23
+ rename_prompt_dir,
24
+ reshelve_transcripts,
25
+ )
26
+
27
+ __all__ = [
28
+ "UPGRADES",
29
+ "Upgrade",
30
+ "UpgradeReport",
31
+ "apply_upgrades",
32
+ "fold_credentials",
33
+ "relocate_binaries",
34
+ "rename_prompt_dir",
35
+ "reshelve_transcripts",
36
+ ]
@@ -0,0 +1,125 @@
1
+ """Upgrade driver — folds the ordered :data:`~.upgrades.UPGRADES` registry
2
+ over a :class:`~induscode.workspace.Workspace`, exactly once per step.
3
+
4
+ Port of TS ``src/boot/upgrade/apply.ts``. The driver is the only place that
5
+ knows about the *marker file*: a small JSON record under the profile
6
+ directory listing the ids of upgrades that have already run
7
+ (``.upgrade-state.json`` with the ``{"appliedIds": [...]}`` shape — kept
8
+ byte-compatible with the TS app since this app owns the file). Each registry
9
+ step is skipped if its id is present in the marker; otherwise it is applied
10
+ and, on success, its id is appended and the marker is persisted. A step that
11
+ raises is *not* recorded (so it is retried next launch) and is reported as a
12
+ warning rather than aborting the remaining steps — one bad migration must
13
+ never wedge startup.
14
+
15
+ The result is purely informational: :attr:`UpgradeReport.applied` lists ids
16
+ run *this* invocation (empty on an already-current profile), and
17
+ :attr:`UpgradeReport.warnings` carries human-readable notes for any step that
18
+ failed. Callers typically log both and continue.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ from dataclasses import dataclass
25
+ from pathlib import Path
26
+ from typing import Final
27
+
28
+ from induscode.workspace import Workspace
29
+
30
+ from .upgrades import UPGRADES
31
+
32
+ __all__ = [
33
+ "UpgradeReport",
34
+ "apply_upgrades",
35
+ ]
36
+
37
+ #: Basename of the marker file recording which upgrade ids have been applied.
38
+ _MARKER_FILENAME: Final[str] = ".upgrade-state.json"
39
+
40
+
41
+ @dataclass(frozen=True, slots=True)
42
+ class UpgradeReport:
43
+ """The outcome of an :func:`apply_upgrades` pass.
44
+
45
+ - ``applied`` — ids of upgrades that ran successfully *this* invocation,
46
+ in apply order. Empty when the profile was already current.
47
+ - ``warnings`` — one entry per step that raised, naming the step and its
48
+ error. Non-fatal: applying continues past a failed step.
49
+ """
50
+
51
+ #: Ids successfully applied during this call, in order.
52
+ applied: list[str]
53
+ #: Human-readable notes for steps that failed (non-fatal).
54
+ warnings: list[str]
55
+
56
+
57
+ def _marker_path(ws: Workspace) -> Path:
58
+ """Absolute path of the marker file inside the profile directory."""
59
+ return Path(ws.profile_dir) / _MARKER_FILENAME
60
+
61
+
62
+ def _read_applied_ids(ws: Workspace) -> set[str]:
63
+ """Read the set of already-applied ids from the marker file. A missing,
64
+ unreadable, or malformed marker is treated as "nothing applied yet" so a
65
+ corrupted marker degrades to re-attempting (idempotent) steps rather than
66
+ crashing."""
67
+ try:
68
+ parsed = json.loads(_marker_path(ws).read_text(encoding="utf-8"))
69
+ ids = parsed.get("appliedIds") if isinstance(parsed, dict) else None
70
+ if not isinstance(ids, list):
71
+ return set()
72
+ return {item for item in ids if isinstance(item, str)}
73
+ except Exception:
74
+ return set()
75
+
76
+
77
+ def _write_applied_ids(ws: Workspace, applied_ids: set[str]) -> None:
78
+ """Persist the marker file with the given applied ids, creating parents
79
+ if needed. Order is the registry order for the ids present, so the file
80
+ reads as the apply history."""
81
+ ordered = [step.id for step in UPGRADES if step.id in applied_ids]
82
+ # Carry through any ids the registry no longer knows (forward compat).
83
+ ordered.extend(sorted(applied_ids - set(ordered)))
84
+ state = {"appliedIds": ordered}
85
+ path = _marker_path(ws)
86
+ path.parent.mkdir(parents=True, exist_ok=True)
87
+ path.write_text(f"{json.dumps(state, indent=2)}\n", encoding="utf-8")
88
+
89
+
90
+ def _describe_error(error: BaseException) -> str:
91
+ """Render a raised value as a single-line message for a warning."""
92
+ text = str(error)
93
+ return text if text else type(error).__name__
94
+
95
+
96
+ async def apply_upgrades(ws: Workspace) -> UpgradeReport:
97
+ """Apply every not-yet-applied upgrade in :data:`UPGRADES`, in registry
98
+ order, recording each success in the marker file so it never runs again.
99
+
100
+ Steps already named in the marker are skipped. A step that raises is
101
+ recorded as a warning, left unmarked (so it is retried on a later
102
+ launch), and does not block the remaining steps. The marker is rewritten
103
+ after each success, so a crash mid-pass still preserves the progress made
104
+ so far.
105
+
106
+ :param ws: the resolved, absolute on-disk layout to upgrade
107
+ :returns: the ids applied this pass and any non-fatal warnings
108
+ """
109
+ already_applied = _read_applied_ids(ws)
110
+ applied: list[str] = []
111
+ warnings: list[str] = []
112
+
113
+ for upgrade in UPGRADES:
114
+ if upgrade.id in already_applied:
115
+ continue
116
+ try:
117
+ await upgrade.apply(ws)
118
+ already_applied.add(upgrade.id)
119
+ applied.append(upgrade.id)
120
+ # Persist after each success so partial progress survives a crash.
121
+ _write_applied_ids(ws, already_applied)
122
+ except Exception as error: # noqa: BLE001 — reported, never fatal
123
+ warnings.append(f'upgrade "{upgrade.id}" failed: {_describe_error(error)}')
124
+
125
+ return UpgradeReport(applied=applied, warnings=warnings)
@@ -0,0 +1,136 @@
1
+ """Upgrade registry — the ordered, idempotent catalog of one-time profile
2
+ migrations.
3
+
4
+ Port of TS ``src/boot/upgrade/upgrades.ts``, under the port plan's locked
5
+ migration decision: the Python build's profile root (``~/.pindusagi``)
6
+ **starts empty** — no TS-era legacy layout (``oauth.json`` + embedded
7
+ ``apiKeys``, loose ``*.jsonl`` transcripts, a ``tools/`` binary dir, a
8
+ ``commands/`` template dir) can ever exist under it. The migration
9
+ *mechanism* is therefore kept fully exercisable (registry → marker-file
10
+ driver → idempotence pinned by the boot tests), while the four TS steps are
11
+ registered as **documented no-ops**:
12
+
13
+ - their :attr:`Upgrade.id` strings are preserved **verbatim** so the ids stay
14
+ reserved — a future step can never accidentally reuse one with different
15
+ semantics, and a marker file written by this build remains meaningful;
16
+ - their bodies perform no filesystem work (each detects "nothing legacy can
17
+ exist here" by construction and returns).
18
+
19
+ Idempotence contract (every step must honor it, no-op or not):
20
+
21
+ - Detect "already done" cheaply and return early without side effects.
22
+ - Treat a *missing* source (the thing being migrated from) as success.
23
+ - Never destroy data.
24
+
25
+ Append new (real) steps to the end of :data:`UPGRADES` — never reorder or
26
+ rename existing ids, as that would re-run already-applied migrations.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ from collections.abc import Awaitable, Callable
32
+ from dataclasses import dataclass
33
+
34
+ from induscode.workspace import Workspace
35
+
36
+ __all__ = [
37
+ "UPGRADES",
38
+ "Upgrade",
39
+ "fold_credentials",
40
+ "relocate_binaries",
41
+ "rename_prompt_dir",
42
+ "reshelve_transcripts",
43
+ ]
44
+
45
+
46
+ # ---------------------------------------------------------------------------
47
+ # The Upgrade shape
48
+ # ---------------------------------------------------------------------------
49
+
50
+
51
+ @dataclass(frozen=True, slots=True)
52
+ class Upgrade:
53
+ """One ordered, idempotent profile-layout upgrade.
54
+
55
+ An upgrade is pure data plus one async effect. The registry order is the
56
+ apply order; :attr:`id` is the durable key recorded in the marker file
57
+ once :attr:`apply` completes, so it must be stable across releases
58
+ (renaming an id re-runs the step). :attr:`apply` must be safe to invoke
59
+ against a profile in any state — fresh, partially-migrated, or
60
+ fully-migrated.
61
+ """
62
+
63
+ #: Durable identifier recorded once this step has run. Never rename.
64
+ id: str
65
+ #: One-line human summary for logs and verbose output.
66
+ describe: str
67
+ #: Perform the upgrade against the resolved workspace (idempotent).
68
+ apply: Callable[[Workspace], Awaitable[None]]
69
+
70
+
71
+ # ---------------------------------------------------------------------------
72
+ # The four TS migrations, registered as documented no-ops
73
+ # ---------------------------------------------------------------------------
74
+
75
+
76
+ async def _noop(ws: Workspace) -> None:
77
+ """The shared no-op body: the legacy source this step migrated from
78
+ belongs to the TS-era ``~/.indusagi/agent`` layout and cannot exist under
79
+ the fresh ``~/.pindusagi`` root, so there is nothing to detect and
80
+ nothing to move. (Idempotent by construction.)"""
81
+ del ws
82
+
83
+
84
+ #: TS step 1 — folded a legacy split credential layout (a standalone
85
+ #: ``oauth.json`` plus an ``apiKeys`` block embedded in ``settings.json``)
86
+ #: into the single consolidated 0600 ``auth.json``. No-op here: the Python
87
+ #: profile root never carried the split layout; the disk vault
88
+ #: (:mod:`induscode.boot.auth_vault`) writes the consolidated shape from day
89
+ #: one.
90
+ fold_credentials = Upgrade(
91
+ id="fold-credentials-into-secure-auth-file",
92
+ describe="Consolidate legacy oauth.json + settings.apiKeys into a 0600 auth.json (no-op on the fresh root)",
93
+ apply=_noop,
94
+ )
95
+
96
+ #: TS step 2 — relocated loose ``*.jsonl`` transcripts written directly under
97
+ #: the profile root into the per-cwd ``sessions/`` tree. No-op here: the
98
+ #: conductor's filesystem backend has always written ``.ndjson`` transcripts
99
+ #: under the cwd-scoped sessions directory.
100
+ reshelve_transcripts = Upgrade(
101
+ id="reshelve-loose-transcripts-into-sessions-dir",
102
+ describe="Move loose session .jsonl files under the profile root into sessions/<cwd>/ (no-op on the fresh root)",
103
+ apply=_noop,
104
+ )
105
+
106
+ #: TS step 3 — moved the managed ``fd`` / ``rg`` helper binaries from the
107
+ #: legacy ``tools/`` directory into ``bin/``. No-op here: the Python layout
108
+ #: provisions helpers under ``bin/`` directly (``tools_dir`` == ``bin_dir``).
109
+ relocate_binaries = Upgrade(
110
+ id="relocate-managed-helper-binaries-to-bin",
111
+ describe="Move managed fd/rg helper binaries from the legacy tools/ dir into bin/ (no-op on the fresh root)",
112
+ apply=_noop,
113
+ )
114
+
115
+ #: TS step 4 — renamed the legacy ``commands/`` template directory to
116
+ #: ``prompts/``. No-op here: the Python layout creates ``prompts/`` directly.
117
+ rename_prompt_dir = Upgrade(
118
+ id="rename-legacy-commands-dir-to-prompts",
119
+ describe="Rename the legacy commands/ template directory to prompts/ (no-op on the fresh root)",
120
+ apply=_noop,
121
+ )
122
+
123
+
124
+ # ---------------------------------------------------------------------------
125
+ # The ordered registry
126
+ # ---------------------------------------------------------------------------
127
+
128
+ #: The ordered list of upgrades. Apply order is array order; ids are the
129
+ #: durable marker keys. Append new steps to the end — never reorder or rename
130
+ #: existing ids.
131
+ UPGRADES: tuple[Upgrade, ...] = (
132
+ fold_credentials,
133
+ reshelve_transcripts,
134
+ relocate_binaries,
135
+ rename_prompt_dir,
136
+ )
@@ -0,0 +1,115 @@
1
+ """Briefing subsystem — public barrel.
2
+
3
+ Re-exports the FROZEN prompt contract: the declarative system-prompt pipeline
4
+ (:class:`BriefingSection` over a :class:`BriefingContext`, composed by
5
+ :func:`compose_briefing`), the single-pass ``$arg`` macro model
6
+ (:class:`Macro`, :class:`MacroScope`, :data:`MacroToken`), and the
7
+ Agent-Skills capability cards (:class:`SkillCard` parsed from a ``SKILL.md``).
8
+
9
+ Port note: the TS barrel additionally re-exported the table-driven SGR
10
+ machine, the HTML-transcript color layer (``ExportTheme`` / ``ThemeBridge`` /
11
+ ``LuminanceLut``), and the publish surface from ``briefing/contract``. In the
12
+ Python build those types belong to ``induscode.transcript_export`` (ported
13
+ separately); this barrel carries only the briefing-owned vocabulary plus the
14
+ shared :class:`BriefingFault`. Consumers import the briefing surface from
15
+ ``induscode.briefing`` rather than reaching into individual modules.
16
+ """
17
+
18
+ from .compose import BRIEFING_SECTIONS, compose_briefing
19
+ from .contract import (
20
+ SKILL_DESCRIPTION_LIMIT,
21
+ SKILL_NAME_LIMIT,
22
+ AgentState,
23
+ AgentTool,
24
+ AllToken,
25
+ Briefing,
26
+ BriefingContext,
27
+ BriefingFault,
28
+ BriefingFaultKind,
29
+ BriefingInputs,
30
+ BriefingSection,
31
+ ContextDoc,
32
+ ImageContent,
33
+ LiteralToken,
34
+ Macro,
35
+ MacroOrigin,
36
+ MacroScope,
37
+ MacroToken,
38
+ MacroTokenKind,
39
+ PositionalToken,
40
+ SkillCard,
41
+ SkillDiagnostic,
42
+ SkillFrontmatter,
43
+ SkillLoad,
44
+ SkillOutcomeKind,
45
+ SliceToken,
46
+ SubagentBrief,
47
+ TextContent,
48
+ briefing_fault,
49
+ )
50
+ from .macros import (
51
+ FrontmatterSplit,
52
+ apply_macros,
53
+ build_macro_scope,
54
+ expand_invocation,
55
+ load_macros,
56
+ read_macro_file,
57
+ resolve_tokens,
58
+ scan_macro_body,
59
+ set_legacy_macro_reporter,
60
+ split_frontmatter,
61
+ )
62
+ from .skills import (
63
+ SkillRoot,
64
+ gather_skill_cards,
65
+ load_skill_cards,
66
+ model_invocable_cards,
67
+ )
68
+
69
+ __all__ = [
70
+ "AgentState",
71
+ "AgentTool",
72
+ "AllToken",
73
+ "BRIEFING_SECTIONS",
74
+ "Briefing",
75
+ "BriefingContext",
76
+ "BriefingFault",
77
+ "BriefingFaultKind",
78
+ "BriefingInputs",
79
+ "BriefingSection",
80
+ "ContextDoc",
81
+ "FrontmatterSplit",
82
+ "ImageContent",
83
+ "LiteralToken",
84
+ "Macro",
85
+ "MacroOrigin",
86
+ "MacroScope",
87
+ "MacroToken",
88
+ "MacroTokenKind",
89
+ "PositionalToken",
90
+ "SKILL_DESCRIPTION_LIMIT",
91
+ "SKILL_NAME_LIMIT",
92
+ "SkillCard",
93
+ "SkillDiagnostic",
94
+ "SkillFrontmatter",
95
+ "SkillLoad",
96
+ "SkillOutcomeKind",
97
+ "SkillRoot",
98
+ "SliceToken",
99
+ "SubagentBrief",
100
+ "TextContent",
101
+ "apply_macros",
102
+ "briefing_fault",
103
+ "build_macro_scope",
104
+ "compose_briefing",
105
+ "expand_invocation",
106
+ "gather_skill_cards",
107
+ "load_macros",
108
+ "load_skill_cards",
109
+ "model_invocable_cards",
110
+ "read_macro_file",
111
+ "resolve_tokens",
112
+ "scan_macro_body",
113
+ "set_legacy_macro_reporter",
114
+ "split_frontmatter",
115
+ ]