Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -1,22 +1,36 @@
name: Tests
# Single light job, pull requests only (keeps Actions minutes low).
# Supported minimum and representative versions/platforms; pull requests only.
# Build inputs are hash-pinned (requirements/build.txt).
on:
pull_request:
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
python: '3.10'
- os: ubuntu-latest
python: '3.12'
- os: ubuntu-latest
python: '3.14'
- os: macos-latest
python: '3.14'
runs-on: ${{ matrix.os }}
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: '3.12'
python-version: ${{ matrix.python }}
- run: python -m unittest discover -s tests -v
- run: python -m pip install --no-deps .
- run: python -m pip install --require-hashes -r requirements/build.txt
- run: python -m pip install --no-deps --no-build-isolation .
- name: Check installed entry point outside the source tree
working-directory: ${{ runner.temp }}
run: task-handoff-check --version
14 changes: 11 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
# Changelog

## Unreleased
## 0.1.1 (2026-10-22)

- A handoff path that is not a string or path-like object (`None`, bytes, a number) raises `HandoffError` instead of `TypeError` (r2 audit observation).
- Reject exponent-overflow JSON numbers, including positive and negative `1e999`, with `HandoffError`.
- Freshness options no longer raise an undocumented `OverflowError`: integers of any size are compared exactly (never converted to float), and every invalid option raises `HandoffError`.
- Report JSON parse locations without repeating input contents or filenames.
- Restore the documented pull-request Python/platform matrix; distinguish configured jobs from executed verification.
- Correct the installation note; it no longer describes the first release as future.
- PyPI metadata (keywords, classifiers, homepage and changelog URLs), README pitch, badge placeholders and install-from-PyPI note.
- CI trimmed to one light Ubuntu job on pull requests to save Actions minutes.
- No behavior changes.
- A handoff path containing a NUL character raises `HandoffError('handoff path is invalid ...')` instead of a misleading JSON message.
- Freshness compares the elapsed time in exact integer microseconds; a long interval just over the limit is now `stale` (independent re-audit RH-01).
- The README no longer states a version number (it said 0.1.0 while the package is 0.1.1rc1; RH-02).
- Local verification of the 0.1.1rc1 candidate (2026-10-16): CPython 3.10, 3.11, 3.12 and 3.14 on macOS (arm64), from source and from the built wheel. Python 3.13, Linux and Windows were not run; no hosted CI run exists for it.

## 0.1.0

Expand Down
29 changes: 18 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

> 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.

<!-- Badges: enable after first PyPI publish -->
<!-- [![PyPI](https://img.shields.io/pypi/v/task-handoff-check)](https://pypi.org/project/task-handoff-check/) -->
<!-- [![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)

Expand All @@ -16,7 +14,7 @@ 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.
Version 0.1.0. MIT licensed.
MIT licensed.

## Try it in one minute

Expand All @@ -32,7 +30,10 @@ 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.

After the first PyPI release: `pipx install task-handoff-check` (or `pip install task-handoff-check`).
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:

Expand Down Expand Up @@ -119,8 +120,8 @@ python -m task_handoff_check examples/in-progress.json \
```

This example passes exactly at the 24-hour boundary. One second later it is
`stale`. `--max-age-hours` must be finite, nonnegative and accompanied by
`--as-of`. Supplying `--as-of` requires a valid `updated_at` and rejects a future
`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,
Expand Down Expand Up @@ -161,8 +162,13 @@ report = validate_handoff(
```

`validate_handoff` returns issues for invalid documents and raises `HandoffError`
for invalid freshness options. `load_handoff` raises `HandoffError` for input
errors. Validation does not mutate the document.
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

Expand All @@ -179,9 +185,10 @@ It never changes a task's state. See [SECURITY.md](SECURITY.md).
python -m unittest discover -s tests -v
```

Tests use synthetic inputs and temporary directories. CI covers Python 3.10,
3.12 and 3.14 on Linux, plus Python 3.14 on macOS.
See [CONTRIBUTING.md](CONTRIBUTING.md).
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

Expand Down
8 changes: 4 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
[build-system]
requires = ["setuptools>=68"]
requires = ["setuptools>=83.0.0"] # CVE-2026-59890 (sdist MANIFEST exclusion bypass) fixed in 83.0.0
build-backend = "setuptools.build_meta"

[project]
name = "task-handoff-check"
version = "0.1.0"
version = "0.1.1"
description = "Check structured task handoffs for missing and contradictory claims."
readme = "README.md"
requires-python = ">=3.10"
license = {file = "LICENSE"}
license = "MIT"
license-files = ["LICENSE"]
authors = [{name = "IT Custom Solution LLC"}]
keywords = ["handoff", "ai-agents", "workflow", "validation", "cli", "json"]
dependencies = []
Expand All @@ -17,7 +18,6 @@ classifiers = [
"Programming Language :: Python :: 3",
"Environment :: Console",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3 :: Only",
"Topic :: Software Development :: Quality Assurance",
Expand Down
2 changes: 2 additions & 0 deletions requirements/build.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
build==1.6.1
setuptools==84.0.0
95 changes: 95 additions & 0 deletions requirements/build.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
build==1.6.1 \
--hash=sha256:51cc11666391ab6f092070437ac747002ff46f3e4113a3622177ee6b488bfc53 \
--hash=sha256:ecd351a4be9d35a9eaaba244a7687143c9c7d4aea6ac964e7e7ddab20cbcf4e7
# via -r task-handoff-check/requirements/build.in
colorama==0.4.6 ; os_name == 'nt' \
--hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
# via build
importlib-metadata==9.0.1 ; python_full_version < '3.10.2' \
--hash=sha256:ab830580bc0ef3db61ce8fae716389e5462b67e033018bab6d8f80ef17172f99 \
--hash=sha256:bba5600596a7e21f3eef53281cf28d6a5195634d2f2b78ff9501a3272c6eaab0
# via build
packaging==26.3 \
--hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \
--hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c
# via build
pyproject-hooks==1.3.3 \
--hash=sha256:5fc53fdac9f7bd63fbcdc868fb5f90b4784d78a53a3d3388cd738b807441a20b \
--hash=sha256:defda19b854fa0d3bd4f76ea4ddcba8abd7dcfcdd585a6690ade050744fc5f43
# via build
setuptools==84.0.0 \
--hash=sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670 \
--hash=sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73
# via -r task-handoff-check/requirements/build.in
tomli==2.5.0 ; python_full_version < '3.11' \
--hash=sha256:069435bd5480429b98c5e5afb02ab21c219b6f0064680671c6dc0d46817346ea \
--hash=sha256:0dc598040da8d42cf20f0be588ed7004f46db12a0ac6c32e03a59dccedaaadcd \
--hash=sha256:1245a6638fc4bb0a60af38a7d45413db34a13842027c77597c712c998c62fdf0 \
--hash=sha256:19b0dd8749f4ea2f112c5fcfb3c5248390c899d7e2e173f1d91abee1fa0ff391 \
--hash=sha256:1f4a40d03fb9f63424f0979855bdeaf44dd7696b8d59501822c10ed30ba532df \
--hash=sha256:20aa36de8f2cf87237143bc1fa1aae8d6612c09118f4da21c6a684db5dd1f6f9 \
--hash=sha256:21e4cae4114aba25aa0d4f85cdf486d290fb35c0954d7bba536248da64d43066 \
--hash=sha256:22185fad8a1e622f064e78008018a0dd3323550dcb479cb7a1d296888d74024f \
--hash=sha256:2419c2a189551987b59d80e63ec355671283336f41c6b9b89462df679c7d0c57 \
--hash=sha256:264507556cd8b8c8e7c6ee037cdf443a463f03f4c958e57195e3d369711b8ff6 \
--hash=sha256:32a7b79ac57a2e83670ce329ccf675798bc5a2094783a63676866b70503f2e2b \
--hash=sha256:3f89d10c1ff6a38d992c27fc8a4816af71a909e08a40ec66934240b1e74347c3 \
--hash=sha256:463b16086865b97facd8d0b3fb4cb7c544e3f58d2a69dc3113d6db9653fdb043 \
--hash=sha256:49096930c8d886c9bbdab62d2d0d17ce823ddeea522309a190b36245d5b49e01 \
--hash=sha256:521345fd1f19d45b8df87657aaa38b6f2ca3800059fadf428e7ebf479a383646 \
--hash=sha256:57b1c3b01fab802e2899bc3d168dca320e14165e2fd9fd584760fb4ca5826859 \
--hash=sha256:5d8bac3d603c97e6854424e5b2b5b741bdbde387e09f162fb0446812b4a8362b \
--hash=sha256:610b27d99f28ec5f191c7064a48f3ddb179a1fe6ca73d571483ae859f57b605e \
--hash=sha256:61ea1ebe1e55a34ea8199cc8dbff398d35027b82271c8ac4802fd3a1fd5b1bcc \
--hash=sha256:62fc1bc8eb03e3a9cadfca713d65614ed8e09d974a283295ffe3a831976b4dc5 \
--hash=sha256:6664b7ae7af7294256c53960a6103077f4914cec8ff98479c352f622c6f6b2f0 \
--hash=sha256:667e521b37a6c5ccaa044202c235b530f90177ffe2cd4a64ecc213c7dd535feb \
--hash=sha256:69491c143d2fe063046e0301e62a810bed338fa4d1ce0fd870c27dc1e09b0d84 \
--hash=sha256:6cf74416bdc94ae458b14e37286c1073081850ac8459a00d0c5efef5d44294c6 \
--hash=sha256:6e95c7614e705bfe2b04b27aa124adec59752d15813df37e2156747cab3a006b \
--hash=sha256:6f041843c4d3a37245c0c056fd955b186bf8b1fb85690cbe40b81230891dc34b \
--hash=sha256:752e8b1aa6a4367ef8bf6a1a1e005540f7ed055ba36d7193796812ca5404eb52 \
--hash=sha256:75dbcde8751b0a960aa3de173aa5e894d590755c6d7758b7e774c06f1dc3cbdd \
--hash=sha256:7ac2027d37c3afbdf4bdd377f2676f6f1d2122a5be1f1137b49dced590b37e75 \
--hash=sha256:7ad1ea345759240d6463efa0ed1c704402752e49aa21476620738d74d72d8aa1 \
--hash=sha256:86665cee9c4835b7a7f1e8ec2c719b5258d4dc782887aded5a8ae7352a96843b \
--hash=sha256:8ff3a2ca028c7eee0c777f9a092038d0a594a9fa04e215f929a22c329e2cb142 \
--hash=sha256:91294a9fb94a75542f6e46e4a2ae709bd8d9b51134098cae5cf3bea5478b6d03 \
--hash=sha256:943276cf269e0071948d9ff697159c1735e623c1151d88abb09b74659ef0cbea \
--hash=sha256:96243987194634bd411066ce40c952e108f86af04db533ecd8ac3ff2a85b1885 \
--hash=sha256:984012f71908165449a951de2050d52f276bfe3aa5d5f570f63ddad814370374 \
--hash=sha256:9b03d7dc168353b4132965bde20feceabaa470e570c6f59660dfae59b1f9eeb3 \
--hash=sha256:9dbb18c1cfb2f6517942fc9314437f66aa06d94436ffb1f06102ef3572f35276 \
--hash=sha256:9ebf8d19b17bd0daeb7b7dec81a946a439b753942fd0210d6e96c532249eea6b \
--hash=sha256:a525685c2f97da40762b8695eb7aa0af4c8344ca1905c73e4e29cb04d34607dc \
--hash=sha256:abdbf6313b8d9efe157edeb7ab6eae4de064b1300ad31abf73755154b30abe68 \
--hash=sha256:b69564772b5c8f22ea5f498dff08cfa825045b4d4c4400529000bdf818aa3b2a \
--hash=sha256:b8ade5023067f99fe72b88accd30d0ea05a158e9e32a11f124e731ea9695313f \
--hash=sha256:bbaefc84548d754be821bba7c4141c4787dda182f9e77f2f87b71213529efa7b \
--hash=sha256:bd05de8c1698f8413dd7d869492693a0bf2211543b787ac78cd5e7536af1a6d7 \
--hash=sha256:bf0b5e8e0f68ebb494356e577c06c139161efd8d3b9050f93b39b7c26cc54ff0 \
--hash=sha256:c414be4ed9d3cac80c42e348fa5a956117d1a48227f48026e31f59cb4a7671eb \
--hash=sha256:c47300f9bf791808f77d82747691c4bb09cb14bdf3060cca99b42cdc4361d5a7 \
--hash=sha256:c4dc1c1781f2f716de763d1e9a7b34c6a894e167e291c7c5d16c72f7a9538545 \
--hash=sha256:c804ae44fe7b4bab5da295e4f980a1ff04670bca9d23fe0a4e887e08ebd741a8 \
--hash=sha256:cfac177ebd6236003846ea339981f71457cb6eb748f23381eb257e45092e3980 \
--hash=sha256:d2ba24db8a9376921b5e87b4762b9adb0f3f1deaea68f2b8b0bb2c11efb9c3e7 \
--hash=sha256:d3182ee2d887e507bd67319a0a61105d1dd33facc111329559a233b772c1a105 \
--hash=sha256:d747252933c8a65ef6bd8da0fbb7ce28a90eb6119d8cd00772cd528aa07b68d5 \
--hash=sha256:d7e369fd63331746182360977b1892bfc215476a30d61612d732425311639f56 \
--hash=sha256:e12bbcd32897272fb05929110362ae9ff4c1b9bb26bd9e971e71dcd3275b4c3d \
--hash=sha256:e7ad033e27a516a233bea839cdb77b80146facb3b4f40bf02cd0cac165cdd5c2 \
--hash=sha256:e9e15b4a6c7dd6b85b5fbab29488a73f1f70de516942308daa266bf0e0aeb0d4 \
--hash=sha256:ed53f7e89bb04f6d9e8e7799112360b0c4d5cbff067de0814c98c37c39b920f7 \
--hash=sha256:eff8babca5a7999bc137acbc7482a8b7e17ffca5075ab41f5d770ab408c7bfef \
--hash=sha256:f15e3e0b835a6d68b10c86bf80a3149780498d6911c93c3ffd1861d19f9200f1 \
--hash=sha256:f3fcbc57b1791fa6cbe5d8434179d51de12be1a4811469529f47f6e7487a2571 \
--hash=sha256:f4b653094e18f9031102d3a1da5c729c8f222d85225b18037dac621695e46e1a \
--hash=sha256:f79203b3965b4000e91808aaa7c040206093f2b8bf86f455982f2274c9ccf442 \
--hash=sha256:fd4dc129784e0c5335bd4e61dfcc4487499a013419e655cf2da1d091b7e0efdc
# via build
zipp==4.1.1 ; python_full_version < '3.10.2' \
--hash=sha256:7ebb7a44c021b29fd8dbd7cce6812d0d7b5b454521f93cc71af6ccd155aaa70b \
--hash=sha256:8979f52d874162f485ff2981e3891f3a3317b7a3dd43ff1e1775b9304f307a9c
# via importlib-metadata
2 changes: 1 addition & 1 deletion task_handoff_check/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Structural checks for explicit, human-readable task handoffs."""
from .core import HandoffError, load_handoff, validate_handoff

__version__ = '0.1.0'
__version__ = '0.1.1'
__all__ = ['HandoffError', 'load_handoff', 'validate_handoff']
3 changes: 2 additions & 1 deletion task_handoff_check/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ def main(argv=None):
parser.add_argument('handoff', type=Path)
parser.add_argument('--json', action='store_true', help='print a machine-readable report')
parser.add_argument('--as-of', help='explicit timestamp for comparison; no implicit current time')
parser.add_argument('--max-age-hours', type=float, help='maximum age; requires --as-of')
parser.add_argument('--max-age-hours', type=float,
help='maximum age in hours (finite, nonnegative); requires --as-of')
args = parser.parse_args(argv)
options = {'as_of': args.as_of, 'max_age_hours': args.max_age_hours}
try:
Expand Down
Loading
Loading