flywheel-bootstrap-staging 0.1.9.202601271835__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.
bootstrap/payload.py ADDED
@@ -0,0 +1,119 @@
1
+ """Bootstrap payload fetch/validation (skeleton)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ import json
7
+ import time
8
+ import urllib.request
9
+ import urllib.error
10
+ import ssl
11
+ import http.client
12
+ from typing import Any, Mapping
13
+
14
+
15
+ @dataclass
16
+ class RepoContext:
17
+ """Repository context for code persistence.
18
+
19
+ Mirrors the core.models.RepoContext class but is a standalone
20
+ definition to avoid tight coupling between bootstrap and core.
21
+ """
22
+
23
+ repo_url: str
24
+ repo_owner: str
25
+ repo_name: str
26
+ branch_name: str
27
+ base_branch: str
28
+ is_fork: bool = False
29
+ fork_source_url: str | None = None
30
+ base_commit_sha: str | None = None
31
+ head_commit_sha: str | None = None
32
+
33
+ @classmethod
34
+ def from_dict(cls, payload: Mapping[str, Any]) -> "RepoContext":
35
+ return cls(
36
+ repo_url=str(payload["repo_url"]),
37
+ repo_owner=str(payload["repo_owner"]),
38
+ repo_name=str(payload["repo_name"]),
39
+ branch_name=str(payload["branch_name"]),
40
+ base_branch=str(payload["base_branch"]),
41
+ is_fork=bool(payload.get("is_fork", False)),
42
+ fork_source_url=payload.get("fork_source_url"),
43
+ base_commit_sha=payload.get("base_commit_sha"),
44
+ head_commit_sha=payload.get("head_commit_sha"),
45
+ )
46
+
47
+
48
+ @dataclass
49
+ class BootstrapPayload:
50
+ """Representation of the backend bootstrap payload."""
51
+
52
+ prompt: str
53
+ repo_context: RepoContext | None = None
54
+ github_token: str | None = None
55
+
56
+
57
+ def fetch_bootstrap_payload(
58
+ server_url: str,
59
+ run_id: str,
60
+ token: str,
61
+ ) -> BootstrapPayload:
62
+ """Retrieve bootstrap payload from the backend server.
63
+
64
+ Raises:
65
+ NotImplementedError: placeholder until HTTP wiring is added.
66
+ """
67
+
68
+ url = f"{server_url.rstrip('/')}/runs/{run_id}/bootstrap"
69
+ req = urllib.request.Request(url, headers={"X-Run-Token": token})
70
+ payload = _urlopen_json_with_retries(
71
+ req,
72
+ timeout_seconds=30,
73
+ attempts=6,
74
+ base_delay_seconds=0.5,
75
+ )
76
+ inner = payload.get("payload", {})
77
+
78
+ # Parse repo context if present
79
+ repo_context = None
80
+ if inner.get("repo_context"):
81
+ repo_context = RepoContext.from_dict(inner["repo_context"])
82
+
83
+ return BootstrapPayload(
84
+ prompt=str(inner.get("prompt", "")),
85
+ repo_context=repo_context,
86
+ github_token=inner.get("github_token"),
87
+ )
88
+
89
+
90
+ def _urlopen_json_with_retries(
91
+ req: urllib.request.Request,
92
+ timeout_seconds: int,
93
+ attempts: int,
94
+ base_delay_seconds: float,
95
+ ) -> dict:
96
+ """Best-effort JSON fetch with retries.
97
+
98
+ Fly.io apps can be cold-started (min_machines_running=0), and network edges
99
+ can occasionally drop connections. This helper retries common transient
100
+ failures with exponential backoff.
101
+ """
102
+ last_exc: Exception | None = None
103
+ for i in range(attempts):
104
+ try:
105
+ with urllib.request.urlopen(req, timeout=timeout_seconds) as resp:
106
+ return json.loads(resp.read().decode("utf-8"))
107
+ except (
108
+ TimeoutError,
109
+ urllib.error.URLError,
110
+ http.client.RemoteDisconnected,
111
+ ssl.SSLError,
112
+ ) as exc:
113
+ last_exc = exc
114
+ if i == attempts - 1:
115
+ break
116
+ delay = base_delay_seconds * (2**i)
117
+ time.sleep(delay)
118
+ assert last_exc is not None
119
+ raise last_exc
bootstrap/prompts.py ADDED
@@ -0,0 +1,79 @@
1
+ """Prompt assembly helpers for bootstrap."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import textwrap
6
+
7
+
8
+ def build_prompt_text(
9
+ *, server_prompt: str, workspace_instructions: str, artifact_manifest: str
10
+ ) -> str:
11
+ """Combine base context, task prompt, and user workspace instructions."""
12
+
13
+ base_context = textwrap.dedent(
14
+ f"""
15
+ You are an autonomous engineer with FULL SYSTEM ACCESS. Your job is to complete the task by ACTUALLY EXECUTING commands and writing files—not by describing what to do.
16
+
17
+ CRITICAL: You must USE your shell and file-writing capabilities to:
18
+ - Create files directly (don't tell the user to save code—write it yourself)
19
+ - Run commands directly (don't tell the user to run something—execute it yourself)
20
+ - Install dependencies if needed
21
+ - Execute training scripts and collect results
22
+
23
+ Work autonomously until completion—there is no user to respond to questions. Execute all necessary commands yourself. The task description and workspace instructions below contain the context you need. Leverage the provided GPU hardware fully.
24
+
25
+ ARTIFACT MANIFEST (CRITICAL):
26
+ - The environment variable $FLYWHEEL_WORKSPACE contains the absolute path to your workspace root.
27
+ - When finished, write the manifest to: $FLYWHEEL_WORKSPACE/{artifact_manifest}
28
+ - The manifest MUST be a top-level JSON array (list). Do NOT wrap it in an object.
29
+ - All file paths in the manifest must be relative to $FLYWHEEL_WORKSPACE.
30
+ - Each entry must be an object with "artifact_type" and "payload" keys.
31
+
32
+ MANIFEST FORMAT — the file must look exactly like this (a JSON array):
33
+ [
34
+ {{"artifact_type": "text", "payload": {{"content": "Summary of results..."}}}},
35
+ {{"artifact_type": "image", "payload": {{"path": "plots/loss_curve.png", "format": "png"}}}},
36
+ {{"artifact_type": "table", "payload": {{"path": "results/metrics.csv", "format": "csv"}}}}
37
+ ]
38
+
39
+ IMPORTANT: The file must start with [ and end with ]. Do NOT write {{"artifacts": [...]}} or any other wrapper object.
40
+
41
+ SUPPORTED ARTIFACT TYPES (use ONLY these):
42
+ - "text": For text/markdown. Payload: {{"content": "..."}} or {{"path": "path/to/file.txt"}}
43
+ - "table": For CSV data. Payload: {{"path": "path/to/data.csv", "format": "csv"}}
44
+ - "json": For JSON data. Payload: {{"path": "path/to/data.json"}} or inline the object directly
45
+ - "image": For PNG/JPEG. Payload: {{"path": "path/to/plot.png", "format": "png"}}
46
+ - "html": For interactive HTML (e.g., Plotly). Payload: {{"path": "path/to/chart.html"}} or {{"content": "<!doctype html>..."}}
47
+ - "vega": For Vega/Vega-Lite specs. Payload: {{"spec": {{...}}}} or {{"path": "path/to/chart.vl.json"}}
48
+
49
+ OUTPUT:
50
+ - Print concise progress updates to stdout; they will be forwarded as logs.
51
+
52
+ ARTIFACT QUALITY TIPS:
53
+ Before finalizing any artifact, verify it renders correctly. Common pitfalls:
54
+
55
+ Plots & Visualizations:
56
+ - Convert tensors to plain Python/numpy BEFORE plotting: use .detach().cpu().numpy() or .item()
57
+ - Verify numeric axes show numbers, not categorical labels (symptom of unconverted data)
58
+ - Always label axes and include a title
59
+ - For Plotly HTML, save with include_plotlyjs=True to make it self-contained
60
+
61
+ Tables & Data:
62
+ - Ensure consistent column types (don't mix strings and numbers in a column)
63
+ - Handle NaN/None values explicitly rather than leaving them as raw representations
64
+ - Round floats to reasonable precision for readability
65
+
66
+ General:
67
+ - Inspect your output files before declaring success
68
+ - If something looks wrong (e.g., a blank plot, garbled text), debug and fix it
69
+ - Prefer self-contained artifacts (inline CSS/JS, no external dependencies)
70
+ """
71
+ ).strip()
72
+
73
+ return (
74
+ f"{base_context}\n\n"
75
+ "Task Description:\n"
76
+ f"{server_prompt.strip()}\n\n"
77
+ "Workspace Instructions:\n"
78
+ f"{workspace_instructions.strip()}"
79
+ )
bootstrap/py.typed ADDED
@@ -0,0 +1 @@
1
+
bootstrap/runner.py ADDED
@@ -0,0 +1,145 @@
1
+ """Codex process launcher and stream parser (skeleton)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Iterable, Mapping, Iterator
8
+ import json
9
+ import subprocess
10
+ import sys
11
+
12
+
13
+ @dataclass
14
+ class CodexInvocation:
15
+ """Command and environment prepared for invoking codex."""
16
+
17
+ args: list[str]
18
+ env: Mapping[str, str]
19
+ workdir: Path
20
+ prompt: str = "" # Prompt to pass via stdin
21
+ exit_code: int | None = None
22
+ stderr_output: str = "" # Captured stderr for debugging
23
+
24
+
25
+ @dataclass
26
+ class CodexEvent:
27
+ """Structured event emitted by codex --json."""
28
+
29
+ raw: dict
30
+
31
+
32
+ def build_invocation(
33
+ codex_executable: Path,
34
+ prompt: str,
35
+ workdir: Path,
36
+ env: Mapping[str, str],
37
+ extra_flags: Iterable[str] = (),
38
+ ) -> CodexInvocation:
39
+ """Assemble the codex exec command.
40
+
41
+ The prompt is written to a file in the workdir and passed via --prompt-file
42
+ to avoid shell argument length limits and stdin handling issues.
43
+ """
44
+ # Write prompt to file in the workdir (which Codex has access to)
45
+ prompt_file = workdir / "flywheel_prompt.txt"
46
+ prompt_file.write_text(prompt, encoding="utf-8")
47
+
48
+ # Use '-' to read prompt from stdin (avoids CLI arg length limits)
49
+ args = [
50
+ str(codex_executable),
51
+ "exec",
52
+ "--json",
53
+ "--cd",
54
+ str(workdir),
55
+ "--skip-git-repo-check",
56
+ *list(extra_flags),
57
+ "-", # Read prompt from stdin
58
+ ]
59
+ return CodexInvocation(args=args, env=dict(env), workdir=workdir, prompt=prompt)
60
+
61
+
62
+ def run_and_stream(invocation: CodexInvocation) -> Iterator[CodexEvent]:
63
+ """Launch codex and yield parsed events as they arrive.
64
+
65
+ The prompt is passed via file (referenced in args) to avoid stdin issues.
66
+ """
67
+ # Log the command being run (redact prompt for brevity)
68
+ cmd_display = " ".join(invocation.args)
69
+ print(f"[bootstrap] Running: {cmd_display}", file=sys.stderr)
70
+ print(f"[bootstrap] Workdir: {invocation.workdir}", file=sys.stderr)
71
+ print(f"[bootstrap] Prompt length: {len(invocation.prompt)} chars", file=sys.stderr)
72
+
73
+ # Emit command info as first event for server-side logging
74
+ yield CodexEvent(
75
+ raw={
76
+ "bootstrap_debug": "codex_launch",
77
+ "command": invocation.args,
78
+ "workdir": str(invocation.workdir),
79
+ "prompt_length": len(invocation.prompt),
80
+ }
81
+ )
82
+
83
+ try:
84
+ proc = subprocess.Popen(
85
+ invocation.args,
86
+ cwd=invocation.workdir,
87
+ env=invocation.env if invocation.env else None,
88
+ stdin=subprocess.PIPE,
89
+ stdout=subprocess.PIPE,
90
+ stderr=subprocess.PIPE,
91
+ text=True,
92
+ encoding="utf-8", # Explicit UTF-8 for cross-platform Unicode support
93
+ bufsize=1,
94
+ )
95
+ except Exception as e:
96
+ error_msg = f"Failed to start codex process: {e}"
97
+ print(f"[bootstrap] ERROR: {error_msg}", file=sys.stderr)
98
+ yield CodexEvent(raw={"bootstrap_error": error_msg})
99
+ invocation.exit_code = 127
100
+ return
101
+
102
+ assert proc.stdin is not None
103
+ assert proc.stdout is not None
104
+
105
+ # Write prompt to stdin and close it so Codex knows input is complete
106
+ try:
107
+ proc.stdin.write(invocation.prompt)
108
+ proc.stdin.close()
109
+ except Exception as e:
110
+ error_msg = f"Failed to write prompt to stdin: {e}"
111
+ print(f"[bootstrap] ERROR: {error_msg}", file=sys.stderr)
112
+ yield CodexEvent(raw={"bootstrap_error": error_msg})
113
+
114
+ for line in proc.stdout:
115
+ line = line.strip()
116
+ if not line:
117
+ continue
118
+ try:
119
+ payload = json.loads(line)
120
+ except json.JSONDecodeError:
121
+ payload = {"message": line}
122
+ yield CodexEvent(raw=payload)
123
+
124
+ proc.wait()
125
+ invocation.exit_code = proc.returncode
126
+
127
+ # Always capture stderr for debugging
128
+ stderr_content = ""
129
+ if proc.stderr:
130
+ stderr_content = proc.stderr.read().strip()
131
+ invocation.stderr_output = stderr_content
132
+
133
+ # Log exit status
134
+ print(f"[bootstrap] Codex exited with code: {proc.returncode}", file=sys.stderr)
135
+ if stderr_content:
136
+ print(f"[bootstrap] Codex stderr:\n{stderr_content}", file=sys.stderr)
137
+
138
+ # Emit detailed exit info
139
+ yield CodexEvent(
140
+ raw={
141
+ "bootstrap_debug": "codex_exit",
142
+ "exit_code": proc.returncode,
143
+ "stderr": stderr_content if stderr_content else None,
144
+ }
145
+ )
bootstrap/telemetry.py ADDED
@@ -0,0 +1,147 @@
1
+ """Heartbeat, log, and artifact POST helpers (skeleton)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime, timezone
6
+ from typing import Mapping, Sequence
7
+ import json
8
+ import time
9
+ import urllib.request
10
+ import urllib.error
11
+ import ssl
12
+ import http.client
13
+ import sys
14
+
15
+ # Defaults are tuned to be tolerant of transient backend stalls.
16
+ READ_TIMEOUT_SECONDS = 30
17
+ MAX_ATTEMPTS = 3
18
+ BASE_DELAY_SECONDS = 0.5
19
+
20
+
21
+ def utcnow() -> datetime:
22
+ """Return a timezone-aware UTC timestamp."""
23
+ return datetime.now(tz=timezone.utc)
24
+
25
+
26
+ def post_heartbeat(
27
+ server_url: str, run_id: str, token: str, summary: str | None
28
+ ) -> None:
29
+ """Send a heartbeat to the backend."""
30
+ _post(
31
+ f"{server_url.rstrip('/')}/runs/{run_id}/heartbeat",
32
+ token,
33
+ {"observed_at": utcnow().isoformat(), "summary": summary},
34
+ )
35
+
36
+
37
+ def post_log(
38
+ server_url: str,
39
+ run_id: str,
40
+ token: str,
41
+ level: str,
42
+ message: str,
43
+ extra: Mapping[str, object] | None = None,
44
+ ) -> None:
45
+ """Send a log entry to the backend."""
46
+ _post(
47
+ f"{server_url.rstrip('/')}/runs/{run_id}/logs",
48
+ token,
49
+ {
50
+ "created_at": utcnow().isoformat(),
51
+ "level": level,
52
+ "message": message,
53
+ "extra": extra or {},
54
+ },
55
+ )
56
+
57
+
58
+ def post_artifacts(
59
+ server_url: str,
60
+ run_id: str,
61
+ token: str,
62
+ artifacts: Sequence[Mapping[str, object]],
63
+ ) -> None:
64
+ """Send artifact manifest entries to the backend."""
65
+ for artifact in artifacts:
66
+ payload: Mapping[str, object] | object = artifact
67
+ if isinstance(artifact, Mapping):
68
+ inner = artifact.get("payload", artifact)
69
+ if isinstance(inner, Mapping):
70
+ payload = inner
71
+ _post(
72
+ f"{server_url.rstrip('/')}/runs/{run_id}/artifacts",
73
+ token,
74
+ {
75
+ "artifact_type": str(artifact.get("artifact_type", "unknown")),
76
+ "payload": payload,
77
+ },
78
+ )
79
+
80
+
81
+ def post_completion(server_url: str, run_id: str, token: str, summary: str) -> None:
82
+ """Mark run complete."""
83
+ _post(
84
+ f"{server_url.rstrip('/')}/runs/{run_id}/complete",
85
+ token,
86
+ {"summary": summary},
87
+ )
88
+
89
+
90
+ def post_error(
91
+ server_url: str, run_id: str, token: str, reason: str, summary: str | None = None
92
+ ) -> None:
93
+ """Mark run errored."""
94
+ _post(
95
+ f"{server_url.rstrip('/')}/runs/{run_id}/error",
96
+ token,
97
+ {"summary": summary or "", "reason": reason},
98
+ )
99
+
100
+
101
+ def _post(url: str, token: str, body: Mapping[str, object]) -> None:
102
+ data = json.dumps(body).encode("utf-8")
103
+ req = urllib.request.Request(
104
+ url,
105
+ data=data,
106
+ headers={
107
+ "Content-Type": "application/json",
108
+ "X-Run-Token": token,
109
+ },
110
+ method="POST",
111
+ )
112
+ success = _urlopen_with_retries(
113
+ req,
114
+ timeout_seconds=READ_TIMEOUT_SECONDS,
115
+ attempts=MAX_ATTEMPTS,
116
+ base_delay_seconds=BASE_DELAY_SECONDS,
117
+ )
118
+ if not success:
119
+ # Best-effort: log locally but do not raise, to avoid failing the run on transient stalls.
120
+ print(f"telemetry POST to {url} failed after retries", file=sys.stderr)
121
+
122
+
123
+ def _urlopen_with_retries(
124
+ req: urllib.request.Request,
125
+ timeout_seconds: int,
126
+ attempts: int,
127
+ base_delay_seconds: float,
128
+ ) -> bool:
129
+ """Best-effort POST with retries for transient network errors."""
130
+ for i in range(attempts):
131
+ try:
132
+ with urllib.request.urlopen(
133
+ req, timeout=timeout_seconds
134
+ ) as resp: # pragma: no cover - network
135
+ resp.read()
136
+ return True
137
+ except (
138
+ TimeoutError,
139
+ urllib.error.URLError,
140
+ http.client.RemoteDisconnected,
141
+ ssl.SSLError,
142
+ ):
143
+ if i == attempts - 1:
144
+ break
145
+ delay = base_delay_seconds * (2**i)
146
+ time.sleep(delay)
147
+ return False
@@ -0,0 +1,94 @@
1
+ Metadata-Version: 2.4
2
+ Name: flywheel-bootstrap-staging
3
+ Version: 0.1.9.202601271835
4
+ Summary: Bootstrap runner for Flywheel provisioned GPU instances
5
+ Project-URL: Homepage, http://paradigma.inc/
6
+ Author: Paradigma Labs
7
+ License: MIT
8
+ Keywords: bootstrap,flywheel,gpu,machine-learning,ml
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Requires-Python: >=3.11
15
+ Description-Content-Type: text/markdown
16
+
17
+ # Bootstrap
18
+
19
+ This package hosts the BYOC bootstrapper that:
20
+
21
+ - Ensures Codex is available (prefers release tarball; skips install if already
22
+ on `PATH`).
23
+ - Fetches the bootstrap payload for a run from the Flywheel backend.
24
+ - Launches `codex exec` with the provided prompt/config and streams logs.
25
+ - Collects artifacts (manifest-on-exit) and reports completion or error back to
26
+ the backend.
27
+
28
+ ## Configuration
29
+
30
+ Bootstrap reads the user's Codex `config.toml` and requires one of:
31
+
32
+ ```toml
33
+ [flywheel]
34
+ # inline instructions (host-specific tips, paths, sandbox notes)
35
+ workspace_instructions = """
36
+ Use /mnt/work as your workspace. Write artifacts under ./artifacts.
37
+ """
38
+
39
+ # or: reference a file (relative paths are resolved against the config file directory)
40
+ workspace_instructions_file = "workspace_notes.md"
41
+ ```
42
+
43
+ Rules:
44
+
45
+ - At least one of `workspace_instructions` or `workspace_instructions_file` is
46
+ required; otherwise bootstrap exits before contacting the server.
47
+ - If both are set, the file wins and the inline value is ignored (warns once).
48
+ - File contents must be non-empty; the path is resolved relative to the config
49
+ file if not absolute.
50
+
51
+ Prompt assembly written to `flywheel_prompt.txt`:
52
+
53
+ 1. Flywheel engineer context (logging/artifact expectations).
54
+ 2. Task Description (prompt fetched from the server).
55
+ 3. Workspace Instructions (resolved from config as above).
56
+
57
+ Example config: `project/bootstrap/examples/config.example.toml` can be used as
58
+ a starting point; update the paths and instructions for your machine.
59
+
60
+ ## End-to-end flow (bootstrap.sh → Python bootstrapper)
61
+
62
+ 1. User runs `bash ./bootstrap.sh --run-id <id> --token <token> --config /path/to/config.toml [--server <url>]` on their BYOC machine.
63
+ 2. The shim:
64
+ - Ensures `uvx` is available (installs via `https://astral.sh/uv/install.sh` if missing, then rechecks PATH with `~/.cargo/bin`).
65
+ - Points `PKG_PATH` to the local repo copy `project/bootstrap`.
66
+ - Executes `uvx --no-cache --from "$PKG_PATH" flywheel-bootstrap "$@"` so the latest local package runs.
67
+ 3. Python entrypoint (`python -m bootstrap`):
68
+ - Parses args/env: requires run id + token, required `--config`, optional `--server` (default `http://localhost:8000`).
69
+ - Loads Codex config.toml, enforces presence of workspace instructions (inline or file), extracts workspace/sandbox settings.
70
+ 4. Workspace resolution:
71
+ - Uses `cd`/`workspace_dir` from config if set; otherwise `~/.flywheel/runs/<run_id>`.
72
+ - Creates the workspace and validates the artifact manifest path is inside sandbox `writable_roots` when sandboxing is enabled; else exits with an error.
73
+ 5. Codex availability:
74
+ - If `BOOTSTRAP_MOCK_CODEX` is set, skips install and runs a mock flow.
75
+ - Else, if `codex` is already on PATH, reuse it; otherwise download the Codex release tarball to the workspace/run root and mark it executable.
76
+ 6. Fetch bootstrap payload:
77
+ - `GET <server>/runs/<run_id>/bootstrap` with `X-Run-Token`; payload contains the task prompt.
78
+ 7. Build prompt file:
79
+ - Combine base Flywheel engineer context, “Task Description” (server prompt), and “Workspace Instructions” (user config) into `flywheel_prompt.txt` in the workspace.
80
+ 8. Launch Codex:
81
+ - Run `codex exec --json --cd <workspace> --skip-git-repo-check flywheel_prompt.txt` with env `FLYWHEEL_RUN_ID/TOKEN/SERVER`.
82
+ - Start a heartbeat thread posting `/runs/{id}/heartbeat` every 30s.
83
+ - Stream Codex stdout lines as logs to `/runs/{id}/logs`; capture Codex `run_id` if emitted.
84
+ 9. After Codex exits:
85
+ - Read `flywheel_artifacts.json`; if empty and Codex `run_id` is known, attempt one `codex resume <id>` then re-read.
86
+ - POST artifacts to `/runs/{id}/artifacts`; POST `/complete` on exit 0, else `/error` with the exit code.
87
+ - Stop/join the heartbeat thread.
88
+ 10. Mock mode (`BOOTSTRAP_MOCK_CODEX=1`):
89
+ - Sends a heartbeat, a few logs, writes a mock artifact manifest, returns 0 (used in e2e tests).
90
+
91
+ ## Next steps
92
+
93
+ - Publish bootstrap package and switch `uvx --from` to a release URL.
94
+ - Iterate on prompts / general polish
@@ -0,0 +1,17 @@
1
+ bootstrap/__init__.py,sha256=vRTDRRAXOf9l3PGRNGMUowKObnxFzmWCYUwtn31LzIM,39
2
+ bootstrap/__main__.py,sha256=F94dlGd3P4icWVZnb16LjrKpBhLPCvjwqyWDqhX3JjE,1338
3
+ bootstrap/artifacts.py,sha256=pdwDfxduT-G9VMJiGdm9MHkfUf35TzMGhFTHn135nU4,4188
4
+ bootstrap/config_loader.py,sha256=dgypIOi4a5auGsarIDB5MI6qJO38UWO6RJkRTzvA9Zs,3936
5
+ bootstrap/constants.py,sha256=Q3bNGvKKgQxyP9Hrx6o5GydFHwDq2SRkLnxwRNyM-OY,633
6
+ bootstrap/git_ops.py,sha256=HPn-CAURQEMRSLJRUuy1Lor7YwPYZjb6i6ku6WhJo7M,9267
7
+ bootstrap/install.py,sha256=Es1F0UBOYDhuv7i9veA5Bt-yyTyTZ-dAsHmGc-iRY5k,4166
8
+ bootstrap/orchestrator.py,sha256=zWRroJSoC6rjyNS1-YAnl4frk-uCWveWO_BeQnr_iRw,33468
9
+ bootstrap/payload.py,sha256=JugHguvdjHuoEY_wTE72PFDWlmFFgEk8_wiINNNjV_Q,3417
10
+ bootstrap/prompts.py,sha256=jywNCfrMN30uYV6cIBSex7NxWDM9IS-y7aD_x_bj3XY,4149
11
+ bootstrap/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
12
+ bootstrap/runner.py,sha256=qFqDnHKxUIOBAfk9cX7EipXz7mJCwTJPeKMQI30x4Do,4509
13
+ bootstrap/telemetry.py,sha256=ONPvKTpc2puCygkakdQW7-Kkrx_yqEzZz-0FMtCQ-RQ,3983
14
+ flywheel_bootstrap_staging-0.1.9.202601271835.dist-info/METADATA,sha256=8Fps4kjtomDvcvV2c_BFX44rB8-BY5ay7N-jbAt3UPo,4638
15
+ flywheel_bootstrap_staging-0.1.9.202601271835.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
16
+ flywheel_bootstrap_staging-0.1.9.202601271835.dist-info/entry_points.txt,sha256=uluR7l4k_RhY5K4nYN5e6uwtceW3_7h_kvpM-csjzrg,63
17
+ flywheel_bootstrap_staging-0.1.9.202601271835.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ flywheel-bootstrap = bootstrap.__main__:main