7oct toolchain integration — lint, health, typecheck, file generator, and git suite.
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.
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.
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
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
44- ``TOOLS`` and ``SCHEMAS`` exports for auto-registration.
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.
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)
80oc_chat/docs/conventions/CONTRACTS.md §3 Tool registry contract; §4 Tool return-
81value conventions (oct_* outputs → §4.1; missing-dep hint → §4.6).
88from collections.abc
import Callable
89from pathlib
import Path
90from typing
import Optional
92from oc_diagnostics
import _dbg
101_OctResult = CommandResult | MissingDependency | ToolError
105 """Return the path to the ``oct`` executable, or ``None`` if absent."""
106 return shutil.which(
"oct")
110 """Return the observer that shows ONE stream's lines on the status bar.
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.
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.
123 from ._sandbox
import LineRedactor
126 redactor = LineRedactor()
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))
132 return _note_activity_line
135def _run_oct(args: list[str], cwd: str, timeout: int = 60) -> _OctResult:
136 """Execute ``oct <args>`` and return ``{stdout, stderr, returncode}``."""
138 _dbg(
"OC_CHAT.TOOLS", func, f
"args={args} cwd={cwd} timeout={timeout}", 3)
142 "missing_dependency":
"oct",
143 "install_hint":
"pip install -e .[option_c]",
148 from ._sandbox
import scrubbed_env
149 env = scrubbed_env({
"TMPDIR": str(Path(cwd) /
"logs")})
151 from ._proc
import run_cancellable
154 result = run_cancellable(
159 on_line_factory=_note_activity_lines_of_one_stream,
162 "stdout": result.stdout,
163 "stderr": result.stderr,
164 "returncode": result.returncode,
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}
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).
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.
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}"}
193 """Apply :func:`_reject_operand` to each value in a list operand."""
203 fix_headers: bool =
False,
204 dry_run: bool =
False,
205 json_output: bool =
False,
206 changed_only: bool =
False,
209 """Run ``oct lint`` on the project or a specific path.
214 File or directory to lint (default: entire project).
216 Auto-fix shebang, encoding, and path comment headers.
218 Preview fixes without writing (only meaningful with ``fix_headers``).
220 Emit machine-readable JSON instead of human text.
222 Lint only git-modified Python files.
224 Seconds before the subprocess is killed (default 120).
226 root = str(_sandbox_root())
229 args.append(
"--fix-headers")
231 args.append(
"--dry-run")
233 args.append(
"--json")
235 args.append(
"--changed")
236 if path
and path !=
".":
241 return _run_oct(args, cwd=root, timeout=timeout)
245 json_output: bool =
False,
246 verbose: bool =
False,
249 """Display the Option C project compliance dashboard.
254 Machine-readable JSON output.
256 Detailed per-file breakdown.
258 Seconds before the subprocess is killed (default 300).
260 root = str(_sandbox_root())
263 args.append(
"--json")
265 args.append(
"--verbose")
266 return _run_oct(args, cwd=root, timeout=timeout)
270 json_output: bool =
False,
273 """Run type checking on the project via ``oct typecheck``.
278 Machine-readable JSON summary.
280 Seconds before the subprocess is killed (default 120).
282 root = str(_sandbox_root())
285 args.append(
"--json")
286 return _run_oct(args, cwd=root, timeout=timeout)
293 dry_run: bool =
False,
294 verbose: bool =
False,
297 """Create a new Option C-compliant Python file via ``oct new``.
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.
307 Target file path (required, must end in ``.py``).
309 Overwrite the file if it already exists.
311 Also generate a companion test file in ``tests/``.
313 Preview what would be created without writing anything.
315 Print generated file content to stdout.
317 Seconds before the subprocess is killed (default 30).
319 root = str(_sandbox_root())
329 SandboxViolation, is_human_confirm_write_path, is_importable_write_path,
330 is_protected_write_path, require_inside_sandbox,
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)")
346 args.append(
"--force")
348 args.append(
"--test")
350 args.append(
"--dry-run")
352 args.append(
"--verbose")
353 return _run_oct(args, cwd=root, timeout=timeout)
363_OC_CHAT_SUBPROJECT_ROOT = Path(__file__).resolve().parents[2]
369_OCT_TEST_TIMEOUT_CAP = 1800
374_OCT_TEST_NON_PASSED_CAP = 50
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,
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.
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).
403 ``strict`` (default) | ``smoke`` | ``full`` — validated by the
404 stand-in (a bad value returns an exit-4 payload).
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.
412 Extra marker expression, AND-composed with the profile expression.
413 nodeids : list[str] | None
414 Individual pytest nodeids (repeatable ``--nodeid``).
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).
420 Tool-level wall clock (default 900 s; clamped to [1, 1800]).
423 _dbg(
"OC_CHAT.TOOLS", func,
424 f
"profile={profile} dry_run={dry_run} nodeids={len(nodeids or [])}", 3)
426 for value, field
in ((profile,
"oct_test profile"),
427 (marker,
"oct_test marker")):
428 if value
is not None:
432 if isinstance(workers, str)
and workers !=
"auto":
441 clamped = max(1, min(int(timeout), _OCT_TEST_TIMEOUT_CAP))
443 sandbox_logs = Path(_sandbox_root()) /
"logs"
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}"}
450 args = [sys.executable,
"-m",
"oc_chat.testing",
451 "--package",
"oc_chat",
"--json",
452 "--profile", profile]
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)]
464 args.append(
"--dry-run")
467 from ._sandbox
import scrubbed_env
468 env = scrubbed_env({
"TMPDIR": str(sandbox_logs)})
470 from ._proc
import run_cancellable
472 result = run_cancellable(
475 cwd=str(_OC_CHAT_SUBPROJECT_ROOT),
477 on_line_factory=_note_activity_lines_of_one_stream,
479 except subprocess.TimeoutExpired:
480 return {
"error": f
"oct_test timed out after {clamped}s",
482 except Exception
as exc:
483 return {
"error": str(exc),
"args": args}
486 result.returncode == -2
487 or (result.stderr
or "").rstrip().endswith(
"[cancelled by user]")
494 return {
"stdout": result.stdout,
"stderr": result.stderr,
495 "returncode": result.returncode}
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}")}
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}")}
509 run_id = report.get(
"run_id")
or "unidentified"
510 report_path = sandbox_logs / f
"oct_test-{run_id}.json"
512 report_path.write_text(json.dumps(report, indent=2),
514 except OSError
as exc:
515 return {
"error": f
"oct_test: cannot write the full report to "
516 f
"{report_path}: {exc}"}
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
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)
540 json_output: bool =
False,
541 no_check: bool =
False,
542 scope: Optional[str] =
None,
545 """Show git status with Option C compliance annotations.
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.
554 Machine-readable JSON output.
556 Skip compliance annotations (plain status).
558 File scope: ``"project"`` (default), ``"repo"``, or ``"auto"``.
560 Seconds before the subprocess is killed (default 30).
562 root = str(_sandbox_root())
563 args = [
"git",
"status"]
565 args.append(
"--json")
567 args.append(
"--no-check")
572 args.extend([
"--scope", scope])
573 return _run_oct(args, cwd=root, timeout=timeout)
577 staged_only: bool =
False,
578 all_files: bool =
False,
579 include_tests: bool =
False,
581 profile: Optional[str] =
None,
582 json_output: bool =
False,
583 sandbox: bool =
False,
586 """Run the unified Option C quality gate (lint + format + secrets).
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.
595 Check only staged files (default scope).
597 Check every Python file in the project.
599 Also run pytest (exit 4 on failure).
601 Attempt auto-fix of lint/format violations, then re-check.
603 Override lint profile: ``"proto"``, ``"compact"``, or ``"strict"``.
605 Machine-readable JSON output.
607 Run pytest under MCP sandbox (strips PYTHONPATH and secret env vars).
609 Seconds before the subprocess is killed (default 120).
611 root = str(_sandbox_root())
612 args = [
"git",
"check"]
614 args.append(
"--staged-only")
618 args.append(
"--include-tests")
625 args.extend([
"--profile", profile])
627 args.append(
"--json")
629 args.append(
"--sandbox")
630 return _run_oct(args, cwd=root, timeout=timeout)
634 dry_run: bool =
False,
635 github: bool =
False,
636 no_hooks: bool =
False,
639 """Initialize / enhance a git repo with Option C conventions.
641 Preferred over manually editing ``.gitignore`` / ``.gitattributes`` /
642 ``.pre-commit-config.yaml`` because it applies the canonical Option C
643 templates and merges idempotently.
648 Show what would be created/modified without writing.
650 Also generate ``.github/workflows/option-c.yml``.
652 Skip ``.pre-commit-config.yaml`` creation.
654 Seconds before the subprocess is killed (default 60).
656 root = str(_sandbox_root())
657 args = [
"git",
"init"]
659 args.append(
"--dry-run")
661 args.append(
"--github")
663 args.append(
"--no-hooks")
664 return _run_oct(args, cwd=root, timeout=timeout)
668 paths: Optional[list[str]] =
None,
670 update: bool =
False,
671 all_files: bool =
False,
672 dry_run: bool =
False,
673 json_output: bool =
False,
676 """Stage files for commit, with optional post-stage quality-gate summary.
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.
683 paths : list[str] | None
684 Files / directories to stage.
686 Interactive hunk-level staging (passthrough to ``git add -p``).
688 Stage all already-tracked modified files.
690 Stage all changes (tracked + untracked) within project scope.
692 Show what would be staged without staging.
694 Machine-readable JSON output.
696 Seconds before the subprocess is killed (default 60).
698 root = str(_sandbox_root())
699 args = [
"git",
"add"]
701 args.append(
"--patch")
703 args.append(
"--update")
707 args.append(
"--dry-run")
709 args.append(
"--json")
715 return _run_oct(args, cwd=root, timeout=timeout)
721 no_verify: bool =
False,
722 profile: Optional[str] =
None,
723 json_output: bool =
False,
724 ignore_unstaged_mutation: bool =
False,
727 """Commit staged changes with the Option C quality gate enforced.
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``.
736 One or more commit message paragraphs (each becomes a ``-m TEXT``).
737 Required. Must follow Conventional Commits format.
739 Skip lint/format checks (secrets still enforced).
741 Forward ``--no-verify`` to ``git commit`` (skips git hooks; does
742 NOT skip oct's quality checks).
744 Override lint profile: ``"proto"``, ``"compact"``, or ``"strict"``.
746 Machine-readable JSON output.
747 ignore_unstaged_mutation : bool
748 Downgrade unstaged-tracked-file mutation detection from block to warn.
750 Seconds before the subprocess is killed (default 180).
752 root = str(_sandbox_root())
753 args = [
"git",
"commit"]
755 args.extend([
"-m", m])
757 args.append(
"--force")
759 args.append(
"--no-verify")
764 args.extend([
"--profile", profile])
766 args.append(
"--json")
767 if ignore_unstaged_mutation:
768 args.append(
"--ignore-unstaged-mutation")
769 return _run_oct(args, cwd=root, timeout=timeout)
773 create: Optional[str] =
None,
774 switch: Optional[str] =
None,
775 json_output: bool =
False,
778 """Create or switch branches with Option C naming enforcement.
780 Preferred over raw ``git branch`` / ``git checkout -b`` because it
781 enforces the project's branch-naming convention (``feat/``, ``fix/``,
782 ``chore/`` prefixes).
784 Pass exactly one of ``create`` or ``switch``.
789 Create a new branch from HEAD with this name.
791 Switch to an existing branch with this name.
793 Machine-readable JSON output.
795 Seconds before the subprocess is killed (default 30).
797 root = str(_sandbox_root())
798 args = [
"git",
"branch"]
803 args.extend([
"--create", create])
808 args.extend([
"--switch", switch])
810 args.append(
"--json")
811 return _run_oct(args, cwd=root, timeout=timeout)
815 paths: Optional[list[str]] =
None,
816 staged: bool =
False,
817 all_files: bool =
False,
819 json_output: bool =
False,
822 """Restore working-tree edits or unstage paths.
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.
830 paths : list[str] | None
831 Files / directories to restore.
833 Unstage from index (leaves working tree intact). Without this,
834 discards working-tree edits.
836 Restore all in-scope paths (requires ``yes=True`` if also ``staged``).
838 Skip the confirmation prompt for ``staged=True, all_files=True``.
840 Machine-readable JSON output.
842 Seconds before the subprocess is killed (default 30).
844 root = str(_sandbox_root())
845 args = [
"git",
"restore"]
847 args.append(
"--staged")
853 args.append(
"--json")
859 return _run_oct(args, cwd=root, timeout=timeout)
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,
870 """Manage ``.gitignore`` patterns within the OCT-managed block.
872 Preferred over manually editing ``.gitignore`` because OCT manages a
873 delimited block that survives ``oct git init`` re-runs.
875 Pass exactly one of ``patterns``, ``remove``, or ``list_patterns``.
879 patterns : list[str] | None
880 Patterns to add to ``.gitignore``.
882 Pattern to remove from the OCT-managed block.
884 List currently active ignore patterns.
886 Which ``.gitignore`` to target: ``"workspace"``, ``"project"``,
887 or ``"nearest"`` (default).
889 Machine-readable JSON output.
891 Seconds before the subprocess is killed (default 30).
893 root = str(_sandbox_root())
894 args = [
"git",
"ignore"]
899 args.extend([
"--remove", remove])
901 args.append(
"--list")
906 args.extend([
"--scope", scope])
908 args.append(
"--json")
913 args.extend(patterns)
914 return _run_oct(args, cwd=root, timeout=timeout)
919 json_output: bool =
False,
922 """Validate a Conventional Commit message file.
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.
931 Path to the commit-message file (e.g. ``.git/COMMIT_EDITMSG``).
933 Machine-readable JSON output.
935 Seconds before the subprocess is killed (default 30).
937 root = str(_sandbox_root())
941 args = [
"git",
"commit-msg", message_file]
943 args.append(
"--json")
944 return _run_oct(args, cwd=root, timeout=timeout)
948 release: Optional[str] =
None,
949 since: Optional[str] =
None,
950 dry_run: bool =
False,
951 json_output: bool =
False,
954 """Generate a changelog from Conventional Commits history.
956 Preferred over hand-writing ``CHANGELOG.md`` entries because output is
957 aligned with Option C's CHANGELOG-format spec.
962 Release version tag (e.g. ``"v0.13.0"``). When set (and not
963 ``dry_run``), writes/appends to ``CHANGELOG.md``.
965 Git ref to start from (default: last tag).
967 Print to stdout without writing.
969 Machine-readable JSON output.
971 Seconds before the subprocess is killed (default 60).
973 root = str(_sandbox_root())
974 args = [
"git",
"changelog"]
979 args.extend([
"--release", release])
984 args.extend([
"--since", since])
986 args.append(
"--dry-run")
988 args.append(
"--json")
989 return _run_oct(args, cwd=root, timeout=timeout)
997 """Manage Option C pre-commit hooks (install / status / remove).
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.
1006 One of ``"install"``, ``"status"``, or ``"remove"``.
1008 For ``action="install"``: overwrite existing configuration.
1010 Seconds before the subprocess is killed (default 30).
1012 root = str(_sandbox_root())
1016 args = [
"git",
"hooks", action]
1017 if action ==
"install" and force:
1018 args.append(
"--force")
1019 return _run_oct(args, cwd=root, timeout=timeout)
1023 "oct_lint": oct_lint,
1024 "oct_health": oct_health,
1025 "oct_typecheck": oct_typecheck,
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,
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)."
1057 "description":
"File or directory to lint (default: entire project '.')",
1061 "description":
"Auto-fix shebang, encoding, and path comment headers",
1065 "description":
"Preview fixes without writing (combine with fix_headers)",
1069 "description":
"Emit machine-readable JSON instead of human text",
1073 "description":
"Lint only git-modified Python files",
1077 "description":
"Subprocess timeout in seconds (default 120)",
1087 "name":
"oct_health",
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."
1098 "description":
"Machine-readable JSON output",
1102 "description":
"Detailed per-file breakdown",
1106 "description":
"Subprocess timeout in seconds (default 300)",
1116 "name":
"oct_typecheck",
1118 "Run type checking on the project via oct typecheck (delegates to pyright or mypy). "
1119 "Use json_output=true for machine-readable summary."
1126 "description":
"Machine-readable JSON summary",
1130 "description":
"Subprocess timeout in seconds (default 120)",
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."
1152 "description":
"Target file path (required, must end in .py)",
1156 "description":
"Overwrite the file if it already exists",
1160 "description":
"Also generate a companion test file in tests/",
1164 "description":
"Preview what would be created without writing anything",
1168 "description":
"Show generated file content",
1172 "description":
"Subprocess timeout in seconds (default 30)",
1175 "required": [
"path"],
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."
1199 "enum": [
"strict",
"smoke",
"full"],
1200 "description":
"Test profile (default: strict)",
1204 "description":
"Pin randomized test ordering (pytest-randomly seed)",
1207 "type": [
"integer",
"string"],
1208 "description":
"Worker count or 'auto' (the stand-in runs serially and reports actual: 1)",
1212 "description":
"Extra pytest marker expression, AND-composed with the profile",
1216 "items": {
"type":
"string"},
1217 "description":
"Individual pytest nodeids to run",
1221 "description":
"Collection and static checks only; no test executes",
1223 "per_test_timeout": {
1225 "description":
"Per-test wall-clock bound in seconds",
1229 "description":
"Tool-level wall clock in seconds (default 900, max 1800)",
1239 "name":
"oct_git_status",
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."
1249 "json_output": {
"type":
"boolean",
"description":
"Machine-readable JSON output"},
1250 "no_check": {
"type":
"boolean",
"description":
"Skip compliance annotations (plain status)"},
1253 "enum": [
"project",
"repo",
"auto"],
1254 "description":
"File scope (default: project)",
1256 "timeout": {
"type":
"integer",
"description":
"Subprocess timeout in seconds (default 30)"},
1265 "name":
"oct_git_check",
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."
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"},
1281 "enum": [
"proto",
"compact",
"strict"],
1282 "description":
"Override lint profile",
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)"},
1295 "name":
"oct_git_init",
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."
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)"},
1317 "name":
"oct_git_add",
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 "
1328 "items": {
"type":
"string"},
1329 "description":
"Files / directories to stage",
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)"},
1345 "name":
"oct_git_commit",
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."
1357 "items": {
"type":
"string"},
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."
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)"},
1368 "enum": [
"proto",
"compact",
"strict"],
1369 "description":
"Override lint profile",
1371 "json_output": {
"type":
"boolean",
"description":
"Machine-readable JSON output"},
1372 "ignore_unstaged_mutation": {
1374 "description":
"Downgrade unstaged-tracked-file mutation detection from block to warn",
1376 "timeout": {
"type":
"integer",
"description":
"Subprocess timeout in seconds (default 180)"},
1378 "required": [
"message"],
1385 "name":
"oct_git_branch",
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."
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)"},
1407 "name":
"oct_git_restore",
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."
1418 "items": {
"type":
"string"},
1419 "description":
"Files / directories to restore",
1423 "description":
"Unstage from index (leaves working tree intact); without this, discards working-tree edits",
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)"},
1437 "name":
"oct_git_ignore",
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 "
1449 "items": {
"type":
"string"},
1450 "description":
"Patterns to add to .gitignore",
1452 "remove": {
"type":
"string",
"description":
"Pattern to remove from the OCT-managed block"},
1453 "list_patterns": {
"type":
"boolean",
"description":
"List currently active ignore patterns"},
1456 "enum": [
"workspace",
"project",
"nearest"],
1457 "description":
"Which .gitignore to target (default: nearest)",
1459 "json_output": {
"type":
"boolean",
"description":
"Machine-readable JSON output"},
1460 "timeout": {
"type":
"integer",
"description":
"Subprocess timeout in seconds (default 30)"},
1469 "name":
"oct_git_commit_msg",
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."
1481 "description":
"Path to the commit-message file (e.g. .git/COMMIT_EDITMSG)",
1483 "json_output": {
"type":
"boolean",
"description":
"Machine-readable JSON output"},
1484 "timeout": {
"type":
"integer",
"description":
"Subprocess timeout in seconds (default 30)"},
1486 "required": [
"message_file"],
1493 "name":
"oct_git_changelog",
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 "
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)"},
1517 "name":
"oct_git_hooks",
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."
1531 "enum": [
"install",
"status",
"remove"],
1532 "description":
"Hook subcommand to run",
1536 "description":
"For action='install': overwrite existing configuration",
1538 "timeout": {
"type":
"integer",
"description":
"Subprocess timeout in seconds (default 30)"},
1540 "required": [
"action"],