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.
- {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/.claude-plugin/marketplace.json +1 -1
- {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/.claude-plugin/plugin.json +1 -1
- meanwhile_cli-0.1.2/PKG-INFO +176 -0
- meanwhile_cli-0.1.2/README.md +161 -0
- {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/plugin.json +1 -1
- {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/pyproject.toml +1 -1
- meanwhile_cli-0.1.2/src/meanwhile/__init__.py +1 -0
- {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/src/meanwhile/app.py +44 -16
- {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/src/meanwhile/build_prompt.md +20 -2
- {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/src/meanwhile/game.py +21 -4
- meanwhile_cli-0.1.2/src/meanwhile/scores.py +75 -0
- {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/src/meanwhile/strings.py +4 -6
- {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/tests/test_app.py +98 -28
- {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/tests/test_game.py +12 -0
- {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/tests/test_plugin_files.py +11 -0
- {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/tests/test_scores.py +32 -0
- {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/tests/test_strings.py +1 -1
- meanwhile_cli-0.1.0/PKG-INFO +0 -265
- meanwhile_cli-0.1.0/README.md +0 -250
- meanwhile_cli-0.1.0/src/meanwhile/__init__.py +0 -1
- meanwhile_cli-0.1.0/src/meanwhile/scores.py +0 -44
- {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/.agents/plugins/marketplace.json +0 -0
- {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/.gitignore +0 -0
- {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/LICENSE +0 -0
- {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/commands/meanwhile-build.md +0 -0
- {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/hooks/codex-hooks.json +0 -0
- {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/hooks/hooks.json +0 -0
- {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/hooks/meanwhile-state.sh +0 -0
- {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/src/meanwhile/agents.py +0 -0
- {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/src/meanwhile/app.tcss +0 -0
- {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/src/meanwhile/cli.py +0 -0
- {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/src/meanwhile/hooks_cli.py +0 -0
- {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/src/meanwhile/meanwhile-state.sh +0 -0
- {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/src/meanwhile/pool.py +0 -0
- {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/src/meanwhile/repo.py +0 -0
- {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/src/meanwhile/state.py +0 -0
- {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/tests/test_agents.py +0 -0
- {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/tests/test_cli.py +0 -0
- {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/tests/test_hooks_cli.py +0 -0
- {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/tests/test_pool.py +0 -0
- {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/tests/test_repo.py +0 -0
- {meanwhile_cli-0.1.0 → meanwhile_cli-0.1.2}/tests/test_state.py +0 -0
|
@@ -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.
|
|
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"
|
|
@@ -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
|
|
16
|
-
#
|
|
17
|
-
|
|
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 =
|
|
56
|
-
self.phase =
|
|
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 =
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
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 = ("
|
|
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": "
|
|
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": "
|
|
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ış",
|