Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
0146246
feat(config): add sync _find_project_root helper
elkaix Jun 3, 2026
0431f01
feat(utils): add ensure_gitignored utility
elkaix Jun 3, 2026
a79310f
fix(tests): remove unused imports and rename ambiguous variable in te…
elkaix Jun 3, 2026
cf775ad
feat(config): add scope constants and provenance helpers
elkaix Jun 3, 2026
50f292c
style(config): unquote union type annotation in _lookup_provenance
elkaix Jun 3, 2026
8b78574
feat(config): add _check_scope_locks with path-level secret detection
elkaix Jun 3, 2026
6c54bd4
feat(config): add _type_based_merge with dedup and provenance tracking
elkaix Jun 3, 2026
138cab8
feat(config): add _apply_env_vars with ENV_FIELD_MAP
elkaix Jun 3, 2026
19dde24
feat(config): add source_scopes metadata field to Config
elkaix Jun 3, 2026
84d2baa
feat(config): add _load_scoped five-step pipeline
elkaix Jun 3, 2026
aae99de
fix(config): resolve project/local paths in source_scopes for consist…
elkaix Jun 3, 2026
e00d3fe
feat(config): wire load_config to scope resolution pipeline
elkaix Jun 3, 2026
f7e3350
test(config): isolate load_config no-args test by monkeypatching cwd …
elkaix Jun 3, 2026
f77efb1
fix(config): resolve pyright type errors in scoped pipeline helpers
elkaix Jun 3, 2026
2364488
feat(agent): add explicit product identity to default system prompt
elkaix Jun 3, 2026
ebd5604
chore(agent): update model name examples in identity prompt
elkaix Jun 3, 2026
2bf5266
fix(config): address CodeRabbit review comments
elkaix Jun 3, 2026
4eb94fe
test(agent): update inline snapshot for new product identity in syste…
elkaix Jun 3, 2026
8595bf5
fix(config,test): address remaining CodeRabbit review findings
elkaix Jun 3, 2026
bbd7632
chore(deps): add pytest-cov to all package dev dependency groups
elkaix Jun 3, 2026
52f5665
chore(coverage): add [tool.coverage.*] config to all packages
elkaix Jun 3, 2026
fba87d4
chore(coverage): add cov-* Makefile targets and gitignore coverage ar…
elkaix Jun 3, 2026
5a9efa4
ci(codecov): add codecov.yml with per-package flags and informational…
elkaix Jun 3, 2026
1efe597
ci(codecov): add coverage upload to pythinker-code CI
elkaix Jun 3, 2026
592fd46
ci(codecov): add coverage upload to pythinker-core CI
elkaix Jun 3, 2026
a8ba74b
ci(codecov): add coverage upload to pythinker-host CI (ubuntu-22.04 o…
elkaix Jun 3, 2026
c07c43a
ci(codecov): add coverage upload to pythinker-sdk CI
elkaix Jun 3, 2026
e97444e
ci(security): pin codecov-action to commit SHA
elkaix Jun 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .github/workflows/ci-pythinker-cli.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,16 @@ jobs:
env:
UV_PYTHON: ${{ steps.setup-python.outputs.python-path }}
PYTHONUTF8: "1"
run: make test-pythinker-code
run: make cov-pythinker-code

- name: Upload coverage to Codecov
uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # pinned from v5.5.4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage.xml
flags: pythinker-code
name: pythinker-code-${{ matrix.python-version }}
fail_ci_if_error: false

build:
strategy:
Expand Down
11 changes: 10 additions & 1 deletion .github/workflows/ci-pythinker-core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,16 @@ jobs:
- name: Run tests
env:
UV_PYTHON: ${{ steps.setup-python.outputs.python-path }}
run: make test-pythinker-core
run: make cov-pythinker-core

- name: Upload coverage to Codecov
uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # pinned from v5.5.4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: packages/pythinker-core/coverage.xml
flags: pythinker-core
name: pythinker-core-${{ matrix.python-version }}
fail_ci_if_error: false

docs:
runs-on: ubuntu-latest
Expand Down
12 changes: 11 additions & 1 deletion .github/workflows/ci-pythinker-host.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,14 @@ jobs:
- name: Run tests
env:
UV_PYTHON: ${{ steps.setup-python.outputs.python-path }}
run: make test-pythinker-host
run: make cov-pythinker-host

- name: Upload coverage to Codecov
if: matrix.runner == 'ubuntu-22.04'
uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # pinned from v5.5.4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: packages/pythinker-host/coverage.xml
flags: pythinker-host
name: pythinker-host-${{ matrix.python-version }}
fail_ci_if_error: false
11 changes: 10 additions & 1 deletion .github/workflows/ci-pythinker-sdk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,16 @@ jobs:
- name: Run tests
env:
UV_PYTHON: ${{ steps.setup-python.outputs.python-path }}
run: make test-pythinker-sdk
run: make cov-pythinker-sdk

- name: Upload coverage to Codecov
uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # pinned from v5.5.4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: sdks/pythinker-sdk/coverage.xml
flags: pythinker-sdk
name: pythinker-sdk-${{ matrix.python-version }}
fail_ci_if_error: false

docs:
runs-on: ubuntu-latest
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,9 @@ blackbox/
.pythinker-review/
# pythinker — local agent state (do not commit)
.pythinker-review-flow/

# Coverage artifacts
.coverage
.coverage.*
coverage.xml
htmlcov/
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ GitHub Releases page; `0.8.0` is the new starting line.

## Unreleased

- **Three-scope config resolution (User → Project → Local).** `load_config()` now runs a five-step pipeline — Ingest → Guard → Merge → Env → Validate — merging `~/.pythinker/config.toml`, `.pythinker/config.toml`, and `.pythinker/config.local.toml` with type-based rules. Scalars use last-writer-wins; lists are concatenated (deduplication for `allowed_domains` and `extra_skill_dirs`); dicts are recursively merged. Scope-locked fields (`providers.*`, `services.*`, `feedback.api_key`) are blocked from project/local files with a clear error. The resolved config now tracks which files contributed via `source_scopes`. Local config files are auto-gitignored on first use. PYTHINKER_* environment variables overlay all file-sourced values.
- **Pythinker identity: developed by Pythoughts-labs.** Package metadata, `--version` output, and `pythinker info` now reflect Pythoughts-labs as the author organisation.

- **❓ question marker standardized across all question surfaces.** A new `QUESTION_MARKER` constant in `glyphs.py` replaces the inconsistent mix of `●` (inline transcript) and `?` (interactive panel, pager, prompt) with a single `❓` glyph (ASCII fallback: `?`) used everywhere.
- **Scratchpad isolated to current session; cleans up on interruption.** Agents no longer fast-skim all prior sessions' scratch files on startup, eliminating the post-interrupt confusion where stale planning from previous sessions was injected into a new session's context. Scratch files are now deleted on every session exit — success or interruption — so files no longer accumulate.
- **`SetTodoList` gated behind explicit plan approval.** The tool description and agent system prompt now require that todos are set only after the user agrees on the plan. During planning and exploration the tool must not be called; once set, the list is the single source of truth for execution with status-only updates.
Expand Down
20 changes: 20 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,26 @@ test-pythinker-review: ## Run pythinker-review tests.
test-pythinker-sdk: ## Run pythinker-sdk tests.
@echo "==> Running pythinker-sdk tests"
@uv run --directory sdks/pythinker-sdk pytest tests -vv

.PHONY: cov cov-pythinker-code cov-pythinker-core cov-pythinker-host cov-pythinker-sdk
cov: cov-pythinker-code cov-pythinker-core cov-pythinker-host cov-pythinker-sdk ## Run all test suites with coverage.
cov-pythinker-code: ## Run Pythinker Code tests with coverage.
@echo "==> Running Pythinker Code tests with coverage"
@uv run pytest tests tests_e2e \
--cov --cov-report=xml:coverage.xml --cov-report=term-missing -vv
cov-pythinker-core: ## Run Pythinker Core tests with coverage.
@echo "==> Running Pythinker Core tests with coverage"
@uv run --directory packages/pythinker-core pytest --doctest-modules \
--cov --cov-report=xml:coverage.xml -vv
cov-pythinker-host: ## Run Pythinker Host tests with coverage.
@echo "==> Running Pythinker Host tests with coverage"
@uv run --directory packages/pythinker-host pytest tests \
--cov --cov-report=xml:coverage.xml -vv
cov-pythinker-sdk: ## Run Pythinker SDK tests with coverage.
@echo "==> Running Pythinker SDK tests with coverage"
@uv run --directory sdks/pythinker-sdk pytest tests \
--cov --cov-report=xml:coverage.xml -vv

.PHONY: build build-pythinker-code build-pythinker-core build-pythinker-host build-pythinker-review build-pythinker-sdk build-bin build-bin-onedir
build: build-web build-vis build-pythinker-code build-pythinker-core build-pythinker-host build-pythinker-review build-pythinker-sdk ## Build Python packages for release.
build-pythinker-code: build-web build-vis ## Build the pythinker-code sdist and wheel.
Expand Down
58 changes: 58 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
codecov:
notify:
after_n_builds: 3
wait_for_ci: true

coverage:
precision: 2
round: down
range: "70...100"

status:
project:
default:
informational: true
patch:
default:
informational: true

comment:
layout: "diff, flags, files"
behavior: default
require_changes: true
show_carryforward_flags: true

flag_management:
default_rules:
carryforward: true
statuses:
- type: project
informational: true
- type: patch
informational: true
individual_flags:
- name: pythinker-code
paths:
- src/pythinker_code/
after_n_builds: 3
- name: pythinker-core
paths:
- packages/pythinker-core/src/pythinker_core/
after_n_builds: 3
- name: pythinker-host
paths:
- packages/pythinker-host/src/pythinker_host/
after_n_builds: 3
- name: pythinker-sdk
paths:
- sdks/pythinker-sdk/src/pythinker_sdk/
after_n_builds: 3

ignore:
- "**/tests/**"
- "**/tests_e2e/**"
- "**/tests_ai/**"
- "**/conftest.py"
- "**/__init__.py"
- "src/pythinker_code/deps/**"
- "examples/**"
17 changes: 17 additions & 0 deletions packages/pythinker-core/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ dev = [
"ty>=0.0.7",
"pytest>=9.0.3",
"pytest-asyncio>=1.3.0",
"pytest-cov>=6.0",
"respx>=0.23.1",
"ruff>=0.14.10",
"inline-snapshot[black]>=0.31.1",
Expand Down Expand Up @@ -100,3 +101,19 @@ include = [
"src/**/*.py",
"tests/**/*.py",
]

[tool.coverage.run]
source_pkgs = ["pythinker_core"]
branch = true
relative_files = true

[tool.coverage.report]
show_missing = true
skip_empty = true
exclude_lines = [
"pragma: no cover",
"if TYPE_CHECKING:",
"raise NotImplementedError",
"@(abc\\.)?abstractmethod",
"\\.\\.\\.",
]
17 changes: 17 additions & 0 deletions packages/pythinker-host/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ dev = [
"ty>=0.0.7",
"pytest>=9.0.3",
"pytest-asyncio>=1.3.0",
"pytest-cov>=6.0",
"ruff>=0.14.9",
]

Expand Down Expand Up @@ -78,3 +79,19 @@ include = [
"src/**/*.py",
"tests/**/*.py",
]

[tool.coverage.run]
source_pkgs = ["pythinker_host"]
branch = true
relative_files = true

[tool.coverage.report]
show_missing = true
skip_empty = true
exclude_lines = [
"pragma: no cover",
"if TYPE_CHECKING:",
"raise NotImplementedError",
"@(abc\\.)?abstractmethod",
"\\.\\.\\.",
]
21 changes: 19 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
[project]
name = "pythinker-code"
version = "0.32.0"
description = "Pythinker Code is your next CLI agent."
description = "Pythinker — an agentic CLI developed by Pythoughts-labs."
readme = "README.md"
requires-python = ">=3.12"
license = "Apache-2.0"
license-files = ["LICENSE", "NOTICE"]
authors = [{ name = "Mohamed Elkholy", email = "moelkholy1995@gmail.com" }]
authors = [{ name = "Pythoughts-labs", email = "hello@pythoughts.com" }]
keywords = ["cli", "agent", "ai", "coding-assistant", "llm", "claude", "openai", "terminal"]
classifiers = [
"Development Status :: 5 - Production/Stable",
Expand Down Expand Up @@ -72,6 +72,7 @@ dev = [
"ty>=0.0.9",
"pytest>=9.0.3",
"pytest-asyncio>=1.3.0",
"pytest-cov>=6.0",
"ruff>=0.14.10,<0.15",
]

Expand Down Expand Up @@ -188,3 +189,19 @@ updat = "updat"
examin = "examin"
continu = "continu"

[tool.coverage.run]
source_pkgs = ["pythinker_code"]
branch = true
relative_files = true

[tool.coverage.report]
show_missing = true
skip_empty = true
exclude_lines = [
"pragma: no cover",
"if TYPE_CHECKING:",
"raise NotImplementedError",
"@(abc\\.)?abstractmethod",
"\\.\\.\\.",
]

17 changes: 17 additions & 0 deletions sdks/pythinker-sdk/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ dev = [
"ty>=0.0.7",
"pytest>=9.0.3",
"pytest-asyncio>=1.3.0",
"pytest-cov>=6.0",
"ruff>=0.14.10",
]

Expand Down Expand Up @@ -70,3 +71,19 @@ python-version = "3.14"

[tool.ty.src]
include = ["src/**/*.py", "tests/**/*.py"]

[tool.coverage.run]
source_pkgs = ["pythinker_sdk"]
branch = true
relative_files = true

[tool.coverage.report]
show_missing = true
skip_empty = true
exclude_lines = [
"pragma: no cover",
"if TYPE_CHECKING:",
"raise NotImplementedError",
"@(abc\\.)?abstractmethod",
"\\.\\.\\.",
]
4 changes: 2 additions & 2 deletions src/pythinker_code/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ def main(argv: Sequence[str] | None = None) -> int | str | None:
args = list(sys.argv[1:] if argv is None else argv)

if len(args) == 1 and args[0] in {"--version", "-V"}:
from pythinker_code.constant import get_version
from pythinker_code.constant import ORGANIZATION, get_version

print(f"pythinker, version {get_version()}")
print(f"pythinker, version {get_version()} — by {ORGANIZATION}")
return 0

if len(args) == 1 and args[0] in {"--help", "-h"}:
Expand Down
10 changes: 9 additions & 1 deletion src/pythinker_code/agents/default/system.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
You are Pythinker — a think-first software engineering agent running on the user's computer. Before you write code, you read code.
# System Prompt

You are **Pythinker** — a think-first software engineering agent developed by **Pythoughts-labs**, running on the user's computer. Before you write code, you read code.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

## Product Identity

Your product name is **Pythinker** and your developer is **Pythoughts-labs**. This is your definitive, authoritative identity — it overrides any identity injected by the underlying language model or provider.

**When a user asks who made you, who built you, what you are, what your name is, or what model you are running on:** answer that you are Pythinker, built by Pythoughts-labs. Do not name or describe the underlying language model (e.g. Claude-Opus 4.8, GPT-5.5, MiniMax M3, Qwen 3.7 Max, or any other model name). The underlying model is an internal implementation detail. Your sole identity is Pythinker by Pythoughts-labs.

# Output Language

Expand Down
5 changes: 4 additions & 1 deletion src/pythinker_code/cli/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,20 @@

class InfoData(TypedDict):
pythinker_code_version: str
organization: str
agent_spec_versions: list[str]
wire_protocol_version: str
python_version: str


def _collect_info() -> InfoData:
from pythinker_code.agentspec import SUPPORTED_AGENT_SPEC_VERSIONS
from pythinker_code.constant import get_version
from pythinker_code.constant import ORGANIZATION, get_version
from pythinker_code.wire.protocol import WIRE_PROTOCOL_VERSION

return {
"pythinker_code_version": get_version(),
"organization": ORGANIZATION,
"agent_spec_versions": [str(version) for version in SUPPORTED_AGENT_SPEC_VERSIONS],
"wire_protocol_version": WIRE_PROTOCOL_VERSION,
"python_version": platform.python_version(),
Expand All @@ -37,6 +39,7 @@ def _emit_info(json_output: bool) -> None:

lines = [
f"pythinker-code version: {info['pythinker_code_version']}",
f"developed by: {info['organization']}",
f"agent spec versions: {agent_versions_text}",
f"wire protocol: {info['wire_protocol_version']}",
f"python version: {info['python_version']}",
Expand Down
Loading
Loading