usecix 1.0.6__tar.gz
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.
- usecix-1.0.6/LICENSE +37 -0
- usecix-1.0.6/PKG-INFO +110 -0
- usecix-1.0.6/README.md +167 -0
- usecix-1.0.6/README.pypi.md +79 -0
- usecix-1.0.6/apps/cli/cix/scripts/__init__.py +0 -0
- usecix-1.0.6/apps/cli/cix/scripts/_cloud_wait.py +271 -0
- usecix-1.0.6/apps/cli/cix/scripts/agent_framing_hook.py +78 -0
- usecix-1.0.6/apps/cli/cix/scripts/audit.py +195 -0
- usecix-1.0.6/apps/cli/cix/scripts/bash_workaround_hook.py +395 -0
- usecix-1.0.6/apps/cli/cix/scripts/build_tool_manifest.py +114 -0
- usecix-1.0.6/apps/cli/cix/scripts/catchup.py +352 -0
- usecix-1.0.6/apps/cli/cix/scripts/clean.py +418 -0
- usecix-1.0.6/apps/cli/cix/scripts/cli.py +82 -0
- usecix-1.0.6/apps/cli/cix/scripts/codex_policy_debug.py +92 -0
- usecix-1.0.6/apps/cli/cix/scripts/codex_policy_hook.py +366 -0
- usecix-1.0.6/apps/cli/cix/scripts/config.py +77 -0
- usecix-1.0.6/apps/cli/cix/scripts/convention_validator_hook.py +686 -0
- usecix-1.0.6/apps/cli/cix/scripts/coverage.py +294 -0
- usecix-1.0.6/apps/cli/cix/scripts/cross_project_advisor_hook.py +136 -0
- usecix-1.0.6/apps/cli/cix/scripts/doctor.py +535 -0
- usecix-1.0.6/apps/cli/cix/scripts/feedback.py +134 -0
- usecix-1.0.6/apps/cli/cix/scripts/file_claim_hook.py +177 -0
- usecix-1.0.6/apps/cli/cix/scripts/guard.py +360 -0
- usecix-1.0.6/apps/cli/cix/scripts/hooks_ctl.py +134 -0
- usecix-1.0.6/apps/cli/cix/scripts/impact.py +161 -0
- usecix-1.0.6/apps/cli/cix/scripts/index.py +638 -0
- usecix-1.0.6/apps/cli/cix/scripts/init.py +829 -0
- usecix-1.0.6/apps/cli/cix/scripts/md_edit_guard_hook.py +190 -0
- usecix-1.0.6/apps/cli/cix/scripts/memory_guard_hook.py +97 -0
- usecix-1.0.6/apps/cli/cix/scripts/memory_seeds/tool_surface.md +132 -0
- usecix-1.0.6/apps/cli/cix/scripts/orient.py +493 -0
- usecix-1.0.6/apps/cli/cix/scripts/post_search_hook.py +333 -0
- usecix-1.0.6/apps/cli/cix/scripts/post_write_hook.py +97 -0
- usecix-1.0.6/apps/cli/cix/scripts/precommit.py +890 -0
- usecix-1.0.6/apps/cli/cix/scripts/read_advisor_hook.py +424 -0
- usecix-1.0.6/apps/cli/cix/scripts/reconcile.py +279 -0
- usecix-1.0.6/apps/cli/cix/scripts/schema_pull.py +848 -0
- usecix-1.0.6/apps/cli/cix/scripts/session_start_hook.py +898 -0
- usecix-1.0.6/apps/cli/cix/scripts/source_of_truth_hook.py +118 -0
- usecix-1.0.6/apps/cli/cix/scripts/summarize.py +278 -0
- usecix-1.0.6/apps/cli/cix/scripts/version_check.py +139 -0
- usecix-1.0.6/apps/cli/cix/scripts/vital.py +464 -0
- usecix-1.0.6/apps/cli/cix/scripts/vital_sign.py +29 -0
- usecix-1.0.6/apps/cli/cix/scripts/wedge_ack.py +69 -0
- usecix-1.0.6/apps/client/cix/cloud/__init__.py +9 -0
- usecix-1.0.6/apps/client/cix/cloud/_changed_files.py +92 -0
- usecix-1.0.6/apps/client/cix/cloud/access_check.py +204 -0
- usecix-1.0.6/apps/client/cix/cloud/auth_check.py +196 -0
- usecix-1.0.6/apps/client/cix/cloud/client.py +225 -0
- usecix-1.0.6/apps/client/cix/cloud/config.py +45 -0
- usecix-1.0.6/apps/client/cix/cloud/credentials.py +69 -0
- usecix-1.0.6/apps/client/cix/cloud/dispatch.py +2443 -0
- usecix-1.0.6/apps/client/cix/cloud/freshness.py +151 -0
- usecix-1.0.6/apps/client/cix/cloud/license.py +128 -0
- usecix-1.0.6/apps/client/cix/cloud/login.py +503 -0
- usecix-1.0.6/apps/client/cix/cloud/login_gate.py +59 -0
- usecix-1.0.6/apps/client/cix/cloud/telemetry.py +139 -0
- usecix-1.0.6/apps/client/cix/cloud/token_refresh.py +68 -0
- usecix-1.0.6/apps/client/cix/cloud/transport.py +2078 -0
- usecix-1.0.6/apps/core/cix/core/__init__.py +48 -0
- usecix-1.0.6/apps/core/cix/core/diagnostics.py +266 -0
- usecix-1.0.6/apps/core/cix/core/edit_guard.py +327 -0
- usecix-1.0.6/apps/core/cix/core/get_lines.py +413 -0
- usecix-1.0.6/apps/core/cix/core/helpers.py +1196 -0
- usecix-1.0.6/apps/core/cix/core/precommit.py +106 -0
- usecix-1.0.6/apps/core/cix/core/presenter.py +310 -0
- usecix-1.0.6/apps/core/cix/core/registry.py +367 -0
- usecix-1.0.6/apps/core/cix/core/troubleshoot_log.py +155 -0
- usecix-1.0.6/apps/core/cix/core/working_tree.py +662 -0
- usecix-1.0.6/cix/__init__.py +1 -0
- usecix-1.0.6/cix/_indexed_extensions.py +68 -0
- usecix-1.0.6/cix/_ip_boundary.py +97 -0
- usecix-1.0.6/cix/_tool_manifest.py +1359 -0
- usecix-1.0.6/cix/install.py +1432 -0
- usecix-1.0.6/cix/integrations/__init__.py +2 -0
- usecix-1.0.6/cix/integrations/clients.py +50 -0
- usecix-1.0.6/cix/integrations/docs.py +347 -0
- usecix-1.0.6/cix/integrations/gemini.py +240 -0
- usecix-1.0.6/cix/integrations/windows.py +190 -0
- usecix-1.0.6/cix/lib/__init__.py +0 -0
- usecix-1.0.6/cix/lib/file_claims.py +63 -0
- usecix-1.0.6/cix/lib/git.py +59 -0
- usecix-1.0.6/cix/lib/naming.py +36 -0
- usecix-1.0.6/cix/lib/project_config.py +235 -0
- usecix-1.0.6/cix/lib/project_id.py +105 -0
- usecix-1.0.6/cix/lib/registration_guard.py +350 -0
- usecix-1.0.6/cix/lib/rewrite.py +70 -0
- usecix-1.0.6/cix/lib/schema_json.py +178 -0
- usecix-1.0.6/cix/lib/testdb.py +133 -0
- usecix-1.0.6/cix/mcp_server/__init__.py +0 -0
- usecix-1.0.6/cix/mcp_server/main.py +576 -0
- usecix-1.0.6/cix/mcp_server/watcher.py +507 -0
- usecix-1.0.6/cix/uninstall.py +538 -0
- usecix-1.0.6/packages/shared-schema/py/cix_shared_schema/__init__.py +17 -0
- usecix-1.0.6/packages/shared-schema/py/cix_shared_schema/parse_error.py +21 -0
- usecix-1.0.6/packages/shared-schema/py/cix_shared_schema/sync_envelope.py +125 -0
- usecix-1.0.6/pyproject.toml +115 -0
- usecix-1.0.6/setup.cfg +4 -0
- usecix-1.0.6/usecix.egg-info/PKG-INFO +110 -0
- usecix-1.0.6/usecix.egg-info/SOURCES.txt +126 -0
- usecix-1.0.6/usecix.egg-info/dependency_links.txt +1 -0
- usecix-1.0.6/usecix.egg-info/entry_points.txt +36 -0
- usecix-1.0.6/usecix.egg-info/requires.txt +25 -0
- usecix-1.0.6/usecix.egg-info/top_level.txt +2 -0
usecix-1.0.6/LICENSE
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
cix — Proprietary Software License
|
|
2
|
+
Copyright (c) 2026 cix. All rights reserved.
|
|
3
|
+
|
|
4
|
+
This software, including the cix client, command-line tools, MCP server,
|
|
5
|
+
and all associated files (collectively, the "Software"), is the proprietary
|
|
6
|
+
and confidential property of cix. The Software is licensed, not sold.
|
|
7
|
+
|
|
8
|
+
This is NOT open-source software. It is NOT licensed under the MIT license,
|
|
9
|
+
and it is NOT dual-licensed.
|
|
10
|
+
|
|
11
|
+
1. Grant. Subject to a valid subscription and the cix Terms of Service, you
|
|
12
|
+
are granted a limited, non-exclusive, non-transferable, revocable license
|
|
13
|
+
to install and use the Software solely to interact with the cix cloud
|
|
14
|
+
service for your own internal development purposes.
|
|
15
|
+
|
|
16
|
+
2. Restrictions. You may not, in whole or in part: (a) copy, redistribute,
|
|
17
|
+
sublicense, sell, rent, or lease the Software; (b) reverse engineer,
|
|
18
|
+
decompile, or disassemble the Software, or attempt to derive its source
|
|
19
|
+
code, parsing logic, or indexing heuristics; (c) create derivative works;
|
|
20
|
+
(d) remove or alter any proprietary notices; or (e) use the Software to
|
|
21
|
+
build a competing product.
|
|
22
|
+
|
|
23
|
+
3. Ownership. All right, title, and interest in and to the Software,
|
|
24
|
+
including all intellectual property rights, remain exclusively with cix.
|
|
25
|
+
|
|
26
|
+
4. Termination. This license terminates automatically if you breach any of
|
|
27
|
+
its terms. Upon termination you must cease all use and destroy all copies.
|
|
28
|
+
|
|
29
|
+
5. No Warranty. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
|
|
30
|
+
KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
31
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.
|
|
32
|
+
|
|
33
|
+
6. Limitation of Liability. IN NO EVENT SHALL cix BE LIABLE FOR ANY CLAIM,
|
|
34
|
+
DAMAGES, OR OTHER LIABILITY ARISING FROM OR IN CONNECTION WITH THE
|
|
35
|
+
SOFTWARE OR ITS USE.
|
|
36
|
+
|
|
37
|
+
For licensing inquiries: legal@usecix.com
|
usecix-1.0.6/PKG-INFO
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: usecix
|
|
3
|
+
Version: 1.0.6
|
|
4
|
+
Summary: Git-anchored cloud-first code index + MCP for Claude, Codex, and Gemini
|
|
5
|
+
License-Expression: LicenseRef-cix-Proprietary
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: mcp>=1.0.0
|
|
10
|
+
Requires-Dist: pyjwt[crypto]>=2.8
|
|
11
|
+
Requires-Dist: tqdm>=4.65
|
|
12
|
+
Requires-Dist: watchfiles>=0.24
|
|
13
|
+
Provides-Extra: remote
|
|
14
|
+
Requires-Dist: uvicorn>=0.31; extra == "remote"
|
|
15
|
+
Requires-Dist: starlette>=0.41; extra == "remote"
|
|
16
|
+
Provides-Extra: db-pull
|
|
17
|
+
Requires-Dist: psycopg[binary]>=3.1; extra == "db-pull"
|
|
18
|
+
Requires-Dist: pymysql>=1.1; extra == "db-pull"
|
|
19
|
+
Requires-Dist: pymongo>=4.6; extra == "db-pull"
|
|
20
|
+
Requires-Dist: PyYAML>=6.0; extra == "db-pull"
|
|
21
|
+
Provides-Extra: test
|
|
22
|
+
Requires-Dist: testcontainers>=4.0; extra == "test"
|
|
23
|
+
Requires-Dist: psycopg[binary]>=3.1; extra == "test"
|
|
24
|
+
Requires-Dist: pymysql>=1.1; extra == "test"
|
|
25
|
+
Requires-Dist: pymongo>=4.6; extra == "test"
|
|
26
|
+
Requires-Dist: PyYAML>=6.0; extra == "test"
|
|
27
|
+
Provides-Extra: build
|
|
28
|
+
Requires-Dist: build>=1.0; extra == "build"
|
|
29
|
+
Requires-Dist: nuitka>=2.0; extra == "build"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# cix
|
|
33
|
+
|
|
34
|
+
**Git moves code. cix understands code. AI coding tools use cix to make safer changes.**
|
|
35
|
+
|
|
36
|
+
cix is a code-intelligence layer that sits between Git and AI coding tools
|
|
37
|
+
(Claude Code, Codex, Gemini). It indexes your committed source by Git commit
|
|
38
|
+
and exposes it to your AI client as a set of structured tools — symbol search,
|
|
39
|
+
navigation, schema lookup, impact analysis, and structured edits — so the
|
|
40
|
+
assistant works from what your repository *actually* contains instead of
|
|
41
|
+
guessing.
|
|
42
|
+
|
|
43
|
+
## Why
|
|
44
|
+
|
|
45
|
+
AI coding tools are strong at writing code in isolation and weak at
|
|
46
|
+
understanding *your* repository. They re-read files they have already seen,
|
|
47
|
+
reinvent helpers that already exist, import modules that were deprecated, or
|
|
48
|
+
rename a function and miss half of its callers. cix gives the assistant a
|
|
49
|
+
precise, current answer to "what does this repository mean right now?" — so it
|
|
50
|
+
writes code that fits.
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install usecix
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The heavy indexing runs in the cix cloud, not on your machine; the local
|
|
59
|
+
package is a thin client that connects over HTTPS.
|
|
60
|
+
|
|
61
|
+
## Quick start
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# 1. Connect this machine to your cix account (opens a browser)
|
|
65
|
+
cix login
|
|
66
|
+
|
|
67
|
+
# 2. Register cix with your AI clients (Claude Code, Codex, Gemini)
|
|
68
|
+
cix install --client all
|
|
69
|
+
|
|
70
|
+
# 3. Restart your client so it picks up the cix tools
|
|
71
|
+
|
|
72
|
+
# 4. In a project you want indexed
|
|
73
|
+
cd ~/path/to/your/project
|
|
74
|
+
cix index
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Then open your AI client in that project and start working — it will use the
|
|
78
|
+
cix tools to search, read by symbol, check schemas, and assess impact before
|
|
79
|
+
editing.
|
|
80
|
+
|
|
81
|
+
## Commands
|
|
82
|
+
|
|
83
|
+
cix installs a single `cix` dispatcher; each subcommand is also available as
|
|
84
|
+
`cix-<command>` (for example, `cix-index`).
|
|
85
|
+
|
|
86
|
+
| Command | What it does |
|
|
87
|
+
| --- | --- |
|
|
88
|
+
| `cix login` | Authenticate and fetch credentials |
|
|
89
|
+
| `cix install` | Install cix integrations (Claude, Codex, Gemini) |
|
|
90
|
+
| `cix uninstall` | Remove cix integrations |
|
|
91
|
+
| `cix index` | Build / update the index for the current project |
|
|
92
|
+
| `cix mcp` | Run the cix MCP server (stdio) |
|
|
93
|
+
| `cix serve` | Run the cix MCP server (remote SSE) |
|
|
94
|
+
| `cix doctor` | Diagnose your cix install and project setup |
|
|
95
|
+
| `cix config` | View / update per-project settings |
|
|
96
|
+
|
|
97
|
+
Run `cix <command> --help` for details on any command.
|
|
98
|
+
|
|
99
|
+
## Requirements
|
|
100
|
+
|
|
101
|
+
- **Python 3.11+**
|
|
102
|
+
- **Git** — cix indexes committed source and tracks freshness by commit.
|
|
103
|
+
- **A GitHub-hosted repository** for the project you want indexed.
|
|
104
|
+
- **A supported AI client** — Claude Code, Codex CLI, and/or Gemini CLI.
|
|
105
|
+
- **A cix account** — `cix login` walks you through it.
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
cix is proprietary software. See the bundled `LICENSE` file. It is not open
|
|
110
|
+
source, and is not MIT- or dual-licensed.
|
usecix-1.0.6/README.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# cix — Code Index for Claude and Codex
|
|
2
|
+
|
|
3
|
+
> **Git moves code. cix understands code. AI clients use cix to make safer changes.**
|
|
4
|
+
|
|
5
|
+
cix is a semantic intelligence layer that sits between Git and AI coding tools (Claude Code, Codex, Gemini, ChatGPT, GitHub PR bots). It indexes committed source by Git SHA, exposes structured queries (symbols, routes, schemas, callers, impact), and validates AI-proposed edits before they hit the repo.
|
|
6
|
+
|
|
7
|
+
> **Status:** cix is in **v0 (cloud-first preview)**. The cloud index, MCP tool surface, convention hooks, and per-edit validation are used daily on the dev project.
|
|
8
|
+
|
|
9
|
+
> **Docs:** [Goals](docs/goals.md) · [Components](docs/components.md) · [Problem](docs/problem.md) · [Roadmap](docs/roadmap.md) · [Tools](docs/tools.md) · [Where content lives](docs/where_content_lives.md)
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## How it works
|
|
14
|
+
|
|
15
|
+
cix is split across four repos, but you only install one:
|
|
16
|
+
|
|
17
|
+
- **`cix`** (this repo) — the thin client. CLI, MCP server, edit application, freshness gate. Ships to your machine via `pipx`.
|
|
18
|
+
- **`cix-api`** — the cloud backend. Owns the parser, the indexer, and the symbol DB. Clones repos via Git provider OAuth, parses with the proprietary parser, stores the symbol graph in cloud Neon. Never ships to clients.
|
|
19
|
+
- **`cix-cloud`** — auth, billing, telemetry, dashboard data feeds.
|
|
20
|
+
- **`cix-web`** — the dashboard at usecix.com.
|
|
21
|
+
|
|
22
|
+
When an AI tool calls a cix MCP tool (e.g. `search_code`), the local client routes it to cix-api. When you edit a file, the local client POSTs the new bytes to cix-api `/v1/validate`, which parses, returns ok/err, and populates an overlay index so the next read sees the fresh shape. The local process never holds the parser or the index — it's a thin client over the cloud.
|
|
23
|
+
|
|
24
|
+
See [Components](docs/components.md) for the full architecture and [Goals](docs/goals.md) for the contract.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## What cix gives an AI agent
|
|
29
|
+
|
|
30
|
+
Coding agents waste tokens rediscovering the same project on every prompt. They `Read` files they've read before, `Glob` directories that haven't moved, `Grep` strings whose home they already know. Each rediscovery costs latency, tokens, and — critically — context window.
|
|
31
|
+
|
|
32
|
+
cix replaces those blind loops with indexed navigation against the cloud index:
|
|
33
|
+
|
|
34
|
+
- **Search before write.** `search_code`, `get_symbol`, `find_usages` answer "does this exist? who calls it?" in milliseconds against a parser-built symbol graph that lives in cloud Neon.
|
|
35
|
+
- **Read by symbol, not by file.** `get_symbol`, `get_file_outline` return only the slice the agent needs. A 500-line file becomes a 30-line symbol.
|
|
36
|
+
- **Impact before edit.** `impact_analysis` answers "what would break if X changed?" — direct + transitive callers, affected HTTP routes, GraphQL handlers, tests, and data models, with a risk tier and reasons.
|
|
37
|
+
- **Schema before query.** `get_schema` returns parsed table definitions for Laravel, Django, Alembic / SQLAlchemy, Prisma, EF Core (and surfaces ORM evidence when no concrete schema is indexed), raw SQL — so the agent stops inventing column names.
|
|
38
|
+
- **Validation at edit time.** When the agent writes a file, the local cix client POSTs the new bytes to cix-api `/v1/validate`. Cloud parses, returns ok/err, and refreshes the overlay so the next read is current — no separate reindex step.
|
|
39
|
+
- **Convention enforcement at write time.** A PreToolUse hook blocks writes that violate `conventions.json` folder/naming rules and blocks new files whose stem collides with an existing indexed symbol.
|
|
40
|
+
- **Honest freshness.** Every read carries `canonical_sha` and `pending_edits`. cix never claims more certainty than it has.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Quick start
|
|
45
|
+
|
|
46
|
+
From zero to cix working inside Claude / Codex in about three minutes:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# 1. Install the package (pipx keeps it out of your project venvs)
|
|
50
|
+
pipx install git+https://github.com/usecix/cix
|
|
51
|
+
|
|
52
|
+
# 2. Register with your clients
|
|
53
|
+
# --client accepts: claude, codex, gemini, a comma-separated subset, or all
|
|
54
|
+
cix-install --client all
|
|
55
|
+
|
|
56
|
+
# 3. Connect this client to cix-api (one-time, opens a browser)
|
|
57
|
+
cix-login
|
|
58
|
+
|
|
59
|
+
# 4. Restart Claude Code, Codex, and/or Gemini CLI so they pick up the new MCP servers.
|
|
60
|
+
|
|
61
|
+
# 5. Inside each project you want indexed
|
|
62
|
+
cd ~/sites/myproject
|
|
63
|
+
cix-init --client all
|
|
64
|
+
# ▸ kicks off a cloud index of the repo via cix-api (~30-60s)
|
|
65
|
+
# ▸ MCP picks up the canonical index automatically when ready
|
|
66
|
+
|
|
67
|
+
# 6. Open your client in the project and start asking it to do things.
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
cix-init also installs a Git push-side hook so subsequent pushes to your default branch trigger an incremental reindex via the GitHub App webhook. Out-of-band edits (manual edits in another editor) trigger an overlay revalidation through the same path Claude / Codex go through.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## What to try first
|
|
75
|
+
|
|
76
|
+
After install + `cix-init`, restart your client and give it a real task. You should see it call cix tools before reaching for `Read` / `Glob` / `Grep`. Good first prompts on a freshly indexed project:
|
|
77
|
+
|
|
78
|
+
- *"Where is the user authentication handler?"* — should call `search_code` or `find_backend_handler`, not `Glob`.
|
|
79
|
+
- *"Add a new component called X"* — should call `check_convention` and `search_code` before writing.
|
|
80
|
+
- *"What columns are on the invoices table?"* — should call `get_schema` before answering.
|
|
81
|
+
|
|
82
|
+
If the client jumps straight to file reads without calling cix, restart it (MCP servers load at client boot, not at `/clear`) and re-run `cix-doctor` to confirm registration + cix-api connectivity.
|
|
83
|
+
|
|
84
|
+
After a `git pull`: run `cix catchup` for a backward-looking briefing — new routes / removed services / schema changes since your last visit. Pair with `--mine` to filter to deltas your current branch touches. Team-tier; see [`docs/feature_cix_catchup.md`](docs/feature_cix_catchup.md).
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## What works today
|
|
89
|
+
|
|
90
|
+
- **Cloud index via cix-api.** GitHub App OAuth → clone → parse → write to cloud Neon. Push webhook reindexes incrementally.
|
|
91
|
+
- **MCP tool surface** for symbol search, navigation, schema lookup, file outlines, impact analysis, structured edits, and refactor-rename. All routed through cix-api. Full list in [Tools](docs/tools.md).
|
|
92
|
+
- **Per-edit validation.** When the agent writes a file, the local client POSTs to cix-api `/v1/validate` and surfaces ok/err. No separate reindex step.
|
|
93
|
+
- **Pre-commit impact summary.** Every `git commit` prints a one-line headline plus a per-symbol caller tree before the commit lands — `Impact: 3 symbols changed, 14 callers affected across 2 files` and the routes / schema columns touched. Always exits 0 so it informs but never blocks. Configurable via `.cix/config.json` (`precommit.enabled`, `precommit.warn_threshold`, `precommit.max_callers_per_symbol`).
|
|
94
|
+
- **Silent Diff pre-commit guard.** Same hook, narrower job: interrupts only when your staged change deletes / renames / breaks the signature of a symbol that still has callers in files you haven't touched in the same commit. `[y]` continues, `[n]` aborts, `[show me]` expands. Fail-open on stale index / unreachable service / no TTY. Toggle with `cix config --no-guard`. See [feature_silent_diff.md](docs/feature_silent_diff.md).
|
|
95
|
+
- **Convention enforcement.** A PreToolUse hook blocks writes that violate `conventions.json` folder / naming rules, and blocks new files whose filename stem collides with an existing indexed symbol.
|
|
96
|
+
- **DB schema indexing** for Laravel, Django, Alembic / SQLAlchemy, Prisma, EF Core, and raw SQL. `get_schema` returns parsed table definitions; the instruction docs tell the client to call it before any model / migration / query.
|
|
97
|
+
- **Freshness metadata on every response** — `canonical_sha`, `pending_edits`, `trust_level`, `stale` — so the agent knows when to trust cix and when to re-check.
|
|
98
|
+
- **Multi-client support.** Claude Code, Codex CLI, Gemini CLI all get the MCP tools. Hooks (PreToolUse convention block, PostToolUse validate, SessionStart guide) are currently Claude-only.
|
|
99
|
+
|
|
100
|
+
See [Tools](docs/tools.md) for the full surface and [Components](docs/components.md) for the architecture.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Known beta limitations
|
|
105
|
+
|
|
106
|
+
- **First-time index requires a Git remote.** cix-api clones the repo via the GitHub App. Repos without a remote — or repos behind a Git host we don't yet support — won't index. GitLab and Bitbucket OAuth are on the v1 path but not landed; today, GitHub-hosted repos only.
|
|
107
|
+
- **Hooks are Claude-only.** Codex and Gemini get the MCP tools but not the PreToolUse convention block, the PostToolUse validate, or the SessionStart tool-order guide. Parity is on the roadmap; it is not done.
|
|
108
|
+
- **No PyPI release yet.** Install is `pipx install git+https://...`. Tag-and-publish to PyPI lands with the v0.1 ship.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## How to report useful failures
|
|
113
|
+
|
|
114
|
+
A good failure report is reproducible by someone who has never seen your project. Please include:
|
|
115
|
+
|
|
116
|
+
1. **The literal prompt** you gave the agent — not a paraphrase.
|
|
117
|
+
2. **The cix tool call and its raw response** — including the `canonical_sha`, `pending_edits`, `trust_level`, `stale`, and `error_code` fields. If the agent skipped cix entirely, say so explicitly.
|
|
118
|
+
3. **What the file or symbol actually looks like on disk** at the moment of the call — paste the relevant span or attach the file.
|
|
119
|
+
4. **`cix-doctor` output** — captures install mode, cix-api connectivity, and client registration.
|
|
120
|
+
5. **The cix version** — `pipx list | grep cix` or `pip show cix`.
|
|
121
|
+
|
|
122
|
+
What we don't need: long IDE transcripts, screenshots, or "it felt slow." A five-line repro is worth more than a 500-line trace.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Requirements
|
|
127
|
+
|
|
128
|
+
- **Python 3.11+** — cix uses 3.11 stdlib features (`tomllib` and friends). `python3 --version` to check.
|
|
129
|
+
- **Git** — cix's freshness model and the canonical reindex both rely on a Git remote.
|
|
130
|
+
- **A GitHub-hosted repo** for the project you want indexed. GitLab and Bitbucket are on the v1 path; not yet supported.
|
|
131
|
+
- **A coding-agent client** — Claude Code, Codex CLI, and / or Gemini CLI. `cix-install` configures whichever you have.
|
|
132
|
+
- **A cix account** — `cix-login` walks you through it (free tier covers one public repo).
|
|
133
|
+
- **pipx** (recommended for install) — see [Why pipx](#why-pipx) below. Plain `pip` works too — see [Don't want pipx? Use pip](#dont-want-pipx-use-pip).
|
|
134
|
+
|
|
135
|
+
### Why pipx
|
|
136
|
+
|
|
137
|
+
cix is a CLI tool, not a Python library you `import` from your code. `pipx` installs it into an isolated virtualenv and links the entry points (`cix`, `cix-init`, `cix-doctor`, `cix-mcp`) onto your `PATH`. Three concrete benefits:
|
|
138
|
+
|
|
139
|
+
- **No dependency collisions.** cix pins `tree-sitter`, `httpx`, and a handful of other libraries. With pipx those pins live in cix's own venv — they can't fight whatever your project venv pins.
|
|
140
|
+
- **Runnable from any cwd.** MCP clients spawn `cix-mcp` directly. No `cd ~/sites/cix && source venv/bin/activate` step before each run.
|
|
141
|
+
- **Clean upgrades and uninstalls.** `pipx upgrade cix` or `pipx uninstall cix` touches only cix's own venv — nothing else on the system.
|
|
142
|
+
|
|
143
|
+
It's the same install model as `ruff`, `black`, `poetry`, and most other CLI-first Python tools. To install pipx itself: `brew install pipx` on macOS, or `python3 -m pip install --user pipx && python3 -m pipx ensurepath` elsewhere.
|
|
144
|
+
|
|
145
|
+
### Don't want pipx? Use pip
|
|
146
|
+
|
|
147
|
+
Plain `pip` in a virtualenv works too:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
python3 -m venv ~/.local/cix-venv
|
|
151
|
+
~/.local/cix-venv/bin/pip install git+https://github.com/usecix/cix
|
|
152
|
+
|
|
153
|
+
# Then either put the venv's bin dir on PATH:
|
|
154
|
+
echo 'export PATH="$HOME/.local/cix-venv/bin:$PATH"' >> ~/.zshrc
|
|
155
|
+
# ...or symlink the entry points individually into ~/.local/bin.
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Installing into a project's existing venv or your global Python also works — but you take on the responsibility of keeping cix's dependencies from clashing with whatever else you have installed there. We don't recommend `sudo pip install` into the system Python on any platform.
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Start here
|
|
163
|
+
|
|
164
|
+
- **New to cix?** Read [The Problem](docs/problem.md) — why this exists.
|
|
165
|
+
- **Want the architecture?** [Components](docs/components.md) covers the four-repo split and the IP boundary.
|
|
166
|
+
- **Want the contract?** [Goals](docs/goals.md) lists the five invariants and the v1 done criteria.
|
|
167
|
+
- **Where are we headed?** [Roadmap](docs/roadmap.md) is the phased migration plan.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# cix
|
|
2
|
+
|
|
3
|
+
**Git moves code. cix understands code. AI coding tools use cix to make safer changes.**
|
|
4
|
+
|
|
5
|
+
cix is a code-intelligence layer that sits between Git and AI coding tools
|
|
6
|
+
(Claude Code, Codex, Gemini). It indexes your committed source by Git commit
|
|
7
|
+
and exposes it to your AI client as a set of structured tools — symbol search,
|
|
8
|
+
navigation, schema lookup, impact analysis, and structured edits — so the
|
|
9
|
+
assistant works from what your repository *actually* contains instead of
|
|
10
|
+
guessing.
|
|
11
|
+
|
|
12
|
+
## Why
|
|
13
|
+
|
|
14
|
+
AI coding tools are strong at writing code in isolation and weak at
|
|
15
|
+
understanding *your* repository. They re-read files they have already seen,
|
|
16
|
+
reinvent helpers that already exist, import modules that were deprecated, or
|
|
17
|
+
rename a function and miss half of its callers. cix gives the assistant a
|
|
18
|
+
precise, current answer to "what does this repository mean right now?" — so it
|
|
19
|
+
writes code that fits.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install usecix
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The heavy indexing runs in the cix cloud, not on your machine; the local
|
|
28
|
+
package is a thin client that connects over HTTPS.
|
|
29
|
+
|
|
30
|
+
## Quick start
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# 1. Connect this machine to your cix account (opens a browser)
|
|
34
|
+
cix login
|
|
35
|
+
|
|
36
|
+
# 2. Register cix with your AI clients (Claude Code, Codex, Gemini)
|
|
37
|
+
cix install --client all
|
|
38
|
+
|
|
39
|
+
# 3. Restart your client so it picks up the cix tools
|
|
40
|
+
|
|
41
|
+
# 4. In a project you want indexed
|
|
42
|
+
cd ~/path/to/your/project
|
|
43
|
+
cix index
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Then open your AI client in that project and start working — it will use the
|
|
47
|
+
cix tools to search, read by symbol, check schemas, and assess impact before
|
|
48
|
+
editing.
|
|
49
|
+
|
|
50
|
+
## Commands
|
|
51
|
+
|
|
52
|
+
cix installs a single `cix` dispatcher; each subcommand is also available as
|
|
53
|
+
`cix-<command>` (for example, `cix-index`).
|
|
54
|
+
|
|
55
|
+
| Command | What it does |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| `cix login` | Authenticate and fetch credentials |
|
|
58
|
+
| `cix install` | Install cix integrations (Claude, Codex, Gemini) |
|
|
59
|
+
| `cix uninstall` | Remove cix integrations |
|
|
60
|
+
| `cix index` | Build / update the index for the current project |
|
|
61
|
+
| `cix mcp` | Run the cix MCP server (stdio) |
|
|
62
|
+
| `cix serve` | Run the cix MCP server (remote SSE) |
|
|
63
|
+
| `cix doctor` | Diagnose your cix install and project setup |
|
|
64
|
+
| `cix config` | View / update per-project settings |
|
|
65
|
+
|
|
66
|
+
Run `cix <command> --help` for details on any command.
|
|
67
|
+
|
|
68
|
+
## Requirements
|
|
69
|
+
|
|
70
|
+
- **Python 3.11+**
|
|
71
|
+
- **Git** — cix indexes committed source and tracks freshness by commit.
|
|
72
|
+
- **A GitHub-hosted repository** for the project you want indexed.
|
|
73
|
+
- **A supported AI client** — Claude Code, Codex CLI, and/or Gemini CLI.
|
|
74
|
+
- **A cix account** — `cix login` walks you through it.
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
cix is proprietary software. See the bundled `LICENSE` file. It is not open
|
|
79
|
+
source, and is not MIT- or dual-licensed.
|
|
File without changes
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""Poll a cix-api reindex job to completion, emitting one line per phase.
|
|
2
|
+
|
|
3
|
+
Shared by cix-init's _kick_cloud_index and cix-index's full-reindex
|
|
4
|
+
trigger so customers get the same UX (and the same Ctrl-C escape
|
|
5
|
+
hatch) regardless of which entry point started the job.
|
|
6
|
+
|
|
7
|
+
Cloud reindex jobs are async: the kickoff endpoint returns a job_id
|
|
8
|
+
immediately, the orchestrator runs in a Vercel sandbox for ~30-60s,
|
|
9
|
+
then writes status back. We print a line each time the backend reports
|
|
10
|
+
a new heartbeat stage — no fake percentage, no spinner pinned at 100%.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import sys
|
|
14
|
+
import time
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from cix.cloud.config import CloudConfig
|
|
19
|
+
from cix.cloud.transport import TransportError, TransportNotConfigured, repos_index_job
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
POLL_INTERVAL_S = 2
|
|
23
|
+
TIMEOUT_S = 300
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def wait_for_index_job(
|
|
27
|
+
config: CloudConfig,
|
|
28
|
+
job_id: str,
|
|
29
|
+
*,
|
|
30
|
+
label: str = "Indexing",
|
|
31
|
+
bar_indent: str = " ",
|
|
32
|
+
out=None,
|
|
33
|
+
) -> dict[str, Any] | None:
|
|
34
|
+
"""Poll ``/v1/repos/index/{job_id}`` until the job finishes.
|
|
35
|
+
|
|
36
|
+
Returns the final job row on success/failed, or ``None`` if the
|
|
37
|
+
user interrupted (Ctrl-C) or the wait timed out. In every
|
|
38
|
+
non-success case we print a one-line note so the customer knows
|
|
39
|
+
where things stand.
|
|
40
|
+
|
|
41
|
+
Emits one line per backend phase transition (sourced from the
|
|
42
|
+
indexer's heartbeat ``last_heartbeat_stage``) so the user sees real
|
|
43
|
+
motion through the run rather than a fake percentage:
|
|
44
|
+
|
|
45
|
+
{indent}→ preparing sandbox
|
|
46
|
+
{indent}→ cloning repo
|
|
47
|
+
{indent}→ refreshing files (3/17)
|
|
48
|
+
{indent}→ finalizing
|
|
49
|
+
|
|
50
|
+
Before the indexer posts its first heartbeat the job row carries
|
|
51
|
+
no stage (the sandbox is still booting/cloning), so we emit
|
|
52
|
+
"preparing sandbox" up-front — otherwise the first seconds of every
|
|
53
|
+
run read as a silent freeze. When the backend exposes no stage
|
|
54
|
+
detail at all we degrade to that single line.
|
|
55
|
+
|
|
56
|
+
On timeout we print the last observed phase and how long the
|
|
57
|
+
heartbeat has been frozen — a stuck sandbox (heartbeat age ≈ the
|
|
58
|
+
whole wait) reads very differently from a genuinely slow one
|
|
59
|
+
(heartbeat still advancing), and that distinction is the first
|
|
60
|
+
thing an operator needs.
|
|
61
|
+
"""
|
|
62
|
+
out = out or sys.stdout
|
|
63
|
+
t0 = time.time()
|
|
64
|
+
current_stage: str | None = None
|
|
65
|
+
last_resp: dict[str, Any] | None = None
|
|
66
|
+
print(f"{bar_indent}→ preparing sandbox", file=out, flush=True)
|
|
67
|
+
try:
|
|
68
|
+
while True:
|
|
69
|
+
elapsed = int(time.time() - t0)
|
|
70
|
+
if elapsed > TIMEOUT_S:
|
|
71
|
+
print(_timeout_note(bar_indent, elapsed, last_resp), file=out)
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
resp = repos_index_job(config, job_id=job_id)
|
|
76
|
+
except (TransportError, TransportNotConfigured):
|
|
77
|
+
# Treat transient network errors as a missed poll, not a fatal
|
|
78
|
+
# condition. The job is still running cloud-side.
|
|
79
|
+
resp = None
|
|
80
|
+
|
|
81
|
+
if resp:
|
|
82
|
+
last_resp = resp
|
|
83
|
+
status = resp.get("status")
|
|
84
|
+
if status in ("success", "failed"):
|
|
85
|
+
return resp
|
|
86
|
+
stage = resp.get("last_heartbeat_stage")
|
|
87
|
+
if stage and stage != current_stage:
|
|
88
|
+
current_stage = stage
|
|
89
|
+
print(
|
|
90
|
+
f"{bar_indent}→ {_stage_label(stage)}",
|
|
91
|
+
file=out,
|
|
92
|
+
flush=True,
|
|
93
|
+
)
|
|
94
|
+
time.sleep(POLL_INTERVAL_S)
|
|
95
|
+
except KeyboardInterrupt:
|
|
96
|
+
print(
|
|
97
|
+
f"\n{bar_indent}→ {label} continues cloud-side (job={job_id[:12]}); "
|
|
98
|
+
f"check with `cix-index`.",
|
|
99
|
+
file=out,
|
|
100
|
+
)
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def print_job_result(
|
|
106
|
+
final: dict[str, Any] | None,
|
|
107
|
+
*,
|
|
108
|
+
started_at: float,
|
|
109
|
+
label: str = "Index",
|
|
110
|
+
indent: str = " ",
|
|
111
|
+
out=None,
|
|
112
|
+
) -> None:
|
|
113
|
+
"""Pretty-print the outcome of a wait_for_index_job call.
|
|
114
|
+
|
|
115
|
+
Separated from ``wait_for_index_job`` so callers that want to
|
|
116
|
+
customise the success/failure messaging can omit it. ``started_at``
|
|
117
|
+
is the wall-clock time the wait began; used to render duration.
|
|
118
|
+
"""
|
|
119
|
+
if final is None:
|
|
120
|
+
# Already printed a note inside wait_for_index_job.
|
|
121
|
+
return
|
|
122
|
+
out = out or sys.stdout
|
|
123
|
+
elapsed = max(0, int(time.time() - started_at))
|
|
124
|
+
status = final.get("status")
|
|
125
|
+
mode = final.get("mode") # may be None on older cix-api responses
|
|
126
|
+
if status == "skip":
|
|
127
|
+
# Server short-circuited because nothing changed since the last
|
|
128
|
+
# cloud index. No sandbox provisioned, no work done.
|
|
129
|
+
print(f"{indent}✓ No changes to index — skipped ({elapsed}s).", file=out)
|
|
130
|
+
return
|
|
131
|
+
if status == "success":
|
|
132
|
+
symbols = final.get("symbol_count")
|
|
133
|
+
symbols_str = f"{symbols:,}" if isinstance(symbols, int) else "?"
|
|
134
|
+
if mode == "incremental":
|
|
135
|
+
files_touched = final.get("files_touched")
|
|
136
|
+
if isinstance(files_touched, int):
|
|
137
|
+
plural = "" if files_touched == 1 else "s"
|
|
138
|
+
# "5,446 total symbols" makes clear the count is the
|
|
139
|
+
# repo-wide total, not what this run processed.
|
|
140
|
+
print(
|
|
141
|
+
f"{indent}✓ Indexed {files_touched} file{plural}; "
|
|
142
|
+
f"{symbols_str} total symbols ({elapsed}s).",
|
|
143
|
+
file=out,
|
|
144
|
+
)
|
|
145
|
+
else:
|
|
146
|
+
# Old job rows pre-files_touched column. Fall back gracefully.
|
|
147
|
+
print(
|
|
148
|
+
f"{indent}✓ Indexed (incremental); {symbols_str} total symbols "
|
|
149
|
+
f"({elapsed}s).",
|
|
150
|
+
file=out,
|
|
151
|
+
)
|
|
152
|
+
elif mode == "full_rebuild":
|
|
153
|
+
print(
|
|
154
|
+
f"{indent}✓ Indexed {symbols_str} symbols (full rebuild, {elapsed}s).",
|
|
155
|
+
file=out,
|
|
156
|
+
)
|
|
157
|
+
else:
|
|
158
|
+
# Old cix-api response without mode field — keep prior phrasing.
|
|
159
|
+
suffix = f"{symbols_str} symbols" if symbols_str != "?" else "indexed"
|
|
160
|
+
print(f"{indent}✓ {label} ready ({suffix}, {elapsed}s).", file=out)
|
|
161
|
+
return
|
|
162
|
+
# Failure path. Surface the stage the job died at (strip the
|
|
163
|
+
# 'stopped-by:' marker prefix so the user sees a clean label) and
|
|
164
|
+
# the error message tail.
|
|
165
|
+
err = (final.get("error_message") or "").strip()
|
|
166
|
+
if err:
|
|
167
|
+
err = err.splitlines()[-1][:200]
|
|
168
|
+
stage = (final.get("last_heartbeat_stage") or "").strip()
|
|
169
|
+
if stage.startswith("stopped-by:"):
|
|
170
|
+
stage = stage[len("stopped-by:"):]
|
|
171
|
+
if stage:
|
|
172
|
+
print(
|
|
173
|
+
f"{indent}✗ {label} failed at {stage} (+{elapsed}s): "
|
|
174
|
+
f"{err or status}",
|
|
175
|
+
file=out,
|
|
176
|
+
)
|
|
177
|
+
else:
|
|
178
|
+
print(f"{indent}✗ {label} failed after {elapsed}s: {err or status}", file=out)
|
|
179
|
+
|
|
180
|
+
def _stage_label(stage: str) -> str:
|
|
181
|
+
"""Map sandbox/indexer.py heartbeat stages to short human labels.
|
|
182
|
+
|
|
183
|
+
The indexer emits granular incremental stages (per-file refresh) and
|
|
184
|
+
coarser full-rebuild stages. We translate both into the honest, stable
|
|
185
|
+
vocabulary the user sees: preparing sandbox -> cloning repo -> detecting
|
|
186
|
+
changes -> refreshing files -> shipping batches -> finalizing -> wrapping
|
|
187
|
+
up. Unknown stages fall through as-is so a new heartbeat added cloud-side
|
|
188
|
+
still shows something useful before the CLI knows the new name.
|
|
189
|
+
|
|
190
|
+
The per-file incremental stage is "incremental-file-{idx}/{total}" — we
|
|
191
|
+
surface the "{idx}/{total}" counter as "refreshing files (3/17)" so the
|
|
192
|
+
user sees real, honest motion through the changed set rather than a raw
|
|
193
|
+
slug. No fake precision: the counter is the indexer's own file index.
|
|
194
|
+
"""
|
|
195
|
+
if stage.startswith("incremental-file-"):
|
|
196
|
+
progress = stage[len("incremental-file-"):]
|
|
197
|
+
return f"refreshing files ({progress})"
|
|
198
|
+
return {
|
|
199
|
+
"starting": "starting",
|
|
200
|
+
"initializing": "starting",
|
|
201
|
+
"cloning": "cloning repo",
|
|
202
|
+
"clone-done": "preparing",
|
|
203
|
+
"incremental-starting": "detecting changes",
|
|
204
|
+
"incremental-done": "refreshing files",
|
|
205
|
+
"shipping-batches": "shipping batches",
|
|
206
|
+
"schema-extracting": "reading schema",
|
|
207
|
+
"history-walking": "recording history",
|
|
208
|
+
"index-status-posting": "finalizing",
|
|
209
|
+
"index-status-unconfirmed": "finalizing (retrying)",
|
|
210
|
+
"finalize-posting": "finalizing",
|
|
211
|
+
"commits-posting": "wrapping up",
|
|
212
|
+
"shutdown-signaling": "wrapping up",
|
|
213
|
+
}.get(stage, stage)
|
|
214
|
+
|
|
215
|
+
def _heartbeat_age_s(last_heartbeat_at: Any) -> int | None:
|
|
216
|
+
"""Seconds since the indexer last emitted a heartbeat.
|
|
217
|
+
|
|
218
|
+
``last_heartbeat_at`` is the ISO-8601 string from the job row
|
|
219
|
+
(``_serialise`` in cix-api). Returns None if absent or unparseable
|
|
220
|
+
so callers can fall back to a stage-only message. A large value is
|
|
221
|
+
the signal that the sandbox wedged rather than merely running slow.
|
|
222
|
+
"""
|
|
223
|
+
if not isinstance(last_heartbeat_at, str) or not last_heartbeat_at.strip():
|
|
224
|
+
return None
|
|
225
|
+
try:
|
|
226
|
+
hb = datetime.fromisoformat(last_heartbeat_at)
|
|
227
|
+
except ValueError:
|
|
228
|
+
return None
|
|
229
|
+
if hb.tzinfo is None:
|
|
230
|
+
hb = hb.replace(tzinfo=timezone.utc)
|
|
231
|
+
return max(0, int((datetime.now(timezone.utc) - hb).total_seconds()))
|
|
232
|
+
|
|
233
|
+
def _timeout_note(
|
|
234
|
+
bar_indent: str,
|
|
235
|
+
elapsed: int,
|
|
236
|
+
last_resp: dict[str, Any] | None,
|
|
237
|
+
) -> str:
|
|
238
|
+
"""Build the message printed when the CLI poll wait exceeds TIMEOUT_S.
|
|
239
|
+
|
|
240
|
+
Replaces the old "still running after Ns" line, which discarded the
|
|
241
|
+
stage/heartbeat data the poll already had in hand. We now surface:
|
|
242
|
+
|
|
243
|
+
* the last phase the indexer reported (``last_heartbeat_stage``),
|
|
244
|
+
* how long that heartbeat has been frozen.
|
|
245
|
+
|
|
246
|
+
A heartbeat age close to ``elapsed`` means the sandbox wedged at
|
|
247
|
+
that phase (stuck); an age that keeps tracking the poll interval
|
|
248
|
+
means the job is merely slow. That one number is what tells an
|
|
249
|
+
operator which of the two they're looking at.
|
|
250
|
+
"""
|
|
251
|
+
lines = [f"{bar_indent}⚠ Indexing still running after {elapsed}s."]
|
|
252
|
+
if last_resp:
|
|
253
|
+
stage = (last_resp.get("last_heartbeat_stage") or "").strip()
|
|
254
|
+
age = _heartbeat_age_s(last_resp.get("last_heartbeat_at"))
|
|
255
|
+
if stage:
|
|
256
|
+
phase = _stage_label(stage)
|
|
257
|
+
if age is not None:
|
|
258
|
+
frozen = " — heartbeat frozen" if age >= 30 else ""
|
|
259
|
+
lines.append(
|
|
260
|
+
f"{bar_indent} Last phase: {phase} "
|
|
261
|
+
f"(no heartbeat for {age}s{frozen})."
|
|
262
|
+
)
|
|
263
|
+
else:
|
|
264
|
+
lines.append(f"{bar_indent} Last phase: {phase}.")
|
|
265
|
+
elif age is not None:
|
|
266
|
+
lines.append(f"{bar_indent} No heartbeat for {age}s.")
|
|
267
|
+
lines.append(
|
|
268
|
+
f"{bar_indent} Job will keep running cloud-side; "
|
|
269
|
+
f"check later with `cix-index`."
|
|
270
|
+
)
|
|
271
|
+
return "\n".join(lines)
|