taskill 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
taskill/state.py ADDED
@@ -0,0 +1,42 @@
1
+ """Persistent state — what was the project's snapshot last time we ran?"""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from dataclasses import asdict, dataclass, field
6
+ from datetime import datetime, timezone
7
+ from pathlib import Path
8
+
9
+
10
+ @dataclass
11
+ class TaskillState:
12
+ last_run_iso: str | None = None
13
+ last_commit_sha: str | None = None
14
+ last_coverage_pct: float | None = None
15
+ last_failed_tests: int | None = None
16
+ last_sumd_hash: str | None = None
17
+ file_mtimes: dict[str, float] = field(default_factory=dict)
18
+
19
+ @property
20
+ def last_run_dt(self) -> datetime | None:
21
+ if not self.last_run_iso:
22
+ return None
23
+ return datetime.fromisoformat(self.last_run_iso)
24
+
25
+ def stamp(self) -> None:
26
+ self.last_run_iso = datetime.now(timezone.utc).isoformat()
27
+
28
+
29
+ def load_state(path: Path) -> TaskillState:
30
+ if not path.exists():
31
+ return TaskillState()
32
+ try:
33
+ data = json.loads(path.read_text(encoding="utf-8"))
34
+ return TaskillState(**data)
35
+ except (json.JSONDecodeError, TypeError):
36
+ # corrupt state → start fresh, don't crash
37
+ return TaskillState()
38
+
39
+
40
+ def save_state(path: Path, state: TaskillState) -> None:
41
+ path.parent.mkdir(parents=True, exist_ok=True)
42
+ path.write_text(json.dumps(asdict(state), indent=2), encoding="utf-8")
taskill/triggers.py ADDED
@@ -0,0 +1,94 @@
1
+ """Decide whether thresholds in taskill.yaml are met → should we run?"""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+ from datetime import datetime, timezone
6
+ from pathlib import Path
7
+
8
+ from taskill.config import Triggers
9
+ from taskill.git_state import ProjectSnapshot
10
+ from taskill.state import TaskillState
11
+
12
+
13
+ @dataclass
14
+ class TriggerEvaluation:
15
+ should_run: bool
16
+ reasons: list[str]
17
+ skipped: list[str]
18
+
19
+ def summary(self) -> str:
20
+ if self.should_run:
21
+ return "RUN: " + "; ".join(self.reasons)
22
+ return "SKIP: thresholds not met"
23
+
24
+
25
+ def evaluate(
26
+ triggers: Triggers,
27
+ state: TaskillState,
28
+ snapshot: ProjectSnapshot,
29
+ project_root: Path,
30
+ ) -> TriggerEvaluation:
31
+ reasons: list[str] = []
32
+ skipped: list[str] = []
33
+
34
+ # 1. time since last run
35
+ if state.last_run_dt is None:
36
+ reasons.append("never run before")
37
+ else:
38
+ delta_h = (datetime.now(timezone.utc) - state.last_run_dt).total_seconds() / 3600
39
+ if delta_h >= triggers.min_hours_since_last_run:
40
+ reasons.append(f"{delta_h:.1f}h since last run (≥{triggers.min_hours_since_last_run}h)")
41
+ else:
42
+ skipped.append(f"only {delta_h:.1f}h since last run")
43
+
44
+ # 2. commits since last
45
+ n_commits = len(snapshot.commits_since_last_run)
46
+ if n_commits >= triggers.min_commits_since_last_run:
47
+ reasons.append(f"{n_commits} new commit(s)")
48
+ else:
49
+ skipped.append(f"only {n_commits} commits (<{triggers.min_commits_since_last_run})")
50
+
51
+ # 3. changed files
52
+ n_changed = len(snapshot.changed_files)
53
+ if n_changed >= triggers.changed_files_threshold:
54
+ reasons.append(f"{n_changed} changed file(s)")
55
+ else:
56
+ skipped.append(f"only {n_changed} files changed")
57
+
58
+ # 4. coverage delta
59
+ if triggers.coverage_change_pct is not None and snapshot.coverage_pct is not None and \
60
+ state.last_coverage_pct is not None:
61
+ delta = abs(snapshot.coverage_pct - state.last_coverage_pct)
62
+ if delta >= triggers.coverage_change_pct:
63
+ reasons.append(f"coverage moved {delta:.1f}pp")
64
+ else:
65
+ skipped.append(f"coverage Δ {delta:.1f}pp")
66
+
67
+ # 5. failed tests changed
68
+ if triggers.failed_tests_changed and snapshot.failed_tests is not None and \
69
+ state.last_failed_tests is not None and snapshot.failed_tests != state.last_failed_tests:
70
+ reasons.append(
71
+ f"failed tests {state.last_failed_tests} → {snapshot.failed_tests}"
72
+ )
73
+
74
+ # 6. SUMD changed
75
+ if snapshot.sumd_hash and snapshot.sumd_hash != state.last_sumd_hash:
76
+ reasons.append("SUMD.md changed")
77
+
78
+ # 7. watched files mtime
79
+ for rel in triggers.watch_files:
80
+ p = project_root / rel
81
+ if not p.exists():
82
+ continue
83
+ mtime = p.stat().st_mtime
84
+ prev = state.file_mtimes.get(rel)
85
+ if prev is None or mtime > prev:
86
+ reasons.append(f"{rel} touched")
87
+
88
+ if triggers.require_all:
89
+ # AND semantics: must have NO skipped reasons
90
+ should_run = bool(reasons) and not skipped
91
+ else:
92
+ should_run = bool(reasons)
93
+
94
+ return TriggerEvaluation(should_run=should_run, reasons=reasons, skipped=skipped)
@@ -0,0 +1,6 @@
1
+ """Document updaters — apply GeneratedDocs to README/CHANGELOG/TODO."""
2
+ from taskill.updaters.changelog import update_changelog
3
+ from taskill.updaters.todo import update_todo
4
+ from taskill.updaters.readme import update_readme
5
+
6
+ __all__ = ["update_changelog", "update_todo", "update_readme"]
@@ -0,0 +1,92 @@
1
+ """Update CHANGELOG.md following the Keep a Changelog convention.
2
+
3
+ Strategy:
4
+ - Keep the file's prelude (intro before first version heading).
5
+ - Find or create the `## [Unreleased]` section.
6
+ - Append new entries under it (deduplicated by exact line match).
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from datetime import date
12
+ from pathlib import Path
13
+
14
+ UNRELEASED_HEADING = "## [Unreleased]"
15
+
16
+ DEFAULT_HEADER = """# Changelog
17
+
18
+ All notable changes to this project will be documented in this file.
19
+
20
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
21
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
22
+
23
+ """
24
+
25
+
26
+ def update_changelog(path: Path, entries: list[str]) -> bool:
27
+ """Append entries under [Unreleased]. Returns True if the file changed."""
28
+ if not entries:
29
+ return False
30
+
31
+ if path.exists():
32
+ original = path.read_text(encoding="utf-8")
33
+ else:
34
+ original = DEFAULT_HEADER + UNRELEASED_HEADING + "\n\n"
35
+
36
+ # ensure [Unreleased] section exists
37
+ if UNRELEASED_HEADING not in original:
38
+ # insert it after the header (after first blank line following "# Changelog")
39
+ if original.startswith("# "):
40
+ head, _, tail = original.partition("\n\n")
41
+ original = f"{head}\n\n{UNRELEASED_HEADING}\n\n{tail}"
42
+ else:
43
+ original = UNRELEASED_HEADING + "\n\n" + original
44
+
45
+ # find the [Unreleased] block (until next "## [" or EOF)
46
+ pattern = re.compile(
47
+ rf"({re.escape(UNRELEASED_HEADING)}\n)(.*?)(?=^## \[|\Z)",
48
+ re.DOTALL | re.MULTILINE,
49
+ )
50
+ m = pattern.search(original)
51
+ if not m:
52
+ # shouldn't happen, but fail safely
53
+ return False
54
+
55
+ block_body = m.group(2)
56
+ existing_lines = set(line.rstrip() for line in block_body.splitlines() if line.strip())
57
+
58
+ # add only genuinely new entries; preserve order from `entries`
59
+ new_lines: list[str] = []
60
+ for entry in entries:
61
+ clean = entry.rstrip()
62
+ if clean and clean not in existing_lines:
63
+ new_lines.append(clean)
64
+ existing_lines.add(clean)
65
+
66
+ if not new_lines:
67
+ return False
68
+
69
+ # rebuild block: keep original body, append new bullets at the end of the
70
+ # appropriate subsection if a heading-of-headings exists; otherwise dump at end
71
+ new_block = block_body.rstrip() + "\n\n" + "\n".join(new_lines) + "\n\n"
72
+ updated = original[:m.start(2)] + new_block + original[m.end(2):]
73
+
74
+ path.write_text(updated, encoding="utf-8")
75
+ return True
76
+
77
+
78
+ def release_unreleased(path: Path, version: str, today: date | None = None) -> bool:
79
+ """Move [Unreleased] block to a versioned [x.y.z] - YYYY-MM-DD heading.
80
+
81
+ Optional helper, not invoked by default. Useful for `taskill release`.
82
+ """
83
+ if not path.exists():
84
+ return False
85
+ today = today or date.today()
86
+ txt = path.read_text(encoding="utf-8")
87
+ if UNRELEASED_HEADING not in txt:
88
+ return False
89
+ new_heading = f"## [{version}] - {today.isoformat()}"
90
+ txt = txt.replace(UNRELEASED_HEADING, f"{UNRELEASED_HEADING}\n\n{new_heading}", 1)
91
+ path.write_text(txt, encoding="utf-8")
92
+ return True
@@ -0,0 +1,74 @@
1
+ """README updater — refresh marked sections only.
2
+
3
+ Philosophy: never overwrite the whole README. We only touch sections wrapped in
4
+ HTML comment markers, so users can curate the rest by hand:
5
+
6
+ <!-- taskill:status:start -->
7
+ ...auto-generated content...
8
+ <!-- taskill:status:end -->
9
+
10
+ If the markers don't exist, we append a status block at the bottom.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import re
15
+ from datetime import datetime, timezone
16
+ from pathlib import Path
17
+
18
+ from taskill.git_state import ProjectSnapshot
19
+
20
+ START = "<!-- taskill:status:start -->"
21
+ END = "<!-- taskill:status:end -->"
22
+
23
+
24
+ def render_status_block(snapshot: ProjectSnapshot, summary: str) -> str:
25
+ now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
26
+ coverage = f"{snapshot.coverage_pct:.1f}%" if snapshot.coverage_pct is not None else "—"
27
+ failed = (
28
+ str(snapshot.failed_tests) if snapshot.failed_tests is not None else "—"
29
+ )
30
+ head = (snapshot.head_sha or "")[:7] or "—"
31
+
32
+ lines = [
33
+ START,
34
+ "",
35
+ "## Status",
36
+ "",
37
+ f"_Last updated by [taskill](https://github.com/oqlos/taskill) at {now}_",
38
+ "",
39
+ "| Metric | Value |",
40
+ "|---|---|",
41
+ f"| HEAD | `{head}` |",
42
+ f"| Coverage | {coverage} |",
43
+ f"| Failing tests | {failed} |",
44
+ f"| Commits in last cycle | {len(snapshot.commits_since_last_run)} |",
45
+ ]
46
+ if summary:
47
+ lines += ["", f"> {summary}"]
48
+ lines += ["", END]
49
+ return "\n".join(lines)
50
+
51
+
52
+ def update_readme(path: Path, snapshot: ProjectSnapshot, summary: str) -> bool:
53
+ block = render_status_block(snapshot, summary)
54
+
55
+ if not path.exists():
56
+ # don't invent a README — too presumptuous. Just write the block.
57
+ path.write_text(f"# {path.parent.name}\n\n{block}\n", encoding="utf-8")
58
+ return True
59
+
60
+ original = path.read_text(encoding="utf-8")
61
+
62
+ if START in original and END in original:
63
+ pattern = re.compile(
64
+ rf"{re.escape(START)}.*?{re.escape(END)}", re.DOTALL
65
+ )
66
+ updated = pattern.sub(block, original)
67
+ else:
68
+ sep = "\n\n" if not original.endswith("\n\n") else ""
69
+ updated = original.rstrip() + "\n\n" + block + "\n"
70
+
71
+ if updated == original:
72
+ return False
73
+ path.write_text(updated, encoding="utf-8")
74
+ return True
@@ -0,0 +1,77 @@
1
+ """Update TODO.md.
2
+
3
+ Two operations:
4
+ 1. Remove (or strike-through) lines that are now done.
5
+ 2. Append newly-discovered TODOs.
6
+
7
+ Behavior is conservative — we never delete user-authored text we don't recognize.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from pathlib import Path
12
+
13
+ DEFAULT_HEADER = "# TODO\n\n"
14
+
15
+
16
+ def update_todo(
17
+ path: Path,
18
+ completed_lines: list[str],
19
+ new_items: list[str],
20
+ *,
21
+ archive_completed: bool = True,
22
+ ) -> bool:
23
+ """Remove completed_lines from TODO and append new_items. Returns True on change."""
24
+ if not (completed_lines or new_items):
25
+ return False
26
+
27
+ if path.exists():
28
+ original = path.read_text(encoding="utf-8")
29
+ else:
30
+ original = DEFAULT_HEADER
31
+
32
+ lines = original.splitlines(keepends=False)
33
+ completed_set = {l.rstrip() for l in completed_lines if l.strip()}
34
+
35
+ kept: list[str] = []
36
+ archived: list[str] = []
37
+ for line in lines:
38
+ if line.rstrip() in completed_set and line.strip().startswith(("-", "*", "+")):
39
+ archived.append(line)
40
+ else:
41
+ kept.append(line)
42
+
43
+ # dedup new items: don't re-add what's already in TODO
44
+ existing = {l.strip() for l in kept if l.strip()}
45
+ fresh_new = [item for item in new_items if item.strip() not in existing]
46
+
47
+ # build output
48
+ out_lines = list(kept)
49
+ if fresh_new:
50
+ if out_lines and out_lines[-1].strip():
51
+ out_lines.append("")
52
+ # if no header, prepend one
53
+ if not any(l.startswith("# ") for l in out_lines):
54
+ out_lines = ["# TODO", ""] + out_lines
55
+ out_lines.append("## Discovered")
56
+ out_lines.append("")
57
+ out_lines.extend(fresh_new)
58
+ out_lines.append("")
59
+
60
+ if archive_completed and archived:
61
+ out_lines.append("")
62
+ out_lines.append("## Done (moved to CHANGELOG)")
63
+ out_lines.append("")
64
+ out_lines.extend(archived)
65
+ out_lines.append("")
66
+
67
+ new_content = "\n".join(out_lines).rstrip() + "\n"
68
+ if new_content == original:
69
+ return False
70
+
71
+ path.write_text(new_content, encoding="utf-8")
72
+ return True
73
+
74
+
75
+ def empty_todo(path: Path, header: str = DEFAULT_HEADER) -> None:
76
+ """Reset TODO.md to a clean empty header. Used by `taskill clean-todo`."""
77
+ path.write_text(header, encoding="utf-8")
@@ -0,0 +1,194 @@
1
+ Metadata-Version: 2.4
2
+ Name: taskill
3
+ Version: 0.1.1
4
+ Summary: Daily project hygiene: keep README / CHANGELOG / TODO in sync with reality. LLM-first, algorithmic fallback.
5
+ Project-URL: Homepage, https://github.com/oqlos/taskill
6
+ Project-URL: Issues, https://github.com/oqlos/taskill/issues
7
+ Author: Tom Sapletta
8
+ Author-email: Tom Sapletta <tom@sapletta.com>
9
+ License-Expression: Apache-2.0
10
+ License-File: LICENSE
11
+ Keywords: automation,changelog,ci-cd,llm,mcp,openrouter,readme,todo
12
+ Requires-Python: >=3.10
13
+ Requires-Dist: click>=8.1
14
+ Requires-Dist: costs>=0.1.20
15
+ Requires-Dist: goal>=2.1.0
16
+ Requires-Dist: httpx>=0.27
17
+ Requires-Dist: pfix>=0.1.60
18
+ Requires-Dist: python-dotenv>=1.0
19
+ Requires-Dist: pyyaml>=6.0
20
+ Requires-Dist: rich>=13.7
21
+ Provides-Extra: dev
22
+ Requires-Dist: costs>=0.1.20; extra == 'dev'
23
+ Requires-Dist: goal>=2.1.0; extra == 'dev'
24
+ Requires-Dist: mypy>=1.5; extra == 'dev'
25
+ Requires-Dist: pfix>=0.1.60; extra == 'dev'
26
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
27
+ Requires-Dist: pytest>=7.0; extra == 'dev'
28
+ Requires-Dist: ruff>=0.1; extra == 'dev'
29
+ Provides-Extra: mcp
30
+ Requires-Dist: mcp>=1.0; extra == 'mcp'
31
+ Provides-Extra: schedule
32
+ Requires-Dist: apscheduler>=3.10; extra == 'schedule'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # taskill
36
+
37
+
38
+ ## AI Cost Tracking
39
+
40
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.1.1-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
41
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$0.15-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-1.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
42
+
43
+ - 🤖 **LLM usage:** $0.1500 (1 commits)
44
+ - 👤 **Human dev:** ~$100 (1.0h @ $100/h, 30min dedup)
45
+
46
+ Generated on 2026-04-25 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
47
+
48
+ ---
49
+
50
+
51
+
52
+ > Daily project hygiene: keep `README.md` / `CHANGELOG.md` / `TODO.md` in sync with reality.
53
+ > LLM-first, algorithmic fallback. Works standalone, in CI, or via Ansible.
54
+
55
+ `taskill` is the small daemon you stop forgetting to run. Once a day (or whenever metrics drift), it reads your git log, your `SUMD.md` / `SUMR.md`, your coverage report, and updates the three documentation files everyone tells themselves they'll keep current and never do.
56
+
57
+ It uses an LLM where one is available — Windsurf MCP first (because you're probably already running it in JetBrains), OpenRouter second — and falls back to a deterministic Conventional Commits parser when no LLM is reachable. The fallback is always available and always runs offline.
58
+
59
+ `taskill` deliberately doesn't replace `pyqual`, `llx`, or `prefact`. It calls them as subprocesses when configured, picks up their reports, and stays out of the way otherwise.
60
+
61
+ ## Install
62
+
63
+ ```bash
64
+ pip install taskill # core
65
+ pip install "taskill[mcp]" # with Windsurf MCP support
66
+ pip install "taskill[mcp,schedule]" # with built-in scheduler
67
+ ```
68
+
69
+ ## Quickstart
70
+
71
+ ```bash
72
+ cd your-project/
73
+ taskill init # writes taskill.yaml + .env.example
74
+ cp .env.example .env # add OPENROUTER_API_KEY
75
+ taskill status # preview without running
76
+ taskill run --dry-run # see what would change
77
+ taskill run # do it
78
+ ```
79
+
80
+ ## What it does
81
+
82
+ Every run produces three (idempotent) edits:
83
+
84
+ 1. **`CHANGELOG.md`** — appends new entries under `## [Unreleased]`, grouped by Conventional Commit type (`### Added`, `### Fixed`, `### Performance`, etc.). Uses [Keep a Changelog](https://keepachangelog.com/) layout. Existing entries are deduplicated.
85
+ 2. **`TODO.md`** — moves completed items to a `## Done (moved to CHANGELOG)` section, and appends `TODO:` / `FIXME:` markers found in new commit bodies under `## Discovered`.
86
+ 3. **`README.md`** — refreshes only the block between `<!-- taskill:status:start -->` and `<!-- taskill:status:end -->` markers (HEAD, coverage, failing tests, summary). Never touches the rest of the file.
87
+
88
+ ## Provider chain
89
+
90
+ The chain runs top-to-bottom. First provider that's available *and* succeeds wins.
91
+
92
+ | Order | Provider | Used when |
93
+ |---|---|---|
94
+ | 1 | `windsurf_mcp` | `mcp` package installed and a Windsurf endpoint resolves |
95
+ | 2 | `openrouter` | `OPENROUTER_API_KEY` is set |
96
+ | 3 | `algorithmic` | always — pure git-log + Conventional Commits parser |
97
+
98
+ You can reorder, disable, or pass options via `taskill.yaml`:
99
+
100
+ ```yaml
101
+ providers:
102
+ - name: openrouter # skip windsurf, go straight to OpenRouter
103
+ enabled: true
104
+ - name: algorithmic
105
+ enabled: true
106
+ ```
107
+
108
+ ## Triggers
109
+
110
+ `taskill run` is a no-op unless one of the configured thresholds is crossed. State lives in `.taskill/state.json` so cron, GitHub Actions, and Ansible all share the same delta logic.
111
+
112
+ ```yaml
113
+ triggers:
114
+ min_hours_since_last_run: 24
115
+ min_commits_since_last_run: 1
116
+ changed_files_threshold: 1
117
+ coverage_change_pct: 2.0 # absolute pp; null to disable
118
+ failed_tests_changed: true
119
+ watch_files: [SUMD.md, SUMR.md]
120
+ require_all: false # OR by default; set true for AND
121
+ ```
122
+
123
+ `taskill run --force` ignores triggers entirely.
124
+
125
+ ## Running it
126
+
127
+ ### Cron / systemd timer
128
+
129
+ ```cron
130
+ 0 6 * * * cd /path/to/project && /usr/local/bin/taskill run >> ~/.taskill.log 2>&1
131
+ ```
132
+
133
+ ### GitHub Actions
134
+
135
+ See `examples/github-action.yml`. Triggers on `push` to main, runs `taskill run`, opens a PR if files changed.
136
+
137
+ ### GitLab CI
138
+
139
+ See `examples/gitlab-ci.yml`. Same idea, with merge-request creation via the GitLab API.
140
+
141
+ ### Ansible
142
+
143
+ See `examples/ansible-playbook.yml`. Useful for fleet-wide hygiene across many self-hosted repos.
144
+
145
+ ## CLI
146
+
147
+ ```
148
+ taskill init # generate taskill.yaml + .env.example
149
+ taskill status # show what would happen, no writes
150
+ taskill run # execute (respects triggers)
151
+ taskill run --force # ignore triggers
152
+ taskill run --dry-run # don't write files or state
153
+ taskill run --json # machine-readable output
154
+ taskill release X.Y.Z # promote [Unreleased] → versioned heading
155
+ taskill clean-todo # wipe TODO.md (after a release)
156
+ ```
157
+
158
+ ## Configuration reference
159
+
160
+ See [`taskill.yaml`](./taskill.yaml) at the repo root for the annotated default config.
161
+
162
+ ## Reusing existing tools
163
+
164
+ `taskill` doesn't try to absorb `pyqual` / `llx` / `prefact`. It calls them by `subprocess` when toggled in `reuse:` and feeds their JSON output to the LLM as extra context:
165
+
166
+ ```yaml
167
+ reuse:
168
+ pyqual: true # taskill will run `pyqual report --json`
169
+ llx: false # ...add llx context (planned for v0.2)
170
+ prefact: false # ...add prefact suggestions (planned for v0.2)
171
+ ```
172
+
173
+ If a tool isn't on `PATH`, `taskill` skips it silently — no hard dependency.
174
+
175
+ ## How it relates to the wider stack
176
+
177
+ ```
178
+ SUMD (description) ─┐
179
+ ├─→ taskill ──→ README.md
180
+ git log ────────────┤ CHANGELOG.md
181
+ pyqual report ──────┤ TODO.md
182
+ SUMR (state) ───────┘
183
+ ```
184
+
185
+ `taskill` reads, never generates code. That's what `prefact` / `llx` / `pyqual` are for.
186
+
187
+ ## License
188
+
189
+ Licensed under Apache-2.0.
190
+ ## Status
191
+
192
+ <!-- taskill:status:start -->
193
+ _Bootstrapped — no live project status yet. Will populate after first `taskill run`._
194
+ <!-- taskill:status:end -->
@@ -0,0 +1,21 @@
1
+ taskill/__init__.py,sha256=ghzG0Zm7axLz8dgGLVma8qCNXD196BLIQQoec80WodI,812
2
+ taskill/cli.py,sha256=cChFaM96O8LSrYvvKEz14iPtEEp2lX5cQXAix4dnQRc,8731
3
+ taskill/config.py,sha256=Z32qtXgLTqIF5o8PhXBPT4MCt7Xo47rKnWTm2zDoagE,5660
4
+ taskill/core.py,sha256=VhzZ6Y8j8539RXWt3BjHsP5wncKAUSJHSMRc9x02YWc,8126
5
+ taskill/git_state.py,sha256=4GCs-9yp0xUyRgYGV8rIYzzj-YhG0kwWCfCYDMW55cQ,5055
6
+ taskill/state.py,sha256=pEhShu_tEus5LuL2qwXDu4xETyscMmtnchdJlPT5vQk,1327
7
+ taskill/triggers.py,sha256=6LIS7TrD0TXGUkbMWrnkVOTxuAYmm2a4W2BZjQibybE,3228
8
+ taskill/providers/__init__.py,sha256=NZARGAesk3QfNMDlFMXbC8IcA1NBhHTNtdgns7prBOE,1128
9
+ taskill/providers/algorithmic.py,sha256=a5BVG41NOFhwxIJoMJOiwYAIhJgs8HdSV6JZEoxI7eA,5670
10
+ taskill/providers/base.py,sha256=-zEqWn_ho2fKjsOmGNbUN5qPukze6He8G_koj5mxTNU,3497
11
+ taskill/providers/openrouter.py,sha256=p1J-EprSOPbSjJlZbLjChFwPqY-9DT4IAoR8gksdPUI,4000
12
+ taskill/providers/windsurf_mcp.py,sha256=KrafaByK-6OJkA80C5J86V-sKzX1x6i8EcTtE2_k5LE,5199
13
+ taskill/updaters/__init__.py,sha256=Ks0dfgA7KlnrmIvPTytMTcr8PPkwJsXEVd2hC-R0m4E,290
14
+ taskill/updaters/changelog.py,sha256=m_y1enqLDMNt3G4_U6QcbzwPelQwlu1IMyzXu_EpiFc,3174
15
+ taskill/updaters/readme.py,sha256=W_eOwcasYPvJtT4JwOUZFS77NJ3XnthkRHMYN76JO1g,2344
16
+ taskill/updaters/todo.py,sha256=7V9oGp9oI5tERj-w18La_IOHVLVIeWbRsoyL66-u64A,2301
17
+ taskill-0.1.1.dist-info/METADATA,sha256=W3PZEH9bqmXFlDpAZF00-i9mdgP8LIS8oZFYgNONr8A,7357
18
+ taskill-0.1.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
19
+ taskill-0.1.1.dist-info/entry_points.txt,sha256=LRZ0ou7wTswiRh3bMpi9E0O57tf4njpjv5jd8jX3_GE,45
20
+ taskill-0.1.1.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
21
+ taskill-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ taskill = taskill.cli:main