code-context-control 2.136.0__py3-none-any.whl → 2.139.0__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.
cli/c3.py CHANGED
@@ -91,7 +91,7 @@ console = Console() if HAS_RICH else None
91
91
  # Config
92
92
  CONFIG_DIR = ".c3"
93
93
  CONFIG_FILE = ".c3/config.json"
94
- __version__ = "2.136.0"
94
+ __version__ = "2.139.0"
95
95
 
96
96
 
97
97
  def _compress_file_cli(compressor, path, mode="smart", **kw):
@@ -1583,6 +1583,19 @@ def cmd_delegate_eval(args):
1583
1583
 
1584
1584
  targets = [t.strip() for t in (args.targets or "").split(",") if t.strip()]
1585
1585
  case_ids = [c.strip() for c in (args.cases or "").split(",") if c.strip()]
1586
+ if args.suite == "write":
1587
+ # Write mode (docs/delegate-write.md): graded on the files left behind.
1588
+ from services.bench import delegate_write_eval as dwe
1589
+ if not targets:
1590
+ raise RuntimeError("--suite write needs --targets (e.g. claude:small,claude:medium, or gold)")
1591
+ try:
1592
+ overrides = json.loads(args.config) if getattr(args, "config", None) else None
1593
+ report = dwe.run_write_suite(targets, case_ids=case_ids or None, record=args.record,
1594
+ delegate_overrides=overrides)
1595
+ except ValueError as e:
1596
+ raise RuntimeError(str(e))
1597
+ print(json.dumps(report, indent=2) if args.json else dwe.render(report))
1598
+ return
1586
1599
  overrides = json.loads(args.config) if getattr(args, "config", None) else None
1587
1600
  if overrides is not None and not isinstance(overrides, dict):
1588
1601
  raise RuntimeError("--config must be a JSON object")
@@ -5065,7 +5078,7 @@ back to native tools as the task progresses.
5065
5078
  - **Filter**: `c3_filter(text=...)` — for terminal output >10 lines
5066
5079
  - **Shell**: `c3_shell(cmd, timeout=60)` — structured shell exec (tests/git/build). Auto-filters output, logs git mutations to the ledger. Native Bash for interactive/TTY only
5067
5080
  - **Memory**: `c3_memory(action='recall')` — full recall. `index` + `fetch` for token-efficient two-step retrieval
5068
- - **Delegate**: `c3_delegate(task, task_type, context|file_path)` — your own provider one tier down (default `backend='host'`, `tier='small'`; Claude Code: Haiku) for bounded answers: summarize a diff/log, explain a function, triage a traceback. `scout=true` lets it look files up itself. Multi-step work: the `c3-scout` (haiku) / `c3-worker` (sonnet) subagents
5081
+ - **Delegate**: `c3_delegate(task, task_type, context|file_path)` — your own provider one tier down (default `backend='host'`, `tier='small'`; Claude Code: Haiku) for bounded answers: summarize a diff/log, explain a function, triage a traceback. `scout=true` lets it look files up itself; `write_paths='a.py,b.py'` has Sonnet make a change you specified in only those files and return the diff. Multi-step work: the `c3-scout` (haiku) / `c3-worker` (sonnet) subagents
5069
5082
  - **Local CI** (v2.79.0+): `c3_ci(action='inspect|run|rerun|failures')` — run THIS repo's real `.github/workflows` here instead of pushing for feedback. `run` executes in `needs` order; `failures` gives {file,line,message}; `rerun` retries only what failed. Only `FULL_CI_PASS` means safe to push — `PARTIAL_PASS` means something did not run (other OS, unsupported action, or your selection).
5070
5083
  - **Bitbucket** (v2.30.0+, when `c3 bitbucket login` has run): `c3_bitbucket(action='list_prs|get_pr|merge_pr|...')` — self-hosted Bitbucket Data Center / Server. Token in OS keyring; mutating actions auto-log to the edit ledger.
5071
5084
  - **Cross-project** (v2.31.0+): `c3_project(action='list|scan|search|read|edit|...', project='<name|path>')` — discover and operate on OTHER c3-installed projects. Reads run freely; writes (edit/shell/memory) need `allow_write=true`.
cli/commands/parser.py CHANGED
@@ -92,7 +92,8 @@ def build_parser(version: str, parse_cli_ide_arg):
92
92
  p_delegate_eval = subparsers.add_parser(
93
93
  "delegate-eval", help="Grade c3_delegate answers per backend/tier (docs/delegate-eval.md)")
94
94
  p_delegate_eval.add_argument("--suite", default="gold",
95
- help="'gold' (bundled) or a .jsonl path")
95
+ help="'gold' (bundled), 'write' (write mode, docs/delegate-write.md) "
96
+ "or a .jsonl path")
96
97
  p_delegate_eval.add_argument("--targets", default="",
97
98
  help="Comma-separated backend[:tier] list, e.g. 'claude:small,claude:medium'")
98
99
  p_delegate_eval.add_argument("--cases", default="", help="Comma-separated case ids (default: all)")
cli/guide/tools.html CHANGED
@@ -940,7 +940,7 @@ c3_status(view=<span class="str">'sessions'</span>) <span class="com">#
940
940
  <td class="param-name">tier</td>
941
941
  <td class="param-type">string</td>
942
942
  <td class="param-default">"small"</td>
943
- <td class="param-desc"><code>small</code> · <code>medium</code> · <code>large</code> · <code>default</code>. Claude: Haiku / Sonnet / Opus. Codex and Grok: reasoning effort low / medium / high on the account's model. Gemini: flash-lite / flash / pro.</td>
943
+ <td class="param-desc"><code>small</code> · <code>medium</code> · <code>large</code> · <code>default</code>. Claude: Haiku (thinking capped at 1024 tokens) / Sonnet / Opus; a scout defaults to Sonnet, which finds things in far fewer turns. Codex and Grok: reasoning effort low / medium / high on the account's model. Gemini: flash-lite / flash / pro.</td>
944
944
  </tr>
945
945
  <tr>
946
946
  <td class="param-name">model</td>
@@ -954,6 +954,12 @@ c3_status(view=<span class="str">'sessions'</span>) <span class="com">#
954
954
  <td class="param-default">false</td>
955
955
  <td class="param-desc">Let the delegate Read/Grep/Glob the project itself, for lookups when you cannot name the files. Claude runs <code>--restricted</code> with Access Guard's read denies; Codex uses its read-only sandbox.</td>
956
956
  </tr>
957
+ <tr>
958
+ <td class="param-name">write_paths</td>
959
+ <td class="param-type">string</td>
960
+ <td class="param-default">""</td>
961
+ <td class="param-desc">Write mode (Claude only, defaults to Sonnet). Comma-separated project-relative globs the delegate may edit or create. You decide the change; it makes it with Read/Grep/Glob/Edit/Write, no commands. You get back the diff, and each change is in the edit ledger. Access Guard applies; confirm-held, locked, <code>.git/</code> and <code>.c3/</code> paths are refused. Review the diff and run the tests yourself.</td>
962
+ </tr>
957
963
  </tbody>
958
964
  </table>
959
965
 
@@ -965,7 +971,10 @@ c3_delegate(task=<span class="str">'Summarize this diff for a changelog in three
965
971
  c3_delegate(task=<span class="str">'What does resolve_host return inside a grok child?'</span>, file_path=<span class="str">'core/host.py'</span>, task_type=<span class="str">'explain'</span>, tier=<span class="str">'medium'</span>)
966
972
 
967
973
  <span class="com"># A lookup the delegate does itself</span>
968
- c3_delegate(task=<span class="str">'Which functions call charge_card, and where?'</span>, scout=<span class="kw">True</span>)</code></pre>
974
+ c3_delegate(task=<span class="str">'Which functions call charge_card, and where?'</span>, scout=<span class="kw">True</span>)
975
+
976
+ <span class="com"># A change you have decided, made by Sonnet inside a write set</span>
977
+ c3_delegate(task=<span class="str">'Rename calc_total to order_total in its definition and every use; no alias'</span>, write_paths=<span class="str">'inv/orders.py, inv/report.py, tests/test_orders.py'</span>)</code></pre>
969
978
 
970
979
  <div class="callout callout-info">
971
980
  <span class="callout-icon">ℹ️</span>
cli/hook_access_guard.py CHANGED
@@ -503,14 +503,77 @@ def run(payload: dict, project_path: Path | None = None,
503
503
  return None
504
504
 
505
505
 
506
+ def _worker_refusal(rel: str, why: str) -> dict:
507
+ return _deny(f"[c3-delegate:refused] {rel}: {why}. Do not write it another way; "
508
+ "leave it and say in your report what change it needed.")
509
+
510
+
511
+ def worker_run(payload: dict, project_path: Path, state_dir: Path) -> dict | None:
512
+ """PreToolUse for a ``c3_delegate`` write-mode worker (see services/delegate_write).
513
+
514
+ Reads go through ``run`` exactly as for a scout. A write must pass, in
515
+ order: canonicalization inside the project, never ``.git/`` or ``.c3/``,
516
+ never the credential vault, Access Guard with NO grants and NO filed
517
+ requests (a confirm hold is a refusal here — a headless worker cannot
518
+ wait for a human, and the caller can make that edit itself), the caller's
519
+ write set, and another agent's lock. Then the pre-image is saved before
520
+ the write is allowed. Raises on a broken spec; main() turns that into a
521
+ deny.
522
+ """
523
+ from services import delegate_write as dw
524
+
525
+ tool = normalize_tool_name(payload.get("tool_name", ""))
526
+ if tool not in _WRITE_TOOLS:
527
+ return run(payload, project_path)
528
+ spec = dw.load_spec(state_dir)
529
+ fp = _target(payload.get("tool_input", {}) or {})
530
+ if not fp:
531
+ return _deny("[c3-delegate:refused] a write with no target path")
532
+ base = str(project_path)
533
+ canon, rel, denial = ag.canonicalize(fp, base)
534
+ if denial:
535
+ return _worker_refusal(fp, f"path refused ({denial.reason})")
536
+ if not rel:
537
+ return _worker_refusal(fp, "outside the project")
538
+ if dw.forbidden(rel):
539
+ return _worker_refusal(rel, "under .git/ or .c3/, never delegate-writable")
540
+ try:
541
+ from services import credential_store as _cs
542
+ vault = _cs.vault_guard_reason(fp)
543
+ except Exception as exc: # fail closed: an unloadable vault check refuses
544
+ vault = f"vault check unavailable ({type(exc).__name__})"
545
+ if vault:
546
+ return _worker_refusal(rel, "credential vault file")
547
+ op = "write" if os.path.exists(fp) else "create"
548
+ denial = ag.check(fp, op, base)
549
+ if denial:
550
+ _record(denial, tool, op, fp, base, str(payload.get("session_id") or ""))
551
+ return _worker_refusal(rel, f"Access Guard {denial.kind} rule '{denial.rule}' "
552
+ f"({denial.scope})")
553
+ if not dw.in_write_set(rel, spec["write_paths"]):
554
+ return _worker_refusal(rel, "outside the write set the caller gave ("
555
+ + ", ".join(spec["write_paths"]) + ")")
556
+ try:
557
+ from services import agent_locks
558
+ holder = agent_locks.check(fp, base, str(spec.get("session_id") or ""))
559
+ except Exception:
560
+ holder = None
561
+ if holder:
562
+ return _worker_refusal(rel, f"locked by {holder.get('agent_id', 'another agent')}")
563
+ dw.snapshot(state_dir, fp, canon)
564
+ return None
565
+
566
+
506
567
  def main() -> None:
507
- """Standalone PreToolUse entry for a ``c3_delegate`` scout run.
568
+ """Standalone PreToolUse entry for a ``c3_delegate`` scout or worker run.
508
569
 
509
570
  A scout is ``claude -p --restricted`` with Read/Grep/Glob and no C3 MCP
510
571
  server, so the dispatcher's discipline hook (hook_pretool_enforce) would
511
572
  refuse every native read. Only the access guard runs here. It prints a
512
573
  deny and nothing else: grants and search footers are for an agent that
513
574
  can act on them. Any failure denies (fail closed).
575
+
576
+ ``--worker-state DIR``: a write-mode worker; see ``worker_run``.
514
577
  """
515
578
  import argparse
516
579
  import json
@@ -520,6 +583,7 @@ def main() -> None:
520
583
  ensure_utf8_stdio()
521
584
  parser = argparse.ArgumentParser()
522
585
  parser.add_argument("--project", required=True)
586
+ parser.add_argument("--worker-state", default="")
523
587
  args = parser.parse_args()
524
588
  try:
525
589
  raw = sys.stdin.read()
@@ -527,9 +591,13 @@ def main() -> None:
527
591
  if not isinstance(payload, dict):
528
592
  raise ValueError("hook payload must be a JSON object")
529
593
  # access_guard canonicalizes the base itself (no direct resolve here).
530
- out = run(payload, Path(args.project))
594
+ if args.worker_state:
595
+ out = worker_run(payload, Path(args.project), Path(args.worker_state))
596
+ else:
597
+ out = run(payload, Path(args.project))
531
598
  except Exception as exc:
532
- out = _deny(f"[c3-access:error] scout guard failed ({type(exc).__name__}: {exc}); "
599
+ who = "worker" if args.worker_state else "scout"
600
+ out = _deny(f"[c3-access:error] {who} guard failed ({type(exc).__name__}: {exc}); "
533
601
  "refusing the call")
534
602
  if isinstance(out, dict) and isinstance(out.get("hookSpecificOutput"), dict):
535
603
  print(json.dumps({"hookSpecificOutput": out["hookSpecificOutput"]}))
cli/mcp_server.py CHANGED
@@ -806,11 +806,13 @@ async def c3_delegate(task: str, task_type: str = "ask", context: str = "",
806
806
  file_path: str = "", backend: str = "host",
807
807
  allow_write_delegation: bool = False,
808
808
  tier: str = "", model: str = "", scout: bool = False,
809
+ write_paths: str = "",
809
810
  ctx: Context = None) -> str:
810
811
  """OFFLOAD to another model — use when the subtask is local-model-sized or needs a different perspective.
811
812
  backend: host (default: your own provider, one tier down — Claude Code->claude, Codex->codex, Grok->grok, Antigravity->gemini) | claude|codex|gemini|grok|ollama|auto. task_type: auto, summarize, explain, review, ask, test, diagnose, ping (live auth check), available, codex_check, gemini_check, grok_check, codex_resume.
812
- tier: small (default) | medium | large | default. claude: haiku/sonnet/opus; codex/grok: reasoning effort; gemini: flash-lite/flash/pro. model overrides the tier. Context + file_path are packed by C3 under Access Guard; a file over 8k tokens goes as its map, so for line-level questions on a big file paste the excerpt or use scout=true.
813
+ tier: small (default; scout=true defaults to medium) | medium | large | default. claude: haiku/sonnet/opus; codex/grok: reasoning effort; gemini: flash-lite/flash/pro. model overrides the tier. Context + file_path are packed by C3 under Access Guard; a file over 8k tokens goes as its map, so for line-level questions on a big file paste the excerpt or use scout=true.
813
814
  scout=true: the delegate may Read/Grep/Glob the project itself (claude: read-only, guard-denied paths refused; codex: its read-only sandbox) — for lookups when you cannot name the files.
815
+ write_paths: comma-separated project-relative globs the delegate may edit or create (write mode; claude only, tier defaults to medium). You decide the change: say what to change, where, and the intended behaviour. It cannot run commands. You get back the diff (each change is in the edit ledger): review it and run the tests yourself. Access Guard applies; confirm-held, locked, .git/ and .c3/ paths are refused.
814
816
  grok runs read-only in a temp dir unless delegate.grok_allow_write=true (then --yolo in the project, loading its trusted .grok MCP servers and hooks).
815
817
  allow_write_delegation: explicit user opt-in for write-capable backends (gemini/grok write mode/codex_resume) while Access Guard rules are active; codex is pinned read-only instead."""
816
818
  svc = _svc(ctx)
@@ -828,7 +830,8 @@ async def c3_delegate(task: str, task_type: str = "ask", context: str = "",
828
830
  try:
829
831
  return await asyncio.to_thread(handle_delegate, task, task_type, context,
830
832
  file_path, svc, finalize, backend,
831
- allow_write_delegation, tier, model, scout)
833
+ allow_write_delegation, tier, model, scout,
834
+ write_paths)
832
835
  finally:
833
836
  svc._agent_progress_cb = None
834
837