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
47 changes: 47 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,53 @@ All notable changes to `fleet` are documented here. The format is loosely
based on [Keep a Changelog](https://keepachangelog.com/), and the project
adheres to [Semantic Versioning](https://semver.org/).

## [0.2.0] - 2026-08-20

Second release: **Health v2**. Adds a git-aware, four-class health scheme
(`stranded` / `active` / `paused` / `dead`) that runs alongside the v1
recency-only scheme, and ships it end-to-end through the report, CLI, and
snapshot layers. The v2 classifier is a pure function of days-since-activity,
last outcome, and a git-side work-in-flight signal; it resolves
**most-severe-wins** (`stranded` > `active` > `paused` > `dead`), and only the
exact outcome `max_steps_reached` counts as "work in flight". The v1 and v2
dead boundaries intentionally diverge at exactly 30 days (v1 `> 30`, v2
`>= 30`) and are not unified.

### Added

- **Git-side signal** (`fleet.gittest`): `read_gitstate(repo_path) ->
GitState(unmerged_build_branches, unpushed_commits)` and `EMPTY_STATE`.
Reports local `build*` branches with commits not on `main` and unpushed
commits on `main`; never raises (missing repo / no `origin` / no `main` /
any git failure → `EMPTY_STATE`).
- **v2 classifier** (`fleet.health.classify_health_v2`): a pure, four-class,
most-severe-wins classifier (`stranded` / `active` / `paused` / `dead`).
`stranded` = an unmerged `build*` branch OR unpushed commits on `main`,
regardless of recency; `active` = touched ≤ 7 days AND work in flight;
`paused` = recently touched but done, or idle in the 8-29 day band; `dead`
= 30+ days untouched (or no activity signal) with nothing in flight.
- **`ProjectHealth.health_v2`**: a defaulted field (`None`) carrying the v2
class when known.
- **Report** (`fleet.report`): `render_portfolio(..., git_states=...)` gains an
opt-in trailing `Git` column (a compact work-in-flight summary:
`unmerged:<branch>` / `unpushed:<n>` / `-` when clean).
- **CLI** (`fleet.cli`): `status` always shows the `Git` column; `--filter`
now accepts `stranded` / `paused` (selected with the v2 classifier over each
project's git state) in addition to the v1 `active` / `stalled` / `dead` /
`all`.
- **Snapshot v2** (`fleet.snapshot`): `save_snapshot(..., git_states=...)`
computes and stores a per-row `health_v2`; `snapshot_diff` surfaces a
`health_v2 <a>-><b>` fragment (`-` for `None`); old v1 snapshots (no
`health_v2` key) still load with `health_v2` defaulting to `None`.
- **Integration**: an end-to-end v2 pipeline test (in-tree fixture) plus a
live-root self-consistency smoke test.

### Notes

- The v1 `Health` column and `classify_health` are unchanged; v2 is additive.
- The v1/v2 30-day dead-boundary divergence (`> 30` vs `>= 30`) is intentional
and pinned by tests in both schemes.

## [0.1.0] - 2026-08-19

First release. `fleet` is a multi-project health scanner for the four
Expand Down
46 changes: 39 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,31 +18,63 @@ The portfolio table is produced by a four-stage pipeline:
2. **assess** (`fleet.health.assess` / `project_health`) extracts per-project
metrics via the `fourseer` parsers: last cycle, last outcome, days since
activity, and open issues.
3. **classify** (`fleet.health.classify_health`) maps those metrics to a health
label: `active`, `stalled`, or `dead`.
3. **classify** (`fleet.health.classify_health`) maps those metrics to a v1
health label: `active`, `stalled`, or `dead`. A second, git-aware scheme
(`fleet.health.classify_health_v2`) classifies into four classes
(`stranded` / `active` / `paused` / `dead`) — see below.
4. **render** (`fleet.report.render_portfolio`) emits the markdown table,
sorted by last-activity descending (no-activity last).

## Health classification
## Health classification (v1)

`classify_health` is the recency-only scheme used by the `Health` column:

| Health | Condition |
|------------|-----------|
| **active** | has trajectories AND ≤ 7 days since activity |
| **stalled** | has trajectories AND 8-30 days, OR has trajectories but no activity signal |
| **dead** | no trajectories, OR > 30 days since activity |

## Health classification (v2)

`classify_health_v2(days, last_outcome, git_state)` is a pure, git-aware scheme
with **four classes**, resolved **most-severe-wins**
(`stranded` > `active` > `paused` > `dead`). "Work in flight" means an unmerged
`build*` branch, unpushed commits on `main`, or a last outcome of exactly
`max_steps_reached` (no other outcome counts as in-flight).

| Health | Condition |
|--------------|-----------|
| **stranded** | an unmerged `build*` branch OR unpushed commits on `main`, regardless of recency (git work in flight) |
| **active** | touched ≤ 7 days AND work in flight (last outcome `max_steps_reached`; an unmerged branch already → `stranded`) |
| **paused** | recently touched but done (nothing in flight), or idle in the 8-29 day band with nothing in flight |
| **dead** | 30+ days untouched AND nothing in flight, or no activity signal at all with nothing in flight |

> **v1/v2 30-day boundary divergence.** The two schemes intentionally disagree
> at exactly 30 days: v1 `dead` is `> 30` days (so 30 days is `stalled`), while
> v2 `dead` is `>= 30` days (so 30 days is `dead`). They are **not** unified —
> each is pinned by its own tests. Do not "normalize" one to match the other.

## CLI

fleet status [--root ~/AI] [--filter active|stalled|dead|all]
fleet status [--root ~/AI] [--filter active|stalled|dead|stranded|paused|all]
fleet snapshot [--root ~/AI] [--snapshot SNAPSHOT]
fleet diff [--root ~/AI] [--snapshot SNAPSHOT]

- `status` prints the current portfolio as a markdown table, optionally
filtered by health (`active`/`stalled`/`dead`/`all`).
filtered by health (`active`/`stalled`/`dead`/`stranded`/`paused`/`all`).
The v1 classes (`active`/`stalled`/`dead`) match the `Health` column; the two
v2-only classes (`stranded`/`paused`) are selected with the v2 classifier
over each project's git state. The table always shows a trailing `Git`
column — a compact work-in-flight summary: `unmerged:<branch>` /
`unpushed:<n>` / `-` when clean.
- `snapshot` saves the current portfolio as a snapshot JSON (the baseline that
`diff` compares against).
`diff` compares against). Each row now stores a `health_v2` field (the v2
class computed from the project's git state).
- `diff` compares the current portfolio against a saved snapshot JSON and
prints a markdown diff table.
prints a markdown diff table. A v2 transition surfaces as a
`health_v2 <a>-><b>` fragment (`-` for `None`). Old v1 snapshots (no
`health_v2` key) still load, with `health_v2` defaulting to `None`.

Output is a markdown table sorted by last-activity descending.

Expand Down
2 changes: 1 addition & 1 deletion fleet/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import os
import sys

__version__ = "0.1.0"
__version__ = "0.2.0"

# Bootstrap: make the fourseer seed package importable. fleet's only
# third-party dependency is fourseer, imported from the seed path. The seed
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "fleet"
version = "0.1.0"
version = "0.2.0"
description = "Multi-project health scanner for the four pipeline: portfolio status table."
readme = "README.md"
requires-python = ">=3.10"
Expand Down
6 changes: 3 additions & 3 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -583,9 +583,9 @@ def test_cli_version_flag_prints_exact_version(capsys) -> None:
assert out.strip() == f"fleet {__version__}"


def test_cli_version_is_0_1_0() -> None:
"""The shipped version is pinned to 0.1.0 (first release)."""
assert __version__ == "0.1.0"
def test_cli_version_is_0_2_0() -> None:
"""The shipped version is pinned to 0.2.0 (second release / Health v2)."""
assert __version__ == "0.2.0"


# ---------------------------------------------------------------------------
Expand Down
30 changes: 30 additions & 0 deletions tickets/TICKET-073-bump-version-to-0-2-0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# TICKET-073: Bump the shipped version to 0.2.0 (Health v2 release)

## Title
Move `fleet` from `0.1.0` to `0.2.0` consistently across the version string,
the packaging metadata, and the hard-coded version test, so the Health v2 arc
(Cycles 14-18) ships as the second release.

## Evidence
- `fleet/__init__.py:23` — `__version__ = "0.1.0"`.
- `pyproject.toml:7` — `version = "0.1.0"` (the `[project]` version, not the
`ruff>=0.1.0` dev pin on line 18, which is unrelated and must not change).
- `tests/test_cli.py:586-588` — `test_cli_version_is_0_1_0` hard-asserts
`__version__ == "0.1.0"`. The two sibling version tests
(`test_cli_version_flag_exits_zero_and_prints_version`,
`test_cli_version_flag_prints_exact_version`) are dynamic (they compare
against `__version__`) and need no change.

## Change
- `fleet/__init__.py`: `__version__ = "0.2.0"`.
- `pyproject.toml`: `version = "0.2.0"`.
- `tests/test_cli.py`: rename `test_cli_version_is_0_1_0` to
`test_cli_version_is_0_2_0`, assert `__version__ == "0.2.0"`, and update the
docstring to "second release / Health v2".

## Acceptance
- `fleet.__version__ == "0.2.0"` and `pyproject.toml` `version == "0.2.0"`.
- `python3 -m fleet --version` prints exactly `fleet 0.2.0` and exits 0.
- `test_cli_version_is_0_2_0` passes; the two dynamic version tests stay green.
- Gate: `pytest tests/ -x -q`, `ruff check fleet/`,
`mypy fleet/ --ignore-missing-imports`.
42 changes: 42 additions & 0 deletions tickets/TICKET-074-readme-health-v2-threshold-table.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# TICKET-074: Document the Health v2 threshold table in the README

## Title
The README documents only the v1 health table (`active`/`stalled`/`dead`).
Add a **Health classification (v2)** section documenting the four-class,
git-aware scheme, the most-severe-wins rule, the `max_steps_reached`-only
in-flight rule, and the v1/v2 30-day boundary divergence.

## Evidence
- `README.md` (pre-change) — a single "Health classification" section with a
three-row v1 table; no mention of `stranded`/`paused`, the `Git` column,
`--filter stranded|paused`, the `health_v2` snapshot field, or the 30-day
divergence.
- `fleet/health.py` — `classify_health_v2(days, last_outcome, git_state)` is
the four-class, most-severe-wins classifier (`stranded` > `active` >
`paused` > `dead`); only the exact outcome `max_steps_reached` counts as
"work in flight".
- `fleet/health.py` — `DEAD_MIN_DAYS = 30` (v2 dead is `>= 30`) vs
`STALLED_MAX_DAYS = 30` (v1 dead is `> 30`); the two intentionally disagree
at exactly 30 days (pinned by tests in Cycles 15/17; see TICKET-059).

## Change
- Rename the existing section to **Health classification (v1)** and keep the
v1 table.
- Add **Health classification (v2)** with a four-row table:
- `stranded` — unmerged `build*` branch OR unpushed commits on `main`,
regardless of recency.
- `active` — touched ≤ 7 days AND work in flight (last outcome
`max_steps_reached`; an unmerged branch already → `stranded`).
- `paused` — recently touched but done, or idle in the 8-29 day band with
nothing in flight.
- `dead` — 30+ days untouched (or no activity signal) with nothing in
flight.
- State the most-severe-wins rule and that only `max_steps_reached` counts as
in-flight.
- Add a callout documenting the v1/v2 30-day boundary divergence (v1 `> 30`,
v2 `>= 30`; not unified; do not normalize).

## Acceptance
- README documents all four v2 classes, the most-severe-wins rule, the
`max_steps_reached`-only in-flight rule, and the v1/v2 30-day divergence.
- The v1 table is preserved.
36 changes: 36 additions & 0 deletions tickets/TICKET-075-readme-cli-git-column-filter-snapshot.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# TICKET-075: Document the v2 CLI surface (Git column, --filter, snapshot health_v2)

## Title
The README's CLI section still shows the v1 `--filter active|stalled|dead|all`
and says nothing about the `Git` column, the `stranded`/`paused` filter
values, or the `health_v2` snapshot field. Update it to match the shipped v2
CLI behavior.

## Evidence
- `fleet/cli.py` — `_VALID_FILTERS = ("active", "stalled", "dead", "stranded",
"paused", "all")`; `_cmd_status` selects `stranded`/`paused` with
`classify_health_v2` over each project's git state, and the v1 classes match
the `Health` column.
- `fleet/cli.py` — `_cmd_status` always passes `git_states` to
`render_portfolio`, so the `status` table always shows a trailing `Git`
column.
- `fleet/report.py` — `_fmt_git` renders `unmerged:<b1>+<b2>` / `unpushed:<n>`
/ `-` when clean.
- `fleet/snapshot.py` — `save_snapshot(..., git_states=...)` stores a per-row
`health_v2`; `_field_changes` surfaces a `health_v2 <a>-><b>` fragment
(`-` for `None`); `_health_from_dict` reads `health_v2=d.get("health_v2")`
so old v1 snapshots load with `health_v2 is None`.

## Change
- Update the `status` usage line to
`[--filter active|stalled|dead|stranded|paused|all]`.
- Note that the `status` table always shows a trailing `Git` column
(`unmerged:<branch>` / `unpushed:<n>` / `-` when clean) and that
`stranded`/`paused` are selected with the v2 classifier over the git state.
- Note that `snapshot` now stores a per-row `health_v2` field and `diff`
surfaces a `health_v2 <a>-><b>` fragment (`-` for `None`), and that old v1
snapshots still load with `health_v2` defaulting to `None`.

## Acceptance
- README CLI section matches the shipped v2 CLI: filter choices, the always-on
`Git` column, and the snapshot `health_v2` field.
33 changes: 33 additions & 0 deletions tickets/TICKET-076-changelog-0-2-0-entry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# TICKET-076: Add a [0.2.0] CHANGELOG entry covering the Health v2 arc

## Title
`CHANGELOG.md` has a single `[0.1.0]` entry. Add a `## [0.2.0]` section above
it that records the Health v2 arc (Cycles 14-18) as the second release,
leaving `[0.1.0]` intact.

## Evidence
- `CHANGELOG.md` (pre-change) — only `## [0.1.0] - 2026-08-19`.
- The Health v2 arc landed across Cycles 14-18: `fleet.gittest`
(`read_gitstate`/`GitState`/`EMPTY_STATE`), `classify_health_v2`,
`ProjectHealth.health_v2`, the report `Git` column, the CLI
`--filter stranded|paused`, snapshot v2 (`health_v2` round-trip + diff
fragment + old-snapshot loading), and the end-to-end v2 integration test.

## Change
- Add `## [0.2.0] - <date>` above `[0.1.0]` with a summary paragraph (four
classes, most-severe-wins, `max_steps_reached`-only in-flight, v1/v2 30-day
divergence) and an **Added** list covering:
- `fleet.gittest` — `read_gitstate` / `GitState` / `EMPTY_STATE`.
- `fleet.health.classify_health_v2` — four-class, most-severe-wins.
- `ProjectHealth.health_v2` — defaulted field.
- Report — opt-in `Git` column.
- CLI — `Git` column always shown; `--filter` accepts `stranded`/`paused`.
- Snapshot v2 — `save_snapshot(..., git_states=...)` stores `health_v2`;
`snapshot_diff` surfaces a `health_v2 <a>-><b>` fragment; old v1 snapshots
still load.
- Integration — end-to-end v2 pipeline test + live-root smoke test.
- Keep the existing `[0.1.0]` entry byte-for-byte intact.

## Acceptance
- `CHANGELOG.md` has a `[0.2.0]` entry covering the Health v2 arc; `[0.1.0]`
is unchanged.
29 changes: 29 additions & 0 deletions tickets/TICKET-077-release-verification-and-tag-decision.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# TICKET-077: Release verification — gate green, --version prints 0.2.0, tag decision

## Title
Cut the 0.2.0 release: confirm the full gate is green, confirm `--version`
prints `fleet 0.2.0`, and record the decision on whether to create a `v0.2.0`
git tag.

## Evidence
- `fleet/cli.py` — the top-level `--version` flag already exists
(`action="version"`, `version=f"fleet {__version__}"`); after the bump it
prints `fleet 0.2.0`. No new flag is needed.
- `git tag -l` — returns empty: the repo has **no prior tags**. Per the
Cycle 19 briefing, a tag is optional when there are no prior tags and may be
skipped to keep the cycle minimal.

## Change / Decision
- Verify the gate: `pytest tests/ -x -q`, `ruff check fleet/`,
`mypy fleet/ --ignore-missing-imports` — all green.
- Verify `python3 -m fleet --version` prints exactly `fleet 0.2.0` and exits 0.
- **Tag decision: skip the `v0.2.0` tag.** The repo has no prior tags, so a
single tag would not establish a tagging convention and the briefing marks
it optional. Revisit tagging when a convention is adopted.

## Acceptance
- All pre-existing tests (report/cli/integration/integration_v2/snapshot/
health/gittest/smoke/examples) stay green.
- `fleet --version` prints `fleet 0.2.0`.
- No `fleet/` source logic changed (only the `__version__` string in
`fleet/__init__.py`); the release is docs + version only.
Loading