Skip to content

Commit 5c27a9a

Browse files
committed
release: v2.44.0 — sub-projects (linked child .c3 branches, federated search/memory) + Hub v2 modular UI (drill-in panel, cross-project search, config editor, sub-project tree)
1 parent 94e3cd9 commit 5c27a9a

46 files changed

Lines changed: 6714 additions & 40 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,73 @@ All notable changes to Code Context Control (C3) are documented here.
44
The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [2.44.0] - 2026-07-02
8+
9+
### Sub-projects: linked child `.c3` branches under one parent
10+
11+
- **Designate sub-folders as governed sub-projects.** `c3 sub add <folder>`
12+
runs a full init in the folder (or *adopts* an existing `.c3`) and links it
13+
three ways: the parent's `.c3/config.json` gains a `subprojects` list
14+
(POSIX `rel_path` is the source of truth), the child config gains a `parent`
15+
back-link, and the child's registry entry in `~/.c3/projects.json` gains
16+
`parent_path`. New `services/subprojects.py` (`SubprojectManager`) owns
17+
designation, validation (depth-1 only, containment enforced, adopt vs init),
18+
unlink/clear removal, consistency reconciliation (`c3 sub check [--fix
19+
--prune]` — statuses `ok/missing_folder/missing_c3/backlink_broken/
20+
unregistered` + registry-orphan cleanup), and cascade operations
21+
(`c3 sub run update|reindex|health [--include-parent] [--json]`).
22+
- **Parent index excludes children.** Designated folders are skipped by the
23+
parent's code index, doc index, compression dictionary, and file watcher via
24+
relative-path-prefix matching (a child named `api` does *not* shadow a
25+
root-level `api/` sibling). The parent reindexes automatically on
26+
add/remove; `c3 init --clear` on a child warns about the remaining parent
27+
link. `transfer_project` repairs child links after a move.
28+
- **Federated search & memory rollup.** `c3_search(scope='all')` fans out to
29+
linked children (per-scope sections `=== [sub:name] ===`, 60/40 parent/child
30+
token split, per-child failures isolated); `scope='<name>'` targets one
31+
child. `c3_memory` recall unions child facts tagged `[sub:name][category]`
32+
(on by default; `hybrid.subprojects.memory_rollup: false` or
33+
`scope='project'` disables). `c3_project` gains `subprojects` (tree +
34+
rollup), `sub_add`, `sub_remove`, and `sub_cascade` actions (writes require
35+
`allow_write=true` and audit to the target's activity log). New
36+
`hybrid.subprojects` config block; runtime cache default 4 → 8
37+
(`C3_RUNTIME_CACHE_SIZE` env override).
38+
- **Hub endpoints.** `GET /api/projects/subprojects` (tree + rollup),
39+
`POST …/add|remove|validate|reconcile`, async `POST …/cascade` +
40+
`status`/`cancel` polling; `GET /api/projects` entries now carry
41+
`parent_path`/`is_parent`; removing a parent reports `orphaned_children`.
42+
43+
### Project Hub v2: modular UI + full project capabilities
44+
45+
- **Monolith retired.** The 187 KB `hub.html` is replaced at `/` by a modular
46+
React bundle (`cli/hub_ui.html` shell + `cli/hub_ui/*` components,
47+
concatenated server-side exactly like the per-project UI) sharing
48+
`cli/ui/theme.js` design tokens — one design system across both UIs. The old
49+
hub remains frozen at `/legacy` for one release as an escape hatch.
50+
- **Modern/minimal redesign.** Slim project rows (status dot, version chip,
51+
alert chip, one mono meta line, primary action, kebab menu) — everything
52+
else moved into a **drill-in panel** (click a project): Overview, Memory,
53+
Ledger, Sessions, Health, Budget, Config, and MCP tabs, served without
54+
launching the per-project UI server (`POST /api/projects/inspect`, backed by
55+
a hub-owned in-process runtime LRU; size via hub config
56+
`runtime_cache_size`).
57+
- **Cross-project search.** Ctrl/Cmd-K overlay queries code + memory across
58+
all registered projects (`POST /api/search/global`, per-project buckets,
59+
broken indexes isolated per row, 10-project cap per request).
60+
- **Structured config editing.** `GET/PUT /api/projects/config` edits
61+
whitelisted `.c3/config.json` sections (hybrid/agents/delegate/proxy/mcp/
62+
meta) with typed controls, defaults for reset, atomic writes, and a
63+
`hub_config_write` audit event on the target project. Protected keys
64+
(`version`, `project_path`, `permission_tier`, `subprojects`, `parent`)
65+
are refused.
66+
- **Sub-project tree.** Parents render as collapsible trees with client-side
67+
rollup chips; designate via a FolderPicker (`POST /api/projects/browse` +
68+
validate pre-check); promote/unlink from the child row; cascade
69+
update/reindex/health with progress toasts.
70+
- **Fixes.** Hub config no longer silently drops `sidebar_group` /
71+
`sidebar_collapsed`; new `runtime_cache_size` key; wheel packaging gains
72+
explicit `cli/ui/*` + `cli/hub_ui/*` globs.
73+
774
## [2.43.0] - 2026-07-02
875

976
### Ghost-file generation fixed at the source

CLAUDE.md

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ When falling back, state which c3_* tool was attempted and why it was insufficie
1717
7. **VALIDATE**: `c3_validate(file_path)` — after edits or before reporting done. Runs deep type check (pyright/tsc) automatically if installed
1818
8. **LOG**: `c3_session(action='log')` for decisions. `c3_session(action='snapshot')` before /clear
1919
9. **DELEGATE**: `c3_delegate(task, backend='ollama|codex|gemini|claude|auto')` or `c3_agent(workflow=...)` for multi-model pipelines
20-
10. **BITBUCKET** (when configured, v2.30.0+): `c3_bitbucket(action='...')` — for self-hosted enterprise Bitbucket Data Center / Server: PRs, branches, builds, repo admin. Tokens live in the OS keyring (set up via `c3 bitbucket login`). Read actions are safe in plan mode; write actions (`merge_pr`, `create_branch`, etc.) are auto-logged to the edit ledger.
20+
10. **BITBUCKET** (when configured, v2.30.0+): `c3_bitbucket(action='...')` — for self-hosted enterprise Bitbucket Data Center / Server: PRs, branches, builds, repo admin. Tokens live in the OS keyring (set up via `c3 bitbucket login`, or `login --global` for a home config reusable across projects; account resolution precedence is project → home). Read actions are safe in plan mode; write actions (`merge_pr`, `create_branch`, etc.) are auto-logged to the edit ledger.
2121
11. **CROSS-PROJECT** (v2.31.0+): `c3_project(action='list|scan|info|search|read|edit|shell|...', project='<name|path>')` — discover and operate on OTHER c3-installed projects. `list`/`scan` need no project; reads (search/read/compress/status/memory/impact/edits/validate/filter) run freely; writes (`edit`, `shell`, memory add/update/delete) require `allow_write=true` and are logged to the target project's ledger.
2222

2323
## Plan mode
@@ -33,6 +33,7 @@ In plan mode, all c3_* read tools (search, read, compress, filter, validate, sta
3333

3434
```
3535
claude-companion - v2/
36+
.gitattributes
3637
.gitignore
3738
.manifest.swo
3839
.manifest.swp
@@ -47,8 +48,7 @@ claude-companion - v2/
4748
SECURITY.md
4849
THIRD_PARTY_LICENSES.md
4950
c3.bat
50-
install.bat
51-
... +3 more
51+
... +5 more
5252
.claude/
5353
settings.local.json
5454
.codex/
@@ -84,17 +84,18 @@ claude-companion - v2/
8484
hook_auto_snapshot.py
8585
hook_c3_signal.py
8686
hook_c3read.py
87+
hook_dispatch.py
8788
hook_edit_ledger.py
8889
hook_edit_unlock.py
8990
hook_filter.py
9091
hook_ghost_files.py
9192
hook_pretool_enforce.py
9293
hook_read.py
93-
hook_session_stats.py
94-
... +9 more
94+
... +11 more
9595
commands/ (3 files)
9696
guide/ (7 files)
97-
tools/ (18 files)
97+
hub_ui/ (2 files)
98+
tools/ (19 files)
9899
ui/ (5 files)
99100
code_context_control.egg-info/
100101
SOURCES.txt
@@ -125,7 +126,7 @@ claude-companion - v2/
125126
mcp_oracle.py
126127
oracle.html
127128
oracle_server.py
128-
services/ (16 files)
129+
services/ (17 files)
129130
oracle-guide/
130131
README.md
131132
api-reference.md
@@ -142,32 +143,32 @@ claude-companion - v2/
142143
benchmark_dashboard.py
143144
bitbucket_client.py
144145
bitbucket_credentials.py
146+
circuit_breaker.py
145147
claude_md.py
146148
compressor.py
147149
context_snapshot.py
148150
conversation_store.py
149151
doc_index.py
150152
e2e_benchmark.py
151-
e2e_evaluator.py
152-
... +35 more
153+
... +40 more
153154
bench/ (1 files)
154155
tests/
156+
test_activity_reporter.py
155157
test_aider_polyglot.py
156158
test_bitbucket_cli_smoke.py
157159
test_bitbucket_client.py
160+
test_bitbucket_config_fallback.py
158161
test_bitbucket_credentials.py
159162
test_bitbucket_tool.py
160163
test_c3_shell.py
164+
test_circuit_breaker.py
165+
test_claude_md_merge.py
161166
test_cli_smoke.py
167+
test_compressor_large_file.py
168+
test_delegate_cascade.py
162169
test_e2e_benchmark.py
163-
test_edit_normalization.py
164-
test_enforcement_flip.py
165-
test_federated_graph.py
166-
test_ghost_files.py
167-
test_git_branch_awareness.py
168-
test_hub_server_smoke.py
169-
test_install_mcp_entrypoint.py
170-
... +24 more
170+
test_edit_ledger_hook.py
171+
... +48 more
171172
tui/
172173
__init__.py
173174
backend.py
@@ -193,8 +194,5 @@ Python (Modern)
193194

194195
## Key Facts (use c3_memory for more)
195196

196-
- [architecture] File Memory system: FileMemoryStore in services/file_memory.py provides persistent structural index of so
197-
- c3_delegate now supports allow_model_fallback/fallback_models and resolves nearest installed Ollama model when the reque
198-
- ConversationStore sync now supports source='all|claude|imports', and sessions/turns persist normalized source labels for
199-
- SessionManager.parse_claude_session_tokens now resolves Claude transcript dirs via project slug candidates and constrain
200-
- `c3 benchmark` now provides a repeatable local benchmark for compression savings, retrieval token reduction, and groundi
197+
- Session summary (20260701): Decision: Deep 4-agent evaluation of C3 completed (tool surface, hooks, services, claims-vs-
198+
- Session summary (20260701): Files (modified): README.md, services/subprojects.py, services/project_manager.py, services/

cli/c3.py

Lines changed: 137 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@
8585
# Config
8686
CONFIG_DIR = ".c3"
8787
CONFIG_FILE = ".c3/config.json"
88-
__version__ = "2.43.0"
88+
__version__ = "2.44.0"
8989

9090

9191
def _command_deps() -> CommandDeps:
@@ -1051,6 +1051,10 @@ def cmd_init(args):
10511051
# ── Non-interactive (--clear) ──────────────────────────────
10521052
if getattr(args, "clear", False):
10531053
print("\n[--clear] Wiping C3 files...")
1054+
parent_link = (load_config(project_path) or {}).get("parent") or {}
1055+
if parent_link.get("path"):
1056+
print(f" [!] This project is a sub-project of {parent_link['path']}")
1057+
print(" The parent still lists it -- run 'c3 sub check --fix' there.")
10541058
_uninstall_mcp_all(project_path)
10551059
if c3_dir.exists():
10561060
shutil.rmtree(c3_dir)
@@ -5733,6 +5737,137 @@ def cmd_projects(args):
57335737
print(f" Port {s['port']:>5} {s.get('project_name', '?'):<25} {s.get('project_path', '')}")
57345738

57355739

5740+
def cmd_sub(args):
5741+
"""Manage sub-projects: designated sub-folders with linked .c3 branches."""
5742+
parent = str(Path(getattr(args, "parent", ".") or ".").resolve())
5743+
if not (Path(parent) / CONFIG_DIR).is_dir():
5744+
print(f"No .c3 found in {parent}. Run 'c3 init' there first.")
5745+
return
5746+
# Import after the cheap guard — the registry module needs a resolvable home.
5747+
from services.subprojects import VALID_CASCADE_OPS, SubprojectManager
5748+
5749+
sm = SubprojectManager(parent)
5750+
sub = getattr(args, "sub_cmd", "list") or "list"
5751+
target = getattr(args, "target", None)
5752+
as_json = getattr(args, "json", False)
5753+
5754+
if sub == "add":
5755+
if not target:
5756+
print("Usage: c3 sub add <folder> [--parent PATH] [--name NAME]")
5757+
return
5758+
result = sm.add(
5759+
target,
5760+
name=getattr(args, "name", None),
5761+
ide=getattr(args, "ide", None),
5762+
run_init=not getattr(args, "no_init", False),
5763+
reindex_parent=not getattr(args, "no_reindex_parent", False),
5764+
)
5765+
if as_json:
5766+
print(json.dumps(result, indent=2))
5767+
return
5768+
if not result.get("added"):
5769+
print(f"Failed: {result.get('error')}")
5770+
return
5771+
verb = "Adopted (existing .c3 kept)" if result.get("adopted") else "Initialized"
5772+
print(f"\n[OK] {verb}: {result['name']} ({result['path']})")
5773+
code = (result.get("parent_reindex") or {}).get("code")
5774+
if code:
5775+
print(f" Parent reindexed: {code.get('files_indexed', '?')} files, "
5776+
f"{code.get('chunks_created', '?')} chunks (sub-project now excluded)")
5777+
5778+
elif sub == "list":
5779+
report = sm.reconcile(fix=False) # report-only consistency pass
5780+
children = sm.list()
5781+
if as_json:
5782+
print(json.dumps({"children": children, "orphans": report.get("orphans", [])}, indent=2))
5783+
return
5784+
if not children:
5785+
print("No sub-projects designated. Use `c3 sub add <folder>`.")
5786+
return
5787+
fmt = "{:<22} {:<16} {:>6} {:>7} {}"
5788+
print(fmt.format("NAME", "STATUS", "FACTS", "ALERTS", "REL PATH"))
5789+
print("-" * 76)
5790+
for c in children:
5791+
print(fmt.format(
5792+
(c.get("name") or "?")[:21],
5793+
c.get("status", "?"),
5794+
c.get("facts_count", 0),
5795+
c.get("notification_count", 0),
5796+
c.get("rel_path", ""),
5797+
))
5798+
issues = sum(1 for c in children if c["status"] != "ok")
5799+
line = f"\n{len(children)} sub-project(s)"
5800+
if issues:
5801+
line += f" -- {issues} with issues (run `c3 sub check --fix`)"
5802+
if report.get("orphans"):
5803+
line += f" -- {len(report['orphans'])} registry orphan(s)"
5804+
print(line)
5805+
5806+
elif sub == "remove":
5807+
if not target:
5808+
print("Usage: c3 sub remove <name|path> [--clear] [--yes]")
5809+
return
5810+
mode = "clear" if getattr(args, "clear", False) else "unlink"
5811+
if mode == "clear" and not getattr(args, "yes", False):
5812+
print("This will DELETE the sub-project's .c3 directory and instruction docs.")
5813+
confirm = input("Type 'clear' to confirm: ").strip().lower()
5814+
if confirm != "clear":
5815+
print("Aborted.")
5816+
return
5817+
result = sm.remove(target, mode=mode,
5818+
reindex_parent=not getattr(args, "no_reindex_parent", False))
5819+
if as_json:
5820+
print(json.dumps(result, indent=2))
5821+
return
5822+
if not result.get("removed"):
5823+
print(f"Failed: {result.get('error')}")
5824+
return
5825+
print(f"\n[OK] {'Cleared' if mode == 'clear' else 'Unlinked'}: "
5826+
f"{result.get('name')} ({result.get('path')})")
5827+
for w in result.get("warnings", []):
5828+
print(f" warning: {w}")
5829+
5830+
elif sub == "run":
5831+
if target not in VALID_CASCADE_OPS:
5832+
print(f"Usage: c3 sub run {{{'|'.join(VALID_CASCADE_OPS)}}} [--include-parent] [--json]")
5833+
return
5834+
result = sm.cascade(target,
5835+
include_parent=getattr(args, "include_parent", False),
5836+
mcp=getattr(args, "mcp", False))
5837+
if as_json:
5838+
print(json.dumps(result, indent=2))
5839+
return
5840+
for row in result["results"]:
5841+
mark = "OK " if row["ok"] else "FAIL"
5842+
extra = f" -- {row.get('error')}" if row.get("error") else ""
5843+
print(f" [{mark}] {row['name']:<22} {row['elapsed_ms']:>6}ms{extra}")
5844+
s = result["summary"]
5845+
print(f"\n{target}: {s['ok']}/{s['total']} ok, {s['failed']} failed")
5846+
5847+
elif sub == "check":
5848+
result = sm.reconcile(fix=getattr(args, "fix", False),
5849+
prune=getattr(args, "prune", False))
5850+
if as_json:
5851+
print(json.dumps(result, indent=2))
5852+
return
5853+
if not result["children"] and not result["orphans"] and not result["pruned"]:
5854+
print("No sub-projects designated.")
5855+
return
5856+
for c in result["children"]:
5857+
print(f" [{c['status']:<16}] {c.get('name') or '?':<22} {c.get('rel_path', '')}")
5858+
for o in result["orphans"]:
5859+
print(f" [orphan_registry ] {o}")
5860+
for f in result.get("fixed", []):
5861+
print(f" fixed: {f.get('action')} -> {f.get('path') or f.get('rel_path')}")
5862+
for p in result.get("pruned", []):
5863+
print(f" pruned: {p.get('rel_path')}")
5864+
if result["ok"]:
5865+
print("\nAll links consistent.")
5866+
else:
5867+
hint = "" if getattr(args, "fix", False) else " Run `c3 sub check --fix` to repair."
5868+
print(f"\nIssues found.{hint}")
5869+
5870+
57365871
def cmd_session_benchmark(args):
57375872
"""Run real-world session workflow benchmark."""
57385873
if getattr(args, "command", "") == "session-benchmark":
@@ -6577,6 +6712,7 @@ def main():
65776712
"terse": cmd_terse,
65786713
"ui": cmd_ui,
65796714
"projects": cmd_projects,
6715+
"sub": cmd_sub,
65806716
"hub": cmd_hub,
65816717
"bitbucket": cmd_bitbucket,
65826718
"oracle": cmd_oracle,

cli/commands/parser.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,33 @@ def build_parser(version: str, parse_cli_ide_arg):
138138
)
139139
p_projects.add_argument("--name", default=None, help="Display name (for add)")
140140

141+
p_sub = subparsers.add_parser("sub", help="Manage sub-projects (linked child .c3 branches)")
142+
p_sub.add_argument(
143+
"sub_cmd",
144+
nargs="?",
145+
choices=["add", "list", "remove", "run", "check"],
146+
default="list",
147+
help="Sub-command (default: list)",
148+
)
149+
p_sub.add_argument(
150+
"target",
151+
nargs="?",
152+
default=None,
153+
help="Folder (add), sub-project name/path (remove), or operation update|reindex|health (run)",
154+
)
155+
p_sub.add_argument("--parent", default=".", help="Parent project path (default: current directory)")
156+
p_sub.add_argument("--name", default=None, help="Display name for the sub-project (add)")
157+
p_sub.add_argument("--ide", default=None, type=parse_cli_ide_arg, help="IDE for the sub-project init (add)")
158+
p_sub.add_argument("--no-reindex-parent", action="store_true", help="Skip the parent reindex after add/remove")
159+
p_sub.add_argument("--no-init", action="store_true", help="Link only; skip running init in the folder (add)")
160+
p_sub.add_argument("--clear", action="store_true", help="Also wipe the sub-project's .c3 and unregister it (remove; default keeps .c3)")
161+
p_sub.add_argument("--yes", action="store_true", help="Skip confirmation prompts")
162+
p_sub.add_argument("--include-parent", action="store_true", help="Also run the operation on the parent (run)")
163+
p_sub.add_argument("--mcp", action="store_true", help="Also reinstall MCP config on update (run update)")
164+
p_sub.add_argument("--fix", action="store_true", help="Repair links from the parent config (check)")
165+
p_sub.add_argument("--prune", action="store_true", help="With --fix: drop entries whose folder is gone (check)")
166+
p_sub.add_argument("--json", action="store_true", help="Emit JSON output")
167+
141168
p_perms = subparsers.add_parser("permissions",
142169
help="Manage Claude Code permissions — show | preview <tier> | diff | clean | <tier>")
143170
p_perms.add_argument("tier", nargs="?", default="show",

0 commit comments

Comments
 (0)