meanwhile-cli 0.1.0__tar.gz → 0.1.2__tar.gz

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.
Files changed (42) hide show
  1. {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/.claude-plugin/marketplace.json +1 -1
  2. {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/.claude-plugin/plugin.json +1 -1
  3. meanwhile_cli-0.1.2/PKG-INFO +176 -0
  4. meanwhile_cli-0.1.2/README.md +161 -0
  5. {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/plugin.json +1 -1
  6. {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/pyproject.toml +1 -1
  7. meanwhile_cli-0.1.2/src/meanwhile/__init__.py +1 -0
  8. {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/src/meanwhile/app.py +44 -16
  9. {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/src/meanwhile/build_prompt.md +20 -2
  10. {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/src/meanwhile/game.py +21 -4
  11. meanwhile_cli-0.1.2/src/meanwhile/scores.py +75 -0
  12. {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/src/meanwhile/strings.py +4 -6
  13. {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/tests/test_app.py +98 -28
  14. {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/tests/test_game.py +12 -0
  15. {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/tests/test_plugin_files.py +11 -0
  16. {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/tests/test_scores.py +32 -0
  17. {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/tests/test_strings.py +1 -1
  18. meanwhile_cli-0.1.0/PKG-INFO +0 -265
  19. meanwhile_cli-0.1.0/README.md +0 -250
  20. meanwhile_cli-0.1.0/src/meanwhile/__init__.py +0 -1
  21. meanwhile_cli-0.1.0/src/meanwhile/scores.py +0 -44
  22. {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/.agents/plugins/marketplace.json +0 -0
  23. {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/.gitignore +0 -0
  24. {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/LICENSE +0 -0
  25. {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/commands/meanwhile-build.md +0 -0
  26. {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/hooks/codex-hooks.json +0 -0
  27. {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/hooks/hooks.json +0 -0
  28. {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/hooks/meanwhile-state.sh +0 -0
  29. {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/src/meanwhile/agents.py +0 -0
  30. {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/src/meanwhile/app.tcss +0 -0
  31. {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/src/meanwhile/cli.py +0 -0
  32. {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/src/meanwhile/hooks_cli.py +0 -0
  33. {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/src/meanwhile/meanwhile-state.sh +0 -0
  34. {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/src/meanwhile/pool.py +0 -0
  35. {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/src/meanwhile/repo.py +0 -0
  36. {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/src/meanwhile/state.py +0 -0
  37. {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/tests/test_agents.py +0 -0
  38. {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/tests/test_cli.py +0 -0
  39. {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/tests/test_hooks_cli.py +0 -0
  40. {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/tests/test_pool.py +0 -0
  41. {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/tests/test_repo.py +0 -0
  42. {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/tests/test_state.py +0 -0
@@ -8,7 +8,7 @@
8
8
  {
9
9
  "name": "meanwhile",
10
10
  "source": ".",
11
- "version": "0.1.0",
11
+ "version": "0.1.2",
12
12
  "description": "Play a quiz about your own repository while Claude Code is working, so your context stays loaded for the diff review that follows."
13
13
  }
14
14
  ]
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "meanwhile",
3
3
  "description": "Play a quiz about your own repository while Claude Code is working, so your context stays loaded for the diff review that follows.",
4
- "version": "0.1.0",
4
+ "version": "0.1.2",
5
5
  "author": {
6
6
  "name": "Ismail Bayram"
7
7
  },
@@ -0,0 +1,176 @@
1
+ Metadata-Version: 2.5
2
+ Name: meanwhile-cli
3
+ Version: 0.1.2
4
+ Summary: Play a quiz about your own repository while your coding agent works
5
+ Project-URL: Homepage, https://github.com/ismailbayram/meanwhile
6
+ Project-URL: Repository, https://github.com/ismailbayram/meanwhile
7
+ License: MIT
8
+ License-File: LICENSE
9
+ Requires-Python: >=3.10
10
+ Requires-Dist: textual>=0.80
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
13
+ Requires-Dist: pytest>=8.0; extra == 'dev'
14
+ Description-Content-Type: text/markdown
15
+
16
+ # meanwhile
17
+
18
+ A quiz about your own repository that you play while your coding agent works,
19
+ so the code is still fresh in your head when the diff lands. Works with
20
+ Claude Code, Codex, Cursor and OpenCode.
21
+
22
+ <p align="center">
23
+ <img src="docs/images/card.svg" alt="A multiple-choice card about the repository, waiting for an answer" width="720">
24
+ </p>
25
+
26
+ <p align="center">
27
+ <img src="docs/images/done.svg" alt="The answered card, with a banner saying Claude Code is done" width="720">
28
+ </p>
29
+
30
+ ## Quick start
31
+
32
+ Three steps, once per machine and project:
33
+
34
+ 1. **Connect your agent** — see [Installation](#installation) for your agent.
35
+ 2. **Build the question pool**, once per project:
36
+ - Claude Code: run `/meanwhile-build`
37
+ - any other agent: run `uvx meanwhile-cli build-prompt --lang en --repo .`
38
+ and paste the output to your agent
39
+ 3. **Open a second terminal** in the same directory as your agent and run:
40
+
41
+ ```
42
+ uvx meanwhile-cli
43
+ ```
44
+
45
+ The pane starts asking questions right away. When your agent finishes a
46
+ prompt, the pane tells you so you can switch back to the diff.
47
+
48
+ ## Installation
49
+
50
+ You need [uv](https://docs.astral.sh/uv/) for the `uvx` commands. Then pick
51
+ your agent:
52
+
53
+ ### Claude Code
54
+
55
+ ```
56
+ /plugin marketplace add ismailbayram/meanwhile
57
+ /plugin install meanwhile@meanwhile
58
+ ```
59
+
60
+ Restart Claude Code.
61
+
62
+ ### Codex
63
+
64
+ ```
65
+ codex plugin marketplace add ismailbayram/meanwhile
66
+ ```
67
+
68
+ Install `meanwhile` from the Plugins Directory, then **review and trust its
69
+ hooks** — Codex skips plugin hooks until you do.
70
+
71
+ ### Cursor
72
+
73
+ ```
74
+ uvx meanwhile-cli hooks install --agent cursor
75
+ ```
76
+
77
+ ### OpenCode
78
+
79
+ ```
80
+ uvx meanwhile-cli hooks install --agent opencode
81
+ ```
82
+
83
+ Restart OpenCode.
84
+
85
+ ### Installing the command permanently (optional)
86
+
87
+ `uvx` runs the latest version without installing anything. If you prefer a
88
+ regular command:
89
+
90
+ ```
91
+ uv tool install meanwhile-cli # or: pipx install meanwhile-cli
92
+ ```
93
+
94
+ The command is then just `meanwhile` — use it wherever this README says
95
+ `uvx meanwhile-cli`.
96
+
97
+ ## Usage
98
+
99
+ | Key | Action |
100
+ | --- | --- |
101
+ | `1`–`9` | answer |
102
+ | `space` | next card |
103
+ | `q` | quit |
104
+
105
+ - Start the pane **from the same directory you started your agent in**,
106
+ otherwise it cannot tell when the agent is done. The quiz itself works
107
+ either way.
108
+ - Leave it open all day. Each new prompt after the agent finishes deals a
109
+ fresh round; when the pool runs out, `space` starts a new one.
110
+ - The interface follows the pool's language. Override it with
111
+ `--lang tr` or `--lang en`.
112
+
113
+ ## The question pool
114
+
115
+ `/meanwhile-build` (or `build-prompt`) has the agent read your repository and
116
+ write `.meanwhile/pool.json` — 30 to 50 multiple-choice questions. An entry
117
+ looks like this:
118
+
119
+ ```json
120
+ {
121
+ "type": "quiz",
122
+ "q": "What happens to an order's reserved stock when payment fails?",
123
+ "choices": [
124
+ "It stays reserved until an admin releases it",
125
+ "It is released back to inventory after 15 minutes",
126
+ "It is released immediately and the order is cancelled"
127
+ ],
128
+ "answer": 1,
129
+ "why": "release_expired_reservations runs every 15 minutes and returns unpaid stock to inventory.",
130
+ "source": "shop/orders/tasks.py"
131
+ }
132
+ ```
133
+
134
+ - **Commit `pool.json`** so the whole team plays the same pool and nobody pays
135
+ to build it again.
136
+ - **Language:** `--lang` takes a language code, not a name. `tr` and `en` are
137
+ the two the interface speaks; any other code (`de`, `fr`, …) writes the
138
+ questions in that language and keeps the interface in English.
139
+ - **Adding questions:** run the build step again. When a pool already
140
+ exists it keeps every question and adds 30 new ones. Ask your agent for
141
+ "a fresh pool" if you want to replace it instead.
142
+ - **Refreshing:** when the repository is about 50 commits past the pool, the
143
+ pane shows a banner. Run the build step again and commit the new file.
144
+ Nothing is regenerated automatically.
145
+ - **Repeats:** within a round no question repeats. Across rounds, questions
146
+ you have not answered yet come first; once you have answered them all, it
147
+ starts over.
148
+ - Scores and the answered-question list are kept in
149
+ `.meanwhile/scores.json`, which is gitignored.
150
+
151
+ ## Commands
152
+
153
+ | Command | What it does |
154
+ | --- | --- |
155
+ | `uvx meanwhile-cli` | open the quiz pane |
156
+ | `uvx meanwhile-cli build-prompt --lang en --repo .` | print the pool-building instructions for any agent |
157
+ | `uvx meanwhile-cli validate --repo .` | check `pool.json` |
158
+ | `uvx meanwhile-cli hooks install --agent <agent>` | connect an agent without its plugin (`claude`, `codex`, `cursor`, `opencode`); add `--user` for every project (not Cursor) |
159
+ | `uvx meanwhile-cli hooks status` | show which agents are connected |
160
+ | `uvx meanwhile-cli hooks uninstall --agent <agent>` | disconnect |
161
+
162
+ `hooks install` writes an absolute path from your machine into the agent's
163
+ config, so don't commit that file for Codex, Cursor or OpenCode.
164
+
165
+ ## How it works
166
+
167
+ Hooks in your agent write `busy` or `idle` to a small file in
168
+ `~/.meanwhile/state/` when you send a prompt and when the agent finishes. The
169
+ pane watches that file and shows cards from `pool.json`. Only the build step
170
+ uses an LLM; the pane makes no network calls.
171
+
172
+ ## Development
173
+
174
+ From a checkout of this repository, use `uv run meanwhile` in place of
175
+ `uvx meanwhile-cli`, and `/plugin marketplace add /path/to/meanwhile` for the
176
+ Claude Code plugin. Tests: `uv run --extra dev python -m pytest`.
@@ -0,0 +1,161 @@
1
+ # meanwhile
2
+
3
+ A quiz about your own repository that you play while your coding agent works,
4
+ so the code is still fresh in your head when the diff lands. Works with
5
+ Claude Code, Codex, Cursor and OpenCode.
6
+
7
+ <p align="center">
8
+ <img src="docs/images/card.svg" alt="A multiple-choice card about the repository, waiting for an answer" width="720">
9
+ </p>
10
+
11
+ <p align="center">
12
+ <img src="docs/images/done.svg" alt="The answered card, with a banner saying Claude Code is done" width="720">
13
+ </p>
14
+
15
+ ## Quick start
16
+
17
+ Three steps, once per machine and project:
18
+
19
+ 1. **Connect your agent** — see [Installation](#installation) for your agent.
20
+ 2. **Build the question pool**, once per project:
21
+ - Claude Code: run `/meanwhile-build`
22
+ - any other agent: run `uvx meanwhile-cli build-prompt --lang en --repo .`
23
+ and paste the output to your agent
24
+ 3. **Open a second terminal** in the same directory as your agent and run:
25
+
26
+ ```
27
+ uvx meanwhile-cli
28
+ ```
29
+
30
+ The pane starts asking questions right away. When your agent finishes a
31
+ prompt, the pane tells you so you can switch back to the diff.
32
+
33
+ ## Installation
34
+
35
+ You need [uv](https://docs.astral.sh/uv/) for the `uvx` commands. Then pick
36
+ your agent:
37
+
38
+ ### Claude Code
39
+
40
+ ```
41
+ /plugin marketplace add ismailbayram/meanwhile
42
+ /plugin install meanwhile@meanwhile
43
+ ```
44
+
45
+ Restart Claude Code.
46
+
47
+ ### Codex
48
+
49
+ ```
50
+ codex plugin marketplace add ismailbayram/meanwhile
51
+ ```
52
+
53
+ Install `meanwhile` from the Plugins Directory, then **review and trust its
54
+ hooks** — Codex skips plugin hooks until you do.
55
+
56
+ ### Cursor
57
+
58
+ ```
59
+ uvx meanwhile-cli hooks install --agent cursor
60
+ ```
61
+
62
+ ### OpenCode
63
+
64
+ ```
65
+ uvx meanwhile-cli hooks install --agent opencode
66
+ ```
67
+
68
+ Restart OpenCode.
69
+
70
+ ### Installing the command permanently (optional)
71
+
72
+ `uvx` runs the latest version without installing anything. If you prefer a
73
+ regular command:
74
+
75
+ ```
76
+ uv tool install meanwhile-cli # or: pipx install meanwhile-cli
77
+ ```
78
+
79
+ The command is then just `meanwhile` — use it wherever this README says
80
+ `uvx meanwhile-cli`.
81
+
82
+ ## Usage
83
+
84
+ | Key | Action |
85
+ | --- | --- |
86
+ | `1`–`9` | answer |
87
+ | `space` | next card |
88
+ | `q` | quit |
89
+
90
+ - Start the pane **from the same directory you started your agent in**,
91
+ otherwise it cannot tell when the agent is done. The quiz itself works
92
+ either way.
93
+ - Leave it open all day. Each new prompt after the agent finishes deals a
94
+ fresh round; when the pool runs out, `space` starts a new one.
95
+ - The interface follows the pool's language. Override it with
96
+ `--lang tr` or `--lang en`.
97
+
98
+ ## The question pool
99
+
100
+ `/meanwhile-build` (or `build-prompt`) has the agent read your repository and
101
+ write `.meanwhile/pool.json` — 30 to 50 multiple-choice questions. An entry
102
+ looks like this:
103
+
104
+ ```json
105
+ {
106
+ "type": "quiz",
107
+ "q": "What happens to an order's reserved stock when payment fails?",
108
+ "choices": [
109
+ "It stays reserved until an admin releases it",
110
+ "It is released back to inventory after 15 minutes",
111
+ "It is released immediately and the order is cancelled"
112
+ ],
113
+ "answer": 1,
114
+ "why": "release_expired_reservations runs every 15 minutes and returns unpaid stock to inventory.",
115
+ "source": "shop/orders/tasks.py"
116
+ }
117
+ ```
118
+
119
+ - **Commit `pool.json`** so the whole team plays the same pool and nobody pays
120
+ to build it again.
121
+ - **Language:** `--lang` takes a language code, not a name. `tr` and `en` are
122
+ the two the interface speaks; any other code (`de`, `fr`, …) writes the
123
+ questions in that language and keeps the interface in English.
124
+ - **Adding questions:** run the build step again. When a pool already
125
+ exists it keeps every question and adds 30 new ones. Ask your agent for
126
+ "a fresh pool" if you want to replace it instead.
127
+ - **Refreshing:** when the repository is about 50 commits past the pool, the
128
+ pane shows a banner. Run the build step again and commit the new file.
129
+ Nothing is regenerated automatically.
130
+ - **Repeats:** within a round no question repeats. Across rounds, questions
131
+ you have not answered yet come first; once you have answered them all, it
132
+ starts over.
133
+ - Scores and the answered-question list are kept in
134
+ `.meanwhile/scores.json`, which is gitignored.
135
+
136
+ ## Commands
137
+
138
+ | Command | What it does |
139
+ | --- | --- |
140
+ | `uvx meanwhile-cli` | open the quiz pane |
141
+ | `uvx meanwhile-cli build-prompt --lang en --repo .` | print the pool-building instructions for any agent |
142
+ | `uvx meanwhile-cli validate --repo .` | check `pool.json` |
143
+ | `uvx meanwhile-cli hooks install --agent <agent>` | connect an agent without its plugin (`claude`, `codex`, `cursor`, `opencode`); add `--user` for every project (not Cursor) |
144
+ | `uvx meanwhile-cli hooks status` | show which agents are connected |
145
+ | `uvx meanwhile-cli hooks uninstall --agent <agent>` | disconnect |
146
+
147
+ `hooks install` writes an absolute path from your machine into the agent's
148
+ config, so don't commit that file for Codex, Cursor or OpenCode.
149
+
150
+ ## How it works
151
+
152
+ Hooks in your agent write `busy` or `idle` to a small file in
153
+ `~/.meanwhile/state/` when you send a prompt and when the agent finishes. The
154
+ pane watches that file and shows cards from `pool.json`. Only the build step
155
+ uses an LLM; the pane makes no network calls.
156
+
157
+ ## Development
158
+
159
+ From a checkout of this repository, use `uv run meanwhile` in place of
160
+ `uvx meanwhile-cli`, and `/plugin marketplace add /path/to/meanwhile` for the
161
+ Claude Code plugin. Tests: `uv run --extra dev python -m pytest`.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "meanwhile",
4
- "version": "0.1.0",
4
+ "version": "0.1.2",
5
5
  "description": "Play a quiz about your own repository while Claude Code is working, so your context stays loaded for the diff review that follows.",
6
6
  "author": {
7
7
  "name": "Ismail Bayram"
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "meanwhile-cli"
7
- version = "0.1.0"
7
+ version = "0.1.2"
8
8
  description = "Play a quiz about your own repository while your coding agent works"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1 @@
1
+ __version__ = "0.1.2"
@@ -12,9 +12,9 @@ from textual.widgets import Footer, Static
12
12
  from . import agents, game, scores, strings
13
13
  from .pool import Pool, QuizItem
14
14
 
15
- # Lifecycle phases. The pane is opened once and left open: it waits for the
16
- # agent to start, plays for exactly as long as it is busy, then waits again.
17
- WAITING = "waiting"
15
+ # Lifecycle phases. The pane deals cards as soon as it opens — a pool is all
16
+ # it needs. The agent's hooks only add to that: an idle edge freezes the card
17
+ # and says the agent is done, and the next busy edge deals a fresh round.
18
18
  PLAYING = "playing"
19
19
  DONE = "done"
20
20
 
@@ -52,11 +52,16 @@ class MeanwhileApp(App):
52
52
  self.poll_seconds = poll_seconds
53
53
  self.language = language
54
54
  self.agent_source = agent_source
55
- self.session = game.start_session(pool)
56
- self.phase = WAITING
55
+ self.session = self._deal()
56
+ self.phase = PLAYING
57
57
  self._last_status = "idle"
58
58
  self._answered = False
59
59
 
60
+ def _deal(self) -> game.Session:
61
+ """A fresh round, questions this player has not answered yet first."""
62
+ seen = scores.load_seen(self.repo_dir) if self.repo_dir is not None else set()
63
+ return game.start_session(self.pool, seen=seen)
64
+
60
65
  def _agent_name(self) -> str:
61
66
  return agents.label_for(self.agent_source()) or strings.text(self.language, "agent")
62
67
 
@@ -115,8 +120,12 @@ class MeanwhileApp(App):
115
120
  self._finish_playing()
116
121
 
117
122
  def _start_playing(self) -> None:
123
+ if self.phase == PLAYING:
124
+ # Already dealing — the pane was opened before the prompt. Keep the
125
+ # card on screen rather than reshuffling under the player.
126
+ return
118
127
  self.phase = PLAYING
119
- self.session = game.start_session(self.pool) # re-shuffled: the cards come back
128
+ self.session = self._deal() # re-shuffled: the cards come back
120
129
  self._answered = False
121
130
  self.query_one("#status").update("")
122
131
  self.query_one("#feedback").update("")
@@ -128,14 +137,24 @@ class MeanwhileApp(App):
128
137
  self.phase = DONE
129
138
  self.refresh_bindings() # DONE takes no input: the card keys leave the footer
130
139
  self.query_one("#status").update(strings.text(self.language, "done", agent=self._agent_name()))
131
- # Exactly once per session: the `phase != PLAYING` guard above is what
132
- # guarantees it — the only way back into PLAYING is _start_playing,
133
- # which deals a new session.
140
+ self._record()
141
+
142
+ def _record(self) -> None:
143
+ # Exactly once per session: both callers leave PLAYING first, and the
144
+ # only way back into it is _start_playing, which deals a new session.
134
145
  if self.repo_dir is not None and self.session.answered:
135
146
  scores.record_session(self.repo_dir, self.session)
136
147
 
137
148
  # --- actions --------------------------------------------------------
138
149
 
150
+ async def action_quit(self) -> None:
151
+ # Without hooks there is never an idle edge, so quitting is the only
152
+ # moment a session ends. Score it here rather than lose it.
153
+ if self.phase == PLAYING:
154
+ self.phase = DONE
155
+ self._record()
156
+ self.exit()
157
+
139
158
  def action_choose(self, number: int) -> None:
140
159
  item = game.current(self.session)
141
160
  if self.phase != PLAYING or self._answered or not isinstance(item, QuizItem):
@@ -144,9 +163,24 @@ class MeanwhileApp(App):
144
163
  if not 0 <= index < len(item.choices):
145
164
  return
146
165
  self._resolve(game.answer(self.session, index))
166
+ if self.repo_dir is not None:
167
+ scores.mark_seen(
168
+ self.repo_dir, game.item_key(item), {game.item_key(i) for i in self.pool.items}
169
+ )
147
170
 
148
171
  def action_next(self) -> None:
149
- if self.phase != PLAYING or not self._answered:
172
+ if self.phase != PLAYING:
173
+ return
174
+ if game.current(self.session) is None:
175
+ # Out of cards. Without hooks no busy edge will ever come to deal
176
+ # another round, so `space` does it: bank this one, start afresh.
177
+ self._record()
178
+ self.session = self._deal()
179
+ self._answered = False
180
+ self.query_one("#feedback").update("")
181
+ self._render()
182
+ return
183
+ if not self._answered:
150
184
  return
151
185
  game.advance(self.session)
152
186
  self._answered = False
@@ -166,12 +200,6 @@ class MeanwhileApp(App):
166
200
  # `check_action` has too.
167
201
  self.refresh_bindings()
168
202
  card = self.query_one("#card")
169
- if self.phase == WAITING:
170
- self.query_one("#status").update(strings.text(self.language, "waiting", agent=self._agent_name()))
171
- card.update("")
172
- self.query_one("#score").update("")
173
- return
174
-
175
203
  item = game.current(self.session)
176
204
  if item is None:
177
205
  card.update(strings.text(self.language, "exhausted"))
@@ -20,9 +20,26 @@ pool's `language` field records exactly that code. `tr` and `en` are the two
20
20
  the playing interface itself speaks; any other code is still fine — the cards
21
21
  get written in that language and the interface stays English.
22
22
 
23
+ ## Extending an existing pool
24
+
25
+ If `.meanwhile/pool.json` already exists, extend it rather than replacing it,
26
+ unless the user asked for a fresh pool:
27
+
28
+ - Keep every existing item exactly as it is — players' progress is keyed on
29
+ the question text, so a reworded question counts as a new one. Only drop
30
+ an item whose `source` file no longer exists.
31
+ - Add 30 new items. None may ask what an existing item already asks; read
32
+ the existing questions first, and look for parts of the repository they
33
+ do not cover yet.
34
+ - Keep the pool's own `language` — it wins over the language named above,
35
+ which is only a default — unless the user asked for another one.
36
+ - Set `builtAt` and `headSha` to today and the current commit.
37
+
23
38
  ## What to write
24
39
 
25
- Write `.meanwhile/pool.json` with 30-50 `quiz` items, using exactly this shape:
40
+ For a new pool, write `.meanwhile/pool.json` with 30-50 `quiz` items; when
41
+ extending, keep the same envelope and append to `items`. Use exactly this
42
+ shape:
26
43
 
27
44
  {
28
45
  "builtAt": "<today, YYYY-MM-DD>",
@@ -60,7 +77,8 @@ Rules:
60
77
  2. Run `uvx meanwhile-cli validate --repo .` (during development, from a
61
78
  checkout of this plugin: `uv run meanwhile validate --repo .`) and fix
62
79
  anything it reports.
63
- 3. Tell the user the item count and that `.meanwhile/pool.json` is ready to
80
+ 3. Tell the user the item count — when extending, how many were added and
81
+ how many there are now — and that `.meanwhile/pool.json` is ready to
64
82
  commit so their team shares the pool.
65
83
  4. Tell the user how to play, in {language} — building the pool starts
66
84
  nothing, and no other step says how. Give them the command itself, in a
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import hashlib
5
6
  import random
6
7
  from dataclasses import dataclass, field
7
8
 
@@ -31,10 +32,26 @@ class Session:
31
32
  _answered_index: int = field(default=-1, init=False, repr=False, compare=False)
32
33
 
33
34
 
34
- def start_session(pool: Pool, rng: random.Random | None = None) -> Session:
35
- items = list(pool.items)
36
- (rng or random.Random()).shuffle(items)
37
- return Session(items=items)
35
+ def item_key(item) -> str:
36
+ """A stable name for a question, for remembering it was answered.
37
+
38
+ The question text, not the item's position: a rebuilt or extended pool
39
+ reorders items, and a question that survives a rebuild word for word is
40
+ the same question to the player.
41
+ """
42
+ return hashlib.sha256(item.q.encode("utf-8")).hexdigest()[:16]
43
+
44
+
45
+ def start_session(
46
+ pool: Pool, rng: random.Random | None = None, seen: frozenset[str] | set[str] = frozenset()
47
+ ) -> Session:
48
+ """A shuffled round over the whole pool, questions not in `seen` first."""
49
+ rng = rng or random.Random()
50
+ fresh = [item for item in pool.items if item_key(item) not in seen]
51
+ repeats = [item for item in pool.items if item_key(item) in seen]
52
+ rng.shuffle(fresh)
53
+ rng.shuffle(repeats)
54
+ return Session(items=fresh + repeats)
38
55
 
39
56
 
40
57
  def finished(session: Session) -> bool:
@@ -0,0 +1,75 @@
1
+ """Per-developer running totals, and which questions this developer has
2
+ answered. Gitignored: both are local, never shared."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import os
8
+ from pathlib import Path
9
+
10
+ from .game import Session
11
+
12
+ SCORES_RELPATH = ".meanwhile/scores.json"
13
+ _ZERO = {"answered": 0, "correct": 0, "best_streak": 0}
14
+
15
+
16
+ def _path(repo_dir: str | Path) -> Path:
17
+ return Path(repo_dir) / SCORES_RELPATH
18
+
19
+
20
+ def _read(repo_dir: str | Path) -> dict:
21
+ try:
22
+ data = json.loads(_path(repo_dir).read_text(encoding="utf-8"))
23
+ except (OSError, json.JSONDecodeError):
24
+ return {}
25
+ return data if isinstance(data, dict) else {}
26
+
27
+
28
+ def _write(repo_dir: str | Path, totals: dict, seen: set[str]) -> None:
29
+ path = _path(repo_dir)
30
+ path.parent.mkdir(parents=True, exist_ok=True)
31
+ tmp = path.with_name(f"{path.name}.tmp.{os.getpid()}")
32
+ tmp.write_text(json.dumps({**totals, "seen": sorted(seen)}, indent=2), encoding="utf-8")
33
+ tmp.replace(path)
34
+
35
+
36
+ def load_scores(repo_dir: str | Path) -> dict:
37
+ data = _read(repo_dir)
38
+ out = dict(_ZERO)
39
+ for key in out:
40
+ value = data.get(key, 0)
41
+ out[key] = value if isinstance(value, int) and value >= 0 else 0
42
+ return out
43
+
44
+
45
+ def record_session(repo_dir: str | Path, session: Session) -> dict:
46
+ totals = load_scores(repo_dir)
47
+ totals["answered"] += session.answered
48
+ totals["correct"] += session.correct
49
+ totals["best_streak"] = max(totals["best_streak"], session.best_streak)
50
+ _write(repo_dir, totals, load_seen(repo_dir))
51
+ return totals
52
+
53
+
54
+ def load_seen(repo_dir: str | Path) -> set[str]:
55
+ """The keys (see `game.item_key`) of the questions answered so far."""
56
+ seen = _read(repo_dir).get("seen", [])
57
+ if not isinstance(seen, list):
58
+ return set()
59
+ return {key for key in seen if isinstance(key, str)}
60
+
61
+
62
+ def mark_seen(repo_dir: str | Path, key: str, pool_keys: set[str]) -> set[str]:
63
+ """Remember one answered question, and return what is remembered now.
64
+
65
+ Written on every answer rather than at the end of a round: a pane closed
66
+ with its terminal never reaches the end of a round. Once every question in
67
+ the pool has been answered the list starts over, so the next rounds go
68
+ back to a plain shuffle instead of ordering by a list that covers it all.
69
+ Keys of questions no longer in the pool are dropped on the way.
70
+ """
71
+ seen = (load_seen(repo_dir) & pool_keys) | {key}
72
+ if pool_keys <= seen:
73
+ seen = set()
74
+ _write(repo_dir, load_scores(repo_dir), seen)
75
+ return seen
@@ -1,4 +1,4 @@
1
- """The interface's six strings, in the languages meanwhile ships.
1
+ """The interface's strings, in the languages meanwhile ships.
2
2
 
3
3
  A dictionary, deliberately, and not an i18n framework: there is no catalog to
4
4
  extract, no compilation step and no locale negotiation. The cards themselves
@@ -10,23 +10,21 @@ from __future__ import annotations
10
10
 
11
11
  DEFAULT_LANGUAGE = "en"
12
12
 
13
- KEYS = ("waiting", "done", "exhausted", "stale", "correct", "wrong", "agent")
13
+ KEYS = ("done", "exhausted", "stale", "correct", "wrong", "agent")
14
14
 
15
15
  _TABLE = {
16
16
  "en": {
17
17
  "agent": "the agent",
18
- "waiting": "Waiting for {agent} to start working — submit a prompt and the game begins.",
19
18
  "done": "{agent} is done ^ — switch back to the diff; this pane keeps running.",
20
- "exhausted": "Pool exhausted for this wait — the next one deals a fresh round.",
19
+ "exhausted": "That was every question in the pool — press space for a fresh round.",
21
20
  "stale": "pool is {behind} commits behind — run the build command for fresh questions",
22
21
  "correct": "correct",
23
22
  "wrong": "wrong",
24
23
  },
25
24
  "tr": {
26
25
  "agent": "ajan",
27
- "waiting": "{agent} çalışmaya başlayınca oyun başlıyor — bir istem gönder.",
28
26
  "done": "{agent} bitirdi ^ — diff'e dön; bu pencere açık kalıyor.",
29
- "exhausted": "Bu bekleme için havuz bitti — sonraki bekleme yeni bir tur dağıtıyor.",
27
+ "exhausted": "Havuzdaki bütün sorular bitti — yeni tur için boşluğa bas.",
30
28
  "stale": "havuz {behind} commit geride — tazelemek için build komutunu çalıştır",
31
29
  "correct": "doğru",
32
30
  "wrong": "yanlış",