reask 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
reask/SKILL.md ADDED
@@ -0,0 +1,85 @@
1
+ ---
2
+ name: reask
3
+ description: "Use when the user asks for a pending question to be asked again or explained — /reask, \"ask again\", \"I don't get the question\", \"explain the options\", \"what are you asking me\" (in any language) — or when they answer a question of yours with confusion rather than with a choice."
4
+ license: MIT
5
+ metadata:
6
+ version: "0.1.0"
7
+ ---
8
+
9
+ # /reask
10
+
11
+ Ask the question you are already waiting on again, written for someone who just walked in and
12
+ remembers nothing. Make that decision answerable: do not decide it for the user, and do not swap
13
+ it for a different question.
14
+
15
+ Write in the language the user writes in, headings included; the English headings below are a
16
+ template.
17
+
18
+ ## First, which case is this
19
+
20
+ - **Nothing is pending.** Say so in one line and ask what they want to decide.
21
+ - **The user has already chosen.** Confirm the choice in one line and carry on.
22
+ - **A yes/no question, or two options that differ in one fact.** The short form, under 100 words:
23
+ what is being decided and what waits on it, the fact that separates the answers, your
24
+ recommendation, then the question.
25
+ - **Anything else.** The full briefing below.
26
+
27
+ ## The briefing
28
+
29
+ | Part | Contents | Budget |
30
+ |---|---|---|
31
+ | **1. "Where we are and why this comes up"** | What we are building in terms of what it does for the user; where the work stands; what stops until this is answered. Then one line of what is already **decided**, and one line, named as yours, of what you **carry over as an assumption** (a dimension copied from a neighbouring table, a component you expect to reuse), so the user can overturn it now instead of meeting it later as a decision they never made. | 3-5 sentences |
32
+ | **2. "A live example"** | One concrete case with real values from this work (an actual record, an actual query, an actual number) carried through every option, so the difference is something the user sees rather than infers. | ~120 words, usually a table |
33
+ | **3. "Options"** | Per option: one line of what it is in plain words, then **Pros** and **Cons**. At most three options in full; name any others in one line each. | up to 3 pros and 3 cons, one line each |
34
+ | **4. "What I recommend"** | Which one, and why from the facts of *this* situation: numbers, existing code, what already ships. Then what would change your mind. | up to 5 lines |
35
+ | **5. "The question"** | The decision as one answerable question. Ask it with your harness's structured-question tool if it has one (`AskUserQuestion` in Claude Code): short labels, recommended first. Otherwise ask it as plain text. | 1 question |
36
+ | | | **under 500 words total** |
37
+
38
+ The word total binds before any part's budget. A briefing that costs more to read than the
39
+ question costs to answer has failed.
40
+
41
+ ## Rules
42
+
43
+ **Say it once.** Each fact lives in one part; later parts point back at the example instead of
44
+ re-describing it. The fact that settles the decision appears by part 2: part 4 argues from it and
45
+ never reveals it. Cut every sentence that restates the previous one, and every clause that adds
46
+ emphasis rather than information.
47
+
48
+ **Plain words.** A term the user would have to look up gets a short gloss at first use:
49
+ `HNSW (a kind of index: slower to build, but never needs retraining)`. A term that does not carry
50
+ the decision is dropped, not glossed.
51
+
52
+ **Fair to every option.** Every option in part 3 gets both lists, the one you recommend included,
53
+ and every con stands at full strength, written as its strongest critic would write it. Your case
54
+ for or against an option goes in part 4, where the user can overrule it.
55
+
56
+ **Decided means decided.** Only what was actually settled goes in the decided line. Anything you
57
+ inferred is an assumption, and is labelled as yours.
58
+
59
+ **End on the question**, never on an intention: no "otherwise I'll take B", no starting on your
60
+ default.
61
+
62
+ **A better question is an aside.** If you now think a different question should be asked, the
63
+ briefing still covers the original. Add one line after part 5 naming the better question and why,
64
+ and let the user choose which to answer.
65
+
66
+ ## When a picture is faster than prose
67
+
68
+ Some differences cost more words than they are worth: screen layout and flow, what the user ends
69
+ up seeing, how components connect. When one of those *is* the subject and you can show the user a
70
+ rendered file (in Claude Code, `SendUserFile` with `display: "render"`), write one small
71
+ self-contained HTML file outside the user's project, in a scratch or temporary directory. Use the
72
+ user's language, one screen, no scrolling, the options side by side, inline CSS; no scripts, no
73
+ external URLs or images, no forms. Then cut part 2 to one line pointing at it, so the briefing
74
+ gets shorter.
75
+
76
+ Skip it when the difference is a property of data or behaviour rather than something visible, or
77
+ when you cannot show a file: then the example stays in prose.
78
+
79
+ ## Before you send
80
+
81
+ - The short form, or all five parts in order, in the user's language.
82
+ - Under 500 words (under 100 for the short form).
83
+ - In the full briefing, every option has pros and cons, the recommended one included, and the
84
+ deciding fact appears before part 4.
85
+ - The last thing is the question, with no default action attached.
reask/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ """reask: an agent skill that re-asks a pending question as a self-contained briefing."""
2
+
3
+ from importlib.metadata import version
4
+ from importlib.resources import files
5
+ from pathlib import Path
6
+
7
+ __version__ = version("reask")
8
+
9
+ # skills/reask/SKILL.md is the one copy; the wheel carries it as reask/SKILL.md
10
+ # (pyproject.toml, force-include). An editable install has no such copy, so it reads the source.
11
+ _CHECKOUT_SKILL = Path(__file__).resolve().parents[2] / "skills" / "reask" / "SKILL.md"
12
+
13
+
14
+ def skill_text() -> str:
15
+ """Return the bundled SKILL.md."""
16
+ bundled = files("reask").joinpath("SKILL.md")
17
+ source = bundled if bundled.is_file() else _CHECKOUT_SKILL
18
+ return source.read_text(encoding="utf-8")
reask/cli.py ADDED
@@ -0,0 +1,125 @@
1
+ """Command line: install, print or remove the reask skill."""
2
+
3
+ import argparse
4
+ import os
5
+ import sys
6
+ from collections.abc import Callable
7
+ from pathlib import Path
8
+
9
+ from reask import __version__, skill_text
10
+
11
+ NAME = "reask"
12
+
13
+
14
+ class RefusedError(Exception):
15
+ """An operation reask will not perform; the message says why and what to do instead."""
16
+
17
+
18
+ def _default_skills_dir() -> Path:
19
+ config = os.environ.get("CLAUDE_CONFIG_DIR")
20
+ return (Path(config).expanduser() if config else Path.home() / ".claude") / "skills"
21
+
22
+
23
+ def _locate(args: argparse.Namespace) -> tuple[Path, Path]:
24
+ """Return the directory the user named, and the SKILL.md reask manages below it."""
25
+ if args.project:
26
+ base = Path.cwd()
27
+ return base, base / ".claude" / "skills" / NAME / "SKILL.md"
28
+ base = args.skills_dir.expanduser() if args.skills_dir else _default_skills_dir()
29
+ return base, base / NAME / "SKILL.md"
30
+
31
+
32
+ def _refuse_links(base: Path, target: Path) -> None:
33
+ # Everything under a project is whatever the cloned repository put there, so a link below
34
+ # the named directory could aim a write or a delete anywhere. Below it, reask follows none.
35
+ path = base
36
+ for part in target.relative_to(base).parts:
37
+ path /= part
38
+ if path.is_symlink():
39
+ raise RefusedError(
40
+ f"{path} is a symbolic link, and reask does not write or delete through links. "
41
+ "If another tool installed the skill there (the Skills CLI links its folders), "
42
+ "manage it with that tool."
43
+ )
44
+
45
+
46
+ def _refuse_edited(target: Path, data: bytes, force: bool, verb: str) -> None:
47
+ if not target.is_file():
48
+ raise RefusedError(f"{target} is not a file")
49
+ if not force and target.read_bytes() != data:
50
+ raise RefusedError(
51
+ f"{target} differs from reask {__version__}'s copy (edited, or another version); "
52
+ f"pass --force to {verb} it"
53
+ )
54
+
55
+
56
+ def _install(args: argparse.Namespace) -> int:
57
+ base, target = _locate(args)
58
+ _refuse_links(base, target)
59
+ data = skill_text().encode("utf-8")
60
+ if target.exists():
61
+ _refuse_edited(target, data, args.force, "replace")
62
+ target.parent.mkdir(parents=True, exist_ok=True)
63
+ target.write_bytes(data)
64
+ print(f"installed {target}")
65
+ return 0
66
+
67
+
68
+ def _uninstall(args: argparse.Namespace) -> int:
69
+ base, target = _locate(args)
70
+ _refuse_links(base, target)
71
+ if not target.exists():
72
+ raise RefusedError(f"{target} not found; nothing to remove")
73
+ _refuse_edited(target, skill_text().encode("utf-8"), args.force, "remove")
74
+ target.unlink()
75
+ folder = target.parent
76
+ if any(folder.iterdir()):
77
+ print(f"removed {target}; kept {folder}, which holds other files")
78
+ else:
79
+ folder.rmdir()
80
+ print(f"removed {folder}")
81
+ return 0
82
+
83
+
84
+ def _print(_: argparse.Namespace) -> int:
85
+ # Bytes, not text: the console's encoding (cp1252 on Windows) and newline translation must
86
+ # not change the skill on its way to `reask print > SKILL.md`.
87
+ sys.stdout.flush()
88
+ sys.stdout.buffer.write(skill_text().encode("utf-8"))
89
+ sys.stdout.buffer.flush()
90
+ return 0
91
+
92
+
93
+ def main(argv: list[str] | None = None) -> int:
94
+ parser = argparse.ArgumentParser(prog="reask", description=__doc__)
95
+ parser.add_argument("--version", action="version", version=__version__)
96
+ sub = parser.add_subparsers(dest="command", required=True)
97
+
98
+ for name, func, help_text, force_help in (
99
+ ("install", _install, "write SKILL.md into a skills directory", "replace an edited copy"),
100
+ ("uninstall", _uninstall, "remove the installed SKILL.md", "remove an edited copy"),
101
+ ):
102
+ cmd = sub.add_parser(name, help=help_text)
103
+ where = cmd.add_mutually_exclusive_group()
104
+ where.add_argument("--project", action="store_true", help="use ./.claude/skills")
105
+ where.add_argument(
106
+ "--skills-dir",
107
+ type=Path,
108
+ help="skills directory (default: $CLAUDE_CONFIG_DIR/skills, else ~/.claude/skills)",
109
+ )
110
+ cmd.add_argument("--force", action="store_true", help=force_help)
111
+ cmd.set_defaults(func=func)
112
+
113
+ sub.add_parser("print", help="print SKILL.md to stdout").set_defaults(func=_print)
114
+
115
+ args = parser.parse_args(argv)
116
+ command: Callable[[argparse.Namespace], int] = args.func
117
+ try:
118
+ return command(args)
119
+ except (RefusedError, OSError) as error:
120
+ print(f"reask: {error}", file=sys.stderr)
121
+ return 1
122
+
123
+
124
+ if __name__ == "__main__":
125
+ raise SystemExit(main())
@@ -0,0 +1,143 @@
1
+ Metadata-Version: 2.5
2
+ Name: reask
3
+ Version: 0.1.0
4
+ Summary: An agent skill that re-asks a pending question as a self-contained briefing
5
+ Project-URL: Homepage, https://github.com/Nezhinskiy/reask
6
+ Project-URL: Source, https://github.com/Nezhinskiy/reask
7
+ Project-URL: Changelog, https://github.com/Nezhinskiy/reask/blob/main/CHANGELOG.md
8
+ Project-URL: Issues, https://github.com/Nezhinskiy/reask/issues
9
+ Author: Mikhail Nezhinsky
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: agent-skills,ai-agents,claude,claude-code,llm,skill
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Software Development
24
+ Requires-Python: >=3.11
25
+ Description-Content-Type: text/markdown
26
+
27
+ # reask
28
+
29
+ [![ci](https://github.com/Nezhinskiy/reask/actions/workflows/ci.yml/badge.svg)](https://github.com/Nezhinskiy/reask/actions/workflows/ci.yml)
30
+ [![PyPI](https://img.shields.io/pypi/v/reask)](https://pypi.org/project/reask/)
31
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](https://github.com/Nezhinskiy/reask/blob/main/LICENSE)
32
+
33
+ **Your agent asked you something and you cannot answer it.** You lost the context, the options
34
+ are jargon, or you cannot see what actually differs between them. Type `/reask`.
35
+
36
+ The agent asks the same question again, written for someone who just walked in: where the work
37
+ stands, one real example carried through every option, the pros *and* cons of each (the one it
38
+ recommends included), its recommendation, and one question you can answer. Under 500 words, in
39
+ your language.
40
+
41
+ reask is an [Agent Skill](https://agentskills.io): one file,
42
+ [`SKILL.md`](https://github.com/Nezhinskiy/reask/blob/main/skills/reask/SKILL.md), that Claude
43
+ Code, Codex, Cursor, Gemini CLI and other agents that read the format can load.
44
+
45
+ ## Install
46
+
47
+ **Any agent, with the [Skills CLI](https://github.com/vercel-labs/skills):**
48
+
49
+ ```bash
50
+ npx skills add Nezhinskiy/reask --global
51
+ ```
52
+
53
+ It asks which agents to install into; `--agent claude-code` (or `--agent '*'`) skips the
54
+ question, and leaving off `--global` installs into the current project.
55
+
56
+ **Claude Code plugin:**
57
+
58
+ ```text
59
+ /plugin marketplace add Nezhinskiy/reask
60
+ /plugin install reask@reask
61
+ ```
62
+
63
+ A plugin's skills are namespaced, so this one answers to `/reask:reask`. Updates arrive with
64
+ `/plugin marketplace update reask`.
65
+
66
+ **Python**, for Claude Code:
67
+
68
+ ```bash
69
+ uvx reask install
70
+ ```
71
+
72
+ or `pipx run reask install`. This writes `~/.claude/skills/reask/SKILL.md` (under
73
+ `$CLAUDE_CONFIG_DIR` if you set it); `--project` writes `./.claude/skills` instead and
74
+ `--skills-dir PATH` any other location. `reask print` prints the skill and `reask uninstall`
75
+ removes it. Neither command overwrites or deletes a copy you have edited unless you pass `--force`.
76
+
77
+ **claude.ai and Claude Desktop:** download `reask-skill.zip` from the
78
+ [latest release](https://github.com/Nezhinskiy/reask/releases/latest) and upload it under
79
+ Settings > Capabilities > Skills.
80
+
81
+ **By hand:** copy `skills/reask/SKILL.md` into a `reask/` folder in your agent's skills
82
+ directory.
83
+
84
+ ## Example
85
+
86
+ Mid-task, the agent asks:
87
+
88
+ > Should the price cache key include the locale, or should we normalise currency at read time?
89
+
90
+ You type `/reask`, and get:
91
+
92
+ > **Where we are and why this comes up.** We are putting a cache in front of the pricing API so
93
+ > product pages stop taking 800 ms. The cache works; what is open is what each entry is keyed
94
+ > on, and nothing merges until that is decided. The pricing API already returns a
95
+ > country-specific price for 3 SKUs. *Decided:* Redis, 10-minute expiry. *My assumption:* the 40
96
+ > locales we ship today are the ones to plan for.
97
+ >
98
+ > **A live example.** SKU `B-2210`, one of those 3, costs €49 in Germany and €45 in Austria.
99
+ > Viewed from Berlin (`de-DE`) and then from Vienna (`de-AT`):
100
+ >
101
+ > | | Key per locale | Normalise at read |
102
+ > |---|---|---|
103
+ > | Cache entries | 2 | 1 |
104
+ > | API calls on first views | 2 | 1 |
105
+ > | Price shown in Vienna | €45 | €49 |
106
+ >
107
+ > **Options.**
108
+ > *Key per locale*: one entry per SKU per locale.
109
+ > Pros: every country sees its own price; no conversion code.
110
+ > Cons: 40 times the entries; 40 cold-cache API calls per SKU instead of one.
111
+ > *Normalise at read*: one entry per SKU in euros, converted when read.
112
+ > Pros: 40 times fewer entries; one warm-up per SKU.
113
+ > Cons: shows the wrong price for those 3 SKUs today; a rate-table lookup on every read.
114
+ >
115
+ > **What I recommend.** Key per locale. Normalising shows Vienna €49 for `B-2210` today, not
116
+ > someday; 40 entries per SKU is about 2 MB at our catalogue size. I would change my mind if
117
+ > those 3 prices turn out to be a data error.
118
+ >
119
+ > **The question.** Key the cache per locale (recommended), or normalise currency at read time?
120
+
121
+ In Claude Code the question arrives with both answers as options to pick, the recommended one
122
+ first. This example was written by hand to show the format; a real briefing is built from your
123
+ session's facts.
124
+
125
+ ## Why it works
126
+
127
+ Agents ask good questions in the wrong register: they have the whole session in context and
128
+ you do not. reask fixes the register, not the question. It keeps the decision yours (it never
129
+ starts on its default), states each option's downside at full strength, and ties the
130
+ recommendation to facts from your work, so you can overrule it with one reply.
131
+
132
+ ## Contributing and security
133
+
134
+ Behaviour reports with a transcript are the most useful contribution; see
135
+ [CONTRIBUTING.md](https://github.com/Nezhinskiy/reask/blob/main/CONTRIBUTING.md). Report
136
+ vulnerabilities privately, as described in
137
+ [SECURITY.md](https://github.com/Nezhinskiy/reask/blob/main/SECURITY.md). Every release is built
138
+ in CI and carries a
139
+ [build provenance attestation](https://github.com/Nezhinskiy/reask/blob/main/SECURITY.md#verifying-what-you-installed).
140
+
141
+ ## License
142
+
143
+ [MIT](https://github.com/Nezhinskiy/reask/blob/main/LICENSE)
@@ -0,0 +1,8 @@
1
+ reask/__init__.py,sha256=5vdgnBNeGP1kwypjf7Qv9eOjNt20-CHsI3UMyXRurws,716
2
+ reask/cli.py,sha256=HClhYjzlQe93OAkNuNyMIKn1W8pIkt9cP8WeYT_045M,4557
3
+ reask/SKILL.md,sha256=lnz0A70v_Bv79MQyD3swgxMnx6W_PiGS24OidpnR0jk,5207
4
+ reask-0.1.0.dist-info/METADATA,sha256=NflQmL3xZiJhk3BDr2JOmtfCkU45ZZiJpsBve9pGAt0,6240
5
+ reask-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
6
+ reask-0.1.0.dist-info/entry_points.txt,sha256=H2C4TvGhWmgAR7bbSqmB2vuX7wxK9ujRoxDkWNepuXw,41
7
+ reask-0.1.0.dist-info/licenses/LICENSE,sha256=UfUIUnkfCOqg0V5JlJDIQbuBiJBWFKQJiWWnbztpfS8,1074
8
+ reask-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ reask = reask.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mikhail Nezhinsky
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.