typesafeai-cli 0.3.2__tar.gz → 0.3.3__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 (58) hide show
  1. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/.gitignore +1 -0
  2. typesafeai_cli-0.3.3/PKG-INFO +107 -0
  3. typesafeai_cli-0.3.3/README.md +83 -0
  4. typesafeai_cli-0.3.3/examples/README.md +12 -0
  5. typesafeai_cli-0.3.3/examples/fan-out/questions.json +28 -0
  6. typesafeai_cli-0.3.3/examples/fan-out/state.json +10 -0
  7. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/pyproject.toml +1 -1
  8. typesafeai_cli-0.3.3/src/typesafe_cli/__init__.py +1 -0
  9. typesafeai_cli-0.3.3/src/typesafe_cli/agent_files.py +69 -0
  10. typesafeai_cli-0.3.3/src/typesafe_cli/answers.py +86 -0
  11. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/src/typesafe_cli/cli.py +1 -1
  12. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/src/typesafe_cli/client.py +4 -1
  13. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/src/typesafe_cli/commands/ask.py +5 -0
  14. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/src/typesafe_cli/commands/decide.py +6 -1
  15. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/src/typesafe_cli/commands/eval.py +3 -0
  16. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/src/typesafe_cli/commands/find.py +1 -0
  17. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/src/typesafe_cli/commands/skills.py +42 -14
  18. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/src/typesafe_cli/data/typesafe-ai/CLI.md +1 -1
  19. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/src/typesafe_cli/data/typesafe-cli/SKILL.md +13 -5
  20. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/src/typesafe_cli/recipes/decide.py +49 -4
  21. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/src/typesafe_cli/recipes/extract.py +3 -1
  22. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/src/typesafe_cli/recipes/find.py +29 -10
  23. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/src/typesafe_cli/recipes/screen.py +8 -4
  24. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/src/typesafe_cli/recipes/verify.py +3 -1
  25. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/src/typesafe_cli/schema.py +2 -2
  26. typesafeai_cli-0.3.2/PKG-INFO +0 -138
  27. typesafeai_cli-0.3.2/README.md +0 -114
  28. typesafeai_cli-0.3.2/examples/README.md +0 -15
  29. typesafeai_cli-0.3.2/src/typesafe_cli/__init__.py +0 -1
  30. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/LICENSE +0 -0
  31. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/examples/code-change/questions.json +0 -0
  32. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/examples/code-change/state.json +0 -0
  33. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/examples/ticket/questions.json +0 -0
  34. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/examples/ticket/state.json +0 -0
  35. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/install.sh +0 -0
  36. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/src/typesafe_cli/commands/__init__.py +0 -0
  37. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/src/typesafe_cli/commands/agent.py +0 -0
  38. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/src/typesafe_cli/commands/auth.py +0 -0
  39. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/src/typesafe_cli/commands/extract.py +0 -0
  40. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/src/typesafe_cli/commands/models.py +0 -0
  41. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/src/typesafe_cli/commands/oneshot.py +0 -0
  42. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/src/typesafe_cli/commands/rank.py +0 -0
  43. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/src/typesafe_cli/commands/screen.py +0 -0
  44. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/src/typesafe_cli/commands/smoke.py +0 -0
  45. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/src/typesafe_cli/commands/suggest_skill.py +0 -0
  46. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/src/typesafe_cli/commands/verify.py +0 -0
  47. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/src/typesafe_cli/config.py +0 -0
  48. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/src/typesafe_cli/data/__init__.py +0 -0
  49. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/src/typesafe_cli/data/typesafe-ai/SKILL.md +0 -0
  50. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/src/typesafe_cli/detect.py +0 -0
  51. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/src/typesafe_cli/format.py +0 -0
  52. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/src/typesafe_cli/help.py +0 -0
  53. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/src/typesafe_cli/io.py +0 -0
  54. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/src/typesafe_cli/questions.py +0 -0
  55. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/src/typesafe_cli/recipes/__init__.py +0 -0
  56. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/src/typesafe_cli/recipes/rank.py +0 -0
  57. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/src/typesafe_cli/recipes/skills.py +0 -0
  58. {typesafeai_cli-0.3.2 → typesafeai_cli-0.3.3}/src/typesafe_cli/recipes/thresholds.py +0 -0
@@ -2,6 +2,7 @@
2
2
  .env
3
3
  PLAN.md
4
4
  plan.md
5
+ PHASE3.md
5
6
  docs/
6
7
  .venv/
7
8
  __pycache__/
@@ -0,0 +1,107 @@
1
+ Metadata-Version: 2.5
2
+ Name: typesafeai-cli
3
+ Version: 0.3.3
4
+ Summary: Agent CLI for TypeSafe System One (Jev)
5
+ Project-URL: Homepage, https://github.com/maddygoround/typesafeai-cli
6
+ Project-URL: Repository, https://github.com/maddygoround/typesafeai-cli
7
+ Project-URL: Issues, https://github.com/maddygoround/typesafeai-cli/issues
8
+ Project-URL: Releases, https://github.com/maddygoround/typesafeai-cli/releases
9
+ Author: safeaiforeveryone
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: cli,jev,typesafe
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Utilities
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: typer>=0.12
22
+ Requires-Dist: typesafe-sdk>=0.6.0
23
+ Description-Content-Type: text/markdown
24
+
25
+ # typesafe
26
+
27
+ CLI for [TypeSafe](https://typesafe.ai) Jev. Run it from a shell, or let an agent run it.
28
+
29
+ Jev answers `noul`, `choice`, and `score` questions over JSON `state`. It does not chat, and it only sees what you send. Invalid answers fail closed. Unused speculative heads cannot act. Ranked `find` lines are not evidence when the document has no answer.
30
+
31
+ ## Install
32
+
33
+ Python 3.10+. API key: [TypeSafe console](https://console.typesafe.ai/settings/keys).
34
+
35
+ ```bash
36
+ curl -fsSL https://github.com/maddygoround/typesafeai-cli/releases/latest/download/install.sh | bash
37
+ ```
38
+
39
+ ```bash
40
+ pipx install typesafeai-cli
41
+ ```
42
+
43
+ `pip install typesafeai-cli` is the same package.
44
+
45
+ ```bash
46
+ export TYPESAFE_API_KEY=apikey_…
47
+ ```
48
+
49
+ ## Usage
50
+
51
+ `typesafe ask` takes a state file and a questions file. Point each instruction at state with backticks.
52
+
53
+ ```json
54
+ {
55
+ "message": "I was charged twice for order A-104. Please refund the duplicate.",
56
+ "policy": "Duplicate charges are eligible for an immediate refund."
57
+ }
58
+ ```
59
+
60
+ ```json
61
+ {
62
+ "refund_requested": {
63
+ "type": "noul",
64
+ "instructions": "Does `message` request a refund?"
65
+ },
66
+ "intent": {
67
+ "type": "choice",
68
+ "instructions": "What does `message` ask for?",
69
+ "criteria": {
70
+ "refund": "The customer wants money returned",
71
+ "information": "Explanation only",
72
+ "other": "Something else"
73
+ }
74
+ }
75
+ }
76
+ ```
77
+
78
+ [`examples/ticket`](examples/ticket), [`examples/code-change`](examples/code-change), and [`examples/fan-out`](examples/fan-out) are full copies.
79
+
80
+ ```bash
81
+ typesafe ask --state-file state.json --questions-file questions.json
82
+ typesafe noul "Does this request a refund?" --state "I was charged twice."
83
+ typesafe decide --answers-file last.json
84
+ ```
85
+
86
+ Keep those JSON files under `${TMPDIR:-/tmp}/codex/<project>/`. `typesafe skills install` matches `npx skills add`: `--project` (default) or `--global` / `-g`. A TTY asks which. Global also writes a pointer into the user agent file (`~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md`, or `~/.grok/rules/typesafe-cli.md`) so investigations load `typesafe`. `typesafe auth status` reports `has_key` and nothing else.
87
+
88
+ ## Commands
89
+
90
+ | | |
91
+ | --- | --- |
92
+ | `ask` | State file + questions file |
93
+ | `noul` / `choice` / `score` | One question |
94
+ | `find` | Search a file |
95
+ | `rank` | Order a JSON shortlist |
96
+ | `extract` | Pick a span already in the text |
97
+ | `verify` | Check a claim against a source |
98
+ | `screen` | Jailbreak / injection / sensitive-data / harm |
99
+ | `suggest-skill` | At most one skill name |
100
+ | `decide` | Map answers to yes / no / uncertain (no HTTP) |
101
+ | `models` | Model aliases |
102
+ | `smoke` | Live docs quickstart |
103
+ | `auth status` | Whether a key is loaded |
104
+ | `agent schema` | JSON command tree |
105
+ | `skills install` | Official TypeSafe skill + typesafe-cli (`-g` for all projects) |
106
+
107
+ Exit `0` answered, `1` request failed or invalid answer, `2` bad flags or questions.
@@ -0,0 +1,83 @@
1
+ # typesafe
2
+
3
+ CLI for [TypeSafe](https://typesafe.ai) Jev. Run it from a shell, or let an agent run it.
4
+
5
+ Jev answers `noul`, `choice`, and `score` questions over JSON `state`. It does not chat, and it only sees what you send. Invalid answers fail closed. Unused speculative heads cannot act. Ranked `find` lines are not evidence when the document has no answer.
6
+
7
+ ## Install
8
+
9
+ Python 3.10+. API key: [TypeSafe console](https://console.typesafe.ai/settings/keys).
10
+
11
+ ```bash
12
+ curl -fsSL https://github.com/maddygoround/typesafeai-cli/releases/latest/download/install.sh | bash
13
+ ```
14
+
15
+ ```bash
16
+ pipx install typesafeai-cli
17
+ ```
18
+
19
+ `pip install typesafeai-cli` is the same package.
20
+
21
+ ```bash
22
+ export TYPESAFE_API_KEY=apikey_…
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ `typesafe ask` takes a state file and a questions file. Point each instruction at state with backticks.
28
+
29
+ ```json
30
+ {
31
+ "message": "I was charged twice for order A-104. Please refund the duplicate.",
32
+ "policy": "Duplicate charges are eligible for an immediate refund."
33
+ }
34
+ ```
35
+
36
+ ```json
37
+ {
38
+ "refund_requested": {
39
+ "type": "noul",
40
+ "instructions": "Does `message` request a refund?"
41
+ },
42
+ "intent": {
43
+ "type": "choice",
44
+ "instructions": "What does `message` ask for?",
45
+ "criteria": {
46
+ "refund": "The customer wants money returned",
47
+ "information": "Explanation only",
48
+ "other": "Something else"
49
+ }
50
+ }
51
+ }
52
+ ```
53
+
54
+ [`examples/ticket`](examples/ticket), [`examples/code-change`](examples/code-change), and [`examples/fan-out`](examples/fan-out) are full copies.
55
+
56
+ ```bash
57
+ typesafe ask --state-file state.json --questions-file questions.json
58
+ typesafe noul "Does this request a refund?" --state "I was charged twice."
59
+ typesafe decide --answers-file last.json
60
+ ```
61
+
62
+ Keep those JSON files under `${TMPDIR:-/tmp}/codex/<project>/`. `typesafe skills install` matches `npx skills add`: `--project` (default) or `--global` / `-g`. A TTY asks which. Global also writes a pointer into the user agent file (`~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md`, or `~/.grok/rules/typesafe-cli.md`) so investigations load `typesafe`. `typesafe auth status` reports `has_key` and nothing else.
63
+
64
+ ## Commands
65
+
66
+ | | |
67
+ | --- | --- |
68
+ | `ask` | State file + questions file |
69
+ | `noul` / `choice` / `score` | One question |
70
+ | `find` | Search a file |
71
+ | `rank` | Order a JSON shortlist |
72
+ | `extract` | Pick a span already in the text |
73
+ | `verify` | Check a claim against a source |
74
+ | `screen` | Jailbreak / injection / sensitive-data / harm |
75
+ | `suggest-skill` | At most one skill name |
76
+ | `decide` | Map answers to yes / no / uncertain (no HTTP) |
77
+ | `models` | Model aliases |
78
+ | `smoke` | Live docs quickstart |
79
+ | `auth status` | Whether a key is loaded |
80
+ | `agent schema` | JSON command tree |
81
+ | `skills install` | Official TypeSafe skill + typesafe-cli (`-g` for all projects) |
82
+
83
+ Exit `0` answered, `1` request failed or invalid answer, `2` bad flags or questions.
@@ -0,0 +1,12 @@
1
+ | Directory | |
2
+ | --- | --- |
3
+ | [ticket/](ticket/) | Support message, refund / intent / frustration |
4
+ | [code-change/](code-change/) | A skill hunk, whether the change needs real code in state |
5
+ | [fan-out/](fan-out/) | One operation Choice plus speculative `*_target` heads |
6
+
7
+ ```bash
8
+ WORKDIR="${TMPDIR:-/tmp}/codex/typesafeai-cli"
9
+ mkdir -p "$WORKDIR"
10
+ cp examples/ticket/*.json "$WORKDIR/"
11
+ typesafe ask --state-file "$WORKDIR/state.json" --questions-file "$WORKDIR/questions.json"
12
+ ```
@@ -0,0 +1,28 @@
1
+ {
2
+ "operation": {
3
+ "type": "choice",
4
+ "instructions": "Given `task` and `files[0].hunk`, what should the agent do next?",
5
+ "criteria": {
6
+ "EDIT": "Change the skill text so agents attach real code slices",
7
+ "ASK": "Ask the human before changing anything",
8
+ "SKIP": "Do nothing; the skill is already clear",
9
+ "abstain": "Not enough in state to decide"
10
+ }
11
+ },
12
+ "edit_target": {
13
+ "type": "choice",
14
+ "instructions": "If the next operation is EDIT, which observed hunk should be changed? This question chooses only a target; another question chooses the operation.",
15
+ "criteria": {
16
+ "files[0]": "The skill hunk already in `files[0].hunk`",
17
+ "none": "No offered hunk should be edited"
18
+ }
19
+ },
20
+ "ask_target": {
21
+ "type": "choice",
22
+ "instructions": "If the next operation is ASK, what should we ask the human? This question chooses only a target; another question chooses the operation.",
23
+ "criteria": {
24
+ "scope": "Ask whether the skill should stay questions-first",
25
+ "none": "Do not ask"
26
+ }
27
+ }
28
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "task": "A coding agent is about to edit a skill. Decide the next operation, then which hunk to touch if the operation is EDIT.",
3
+ "files": [
4
+ {
5
+ "path": "src/typesafe_cli/data/typesafe-cli/SKILL.md",
6
+ "line_range": "50-74",
7
+ "hunk": "If a path is code (`files[0].hunk`), the value must be the slice (path + line range + body), not a filename and not \"see the PR.\""
8
+ }
9
+ ]
10
+ }
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "typesafeai-cli"
3
- version = "0.3.2"
3
+ version = "0.3.3"
4
4
  description = "Agent CLI for TypeSafe System One (Jev)"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -0,0 +1 @@
1
+ __version__ = "0.3.3"
@@ -0,0 +1,69 @@
1
+ """Idempotent TypeSafe pointer in the user's global agent files (not the repo)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ START = "<!-- typesafe-cli:start -->"
8
+ END = "<!-- typesafe-cli:end -->"
9
+
10
+ SNIPPET = """<!-- typesafe-cli:start -->
11
+ ## TypeSafe CLI
12
+
13
+ When the user asks to investigate, triage, verify a claim, rank options, extract a span, screen a message, route a task, or make a typed yes/no or score judgment (Jev, noul, choice, TypeSafe), follow the **typesafe-cli** skill and run `typesafe`. Collect only the named state first. Do not curl TypeSafe.
14
+ <!-- typesafe-cli:end -->
15
+ """
16
+
17
+
18
+ def global_agent_paths(*, home: Path, agent: str) -> list[Path]:
19
+ """Home-level files each agent loads on every session.
20
+
21
+ Same split as `npx skills add -g`: user config, not the git tree.
22
+ """
23
+ claude = home / ".claude" / "CLAUDE.md"
24
+ codex = home / ".codex" / "AGENTS.md"
25
+ grok_rule = home / ".grok" / "rules" / "typesafe-cli.md"
26
+ agents = home / ".agents" / "AGENTS.md"
27
+ cursor = home / ".cursor" / "AGENTS.md"
28
+ gemini = home / ".gemini" / "GEMINI.md"
29
+ by_agent = {
30
+ "claude-code": [claude],
31
+ "codex": [codex],
32
+ "grok": [grok_rule, claude],
33
+ "cursor": [cursor],
34
+ "gemini-code": [gemini],
35
+ "opencode": [agents],
36
+ "generic-agent": [agents],
37
+ }
38
+ if agent in by_agent:
39
+ return by_agent[agent]
40
+ existing = [p for p in (claude, codex, grok_rule, agents, cursor, gemini) if p.is_file()]
41
+ return existing or [grok_rule]
42
+
43
+
44
+ def upsert_global_agent_files(*, home: Path, agent: str) -> list[Path]:
45
+ written: list[Path] = []
46
+ for path in global_agent_paths(home=home, agent=agent):
47
+ path.parent.mkdir(parents=True, exist_ok=True)
48
+ _upsert_snippet(path)
49
+ written.append(path)
50
+ return written
51
+
52
+
53
+ def _upsert_snippet(path: Path) -> None:
54
+ body = path.read_text(encoding="utf-8") if path.is_file() else ""
55
+ block = SNIPPET.strip() + "\n"
56
+ start = body.find(START)
57
+ end = body.find(END)
58
+ if start != -1 and end != -1 and end > start:
59
+ end += len(END)
60
+ while end < len(body) and body[end] == "\n":
61
+ end += 1
62
+ updated = body[:start].rstrip() + "\n\n" + block + body[end:].lstrip("\n")
63
+ if not updated.endswith("\n"):
64
+ updated += "\n"
65
+ elif body.strip():
66
+ updated = body.rstrip() + "\n\n" + block
67
+ else:
68
+ updated = block
69
+ path.write_text(updated, encoding="utf-8")
@@ -0,0 +1,86 @@
1
+ """Fail-closed checks on TypeSafe answers. Invalid JSON is not a decision."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from typing import Any
7
+
8
+
9
+ class AnswerError(ValueError):
10
+ """The envelope is not a usable typed answer; do not act on it."""
11
+
12
+
13
+ def validate_answers(answers: dict[str, Any], *, questions: dict[str, dict[str, Any]] | None = None) -> None:
14
+ if not isinstance(answers, dict) or not answers:
15
+ raise AnswerError("Invalid TypeSafe response; no action executed.")
16
+ questions = questions or {}
17
+ for qid, answer in answers.items():
18
+ if not isinstance(qid, str) or not qid or not isinstance(answer, dict):
19
+ raise AnswerError("Invalid TypeSafe response; no action executed.")
20
+ qtype = answer.get("type") or (questions.get(qid) or {}).get("type")
21
+ if qtype == "choice":
22
+ ids = _choice_ids(qid, answer, questions)
23
+ _validate_choice(answer, ids)
24
+ elif qtype == "noul":
25
+ _validate_noul(answer)
26
+ elif qtype == "score":
27
+ _validate_score(answer)
28
+ else:
29
+ raise AnswerError("Invalid TypeSafe response; no action executed.")
30
+
31
+
32
+ def _choice_ids(qid: str, answer: dict[str, Any], questions: dict[str, dict[str, Any]]) -> set[str]:
33
+ criteria = (questions.get(qid) or {}).get("criteria")
34
+ if isinstance(criteria, dict) and criteria:
35
+ return {str(key) for key in criteria}
36
+ probabilities = answer.get("probabilities") or {}
37
+ if isinstance(probabilities, dict) and probabilities:
38
+ return {str(key) for key in probabilities}
39
+ raise AnswerError("Invalid TypeSafe response; no action executed.")
40
+
41
+
42
+ def _validate_choice(answer: dict[str, Any], ids: set[str]) -> None:
43
+ try:
44
+ probabilities = {str(k): v for k, v in dict(answer["probabilities"]).items()}
45
+ confidence = answer["confidence"]
46
+ numbers = [*probabilities.values(), confidence]
47
+ choice = str(answer["choice"])
48
+ valid = (
49
+ choice in ids
50
+ and set(probabilities) == ids
51
+ and all(isinstance(n, (int, float)) and math.isfinite(n) and 0 <= n <= 1 for n in numbers)
52
+ and abs(sum(probabilities.values()) - 1) < 0.02
53
+ and probabilities[choice] >= max(probabilities.values()) - 1e-6
54
+ )
55
+ except (KeyError, TypeError, ValueError):
56
+ valid = False
57
+ if not valid:
58
+ raise AnswerError("Invalid TypeSafe response; no action executed.")
59
+
60
+
61
+ def _validate_noul(answer: dict[str, Any]) -> None:
62
+ try:
63
+ value = answer["noul"]
64
+ valid = isinstance(value, (int, float)) and math.isfinite(value) and 0 <= value <= 1
65
+ except (KeyError, TypeError):
66
+ valid = False
67
+ if not valid:
68
+ raise AnswerError("Invalid TypeSafe response; no action executed.")
69
+
70
+
71
+ def _validate_score(answer: dict[str, Any]) -> None:
72
+ try:
73
+ score = answer["score"]
74
+ confidence = answer.get("confidence")
75
+ valid = isinstance(score, (int, float)) and math.isfinite(score)
76
+ if confidence is not None:
77
+ valid = (
78
+ valid
79
+ and isinstance(confidence, (int, float))
80
+ and math.isfinite(confidence)
81
+ and 0 <= confidence <= 1
82
+ )
83
+ except (KeyError, TypeError):
84
+ valid = False
85
+ if not valid:
86
+ raise AnswerError("Invalid TypeSafe response; no action executed.")
@@ -25,7 +25,7 @@ app = typer.Typer(
25
25
  no_args_is_help=True,
26
26
  add_completion=False,
27
27
  pretty_exceptions_enable=False,
28
- help="Agent CLI for TypeSafe System One (Jev). Typed judgments, not chat. Agents must not read TYPESAFE_* env vars or key files; use typesafe auth status.",
28
+ help="Agent CLI for TypeSafe System One (Jev). Typed judgments, not chat. Invalid answers fail closed. Agents must not read TYPESAFE_* env vars or key files; use typesafe auth status.",
29
29
  )
30
30
 
31
31
 
@@ -4,6 +4,7 @@ from typing import Any
4
4
 
5
5
  from typesafe_sdk import Choice, Noul, Score, TypeSafeClient
6
6
 
7
+ from typesafe_cli.answers import validate_answers
7
8
  from typesafe_cli.config import Config
8
9
 
9
10
 
@@ -87,7 +88,9 @@ def system_one(
87
88
  questions=to_sdk_questions(questions),
88
89
  model=resolved_model,
89
90
  )
90
- return answers_to_dict(response)
91
+ data = answers_to_dict(response)
92
+ validate_answers(data["answers"], questions=questions)
93
+ return data
91
94
 
92
95
 
93
96
  def list_models(*, config: Config) -> dict[str, Any]:
@@ -7,6 +7,7 @@ from typing import Any
7
7
  import typer
8
8
  from typesafe_sdk import TypeSafeError
9
9
 
10
+ from typesafe_cli.answers import AnswerError
10
11
  from typesafe_cli.client import system_one
11
12
  from typesafe_cli.config import load_config
12
13
  from typesafe_cli.io import emit_success, fail
@@ -38,6 +39,8 @@ def ask(
38
39
 
39
40
  try:
40
41
  data = system_one(config=config, state=resolved_state, questions=questions, model=model)
42
+ except AnswerError as exc:
43
+ fail(code="invalid_answer", message=str(exc), exit_code=1)
41
44
  except TypeSafeError as exc:
42
45
  fail(code="request", message=str(exc), exit_code=1)
43
46
  except Exception as exc: # noqa: BLE001 — surface SDK/network failures as exit 1
@@ -83,6 +86,8 @@ def run_evaluation(
83
86
  fail(code="auth", message="missing TYPESAFE_API_KEY (or --key / --creds)", exit_code=1)
84
87
  try:
85
88
  data = system_one(config=config, state=state, questions=questions, model=model)
89
+ except AnswerError as exc:
90
+ fail(code="invalid_answer", message=str(exc), exit_code=1)
86
91
  except TypeSafeError as exc:
87
92
  fail(code="request", message=str(exc), exit_code=1)
88
93
  except Exception as exc: # noqa: BLE001
@@ -7,7 +7,7 @@ import typer
7
7
 
8
8
  from typesafe_cli.io import emit_success, fail
9
9
  from typesafe_cli.questions import QuestionError
10
- from typesafe_cli.recipes.decide import apply_decide, extract_answers, parse_noul_band
10
+ from typesafe_cli.recipes.decide import apply_decide, decide_action, extract_answers, parse_noul_band
11
11
  from typesafe_cli.recipes.thresholds import CHOICE_MIN_TOP_P, NOUL_UNCERTAIN_HIGH, NOUL_UNCERTAIN_LOW
12
12
 
13
13
 
@@ -34,9 +34,14 @@ def decide(
34
34
  decisions = apply_decide(answers, noul_low=low, noul_high=high, choice_min_p=choice_min_p)
35
35
  except (OSError, json.JSONDecodeError, QuestionError) as exc:
36
36
  fail(code="usage", message=str(exc), exit_code=2)
37
+ gate = decide_action(decisions)
37
38
  emit_success(
38
39
  data={
39
40
  "decisions": decisions,
41
+ "active": gate["active"],
42
+ "ignored": gate["ignored"],
43
+ "action": gate["action"],
44
+ "needs_verify": gate["needs_verify"],
40
45
  "policy": {"noul_band": [low, high], "choice_min_p": choice_min_p},
41
46
  },
42
47
  command="decide",
@@ -5,6 +5,7 @@ from typing import Any
5
5
 
6
6
  from typesafe_sdk import TypeSafeError
7
7
 
8
+ from typesafe_cli.answers import AnswerError
8
9
  from typesafe_cli.client import system_one
9
10
  from typesafe_cli.config import load_config
10
11
  from typesafe_cli.io import fail
@@ -31,6 +32,8 @@ def call_system_one(
31
32
  config = ready_config(key=key, creds=creds, model=model)
32
33
  try:
33
34
  return system_one(config=config, state=state, questions=questions, model=model)
35
+ except AnswerError as exc:
36
+ fail(code="invalid_answer", message=str(exc), exit_code=1)
34
37
  except TypeSafeError as exc:
35
38
  fail(code="request", message=str(exc), exit_code=1)
36
39
  except Exception as exc: # noqa: BLE001
@@ -45,6 +45,7 @@ def find(
45
45
  "file": str(file),
46
46
  "verdict": merged["verdict"],
47
47
  "exists": merged["exists"],
48
+ "usable": merged["usable"],
48
49
  "lines": merged["query_lines"][:20],
49
50
  "model": merged["model"],
50
51
  "usage": merged["usage"],
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import sys
3
4
  import urllib.error
4
5
  import urllib.request
5
6
  from importlib.resources import files
@@ -7,6 +8,7 @@ from pathlib import Path
7
8
 
8
9
  import typer
9
10
 
11
+ from typesafe_cli.agent_files import upsert_global_agent_files
10
12
  from typesafe_cli.detect import detect_agent_info, resolve_agent
11
13
  from typesafe_cli.io import emit_success, fail
12
14
 
@@ -25,16 +27,19 @@ SKILL_DIR_BY_AGENT = {
25
27
  "generic-agent": ".agents/skills",
26
28
  }
27
29
 
28
- USER_SKILL_DIR_BY_AGENT = {
29
- "claude-code": Path.home() / ".claude" / "skills",
30
- "cursor": Path.home() / ".cursor" / "skills",
31
- "windsurf": Path.home() / ".windsurf" / "skills",
32
- "gemini-code": Path.home() / ".gemini" / "skills",
33
- "codex": Path.home() / ".agents" / "skills",
34
- "opencode": Path.home() / ".agents" / "skills",
35
- "grok": Path.home() / ".agents" / "skills",
36
- "generic-agent": Path.home() / ".agents" / "skills",
37
- }
30
+ def _user_skill_dir(agent: str, *, home: Path | None = None) -> Path:
31
+ root = home or Path.home()
32
+ mapping = {
33
+ "claude-code": root / ".claude" / "skills",
34
+ "cursor": root / ".cursor" / "skills",
35
+ "windsurf": root / ".windsurf" / "skills",
36
+ "gemini-code": root / ".gemini" / "skills",
37
+ "codex": root / ".codex" / "skills",
38
+ "opencode": root / ".agents" / "skills",
39
+ "grok": root / ".agents" / "skills",
40
+ "generic-agent": root / ".agents" / "skills",
41
+ }
42
+ return mapping.get(agent, root / ".agents" / "skills")
38
43
 
39
44
  EXISTING_PROJECT_DIRS = (
40
45
  ".agents/skills",
@@ -48,7 +53,11 @@ EXISTING_PROJECT_DIRS = (
48
53
  def install(
49
54
  target: str = typer.Option("auto", "--target", help="claude, cursor, codex, grok, or auto"),
50
55
  directory: Path | None = typer.Option(None, "--dir", help="Explicit skills directory"),
51
- project: bool = typer.Option(True, "--project/--global", help="Install into the current project (default)"),
56
+ global_install: bool = typer.Option(
57
+ False, "--global", "-g", help="Install for this user, all projects (same as npx skills add -g)"
58
+ ),
59
+ project: bool = typer.Option(False, "--project", help="Install into the current project only"),
60
+ yes: bool = typer.Option(False, "--yes", "-y", help="Do not ask global vs project"),
52
61
  offline: bool = typer.Option(False, "--offline", help="Use the vendored official skill, skip GitHub"),
53
62
  ) -> None:
54
63
  try:
@@ -59,7 +68,8 @@ def install(
59
68
  fail(code="request", message=f"could not load skills: {exc}", exit_code=1)
60
69
 
61
70
  agent = resolve_agent(target=target)
62
- dest_root = directory if directory is not None else _skills_root(agent=agent, project=project)
71
+ scope = _resolve_scope(global_install=global_install, project=project, yes=yes)
72
+ dest_root = directory if directory is not None else _skills_root(agent=agent, project=scope == "project")
63
73
  written: list[str] = []
64
74
  try:
65
75
  pairs = [
@@ -71,15 +81,19 @@ def install(
71
81
  path.parent.mkdir(parents=True, exist_ok=True)
72
82
  path.write_text(body, encoding="utf-8")
73
83
  written.append(str(path))
84
+ if scope == "global" and directory is None:
85
+ for path in upsert_global_agent_files(home=Path.home(), agent=agent):
86
+ written.append(str(path))
74
87
  except OSError as exc:
75
88
  fail(code="request", message=str(exc), exit_code=1)
76
89
 
77
90
  emit_success(
78
91
  data={
79
92
  "agent": agent,
93
+ "scope": scope,
80
94
  "written": written,
81
95
  "source": "offline-vendored" if offline else "github-or-vendored",
82
- "note": "Installed typesafe-ai (design) and typesafe-cli (collect state, then typesafe ask). Agents cannot access TYPESAFE_* env vars.",
96
+ "note": "Installed typesafe-ai (design) and typesafe-cli (collect state, then typesafe ask). Global installs also write a TypeSafe pointer into the user agent file (CLAUDE.md / AGENTS.md / ~/.grok/rules). Agents cannot access TYPESAFE_* env vars.",
83
97
  },
84
98
  command="skills install",
85
99
  )
@@ -135,4 +149,18 @@ def _skills_root(*, agent: str, project: bool) -> Path:
135
149
  return candidate
136
150
  rel = SKILL_DIR_BY_AGENT.get(agent, ".agents/skills")
137
151
  return cwd / rel
138
- return USER_SKILL_DIR_BY_AGENT.get(agent, Path.home() / ".agents" / "skills")
152
+ return _user_skill_dir(agent)
153
+
154
+
155
+ def _resolve_scope(*, global_install: bool, project: bool, yes: bool) -> str:
156
+ if global_install and project:
157
+ fail(code="usage", message="use only one of --global or --project", exit_code=2)
158
+ if global_install:
159
+ return "global"
160
+ if project:
161
+ return "project"
162
+ if yes or not sys.stdin.isatty():
163
+ return "project"
164
+ if typer.confirm("Install globally for all projects on this machine?", default=False):
165
+ return "global"
166
+ return "project"
@@ -1,6 +1,6 @@
1
1
  # Using the typesafe CLI
2
2
 
3
- Live evaluations: follow the **typesafe-cli** skill. Prefer `find`, `rank`, `extract`, `verify`, `screen`, `suggest-skill`, and `decide` when those flags fit; otherwise collect local state, write JSON under `${TMPDIR:-/tmp}/codex/<project>/`, then `typesafe ask`. Never drop those files in the git tree. This sidecar does not replace that skill or the official TypeSafe skill.
3
+ Live evaluations: follow the **typesafe-cli** skill. Prefer `find`, `rank`, `extract`, `verify`, `screen`, `suggest-skill`, and `decide` when those flags fit; otherwise collect local state, write JSON under `${TMPDIR:-/tmp}/codex/<project>/`, then `typesafe ask`. Invalid answers fail closed. Never drop those files in the git tree. This sidecar does not replace that skill or the official TypeSafe skill.
4
4
 
5
5
  ## Secrets
6
6