feat: scaffold us-cpa skill
This commit is contained in:
56
docs/us-cpa.md
Normal file
56
docs/us-cpa.md
Normal file
@@ -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
|
||||
<case-dir>/
|
||||
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
|
||||
52
skills/us-cpa/SKILL.md
Normal file
52
skills/us-cpa/SKILL.md
Normal file
@@ -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`.
|
||||
19
skills/us-cpa/pyproject.toml
Normal file
19
skills/us-cpa/pyproject.toml
Normal file
@@ -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"]
|
||||
8
skills/us-cpa/scripts/us-cpa
Executable file
8
skills/us-cpa/scripts/us-cpa
Executable file
@@ -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 "$@"
|
||||
2
skills/us-cpa/src/us_cpa/__init__.py
Normal file
2
skills/us-cpa/src/us_cpa/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
"""us-cpa package."""
|
||||
|
||||
130
skills/us-cpa/src/us_cpa/cli.py
Normal file
130
skills/us-cpa/src/us_cpa/cli.py
Normal file
@@ -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())
|
||||
1
skills/us-cpa/tests/fixtures/documents/.gitkeep
vendored
Normal file
1
skills/us-cpa/tests/fixtures/documents/.gitkeep
vendored
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
skills/us-cpa/tests/fixtures/facts/.gitkeep
vendored
Normal file
1
skills/us-cpa/tests/fixtures/facts/.gitkeep
vendored
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
skills/us-cpa/tests/fixtures/irs/.gitkeep
vendored
Normal file
1
skills/us-cpa/tests/fixtures/irs/.gitkeep
vendored
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
skills/us-cpa/tests/fixtures/returns/.gitkeep
vendored
Normal file
1
skills/us-cpa/tests/fixtures/returns/.gitkeep
vendored
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
71
skills/us-cpa/tests/test_cli.py
Normal file
71
skills/us-cpa/tests/test_cli.py
Normal file
@@ -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()
|
||||
Reference in New Issue
Block a user