Skip to content

gaia pkg

Install, publish, and bootstrap packages.

gaia pkg add <package>            Install a registered package from the registry
gaia pkg add --lkm-index <id> --lkm-paper <paper-id>
                                  Materialize an LKM paper as a local package
gaia pkg add --lkm-index <id> --lkm-claim <claim-id>
                                  Resolve an LKM claim to its backing paper package
gaia pkg add lkm:<index>:paper:<paper-id>
                                  Materialize a canonical LKM paper source ref
gaia pkg add lkm:<index>:claim:<claim-id>
                                  Materialize the backing paper for a claim source ref
gaia pkg add-import --from <m>    Insert a sibling/module import into a package file
gaia pkg add-module --name <m>    Scaffold a sibling Python module
gaia pkg register [path]          Submit a package to the official registry
gaia pkg scaffold --target <p>    Bootstrap a fresh -gaia package directory layout
Verb Purpose
add Resolve a registry entry to a SHA-pinned git URL, add as dependency, optionally cache dep_beliefs/<name>.json; also accepts LKM paper source refs/flags, materializes the paper graph as a project-local Gaia package, compiles it, and adds it as an editable dependency
add-import Insert an idempotent from <module> import <names> line into __init__.py or another package source file
add-module Create src/<import_name>/<module>.py with an optional docstring, optional seeded DSL imports, and a literal empty __all__
register Submit a package to the registry: emit Package/Versions/Deps TOML, exports/premises/holes/bridges/beliefs JSON, and (optionally) push + open a registry PR
scaffold Write the minimal -gaia package skeleton (pyproject.toml with [tool.gaia], src/<import_name>/__init__.py importing claim, .gaia/.gitkeep). Counterpart to gaia author <verb> — bootstraps the package an agent then authors into. See gaia author.

The historical flat verbs map to grouped paths where applicable (gaia register --create-prgaia pkg register --create-pr). The add-import, add-module, and scaffold verbs are v0.5 additions. See CLI Commands for workflow examples and use gaia pkg <verb> --help for the executable option surface.

For LKM search results, gaia pkg add accepts both the friendly action form and canonical source refs:

gaia pkg add --lkm-index bohrium --lkm-paper 811827932371615744
gaia pkg add --lkm-index bohrium --lkm-claim gcn_579430355a0e4bbd
gaia pkg add lkm:bohrium:paper:811827932371615744
gaia pkg add lkm:bohrium:claim:gcn_579430355a0e4bbd
gaia pkg add lkm:paper:811827932371615744

The short lkm:paper:<id> / lkm:claim:<id> forms are default-index compatibility aliases; Gaia emits canonical refs with the explicit index id. The paper form fetches /papers/graph, writes a generated Gaia package under .gaia/lkm_packages/<package-name>/, compiles it so dependency manifests exist, and runs uv add --editable <generated-package>. The claim form first fetches graph-shaped claim reasoning, resolves the backing paper:<id>, then performs the same paper package materialization. If the reasoning response points to multiple backing papers, Gaia refuses to guess; inspect the raw reasoning response and add the intended paper explicitly.

Generated packages are normal Python Gaia packages. Their distribution name is title-first and id-backed, for example lkm-bohrium-controlling-phase-and-morphology-811827932371615744-gaia; their [tool.gaia.source] metadata records the stable source ref lkm:<index>:paper:<paper-id>. LKM paper factors are generated as depends_on(...) scaffold records by default. This is the unformalized authoring counterpart of derive(...): it preserves the premise-conclusion shape, but it does not enter IR/BP until a user reviews and materializes it as formal Gaia reasoning.

For LKM logic-graph responses, Gaia builds the scaffold from graph edges: a factor's concludes edge identifies the conclusion, while incoming claim edges such as previous_conclusion_of, weakpoint_of, and highlight_of are all treated as premise claims in given=[...]; other incoming claim edges to the same factor are treated the same way. addressed_problems and open_questions are generated as paper-context question nodes, not as depends_on(...) premises. The generated scaffold metadata preserves the original LKM edge types for auditability.

Downstream source can import generated claims directly, for example:

from lkm_bohrium_controlling_phase_and_morphology_811827932371615744 import conclusion_1

gaia pkg add still does not install standalone LKM claim nodes. Claim refs are convenience handles for installing the backing paper package.

LKM index URLs are resolver configuration, not package names. bohrium is the built-in index id for https://open.bohrium.com/openapi/v1/lkm; custom indexes can be supplied by environment, for example GAIA_LKM_INDEX_PRIVATE_URL=https://example.test/lkm and gaia pkg add --lkm-index private --lkm-paper <paper-id>. Access keys still come from the LKM credential flow or GAIA_LKM_ACCESS_KEY / LKM_ACCESS_KEY.

The engine-side helpers (loading, compilation, prior application) live at gaia.engine.packaging.

Implementation

gaia.cli.commands.add

gaia pkg add — install registered or LKM-backed Gaia packages.

LKMSourceRef dataclass

LKMSourceRef(index_id: str, kind: str, provider_id: str)

Stable source identity for an LKM-backed package candidate.

ref property

ref: str

Return the canonical LKM source ref.

add_command

add_command(package: str | None = typer.Argument(None, help='Package name (e.g., galileo-falling-bodies-gaia) or LKM ref (lkm:<index>:paper:<id> / lkm:<index>:claim:<id>).'), version: str | None = typer.Option(None, '--version', '-v', help='Specific version'), registry: str = typer.Option(DEFAULT_REGISTRY, '--registry', help='Registry GitHub repo'), lkm_index: str = typer.Option(DEFAULT_LKM_INDEX_ID, '--lkm-index', '--lkm-server', help='Configured LKM index id for --lkm-paper / --lkm-claim.'), lkm_paper: str | None = typer.Option(None, '--lkm-paper', help='Materialize this LKM paper id as a local Gaia package and add it.'), lkm_claim: str | None = typer.Option(None, '--lkm-claim', help='Resolve this LKM claim id to its backing paper package.'), target: str = typer.Option('.', '--target', help='Path to the Gaia knowledge package to add the dependency to (default: cwd). Matches the --target convention used by `gaia author` verbs so the whole package lifecycle runs from one place.')) -> None

Install a registered or LKM-backed Gaia knowledge package.

Resolves <package> against the gaia registry (default: SiliconEinstein/gaia-registry on GitHub), runs uv add on the resolved git+<repo>@<sha> spec, and best-effort downloads the upstream beliefs.json into .gaia/dep_beliefs/<import_name>.json so foreign-node priors flow into local inference. Must be run from within a Gaia knowledge package (pyproject.toml carrying a tool.gaia table).

LKM paper refs/flags fetch the paper graph, generate a local Gaia package under .gaia/lkm_packages/, compile it, and add it as an editable dependency with uv add --editable.

--version pins a specific release; omit to take the latest registered version.

Example:

.. code-block:: bash

gaia pkg add galileo-falling-bodies-gaia
gaia pkg add mendel-v0-5-gaia --version 0.1.0
gaia pkg add --lkm-index bohrium --lkm-paper 811827932371615744
gaia pkg add --lkm-index bohrium --lkm-claim gcn_579430355a0e4bbd
gaia pkg add lkm:bohrium:paper:811827932371615744
Source code in gaia/cli/commands/add.py
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
def add_command(
    package: str | None = typer.Argument(
        None,
        help=(
            "Package name (e.g., galileo-falling-bodies-gaia) or LKM ref "
            "(lkm:<index>:paper:<id> / lkm:<index>:claim:<id>)."
        ),
    ),
    version: str | None = typer.Option(None, "--version", "-v", help="Specific version"),
    registry: str = typer.Option(DEFAULT_REGISTRY, "--registry", help="Registry GitHub repo"),
    lkm_index: str = typer.Option(
        DEFAULT_LKM_INDEX_ID,
        "--lkm-index",
        "--lkm-server",
        help="Configured LKM index id for --lkm-paper / --lkm-claim.",
    ),
    lkm_paper: str | None = typer.Option(
        None,
        "--lkm-paper",
        help="Materialize this LKM paper id as a local Gaia package and add it.",
    ),
    lkm_claim: str | None = typer.Option(
        None,
        "--lkm-claim",
        help="Resolve this LKM claim id to its backing paper package.",
    ),
    target: str = typer.Option(
        ".",
        "--target",
        help=(
            "Path to the Gaia knowledge package to add the dependency to "
            "(default: cwd). Matches the --target convention used by "
            "`gaia author` verbs so the whole package lifecycle runs from "
            "one place."
        ),
    ),
) -> None:
    """Install a registered or LKM-backed Gaia knowledge package.

    Resolves ``<package>`` against the gaia registry (default:
    ``SiliconEinstein/gaia-registry`` on GitHub), runs ``uv add`` on the
    resolved ``git+<repo>@<sha>`` spec, and best-effort downloads the
    upstream ``beliefs.json`` into ``.gaia/dep_beliefs/<import_name>.json``
    so foreign-node priors flow into local inference. Must be run from
    within a Gaia knowledge package (``pyproject.toml`` carrying a
    ``tool.gaia`` table).

    LKM paper refs/flags fetch the paper graph, generate a local Gaia package
    under ``.gaia/lkm_packages/``, compile it, and add it as an editable
    dependency with ``uv add --editable``.

    ``--version`` pins a specific release; omit to take the latest
    registered version.

    Example:

    .. code-block:: bash

        gaia pkg add galileo-falling-bodies-gaia
        gaia pkg add mendel-v0-5-gaia --version 0.1.0
        gaia pkg add --lkm-index bohrium --lkm-paper 811827932371615744
        gaia pkg add --lkm-index bohrium --lkm-claim gcn_579430355a0e4bbd
        gaia pkg add lkm:bohrium:paper:811827932371615744
    """
    try:
        lkm_ref = _resolve_lkm_source_ref(
            package,
            lkm_index=lkm_index,
            lkm_paper=lkm_paper,
            lkm_claim=lkm_claim,
        )
    except GaiaPackagingError as exc:
        typer.echo(f"Error: {exc}", err=True)
        raise typer.Exit(4) from exc
    # Resolve the consumer package once, honoring --target (default cwd). The
    # same --target convention used by `gaia author` verbs / `gaia init` /
    # `gaia pkg scaffold` works here; --target . preserves the historical
    # "run from inside the package" behavior by walking up to the nearest root.
    package_root = _resolve_package_root(target)

    if lkm_ref is not None:
        _handle_lkm_source_add(lkm_ref, package_root=package_root)
        return
    if package is None:
        typer.echo("Error: pass PACKAGE or an LKM source flag.", err=True)
        raise typer.Exit(4)
    _warn_unused_lkm_index(lkm_index)

    try:
        resolved = resolve_package(package, version=version, registry=registry)
    except GaiaPackagingError as exc:
        typer.echo(str(exc), err=True)
        raise typer.Exit(1) from exc

    # Normalize: ensure -gaia suffix for the dep spec
    canonical_name = package if package.endswith("-gaia") else f"{package}-gaia"
    dep_spec = f"{canonical_name} @ git+{resolved.repo}@{resolved.git_sha}"
    typer.echo(f"Resolved {package} v{resolved.version}{resolved.git_sha[:8]}")

    # `uv add` runs in the resolved package root when one was found; otherwise
    # it falls back to the process cwd (matching the prior behavior for the
    # registry path, which uv resolves against the nearest project).
    uv_cwd = package_root if package_root is not None else None
    try:
        result = _run_uv(["uv", "add", dep_spec], cwd=uv_cwd)
    except GaiaPackagingError as exc:
        typer.echo(str(exc), err=True)
        raise typer.Exit(1) from exc
    if result.returncode != 0:
        stderr = result.stderr.strip() or result.stdout.strip()
        typer.echo(f"Error: uv add failed: {stderr}", err=True)
        raise typer.Exit(1)

    typer.echo(f"Added {package} v{resolved.version}")

    # Download upstream beliefs manifest for foreign-node prior injection.
    # This is best-effort: older registry entries may not have beliefs.json.
    if package_root is None:
        typer.echo("Note: not inside a Gaia package; skipping dep_beliefs download")
    else:
        _fetch_dep_beliefs(
            package_name=canonical_name.removesuffix("-gaia"),
            version=resolved.version,
            registry=registry,
            pkg_root=package_root,
        )

gaia.cli.commands.pkg.add_import

gaia pkg add-import — inject from <module> import <names> into a target file.

Plain-Python imports between sibling modules of a Gaia knowledge package are a precondition the author verbs can't handle on their own. When gaia author variable --value DOMINANT_COUNT is invoked against __init__.py, the cli has no way to know DOMINANT_COUNT lives in ./probabilities.py — the per-verb sibling_imports machinery short-circuits whenever the target file is __init__.py since a package can't re-import from itself.

This verb closes that gap with a small, explicit utility: it inserts from .<module> import <names> (or from <dotted.module> import <names>) into the target file, idempotently. Each name that already appears in any matching from line is silently skipped; everything new is folded into a single import line, alphabetically sorted, placed after the docstring + from __future__ block.

The verb covers the "plain Python data plumbing" gap surfaced when reproducing example packages whose sibling modules carry plain numeric constants (probabilities.py) or NamedTuple helpers — content that isn't DSL and so isn't author-verb material, yet still needs to be referenced from __init__.py.

add_import_command

add_import_command(from_: str = typer.Option(..., '--from', help="Module to import from. A bare identifier (`probabilities`) or leading-dot form (`.probabilities`) resolves against the target package's import name as `<import_name>.probabilities`. A dotted absolute form (`other_pkg.helpers`) is used verbatim."), names: str = typer.Option(..., '--names', help='Comma-separated Python identifiers to import. Each must be a valid identifier; duplicates and entries already imported are silently skipped.'), target: str = typer.Option('.', '--target', help='Path to the target Gaia package (default: cwd).'), file: str = typer.Option('__init__.py', '--file', help='Relative path under src/<import_name>/ to write into. Default: `__init__.py`. The file must already exist; use `gaia pkg add-module` first if you need to seed a fresh sibling.'), human: bool = typer.Option(False, '--human', help='Render the envelope in human-readable form instead of JSON.'), json_: bool = typer.Option(True, '--json/--no-json', help='JSON-first output (default; redundant for clarity).')) -> None

Inject from <module> import <names> into a Gaia package file.

Example:

.. code-block:: bash

gaia pkg add-import --from probabilities \
    --names DOMINANT_COUNT,RECESSIVE_COUNT,TOTAL_COUNT
Source code in gaia/cli/commands/pkg/add_import.py
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
def add_import_command(
    from_: str = typer.Option(
        ...,
        "--from",
        help=(
            "Module to import from. A bare identifier (`probabilities`) or "
            "leading-dot form (`.probabilities`) resolves against the target "
            "package's import name as `<import_name>.probabilities`. A dotted "
            "absolute form (`other_pkg.helpers`) is used verbatim."
        ),
    ),
    names: str = typer.Option(
        ...,
        "--names",
        help=(
            "Comma-separated Python identifiers to import. Each must be a "
            "valid identifier; duplicates and entries already imported are "
            "silently skipped."
        ),
    ),
    target: str = typer.Option(
        ".", "--target", help="Path to the target Gaia package (default: cwd)."
    ),
    file: str = typer.Option(
        "__init__.py",
        "--file",
        help=(
            "Relative path under src/<import_name>/ to write into. Default: "
            "`__init__.py`. The file must already exist; use `gaia pkg "
            "add-module` first if you need to seed a fresh sibling."
        ),
    ),
    human: bool = typer.Option(
        False, "--human", help="Render the envelope in human-readable form instead of JSON."
    ),
    json_: bool = typer.Option(
        True, "--json/--no-json", help="JSON-first output (default; redundant for clarity)."
    ),
) -> None:
    r"""Inject ``from <module> import <names>`` into a Gaia package file.

    Example:

    .. code-block:: bash

        gaia pkg add-import --from probabilities \
            --names DOMINANT_COUNT,RECESSIVE_COUNT,TOTAL_COUNT
    """
    del json_

    target_root = Path(target).resolve()

    # Validate the names CSV before structural checks so syntax errors
    # short-circuit on the cheap path.
    raw_names = [item.strip() for item in names.split(",") if item.strip()]
    if not raw_names:
        emit(
            AuthorResult(
                verb="add_import",
                status="error",
                code=EXIT_INPUT_SYNTAX,
                payload={"target": str(target_root)},
                diagnostics=[
                    Diagnostic(
                        kind="prewrite.syntax",
                        level="error",
                        message="--names must list at least one identifier",
                        source="prewrite",
                    )
                ],
            ),
            human=human,
        )
        return
    for n in raw_names:
        if not _is_valid_identifier(n):
            emit(
                AuthorResult(
                    verb="add_import",
                    status="error",
                    code=EXIT_INPUT_SYNTAX,
                    payload={"target": str(target_root)},
                    diagnostics=[
                        Diagnostic(
                            kind="prewrite.syntax",
                            level="error",
                            message=(f"--names entry {n!r} is not a valid Python identifier"),
                            source="prewrite",
                        )
                    ],
                ),
                human=human,
            )
            return

    # Pre-write target-structure invariant (a) — reuse the prewrite_check
    # probe so we share the toml validation + source-root discovery with
    # the rest of the family.
    probe_op = ProposedAuthorOp(
        verb="add_import",
        kind="scaffold",
        label=None,
        references=[],
        generated_code="pass\n",
        required_imports=(),
    )
    pre = prewrite_check(target_root, probe_op)
    if not pre.ok:
        emit(
            AuthorResult(
                verb="add_import",
                status="error",
                code=pre.exit_code,
                payload={"target": str(target_root)},
                diagnostics=pre.diagnostics,
            ),
            human=human,
        )
        return

    assert pre.source_root is not None
    source_root = pre.source_root
    import_name = pre.import_name or ""

    resolved_module, resolve_err = _resolve_module(from_, default_package=import_name or None)
    if resolve_err is not None:
        emit(
            AuthorResult(
                verb="add_import",
                status="error",
                code=EXIT_INPUT_SYNTAX,
                payload={"target": str(target_root)},
                diagnostics=[
                    Diagnostic(
                        kind="prewrite.syntax",
                        level="error",
                        message=resolve_err,
                        source="prewrite",
                    )
                ],
            ),
            human=human,
        )
        return
    assert resolved_module is not None

    target_file_rel = file.strip().lstrip("./")
    if not target_file_rel:
        target_file_rel = "__init__.py"
    if not target_file_rel.endswith(".py"):
        target_file_rel = f"{target_file_rel}.py"
    target_path = source_root / target_file_rel
    if not target_path.exists():
        emit(
            AuthorResult(
                verb="add_import",
                status="error",
                code=EXIT_PREWRITE_STRUCTURAL,
                payload={
                    "target": str(target_root),
                    "file": str(target_path),
                },
                diagnostics=[
                    Diagnostic(
                        kind="prewrite.target_invalid",
                        level="error",
                        message=(
                            f"target file {target_path} does not exist; run "
                            "`gaia pkg add-module --name <name>` first to "
                            "scaffold a sibling module"
                        ),
                        source="prewrite",
                    )
                ],
            ),
            human=human,
        )
        return

    needed: tuple[tuple[str, str], ...] = tuple((n, resolved_module) for n in raw_names)
    try:
        source = target_path.read_text()
    except OSError as exc:
        emit(
            AuthorResult(
                verb="add_import",
                status="error",
                code=EXIT_SYSTEM_IO,
                payload={"target": str(target_root), "file": str(target_path)},
                diagnostics=[
                    Diagnostic(
                        kind="prewrite.target_invalid",
                        level="error",
                        message=f"failed to read target file {target_path}: {exc}",
                        source="prewrite",
                    )
                ],
            ),
            human=human,
        )
        return

    new_source, added = _ensure_sibling_imports(
        source,
        needed,
        default_package=None,  # all entries carry a non-empty package explicitly
    )
    if added:
        try:
            target_path.write_text(new_source)
        except OSError as exc:
            emit(
                AuthorResult(
                    verb="add_import",
                    status="error",
                    code=EXIT_SYSTEM_IO,
                    payload={"target": str(target_root), "file": str(target_path)},
                    diagnostics=[
                        Diagnostic(
                            kind="prewrite.target_invalid",
                            level="error",
                            message=f"failed to write target file {target_path}: {exc}",
                            source="prewrite",
                        )
                    ],
                ),
                human=human,
            )
            return

    payload: dict[str, Any] = {
        "target": str(target_root),
        "file": str(target_path),
        "import_name": import_name,
        "from": resolved_module,
        "names_requested": list(raw_names),
        "names_added": list(added),
        "names_already_present": [n for n in raw_names if n not in added],
    }
    emit(
        AuthorResult(
            verb="add_import",
            status="ok",
            code=EXIT_OK,
            payload=payload,
        ),
        human=human,
    )

gaia.cli.commands.pkg.add_module

gaia pkg add-module — scaffold a fresh sibling Python module.

Sibling files in a Gaia knowledge package (e.g. priors.py, probabilities.py) need a precondition step before gaia author <verb> --file <relative> can write into them. This verb fills the gap by:

  1. Validating the target package is a Gaia knowledge package (via the existing pre-write invariant (a) machinery).
  2. Creating src/<import_name>/<module>.py with a minimal header and an empty __all__ list.
  3. Optionally seeding from gaia.engine.lang import <verbs> based on the --imports flag (CSV).

The verb emits the same uniform JSON envelope as the rest of the gaia author / gaia pkg scaffold family so an agent consumer can chain it into authoring pipelines.

Why a separate sub-verb instead of letting the author verbs auto-create their target file? Two reasons:

  • Pre-write hygiene. Author verbs assume the target file already exists so the (c) collision-and-reference scan has stable inputs; letting them silently mint a fresh file would diverge their pre-write invariants.

  • Explicit intent. Adding a sibling module is a structural choice about package organisation. Splitting it from the per-statement verb keeps the agent's actions auditable.

add_module_command

add_module_command(name: str = typer.Option(..., '--name', help='Module name relative to the package source root. Accepts a bare identifier (`priors`) or a filename (`priors.py`). Required.'), target: str = typer.Option('.', '--target', help='Path to the target Gaia package (default: cwd).'), imports: str | None = typer.Option(None, '--imports', help="Comma-separated DSL verbs to seed the module's imports (e.g. `register_prior,note`). Default: no imports beyond `from __future__ import annotations`."), docstring: str | None = typer.Option(None, '--docstring', help='Module docstring for the generated sibling file. Wrapped in triple quotes at line 1. Default: no docstring.'), human: bool = typer.Option(False, '--human', help='Render the envelope in human-readable form instead of JSON.'), json_: bool = typer.Option(True, '--json/--no-json', help='JSON-first output (default; redundant for clarity).')) -> None

Scaffold a sibling Python module under src/<import_name>/authored/.

The module is created inside the composed authored/ submodule so a later gaia author <verb> --file <name>.py (which routes into authored/) can write into it.

Example:

.. code-block:: bash

gaia pkg add-module --name priors --imports register_prior \
    --target ./mypkg-gaia --docstring "Priors module."
Source code in gaia/cli/commands/pkg/add_module.py
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
def add_module_command(
    name: str = typer.Option(
        ...,
        "--name",
        help=(
            "Module name relative to the package source root. Accepts a bare "
            "identifier (`priors`) or a filename (`priors.py`). Required."
        ),
    ),
    target: str = typer.Option(
        ".", "--target", help="Path to the target Gaia package (default: cwd)."
    ),
    imports: str | None = typer.Option(
        None,
        "--imports",
        help=(
            "Comma-separated DSL verbs to seed the module's imports "
            "(e.g. `register_prior,note`). Default: no imports beyond "
            "`from __future__ import annotations`."
        ),
    ),
    docstring: str | None = typer.Option(
        None,
        "--docstring",
        help=(
            "Module docstring for the generated sibling file. Wrapped in "
            "triple quotes at line 1. Default: no docstring."
        ),
    ),
    human: bool = typer.Option(
        False, "--human", help="Render the envelope in human-readable form instead of JSON."
    ),
    json_: bool = typer.Option(
        True, "--json/--no-json", help="JSON-first output (default; redundant for clarity)."
    ),
) -> None:
    r"""Scaffold a sibling Python module under ``src/<import_name>/authored/``.

    The module is created inside the composed ``authored/`` submodule so
    a later ``gaia author <verb> --file <name>.py`` (which routes into
    ``authored/``) can write into it.

    Example:

    .. code-block:: bash

        gaia pkg add-module --name priors --imports register_prior \
            --target ./mypkg-gaia --docstring "Priors module."
    """
    del json_

    target_root = Path(target).resolve()
    module_name = name[:-3] if name.endswith(".py") else name
    pre_errors = _validate_inputs(target=target_root, module_name=module_name)
    if pre_errors:
        first = pre_errors[0]
        result = AuthorResult(
            verb="add_module",
            status="error",
            code=EXIT_INPUT_SYNTAX,
            payload={"target": str(target_root), "module_name": module_name},
            diagnostics=list(pre_errors),
        )
        del first
        emit(result, human=human)
        return

    # Run the pre-write target-structure invariant (a) by constructing a
    # throwaway no-op proposed op against the entrypoint. The
    # ``prewrite_check`` already encapsulates the toml validation +
    # source-root discovery; reusing it avoids drift.
    probe_op = ProposedAuthorOp(
        verb="add_module",
        kind="scaffold",
        label=None,
        references=[],
        generated_code="pass\n",
        required_imports=(),
    )
    pre = prewrite_check(target_root, probe_op)
    if not pre.ok:
        # Hand-pick the structural errors (we know the probe statement
        # itself is syntactically clean).
        result = AuthorResult(
            verb="add_module",
            status="error",
            code=pre.exit_code,
            payload={"target": str(target_root)},
            diagnostics=pre.diagnostics,
        )
        emit(result, human=human)
        return

    assert pre.source_root is not None
    assert pre.source_init_path is not None
    source_root = pre.source_root
    import_name = pre.import_name or ""

    # Sibling modules for CLI-authored statements live inside the composed
    # ``authored/`` submodule (canon), so ``gaia author <verb>
    # --file <name>.py`` (which routes to ``authored/<name>.py``) finds the
    # module this verb created. Ensure the submodule exists first.
    authored_init = ensure_authored_submodule(source_root, pre.source_init_path)
    module_path = authored_init.parent / f"{module_name}.py"
    if module_path.exists():
        result = AuthorResult(
            verb="add_module",
            status="error",
            code=EXIT_PREWRITE_STRUCTURAL,
            payload={
                "target": str(target_root),
                "module_path": str(module_path),
            },
            diagnostics=[
                Diagnostic(
                    kind="prewrite.collision",
                    level="error",
                    message=(
                        f"module {module_name!r} already exists at {module_path}; "
                        "delete it or pick a different --name"
                    ),
                    source="prewrite",
                )
            ],
        )
        emit(result, human=human)
        return

    imports_tuple: tuple[str, ...] = tuple(
        item.strip() for item in (imports or "").split(",") if item.strip()
    )

    try:
        module_path.write_text(_build_module_text(imports=imports_tuple, docstring=docstring))
    except (OSError, PermissionError) as exc:
        result = AuthorResult(
            verb="add_module",
            status="error",
            code=EXIT_SYSTEM_IO,
            payload={"target": str(target_root)},
            diagnostics=[
                Diagnostic(
                    kind="prewrite.target_invalid",
                    level="error",
                    message=f"failed to write module {module_path}: {exc}",
                    source="prewrite",
                )
            ],
        )
        emit(result, human=human)
        return

    payload: dict[str, Any] = {
        "target": str(target_root),
        "module_name": module_name,
        "module_path": str(module_path),
        "relative_path": f"{module_name}.py",
        "import_name": import_name,
        "imports": list(imports_tuple),
    }
    result = AuthorResult(
        verb="add_module",
        status="ok",
        code=EXIT_OK,
        payload=payload,
    )
    emit(result, human=human)

gaia.cli.commands.register

gaia pkg register -- prepare or submit a registry registration for a Gaia package.

register_command

register_command(path: str = typer.Argument('.', help='Path to knowledge package directory'), tag: str | None = typer.Option(None, help='Git tag to register. Defaults to v<version>.'), repo: str | None = typer.Option(None, help='GitHub repository URL. Defaults to the git origin remote.'), registry_dir: str | None = typer.Option(None, help='Path to a local checkout of the official registry repository.'), registry_repo: str = typer.Option('SiliconEinstein/gaia-registry', help='Registry GitHub repo slug for PR creation.'), create_pr: bool = typer.Option(False, help='Push the registry branch and open a GitHub PR.')) -> None

Prepare or submit a registration for a tagged GitHub-backed Gaia package.

Reads the freshly-compiled package, validates git state (clean worktree, tag pointing at HEAD, tag pushed to origin), builds the registry-side Package.toml / Versions.toml / Deps.toml + per-release manifests + beliefs.json, and either prints the registration plan as JSON (default) or writes it into a local checkout of the registry repo and opens a PR.

Preconditions checked: [project].name ends with -gaia, [tool.gaia].uuid set to a valid UUID, .gaia/ir_hash matches the freshly-compiled IR (run gaia build compile first), git worktree clean, tag v<version> (or --tag) on HEAD and pushed.

Example:

.. code-block:: bash

# Dry run — print the registration plan as JSON:
gaia pkg register .

# Write a registry branch into a local checkout and open a PR:
gaia pkg register . --registry-dir ~/dev/gaia-registry --create-pr
Source code in gaia/cli/commands/register.py
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
def register_command(
    path: str = typer.Argument(".", help="Path to knowledge package directory"),
    tag: str | None = typer.Option(None, help="Git tag to register. Defaults to v<version>."),
    repo: str | None = typer.Option(
        None, help="GitHub repository URL. Defaults to the git origin remote."
    ),
    registry_dir: str | None = typer.Option(
        None, help="Path to a local checkout of the official registry repository."
    ),
    registry_repo: str = typer.Option(
        "SiliconEinstein/gaia-registry", help="Registry GitHub repo slug for PR creation."
    ),
    create_pr: bool = typer.Option(False, help="Push the registry branch and open a GitHub PR."),
) -> None:
    """Prepare or submit a registration for a tagged GitHub-backed Gaia package.

    Reads the freshly-compiled package, validates git state (clean
    worktree, tag pointing at HEAD, tag pushed to origin), builds the
    registry-side ``Package.toml`` / ``Versions.toml`` / ``Deps.toml`` +
    per-release manifests + ``beliefs.json``, and either prints the
    registration plan as JSON (default) or writes it into a local
    checkout of the registry repo and opens a PR.

    Preconditions checked: ``[project].name`` ends with ``-gaia``,
    ``[tool.gaia].uuid`` set to a valid UUID, ``.gaia/ir_hash`` matches
    the freshly-compiled IR (run ``gaia build compile`` first), git
    worktree clean, tag ``v<version>`` (or ``--tag``) on HEAD and
    pushed.

    Example:

    .. code-block:: bash

        # Dry run — print the registration plan as JSON:
        gaia pkg register .

        # Write a registry branch into a local checkout and open a PR:
        gaia pkg register . --registry-dir ~/dev/gaia-registry --create-pr
    """
    loaded, compiled, ir, manifests = _load_registration_artifacts(path)
    _validate_registration_ir(ir)
    gaia_uuid, _dependencies = _validate_registration_package_config(loaded, ir)
    package_name = loaded.project_name.removesuffix("-gaia")
    version = loaded.project_config["version"]
    tag_name = tag or f"v{version}"
    repo_url, tag_sha = _validated_registration_git_state(loaded, tag_name=tag_name, repo=repo)
    plan, deps, versions, _deps_payload = _build_registration_plan(
        loaded=loaded,
        compiled=compiled,
        ir=ir,
        manifests=manifests,
        gaia_uuid=gaia_uuid,
        tag_name=tag_name,
        tag_sha=tag_sha,
        repo_url=repo_url,
        registry_repo=registry_repo,
    )

    if registry_dir is None:
        typer.echo(json.dumps(plan, ensure_ascii=False, indent=2, sort_keys=True))
        return

    registry_path, package_dir, release_path, versions_toml_path, deps_toml_path, branch_name = (
        _validate_registry_target(registry_dir, package_name, version, gaia_uuid)
    )
    _write_registry_registration(
        registry_path=registry_path,
        package_dir=package_dir,
        release_path=release_path,
        versions_toml_path=versions_toml_path,
        deps_toml_path=deps_toml_path,
        branch_name=branch_name,
        loaded=loaded,
        package_name=package_name,
        version=version,
        plan=plan,
        deps=deps,
        versions=versions,
    )
    typer.echo(f"Prepared registry branch: {branch_name}")
    typer.echo(f"Updated metadata under: {package_dir}")
    _maybe_create_registry_pr(
        registry_path=registry_path,
        branch_name=branch_name,
        registry_repo=registry_repo,
        pr_title=plan["pull_request"]["title"],
        pr_body=plan["pull_request"]["body"],
        create_pr=create_pr,
    )

gaia.cli.commands.pkg.scaffold

gaia pkg scaffold — initialise a fresh Gaia knowledge package.

Maps the agent-first authoring story to package initialisation: given a target directory and a package name, lay down the minimal layout that the rest of the gaia author cycle needs (pyproject.toml with [tool.gaia] block, src/<import_name>/__init__.py template, .gaia/ artifact directory).

Why a new verb instead of reusing the legacy gaia init / gaia build init? Two reasons:

  1. Agent-first envelope. gaia init writes human-oriented text to stdout and shells out to uv; gaia pkg scaffold emits the same uniform {status, code, verb, payload, warnings, diagnostics} shape the gaia author <verb> family does, so an LLM agent can parse one envelope schema across the whole package lifecycle.
  2. Independent pre-validation surface. scaffold knows about -gaia naming, namespace defaults, and --check integration that runs gaia build check against the freshly created package.

The scaffold verb writes its own pre-validation pipeline (target directory absence + name ending + import-name validity) rather than reusing :mod:gaia.cli.commands.author._prewrite — its 4 invariants assume a pre-existing Gaia package, while scaffold creates one from scratch. The JSON envelope is shared verbatim.

scaffold_command

scaffold_command(target: str = typer.Option(..., '--target', help='Path to the directory to initialise (must be empty or non-existent). Author verbs are run from outside the package, with --target <path>.'), name: str | None = typer.Option(None, '--name', help="Package name (must end with '-gaia'); defaults to target dir name."), namespace: str | None = typer.Option(None, '--namespace', help='Package namespace; defaults to the import name.'), description: str | None = typer.Option(None, '--description', help='Short description for pyproject.toml.'), with_uuid: bool = typer.Option(False, '--with-uuid', help='Generate a [tool.gaia].uuid for the package. Default is to omit the field (matches the shipping example packages).'), docstring: str | None = typer.Option(None, '--docstring', help='Module docstring for the generated src/<import_name>/__init__.py. Wrapped in triple quotes at line 1. Default: no docstring.'), check: bool = typer.Option(False, '--check/--no-check', help='Run post-write `gaia build check` on the freshly created package. Default off — a fresh scaffold has no declarations yet, which the engine treats as an error. Re-run with `gaia build check` once author commands have added statements.'), human: bool = typer.Option(False, '--human', help='Render the envelope in human-readable form instead of JSON.'), interactive: bool = typer.Option(False, '--interactive', help='Prompt on pre-write warnings (no-op for scaffold — it emits no warnings).'), json_: bool = typer.Option(True, '--json/--no-json', help='JSON-first output (default; redundant for clarity).')) -> None

Scaffold a fresh Gaia knowledge package.

Example:

.. code-block:: bash

gaia pkg scaffold --target ./mypkg-gaia --name mypkg-gaia \
    --docstring "My package."
Source code in gaia/cli/commands/pkg/scaffold.py
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
def scaffold_command(
    target: str = typer.Option(
        ...,
        "--target",
        help=(
            "Path to the directory to initialise (must be empty or non-existent). "
            "Author verbs are run from outside the package, with --target <path>."
        ),
    ),
    name: str | None = typer.Option(
        None,
        "--name",
        help="Package name (must end with '-gaia'); defaults to target dir name.",
    ),
    namespace: str | None = typer.Option(
        None,
        "--namespace",
        help="Package namespace; defaults to the import name.",
    ),
    description: str | None = typer.Option(
        None, "--description", help="Short description for pyproject.toml."
    ),
    with_uuid: bool = typer.Option(
        False,
        "--with-uuid",
        help=(
            "Generate a [tool.gaia].uuid for the package. Default is to omit "
            "the field (matches the shipping example packages)."
        ),
    ),
    docstring: str | None = typer.Option(
        None,
        "--docstring",
        help=(
            "Module docstring for the generated src/<import_name>/__init__.py. "
            "Wrapped in triple quotes at line 1. Default: no docstring."
        ),
    ),
    check: bool = typer.Option(
        False,
        "--check/--no-check",
        help=(
            "Run post-write `gaia build check` on the freshly created package. "
            "Default off — a fresh scaffold has no declarations yet, which the "
            "engine treats as an error. Re-run with `gaia build check` once "
            "author commands have added statements."
        ),
    ),
    human: bool = typer.Option(
        False, "--human", help="Render the envelope in human-readable form instead of JSON."
    ),
    interactive: bool = typer.Option(
        False,
        "--interactive",
        help="Prompt on pre-write warnings (no-op for scaffold — it emits no warnings).",
    ),
    json_: bool = typer.Option(
        True, "--json/--no-json", help="JSON-first output (default; redundant for clarity)."
    ),
) -> None:
    r"""Scaffold a fresh Gaia knowledge package.

    Example:

    .. code-block:: bash

        gaia pkg scaffold --target ./mypkg-gaia --name mypkg-gaia \
            --docstring "My package."
    """
    del json_, interactive  # see helper-doc rationale

    target_root = Path(target).resolve()

    plan, pre_diagnostics = _validate_inputs(
        target=target_root,
        name=name,
        namespace=namespace,
        description=description,
        with_uuid=with_uuid,
        docstring=docstring,
    )
    if plan is None:
        # Pick semantic exit code from the first diagnostic kind.
        first = pre_diagnostics[0]
        code = {
            "prewrite.target_not_gaia_package": EXIT_SYSTEM_IO,
            "prewrite.target_invalid": EXIT_SYSTEM_IO,
            "prewrite.collision": EXIT_INPUT_SYNTAX,
        }.get(first.kind, EXIT_PREWRITE_STRUCTURAL)
        result = AuthorResult(
            verb="scaffold",
            status="error",
            code=code,
            payload={"target": str(target_root)},
            diagnostics=pre_diagnostics,
        )
        emit(result, human=human)
        return

    try:
        created = _scaffold_layout(plan)
    except (OSError, PermissionError) as exc:
        result = AuthorResult(
            verb="scaffold",
            status="error",
            code=EXIT_SYSTEM_IO,
            payload={"target": str(target_root)},
            diagnostics=[
                Diagnostic(
                    kind="prewrite.target_invalid",
                    level="error",
                    message=f"failed to lay out scaffold under {target_root}: {exc}",
                    source="prewrite",
                    where={"target": str(target_root)},
                )
            ],
        )
        emit(result, human=human)
        return

    post_diagnostics: list[Diagnostic] = []
    post_warnings: list[Diagnostic] = []
    counts: dict[str, int] | None = None
    if check:
        post = postwrite_check(target_root)
        post_diagnostics.extend(post.diagnostics)
        post_warnings.extend(post.warnings)
        if post.ok:
            counts = {
                "knowledge_count": post.knowledge_count,
                "strategy_count": post.strategy_count,
                "operator_count": post.operator_count,
            }

    _emit_scaffold_envelope(
        plan=plan,
        created=created,
        post_diagnostics=post_diagnostics,
        post_warnings=post_warnings,
        counts=counts,
        human=human,
    )
    if not post_diagnostics and human:
        typer.echo("\n" + _next_steps_text(plan))