diff --git a/.ytstack/DECISIONS.md b/.ytstack/DECISIONS.md index 870c45d..97c5fd5 100644 --- a/.ytstack/DECISIONS.md +++ b/.ytstack/DECISIONS.md @@ -797,3 +797,22 @@ Plus: ops-layer split during the same session (yopstack created as own public re **How to apply:** Keep Codex tool mappings in codex/adapter.md. The Codex SessionStart entry passes these as optional host context to the shared hook. Without trusted hooks, read the adapter explicitly before following skills. The small runtime helper supplies the shared scripts' existing CLAUDE_* inputs and resolves the repository root; those compatibility names do not require a Claude executable. All three lifecycle events use the original hook scripts. Do not reintroduce generated copies or a second skill tree. **Supersedes:** The earlier generated-package decision and per-skill Codex-link approach. The host-specific manifests, three-event Codex registration, hook trust, and repository-subdirectory fix remain. + +--- + +## 2026-11-11: Status line progress is one optional `progress` block in STATE.md, read by a plugin script (M021, issue #31) + +**Context:** A project built its own status line segment by scraping a totals row out of a prose table in `STATE.md`. That works for one table and breaks silently when the prose changes. Claude Code plugins cannot set the main `statusLine`, and no documented path lets a status line command find plugin files across versions. + +**Options considered:** +- A) A new progress file per project. +- B) An optional `progress` block in the `STATE.md` frontmatter, with the slice plan checkboxes as fallback. +- C) Several named tracks, or a `done` / `open` pair instead of `done` / `total`. + +**Chose:** B with exactly one track (`label`, `done`, `total`, optional `waiting_on`). `bin/ytstack-status` prints the segment; `yt_progress_parse` is the only parser and serves the script and the STATE lint (W7). Plan progress is read from the default branch when the checkout is on another branch, the task pointer from the checkout. The script is found by a glob over the plugin cache in the user's own status line script. SessionStart does not maintain a link. + +**Reason:** `STATE.md` exists in every project, already has machine-read frontmatter and is already linted. One parser means the segment and the lint cannot disagree, so a malformed block shows nothing instead of a wrong number. One track covers the case that motivated the issue; a list of tracks can be added later without breaking readers of the single block. A hook-maintained link would break the read-only contract of SessionStart and force every suite that fires the hook to isolate HOME. + +**How to apply:** New readers of the block call `yt_progress_parse`; do not add a second parser. Skills that advance STATE update the block (`summarize-task`, `quick-task`). The status script stays free of network calls and `gh`. C is open for a later milestone when a project needs it. + +**Supersedes:** None. diff --git a/.ytstack/M021-CONTEXT.md b/.ytstack/M021-CONTEXT.md new file mode 100644 index 0000000..30bc5fa --- /dev/null +++ b/.ytstack/M021-CONTEXT.md @@ -0,0 +1,43 @@ +--- +milestone: M021 +project: ytstack +created: 2026-11-11T10:25:52Z +size: M +--- + +# M021 -- Context + +## Goal + +Any status line script can append one compact ytstack segment that shows project progress and the current task of the directory it runs in, read from `.ytstack/STATE.md` without per-project scraping. + +## Exit criteria + +- With a `progress` block in the `STATE.md` frontmatter, `bin/ytstack-status` prints label, percentage and done/total; with `waiting_on` set it also prints that item, truncated. +- Without the block it prints the active slice's task count taken from the checkboxes in `M###-S##-PLAN.md`. +- Outside a ytstack project it prints nothing and exits non-zero. +- In a linked worktree the plan progress comes from the default branch and the task pointer from the worktree's own `STATE.md`. +- A malformed block prints nothing; `yt_state_lint` reports it (integers, `done <= total`, short label). +- `summarize-task` and `quick-task` keep the block current when they advance STATE. +- `docs/statusline.md` documents the block, the two-line integration and a stable script location that survives plugin updates; README and CHANGELOG point at it. +- `bash tests/run-all.sh` passes and `bin/ytstack-check` reports no new failures. + +## Size + +M -- 2 slices. See `M021-ROADMAP.md` for slice breakdown. + +## Planning input + +GitHub issue #31 and the user's instruction to implement it. Milestone number M021 because M020 (Kimi adapter, issue #30) is planned in the main checkout and not committed yet. + +## Decisions locked in discuss phase + +2026-11-11: Exactly one `progress` track with `label`, `done`, `total` and optional `waiting_on`, as the issue proposes. Several named tracks and a `done`/`open` pair (the issue's open questions) stay out until a project needs them; the single block can be extended to a list later without breaking readers. + +2026-11-11: One parser for the block lives in `hooks/lib/git-ctx.sh` and serves both the status script and the STATE lint, so the two cannot disagree about what is malformed. + +2026-11-11: No network and no `gh` in the status script. The default branch is read from local refs only. + +## Open questions + +(none) diff --git a/.ytstack/M021-ROADMAP.md b/.ytstack/M021-ROADMAP.md new file mode 100644 index 0000000..5554803 --- /dev/null +++ b/.ytstack/M021-ROADMAP.md @@ -0,0 +1,40 @@ +--- +milestone: M021 +project: ytstack +size: M +created: 2026-11-11T10:25:52Z +status: done +total_slices: 2 +completed_slices: 2 +--- + +# M021 Roadmap + +**Goal:** Any status line script can append one compact ytstack segment that shows project progress and the current task of the directory it runs in, read from `.ytstack/STATE.md` without per-project scraping. + +**Exit criteria:** +- With a `progress` block in the `STATE.md` frontmatter, `bin/ytstack-status` prints label, percentage and done/total; with `waiting_on` set it also prints that item, truncated. +- Without the block it prints the active slice's task count taken from the checkboxes in `M###-S##-PLAN.md`. +- Outside a ytstack project it prints nothing and exits non-zero. +- In a linked worktree the plan progress comes from the default branch and the task pointer from the worktree's own `STATE.md`. +- A malformed block prints nothing; `yt_state_lint` reports it (integers, `done <= total`, short label). +- `summarize-task` and `quick-task` keep the block current when they advance STATE. +- `docs/statusline.md` documents the block, the two-line integration and a stable script location that survives plugin updates; README and CHANGELOG point at it. +- `bash tests/run-all.sh` passes and `bin/ytstack-check` reports no new failures. + +## Slices + +Slice detail lives in per-slice `M021-S##-PLAN.md` files, created by `ytstack:slice-milestone`. + +- [x] S01 -- `bin/ytstack-status` prints the progress segment for any directory, from one shared parser that the STATE lint also uses. +- [x] S02 -- The block stays current without hand upkeep and a user can wire the segment into their status line from the documentation. + +## Run order + +Slices execute sequentially. After each slice, `ytstack:reassess-roadmap` checks if the plan still fits reality. + +## How to update this file + +- Flip slice checkbox `[ ]` -> `[x]` when its tasks are all `summarize-task`-confirmed +- Update `completed_slices` count +- On milestone completion, flip `status: planned` -> `status: done` and update global ROADMAP.md diff --git a/.ytstack/M021-S01-PLAN.md b/.ytstack/M021-S01-PLAN.md new file mode 100644 index 0000000..7470d92 --- /dev/null +++ b/.ytstack/M021-S01-PLAN.md @@ -0,0 +1,27 @@ +--- +milestone: M021 +slice: S01 +project: ytstack +created: 2026-11-11T10:25:52Z +status: done +task_count: 2 +completed_tasks: 2 +--- + +# M021-S01 -- Slice Plan + +**Goal:** `bin/ytstack-status` prints the progress segment for any directory, from one shared parser that the STATE lint also uses. + +## Tasks + +- [x] T01 -- `yt_progress_parse` in `hooks/lib/git-ctx.sh` (reads STATE content on stdin; sets state none / ok / bad plus label, done, total, waiting_on) and lint finding W7 in `yt_state_lint` for a malformed block; cases in `tests/git-ctx.test.sh`. +- [x] T02 -- `bin/ytstack-status`: cwd from the status line JSON (`workspace.current_dir`, `cwd`, else `$PWD`), plan progress from the default branch outside it, task pointer from the local `STATE.md`, slice checkbox fallback, `waiting_on` truncation, `NO_COLOR` / `--no-color`, silent non-zero exit outside a project or on a malformed block; new `tests/ytstack-status.test.sh`. + +## Done when + +All tasks marked `[x]` and verified via `ytstack:summarize-task`. + +## Notes + +- Claude Code documents no stable path to plugin files for a `statusLine` command: `bin/` is on PATH for the Bash tool only, and `CLAUDE_PLUGIN_ROOT` changes on every update (statusline and plugin manifest reference, read 2026-11-11). +- Considered and dropped: SessionStart maintaining a `~/.ytstack/bin/ytstack-status` link. The hook is read-only by contract and every suite that fires it would need an isolated HOME. The integration snippet in `docs/statusline.md` (S02-T02) resolves the newest installed version itself instead. diff --git a/.ytstack/M021-S01-T01-PLAN.md b/.ytstack/M021-S01-T01-PLAN.md new file mode 100644 index 0000000..a3040fb --- /dev/null +++ b/.ytstack/M021-S01-T01-PLAN.md @@ -0,0 +1,43 @@ +--- +milestone: M021 +slice: S01 +task: T01 +project: ytstack +created: 2026-11-11T10:27:00Z +status: planned +--- + +# M021-S01-T01 -- Task Plan + +**Summary:** `yt_progress_parse` in `hooks/lib/git-ctx.sh` and lint finding W7 in `yt_state_lint` for a malformed `progress` block. + +## Files + +- Modify: `hooks/lib/git-ctx.sh` (new function before `yt_state_lint`; W7 inside it; header comment lists W7) +- Modify: `tests/git-ctx.test.sh` (new section after the STATE lint cases) + +## What this does + +`yt_progress_parse` reads STATE.md content on stdin, so the caller can feed a file or `git show` output. It looks at the frontmatter only (up to the closing `---`, or the first `#` heading when the file has no delimiters) for a `progress:` key followed by indented `label`, `done`, `total` and optional `waiting_on`. Values lose surrounding quotes and a trailing ` # comment`; comment lines and unknown keys inside the block are skipped. + +Result variables: + +- `YT_PROGRESS`: `none` (no block), `ok`, or `bad` +- `YT_PROGRESS_LABEL`, `YT_PROGRESS_DONE`, `YT_PROGRESS_TOTAL`, `YT_PROGRESS_WAITING`: set only when `ok` +- `YT_PROGRESS_ERRORS`: one line per problem when `bad` + +Rules for `ok`: `label` present and at most 16 characters; `done` and `total` are plain digits (at most 9); `total >= 1`; `done <= total`. `progress:` with an inline value is `bad`. + +`yt_state_lint` calls the parser on `STATE.md` and adds one warning per error line (W7). No block means no warning. + +## Verification + +```bash +bash tests/git-ctx.test.sh +``` + +Success: command exits 0 and shows expected output. Failure: re-read plan, check if spec drifted. + +## Done signal + +Run `/ytstack:summarize-task` after execution to mark complete and advance STATE.md. diff --git a/.ytstack/M021-S01-T01-SUMMARY.md b/.ytstack/M021-S01-T01-SUMMARY.md new file mode 100644 index 0000000..3ab4c06 --- /dev/null +++ b/.ytstack/M021-S01-T01-SUMMARY.md @@ -0,0 +1,32 @@ +--- +milestone: M021 +slice: S01 +task: T01 +project: ytstack +closed: 2026-11-11T10:29:03Z +verification: passed +--- + +# M021-S01-T01 -- Summary + +## Commits + +- `d29ab53` M021-S01-T01: add yt_progress_parse and STATE lint finding W7 for the progress block + +## Outcome + +`yt_progress_parse` reads STATE.md content on stdin and reports `none`, `ok` or `bad` for the optional frontmatter `progress` block, with label, done, total and waiting_on on `ok` and one error line per problem on `bad`. `yt_state_lint` runs it on `STATE.md` and turns each error line into a warning (W7). A project without the block gets no warning. + +## Deviations from plan + +none + +## Follow-ups + +none + +## Verification + +Command: `bash tests/git-ctx.test.sh` -- passed (150 passed, 0 failed; same result under `/bin/bash` 3.2). + +Minimal-code review: rung 6. One awk pass instead of a YAML dependency. Left out on purpose: several tracks, a `done`/`open` pair, multi-line YAML scalars. diff --git a/.ytstack/M021-S01-T02-PLAN.md b/.ytstack/M021-S01-T02-PLAN.md new file mode 100644 index 0000000..95c53aa --- /dev/null +++ b/.ytstack/M021-S01-T02-PLAN.md @@ -0,0 +1,45 @@ +--- +milestone: M021 +slice: S01 +task: T02 +project: ytstack +created: 2026-11-11T10:29:30Z +status: planned +--- + +# M021-S01-T02 -- Task Plan + +**Summary:** `bin/ytstack-status` prints one compact segment for the directory named in the status line JSON. + +## Files + +- Create: `bin/ytstack-status` (executable) +- Create: `tests/ytstack-status.test.sh` + +## What this does + +Input: status line JSON on stdin (only `cwd` is used, through `yt_read_stdin`); without usable input the script falls back to `$PWD`. Flag `--no-color`; a non-empty `NO_COLOR` does the same. + +Resolution: `yt_git_ctx` + `yt_resolve_yt_dir`. No `.ytstack` or no `STATE.md`: no output, exit 1. + +Task pointer: `current_milestone` / `active_slice` / `active_task` from the local `STATE.md`; parts that are `none`, `all` or malformed are left out. + +Plan progress: in a project-scope git checkout whose current branch is not the default branch, `STATE.md` is read with `git show` from the newer of `origin/` and the local ``; the default branch comes from `origin/HEAD`, else `main`, else `master`. Otherwise, or when that read yields nothing, the working tree file is used. Local refs only, no network. + +Output (one line, exit 0): + +- block `ok`: `V2 76% 138/180 · M021/S01/T02 · waiting: ` (pointer and waiting part only when present; the item is cut to 32 characters plus `…`) +- no block: `M021/S01/T02 3/7`, the count taken from the task checkboxes of the active slice plan (flat path first, then `yt_find_artifacts`); pointer alone when no slice plan is found +- block `bad`, or neither block nor pointer: no output, exit 1 + +## Verification + +```bash +bash tests/ytstack-status.test.sh +``` + +Success: command exits 0 and shows expected output. Failure: re-read plan, check if spec drifted. + +## Done signal + +Run `/ytstack:summarize-task` after execution to mark complete and advance STATE.md. diff --git a/.ytstack/M021-S01-T02-SUMMARY.md b/.ytstack/M021-S01-T02-SUMMARY.md new file mode 100644 index 0000000..0a4d06b --- /dev/null +++ b/.ytstack/M021-S01-T02-SUMMARY.md @@ -0,0 +1,33 @@ +--- +milestone: M021 +slice: S01 +task: T02 +project: ytstack +closed: 2026-11-11T10:31:28Z +verification: passed +--- + +# M021-S01-T02 -- Summary + +## Commits + +- `967d90d` M021-S01-T02: add bin/ytstack-status, the status line segment for a ytstack project + +## Outcome + +`bin/ytstack-status` reads `cwd` from the status line JSON (else `$PWD`), resolves the ytstack project and prints one line: with a `progress` block `V2 76% 138/180 · M021/S01/T02 · waiting: …`, without it the pointer plus the active slice's checkbox count (`M021/S01/T02 1/2`). On a branch other than the default one the block is read with `git show` from the newer of `origin/` and ``; the pointer stays local. It prints nothing and exits 1 outside a project, for a malformed block, and when there is neither block nor pointer. `NO_COLOR` and `--no-color` switch the colour off. + +## Deviations from plan + +none + +## Follow-ups + +- `shortcut:` in `bin/ytstack-status`: `waiting_on` is cut by characters of the current locale; under `LC_ALL=C` a multi-byte character at the cut can be split. +- One run takes about 90 ms on this machine (ten runs 0.92 s), most of it the git calls in `yt_git_ctx`. Fine for a status line that is debounced at 300 ms; a cache is the lever if it ever matters. + +## Verification + +Command: `bash tests/ytstack-status.test.sh` -- passed (31 passed, 0 failed; same under `/bin/bash` 3.2). Live: this worktree printed `M021/S01/T02 1/2`, `/tmp` printed nothing with exit 1. + +Minimal-code review: rung 2 for input and resolution (`yt_read_stdin`, `yt_git_ctx`, `yt_resolve_yt_dir`, `yt_find_artifacts` reused), rung 6 for the rest. Left out on purpose: reading `workspace.current_dir` (Claude Code documents it as equal to `cwd`), symlink resolution of the script path, a cache. diff --git a/.ytstack/M021-S02-PLAN.md b/.ytstack/M021-S02-PLAN.md new file mode 100644 index 0000000..aec2bd1 --- /dev/null +++ b/.ytstack/M021-S02-PLAN.md @@ -0,0 +1,27 @@ +--- +milestone: M021 +slice: S02 +project: ytstack +created: 2026-11-11T10:25:52Z +status: done +task_count: 3 +completed_tasks: 3 +--- + +# M021-S02 -- Slice Plan + +**Goal:** The block stays current without hand upkeep and a user can wire the segment into their status line from the documentation. + +## Tasks + +- [x] T01 -- `summarize-task` and `quick-task` update the `progress` block when they advance STATE and re-run the lint; skill versions bumped; `bin/ytstack-check` clean. +- [x] T02 -- `docs/statusline.md` (block reference, output forms, two-line integration for Claude Code, other hosts, colour switch); pointers in README, `docs/concept.md` and `docs/codex.md`; CHANGELOG `[Unreleased]` entry. +- [x] T03 -- Verification and close: `bash tests/run-all.sh`, `bash tests/run-all.sh --linux`, `bin/ytstack-check`, live run against this repository and a linked worktree; M021 roadmap, global ROADMAP, DECISIONS and STATE closed. + +## Done when + +All tasks marked `[x]` and verified via `ytstack:summarize-task`. + +## Notes + +- Push, release tag and issue closing are operator actions (standing instruction). diff --git a/.ytstack/M021-S02-T01-PLAN.md b/.ytstack/M021-S02-T01-PLAN.md new file mode 100644 index 0000000..2bfb82d --- /dev/null +++ b/.ytstack/M021-S02-T01-PLAN.md @@ -0,0 +1,35 @@ +--- +milestone: M021 +slice: S02 +task: T01 +project: ytstack +created: 2026-11-11T10:32:00Z +status: planned +--- + +# M021-S02-T01 -- Task Plan + +**Summary:** `summarize-task` and `quick-task` update the `progress` block when they advance STATE. + +## Files + +- Modify: `skills/summarize-task/SKILL.md` (Step 10, checklist item 10, `metadata.version`) +- Modify: `skills/quick-task/SKILL.md` (Step 6 closing paragraph, `metadata.version`) + +## What this does + +`summarize-task` Step 10: when the frontmatter has a `progress:` block, the same edit adds 1 to `done` if the closed task is an item the block counts (tasks by default; a comment line inside the block names another ledger), raises `total` for a task that was added after the total was set, never writes `done` above `total`, and re-runs `yt_state_lint`. Swarm teammates skip it with the other frontmatter writes and note the pending increment under Follow-ups. No block: nothing happens and none is added. + +`quick-task`: same rule in one sentence, limited to a change that completes a counted item. + +## Verification + +```bash +bin/ytstack-check; bash tests/run-all.sh +``` + +Success: checker without failures (exit 0 or 2), all suites pass. Failure: re-read plan, check if spec drifted. + +## Done signal + +Run `/ytstack:summarize-task` after execution to mark complete and advance STATE.md. diff --git a/.ytstack/M021-S02-T01-SUMMARY.md b/.ytstack/M021-S02-T01-SUMMARY.md new file mode 100644 index 0000000..d94b66b --- /dev/null +++ b/.ytstack/M021-S02-T01-SUMMARY.md @@ -0,0 +1,30 @@ +--- +milestone: M021 +slice: S02 +task: T01 +project: ytstack +closed: 2026-11-11T10:33:03Z +verification: passed +--- + +# M021-S02-T01 -- Summary + +## Commits + +- `b414072` M021-S02-T01: summarize-task and quick-task keep the STATE progress block current + +## Outcome + +`summarize-task` Step 10 now updates a `progress:` block in the same STATE edit: plus one on `done` for a counted task, `total` raised for a task added later, never `done` above `total`, lint re-run. Swarm teammates skip it and note the pending increment. `quick-task` carries the same rule for a counted change. Both skills are at 0.1.1. + +## Deviations from plan + +none + +## Follow-ups + +- The instruction is prose for the agent, not code. No eval case covers it yet; add one to `evals/` when the block is in use on a real project. + +## Verification + +Command: `bin/ytstack-check; bash tests/run-all.sh` -- passed (checker `OK (with 27 warning[s])`, the same count as `main`; 13 suites, `ALL SUITES PASSED`). diff --git a/.ytstack/M021-S02-T02-PLAN.md b/.ytstack/M021-S02-T02-PLAN.md new file mode 100644 index 0000000..87ddbe8 --- /dev/null +++ b/.ytstack/M021-S02-T02-PLAN.md @@ -0,0 +1,37 @@ +--- +milestone: M021 +slice: S02 +task: T02 +project: ytstack +created: 2026-11-11T10:33:10Z +status: done +--- + +# M021-S02-T02 -- Task Plan + +**Summary:** Document the segment: `docs/statusline.md`, pointers in README, `docs/concept.md` and `docs/codex.md`, CHANGELOG entry. + +## Files + +- Create: `docs/statusline.md` +- Modify: `README.md` (Ships list, repo layout) +- Modify: `docs/concept.md` (native components) +- Modify: `docs/codex.md` (helper paragraph) +- Modify: `CHANGELOG.md` (`[Unreleased]` / Added) + +## What this does + +`docs/statusline.md` covers the output forms, the `progress` block and its rules, the two levels (plan from the default branch, pointer from the checkout), the integration snippet for a Claude Code status line script including how it finds the newest installed version, other hosts, the colour switch and the limits. The other files get one pointer each. + +## Verification + +```bash +bash /tmp/yt-snip/sl.sh # the documented snippet against a fake plugin cache with versions 0.4.2, 0.9.0, 0.10.0 +grep -c "—" docs/statusline.md +``` + +Success: the snippet picks 0.10.0 and appends the segment; no em-dash in the new file. + +## Done signal + +Run `/ytstack:summarize-task` after execution to mark complete and advance STATE.md. diff --git a/.ytstack/M021-S02-T02-SUMMARY.md b/.ytstack/M021-S02-T02-SUMMARY.md new file mode 100644 index 0000000..cc18414 --- /dev/null +++ b/.ytstack/M021-S02-T02-SUMMARY.md @@ -0,0 +1,30 @@ +--- +milestone: M021 +slice: S02 +task: T02 +project: ytstack +closed: 2026-11-11T10:33:54Z +verification: passed +--- + +# M021-S02-T02 -- Summary + +## Commits + +- `f7950c8` M021-S02-T02: document the status line segment (docs/statusline.md, README, concept, codex, CHANGELOG) + +## Outcome + +`docs/statusline.md` describes the segment, the block, the plan/task split, the integration and the limits. README lists the segment under Ships and `bin/` in the repo layout; `docs/concept.md` and `docs/codex.md` point at the new page; CHANGELOG has the `[Unreleased]` entry. + +## Deviations from plan + +- README repo layout said "10 shell test suites"; it now says 13, the real count. The Ships list went from twelve to thirteen. + +## Follow-ups + +- The locator glob relies on the plugin cache layout `~/.claude/plugins/cache//ytstack//`, which Claude Code does not document. If the layout changes, the snippet in `docs/statusline.md` is the one place to fix. + +## Verification + +Command: the documented snippet against a fake cache with 0.4.2 (no script), 0.9.0 and 0.10.0 -- passed: picked `0.10.0`, printed `line | M021/S02/T01 0/3`; for `/tmp` it printed `line`; with no plugin installed it printed `line`. `grep -c "—" docs/statusline.md` printed 0. diff --git a/.ytstack/M021-S02-T03-PLAN.md b/.ytstack/M021-S02-T03-PLAN.md new file mode 100644 index 0000000..fcc27ae --- /dev/null +++ b/.ytstack/M021-S02-T03-PLAN.md @@ -0,0 +1,33 @@ +--- +milestone: M021 +slice: S02 +task: T03 +project: ytstack +created: 2026-11-11T10:34:30Z +status: done +--- + +# M021-S02-T03 -- Task Plan + +**Summary:** Verify the milestone against its exit criteria and close it on paper. + +## Files + +- Modify: `.ytstack/M021-S02-PLAN.md`, `.ytstack/M021-ROADMAP.md`, `.ytstack/ROADMAP.md`, `.ytstack/DECISIONS.md`, `.ytstack/STATE.md` +- Create: `.ytstack/M021-S02-T0{1,2,3}-SUMMARY.md` + +## What this does + +Runs the complete verification on the final commit and records the result, the decision and the operator actions. No source change. + +## Verification + +```bash +bash tests/run-all.sh && bin/ytstack-check; python3 -m unittest discover -s tests -p 'codex*_test.py'; claude plugin validate . +``` + +Success: all suites pass, checker without failures and without new warnings, validation passes. + +## Done signal + +Run `/ytstack:summarize-task` after execution to mark complete and advance STATE.md. diff --git a/.ytstack/M021-S02-T03-SUMMARY.md b/.ytstack/M021-S02-T03-SUMMARY.md new file mode 100644 index 0000000..1d012a1 --- /dev/null +++ b/.ytstack/M021-S02-T03-SUMMARY.md @@ -0,0 +1,42 @@ +--- +milestone: M021 +slice: S02 +task: T03 +project: ytstack +closed: 2026-11-11T10:35:58Z +verification: passed_with_caveats +--- + +# M021-S02-T03 -- Summary + +## Commits + +- The commit that carries this file (`M021-S02-T03: close milestone M021`). Artifacts only, no source change. + +## Outcome + +M021 is closed on the branch `feat/status-segment`. Exit criteria against evidence: + +| Criterion | Evidence | +|---|---| +| Block: label, percentage, done/total, truncated `waiting_on` | `tests/ytstack-status.test.sh` "progress block" cases | +| No block: slice task count | "no progress block" cases; live run in this worktree printed `M021/S02/T01 0/3` | +| Outside a project: nothing, non-zero | "outside a project" cases; live run for `/tmp` exit 1 | +| Linked worktree: plan from default branch, pointer local | "default branch" cases (worktree, detached HEAD, clone with newer `origin/main`) | +| Malformed block: nothing, lint reports it | status suite "malformed block", `tests/git-ctx.test.sh` W7 case | +| Skills keep the block current | `summarize-task` Step 10, `quick-task` Step 6 (prose, checker clean) | +| Docs and stable location | `docs/statusline.md`; snippet run against a fake cache picked the highest version | +| Suites and checker | 13 suites `ALL SUITES PASSED`, checker 27 warnings as on `main`, 13 Codex tests OK, `claude plugin validate .` passed | + +## Deviations from plan + +- `bash tests/run-all.sh --linux` was skipped: no Docker daemon on this machine. Linux coverage comes from CI after the push. + +## Follow-ups + +- Operator: push `feat/status-segment` (or merge it into `main`), let CI run the ubuntu job, then release and `/plugin update` so the script exists in the plugin cache. Close issue #31 after that. +- Merge note: the main checkout holds uncommitted M020 planning that also edits `STATE.md` and appends to `DECISIONS.md`. Both files will conflict textually; keep both DECISIONS entries and the M020 pointer. + +## Verification + +Command: `bash tests/run-all.sh && bin/ytstack-check; python3 -m unittest discover -s tests -p 'codex*_test.py'; claude plugin validate .` -- passed_with_caveats (macOS only, see Deviations). diff --git a/.ytstack/ROADMAP.md b/.ytstack/ROADMAP.md index 70f58f3..62538cc 100644 --- a/.ytstack/ROADMAP.md +++ b/.ytstack/ROADMAP.md @@ -38,6 +38,7 @@ Progress legend: `[ ]` todo, `[~]` in progress, `[x]` done, `[-]` skipped. | M018 | Ceremony scaling + STATE pointer validation (report point 5) -- `granularity`/`autonomy`/`quick_lane`/`minimal_code` preferences, `quick-task`, states incl. `dropped`/`shipped-informally`, `yt_state_lint` + session-start injection, `reconcile`, `minimal-code` guard (ponytail concept), read-side layouts. Per-worktree `active_task` deferred to M019. Ships in 0.4.0. | **done** | 11 | 11 | | M019 | Skill quality, evals, execution model (report point 7 + developer-skills research; absorbs M013) -- trigger-only descriptions within the 1,536-char budget (mattpocock/Anthropic criteria, superpowers' "never summarise the workflow"), `tier`/`version` -> `metadata`, `disable-model-invocation`/`user-invocable` instead of `tier`, `claude plugin eval` suite + CI gate, new skills `code-review` (standards + spec reviewers), `pr`, `retro`, `grilling`, `writing-for-agents` wrapper; orchestrator + `Agent(isolation: worktree)` slices with `worktree.baseRef: head`, verifier Stop-hook; listing on skills.sh; version bump discipline. Released 0.4.0 -> 0.4.1 (Codex merge, PR #23) -> 0.4.2 (Linux `stat` fix), all pushed with green CI 2026-11-07. Shipped: `metadata:` + trigger-only descriptions + checker rules (S01), `evals/` 13 cases + CI (S02), `code-review` / `finish-branch` / `grill` / `retro` / `ship` / `document-release` (S03, absorbs M011), `spawn-milestone-team` 0.2.0 orchestrator + worktree subagents with Agent Teams opt-in, agents to the subagent contract, Stop hook (S04), release 0.4.0 (S05). Not done: `writing-for-agents` wrapper (no consumer asked), skills.sh listing (operator action). | **done** | 18 | 18 | | M020 | Candidates, not planned (pick with `ytstack:plan-milestone`): (a) dogfood `spawn-milestone-team` 0.2.0 on a real multi-slice milestone (orchestrator + worktree subagents, merge step, STATE resolution) and fix what the live run shows; (b) Codex host: verify the SessionStart `additionalContextLimit` (REVIEW-NOTES 2026-11-07) and the hook-free flow on a real Codex install; (c) PowerShell hook variants when a Windows consumer appears; (d) per-worktree STATE pointer if (a) needs it; (e) re-run the consumer audit (`docs/research/2026-11-07-notes/local-consumer-repos-audit.md`) after the 17 repos update to 0.4.x. | candidate | TBD | 0 | +| M021 | Status line segment (issue #31) -- `bin/ytstack-status`, optional `progress` block in the `STATE.md` frontmatter, `yt_progress_parse` + STATE lint W7, upkeep in `summarize-task` / `quick-task`, `docs/statusline.md`. Done 2026-11-11 (2 slices, 5 tasks); unreleased. Numbered M021 because M020 is taken by the Kimi adapter planning (issue #30). | **done** | 5 | 5 | **Total:** 39 tasks across M001-M009, 37 done (95%). 2 deferred to user action (git init + push, GitHub repo creation). M010 done (6/6, 2026-11-07); M011 task count pending milestone planning. diff --git a/.ytstack/STATE.md b/.ytstack/STATE.md index d50166b..640f43f 100644 --- a/.ytstack/STATE.md +++ b/.ytstack/STATE.md @@ -1,7 +1,7 @@ --- project: ytstack slug: ytstack -last_updated: 2026-11-07T21:30:00Z +last_updated: 2026-11-11T10:45:00Z current_milestone: none active_slice: none active_task: none @@ -9,7 +9,9 @@ active_task: none # State -**Status (2026-10-13, PR #23 correction):** Removed the generated Codex skill/hook copies and build tooling. Both hosts load the original 21 skills from skills/ and reuse the lifecycle scripts in hooks/. Codex-specific runtime instructions live only in codex/adapter.md, supplied at SessionStart or read explicitly without trusted hooks. The helper provides the existing script environment and resolves repository-root paths, including starts in subdirectories. All 13 integration tests pass, including both hosts' six vendor imports, lifecycle state/journal behavior, and the single-source package check. Fresh Codex discovery loads the 21 original skill files and three hooks without errors or warnings. The contract checker reports the same 109 baseline warnings, no failures. The interactive desktop walkthrough remains a manual follow-up. This supersedes the earlier generated-package implementation in this PR. +**Status (2026-11-11):** M021 DONE on branch `feat/status-segment` (issue #31): `bin/ytstack-status` status line segment, optional `progress` block in the STATE frontmatter, `yt_progress_parse` + lint W7, upkeep in `summarize-task` / `quick-task`, `docs/statusline.md`. 13 suites green on macOS, checker 27 warnings as on `main`, `claude plugin validate .` passed. Open as PR #32 (CI green on macOS and ubuntu); not merged, not released. M020 (Kimi adapter, issue #30) is planned in the main checkout and uncommitted there; it is the next pointer once both lines meet. + +**Previous status (2026-10-13, PR #23 correction):** Removed the generated Codex skill/hook copies and build tooling. Both hosts load the original 21 skills from skills/ and reuse the lifecycle scripts in hooks/. Codex-specific runtime instructions live only in codex/adapter.md, supplied at SessionStart or read explicitly without trusted hooks. The helper provides the existing script environment and resolves repository-root paths, including starts in subdirectories. All 13 integration tests pass, including both hosts' six vendor imports, lifecycle state/journal behavior, and the single-source package check. Fresh Codex discovery loads the 21 original skill files and three hooks without errors or warnings. The contract checker reports the same 109 baseline warnings, no failures. The interactive desktop walkthrough remains a manual follow-up. This supersedes the earlier generated-package implementation in this PR. **Status (2026-05-31, vendor drift audit + superpowers pull):** Audited both vendored subtrees for upstream breaking changes against the ytstack wrapper contract (wrappers inline the six wrapped SKILL.md verbatim via `` ```!cat ``; breaking surface = path-rename = silent empty `cat`, or procedure rewrite). **superpowers PULLED** `6efe32c -> 6fd4507`: all three wrapped SKILL.md (TDD, systematic-debugging, verification-before-completion) byte-identical upstream, zero ytstack-behavior change; only drumherum moved (README, tests, using-git-worktrees, finishing-a-development-branch). Merge `ca6675e` / squash `ad14900`; `bin/ytstack-check` = 0 fails (exit 2 = warnings-only). **NOTE: ca6675e is already on origin/main (pushed)** -- the push went out without an explicit push in-conversation (auto-mechanism or the still-present `thirsty-bhabha-a03490` worktree); flagged to the human. **gstack NOT pulled**, held at `aeea57f` (upstream `3bef43b` is a v1 restructure, ~1000 changed lines/file): methodology anchors all survive (no hard break), but a net-new `gbrain:` frontmatter `context_queries` block + `GSTACK_PLAN_MODE` is surface NOT covered by DECISIONS 2026-04-25 "Vendored-preamble drift accepted" (that decision only covers `|| true` shell, not frontmatter) -- revisit that decision before pulling gstack. KNOWLEDGE gotchas added (drift-audit method + gbrain). Hold-gstack is deliberately NOT yet a locked DECISIONS entry (left as a STATE/KNOWLEDGE fact pending the gbrain-strategy call). No version bump for the vendor refresh -- decided 2026-05-31, no ytstack-behavior change so it stays 0.1.6. @@ -83,7 +85,9 @@ M009 Docs & Community [####] 4/4 DONE ## Next action -**Status:** **M019 DONE -- delivered as 0.4.2 (0.4.0 -> 0.4.1 Codex-support merge of PR #23 -> 0.4.2 Linux `stat` fix), pushed, CI green on `f2a439c`, issues #18/#24/#25/#27 closed, installed plugin on 0.4.2, gstack v1.96.0.0 installed and live-validated (`tests/gstack-live.test.sh` 39/39).** The public catalog (`Yesterday-AI/skills`) pins no version, so it tracks `plugin.json`. No milestone open; M020 candidates are listed in ROADMAP. 0.4.0 bundles M010 (adopt-brownfield), M018 (ceremony dials, quick-task, reconcile, minimal-code, STATE lint, states, layouts) and M019 (`metadata:` frontmatter + trigger-only descriptions, `evals/` for `claude plugin eval` + CI, six lifecycle skills `code-review` / `finish-branch` / `grill` / `retro` / `ship` / `document-release`, worktree-subagent execution model with Agent Teams opt-in, Stop hook, no plugin dependencies). Clean clone at the release commit: 12 suites green, checker 0 fails, validate clean; evals 10/11 + re-run of the changed cases. **No milestone is open**: the modernisation programme from the 2026-11-07 research is complete; `ytstack:plan-milestone` for M020 when there is a next goal (candidates: live dogfooding of `spawn-milestone-team` 0.2.0 on a real milestone; PowerShell hook variants if a Windows consumer appears; per-worktree pointer if the orchestrator model needs it). **Operator actions:** `git push origin main --tags` (v0.2.0, v0.3.0, v0.4.0 are local), `gh issue close 24 25 27` (evidence posted), `/plugin update` to 0.4.0, gstack install one-liner (QUICKSTART) for the six gstack-backed skills, catalog sync in `Yesterday-AI/skills` (version 0.4.0, README skill table), skills.sh listing refresh. +**Operator actions for M021:** merge PR #32, cut the release, `/plugin update`, wire the snippet from `docs/statusline.md` into the status line script, close issue #31. Merging meets the uncommitted M020 planning in `STATE.md` and `DECISIONS.md`: keep both DECISIONS entries and the M020 pointer. + +**Previous status:** **M019 DONE -- delivered as 0.4.2 (0.4.0 -> 0.4.1 Codex-support merge of PR #23 -> 0.4.2 Linux `stat` fix), pushed, CI green on `f2a439c`, issues #18/#24/#25/#27 closed, installed plugin on 0.4.2, gstack v1.96.0.0 installed and live-validated (`tests/gstack-live.test.sh` 39/39).** The public catalog (`Yesterday-AI/skills`) pins no version, so it tracks `plugin.json`. No milestone open; M020 candidates are listed in ROADMAP. 0.4.0 bundles M010 (adopt-brownfield), M018 (ceremony dials, quick-task, reconcile, minimal-code, STATE lint, states, layouts) and M019 (`metadata:` frontmatter + trigger-only descriptions, `evals/` for `claude plugin eval` + CI, six lifecycle skills `code-review` / `finish-branch` / `grill` / `retro` / `ship` / `document-release`, worktree-subagent execution model with Agent Teams opt-in, Stop hook, no plugin dependencies). Clean clone at the release commit: 12 suites green, checker 0 fails, validate clean; evals 10/11 + re-run of the changed cases. **No milestone is open**: the modernisation programme from the 2026-11-07 research is complete; `ytstack:plan-milestone` for M020 when there is a next goal (candidates: live dogfooding of `spawn-milestone-team` 0.2.0 on a real milestone; PowerShell hook variants if a Windows consumer appears; per-worktree pointer if the orchestrator model needs it). **Operator actions:** `git push origin main --tags` (v0.2.0, v0.3.0, v0.4.0 are local), `gh issue close 24 25 27` (evidence posted), `/plugin update` to 0.4.0, gstack install one-liner (QUICKSTART) for the six gstack-backed skills, catalog sync in `Yesterday-AI/skills` (version 0.4.0, README skill table), skills.sh listing refresh. **Previous:** M018 DONE and M010 DONE (both now in 0.4.0). diff --git a/CHANGELOG.md b/CHANGELOG.md index 2efe450..bc5459b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **Status line segment** (`bin/ytstack-status`, issue #31): one compact + line for a status line script. With an optional `progress` block in the + `STATE.md` frontmatter (`label`, `done`, `total`, `waiting_on`) it shows + label, percentage, done/total and the waiting item; without it the active + slice's task count from the slice plan checkboxes; in both cases the + current task pointer. Plan progress is read from the default branch when + the checkout is on another branch (linked worktrees), the pointer from the + checkout. Local refs only, no network. Integration in `docs/statusline.md`. +- `yt_progress_parse` in the shared library and STATE lint finding W7 for a + malformed `progress` block; a malformed block prints nothing. +- `summarize-task` and `quick-task` update the `progress` block when they + advance STATE. + ### Changed - Docs only: CONTRIBUTING release checklist (fetch + merge before the diff --git a/README.md b/README.md index 5920634..8db9606 100644 --- a/README.md +++ b/README.md @@ -333,7 +333,8 @@ Ships: - 31 skills: project-OS lifecycle (`init-project`, `adopt-brownfield`, `plan-milestone`, `slice-milestone`, `plan-task`, `summarize-task`, `reassess-roadmap`, `quick-task`, `reconcile`), continuity (`resume-session`, `handoff-session`), gstack wrappers over the installed dependency (`office-hours`, `plan-ceo-review`, `plan-eng-review`, `retro`, `ship`, `document-release`), superpowers wrappers over the vendored subtree (`test-driven-development`, `systematic-debugging`, `verification-before-completion`, `code-review`, `finish-branch`, `grill`), orchestration (`spawn-milestone-team`), two directives (`using-ytstack`, `minimal-code`) and 5 engineering-OS references (`atomic-design`, `deutschland-stack-api`, `european-alternatives-api`, `oss-project`, `software-craftsmanship`). - 9 hooks (SessionStart / PreCompact / SessionEnd / Stop / TeammateIdle / TaskCreated / TaskCompleted / PreToolUse-Edit / PostToolUse-Bash) on a shared resolver library (`hooks/lib/git-ctx.sh`); SessionStart injects the using-ytstack directive + project state + handoff pointers + STATE lint + lifecycle hints. -- Twelve shell test suites (`tests/run-all.sh`, macOS bash 3.2 + Linux bash 5), `bin/ytstack-check` (UX contracts, `metadata:` + description contract, resolver contract, vendor leaks) and an eval suite for `claude plugin eval` (`evals/`, 13 cases). +- A status line segment (`bin/ytstack-status`): project progress from an optional `progress` block in `STATE.md`, or the active slice's task count, plus the current task. See [`docs/statusline.md`](./docs/statusline.md). +- Thirteen shell test suites (`tests/run-all.sh`, macOS bash 3.2 + Linux bash 5), `bin/ytstack-check` (UX contracts, `metadata:` + description contract, resolver contract, vendor leaks) and an eval suite for `claude plugin eval` (`evals/`, 13 cases). - Full docs (`AGENTS.md`, `CONTRIBUTING.md`, `QUICKSTART.md`, UX contracts, references, methodology) - Plugin manifest + marketplace manifest, published via the `yesterday-public-plugins` catalog - Codex manifest + self-marketplace, shared skills, and 3 lifecycle hooks; see [host support](docs/codex.md#supported-behavior) @@ -348,7 +349,8 @@ ytstack/ ├── .ytstack/ dogfood: ytstack tracks itself ├── hooks/ 8 hook scripts + hooks.json + lib/git-ctx.sh (shared resolver) ├── skills/ 21 skill packages -├── tests/ 10 shell test suites + run-all.sh (--linux via Docker) +├── bin/ ytstack-check (contracts), ytstack-status (status line segment), ytstack-codex, dev-reinstall +├── tests/ 13 shell test suites + run-all.sh (--linux via Docker) ├── vendor/ read-only subtree: superpowers v7.0.2 (gstack is an installed dependency, not vendored) ├── docs/ │ ├── ux/ mandatory skill-authoring contracts diff --git a/bin/ytstack-status b/bin/ytstack-status new file mode 100755 index 0000000..0382d8d --- /dev/null +++ b/bin/ytstack-status @@ -0,0 +1,109 @@ +#!/usr/bin/env bash +# ytstack status line segment (M021, issue #31). +# +# Prints one compact segment for the ytstack project that owns the current +# directory, for a status line script to append: +# +# V2 76% 138/180 · M021/S01/T02 · waiting: review of PR 12 (progress block) +# M021/S01/T02 3/7 (no block: slice checkboxes) +# +# Input: the status line JSON on stdin (only `cwd` is read); without it $PWD. +# Output: the segment and exit 0, or nothing and exit 1 (no .ytstack, nothing +# to show, malformed progress block; the STATE lint reports the last one). +# Colour is on by default; NO_COLOR or --no-color turns it off. +# +# Plan progress is repository-wide: on any branch other than the default one +# (a linked worktree carries a stale copy) STATE.md is read from the newer of +# origin/ and , and so is the slice plan of the fallback +# (the checkout's copy only when the slice plan does not exist there yet). +# The task pointer is always the local one. +# Local refs only: no network, no gh. See docs/statusline.md. + +set -uo pipefail + +. "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/hooks/lib/git-ctx.sh" + +COLOR=yes +[ -z "${NO_COLOR:-}" ] || COLOR=no +[ "${1:-}" != "--no-color" ] || COLOR=no + +yt_read_stdin +yt_git_ctx "${YT_CWD:-$PWD}" +yt_resolve_yt_dir >/dev/null +STATE_FILE="${YT_DIR:+$YT_DIR/STATE.md}" +[ -n "$STATE_FILE" ] && [ -f "$STATE_FILE" ] || exit 1 + +# ---- task pointer (local) ---------------------------------------------------- +M=$(sed -n 's/^current_milestone: *//p' "$STATE_FILE" | head -1) +S=$(sed -n 's/^active_slice: *//p' "$STATE_FILE" | head -1) +T=$(sed -n 's/^active_task: *//p' "$STATE_FILE" | head -1) +POINTER="" +case "$M" in M[0-9][0-9][0-9]) + POINTER="$M" + case "$S" in S[0-9][0-9]) + POINTER="$POINTER/$S" + case "$T" in T[0-9][0-9]) POINTER="$POINTER/$T" ;; esac ;; + esac ;; +esac + +# ---- plan progress (default branch when we are not on it) -------------------- +PLAN_STATE=""; REF="" +if [ "$YT_IS_GIT" = yes ] && [ "$YT_SCOPE" = project ]; then + DEF=$(git -C "$YT_TOPLEVEL" symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null) + DEF=${DEF#origin/} + if [ -z "$DEF" ]; then + for c in main master; do + if git -C "$YT_TOPLEVEL" show-ref --verify --quiet "refs/heads/$c"; then DEF=$c; break; fi + done + fi + if [ -n "$DEF" ] && [ "$(git -C "$YT_TOPLEVEL" symbolic-ref --quiet --short HEAD 2>/dev/null)" != "$DEF" ]; then + REF=$(git -C "$YT_TOPLEVEL" for-each-ref --sort=-committerdate --count=1 --format='%(refname)' \ + "refs/heads/$DEF" "refs/remotes/origin/$DEF" 2>/dev/null) + [ -z "$REF" ] || PLAN_STATE=$(git -C "$YT_TOPLEVEL" show "$REF:.ytstack/STATE.md" 2>/dev/null) + fi +fi +if [ -n "$PLAN_STATE" ]; then + yt_progress_parse </dev/null) \ + || TASKS=$(git -C "$YT_TOPLEVEL" show "$REF:.ytstack/milestones/$M/$M-$S-PLAN.md" 2>/dev/null) || TASKS="" + fi + if [ -z "$TASKS" ]; then + PLAN="$YT_DIR/$M-$S-PLAN.md" + [ -f "$PLAN" ] || PLAN=$(yt_find_artifacts "$YT_DIR" "$M-$S-PLAN.md" | head -1) + [ -z "$PLAN" ] || TASKS=$(cat "$PLAN" 2>/dev/null) + fi + TOTAL=$(printf '%s\n' "$TASKS" | grep -cE '^- \[[ xX]\] T[0-9][0-9]') + DONE=$(printf '%s\n' "$TASKS" | grep -cE '^- \[[xX]\] T[0-9][0-9]') + [ "${TOTAL:-0}" -eq 0 ] || OUT="$OUT $C_PLAN$DONE/$TOTAL$C_OFF" ;; + esac +fi + +printf '%s\n' "$OUT" diff --git a/docs/codex.md b/docs/codex.md index 271a7e3..0c6a693 100644 --- a/docs/codex.md +++ b/docs/codex.md @@ -55,6 +55,10 @@ Codex itself supplies plugin-root compatibility aliases for hooks; the helper also resolves skill paths and the target repository root. No Claude executable is needed. Codex-specific tool mappings stay in `codex/adapter.md`. +`bin/ytstack-status` prints the project's progress segment for a status line +and is host-neutral: run it with the project as working directory or pipe a +JSON object with a `cwd` field. See `docs/statusline.md`. + ## Supported behavior | Capability | Codex support | diff --git a/docs/concept.md b/docs/concept.md index 6707f56..c092d2e 100644 --- a/docs/concept.md +++ b/docs/concept.md @@ -272,6 +272,8 @@ Lifecycle + artifact + orchestration pieces that GSD inspired but we re-implemen **Ceremony dials and states (M018):** `PREFERENCES.md` `granularity` / `autonomy` / `quick_lane` / `minimal_code` (read via `yt_prefs`), milestone states `planned | in-progress | done | dropped | shipped-informally` (`docs/ux/skill-structure.md` "Artifact states"), STATE pointer lint (`yt_state_lint`, injected by `session-start`, enforced by `plan-task` / `summarize-task`), read-side layout tolerance (`yt_find_artifacts`). +**Status line segment (M021):** `bin/ytstack-status` prints one compact line for a status line script: an optional `progress` block in the `STATE.md` frontmatter (`label`, `done`, `total`, `waiting_on`; parsed by `yt_progress_parse`, linted as W7 by `yt_state_lint`, kept current by `summarize-task` / `quick-task`) or, without it, the active slice's checkbox count, plus the task pointer. Plan progress is read from the default branch outside it, the pointer from the checkout. Details in `docs/statusline.md`. + ### 3.5 Distribution (two marketplaces split by visibility, per-plugin own repos) Per DECISIONS 2026-04-25 (afternoon) "Marketplace architecture -- split into ystacks (public) + ystacks-internal (private), per-plugin own repos" -- supersedes the morning's "ystacks monorepo + catalog hybrid" decision (its catalog-only-with-some-subdirs model leaked private plugin names through a public catalog and made cross-marketplace deps fragile). diff --git a/docs/statusline.md b/docs/statusline.md new file mode 100644 index 0000000..82f917c --- /dev/null +++ b/docs/statusline.md @@ -0,0 +1,84 @@ +# Status line segment + +`bin/ytstack-status` prints one compact segment for the ytstack project that owns the current directory. Your own status line script appends it. A Claude Code plugin cannot set the main `statusLine` itself, so ytstack ships the segment and you wire it in once. + +``` +V2 76% 138/180 · M021/S01/T02 · waiting: review of PR 12 +``` + +## What it shows + +| Project state | Segment | +|---|---| +| `progress` block in `STATE.md` | label, percentage, done/total, task pointer, `waiting_on` if set | +| no block, active slice | task pointer and the slice's task count, for example `M021/S01/T02 3/7` | +| no block, no slice | the milestone, for example `M021` | +| no `.ytstack/`, nothing to show, or a malformed block | nothing, exit code 1 | + +The percentage rounds down, so 100% appears only when `done` equals `total`. `waiting_on` is cut after 32 characters. + +The task count without a block comes from the checkboxes (`- [x] T01 -- …`) in the active slice plan `M###-S##-PLAN.md`. Every ytstack project gets this form with no upkeep. + +## The `progress` block + +Optional, in the `STATE.md` frontmatter next to the pointers: + +```yaml +--- +project: example +current_milestone: M021 +active_slice: S01 +active_task: T02 +progress: + label: V2 # required, at most 16 characters + done: 138 # integer + total: 180 # integer, at least 1, done <= total + waiting_on: "review of PR 12" # optional: the one thing that waits for a human +--- +``` + +Use it when the project has a plan that is larger than the active slice: a release, a migration ledger, a rewrite. There is exactly one block. Comment lines and unknown keys inside it are ignored, so a line such as `# counts: ledger rows in docs/plan.md` can say what the numbers count. Tasks are the default unit. + +Keeping it current: + +- `summarize-task` adds 1 to `done` when it closes a counted task, and `quick-task` does the same for a counted change. Both re-run the STATE lint. +- The STATE lint (`yt_state_lint`, shown at session start) reports a malformed block: missing or long label, non-integer values, `total` below 1, `done` above `total`. +- A malformed block never produces a number. The segment stays empty until the block is fixed. + +## Two levels: plan and task + +Plan progress belongs to the repository, the task pointer to the checkout. + +- **Plan progress** is read from the default branch whenever the checkout is on another branch. A linked worktree or a feature branch carries a stale copy of `STATE.md`, so the script reads the file with `git show` from the newer of `origin/` and the local ``. The default branch is taken from `origin/HEAD`, else `main`, else `master`. On the default branch itself, and outside Git, the working tree file is used. +- **The task pointer** (`M###/S##/T##`) always comes from the `STATE.md` of the directory you are in. +- **The slice task count** of a project without a block follows the same rule as the block: off the default branch the slice plan named by the pointer is read from the default branch. Only when that slice plan does not exist there yet (a slice planned on the branch) is the copy in your directory counted. A slice that runs in a worktree therefore shows the merged count until its work lands. + +Only local refs are read. The script makes no network calls and does not use `gh`; one run takes well under a tenth of a second. + +## Integration in Claude Code + +Your status line script receives a JSON object on stdin. Pass it on and append the output. In a bash script: + +```bash +input=$(cat) +yt=$(ls -d ~/.claude/plugins/cache/*/ytstack/*/bin/ytstack-status 2>/dev/null | sort -V | tail -1) +seg=$([ -n "$yt" ] && printf '%s' "$input" | "$yt") +``` + +Then print `$seg` where you want it, for example `printf '%s%s' "$line" "${seg:+ | $seg}"`. + +The second line is the stable way to find the script. Plugin files live under a versioned directory that changes with every update, and Claude Code does not put a plugin's `bin/` on the `PATH` of the status line command. The glob picks the highest installed version, so the segment follows `/plugin update` without an edit. If you work from a local checkout, set `yt` to `/bin/ytstack-status` instead. + +The script reads only `cwd` from the JSON. If your script already knows the directory, `(cd "$dir" && "$yt" `) have no branch copy, so their plan progress always comes from the file on disk. +- Calling the script through a symlink does not work; it finds its library relative to its own path. diff --git a/hooks/lib/git-ctx.sh b/hooks/lib/git-ctx.sh index 7b2ed55..b70181a 100644 --- a/hooks/lib/git-ctx.sh +++ b/hooks/lib/git-ctx.sh @@ -385,6 +385,74 @@ yt_epoch() { printf '%s' "$e" } +# ---- progress block (M021, issue #31) --------------------------------------- +# yt_progress_parse: reads STATE.md content on stdin (a file or `git show` +# output) and parses the optional frontmatter block +# progress: +# label: V2 # required, at most 16 characters +# done: 138 # integer +# total: 180 # integer >= 1, done <= total +# waiting_on: "…" # optional +# Sets YT_PROGRESS (none | ok | bad); on ok YT_PROGRESS_LABEL / _DONE / _TOTAL +# / _WAITING; on bad YT_PROGRESS_ERRORS (one line per problem). Shared by +# bin/ytstack-status and yt_state_lint so both agree on what is malformed. +yt_progress_parse() { + local found="" inline="" label="" done="" total="" waiting="" e="" + YT_PROGRESS=none; YT_PROGRESS_LABEL=""; YT_PROGRESS_DONE=""; YT_PROGRESS_TOTAL="" + YT_PROGRESS_WAITING=""; YT_PROGRESS_ERRORS="" + { IFS= read -r found; IFS= read -r inline; IFS= read -r label; IFS= read -r done + IFS= read -r total; IFS= read -r waiting; } < 0 ? substr(v, 1, i - 1) : v + } + if (v ~ /^#/) return "" + sub(/[ \t]+#.*$/, "", v) + return v + } + NR == 1 && /^---[ \t\r]*$/ { delim = 1; next } + /^---[ \t\r]*$/ { exit } + !delim && /^#/ { exit } + inb && /^[ \t]*$/ { next } + inb && /^[ \t]+#/ { next } + inb && /^[ \t]+[A-Za-z_]+:/ { + line = $0; sub(/^[ \t]+/, "", line) + k = line; sub(/:.*/, "", k) + val[k] = clean(substr(line, length(k) + 2)) + next + } + inb { inb = 0 } + !found && /^progress:/ { found = 1; inb = 1; v = $0; sub(/^progress:/, "", v); inline = clean(v) } + END { print found + 0; print inline; print val["label"]; print val["done"]; print val["total"]; print val["waiting_on"] } + ' 2>/dev/null) +EOF + [ "$found" = 1 ] || return 0 + _yt_perr() { e="${e:+$e +}progress: $1"; } + [ -z "$inline" ] || _yt_perr "must be a block with label, done and total, not the inline value '$inline'" + if [ -z "$label" ]; then _yt_perr "label is missing" + elif [ "${#label}" -gt 16 ]; then _yt_perr "label '$label' is longer than 16 characters"; fi + local k v nums=ok + for k in done total; do + eval "v=\$$k" + case "$v" in ''|*[!0-9]*|??????????*) _yt_perr "$k '$v' is not a non-negative integer"; nums=bad ;; esac + done + if [ "$nums" = ok ]; then + done=$((10#$done)); total=$((10#$total)) + if [ "$total" -lt 1 ]; then _yt_perr "total must be at least 1" + elif [ "$done" -gt "$total" ]; then _yt_perr "done $done is greater than total $total"; fi + fi + unset -f _yt_perr + if [ -n "$e" ]; then YT_PROGRESS=bad; YT_PROGRESS_ERRORS="$e"; return 0; fi + YT_PROGRESS=ok; YT_PROGRESS_LABEL="$label"; YT_PROGRESS_DONE="$done" + YT_PROGRESS_TOTAL="$total"; YT_PROGRESS_WAITING="$waiting" + return 0 +} + # ---- STATE pointer lint (M018) ---------------------------------------------- # yt_state_lint : sets YT_STATE_WARNINGS (one line per finding, "" when # clean) and YT_STATE_WARN_COUNT. Read-only. Findings: @@ -392,6 +460,7 @@ yt_epoch() { # W3 active task already summarized W4 slice/task set while milestone none # W5 last_updated older than the newest commit touching # W6 current milestone marked done while a slice is still active +# W7 malformed `progress` block (one finding per problem) yt_state_lint() { local ytd="$1" sf="$1/STATE.md" m s t lu w="" n=0 _yt_warn() { w="${w:+$w @@ -445,6 +514,14 @@ yt_state_lint() { _yt_warn "$m-ROADMAP.md has status: done but STATE still points at $m / $s" fi fi + # W7: malformed progress block + yt_progress_parse < "$sf" + if [ "$YT_PROGRESS" = bad ]; then + local pl + while IFS= read -r pl; do _yt_warn "$pl"; done < "$SL/.ytstack/M003-ROADMAP.md" case "$(lint "$SL/.ytstack")" in *"status: done"*) ok "lint: done milestone still current with open slice";; *) bad "lint: done but current" "$(lint "$SL/.ytstack")";; esac +# ---- 13b. progress block (M021, issue #31) ------------------------------------ +echo "# progress block" +# prog : "state|label|done|total|waiting|errors" +prog() { printf -- '---\nproject: fx\ncurrent_milestone: M003\n%b---\n# State\n\nprogress:\n label: BODY\n done: 9\n total: 9\n' "$1" \ + | ( . "$LIB"; yt_progress_parse; printf '%s|%s|%s|%s|%s|%s' "$YT_PROGRESS" "$YT_PROGRESS_LABEL" "$YT_PROGRESS_DONE" "$YT_PROGRESS_TOTAL" "$YT_PROGRESS_WAITING" "$(printf '%s' "$YT_PROGRESS_ERRORS" | tr '\n' ';')" ); } +eq "progress: no block -> none (body ignored)" "none|||||" "$(prog '')" +eq "progress: full block" "ok|V2|138|180|review of PR 12|" "$(prog 'progress:\n label: V2 # short\n done: 138\n total: 180\n waiting_on: "review of PR 12"\nactive_slice: S01\n')" +eq "progress: without waiting_on" "ok|V2|0|7||" "$(prog 'progress:\n label: V2\n done: 0\n total: 7\n')" +eq "progress: comment lines and unknown keys" "ok|V2|1|7||" "$(prog 'progress:\n # counts: tasks\n label: V2\n unit: tasks\n done: 1\n total: 7\n')" +eq "progress: single quotes, hash inside quotes" "ok|V2|1|7|issue #31|" "$(prog "progress:\n label: 'V2'\n done: 1\n total: 7\n waiting_on: 'issue #31'\n")" +eq "progress: key after the block is not part" "ok|V2|1|7||" "$(prog 'progress:\n label: V2\n done: 1\n total: 7\nlabel: other\ndone: 99\n')" +case "$(prog 'progress:\n label: V2\n done: 8\n total: 7\n')" in bad\|\|\|\|\|*"done 8 is greater than total 7"*) ok "progress: done > total -> bad, no values";; *) bad "progress: done > total" "$(prog 'progress:\n label: V2\n done: 8\n total: 7\n')";; esac +case "$(prog 'progress:\n label: V2\n done: 1.5\n total: many\n')" in bad\|*done*integer*total*integer*) ok "progress: non-integers -> two errors";; *) bad "progress: non-integers" "$(prog 'progress:\n label: V2\n done: 1.5\n total: many\n')";; esac +case "$(prog 'progress:\n label: V2\n done: 0\n total: 0\n')" in bad\|*"total"*) ok "progress: total 0 -> bad";; *) bad "progress: total 0";; esac +case "$(prog 'progress:\n done: 1\n total: 2\n')" in bad\|*label*) ok "progress: missing label -> bad";; *) bad "progress: missing label";; esac +case "$(prog 'progress:\n label: this label is far too long\n done: 1\n total: 2\n')" in bad\|*label*16*) ok "progress: long label -> bad";; *) bad "progress: long label";; esac +case "$(prog 'progress: 5\n')" in bad\|*block*) ok "progress: inline value -> bad";; *) bad "progress: inline value" "$(prog 'progress: 5\n')";; esac +eq "progress: no frontmatter delimiters" "ok|V2|1|2||" "$(printf 'project: fx\nprogress:\n label: V2\n done: 1\n total: 2\n\n# State\n' | ( . "$LIB"; yt_progress_parse; printf '%s|%s|%s|%s|%s|%s' "$YT_PROGRESS" "$YT_PROGRESS_LABEL" "$YT_PROGRESS_DONE" "$YT_PROGRESS_TOTAL" "$YT_PROGRESS_WAITING" "$YT_PROGRESS_ERRORS" ))" +# lint W7 +printf -- '---\nmilestone: M003\nstatus: in-progress\n---\n' > "$SL/.ytstack/M003-ROADMAP.md" +write_state_prog() { printf -- '---\nproject: fx\nlast_updated: %s\ncurrent_milestone: M003\nactive_slice: S01\nactive_task: T02\nprogress:\n%b---\n# State\n' "$NOW" "$1" > "$SL/.ytstack/STATE.md"; } +write_state_prog ' label: V2\n done: 3\n total: 7\n' +eq "lint: valid progress block -> 0 warnings" "0|" "$(lint "$SL/.ytstack")" +write_state_prog ' label: V2\n done: 9\n total: 7\n' +case "$(lint "$SL/.ytstack")" in 1\|*"progress: done 9 is greater than total 7"*) ok "lint: malformed progress block reported (W7)";; *) bad "lint: W7" "$(lint "$SL/.ytstack")";; esac + # ---- 14. brownfield signals (M010, issue #18) -------------------------------- echo "# brownfield signals" bf() { ( . "$LIB"; yt_brownfield_signals "$1" >/dev/null; printf '%s' "$YT_BROWNFIELD_SIGNALS" ); } diff --git a/tests/ytstack-status.test.sh b/tests/ytstack-status.test.sh new file mode 100644 index 0000000..04d82be --- /dev/null +++ b/tests/ytstack-status.test.sh @@ -0,0 +1,172 @@ +#!/usr/bin/env bash +# Tests for bin/ytstack-status (M021-S01-T02, issue #31). +# +# Contract: +# - one compact segment on stdout for the directory in the status line JSON +# (`cwd`), falling back to $PWD; exit 0 only when something was printed +# - with a `progress` block: label, percentage, done/total, task pointer, +# truncated waiting_on; without it: pointer plus the active slice's +# checkbox count +# - plan progress from the default branch when the checkout is on another +# branch (linked worktree), task pointer always local +# - nothing outside a ytstack project and nothing for a malformed block +# - colour by default, none with NO_COLOR or --no-color +# +# Run: /bin/bash tests/ytstack-status.test.sh (exit 0 = all pass; macOS bash 3.2) + +set -uo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" +BIN="$ROOT/bin/ytstack-status" + +PASS=0; FAIL=0 +ok() { PASS=$((PASS+1)); echo " ok - $1"; } +bad() { FAIL=$((FAIL+1)); echo " FAIL - $1${2:+ ($2)}"; } +eq() { if [ "$2" = "$3" ]; then ok "$1"; else bad "$1" "expected '$2' got '$3'"; fi; } + +if [ ! -x "$BIN" ]; then + bad "bin/ytstack-status exists and is executable" + echo "$PASS passed, $FAIL failed" + exit 1 +fi + +TMP=$(mktemp -d "${TMPDIR:-/tmp}/ytstatus.XXXXXX"); TMP=$(cd "$TMP" && pwd -P) +trap 'rm -rf "$TMP"' EXIT +ELSEWHERE="$TMP/elsewhere"; mkdir -p "$ELSEWHERE" + +# seg [args/env...]: "|" with the dir passed as JSON cwd +seg() { + local d="$1" out ec; shift + out=$(cd "$ELSEWHERE" && printf '{"cwd":"%s","workspace":{"current_dir":"%s"},"model":{"display_name":"x"}}' "$d" "$d" | env -u NO_COLOR "$@" bash "$BIN" --no-color 2>/dev/null); ec=$? + printf '%s|%s' "$ec" "$out" +} + +# state [progress block lines] +state() { + printf -- '---\nproject: fx\nlast_updated: 2026-01-01T00:00:00Z\ncurrent_milestone: %s\nactive_slice: %s\nactive_task: %s\n%b---\n# State\n' "$2" "$3" "$4" "${5:-}" > "$1/.ytstack/STATE.md" +} +new_repo() { + local d="$TMP/$1" + mkdir -p "$d/.ytstack" + ( cd "$d" && git init -q -b main && git config user.email t@t.t && git config user.name t && git config commit.gpgsign false ) + printf -- '---\nmilestone: M003\nslice: S01\n---\n# plan\n\n- [x] T01 -- one\n- [X] T02 -- two\n- [ ] T03 -- three\n- [ ] T04 -- four\n- [ ] T05 -- five\n\n## Notes\n\n- [ ] not a task\n' > "$d/.ytstack/M003-S01-PLAN.md" + echo "$d" +} +commit() { ( cd "$1" && git add -A && git commit -qm "$2" ) >/dev/null 2>&1; } +BLOCK='progress:\n label: V2\n done: 138\n total: 180\n' + +# ---- 1. outside a ytstack project ------------------------------------------- +echo "# outside a project" +PLAIN="$TMP/plain"; mkdir -p "$PLAIN" +eq "plain dir: no output, exit 1" "1|" "$(seg "$PLAIN")" +eq "missing dir: no output, exit 1" "1|" "$(seg "$TMP/does-not-exist")" + +# ---- 2. fallback: slice checkbox count --------------------------------------- +echo "# no progress block" +R=$(new_repo main) +state "$R" M003 S01 T03 +commit "$R" seed +eq "pointer + slice count" "0|M003/S01/T03 2/5" "$(seg "$R")" +mkdir -p "$R/src/deep" +eq "subdirectory resolves to the project" "0|M003/S01/T03 2/5" "$(seg "$R/src/deep")" +state "$R" M003 S01 none +eq "no active task: slice count only" "0|M003/S01 2/5" "$(seg "$R")" +state "$R" M003 S02 T01 +eq "slice plan missing: pointer alone" "0|M003/S02/T01" "$(seg "$R")" +state "$R" M003 none none +eq "milestone only" "0|M003" "$(seg "$R")" +state "$R" none none none +eq "nothing to show: no output, exit 1" "1|" "$(seg "$R")" +mkdir -p "$R/.ytstack/milestones/M003"; git -C "$R" mv .ytstack/M003-S01-PLAN.md .ytstack/milestones/M003/ 2>/dev/null +state "$R" M003 S01 T03 +eq "slice plan under milestones/M003/" "0|M003/S01/T03 2/5" "$(seg "$R")" + +# ---- 3. progress block ------------------------------------------------------- +echo "# progress block" +state "$R" M003 S01 T03 "$BLOCK" +eq "label, percentage, done/total, pointer" "0|V2 76% 138/180 · M003/S01/T03" "$(seg "$R")" +state "$R" none none none "$BLOCK" +eq "block without pointer" "0|V2 76% 138/180" "$(seg "$R")" +state "$R" M003 S01 T03 "${BLOCK} waiting_on: \"review of PR 12\"\n" +eq "waiting_on shown" "0|V2 76% 138/180 · M003/S01/T03 · waiting: review of PR 12" "$(seg "$R")" +state "$R" M003 S01 T03 "${BLOCK} waiting_on: the operator has to approve the budget for phase three\n" +eq "waiting_on truncated to 32 characters" "0|V2 76% 138/180 · M003/S01/T03 · waiting: the operator has to approve the …" "$(seg "$R")" +state "$R" M003 S01 T03 'progress:\n label: V2\n done: 7\n total: 7\n' +eq "complete: 100%" "0|V2 100% 7/7 · M003/S01/T03" "$(seg "$R")" +state "$R" M003 S01 T03 'progress:\n label: V2\n done: 179\n total: 180\n' +eq "percentage rounds down (never 100% early)" "0|V2 99% 179/180 · M003/S01/T03" "$(seg "$R")" +state "$R" M003 S01 T03 'progress:\n label: V2\n done: 9\n total: 7\n' +eq "malformed block: no output, exit 1" "1|" "$(seg "$R")" + +# ---- 4. input handling ------------------------------------------------------- +echo "# input" +state "$R" M003 S01 T03 "$BLOCK" +WANT="V2 76% 138/180 · M003/S01/T03" +eq "no stdin JSON: falls back to PWD" "$WANT" "$(cd "$R" && bash "$BIN" --no-color /dev/null 2>&1 +state "$WT" M003 S01 T04 'progress:\n label: V2\n done: 3\n total: 10\n' +eq "linked worktree: plan from main, pointer local" "0|V2 60% 6/10 · M003/S01/T04" "$(seg "$WT")" +state "$WT" M003 S01 T04 'progress:\n label: V2\n done: 99\n total: 10\n' +eq "linked worktree: local block is not read" "0|V2 60% 6/10 · M003/S01/T04" "$(seg "$WT")" +state "$D" M003 S01 T02 'progress:\n label: V2\n done: 99\n total: 10\n'; commit "$D" "broken on main" +state "$WT" M003 S01 T04 'progress:\n label: V2\n done: 3\n total: 10\n' +eq "linked worktree: malformed block on main prints nothing" "1|" "$(seg "$WT")" +state "$D" M003 S01 T02 'progress:\n label: V2\n done: 7\n total: 10\n'; commit "$D" "main state 7" +( cd "$WT" && git checkout -q --detach ) 2>/dev/null +eq "detached HEAD: plan from main" "0|V2 70% 7/10 · M003/S01/T04" "$(seg "$WT")" + +# clone: origin/HEAD names the default branch; origin/main is newer than local main +C="$TMP/clone"; git clone -q "$D" "$C" 2>/dev/null +( cd "$C" && git config user.email t@t.t && git config user.name t && git config commit.gpgsign false && git checkout -q -b topic ) +state "$D" M003 S01 T02 'progress:\n label: V2\n done: 8\n total: 10\n' +( cd "$D" && git add -A && GIT_COMMITTER_DATE="2099-01-01T00:00:00Z" git commit -qm "main state 8" ) >/dev/null 2>&1 +git -C "$C" fetch -q origin 2>/dev/null +state "$C" M003 S01 T05 '' +eq "clone on a topic branch: newer origin/main wins" "0|V2 80% 8/10 · M003/S01/T05" "$(seg "$C")" + +# slice count without a block: same source rule as the block. Off the default +# branch the slice plan is read from the default ref (PR #32 review); the +# checkout's copy is used only for a slice plan that does not exist there yet. +E=$(new_repo slicewt); state "$E" M003 S01 T03; commit "$E" seed +EW="$TMP/slicewt-wt"; git -C "$E" worktree add -q -b slice-work "$EW" >/dev/null 2>&1 +printf -- '---\nmilestone: M003\nslice: S01\n---\n# plan\n\n- [x] T01 -- one\n- [x] T02 -- two\n- [x] T03 -- three\n- [ ] T04 -- four\n- [ ] T05 -- five\n' > "$EW/.ytstack/M003-S01-PLAN.md" +state "$EW" M003 S01 T04 +eq "linked worktree, no block: slice count from main, pointer local" "0|M003/S01/T04 2/5" "$(seg "$EW")" +eq "main checkout: its own working tree" "0|M003/S01/T03 2/5" "$(seg "$E")" +printf -- '---\nmilestone: M003\nslice: S02\n---\n# plan\n\n- [x] T01 -- one\n- [ ] T02 -- two\n' > "$EW/.ytstack/M003-S02-PLAN.md" +state "$EW" M003 S02 T02 +eq "slice plan only in the worktree: local count" "0|M003/S02/T02 1/2" "$(seg "$EW")" +mkdir -p "$E/.ytstack/milestones/M003"; git -C "$E" mv .ytstack/M003-S01-PLAN.md .ytstack/milestones/M003/; commit "$E" "nested layout" +state "$EW" M003 S01 T04; rm -f "$EW/.ytstack/M003-S01-PLAN.md" +eq "slice plan under milestones/M003/ on main" "0|M003/S01/T04 2/5" "$(seg "$EW")" + +# no default branch at all: working tree +N="$TMP/nodef"; mkdir -p "$N/.ytstack" +( cd "$N" && git init -q -b trunk && git config user.email t@t.t && git config user.name t && git config commit.gpgsign false ) +state "$N" M003 none none "$BLOCK"; commit "$N" seed +eq "no main/master/origin: working tree" "0|V2 76% 138/180 · M003" "$(seg "$N")" + +# no git at all +G="$TMP/nogit"; mkdir -p "$G/.ytstack"; state "$G" M003 none none "$BLOCK" +eq "not a git repository: working tree" "0|V2 76% 138/180 · M003" "$(seg "$G")" + +echo +echo "$PASS passed, $FAIL failed" +[ "$FAIL" -eq 0 ]