oc_chat
Loading...
Searching...
No Matches
oct_tools.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oc_chat/tools/oct_tools.py
4
5
6"""
7oct toolchain integration — lint, health, typecheck, file generator, and git suite.
8
9Purpose
10-------
11Wraps the ``oct`` CLI (Option C Tools) as callable agent tools so the
12model can run ``oct lint``, ``oct health``, ``oct typecheck``,
13``oct new``, and the full ``oct git`` subcommand suite (status, check,
14init, add, commit, branch, restore, ignore, commit-msg, changelog,
15hooks) inside a session.
16
17The ``oct_git_*`` wrappers are the agent's only path to mutating git
18operations — raw ``git add`` / ``git commit`` / ``git push`` are
19blocked by ``oc_chat/tools/shell.py``'s whitelist, so every agent-
20driven commit goes through the Option C quality gate (lint + format
21+ secrets) by construction.
22
23Requires: ``pip install -e .[option_c]`` (wires the sibling ``oct``
24package). Returns ``{"missing_dependency": ...}`` when ``oct`` is not
25installed, which the dependency healer can intercept and offer to
26repair.
27
28Responsibilities
29----------------
30- :func:`_oct_exe` / :func:`_run_oct` — locate the binary and invoke
31 it with timeout/cwd controls.
32- :func:`_note_activity_lines_of_one_stream` — build the per-stream
33 status-bar observer, redaction and private-key armor state included.
34- :func:`oct_lint`, :func:`oct_health`, :func:`oct_typecheck`,
35 :func:`oct_new` — first-class agent tools (F.2a-i through iv).
36- :func:`oct_test` — the Plan-187 consumer of the Plan-184 §20 test
37 stand-in (``python -m oc_chat.testing``): a subprocess run returning
38 a distilled ``oct-test/v0.4`` payload (closes F1). Unlike the oct
39 passthrough wrappers it spawns ``sys.executable``, pins cwd to the
40 oc_chat SUBPROJECT root (the stand-in's ADR-T-12 walk anchor), and
41 returns the distilled report rather than ``CommandResult``.
42- :func:`oct_git_*` — 11 wrappers around the ``oct git`` subcommand
43 surface (F.2a-v).
44- ``TOOLS`` and ``SCHEMAS`` exports for auto-registration.
45
46Diagnostics
47-----------
48Domain: OC_CHAT.TOOLS
49Levels:
50 L2 — lifecycle events
51 L3 — semantic details
52 L4 — deep tracing
53
54Contracts
55---------
56- All wrappers are pass-throughs — the schemas and arg parsing live
57 here, but the actual quality logic lives in ``oct``.
58- Missing-binary returns are *structured* (``missing_dependency``)
59 rather than raising, so the healer can offer to install.
60- ``cwd`` is pinned to the effective sandbox root (:func:`oc_chat.tools.filesystem._sandbox_root`) so behaviour
61 is independent of the user's shell CWD — except :func:`oct_test`,
62 whose cwd is the oc_chat SUBPROJECT root (the stand-in's discovery
63 anchor); only its report file uses the sandbox root.
64- Every subprocess call honours an explicit ``timeout`` — defaults
65 are conservative and tunable per-tool.
66
67Dependencies
68------------
69- oc_diagnostics (structured debug logging via _dbg)
70- oc_chat.core.tool_results (CommandResult, MissingDependency and ToolError - the wrappers' result shapes)
71- oc_chat.tools.filesystem (_sandbox_root for the pinned cwd and report directory, and the oct_new write-approval gate)
72- oc_chat.core.tool_activity (lazy; TOOL_ACTIVITY - the status-bar record each redacted output line is noted on)
73- oc_chat.tools._proc (lazy; run_cancellable - the cancellable oct and stand-in subprocesses)
74- oc_chat.tools._sandbox (lazy; scrubbed_env, LineRedactor and the write-path checks - the child environment, status-bar redaction and the oct_new gate)
75- oct (not imported; the oct CLI found on PATH and run as a subprocess by every wrapper except oct_test)
76- oc_chat.testing (not imported; run as python -m oc_chat.testing by oct_test)
77
78See Also
79--------
80oc_chat/docs/conventions/CONTRACTS.md §3 Tool registry contract; §4 Tool return-
81value conventions (oct_* outputs → §4.1; missing-dep hint → §4.6).
82"""
83import os
84import json
85import shutil
86import subprocess
87import sys
88from collections.abc import Callable
89from pathlib import Path
90from typing import Optional
91
92from oc_diagnostics import _dbg
93
94from oc_chat.tools.filesystem import _sandbox_root
95from oc_chat.core.tool_results import (
96 CommandResult,
97 MissingDependency,
98 ToolError,
99)
100
101_OctResult = CommandResult | MissingDependency | ToolError
102
103
104def _oct_exe() -> Optional[str]:
105 """Return the path to the ``oct`` executable, or ``None`` if absent."""
106 return shutil.which("oct")
107
108
109def _note_activity_lines_of_one_stream() -> "Callable[[str], None]":
110 """Return the observer that shows ONE stream's lines on the status bar.
111
112 Each line is redacted with the same filter ``run_command`` applies to its
113 result, even though these wrappers return ``result.stdout`` unredacted: what
114 the terminal shows is a different surface from what the tool returns, and the
115 one that is on screen for anyone walking past is the one worth filtering
116 (plan 279). Terminal-only — nothing here reaches the result, the events, the
117 session or the model.
118
119 One :class:`~oc_chat.tools._sandbox.LineRedactor` per call of this factory,
120 and the runner calls it once per pipe (plan 287), so the private-key armor
121 state the filter carries between lines is this stream's and no other's.
122 """
123 from ._sandbox import LineRedactor
124 from oc_chat.core.tool_activity import TOOL_ACTIVITY
125
126 redactor = LineRedactor()
127
128 def _note_activity_line(line: str) -> None:
129 """Record *line* on the activity record, armor state included."""
130 TOOL_ACTIVITY.note_line(redactor.redact_line(line))
131
132 return _note_activity_line
133
134
135def _run_oct(args: list[str], cwd: str, timeout: int = 60) -> _OctResult:
136 """Execute ``oct <args>`` and return ``{stdout, stderr, returncode}``."""
137 func = "_run_oct"
138 _dbg("OC_CHAT.TOOLS", func, f"args={args} cwd={cwd} timeout={timeout}", 3)
139 exe = _oct_exe()
140 if exe is None:
141 return {
142 "missing_dependency": "oct",
143 "install_hint": "pip install -e .[option_c]",
144 }
145 try:
146 # M8.0 WU-3c: minimal scrubbed env (drop poisonable GIT_*/PIP_*/proxy/
147 # cert/... families, F25/F72/F78/F79); TMPDIR -> OC Chat's logs dir.
148 from ._sandbox import scrubbed_env
149 env = scrubbed_env({"TMPDIR": str(Path(cwd) / "logs")})
150
151 from ._proc import run_cancellable
152
153 # Cancellable: a Ctrl+C terminates the oct child process.
154 result = run_cancellable(
155 [exe, *args],
156 timeout=timeout,
157 cwd=cwd,
158 env=env,
159 on_line_factory=_note_activity_lines_of_one_stream,
160 )
161 return {
162 "stdout": result.stdout,
163 "stderr": result.stderr,
164 "returncode": result.returncode,
165 }
166 except subprocess.TimeoutExpired:
167 return {"error": f"oct command timed out after {timeout}s", "args": args}
168 except Exception as exc:
169 return {"error": str(exc), "args": args}
170
171
172def _reject_operand(value: str, field: str) -> "ToolError | None":
173 """Refuse a model-supplied oct/git operand that could smuggle an option into
174 the argv (M8.0 WU-3 oct parity / argv-injection guard).
175
176 The ``oct_*`` wrappers forward model-controlled path / pattern / identifier
177 operands as positional args or option *values* -- never as options. A leading
178 ``-`` would be reparsed by the oct/git argument parser as a flag (e.g.
179 ``paths=["--all"]`` -> ``git add --all``; ``path="--help"``). Reject a
180 non-string, empty, control-char-bearing, or leading-dash operand. Returns a
181 ToolError dict to surface, or ``None`` if the operand is safe.
182 """
183 if not isinstance(value, str) or value == "":
184 return {"error": f"oct {field}: empty or non-string operand"}
185 if any(c in value for c in ("\x00", "\n", "\r")):
186 return {"error": f"oct {field}: operand contains a control character"}
187 if value.startswith("-"):
188 return {"error": f"oct {field}: operand may not begin with '-' (option-smuggling guard): {value!r}"}
189 return None
190
191
192def _reject_operands(values: "list[str]", field: str) -> "ToolError | None":
193 """Apply :func:`_reject_operand` to each value in a list operand."""
194 for v in values:
195 err = _reject_operand(v, field)
196 if err:
197 return err
198 return None
199
200
202 path: str = ".",
203 fix_headers: bool = False,
204 dry_run: bool = False,
205 json_output: bool = False,
206 changed_only: bool = False,
207 timeout: int = 120,
208) -> _OctResult:
209 """Run ``oct lint`` on the project or a specific path.
210
211 Parameters
212 ----------
213 path : str
214 File or directory to lint (default: entire project).
215 fix_headers : bool
216 Auto-fix shebang, encoding, and path comment headers.
217 dry_run : bool
218 Preview fixes without writing (only meaningful with ``fix_headers``).
219 json_output : bool
220 Emit machine-readable JSON instead of human text.
221 changed_only : bool
222 Lint only git-modified Python files.
223 timeout : int
224 Seconds before the subprocess is killed (default 120).
225 """
226 root = str(_sandbox_root())
227 args = ["lint"]
228 if fix_headers:
229 args.append("--fix-headers")
230 if dry_run:
231 args.append("--dry-run")
232 if json_output:
233 args.append("--json")
234 if changed_only:
235 args.append("--changed")
236 if path and path != ".":
237 err = _reject_operand(path, "lint path")
238 if err:
239 return err
240 args.append(path)
241 return _run_oct(args, cwd=root, timeout=timeout)
242
243
245 json_output: bool = False,
246 verbose: bool = False,
247 timeout: int = 300,
248) -> _OctResult:
249 """Display the Option C project compliance dashboard.
250
251 Parameters
252 ----------
253 json_output : bool
254 Machine-readable JSON output.
255 verbose : bool
256 Detailed per-file breakdown.
257 timeout : int
258 Seconds before the subprocess is killed (default 300).
259 """
260 root = str(_sandbox_root())
261 args = ["health"]
262 if json_output:
263 args.append("--json")
264 if verbose:
265 args.append("--verbose")
266 return _run_oct(args, cwd=root, timeout=timeout)
267
268
270 json_output: bool = False,
271 timeout: int = 120,
272) -> _OctResult:
273 """Run type checking on the project via ``oct typecheck``.
274
275 Parameters
276 ----------
277 json_output : bool
278 Machine-readable JSON summary.
279 timeout : int
280 Seconds before the subprocess is killed (default 120).
281 """
282 root = str(_sandbox_root())
283 args = ["typecheck"]
284 if json_output:
285 args.append("--json")
286 return _run_oct(args, cwd=root, timeout=timeout)
287
288
290 path: str,
291 force: bool = False,
292 test: bool = False,
293 dry_run: bool = False,
294 verbose: bool = False,
295 timeout: int = 30,
296) -> _OctResult:
297 """Create a new Option C-compliant Python file via ``oct new``.
298
299 Generates a file with the mandatory 3-line header block (shebang,
300 encoding, file identity), ``from oc_diagnostics import _dbg``, and
301 a complete module docstring skeleton with Purpose, Responsibilities,
302 Diagnostics, and Contracts sections.
303
304 Parameters
305 ----------
306 path : str
307 Target file path (required, must end in ``.py``).
308 force : bool
309 Overwrite the file if it already exists.
310 test : bool
311 Also generate a companion test file in ``tests/``.
312 dry_run : bool
313 Preview what would be created without writing anything.
314 verbose : bool
315 Print generated file content to stdout.
316 timeout : int
317 Seconds before the subprocess is killed (default 30).
318 """
319 root = str(_sandbox_root())
320 err = _reject_operand(path, "new path")
321 if err:
322 return err
323 # M8.0 WU-5 / F70: oct_new is a path sink that creates an importable .py — gate it
324 # like write_file. Clamp to the sandbox, hard-deny the protected-write class, and
325 # route the config-as-code / importable tier through the human-confirm relay
326 # (re-dispatched under filesystem.human_confirmed_write). dry-run writes nothing.
327 if not dry_run:
328 from oc_chat.tools._sandbox import (
329 SandboxViolation, is_human_confirm_write_path, is_importable_write_path,
330 is_protected_write_path, require_inside_sandbox,
331 )
332 from oc_chat.tools.filesystem import _human_approved_write, _make_protected_write_block
333 try:
334 target = require_inside_sandbox(path, root=Path(root))
335 except SandboxViolation as exc:
336 return {"error": str(exc)}
337 if is_protected_write_path(target):
338 return {"error": "Creating this file is not permitted: it is in the protected-write class."}
339 if _human_approved_write.get() != str(target):
340 if is_importable_write_path(target):
341 return _make_protected_write_block(str(target), "importable-tree write (oct new)")
342 if is_human_confirm_write_path(target):
343 return _make_protected_write_block(str(target), "config-as-code (oct new)")
344 args = ["new", path]
345 if force:
346 args.append("--force")
347 if test:
348 args.append("--test")
349 if dry_run:
350 args.append("--dry-run")
351 if verbose:
352 args.append("--verbose")
353 return _run_oct(args, cwd=root, timeout=timeout)
354
355
356# ── oct_test: the §20 stand-in consumer (Plan 187 / M4.4, closes F1) ─────────
357
358#: The oc_chat SUBPROJECT root, resolved from this file (tools → package →
359#: subproject). The stand-in's ADR-T-12 upward walk must start here — the
360#: session sandbox root is operator-movable (/cwd, env override) and is NOT
361#: guaranteed to be the oc_chat package root, so the tool always tests its
362#: own tree (decision 2). The sandbox root still hosts the report file.
363_OC_CHAT_SUBPROJECT_ROOT = Path(__file__).resolve().parents[2]
364
365#: Tool-level wall-clock cap for ``oct_test`` (the strict live run measures
366#: ~8 min; default 900 s, silently clamped to [1, 1800] mirroring the
367#: family's clamp discipline — local cap, since resource_caps.MAX_TIMEOUT_S
368#: is the run_command shell cap).
369_OCT_TEST_TIMEOUT_CAP = 1800
370
371#: Distillation cap for the non-passed slice (decision 4 — no silent
372#: truncation: the cap is always accompanied by ``non_passed_total`` and
373#: ``non_passed_truncated``).
374_OCT_TEST_NON_PASSED_CAP = 50
375
376
378 profile: str = "strict",
379 seed: "int | None" = None,
380 workers: "int | str | None" = None,
381 marker: "str | None" = None,
382 nodeids: "list[str] | None" = None,
383 dry_run: bool = False,
384 per_test_timeout: "int | None" = None,
385 timeout: int = 900,
386) -> dict:
387 """Run the oc_chat test suite via the Plan-184 §20 stand-in
388 (``python -m oc_chat.testing``) and return a distilled
389 ``oct-test/v0.4`` payload.
390
391 The FULL report is written to ``<sandbox_root>/logs/oct_test-<run_id>.json``
392 (``report_path`` in the payload); the payload itself carries the report's
393 top-level fields MINUS ``tests[]``, PLUS ``non_passed_tests[]`` (every
394 entry with ``outcome != "passed"``) capped at 50 with explicit
395 ``non_passed_total`` / ``non_passed_truncated`` fields. Stand-in exit
396 codes (0 passed / 1 failed / 2 contract violation / 4 configuration
397 error) surface as the payload's ``exit_code`` — structured data, never a
398 raised error (the family's CommandResult philosophy).
399
400 Parameters
401 ----------
402 profile : str
403 ``strict`` (default) | ``smoke`` | ``full`` — validated by the
404 stand-in (a bad value returns an exit-4 payload).
405 seed : int | None
406 Pin pytest-randomly ordering (``--seed N``); unseeded runs are
407 order-stable with seed 0 reported.
408 workers : int | str | None
409 Forwarded verbatim (``auto`` or a count); the serial stand-in
410 reports ``workers.actual: 1`` truthfully.
411 marker : str | None
412 Extra marker expression, AND-composed with the profile expression.
413 nodeids : list[str] | None
414 Individual pytest nodeids (repeatable ``--nodeid``).
415 dry_run : bool
416 Collection + static checks only; no test executes.
417 per_test_timeout : int | None
418 Per-test wall-clock bound in seconds (stand-in watchdog).
419 timeout : int
420 Tool-level wall clock (default 900 s; clamped to [1, 1800]).
421 """
422 func = "oct_test"
423 _dbg("OC_CHAT.TOOLS", func,
424 f"profile={profile} dry_run={dry_run} nodeids={len(nodeids or [])}", 3)
425
426 for value, field in ((profile, "oct_test profile"),
427 (marker, "oct_test marker")):
428 if value is not None:
429 err = _reject_operand(value, field)
430 if err:
431 return err
432 if isinstance(workers, str) and workers != "auto":
433 err = _reject_operand(workers, "oct_test workers")
434 if err:
435 return err
436 if nodeids:
437 err = _reject_operands(nodeids, "oct_test nodeid")
438 if err:
439 return err
440
441 clamped = max(1, min(int(timeout), _OCT_TEST_TIMEOUT_CAP))
442
443 sandbox_logs = Path(_sandbox_root()) / "logs"
444 try:
445 sandbox_logs.mkdir(parents=True, exist_ok=True)
446 except OSError as exc:
447 return {"error": f"oct_test: cannot create the report directory "
448 f"{sandbox_logs}: {exc}"}
449
450 args = [sys.executable, "-m", "oc_chat.testing",
451 "--package", "oc_chat", "--json",
452 "--profile", profile]
453 if seed is not None:
454 args += ["--seed", str(seed)]
455 if workers is not None:
456 args += ["--workers", str(workers)]
457 if marker is not None:
458 args += ["--marker", marker]
459 for nodeid in nodeids or []:
460 args += ["--nodeid", nodeid]
461 if per_test_timeout is not None:
462 args += ["--per-test-timeout", str(per_test_timeout)]
463 if dry_run:
464 args.append("--dry-run")
465
466 try:
467 from ._sandbox import scrubbed_env
468 env = scrubbed_env({"TMPDIR": str(sandbox_logs)})
469
470 from ._proc import run_cancellable
471
472 result = run_cancellable(
473 args,
474 timeout=clamped,
475 cwd=str(_OC_CHAT_SUBPROJECT_ROOT),
476 env=env,
477 on_line_factory=_note_activity_lines_of_one_stream,
478 )
479 except subprocess.TimeoutExpired:
480 return {"error": f"oct_test timed out after {clamped}s",
481 "args": args}
482 except Exception as exc:
483 return {"error": str(exc), "args": args}
484
485 cancelled = (
486 result.returncode == -2
487 or (result.stderr or "").rstrip().endswith("[cancelled by user]")
488 )
489 if cancelled:
490 # Cooperative cancel: pass the family marker through untouched.
491 # The marker is the authoritative signal — _proc appends it and
492 # reports the terminated child's real returncode (1 under Windows
493 # TerminateProcess; -2 only when no code was collected).
494 return {"stdout": result.stdout, "stderr": result.stderr,
495 "returncode": result.returncode}
496
497 stripped = (result.stdout or "").strip()
498 if not stripped.startswith("{"):
499 tail = (result.stderr or "")[-2000:]
500 return {"error": (f"oct_test: stand-in produced no JSON report "
501 f"(exit {result.returncode}): {tail}")}
502 try:
503 report = json.loads(stripped)
504 except (json.JSONDecodeError, ValueError) as exc:
505 tail = (result.stderr or "")[-2000:]
506 return {"error": (f"oct_test: stand-in stdout is not valid JSON "
507 f"({exc}); stderr tail: {tail}")}
508
509 run_id = report.get("run_id") or "unidentified"
510 report_path = sandbox_logs / f"oct_test-{run_id}.json"
511 try:
512 report_path.write_text(json.dumps(report, indent=2),
513 encoding="utf-8")
514 except OSError as exc:
515 return {"error": f"oct_test: cannot write the full report to "
516 f"{report_path}: {exc}"}
517
518 all_tests = report.get("tests") or []
519 non_passed = [t for t in all_tests if t.get("outcome") != "passed"]
520 payload = {k: v for k, v in report.items() if k != "tests"}
521 payload["non_passed_tests"] = non_passed[:_OCT_TEST_NON_PASSED_CAP]
522 payload["non_passed_total"] = len(non_passed)
523 payload["non_passed_truncated"] = (
524 len(non_passed) > _OCT_TEST_NON_PASSED_CAP
525 )
526 payload["report_path"] = str(report_path)
527 _dbg("OC_CHAT.TOOLS", func,
528 f"status={payload.get('status')} exit={payload.get('exit_code')} "
529 f"non_passed={len(non_passed)}", 3)
530 return payload
531
532
533# ── oct git subcommand wrappers ──────────────────────────────────────────────
534#
535# All ``oct_git_*`` wrappers take the workspace root as cwd and delegate to
536# ``_run_oct``. They are the agent's only path to mutating git operations.
537
538
540 json_output: bool = False,
541 no_check: bool = False,
542 scope: Optional[str] = None,
543 timeout: int = 30,
544) -> _OctResult:
545 """Show git status with Option C compliance annotations.
546
547 Preferred over raw ``git status`` because it annotates each file with
548 its compliance state (lint/format/secrets) on top of the standard
549 staged/modified/untracked breakdown.
550
551 Parameters
552 ----------
553 json_output : bool
554 Machine-readable JSON output.
555 no_check : bool
556 Skip compliance annotations (plain status).
557 scope : str | None
558 File scope: ``"project"`` (default), ``"repo"``, or ``"auto"``.
559 timeout : int
560 Seconds before the subprocess is killed (default 30).
561 """
562 root = str(_sandbox_root())
563 args = ["git", "status"]
564 if json_output:
565 args.append("--json")
566 if no_check:
567 args.append("--no-check")
568 if scope:
569 err = _reject_operand(scope, "git status --scope")
570 if err:
571 return err
572 args.extend(["--scope", scope])
573 return _run_oct(args, cwd=root, timeout=timeout)
574
575
577 staged_only: bool = False,
578 all_files: bool = False,
579 include_tests: bool = False,
580 fix: bool = False,
581 profile: Optional[str] = None,
582 json_output: bool = False,
583 sandbox: bool = False,
584 timeout: int = 120,
585) -> _OctResult:
586 """Run the unified Option C quality gate (lint + format + secrets).
587
588 Preferred over running ``oct lint`` and ``oct format`` separately
589 because it bundles secrets scanning and applies the same gate that
590 ``oct git commit`` enforces.
591
592 Parameters
593 ----------
594 staged_only : bool
595 Check only staged files (default scope).
596 all_files : bool
597 Check every Python file in the project.
598 include_tests : bool
599 Also run pytest (exit 4 on failure).
600 fix : bool
601 Attempt auto-fix of lint/format violations, then re-check.
602 profile : str | None
603 Override lint profile: ``"proto"``, ``"compact"``, or ``"strict"``.
604 json_output : bool
605 Machine-readable JSON output.
606 sandbox : bool
607 Run pytest under MCP sandbox (strips PYTHONPATH and secret env vars).
608 timeout : int
609 Seconds before the subprocess is killed (default 120).
610 """
611 root = str(_sandbox_root())
612 args = ["git", "check"]
613 if staged_only:
614 args.append("--staged-only")
615 if all_files:
616 args.append("--all")
617 if include_tests:
618 args.append("--include-tests")
619 if fix:
620 args.append("--fix")
621 if profile:
622 err = _reject_operand(profile, "git check --profile")
623 if err:
624 return err
625 args.extend(["--profile", profile])
626 if json_output:
627 args.append("--json")
628 if sandbox:
629 args.append("--sandbox")
630 return _run_oct(args, cwd=root, timeout=timeout)
631
632
634 dry_run: bool = False,
635 github: bool = False,
636 no_hooks: bool = False,
637 timeout: int = 60,
638) -> _OctResult:
639 """Initialize / enhance a git repo with Option C conventions.
640
641 Preferred over manually editing ``.gitignore`` / ``.gitattributes`` /
642 ``.pre-commit-config.yaml`` because it applies the canonical Option C
643 templates and merges idempotently.
644
645 Parameters
646 ----------
647 dry_run : bool
648 Show what would be created/modified without writing.
649 github : bool
650 Also generate ``.github/workflows/option-c.yml``.
651 no_hooks : bool
652 Skip ``.pre-commit-config.yaml`` creation.
653 timeout : int
654 Seconds before the subprocess is killed (default 60).
655 """
656 root = str(_sandbox_root())
657 args = ["git", "init"]
658 if dry_run:
659 args.append("--dry-run")
660 if github:
661 args.append("--github")
662 if no_hooks:
663 args.append("--no-hooks")
664 return _run_oct(args, cwd=root, timeout=timeout)
665
666
668 paths: Optional[list[str]] = None,
669 patch: bool = False,
670 update: bool = False,
671 all_files: bool = False,
672 dry_run: bool = False,
673 json_output: bool = False,
674 timeout: int = 60,
675) -> _OctResult:
676 """Stage files for commit, with optional post-stage quality-gate summary.
677
678 Preferred over raw ``git add`` because it scopes to the project (vs. the
679 whole repo) and runs a quality-gate summary on the staged set.
680
681 Parameters
682 ----------
683 paths : list[str] | None
684 Files / directories to stage.
685 patch : bool
686 Interactive hunk-level staging (passthrough to ``git add -p``).
687 update : bool
688 Stage all already-tracked modified files.
689 all_files : bool
690 Stage all changes (tracked + untracked) within project scope.
691 dry_run : bool
692 Show what would be staged without staging.
693 json_output : bool
694 Machine-readable JSON output.
695 timeout : int
696 Seconds before the subprocess is killed (default 60).
697 """
698 root = str(_sandbox_root())
699 args = ["git", "add"]
700 if patch:
701 args.append("--patch")
702 if update:
703 args.append("--update")
704 if all_files:
705 args.append("--all")
706 if dry_run:
707 args.append("--dry-run")
708 if json_output:
709 args.append("--json")
710 if paths:
711 err = _reject_operands(paths, "git add path")
712 if err:
713 return err
714 args.extend(paths)
715 return _run_oct(args, cwd=root, timeout=timeout)
716
717
719 message: list[str],
720 force: bool = False,
721 no_verify: bool = False,
722 profile: Optional[str] = None,
723 json_output: bool = False,
724 ignore_unstaged_mutation: bool = False,
725 timeout: int = 180,
726) -> _OctResult:
727 """Commit staged changes with the Option C quality gate enforced.
728
729 Preferred over raw ``git commit`` because the quality gate (lint +
730 format + secrets) runs before the commit; secrets detection is
731 non-bypassable even with ``force=True``.
732
733 Parameters
734 ----------
735 message : list[str]
736 One or more commit message paragraphs (each becomes a ``-m TEXT``).
737 Required. Must follow Conventional Commits format.
738 force : bool
739 Skip lint/format checks (secrets still enforced).
740 no_verify : bool
741 Forward ``--no-verify`` to ``git commit`` (skips git hooks; does
742 NOT skip oct's quality checks).
743 profile : str | None
744 Override lint profile: ``"proto"``, ``"compact"``, or ``"strict"``.
745 json_output : bool
746 Machine-readable JSON output.
747 ignore_unstaged_mutation : bool
748 Downgrade unstaged-tracked-file mutation detection from block to warn.
749 timeout : int
750 Seconds before the subprocess is killed (default 180).
751 """
752 root = str(_sandbox_root())
753 args = ["git", "commit"]
754 for m in message:
755 args.extend(["-m", m])
756 if force:
757 args.append("--force")
758 if no_verify:
759 args.append("--no-verify")
760 if profile:
761 err = _reject_operand(profile, "git commit --profile")
762 if err:
763 return err
764 args.extend(["--profile", profile])
765 if json_output:
766 args.append("--json")
767 if ignore_unstaged_mutation:
768 args.append("--ignore-unstaged-mutation")
769 return _run_oct(args, cwd=root, timeout=timeout)
770
771
773 create: Optional[str] = None,
774 switch: Optional[str] = None,
775 json_output: bool = False,
776 timeout: int = 30,
777) -> _OctResult:
778 """Create or switch branches with Option C naming enforcement.
779
780 Preferred over raw ``git branch`` / ``git checkout -b`` because it
781 enforces the project's branch-naming convention (``feat/``, ``fix/``,
782 ``chore/`` prefixes).
783
784 Pass exactly one of ``create`` or ``switch``.
785
786 Parameters
787 ----------
788 create : str | None
789 Create a new branch from HEAD with this name.
790 switch : str | None
791 Switch to an existing branch with this name.
792 json_output : bool
793 Machine-readable JSON output.
794 timeout : int
795 Seconds before the subprocess is killed (default 30).
796 """
797 root = str(_sandbox_root())
798 args = ["git", "branch"]
799 if create:
800 err = _reject_operand(create, "git branch --create")
801 if err:
802 return err
803 args.extend(["--create", create])
804 if switch:
805 err = _reject_operand(switch, "git branch --switch")
806 if err:
807 return err
808 args.extend(["--switch", switch])
809 if json_output:
810 args.append("--json")
811 return _run_oct(args, cwd=root, timeout=timeout)
812
813
815 paths: Optional[list[str]] = None,
816 staged: bool = False,
817 all_files: bool = False,
818 yes: bool = False,
819 json_output: bool = False,
820 timeout: int = 30,
821) -> _OctResult:
822 """Restore working-tree edits or unstage paths.
823
824 Preferred over raw ``git restore`` / ``git checkout --`` because it
825 scopes to the project and requires explicit confirmation when
826 discarding all staged changes.
827
828 Parameters
829 ----------
830 paths : list[str] | None
831 Files / directories to restore.
832 staged : bool
833 Unstage from index (leaves working tree intact). Without this,
834 discards working-tree edits.
835 all_files : bool
836 Restore all in-scope paths (requires ``yes=True`` if also ``staged``).
837 yes : bool
838 Skip the confirmation prompt for ``staged=True, all_files=True``.
839 json_output : bool
840 Machine-readable JSON output.
841 timeout : int
842 Seconds before the subprocess is killed (default 30).
843 """
844 root = str(_sandbox_root())
845 args = ["git", "restore"]
846 if staged:
847 args.append("--staged")
848 if all_files:
849 args.append("--all")
850 if yes:
851 args.append("--yes")
852 if json_output:
853 args.append("--json")
854 if paths:
855 err = _reject_operands(paths, "git restore path")
856 if err:
857 return err
858 args.extend(paths)
859 return _run_oct(args, cwd=root, timeout=timeout)
860
861
863 patterns: Optional[list[str]] = None,
864 remove: Optional[str] = None,
865 list_patterns: bool = False,
866 scope: Optional[str] = None,
867 json_output: bool = False,
868 timeout: int = 30,
869) -> _OctResult:
870 """Manage ``.gitignore`` patterns within the OCT-managed block.
871
872 Preferred over manually editing ``.gitignore`` because OCT manages a
873 delimited block that survives ``oct git init`` re-runs.
874
875 Pass exactly one of ``patterns``, ``remove``, or ``list_patterns``.
876
877 Parameters
878 ----------
879 patterns : list[str] | None
880 Patterns to add to ``.gitignore``.
881 remove : str | None
882 Pattern to remove from the OCT-managed block.
883 list_patterns : bool
884 List currently active ignore patterns.
885 scope : str | None
886 Which ``.gitignore`` to target: ``"workspace"``, ``"project"``,
887 or ``"nearest"`` (default).
888 json_output : bool
889 Machine-readable JSON output.
890 timeout : int
891 Seconds before the subprocess is killed (default 30).
892 """
893 root = str(_sandbox_root())
894 args = ["git", "ignore"]
895 if remove:
896 err = _reject_operand(remove, "git ignore --remove")
897 if err:
898 return err
899 args.extend(["--remove", remove])
900 if list_patterns:
901 args.append("--list")
902 if scope:
903 err = _reject_operand(scope, "git ignore --scope")
904 if err:
905 return err
906 args.extend(["--scope", scope])
907 if json_output:
908 args.append("--json")
909 if patterns:
910 err = _reject_operands(patterns, "git ignore pattern")
911 if err:
912 return err
913 args.extend(patterns)
914 return _run_oct(args, cwd=root, timeout=timeout)
915
916
918 message_file: str,
919 json_output: bool = False,
920 timeout: int = 30,
921) -> _OctResult:
922 """Validate a Conventional Commit message file.
923
924 Intended as the entry for the ``commit-msg`` pre-commit-framework
925 stage. Preferred over hand-rolled regex validation because it matches
926 exactly what ``oct git commit`` enforces.
927
928 Parameters
929 ----------
930 message_file : str
931 Path to the commit-message file (e.g. ``.git/COMMIT_EDITMSG``).
932 json_output : bool
933 Machine-readable JSON output.
934 timeout : int
935 Seconds before the subprocess is killed (default 30).
936 """
937 root = str(_sandbox_root())
938 err = _reject_operand(message_file, "git commit-msg file")
939 if err:
940 return err
941 args = ["git", "commit-msg", message_file]
942 if json_output:
943 args.append("--json")
944 return _run_oct(args, cwd=root, timeout=timeout)
945
946
948 release: Optional[str] = None,
949 since: Optional[str] = None,
950 dry_run: bool = False,
951 json_output: bool = False,
952 timeout: int = 60,
953) -> _OctResult:
954 """Generate a changelog from Conventional Commits history.
955
956 Preferred over hand-writing ``CHANGELOG.md`` entries because output is
957 aligned with Option C's CHANGELOG-format spec.
958
959 Parameters
960 ----------
961 release : str | None
962 Release version tag (e.g. ``"v0.13.0"``). When set (and not
963 ``dry_run``), writes/appends to ``CHANGELOG.md``.
964 since : str | None
965 Git ref to start from (default: last tag).
966 dry_run : bool
967 Print to stdout without writing.
968 json_output : bool
969 Machine-readable JSON output.
970 timeout : int
971 Seconds before the subprocess is killed (default 60).
972 """
973 root = str(_sandbox_root())
974 args = ["git", "changelog"]
975 if release:
976 err = _reject_operand(release, "git changelog --release")
977 if err:
978 return err
979 args.extend(["--release", release])
980 if since:
981 err = _reject_operand(since, "git changelog --since")
982 if err:
983 return err
984 args.extend(["--since", since])
985 if dry_run:
986 args.append("--dry-run")
987 if json_output:
988 args.append("--json")
989 return _run_oct(args, cwd=root, timeout=timeout)
990
991
993 action: str,
994 force: bool = False,
995 timeout: int = 30,
996) -> _OctResult:
997 """Manage Option C pre-commit hooks (install / status / remove).
998
999 Preferred over hand-editing ``.pre-commit-config.yaml`` because it
1000 installs the canonical Option C hook set and preserves any non-OCT
1001 hooks already present.
1002
1003 Parameters
1004 ----------
1005 action : str
1006 One of ``"install"``, ``"status"``, or ``"remove"``.
1007 force : bool
1008 For ``action="install"``: overwrite existing configuration.
1009 timeout : int
1010 Seconds before the subprocess is killed (default 30).
1011 """
1012 root = str(_sandbox_root())
1013 err = _reject_operand(action, "git hooks action")
1014 if err:
1015 return err
1016 args = ["git", "hooks", action]
1017 if action == "install" and force:
1018 args.append("--force")
1019 return _run_oct(args, cwd=root, timeout=timeout)
1020
1021
1022TOOLS = {
1023 "oct_lint": oct_lint,
1024 "oct_health": oct_health,
1025 "oct_typecheck": oct_typecheck,
1026 "oct_new": oct_new,
1027 "oct_test": oct_test,
1028 "oct_git_status": oct_git_status,
1029 "oct_git_check": oct_git_check,
1030 "oct_git_init": oct_git_init,
1031 "oct_git_add": oct_git_add,
1032 "oct_git_commit": oct_git_commit,
1033 "oct_git_branch": oct_git_branch,
1034 "oct_git_restore": oct_git_restore,
1035 "oct_git_ignore": oct_git_ignore,
1036 "oct_git_commit_msg": oct_git_commit_msg,
1037 "oct_git_changelog": oct_git_changelog,
1038 "oct_git_hooks": oct_git_hooks,
1039}
1040
1041SCHEMAS = [
1042 {
1043 "type": "function",
1044 "function": {
1045 "name": "oct_lint",
1046 "description": (
1047 "Run the Option C linter on the project or a specific file/directory. "
1048 "Returns violations with rule IDs and line numbers. "
1049 "Use fix_headers=true to auto-repair shebang, encoding, and path comment "
1050 "headers (add dry_run=true to preview without writing)."
1051 ),
1052 "parameters": {
1053 "type": "object",
1054 "properties": {
1055 "path": {
1056 "type": "string",
1057 "description": "File or directory to lint (default: entire project '.')",
1058 },
1059 "fix_headers": {
1060 "type": "boolean",
1061 "description": "Auto-fix shebang, encoding, and path comment headers",
1062 },
1063 "dry_run": {
1064 "type": "boolean",
1065 "description": "Preview fixes without writing (combine with fix_headers)",
1066 },
1067 "json_output": {
1068 "type": "boolean",
1069 "description": "Emit machine-readable JSON instead of human text",
1070 },
1071 "changed_only": {
1072 "type": "boolean",
1073 "description": "Lint only git-modified Python files",
1074 },
1075 "timeout": {
1076 "type": "integer",
1077 "description": "Subprocess timeout in seconds (default 120)",
1078 },
1079 },
1080 "required": [],
1081 },
1082 },
1083 },
1084 {
1085 "type": "function",
1086 "function": {
1087 "name": "oct_health",
1088 "description": (
1089 "Display the Option C project compliance dashboard — lint, tests, type "
1090 "checking, and coverage rolled into a single summary. Use json_output=true "
1091 "for machine-readable data."
1092 ),
1093 "parameters": {
1094 "type": "object",
1095 "properties": {
1096 "json_output": {
1097 "type": "boolean",
1098 "description": "Machine-readable JSON output",
1099 },
1100 "verbose": {
1101 "type": "boolean",
1102 "description": "Detailed per-file breakdown",
1103 },
1104 "timeout": {
1105 "type": "integer",
1106 "description": "Subprocess timeout in seconds (default 300)",
1107 },
1108 },
1109 "required": [],
1110 },
1111 },
1112 },
1113 {
1114 "type": "function",
1115 "function": {
1116 "name": "oct_typecheck",
1117 "description": (
1118 "Run type checking on the project via oct typecheck (delegates to pyright or mypy). "
1119 "Use json_output=true for machine-readable summary."
1120 ),
1121 "parameters": {
1122 "type": "object",
1123 "properties": {
1124 "json_output": {
1125 "type": "boolean",
1126 "description": "Machine-readable JSON summary",
1127 },
1128 "timeout": {
1129 "type": "integer",
1130 "description": "Subprocess timeout in seconds (default 120)",
1131 },
1132 },
1133 "required": [],
1134 },
1135 },
1136 },
1137 {
1138 "type": "function",
1139 "function": {
1140 "name": "oct_new",
1141 "description": (
1142 "Create a new Option C-compliant Python file via oct new. "
1143 "Generates the mandatory 3-line header, from oc_diagnostics import _dbg, "
1144 "and a complete docstring skeleton (Purpose, Responsibilities, Diagnostics, "
1145 "Contracts). Preferred over write_file for creating new .py files."
1146 ),
1147 "parameters": {
1148 "type": "object",
1149 "properties": {
1150 "path": {
1151 "type": "string",
1152 "description": "Target file path (required, must end in .py)",
1153 },
1154 "force": {
1155 "type": "boolean",
1156 "description": "Overwrite the file if it already exists",
1157 },
1158 "test": {
1159 "type": "boolean",
1160 "description": "Also generate a companion test file in tests/",
1161 },
1162 "dry_run": {
1163 "type": "boolean",
1164 "description": "Preview what would be created without writing anything",
1165 },
1166 "verbose": {
1167 "type": "boolean",
1168 "description": "Show generated file content",
1169 },
1170 "timeout": {
1171 "type": "integer",
1172 "description": "Subprocess timeout in seconds (default 30)",
1173 },
1174 },
1175 "required": ["path"],
1176 },
1177 },
1178 },
1179 {
1180 "type": "function",
1181 "function": {
1182 "name": "oct_test",
1183 "description": (
1184 "Run the oc_chat test suite through the Option C test stand-in "
1185 "and return a distilled oct-test/v0.4 payload: summary counts, "
1186 "contract violations, errors, and the non-passed tests (capped "
1187 "at 50 with explicit truncation fields). The full report with "
1188 "every test entry is written to logs/ and returned as "
1189 "report_path. Preferred over run_command pytest: structured "
1190 "result, no Tier-2 confirmation. A full strict run takes "
1191 "several minutes; use dry_run=true for a fast collection-only "
1192 "check, or nodeids/marker to narrow the selection."
1193 ),
1194 "parameters": {
1195 "type": "object",
1196 "properties": {
1197 "profile": {
1198 "type": "string",
1199 "enum": ["strict", "smoke", "full"],
1200 "description": "Test profile (default: strict)",
1201 },
1202 "seed": {
1203 "type": "integer",
1204 "description": "Pin randomized test ordering (pytest-randomly seed)",
1205 },
1206 "workers": {
1207 "type": ["integer", "string"],
1208 "description": "Worker count or 'auto' (the stand-in runs serially and reports actual: 1)",
1209 },
1210 "marker": {
1211 "type": "string",
1212 "description": "Extra pytest marker expression, AND-composed with the profile",
1213 },
1214 "nodeids": {
1215 "type": "array",
1216 "items": {"type": "string"},
1217 "description": "Individual pytest nodeids to run",
1218 },
1219 "dry_run": {
1220 "type": "boolean",
1221 "description": "Collection and static checks only; no test executes",
1222 },
1223 "per_test_timeout": {
1224 "type": "integer",
1225 "description": "Per-test wall-clock bound in seconds",
1226 },
1227 "timeout": {
1228 "type": "integer",
1229 "description": "Tool-level wall clock in seconds (default 900, max 1800)",
1230 },
1231 },
1232 "required": [],
1233 },
1234 },
1235 },
1236 {
1237 "type": "function",
1238 "function": {
1239 "name": "oct_git_status",
1240 "description": (
1241 "Show git status with Option C compliance annotations. "
1242 "Preferred over raw `git status` because it annotates files with "
1243 "lint/format/secrets state on top of the standard "
1244 "staged/modified/untracked breakdown."
1245 ),
1246 "parameters": {
1247 "type": "object",
1248 "properties": {
1249 "json_output": {"type": "boolean", "description": "Machine-readable JSON output"},
1250 "no_check": {"type": "boolean", "description": "Skip compliance annotations (plain status)"},
1251 "scope": {
1252 "type": "string",
1253 "enum": ["project", "repo", "auto"],
1254 "description": "File scope (default: project)",
1255 },
1256 "timeout": {"type": "integer", "description": "Subprocess timeout in seconds (default 30)"},
1257 },
1258 "required": [],
1259 },
1260 },
1261 },
1262 {
1263 "type": "function",
1264 "function": {
1265 "name": "oct_git_check",
1266 "description": (
1267 "Run the unified Option C quality gate (lint + format + secrets). "
1268 "Preferred over running oct_lint and oct_format separately because it "
1269 "applies the same gate that oct_git_commit enforces. Use fix=true to "
1270 "auto-fix lint/format violations."
1271 ),
1272 "parameters": {
1273 "type": "object",
1274 "properties": {
1275 "staged_only": {"type": "boolean", "description": "Check only staged files"},
1276 "all_files": {"type": "boolean", "description": "Check every Python file in the project"},
1277 "include_tests": {"type": "boolean", "description": "Also run pytest (exit 4 on failure)"},
1278 "fix": {"type": "boolean", "description": "Attempt auto-fix of lint/format violations, then re-check"},
1279 "profile": {
1280 "type": "string",
1281 "enum": ["proto", "compact", "strict"],
1282 "description": "Override lint profile",
1283 },
1284 "json_output": {"type": "boolean", "description": "Machine-readable JSON output"},
1285 "sandbox": {"type": "boolean", "description": "Run pytest under MCP sandbox"},
1286 "timeout": {"type": "integer", "description": "Subprocess timeout in seconds (default 120)"},
1287 },
1288 "required": [],
1289 },
1290 },
1291 },
1292 {
1293 "type": "function",
1294 "function": {
1295 "name": "oct_git_init",
1296 "description": (
1297 "Initialize / enhance a git repo with Option C conventions "
1298 "(.gitignore, .gitattributes, .pre-commit-config.yaml, optional GitHub "
1299 "Actions workflow). Preferred over manually editing those files because "
1300 "it applies the canonical Option C templates and merges idempotently."
1301 ),
1302 "parameters": {
1303 "type": "object",
1304 "properties": {
1305 "dry_run": {"type": "boolean", "description": "Show what would be created/modified without writing"},
1306 "github": {"type": "boolean", "description": "Also generate .github/workflows/option-c.yml"},
1307 "no_hooks": {"type": "boolean", "description": "Skip .pre-commit-config.yaml creation"},
1308 "timeout": {"type": "integer", "description": "Subprocess timeout in seconds (default 60)"},
1309 },
1310 "required": [],
1311 },
1312 },
1313 },
1314 {
1315 "type": "function",
1316 "function": {
1317 "name": "oct_git_add",
1318 "description": (
1319 "Stage files for commit. Preferred over raw `git add` because it scopes "
1320 "to the project (vs. the whole repo) and runs a quality-gate summary on "
1321 "the staged set."
1322 ),
1323 "parameters": {
1324 "type": "object",
1325 "properties": {
1326 "paths": {
1327 "type": "array",
1328 "items": {"type": "string"},
1329 "description": "Files / directories to stage",
1330 },
1331 "patch": {"type": "boolean", "description": "Interactive hunk-level staging (-p)"},
1332 "update": {"type": "boolean", "description": "Stage all already-tracked modified files (-u)"},
1333 "all_files": {"type": "boolean", "description": "Stage all changes within project scope (-A)"},
1334 "dry_run": {"type": "boolean", "description": "Show what would be staged without staging"},
1335 "json_output": {"type": "boolean", "description": "Machine-readable JSON output"},
1336 "timeout": {"type": "integer", "description": "Subprocess timeout in seconds (default 60)"},
1337 },
1338 "required": [],
1339 },
1340 },
1341 },
1342 {
1343 "type": "function",
1344 "function": {
1345 "name": "oct_git_commit",
1346 "description": (
1347 "Commit staged changes with the Option C quality gate enforced. "
1348 "Preferred over raw `git commit` because lint + format + secrets run "
1349 "before the commit; secrets detection is non-bypassable even with "
1350 "force=true. Message must follow Conventional Commits format."
1351 ),
1352 "parameters": {
1353 "type": "object",
1354 "properties": {
1355 "message": {
1356 "type": "array",
1357 "items": {"type": "string"},
1358 "description": (
1359 "One or more commit message paragraphs (each becomes a "
1360 "separate -m TEXT). First paragraph is the title; subsequent "
1361 "paragraphs become the body."
1362 ),
1363 },
1364 "force": {"type": "boolean", "description": "Skip lint/format checks (secrets still enforced)"},
1365 "no_verify": {"type": "boolean", "description": "Forward --no-verify to git commit (skips git hooks only)"},
1366 "profile": {
1367 "type": "string",
1368 "enum": ["proto", "compact", "strict"],
1369 "description": "Override lint profile",
1370 },
1371 "json_output": {"type": "boolean", "description": "Machine-readable JSON output"},
1372 "ignore_unstaged_mutation": {
1373 "type": "boolean",
1374 "description": "Downgrade unstaged-tracked-file mutation detection from block to warn",
1375 },
1376 "timeout": {"type": "integer", "description": "Subprocess timeout in seconds (default 180)"},
1377 },
1378 "required": ["message"],
1379 },
1380 },
1381 },
1382 {
1383 "type": "function",
1384 "function": {
1385 "name": "oct_git_branch",
1386 "description": (
1387 "Create or switch branches with Option C naming enforcement. Preferred "
1388 "over raw `git branch` / `git checkout -b` because it enforces the "
1389 "branch-naming convention (feat/, fix/, chore/ prefixes). Pass exactly "
1390 "one of create or switch."
1391 ),
1392 "parameters": {
1393 "type": "object",
1394 "properties": {
1395 "create": {"type": "string", "description": "Create a new branch from HEAD with this name"},
1396 "switch": {"type": "string", "description": "Switch to an existing branch with this name"},
1397 "json_output": {"type": "boolean", "description": "Machine-readable JSON output"},
1398 "timeout": {"type": "integer", "description": "Subprocess timeout in seconds (default 30)"},
1399 },
1400 "required": [],
1401 },
1402 },
1403 },
1404 {
1405 "type": "function",
1406 "function": {
1407 "name": "oct_git_restore",
1408 "description": (
1409 "Restore working-tree edits or unstage paths. Preferred over raw "
1410 "`git restore` / `git checkout --` because it scopes to the project "
1411 "and requires explicit confirmation when discarding all staged changes."
1412 ),
1413 "parameters": {
1414 "type": "object",
1415 "properties": {
1416 "paths": {
1417 "type": "array",
1418 "items": {"type": "string"},
1419 "description": "Files / directories to restore",
1420 },
1421 "staged": {
1422 "type": "boolean",
1423 "description": "Unstage from index (leaves working tree intact); without this, discards working-tree edits",
1424 },
1425 "all_files": {"type": "boolean", "description": "Restore all in-scope paths"},
1426 "yes": {"type": "boolean", "description": "Skip confirmation prompt for staged + all_files"},
1427 "json_output": {"type": "boolean", "description": "Machine-readable JSON output"},
1428 "timeout": {"type": "integer", "description": "Subprocess timeout in seconds (default 30)"},
1429 },
1430 "required": [],
1431 },
1432 },
1433 },
1434 {
1435 "type": "function",
1436 "function": {
1437 "name": "oct_git_ignore",
1438 "description": (
1439 "Manage .gitignore patterns within the OCT-managed block. Preferred over "
1440 "manually editing .gitignore because OCT manages a delimited block that "
1441 "survives oct_git_init re-runs. Pass exactly one of patterns, remove, or "
1442 "list_patterns."
1443 ),
1444 "parameters": {
1445 "type": "object",
1446 "properties": {
1447 "patterns": {
1448 "type": "array",
1449 "items": {"type": "string"},
1450 "description": "Patterns to add to .gitignore",
1451 },
1452 "remove": {"type": "string", "description": "Pattern to remove from the OCT-managed block"},
1453 "list_patterns": {"type": "boolean", "description": "List currently active ignore patterns"},
1454 "scope": {
1455 "type": "string",
1456 "enum": ["workspace", "project", "nearest"],
1457 "description": "Which .gitignore to target (default: nearest)",
1458 },
1459 "json_output": {"type": "boolean", "description": "Machine-readable JSON output"},
1460 "timeout": {"type": "integer", "description": "Subprocess timeout in seconds (default 30)"},
1461 },
1462 "required": [],
1463 },
1464 },
1465 },
1466 {
1467 "type": "function",
1468 "function": {
1469 "name": "oct_git_commit_msg",
1470 "description": (
1471 "Validate a Conventional Commit message file. Intended as the entry for "
1472 "the commit-msg pre-commit-framework stage. Preferred over hand-rolled "
1473 "regex validation because it matches exactly what oct_git_commit enforces."
1474 ),
1475 "parameters": {
1476 "type": "object",
1477 "properties": {
1478 "message_file": {
1479 "type": "string",
1480 # OCT-LINT: disable=no_hardcoded_secrets reason="Pre-existing schema description prose (changelog/hooks tool descriptions) mis-flagged as high-entropy; model-facing help text, not credentials"
1481 "description": "Path to the commit-message file (e.g. .git/COMMIT_EDITMSG)",
1482 },
1483 "json_output": {"type": "boolean", "description": "Machine-readable JSON output"},
1484 "timeout": {"type": "integer", "description": "Subprocess timeout in seconds (default 30)"},
1485 },
1486 "required": ["message_file"],
1487 },
1488 },
1489 },
1490 {
1491 "type": "function",
1492 "function": {
1493 "name": "oct_git_changelog",
1494 "description": (
1495 # OCT-LINT: disable=no_hardcoded_secrets reason="Pre-existing schema description prose (changelog/hooks tool descriptions) mis-flagged as high-entropy; model-facing help text, not credentials"
1496 "Generate a changelog from Conventional Commits history. Preferred over "
1497 "hand-writing CHANGELOG.md entries because output is aligned with Option "
1498 "C's CHANGELOG-format spec. With release set (and not dry_run), writes "
1499 "to CHANGELOG.md."
1500 ),
1501 "parameters": {
1502 "type": "object",
1503 "properties": {
1504 "release": {"type": "string", "description": "Release version tag (e.g. 'v0.13.0')"},
1505 "since": {"type": "string", "description": "Git ref to start from (default: last tag)"},
1506 "dry_run": {"type": "boolean", "description": "Print to stdout without writing"},
1507 "json_output": {"type": "boolean", "description": "Machine-readable JSON output"},
1508 "timeout": {"type": "integer", "description": "Subprocess timeout in seconds (default 60)"},
1509 },
1510 "required": [],
1511 },
1512 },
1513 },
1514 {
1515 "type": "function",
1516 "function": {
1517 "name": "oct_git_hooks",
1518 "description": (
1519 # OCT-LINT: disable=no_hardcoded_secrets reason="Pre-existing schema description prose (changelog/hooks tool descriptions) mis-flagged as high-entropy; model-facing help text, not credentials"
1520 "Manage Option C pre-commit hooks: action='install' generates "
1521 ".pre-commit-config.yaml with the canonical hook set; action='status' "
1522 "shows installed hook state; action='remove' removes Option C hooks "
1523 "while preserving any non-OCT hooks. Preferred over hand-editing "
1524 ".pre-commit-config.yaml."
1525 ),
1526 "parameters": {
1527 "type": "object",
1528 "properties": {
1529 "action": {
1530 "type": "string",
1531 "enum": ["install", "status", "remove"],
1532 "description": "Hook subcommand to run",
1533 },
1534 "force": {
1535 "type": "boolean",
1536 "description": "For action='install': overwrite existing configuration",
1537 },
1538 "timeout": {"type": "integer", "description": "Subprocess timeout in seconds (default 30)"},
1539 },
1540 "required": ["action"],
1541 },
1542 },
1543 },
1544]
_OctResult oct_git_status(bool json_output=False, bool no_check=False, Optional[str] scope=None, int timeout=30)
Definition oct_tools.py:544
"ToolError | None" _reject_operands("list[str]" values, str field)
Definition oct_tools.py:192
"ToolError | None" _reject_operand(str value, str field)
Definition oct_tools.py:172
_OctResult oct_typecheck(bool json_output=False, int timeout=120)
Definition oct_tools.py:272
_OctResult _run_oct(list[str] args, str cwd, int timeout=60)
Definition oct_tools.py:135
_OctResult oct_git_check(bool staged_only=False, bool all_files=False, bool include_tests=False, bool fix=False, Optional[str] profile=None, bool json_output=False, bool sandbox=False, int timeout=120)
Definition oct_tools.py:585
dict oct_test(str profile="strict", "int | None" seed=None, "int | str | None" workers=None, "str | None" marker=None, "list[str] | None" nodeids=None, bool dry_run=False, "int | None" per_test_timeout=None, int timeout=900)
Definition oct_tools.py:386
_OctResult oct_lint(str path=".", bool fix_headers=False, bool dry_run=False, bool json_output=False, bool changed_only=False, int timeout=120)
Definition oct_tools.py:208
_OctResult oct_git_branch(Optional[str] create=None, Optional[str] switch=None, bool json_output=False, int timeout=30)
Definition oct_tools.py:777
_OctResult oct_health(bool json_output=False, bool verbose=False, int timeout=300)
Definition oct_tools.py:248
_OctResult oct_git_ignore(Optional[list[str]] patterns=None, Optional[str] remove=None, bool list_patterns=False, Optional[str] scope=None, bool json_output=False, int timeout=30)
Definition oct_tools.py:869
_OctResult oct_git_commit_msg(str message_file, bool json_output=False, int timeout=30)
Definition oct_tools.py:921
_OctResult oct_git_restore(Optional[list[str]] paths=None, bool staged=False, bool all_files=False, bool yes=False, bool json_output=False, int timeout=30)
Definition oct_tools.py:821
"Callable[[str], None]" _note_activity_lines_of_one_stream()
Definition oct_tools.py:109
_OctResult oct_git_add(Optional[list[str]] paths=None, bool patch=False, bool update=False, bool all_files=False, bool dry_run=False, bool json_output=False, int timeout=60)
Definition oct_tools.py:675
Optional[str] _oct_exe()
Definition oct_tools.py:104
_OctResult oct_git_init(bool dry_run=False, bool github=False, bool no_hooks=False, int timeout=60)
Definition oct_tools.py:638
_OctResult oct_new(str path, bool force=False, bool test=False, bool dry_run=False, bool verbose=False, int timeout=30)
Definition oct_tools.py:296
_OctResult oct_git_changelog(Optional[str] release=None, Optional[str] since=None, bool dry_run=False, bool json_output=False, int timeout=60)
Definition oct_tools.py:953
_OctResult oct_git_hooks(str action, bool force=False, int timeout=30)
Definition oct_tools.py:996
_OctResult oct_git_commit(list[str] message, bool force=False, bool no_verify=False, Optional[str] profile=None, bool json_output=False, bool ignore_unstaged_mutation=False, int timeout=180)
Definition oct_tools.py:726