vinv 0.0.2__py3-none-win_amd64.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 (143) hide show
  1. bringup/__init__.py +29 -0
  2. bringup/cli.py +147 -0
  3. bringup/invocation_render.py +191 -0
  4. bringup/prompts/list_instruction.txt +290 -0
  5. bringup/prompts/list_instruction_portable.txt +185 -0
  6. bringup/prompts/otel_pin_block.txt +47 -0
  7. bringup/prompts/start_instruction.txt +1028 -0
  8. bringup/prompts/start_instruction_portable.txt +392 -0
  9. bringup/prompts/tracelens_install_editable.txt +55 -0
  10. bringup/prompts/tracelens_install_missing.txt +7 -0
  11. bringup/runner.py +3429 -0
  12. exerciser/__init__.py +15 -0
  13. exerciser/_worker.py +191 -0
  14. exerciser/agent_loop.py +203 -0
  15. exerciser/bandit.py +449 -0
  16. exerciser/baseline.py +187 -0
  17. exerciser/campaign.py +1178 -0
  18. exerciser/cli.py +750 -0
  19. exerciser/compaction.py +288 -0
  20. exerciser/concurrency.py +411 -0
  21. exerciser/containment.py +857 -0
  22. exerciser/coverage.py +343 -0
  23. exerciser/differential.py +1148 -0
  24. exerciser/envconfig.py +817 -0
  25. exerciser/environment.py +436 -0
  26. exerciser/exception_policy.py +1038 -0
  27. exerciser/execute.py +212 -0
  28. exerciser/faults.py +1084 -0
  29. exerciser/functions.py +4247 -0
  30. exerciser/generators.py +82 -0
  31. exerciser/interpreter.py +842 -0
  32. exerciser/invariants.py +384 -0
  33. exerciser/invocation_render.py +191 -0
  34. exerciser/invocations.py +515 -0
  35. exerciser/issues.py +242 -0
  36. exerciser/openapi.py +417 -0
  37. exerciser/optimize.py +486 -0
  38. exerciser/plan.py +458 -0
  39. exerciser/probe.py +107 -0
  40. exerciser/profile.py +341 -0
  41. exerciser/redact.py +295 -0
  42. exerciser/regress.py +397 -0
  43. exerciser/run.py +1061 -0
  44. exerciser/sandbox.py +1981 -0
  45. exerciser/scenario.py +179 -0
  46. exerciser/schema.py +227 -0
  47. exerciser/scorecard.py +273 -0
  48. exerciser/semantics_corpus.py +4141 -0
  49. exerciser/service_doubles.py +2302 -0
  50. exerciser/services.py +555 -0
  51. exerciser/state.py +251 -0
  52. exerciser/store.py +255 -0
  53. exerciser/throughput.py +193 -0
  54. exerciser/tracing.py +241 -0
  55. exerciser/usl.py +155 -0
  56. goal/__init__.py +17 -0
  57. goal/agents.py +206 -0
  58. goal/cli.py +192 -0
  59. goal/runner.py +70 -0
  60. handbook/__init__.py +18 -0
  61. handbook/cli.py +63 -0
  62. handbook/generator.py +59 -0
  63. handbook/prompts/documentation_instruction.txt +316 -0
  64. handbook/prompts/documentation_instruction_portable.txt +137 -0
  65. identification/__init__.py +39 -0
  66. identification/cli.py +352 -0
  67. identification/runner.py +2940 -0
  68. identification/store.py +254 -0
  69. lens_contracts/__init__.py +25 -0
  70. lens_contracts/code_version.py +28 -0
  71. lens_contracts/determinism_boundary_record.py +88 -0
  72. lens_contracts/event_header.py +24 -0
  73. lens_contracts/external_call_event.py +42 -0
  74. lens_contracts/replay_corpus_row.py +64 -0
  75. lens_contracts/sample_event.py +29 -0
  76. lens_contracts/schemas/__init__.py +7 -0
  77. lens_contracts/schemas/determinism_boundary_record.schema.json +237 -0
  78. lens_contracts/schemas/external_call_event.schema.json +288 -0
  79. lens_contracts/schemas/replay_corpus_row.schema.json +303 -0
  80. lens_contracts/schemas/sample_event.schema.json +238 -0
  81. lens_contracts/schemas/span_event.schema.json +358 -0
  82. lens_contracts/span_event.py +97 -0
  83. lens_contracts/tools/__init__.py +0 -0
  84. lens_contracts/tools/gen_schemas.py +66 -0
  85. tracelens/__init__.py +5 -0
  86. tracelens/_health.py +68 -0
  87. tracelens/analysis/__init__.py +1 -0
  88. tracelens/analysis/circa.py +165 -0
  89. tracelens/analysis/clustering.py +111 -0
  90. tracelens/analysis/code_overlay.py +98 -0
  91. tracelens/analysis/corpus.py +71 -0
  92. tracelens/analysis/correctness.py +191 -0
  93. tracelens/analysis/depgraph.py +35 -0
  94. tracelens/analysis/dynamic_static_diff.py +82 -0
  95. tracelens/analysis/gcm_rca.py +190 -0
  96. tracelens/analysis/metrics.py +70 -0
  97. tracelens/analysis/report.py +1203 -0
  98. tracelens/analysis/scip_acquisition.py +55 -0
  99. tracelens/analysis/spans.py +112 -0
  100. tracelens/analysis/symbol_stats.py +76 -0
  101. tracelens/cli.py +368 -0
  102. tracelens/context.py +48 -0
  103. tracelens/enrich/__init__.py +4 -0
  104. tracelens/enrich/external_invariants.py +197 -0
  105. tracelens/enrich/hashing.py +83 -0
  106. tracelens/enrich/invariants.py +22 -0
  107. tracelens/enrich/sizes.py +100 -0
  108. tracelens/enrich/summaries.py +101 -0
  109. tracelens/io/__init__.py +14 -0
  110. tracelens/io/event_reader.py +162 -0
  111. tracelens/launcher/__init__.py +1 -0
  112. tracelens/launcher/attach.py +13 -0
  113. tracelens/launcher/calibration.py +176 -0
  114. tracelens/launcher/child_bootstrap.py +258 -0
  115. tracelens/launcher/coverage_scan.py +230 -0
  116. tracelens/launcher/determinism_capture.py +190 -0
  117. tracelens/launcher/executor_context.py +135 -0
  118. tracelens/launcher/gc_events.py +181 -0
  119. tracelens/launcher/import_hook.py +513 -0
  120. tracelens/launcher/monitoring_hook.py +286 -0
  121. tracelens/launcher/otel_setup.py +262 -0
  122. tracelens/launcher/proxy.py +13 -0
  123. tracelens/launcher/run.py +1387 -0
  124. tracelens/launcher/summary.py +204 -0
  125. tracelens/launcher/targets.py +92 -0
  126. tracelens/otel/__init__.py +1 -0
  127. tracelens/otel/configurator.py +55 -0
  128. tracelens/otel/exporter.py +434 -0
  129. tracelens/otel/processor.py +348 -0
  130. tracelens/runtime/__init__.py +1 -0
  131. tracelens/runtime/trace_fn.py +210 -0
  132. vinv/__init__.py +3 -0
  133. vinv/_bin/index.exe +0 -0
  134. vinv/_index.py +41 -0
  135. vinv-0.0.2.dist-info/METADATA +142 -0
  136. vinv-0.0.2.dist-info/RECORD +143 -0
  137. vinv-0.0.2.dist-info/WHEEL +4 -0
  138. vinv-0.0.2.dist-info/entry_points.txt +10 -0
  139. vinv_embedder/__init__.py +11 -0
  140. vinv_embedder/cli.py +195 -0
  141. vinv_embedder/config.py +248 -0
  142. vinv_embedder/engine.py +468 -0
  143. vinv_embedder/server.py +368 -0
bringup/__init__.py ADDED
@@ -0,0 +1,29 @@
1
+ """bringup — standalone, two-stage Stage 2 bring-up runbook renderer.
2
+
3
+ Renders the runbooks a coding-agent harness executes off a repository's
4
+ discovery handbook (``<repo>/.vinv/vinv.md``), in two stages:
5
+
6
+ * ``render_list_prompt`` (Stage 2a) — the runbook that enumerates every
7
+ service and its modules into ``<repo>/.vinv/services.json``.
8
+ * ``render_start_prompt`` (Stage 2b) — the runbook that installs and starts
9
+ one selected service, wrapping a Python service with ``tracelens run`` so
10
+ traces land in ``~/.tracelens/baselines/<session>/<service>/trace.jsonl``.
11
+
12
+ Harness-only: no LLM calls are made in-process. The deterministic halves —
13
+ service-inventory validation and the replay verification gate — live in
14
+ ``bringup.runner``.
15
+ """
16
+
17
+ from bringup.runner import (
18
+ expect_vinv_handbook,
19
+ render_list_prompt,
20
+ render_start_prompt,
21
+ verify_replay,
22
+ )
23
+
24
+ __all__ = [
25
+ "expect_vinv_handbook",
26
+ "render_list_prompt",
27
+ "render_start_prompt",
28
+ "verify_replay",
29
+ ]
bringup/cli.py ADDED
@@ -0,0 +1,147 @@
1
+ """Click CLI for bringup: a two-stage Stage 2 bring-up driven by .vinv/vinv.md.
2
+
3
+ Stage 2a — ``bringup list <repo>`` : render the runbook that enumerates every
4
+ service into ``<repo>/.vinv/services.json``.
5
+ Stage 2b — ``bringup start <repo>`` : render the runbook that starts one selected
6
+ service (``--service``), instrumenting its
7
+ ``--module``(s) under tracelens.
8
+
9
+ Harness-only: both commands print the fully rendered task prompt (zero LLM
10
+ calls) for the caller's coding-agent harness to execute. The legacy in-process
11
+ agent mode is gone; ``--print-prompt`` is accepted as a no-op for backwards
12
+ compatibility.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ import sys
19
+ from pathlib import Path
20
+
21
+ import click
22
+
23
+ from bringup.runner import (
24
+ render_list_prompt,
25
+ render_start_prompt,
26
+ )
27
+
28
+
29
+ def _force_utf8_stdio() -> None:
30
+ """On Windows a piped stdout/stderr defaults to the ANSI codepage, and the
31
+ rendered prompts contain characters it cannot encode, so reconfigure the
32
+ streams in-process."""
33
+ for stream in (sys.stdout, sys.stderr):
34
+ try:
35
+ if (stream.encoding or "").replace("-", "").lower() != "utf8":
36
+ stream.reconfigure(encoding="utf-8")
37
+ except Exception:
38
+ pass
39
+
40
+
41
+ _force_utf8_stdio()
42
+
43
+
44
+ def _configure_logging(verbose: bool) -> None:
45
+ logging.basicConfig(
46
+ level=logging.INFO if verbose else logging.WARNING,
47
+ format="%(asctime)s %(levelname)s %(name)s: %(message)s",
48
+ stream=sys.stderr,
49
+ )
50
+
51
+
52
+ _GROUP_HELP = """\
53
+ bringup — render the runbooks that enumerate and start a repository's services.
54
+
55
+ \b
56
+ 1. bringup list <repo>
57
+ Print the runbook that writes a service inventory to
58
+ <repo>/.vinv/services.json. Installs nothing, starts nothing.
59
+ 2. bringup start <repo> --service NAME [--module PKG ...]
60
+ Print the runbook that installs, starts and verifies ONE selected
61
+ service and records the verified start command(s).
62
+
63
+ Both commands render the full task prompt (no LLM calls); pipe the output into
64
+ your coding-agent harness (Claude Code, Cursor, Windsurf, ...) to execute it.
65
+ Requires <repo>/.vinv/vinv.md to exist. Run `bringup list --help` or
66
+ `bringup start --help` for per-command options.
67
+ """
68
+
69
+ _LIST_HELP = """\
70
+ Print the runbook that lists every service and its modules into
71
+ REPO_PATH/.vinv/services.json.
72
+
73
+ The runbook installs nothing and starts nothing; its output feeds
74
+ `bringup start`. Requires REPO_PATH/.vinv/vinv.md to exist. Execute the
75
+ printed runbook with your coding-agent harness.
76
+ """
77
+
78
+ _START_HELP = """\
79
+ Print the runbook that installs, starts and verifies ONE selected service,
80
+ recording the verified start command(s) at
81
+ REPO_PATH/.vinv/start_commands/<service>.json.
82
+
83
+ Pass each package to instrument with a repeated --module flag. Requires
84
+ REPO_PATH/.vinv/vinv.md to exist. Execute the printed runbook with your
85
+ coding-agent harness.
86
+ """
87
+
88
+
89
+ @click.group(help=_GROUP_HELP)
90
+ def main() -> None:
91
+ pass
92
+
93
+
94
+ @main.command("list", help=_LIST_HELP)
95
+ @click.argument("repo_path", type=click.Path(exists=True, file_okay=False))
96
+ @click.option("--print-prompt", is_flag=True, hidden=True,
97
+ help="Deprecated no-op: printing the rendered prompt is the only mode.")
98
+ @click.option("--portable", is_flag=True,
99
+ help="Emit the tool-agnostic runbook variant (no Vinv-specific tool names) "
100
+ "for a foreign coding agent.")
101
+ @click.option("-v", "--verbose", is_flag=True, help="Enable INFO-level logging to stderr.")
102
+ def list_cmd(repo_path: str, print_prompt: bool, portable: bool, verbose: bool) -> None:
103
+ _configure_logging(verbose)
104
+ click.echo(render_list_prompt(Path(repo_path), portable=portable))
105
+
106
+
107
+ @main.command("start", help=_START_HELP)
108
+ @click.argument("repo_path", type=click.Path(exists=True, file_okay=False))
109
+ @click.option("--service", required=True,
110
+ help="Name of the service to bring up (from .vinv/services.json).")
111
+ @click.option("--module", "modules", multiple=True,
112
+ help="Top-level Python package to instrument under tracelens (repeatable).")
113
+ @click.option("--session-id", default="vinv-bringup",
114
+ help="Session id woven into tracelens output paths (default 'vinv-bringup').")
115
+ @click.option("--start-hint", default=None,
116
+ help="How YOU start this service (e.g. 'make run-api'). The runbook verifies it, "
117
+ "then records the tracelens-wrapped equivalent. Defaults to the hint recorded "
118
+ "at .vinv/start_hints/<service>.json; a hint never lowers the verified bar.")
119
+ @click.option("--print-prompt", is_flag=True, hidden=True,
120
+ help="Deprecated no-op: printing the rendered prompt is the only mode.")
121
+ @click.option("--portable", is_flag=True,
122
+ help="Emit the tool-agnostic runbook variant (no Vinv-specific tool names) "
123
+ "for a foreign coding agent.")
124
+ @click.option("-v", "--verbose", is_flag=True, help="Enable INFO-level logging to stderr.")
125
+ def start_cmd(
126
+ repo_path: str,
127
+ service: str,
128
+ modules: tuple[str, ...],
129
+ session_id: str,
130
+ start_hint: str | None,
131
+ print_prompt: bool,
132
+ portable: bool,
133
+ verbose: bool,
134
+ ) -> None:
135
+ _configure_logging(verbose)
136
+ click.echo(render_start_prompt(
137
+ Path(repo_path),
138
+ service=service,
139
+ modules=list(modules),
140
+ session_id=session_id,
141
+ portable=portable,
142
+ start_hint=start_hint,
143
+ ))
144
+
145
+
146
+ if __name__ == "__main__":
147
+ main()
@@ -0,0 +1,191 @@
1
+ """Filling an invocation's command template — the Python half of the contract.
2
+
3
+ A run-to-completion unit is driven many ways: a CLI has one invocation per
4
+ subcommand, a library one per entry point. Each is recorded as a *template* plus
5
+ the parameters that fill it, so the same record can serve a human filling a form
6
+ and a headless pass taking the declared defaults.
7
+
8
+ This module is duplicated VERBATIM in ``exerciser.invocation_render`` and
9
+ mirrored in TypeScript by ``extension/src/bringup/invocations.ts``. Duplication
10
+ rather than a shared import because ``bringup`` and ``exerciser`` deliberately do
11
+ not depend on each other (the field contract is the file on disk, not the code
12
+ that wrote it) — and the drift that invites is caught by
13
+ ``contracts/vectors/invocation_render.json``, which every one of the three suites
14
+ reads. A change made on one side and not the others fails there instead of
15
+ quietly producing a different command in each surface.
16
+
17
+ The invariant worth stating plainly: rendering an invocation with all of its
18
+ defaults must reproduce, byte for byte, the string bring-up actually ran. That is
19
+ what keeps ``verified: true`` meaning something after parameters exist — see
20
+ :func:`defaults_match_verified`.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import re
26
+ from typing import Any
27
+
28
+ #: An ordinary argv token needs no quoting. Leaving these bare is what keeps the
29
+ #: defaults render byte-identical to the verified string — quoting everything
30
+ #: would be equally safe and would break the identity check on every record.
31
+ _SAFE_TOKEN = re.compile(r"^[A-Za-z0-9_@%+=:,./-]+$")
32
+
33
+ #: `C:\x` / `C:/x` → the `/c/x` spelling Git Bash reads. Keyed off the SHAPE of
34
+ #: the value rather than the host platform, so the shared vectors give the same
35
+ #: answer on every OS.
36
+ _DRIVE = re.compile(r"^([A-Za-z]):[\\/](.*)$")
37
+
38
+ #: An escape (`{{` / `}}`) or a placeholder, in one pass.
39
+ _TOKEN = re.compile(r"\{\{|\}\}|\{([A-Za-z_][A-Za-z0-9_]*)\}")
40
+
41
+ #: Values a `flag` parameter treats as "on". Anything else omits the flag.
42
+ _TRUTHY = frozenset({"1", "true", "yes", "on"})
43
+
44
+
45
+ class InvocationRenderError(ValueError):
46
+ """A template and its parameters disagree — never rendered past."""
47
+
48
+
49
+ def shell_quote(value: str) -> str:
50
+ """Quote one value for the ``bash -lc`` the recorded command runs under."""
51
+ if _SAFE_TOKEN.match(value):
52
+ return value
53
+ return "'" + value.replace("'", "'\\''") + "'"
54
+
55
+
56
+ def to_bash_path(value: str) -> str:
57
+ """Rewrite a Windows drive-letter path into the ``/c/…`` spelling.
58
+
59
+ Recorded commands are bash-spelled by contract: a ``C:\\…`` value arrives at
60
+ the program with its backslashes eaten as escapes, and a ``C:/…`` value has
61
+ its colon read as a PATH separator.
62
+ """
63
+ m = _DRIVE.match(value)
64
+ if m:
65
+ return "/" + m.group(1).lower() + "/" + m.group(2).replace("\\", "/")
66
+ return value.replace("\\", "/")
67
+
68
+
69
+ def _substitute(param: dict[str, Any], raw: str) -> str:
70
+ """The text one parameter contributes, already quoted. ``""`` means omit."""
71
+ ptype = param.get("type")
72
+ value = to_bash_path(raw.strip()) if ptype == "path" else raw.strip()
73
+ name = str(param.get("name") or "")
74
+ render = param.get("render")
75
+
76
+ if ptype == "flag":
77
+ if value.lower() not in _TRUTHY:
78
+ return ""
79
+ return render.replace("{value}", "").strip() if isinstance(render, str) else f"--{name}"
80
+
81
+ if value == "":
82
+ if param.get("required"):
83
+ raise InvocationRenderError(f"'{name}' is required but empty")
84
+ return ""
85
+
86
+ choices = param.get("choices")
87
+ if ptype == "enum" and isinstance(choices, list) and choices:
88
+ if value not in choices:
89
+ raise InvocationRenderError(
90
+ f"'{name}' must be one of {', '.join(str(c) for c in choices)} (got '{value}')"
91
+ )
92
+ if ptype == "int" and not re.fullmatch(r"-?\d+", value):
93
+ raise InvocationRenderError(f"'{name}' must be a whole number (got '{value}')")
94
+ if ptype == "float" and not re.fullmatch(r"-?\d+(\.\d+)?", value):
95
+ raise InvocationRenderError(f"'{name}' must be a number (got '{value}')")
96
+
97
+ quoted = shell_quote(value)
98
+ return render.replace("{value}", quoted) if isinstance(render, str) else quoted
99
+
100
+
101
+ def render_invocation(invocation: dict[str, Any], args: dict[str, str] | None = None) -> str:
102
+ """Fill ``invocation``'s template, falling back to each parameter's default.
103
+
104
+ Raises rather than guessing: an unknown placeholder, or a declared parameter
105
+ the template never uses, is a malformed record — rendering past it would
106
+ produce a command nobody verified.
107
+ """
108
+ args = args or {}
109
+ raw_params = invocation.get("params")
110
+ params: dict[str, dict[str, Any]] = {}
111
+ if isinstance(raw_params, list):
112
+ for p in raw_params:
113
+ if isinstance(p, dict) and isinstance(p.get("name"), str):
114
+ params[p["name"]] = p
115
+ command = invocation.get("command")
116
+ if not isinstance(command, str):
117
+ raise InvocationRenderError("invocation has no command string")
118
+ inv_id = invocation.get("id") or "?"
119
+
120
+ used: set[str] = set()
121
+ out: list[str] = []
122
+ last = 0
123
+ for m in _TOKEN.finditer(command):
124
+ out.append(command[last : m.start()])
125
+ last = m.end()
126
+ if m.group(0) in ("{{", "}}"):
127
+ out.append(m.group(0)[0])
128
+ continue
129
+ name = m.group(1)
130
+ param = params.get(name)
131
+ if param is None:
132
+ raise InvocationRenderError(
133
+ f"command uses {{{name}}} but no such parameter is declared on "
134
+ f"invocation '{inv_id}'"
135
+ )
136
+ used.add(name)
137
+ supplied = args.get(name)
138
+ if supplied is None:
139
+ supplied = param.get("default")
140
+ text = _substitute(param, str(supplied) if supplied is not None else "")
141
+ if text == "":
142
+ # An omitted parameter takes its own separating space with it, so the
143
+ # defaults render stays byte-identical to the verified string rather
144
+ # than leaving a tell-tale double space behind.
145
+ if out and out[-1].endswith(" "):
146
+ out[-1] = out[-1][:-1]
147
+ continue
148
+ out.append(text)
149
+ out.append(command[last:])
150
+
151
+ for name in params:
152
+ if name not in used:
153
+ raise InvocationRenderError(
154
+ f"invocation '{inv_id}' declares parameter '{name}' but its command "
155
+ f"has no {{{name}}}"
156
+ )
157
+ return "".join(out)
158
+
159
+
160
+ def default_args(invocation: dict[str, Any]) -> dict[str, str]:
161
+ """Every parameter's default — what every headless consumer runs with."""
162
+ out: dict[str, str] = {}
163
+ raw = invocation.get("params")
164
+ if isinstance(raw, list):
165
+ for p in raw:
166
+ if isinstance(p, dict) and isinstance(p.get("name"), str):
167
+ default = p.get("default")
168
+ out[p["name"]] = "" if default is None else str(default)
169
+ return out
170
+
171
+
172
+ def defaults_match_verified(invocation: dict[str, Any]) -> bool:
173
+ """Does rendering the defaults reproduce the string bring-up verified?
174
+
175
+ True when no ``verification.rendered_command`` was recorded: an older record
176
+ simply makes no claim, and refusing it would break every unit brought up
177
+ before parameters existed.
178
+ """
179
+ verification = invocation.get("verification")
180
+ recorded = verification.get("rendered_command") if isinstance(verification, dict) else None
181
+ if not isinstance(recorded, str) or not recorded:
182
+ return True
183
+ try:
184
+ return render_invocation(invocation, default_args(invocation)) == recorded
185
+ except InvocationRenderError:
186
+ return False
187
+
188
+
189
+ def invocation_slug(value: str) -> str:
190
+ """Filesystem- and id-safe slug, mirroring ``serviceSlug`` on both sides."""
191
+ return re.sub(r"[^A-Za-z0-9_.-]", "_", value) or "invocation"
@@ -0,0 +1,290 @@
1
+ ## Vinv Stage 2a — Python service inventory (read-only, terminal)
2
+
3
+ You are **TerminalExecutor** on an **Vinv bring-up enumeration** task. You are **NOT** starting or
4
+ installing anything in this stage — you are producing a machine-readable inventory of the **Python
5
+ services** in this repo. A later `bringup start <service>` invocation consumes your output to bring
6
+ services up one at a time.
7
+
8
+ ### What counts as a "service" (read carefully)
9
+
10
+ - **A service is a Python distribution/component we instrument** — the unit a later
11
+ `bringup start --service <name>` brings up while tracelens AST-rewrites its import package(s).
12
+ Most repos ship one per packaging manifest (`pyproject.toml` / `setup.py`); the framework scan
13
+ below lists the candidates it found and, for each, the **import package(s)** that belong in
14
+ `modules`. Distribution names may contain `-`; import packages never do.
15
+ - **"Service" is broader than HTTP.** Include every long-running Python process the repo defines:
16
+ web apps (`python_web`), background/queue workers (`python_worker`), **stdio JSON-RPC servers**
17
+ (`python_stdio` — MCP servers and the like, spoken to over stdin/stdout, no port), and
18
+ beat/cron-style schedulers (`python_scheduler`). A port scan finds none of the last three —
19
+ find them from the handbook's enumeration, the manifest-scan list below, and the entry code.
20
+ - **"Service" is also broader than long-running.** Tracelens instruments a *process*, not a
21
+ server: `tracelens run -t pkg -- <any command>` records a run that starts, works and exits
22
+ just as faithfully as one that stays up. So the inventory also covers the two units of work
23
+ that never listen on anything:
24
+ - `python_cli` — a console script or `__main__` the repo declares, invoked with argv and run
25
+ to completion (`acme-tool report --since 7d`);
26
+ - `python_library` — an importable package with **no entrypoint of its own**, whose functions
27
+ are called by the exerciser's function driver.
28
+
29
+ These are not consolation prizes for repos without a server. In a toolchain, a SDK or a
30
+ framework (LangChain and its kin) they are the *only* units of work there are, and an
31
+ inventory that omits them traces nothing at all.
32
+ - **EXCLUDE stateful infrastructure** (Postgres, MySQL, Redis, ClickHouse, Kafka, Mongo, etc.,
33
+ i.e. the handbook's `### Run in Docker` table). We do **not** instrument or bring those up here;
34
+ the start stage starts them as a dependency when needed. They must **not** appear in your output.
35
+ - **EXCLUDE non-Python processes** (Node/Vite/Next frontends). Only Python services count.
36
+ - **NEVER FABRICATE A `command`.** This is the rule the library case used to be a special case of,
37
+ and it is the only one that still bites: a command grounded in nothing wastes an entire
38
+ downstream bring-up attempt before anyone notices it cannot work. A Python package with **no
39
+ runnable process of its own** — no `[project.scripts]` / `[tool.poetry.scripts]` console script,
40
+ no `__main__.py`, no handbook `### Run on host` row naming it — is a library, and inventing an
41
+ entrypoint for it is the classic way to break this rule. Three legitimate outcomes:
42
+ 1. **Its code runs inside another service's process** (the app imports it): list it as a service
43
+ entry whose `command`, `working_directory` and `port` are **the host app's own** (the shared-
44
+ command case below) — the entries then differ only in which package tracelens instruments.
45
+ 2. **Nothing in the repo runs it, but its functions can be called**: list it as
46
+ `kind: "python_library"` and **omit `command` entirely**. The harness synthesizes the
47
+ exerciser function driver — you are not being asked to guess one, and writing one anyway is
48
+ the fabrication this rule forbids. This is the normal outcome for a library.
49
+ 3. **It should not be driven at all** (a vendored copy, a dev-only shim, generated code): leave
50
+ it out of `services`.
51
+
52
+ Outcome 2 is what makes a library-only repo tractable. Reserve outcome 3 for packages nothing
53
+ should ever call — not for "I could not find its entrypoint", which is what `python_library`
54
+ exists to express.
55
+
56
+ ### Candidate services (framework scan — strong prior; the handbook decides)
57
+
58
+ {_pkgs_note}
59
+
60
+ ### Source of truth
61
+
62
+ 1. **Read** the handbook at **`{vinv_md}`** first. It was produced in Stage 1 (discovery) and lists
63
+ runbooks, dependencies, ports, components, and a
64
+ `## Bring-up recipe (host vs container)` section. Use the
65
+ `### Run on host (instrumented by tracelens)` rows to find the **start command, working directory
66
+ and port** for the Python app(s). Ignore the `### Run in Docker` table for the inventory itself.
67
+ 2. Treat the handbook as authoritative and the scan as a strong prior. Where the handbook is thin,
68
+ you may `cat` `pyproject.toml`, `compose.yaml`, the package's own `__main__.py` / entrypoint,
69
+ etc. to fill gaps — but do **not** install dependencies or start any process.
70
+
71
+ ### What to capture for each Python service
72
+
73
+ - **`name`** — the service name: use the candidate name from the scan verbatim; for a service you
74
+ add from the handbook, use its distribution name (its `pyproject.toml` `[project].name` or
75
+ `[tool.poetry].name`, whichever table the manifest uses).
76
+ - **`kind`** — one of:
77
+ - `python_web` — serves HTTP (FastAPI/uvicorn, Flask, Django, a stdlib `http.server`, …);
78
+ - `python_worker` — background worker/consumer (celery/dramatiq/rq worker, queue consumer), no
79
+ HTTP port;
80
+ - `python_stdio` — a stdio JSON-RPC server (an MCP server or similar): long-running, port-less,
81
+ reads requests from stdin and writes JSON responses to stdout;
82
+ - `python_scheduler` — a beat/cron-style scheduler process (celery beat, apscheduler runner);
83
+ - `python_cli` — a console script / `__main__` that runs to completion and exits (a Click or
84
+ argparse CLI, a batch job, a report generator, a migration runner);
85
+ - `python_library` — an importable package with no entrypoint of its own, driven by the
86
+ exerciser calling its functions.
87
+ - **`transport`** — optional: `"http"` for `python_web`, `"stdio"` for `python_stdio`, else omit
88
+ or `null`. `python_cli` and `python_library` are never spoken to over a transport — omit it.
89
+ - **VERIFY each candidate is actually startable by READING ITS ENTRY CODE before you emit it.**
90
+ Open the module the command names (the console script's target function, the `__main__.py`, the
91
+ uvicorn app path) and confirm it really constructs a server / worker / stdio loop — or that it
92
+ returns. This read is also how you classify `kind`: a console script whose target calls
93
+ `uvicorn.run(...)` is `python_web`; one that builds a `FastMCP(...)` app or opens an `mcp` stdio
94
+ transport is `python_stdio`; a celery `worker_main`/`Worker` is `python_worker`; a beat/scheduler
95
+ loop is `python_scheduler`. **A console script whose target computes something, prints it and
96
+ returns is `python_cli`** — that is a unit of work, not a failed server, and it belongs in the
97
+ file. A candidate whose entry code you could not locate does not go in the file.
98
+
99
+ **The discriminator is whether the entrypoint blocks, never whether it declares a console
100
+ script.** A repo can declare a dozen `[project.scripts]` where one parks on `serve_forever` and
101
+ eleven return — that is one `python_web` and eleven `python_cli`, not one service and eleven
102
+ omissions. Read the target function and see which it does.
103
+ - **`command`** — the native start command that runs this service (NEVER `docker compose up`), taken
104
+ from what the repo **actually declares**: the handbook's `### Run on host` row, a
105
+ `[project.scripts]` / `[tool.poetry.scripts]` console script (e.g. `acme-payment-serve`), a
106
+ verified `python -m <module>` target, or the server command the project documents
107
+ (uvicorn/gunicorn/`manage.py runserver`/`flask run`/a worker entrypoint — whatever THIS project
108
+ uses, in `python -m <server> …` form where supported). **Verify before you emit:** only write
109
+ `python -m <module>` if that package really has a `__main__.py` (check it — `ls
110
+ <pkg-dir>/__main__.py`); a command you could not ground in the handbook, a manifest script, or an
111
+ existing entrypoint file is a fabrication and will burn a whole bring-up attempt downstream.
112
+ **`python -m <module>:<attr>` is INVALID** — `-m` takes a module; the `module:attr` form is an
113
+ app-factory reference that only uvicorn/gunicorn understand. If several services are served by the
114
+ **same** app process it is fine for them to share the same `command` — they differ only in which
115
+ package tracelens instruments at start.
116
+ For `python_cli` the same grounding rule applies to the *whole* invocation: the subcommand and
117
+ its argv must come from something the repo declares — the CLI's own `--help`/`@click.command`
118
+ definitions, a README example, a CI workflow step, a Makefile target, a test that shells out.
119
+ For `python_library`, **omit `command`** and let the harness synthesize the driver.
120
+ - **`invocations`** — optional, and only for `python_cli` / `python_library`: the representative
121
+ ways this unit is driven, one entry per way. Each is a **complete runnable command**, not an
122
+ argv fragment: `{{"id": "report", "command": "acme-tool report --since 7d",
123
+ "purpose": "weekly report", "expect_exit": 0}}`. `expect_exit` defaults to `0` — set it only
124
+ where the documented behavior is a non-zero exit (a linter that reports findings, a check
125
+ command). Omit the field entirely and `command` becomes the single invocation, which is the
126
+ right answer for a CLI with one real mode. Prefer two or three invocations that exercise
127
+ genuinely different code paths over ten variations of the same flag; each one is a separate
128
+ traced run.
129
+ - **`id`** — a short, stable slug naming this way of driving the unit (`report`, `check`,
130
+ `migrate`). It is the **unit identity everywhere downstream**: findings, coverage and history
131
+ all key on `<service>#<id>`. Give every invocation one. Without it the id is derived from the
132
+ subcommand, which is usually right and occasionally surprising; with it, nothing moves when
133
+ you reorder or re-word the list.
134
+ - **`params`** — optional, per invocation: the arguments a human should be able to change at run
135
+ time, so the editor can offer a form instead of a frozen command line. Write the command as a
136
+ **template** with `{{name}}` placeholders and declare one entry per placeholder:
137
+
138
+ ```json
139
+ {{"id": "report",
140
+ "command": "acme-tool report --since {{since}} --format {{format}}",
141
+ "params": [
142
+ {{"name": "since", "type": "string", "default": "7d", "help": "lookback window"}},
143
+ {{"name": "format", "type": "enum", "default": "json", "choices": ["json", "csv"]}}
144
+ ]}}
145
+ ```
146
+
147
+ - `type` is one of `string` (default), `int`, `float`, `enum`, `path`, `flag`. A `flag` renders
148
+ as `--<name>` when on and disappears when off; a `path` value is rewritten to the `/c/…`
149
+ spelling bash reads; an `enum` needs a `choices` list.
150
+ - **`default` is not a suggestion — it is what runs.** Every headless consumer (the exercise
151
+ pass, the replay gate, the Run button's one-click path) fills every parameter with its
152
+ default, so rendering the template with the defaults must produce **exactly the command you
153
+ verified**. If it does not, the file is rejected.
154
+ - **Ground the defaults and the choices in what the repo declares**, the same rule as `command`:
155
+ the CLI's own `--help` / `@click.option` / `argparse` definitions, a README example, a CI step.
156
+ Never invent a value to fill a slot — a fabricated default is worse than no parameter, because
157
+ it renders into a command that then claims to be verified.
158
+ - **Only parameterize what is genuinely worth changing.** A slot per flag is noise; the useful
159
+ ones are the inputs a person actually retypes (a path, a date range, an output format).
160
+ - `examples` (optional, a list) is how you offer extra values for the exercise pass to try. It
161
+ runs the declared defaults plus one variant per enumerated value, and **nothing else** — it
162
+ will never invent argv of its own, because `--force` and `--delete` are flags too.
163
+ - **`working_directory`** — absolute path the command runs from (default the candidate's own path
164
+ from the scan, else `{root}`).
165
+ - **`port`** — the HTTP port it listens on (**required** for `python_web`); `null` for every other
166
+ kind (`python_stdio` in particular MUST have `port: null` — it has no socket, and `python_cli` /
167
+ `python_library` MUST have `port: null` — they never listen at all).
168
+ - **Readiness for non-HTTP kinds** — a `python_stdio` service cannot be probed on a port. Its
169
+ readiness check (used by Stage 2b and the replay gate) is a **JSON-RPC initialize round-trip**:
170
+ spawn the command, write one `{{"jsonrpc":"2.0","id":1,"method":"initialize",...}}` line to its
171
+ stdin, expect a JSON line back on stdout. Stage 2b records this as
172
+ `"probe": {{"type": "stdio-jsonrpc"}}` in the start_commands file (`"process"` for
173
+ workers/schedulers that just have to stay alive, `"port"` for web, `"exit"` for the
174
+ run-to-completion kinds). You don't run the probe in this read-only stage — but classify `kind`
175
+ correctly so Stage 2b knows which oracle to use.
176
+ - **Readiness for the run-to-completion kinds** — there is nothing to probe, because the process
177
+ **exiting is the successful outcome**. Stage 2b runs the tracelens-wrapped invocation to
178
+ completion and accepts it when the exit code matches and the trace is non-empty. Getting `kind`
179
+ wrong in this direction is expensive in both directions: a CLI filed as `python_web` hangs
180
+ Stage 2b until its deadline waiting for a port that never opens, and a server filed as
181
+ `python_cli` fails the moment it does not exit.
182
+ - **`modules`** — the top-level **import package(s)** tracelens instruments for this service
183
+ (`--target-package`). Use the import package(s) from the scan — each must be a valid Python
184
+ identifier. A directory or distribution name containing `-` is NEVER a module name. Add another
185
+ package only if this service's process genuinely also runs that package's code and you want it
186
+ traced too.
187
+
188
+ ### How to write the file
189
+
190
+ You have a `save_file(content, file_path)` tool wired into this run. **Use it.** Compose the full
191
+ JSON document as a single string in your reasoning, then call:
192
+
193
+ ```
194
+ save_file(content=<full json>, file_path="{services_json}")
195
+ ```
196
+
197
+ The JSON must have exactly this shape (one entry per Python service, no infra, no frontends):
198
+
199
+ ```json
200
+ {{
201
+ "services": [
202
+ {{
203
+ "name": "acme-payment",
204
+ "kind": "python_web",
205
+ "transport": "http",
206
+ "command": "python -m uvicorn acme_payment.main:app --host 0.0.0.0 --port 8000",
207
+ "working_directory": "{root}",
208
+ "port": 8000,
209
+ "modules": ["acme_payment"]
210
+ }},
211
+ {{
212
+ "name": "toolshed",
213
+ "kind": "python_worker",
214
+ "command": "python -m toolshed",
215
+ "working_directory": "{root}",
216
+ "port": null,
217
+ "modules": ["toolshed"]
218
+ }},
219
+ {{
220
+ "name": "acme-mcp",
221
+ "kind": "python_stdio",
222
+ "transport": "stdio",
223
+ "command": "python -m acme_mcp.server",
224
+ "working_directory": "{root}",
225
+ "port": null,
226
+ "modules": ["acme_mcp"]
227
+ }},
228
+ {{
229
+ "name": "acme-tool",
230
+ "kind": "python_cli",
231
+ "command": "acme-tool report --since 7d",
232
+ "working_directory": "{root}",
233
+ "port": null,
234
+ "modules": ["acme_tool"],
235
+ "invocations": [
236
+ {{"id": "report", "command": "acme-tool report --since {{since}}",
237
+ "purpose": "the reporting path",
238
+ "params": [{{"name": "since", "default": "7d", "help": "lookback window"}}]}},
239
+ {{"id": "check", "command": "acme-tool check ./sample",
240
+ "purpose": "the validation path", "expect_exit": 1}}
241
+ ]
242
+ }},
243
+ {{
244
+ "name": "acme-sdk",
245
+ "kind": "python_library",
246
+ "working_directory": "{root}",
247
+ "port": null,
248
+ "modules": ["acme_sdk"]
249
+ }}
250
+ ]
251
+ }}
252
+ ```
253
+
254
+ (The names above are illustrative of the **shape** — note how the service `name` may carry a dash
255
+ while `modules` carries the underscore import package, how the stdio server carries
256
+ `transport: "stdio"` with `port: null`, how the CLI's `invocations` are each a full command with
257
+ its own stable `id`, how the first carries a `{{since}}` slot a human can change while the second
258
+ documents a legitimate non-zero exit, and how the library entry carries **no `command` at all**.
259
+ Use the actual names for this repo.) Emit
260
+ **valid JSON only** (no trailing commas, no comments in the actual file). Do **not** wrap it in
261
+ Markdown fences inside the file. After `save_file` returns `status: success`, run
262
+ `cat '{services_json}'` once via `send_terminal_command` to confirm it landed and parses, then end
263
+ the trajectory.
264
+
265
+ ### Execution discipline
266
+
267
+ - Use **terminal tools** only for **reading** (`cat`, `ls`, `grep`); set `block: true` on any command
268
+ whose output you need.
269
+ - Do **NOT** run installs, builds, `docker compose up`, `uvicorn`, `npm run dev`, or any process
270
+ start in this stage. Enumeration is read-only.
271
+ - Do **not** call `close_terminal`.
272
+
273
+ ### Finish — what "done" actually means
274
+
275
+ You are done only when **`{services_json}` exists on disk** with a valid `services` array covering
276
+ every service in the handbook. That means: you called
277
+ `save_file(content=<full json>, file_path="{services_json}")`, it returned `status: success`, and
278
+ `cat` confirmed it on disk. Reading and thinking are not a substitute for writing the file.
279
+
280
+ After you finish, the harness validates the file deterministically — every `modules` entry must be
281
+ an importable Python identifier, `kind` must be one of `python_web` / `python_worker` /
282
+ `python_stdio` / `python_scheduler` / `python_cli` / `python_library`, `python_web` entries must
283
+ carry an integer `port`, `python_stdio` / `python_cli` / `python_library` entries must carry
284
+ `port: null`, `transport` (when present) must be `"http"` or `"stdio"` and match the kind,
285
+ `invocations` (when present) may only appear on `python_cli` / `python_library` and each entry
286
+ must carry a full `command` and a unique `id`, every `params` entry must name a `{{placeholder}}`
287
+ the command actually contains (and vice versa) and render with its defaults, every entry except
288
+ `python_library` must carry a `command`, and commands must be runnable (no `-m module:attr`, no
289
+ docker) — and re-opens this task with the exact violations if it fails. Get it right the first
290
+ time.