Metadata-Version: 2.4
Name: task-handoff-check
Version: 0.1.1
Summary: Check structured task handoffs for missing and contradictory claims.
Author: IT Custom Solution LLC
License-Expression: MIT
Project-URL: Homepage, https://github.com/itcustomsolution/task-handoff-check
Project-URL: Changelog, https://github.com/itcustomsolution/task-handoff-check/blob/main/CHANGELOG.md
Project-URL: Repository, https://github.com/itcustomsolution/task-handoff-check
Project-URL: Issues, https://github.com/itcustomsolution/task-handoff-check/issues
Keywords: handoff,ai-agents,workflow,validation,cli,json
Classifier: Development Status :: 3 - Alpha
Classifier: Programming Language :: Python :: 3
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Software Development :: Quality Assurance
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# task-handoff-check

> Lint a task handoff (human or AI agent) for a missing owner, next step, blockers, done criteria or evidence before someone else picks it up.

<!-- [![tests](https://github.com/itcustomsolution/task-handoff-check/actions/workflows/tests.yml/badge.svg)](https://github.com/itcustomsolution/task-handoff-check/actions/workflows/tests.yml) -->
[![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

Catch incomplete or contradictory task handoffs before someone else picks up
the work. This small CLI and library checks an explicit JSON document for a
clear owner, next action, blockers, completion criteria and evidence references.

**A passing handoff is structurally consistent. It is not proof that its claims
are true, that its references exist, or that anyone is authorized to act.**

An open-source engineering utility from [IT Custom Solution](https://github.com/itcustomsolution).
Python 3.10+, no runtime dependencies, no network access, no commands executed.
MIT licensed.

## Try it in one minute

```sh
git clone https://github.com/itcustomsolution/task-handoff-check.git
cd task-handoff-check
python -m task_handoff_check examples/in-progress.json
python -m task_handoff_check examples/complete.json --json
python -m task_handoff_check examples/invalid-complete.json --json
```

The first two checks pass. The third exits 1 because it claims `complete` while
listing an open blocker. All examples, people and references are fictional;
referenced paths deliberately need not exist. The tool never opens them.

Install the latest published release with `pipx install task-handoff-check`
(or `pip install task-handoff-check`). This README describes the source tree,
which can contain unreleased changes; see [CHANGELOG.md](CHANGELOG.md) and use
the source installation below to get them.

To install from source in a virtual environment:

```sh
python -m venv .venv
. .venv/bin/activate
python -m pip install .
task-handoff-check --version
```

Installation uses setuptools as a build dependency. Running from the checkout
needs only the Python standard library; no package registry release is required.

## Handoff format

```json
{
  "schema_version": 1,
  "objective": "Review the sample release notes",
  "owner": "Sample Maintainer",
  "status": "in_progress",
  "next_action": "Compare the notes with the sample changelog",
  "inputs": [
    {
      "label": "changelog",
      "path": "docs/changelog.md"
    }
  ],
  "evidence": [],
  "blockers": [],
  "completion_criteria": [
    {
      "description": "All sample changes reviewed",
      "met": false,
      "evidence": []
    }
  ],
  "updated_at": "2026-01-01T09:00:00Z"
}
```

All shown fields except `updated_at` are required. Unknown fields are errors.

| Field | Rule |
| --- | --- |
| `schema_version` | Integer `1` |
| `objective`, `owner` | Meaningful nonempty text |
| `status` | `ready`, `in_progress`, `blocked`, or `complete` |
| `next_action` | Meaningful text for unfinished work; JSON `null` for complete |
| `inputs`, `evidence` | Arrays of reference objects; may be empty |
| `blockers` | Array of objects with `description` and `owner`; open blockers only |
| `completion_criteria` | Nonempty array with `description`, boolean `met`, and `evidence` |
| `updated_at` | Optional timestamp with seconds and timezone |

A reference contains a meaningful `label` and exactly one `path` or `url`.
Paths must be portable relative paths without traversal, control characters,
reserved device names or Windows-reserved punctuation. URLs must be absolute
HTTP(S) references without embedded credentials. Labels must be distinct within
each array after Unicode NFC normalization and case folding.

Each criterion's `evidence` is an array of exact labels from the top-level
`evidence` array. A `met: true` claim requires at least one such reference.
Repeated evidence labels and repeated criterion descriptions are rejected.
Reference content and accessibility are never checked.

`blocked` requires at least one open blocker and a concrete next action.
Any open blocker requires `blocked`. `complete` requires all criteria to be
`met: true`, no blockers and a null `next_action`. Other statuses may contain
met criteria while further work remains. This is deliberately a small task
state model; it is not a general workflow engine.

Blank text, control characters and exact placeholders such as `TBD`, `todo`,
`unknown`, `unassigned`, `n/a`, `none`, `-` and `?` are rejected. The tool does
not understand free text semantically: an unhelpful sentence can still pass.

## Optional, reproducible freshness checks

Freshness is never inferred from the machine's current time. Supply `--as-of`
to compare timestamps; optionally specify an inclusive maximum age:

```sh
python -m task_handoff_check examples/in-progress.json \
  --as-of 2026-01-02T09:00:00Z --max-age-hours 24 --json
```

This example passes exactly at the 24-hour boundary. One second later it is
`stale`. `--max-age-hours` must be a finite, nonnegative number and accompanied
by `--as-of`; choosing a meaningful freshness policy is up to you. Supplying `--as-of` requires a valid `updated_at` and rejects a future
update. `--as-of` alone compares chronology, without a maximum-age policy.

Accepted timestamps use `YYYY-MM-DDTHH:MM:SS`, optional 1–6 fractional digits,
and `Z` or an explicit `+HH:MM`/`-HH:MM` offset. Leap seconds are unsupported.
The timestamp remains an assertion from the document author.

## Reports and exit codes

JSON reports contain `ok`, `scope` (`handoff_consistency`), `freshness`, and an
`issues` array of `code`, `location` and `message`. `freshness` is one of
`not_evaluated`, `unavailable`, `timestamp_compared`, `future`, `within_limit`
or `stale`. Other schema errors can coexist with a valid timestamp comparison.

| Exit | Meaning |
| --- | --- |
| 0 | Consistency checks pass; or help/version displayed |
| 1 | Invalid input, inconsistent handoff or failed timestamp check |
| 2 | Invalid command-line usage or freshness options |

Common issue codes: `invalid_schema`, `unknown_field`, `missing_text`,
`status_conflict`, `invalid_reference`, `duplicate_label`, `duplicate_criterion`,
`incomplete_criterion`, `missing_evidence`, `unknown_evidence`, `invalid_timestamp`,
`missing_timestamp`, `future_timestamp`, `stale`, `input_error`.
Messages are human-facing. `--json` validation reports use stdout; CLI usage
errors use stderr.

## Library

```python
from task_handoff_check import load_handoff, validate_handoff

handoff = load_handoff("examples/complete.json")
report = validate_handoff(handoff)
assert report["ok"]
report = validate_handoff(
    handoff, as_of="2026-01-02T09:00:00Z", max_age_hours=24
)
```

`validate_handoff` returns issues for invalid documents and raises `HandoffError`
for invalid freshness options. The library accepts `max_age_hours` as a finite,
nonnegative integer or float; integers of any size are compared exactly, a float is compared exactly as the
binary value it holds (so `0.1` is very slightly more than one tenth), and
booleans and nonfinite values are rejected. `load_handoff` raises `HandoffError` for input errors, including
numbers such as `1e999` that overflow Python's finite floating-point range.
Validation does not mutate the document. Parser errors identify a location
without repeating JSON content or the input filename.

## Limits and development

Input must be a regular UTF-8 JSON file of at most 1 MiB; final-component
symlinks, duplicate JSON keys and nonfinite numbers are rejected. Parent
symlinks may be followed for the explicitly supplied input document. Use
trusted local input locations; the input reader is not a filesystem sandbox.

The tool performs no authentication, authorization, link checking, ownership
verification, work execution, scheduling, agent dispatch or compliance assessment.
It never changes a task's state. See [SECURITY.md](SECURITY.md).

```sh
python -m unittest discover -s tests -v
```

Tests use synthetic inputs and temporary directories. The pull-request workflow
is configured for Python 3.10, 3.12 and 3.14 on Linux, plus Python 3.14 on macOS.
Configured jobs are not executed by this change; workflow run results are the
evidence of executed coverage. See [CONTRIBUTING.md](CONTRIBUTING.md).

## Contributing

Issues and small pull requests are welcome; see [CONTRIBUTING.md](CONTRIBUTING.md). Run `python -m unittest discover -s tests` before opening a PR. Release history is in [CHANGELOG.md](CHANGELOG.md).

## License

MIT, see [LICENSE](LICENSE).
