diff --git a/docs/us-cpa.md b/docs/us-cpa.md new file mode 100644 index 0000000..c8b4111 --- /dev/null +++ b/docs/us-cpa.md @@ -0,0 +1,56 @@ +# us-cpa + +`us-cpa` is a Python CLI plus OpenClaw skill wrapper for U.S. federal individual tax work. + +## Current Milestone + +Milestone 1 provides the initial package, CLI surface, skill wrapper, and test harness. Tax logic, IRS corpus download, case workflows, rendering, and review logic are not implemented yet. + +## CLI Surface + +```bash +skills/us-cpa/scripts/us-cpa question --question "What is the standard deduction?" --tax-year 2025 +skills/us-cpa/scripts/us-cpa prepare --tax-year 2025 --case-dir ~/tax-cases/2025-jane-doe +skills/us-cpa/scripts/us-cpa review --tax-year 2025 --case-dir ~/tax-cases/2025-jane-doe +skills/us-cpa/scripts/us-cpa fetch-year --tax-year 2025 +skills/us-cpa/scripts/us-cpa extract-docs --tax-year 2025 --case-dir ~/tax-cases/2025-jane-doe +skills/us-cpa/scripts/us-cpa render-forms --tax-year 2025 --case-dir ~/tax-cases/2025-jane-doe +skills/us-cpa/scripts/us-cpa export-efile-ready --tax-year 2025 --case-dir ~/tax-cases/2025-jane-doe +``` + +## Interaction Model + +- `question` + - stateless by default + - optional case context +- `prepare` + - requires a case directory + - if none exists, OpenClaw should ask whether to create one and where +- `review` + - requires a case directory + - can operate on an existing or newly-created review case + +## Planned Case Layout + +```text +/ + input/ + extracted/ + return/ + output/ + reports/ + issues/ + sources/ +``` + +## Output Contract + +- JSON by default +- markdown available with `--format markdown` +- current milestone responses are scaffold payloads with `status: "not_implemented"` + +## Scope Rules + +- U.S. federal individual returns only in v1 +- official IRS artifacts are the target output for compiled forms +- conflicting facts must stop the workflow for user resolution diff --git a/skills/us-cpa/SKILL.md b/skills/us-cpa/SKILL.md new file mode 100644 index 0000000..d3fccc3 --- /dev/null +++ b/skills/us-cpa/SKILL.md @@ -0,0 +1,52 @@ +--- +name: us-cpa +description: Use when answering U.S. federal individual tax questions, preparing a federal Form 1040 return package, or reviewing a draft/completed federal individual return. +--- + +# US CPA + +`us-cpa` is a Python-first federal individual tax workflow skill. The CLI is the canonical engine. Use the skill to classify the request, gather missing inputs, and invoke the CLI. + +## Modes + +- `question` + - one-off federal tax question + - case folder optional +- `prepare` + - new or existing return-preparation case + - case folder required +- `review` + - new or existing return-review case + - case folder required + +## Agent Workflow + +1. Determine whether the request is: + - question-only + - a new preparation/review case + - work on an existing case +2. If the request is `prepare` or `review` and no case folder is supplied: + - ask whether to create a new case + - ask where to store it +3. Use the bundled CLI: + +```bash +skills/us-cpa/scripts/us-cpa question --question "What is the standard deduction?" --tax-year 2025 +skills/us-cpa/scripts/us-cpa prepare --tax-year 2025 --case-dir ~/tax-cases/2025-jane-doe +skills/us-cpa/scripts/us-cpa review --tax-year 2025 --case-dir ~/tax-cases/2025-jane-doe +``` + +## Rules + +- federal individual returns only in v1 +- IRS materials first; escalate to primary law only when needed +- stop on conflicting facts and ask the user to resolve the issue before continuing +- official IRS PDFs are the target compiled-form artifacts +- overlay-rendered forms must be flagged for human review + +## Output + +- JSON by default +- markdown output available with `--format markdown` + +For operator details, limitations, and the planned case structure, see `docs/us-cpa.md`. diff --git a/skills/us-cpa/pyproject.toml b/skills/us-cpa/pyproject.toml new file mode 100644 index 0000000..eb9a667 --- /dev/null +++ b/skills/us-cpa/pyproject.toml @@ -0,0 +1,19 @@ +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[project] +name = "us-cpa" +version = "0.1.0" +description = "US federal individual tax workflow CLI for questions, preparation, and review." +requires-python = ">=3.9" +dependencies = [] + +[project.scripts] +us-cpa = "us_cpa.cli:main" + +[tool.setuptools] +package-dir = {"" = "src"} + +[tool.setuptools.packages.find] +where = ["src"] diff --git a/skills/us-cpa/scripts/us-cpa b/skills/us-cpa/scripts/us-cpa new file mode 100755 index 0000000..fbcef77 --- /dev/null +++ b/skills/us-cpa/scripts/us-cpa @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SKILL_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +export PYTHONPATH="${SKILL_DIR}/src${PYTHONPATH:+:${PYTHONPATH}}" + +exec python3 -m us_cpa.cli "$@" diff --git a/skills/us-cpa/src/us_cpa/__init__.py b/skills/us-cpa/src/us_cpa/__init__.py new file mode 100644 index 0000000..8014833 --- /dev/null +++ b/skills/us-cpa/src/us_cpa/__init__.py @@ -0,0 +1,2 @@ +"""us-cpa package.""" + diff --git a/skills/us-cpa/src/us_cpa/cli.py b/skills/us-cpa/src/us_cpa/cli.py new file mode 100644 index 0000000..4ef2046 --- /dev/null +++ b/skills/us-cpa/src/us_cpa/cli.py @@ -0,0 +1,130 @@ +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path +from typing import Any + + +COMMANDS = ( + "question", + "prepare", + "review", + "fetch-year", + "extract-docs", + "render-forms", + "export-efile-ready", +) + + +def _add_common_arguments( + parser: argparse.ArgumentParser, *, include_tax_year: bool = True +) -> None: + if include_tax_year: + parser.add_argument("--tax-year", type=int, default=None) + parser.add_argument("--case-dir", default=None) + parser.add_argument("--format", choices=("json", "markdown"), default="json") + + +def _emit(payload: dict[str, Any], output_format: str) -> int: + if output_format == "markdown": + lines = [f"# {payload['command']}"] + for key, value in payload.items(): + if key == "command": + continue + lines.append(f"- **{key}**: {value}") + print("\n".join(lines)) + else: + print(json.dumps(payload, indent=2)) + return 0 + + +def _require_case_dir(args: argparse.Namespace) -> Path: + if not args.case_dir: + raise SystemExit("A case directory is required for this command.") + return Path(args.case_dir).expanduser().resolve() + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="us-cpa", + description="US federal individual tax workflow CLI.", + ) + subparsers = parser.add_subparsers(dest="command", required=True) + + question = subparsers.add_parser("question", help="Answer a tax question.") + _add_common_arguments(question) + question.add_argument("--question", required=True) + + prepare = subparsers.add_parser("prepare", help="Prepare a return case.") + _add_common_arguments(prepare) + + review = subparsers.add_parser("review", help="Review a return case.") + _add_common_arguments(review) + + fetch_year = subparsers.add_parser( + "fetch-year", help="Fetch tax-year forms and instructions." + ) + _add_common_arguments(fetch_year, include_tax_year=False) + fetch_year.add_argument("--tax-year", type=int, required=True) + + extract_docs = subparsers.add_parser( + "extract-docs", help="Extract facts from case documents." + ) + _add_common_arguments(extract_docs) + + render_forms = subparsers.add_parser( + "render-forms", help="Render compiled IRS forms." + ) + _add_common_arguments(render_forms) + + export_efile = subparsers.add_parser( + "export-efile-ready", help="Export an e-file-ready payload." + ) + _add_common_arguments(export_efile) + + return parser + + +def main(argv: list[str] | None = None) -> int: + parser = build_parser() + args = parser.parse_args(argv) + + if args.command == "question": + payload = { + "command": "question", + "format": args.format, + "taxYear": args.tax_year, + "caseDir": args.case_dir, + "question": args.question, + "status": "not_implemented", + } + return _emit(payload, args.format) + + if args.command in {"prepare", "review", "extract-docs", "render-forms", "export-efile-ready"}: + case_dir = _require_case_dir(args) + payload = { + "command": args.command, + "format": args.format, + "taxYear": args.tax_year, + "caseDir": str(case_dir), + "status": "not_implemented", + } + return _emit(payload, args.format) + + if args.command == "fetch-year": + payload = { + "command": "fetch-year", + "format": args.format, + "taxYear": args.tax_year, + "status": "not_implemented", + } + return _emit(payload, args.format) + + parser.error(f"Unsupported command: {args.command}") + return 2 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/skills/us-cpa/tests/fixtures/documents/.gitkeep b/skills/us-cpa/tests/fixtures/documents/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/skills/us-cpa/tests/fixtures/documents/.gitkeep @@ -0,0 +1 @@ + diff --git a/skills/us-cpa/tests/fixtures/facts/.gitkeep b/skills/us-cpa/tests/fixtures/facts/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/skills/us-cpa/tests/fixtures/facts/.gitkeep @@ -0,0 +1 @@ + diff --git a/skills/us-cpa/tests/fixtures/irs/.gitkeep b/skills/us-cpa/tests/fixtures/irs/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/skills/us-cpa/tests/fixtures/irs/.gitkeep @@ -0,0 +1 @@ + diff --git a/skills/us-cpa/tests/fixtures/returns/.gitkeep b/skills/us-cpa/tests/fixtures/returns/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/skills/us-cpa/tests/fixtures/returns/.gitkeep @@ -0,0 +1 @@ + diff --git a/skills/us-cpa/tests/test_cli.py b/skills/us-cpa/tests/test_cli.py new file mode 100644 index 0000000..db4d765 --- /dev/null +++ b/skills/us-cpa/tests/test_cli.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +import json +import os +import subprocess +import sys +import unittest +from pathlib import Path + + +SKILL_DIR = Path(__file__).resolve().parents[1] +SRC_DIR = SKILL_DIR / "src" + + +class UsCpaCliSmokeTests(unittest.TestCase): + def test_skill_scaffold_files_exist(self) -> None: + self.assertTrue((SKILL_DIR / "SKILL.md").exists()) + self.assertTrue((SKILL_DIR / "pyproject.toml").exists()) + self.assertTrue((SKILL_DIR / "scripts" / "us-cpa").exists()) + self.assertTrue( + (SKILL_DIR.parent.parent / "docs" / "us-cpa.md").exists() + ) + + def test_fixture_directories_exist(self) -> None: + fixtures_dir = SKILL_DIR / "tests" / "fixtures" + for name in ("irs", "facts", "documents", "returns"): + self.assertTrue((fixtures_dir / name).exists()) + + def run_cli(self, *args: str) -> subprocess.CompletedProcess[str]: + env = os.environ.copy() + env["PYTHONPATH"] = str(SRC_DIR) + return subprocess.run( + [sys.executable, "-m", "us_cpa.cli", *args], + text=True, + capture_output=True, + env=env, + ) + + def test_help_lists_all_commands(self) -> None: + result = self.run_cli("--help") + + self.assertEqual(result.returncode, 0, result.stderr) + for command in ( + "question", + "prepare", + "review", + "fetch-year", + "extract-docs", + "render-forms", + "export-efile-ready", + ): + self.assertIn(command, result.stdout) + + def test_question_command_emits_json_by_default(self) -> None: + result = self.run_cli("question", "--question", "What is the standard deduction?") + + self.assertEqual(result.returncode, 0, result.stderr) + payload = json.loads(result.stdout) + self.assertEqual(payload["command"], "question") + self.assertEqual(payload["format"], "json") + self.assertEqual(payload["question"], "What is the standard deduction?") + + def test_prepare_requires_case_dir(self) -> None: + result = self.run_cli("prepare", "--tax-year", "2025") + + self.assertNotEqual(result.returncode, 0) + self.assertIn("case directory", result.stderr.lower()) + + +if __name__ == "__main__": + unittest.main()