saga-cli 0.1.0__tar.gz → 0.3.0__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.
- saga_cli-0.3.0/.github/CODEOWNERS +3 -0
- {saga_cli-0.1.0 → saga_cli-0.3.0}/PKG-INFO +78 -8
- {saga_cli-0.1.0 → saga_cli-0.3.0}/README.md +77 -7
- {saga_cli-0.1.0 → saga_cli-0.3.0}/pyproject.toml +1 -1
- {saga_cli-0.1.0 → saga_cli-0.3.0}/saga/assets/saga.css +38 -16
- {saga_cli-0.1.0 → saga_cli-0.3.0}/saga/assets/saga.js +58 -21
- {saga_cli-0.1.0 → saga_cli-0.3.0}/saga/assets/tokens.css +6 -8
- saga_cli-0.3.0/saga/cli.py +186 -0
- saga_cli-0.3.0/saga/diff.py +169 -0
- saga_cli-0.3.0/saga/generate.py +345 -0
- {saga_cli-0.1.0 → saga_cli-0.3.0}/saga/model.py +11 -5
- saga_cli-0.3.0/saga/prompts/saga.md +59 -0
- {saga_cli-0.1.0 → saga_cli-0.3.0}/saga/render.py +38 -13
- saga_cli-0.3.0/tests/cassettes/generate_anthropic.yaml +162 -0
- {saga_cli-0.1.0 → saga_cli-0.3.0}/tests/test_cli.py +84 -5
- saga_cli-0.3.0/tests/test_diff.py +217 -0
- saga_cli-0.3.0/tests/test_generate.py +418 -0
- {saga_cli-0.1.0 → saga_cli-0.3.0}/tests/test_model.py +5 -3
- {saga_cli-0.1.0 → saga_cli-0.3.0}/tests/test_render.py +29 -5
- {saga_cli-0.1.0 → saga_cli-0.3.0}/uv.lock +1 -1
- saga_cli-0.1.0/saga/cli.py +0 -100
- saga_cli-0.1.0/saga/diff.py +0 -79
- saga_cli-0.1.0/saga/generate.py +0 -182
- saga_cli-0.1.0/saga/prompts/saga.md +0 -50
- saga_cli-0.1.0/tests/cassettes/generate_anthropic.yaml +0 -168
- saga_cli-0.1.0/tests/test_diff.py +0 -95
- saga_cli-0.1.0/tests/test_generate.py +0 -153
- {saga_cli-0.1.0 → saga_cli-0.3.0}/.github/workflows/ci.yml +0 -0
- {saga_cli-0.1.0 → saga_cli-0.3.0}/.gitignore +0 -0
- {saga_cli-0.1.0 → saga_cli-0.3.0}/CONTRIBUTING.md +0 -0
- {saga_cli-0.1.0 → saga_cli-0.3.0}/LICENSE +0 -0
- {saga_cli-0.1.0 → saga_cli-0.3.0}/RELEASING.md +0 -0
- {saga_cli-0.1.0 → saga_cli-0.3.0}/docs/example.html +0 -0
- {saga_cli-0.1.0 → saga_cli-0.3.0}/saga/__init__.py +0 -0
- {saga_cli-0.1.0 → saga_cli-0.3.0}/saga/__main__.py +0 -0
- {saga_cli-0.1.0 → saga_cli-0.3.0}/saga/assets/base.css +0 -0
- {saga_cli-0.1.0 → saga_cli-0.3.0}/saga/comments.py +0 -0
- {saga_cli-0.1.0 → saga_cli-0.3.0}/skills/generate_saga/SKILL.md +0 -0
- {saga_cli-0.1.0 → saga_cli-0.3.0}/skills/read_saga_comments/SKILL.md +0 -0
- {saga_cli-0.1.0 → saga_cli-0.3.0}/tests/conftest.py +0 -0
- {saga_cli-0.1.0 → saga_cli-0.3.0}/tests/test_comments.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: saga-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Generate a self-contained static HTML saga of a git diff — a chapter-by-chapter guided tour of a change.
|
|
5
5
|
Project-URL: Homepage, https://github.com/JakeBeresford/saga
|
|
6
6
|
Project-URL: Repository, https://github.com/JakeBeresford/saga
|
|
@@ -42,8 +42,11 @@ the data). Open it offline, email it, commit it, or drop it on any static host.
|
|
|
42
42
|
|
|
43
43
|
- Python 3.11+
|
|
44
44
|
- `git`
|
|
45
|
+
- The [`gh`](https://cli.github.com) CLI, authenticated — only for reviewing a PR
|
|
46
|
+
by URL (`saga <pr-url>`) or pushing review comments
|
|
45
47
|
- An API key for your chosen provider, in the standard environment variable:
|
|
46
|
-
`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or `OPENROUTER_API_KEY`
|
|
48
|
+
`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or `OPENROUTER_API_KEY` — or a running
|
|
49
|
+
local server (Ollama / LM Studio) and no key, with a `local/` model
|
|
47
50
|
|
|
48
51
|
Generation is one structured LLM call made through [`instructor`](https://python.useinstructor.com).
|
|
49
52
|
|
|
@@ -64,7 +67,13 @@ e.g. `uv tool install /path/to/saga`. To upgrade: `uv tool install --force saga-
|
|
|
64
67
|
|
|
65
68
|
## Usage
|
|
66
69
|
|
|
67
|
-
From inside the repo you want to review:
|
|
70
|
+
From inside the repo, on the branch you want to review:
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
saga
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
From inside the repo, reviewing a different branch:
|
|
68
77
|
|
|
69
78
|
```sh
|
|
70
79
|
saga --base main --head my-feature -o saga.html --open
|
|
@@ -76,14 +85,32 @@ Run it from anywhere with `--repo`:
|
|
|
76
85
|
saga --repo ~/src/some-project --base main --head my-feature -o out.html
|
|
77
86
|
```
|
|
78
87
|
|
|
88
|
+
### From a GitHub PR URL
|
|
89
|
+
|
|
90
|
+
Point saga straight at a pull request by passing its URL as the first argument.
|
|
91
|
+
No checkout is needed and it works from any directory:
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
saga https://github.com/owner/repo/pull/5
|
|
95
|
+
saga https://github.com/owner/repo/pull/5 --model openai/gpt-4o -o pr5.html
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
This fetches the PR's diff, commits, and branch names with the
|
|
99
|
+
[`gh`](https://cli.github.com) CLI (so `gh` must be installed and authenticated),
|
|
100
|
+
then builds the saga exactly as it would for a local branch. In this mode the PR
|
|
101
|
+
defines the change set, so `--base`, `--head`, and `--repo` are ignored.
|
|
102
|
+
|
|
103
|
+
The optional positional argument is a GitHub PR URL (as above); all flags below
|
|
104
|
+
apply to both modes.
|
|
105
|
+
|
|
79
106
|
| Flag | Default | Meaning |
|
|
80
107
|
| ---------------------- | --------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
81
|
-
| `--base` |
|
|
82
|
-
| `--head` | current branch | Head ref to walk through
|
|
108
|
+
| `--base` | auto-detected | Base ref to diff against (defaults to the repo's default branch, e.g. `origin/main`); local mode only |
|
|
109
|
+
| `--head` | current branch | Head ref to walk through; local mode only |
|
|
83
110
|
| `--intent PATH` | — | Optional plan/spec describing the change's intent, for plan-aware narration and deviation flagging |
|
|
84
111
|
| `--model` | `anthropic/claude-opus-4-8` | `provider/model` string (see [Providers](#providers)); also `$SAGA_MODEL` |
|
|
85
112
|
| `-o, --output` | `saga.html` | Output file |
|
|
86
|
-
| `--repo` | cwd | A path inside the target git repo
|
|
113
|
+
| `--repo` | cwd | A path inside the target git repo; local mode only |
|
|
87
114
|
| `--open` / `--no-open` | on | Open the result in a browser (on by default; `--no-open` to disable) |
|
|
88
115
|
|
|
89
116
|
## Providers
|
|
@@ -97,6 +124,8 @@ the matching API key:
|
|
|
97
124
|
| Anthropic | `anthropic/claude-opus-4-8` | `ANTHROPIC_API_KEY` |
|
|
98
125
|
| OpenAI | `openai/gpt-4o` | `OPENAI_API_KEY` |
|
|
99
126
|
| OpenRouter | `openrouter/anthropic/claude-3.5-sonnet` | `OPENROUTER_API_KEY` |
|
|
127
|
+
| Local | `local/qwen2.5-coder:14b` | none |
|
|
128
|
+
| Claude CLI | `claude-cli` or `claude-cli/sonnet` | none (Claude Code login) |
|
|
100
129
|
|
|
101
130
|
```sh
|
|
102
131
|
export SAGA_MODEL=openai/gpt-4o
|
|
@@ -104,6 +133,47 @@ export OPENAI_API_KEY=sk-…
|
|
|
104
133
|
saga --base main --head my-feature -o saga.html
|
|
105
134
|
```
|
|
106
135
|
|
|
136
|
+
### Local LLMs (Ollama / LM Studio)
|
|
137
|
+
|
|
138
|
+
A `local/` model runs against any OpenAI-compatible local server, with no API
|
|
139
|
+
key. Pull a capable coder model first (`ollama pull qwen2.5-coder:14b`), then:
|
|
140
|
+
|
|
141
|
+
```sh
|
|
142
|
+
saga --model local/qwen2.5-coder:14b --base main --head my-feature
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`local/` defaults to Ollama's endpoint (`http://localhost:11434/v1`). Point it
|
|
146
|
+
at another server — e.g. LM Studio — with `SAGA_LOCAL_BASE_URL`:
|
|
147
|
+
|
|
148
|
+
```sh
|
|
149
|
+
export SAGA_LOCAL_BASE_URL=http://localhost:1234/v1 # LM Studio
|
|
150
|
+
saga --model local/your-loaded-model --base main --head my-feature
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Two caveats: saga requires schema-valid JSON output, so use an instruction-tuned
|
|
154
|
+
model that follows JSON prompting reliably; and the full diff plus a 16k output
|
|
155
|
+
budget can exceed a small model's context window — prefer larger-context models
|
|
156
|
+
and expect weaker narration than a frontier hosted model.
|
|
157
|
+
|
|
158
|
+
### Claude Code CLI (no API key)
|
|
159
|
+
|
|
160
|
+
If you don't have an Anthropic API key but you are logged into the
|
|
161
|
+
[Claude Code](https://claude.com/claude-code) CLI — for example with a Claude
|
|
162
|
+
Pro/Max subscription — the `claude-cli` model routes generation through
|
|
163
|
+
`claude -p` instead of the API, reusing that login:
|
|
164
|
+
|
|
165
|
+
```sh
|
|
166
|
+
saga --model claude-cli --base main --head my-feature
|
|
167
|
+
saga --model claude-cli/sonnet --base main --head my-feature # pin a model
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
This shells out to the `claude` binary (which must be on your `PATH` and logged
|
|
171
|
+
in), constrains its output to saga's schema via `--json-schema`, and runs it as
|
|
172
|
+
a plain transform with no tools. `ANTHROPIC_API_KEY` and `ANTHROPIC_AUTH_TOKEN`
|
|
173
|
+
are dropped for the subprocess so it uses your Claude Code login rather than
|
|
174
|
+
silently billing an API key. It is slower than a direct API call (Claude Code
|
|
175
|
+
boots an agent per run) and subject to your subscription's usage limits.
|
|
176
|
+
|
|
107
177
|
## As a Claude Code skill
|
|
108
178
|
|
|
109
179
|
The `skills/` directory contains two Claude Code skills. To install both:
|
|
@@ -144,7 +214,8 @@ it on GitHub. Requires the [`gh`](https://cli.github.com) CLI, authenticated.
|
|
|
144
214
|
|
|
145
215
|
## How it works
|
|
146
216
|
|
|
147
|
-
1. `diff.py` computes `git diff base...head` (no checkout) and the commit list
|
|
217
|
+
1. `diff.py` computes `git diff base...head` (no checkout) and the commit list —
|
|
218
|
+
or, given a PR URL, fetches the same diff and metadata from GitHub via `gh`.
|
|
148
219
|
2. `model.py` splits the diff into stable-id hunks (`h0, h1, …`).
|
|
149
220
|
3. `generate.py` sends the labeled diff + commits (+ optional intent) to the chosen
|
|
150
221
|
model via `instructor`, which returns chapters as schema-validated JSON. Coverage
|
|
@@ -154,5 +225,4 @@ it on GitHub. Requires the [`gh`](https://cli.github.com) CLI, authenticated.
|
|
|
154
225
|
|
|
155
226
|
## Not included (yet)
|
|
156
227
|
|
|
157
|
-
- Support for Local LLMs via ollama / LM Studio
|
|
158
228
|
- A GitHub Action to auto-generate saga on PRs.
|
|
@@ -15,8 +15,11 @@ the data). Open it offline, email it, commit it, or drop it on any static host.
|
|
|
15
15
|
|
|
16
16
|
- Python 3.11+
|
|
17
17
|
- `git`
|
|
18
|
+
- The [`gh`](https://cli.github.com) CLI, authenticated — only for reviewing a PR
|
|
19
|
+
by URL (`saga <pr-url>`) or pushing review comments
|
|
18
20
|
- An API key for your chosen provider, in the standard environment variable:
|
|
19
|
-
`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or `OPENROUTER_API_KEY`
|
|
21
|
+
`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or `OPENROUTER_API_KEY` — or a running
|
|
22
|
+
local server (Ollama / LM Studio) and no key, with a `local/` model
|
|
20
23
|
|
|
21
24
|
Generation is one structured LLM call made through [`instructor`](https://python.useinstructor.com).
|
|
22
25
|
|
|
@@ -37,7 +40,13 @@ e.g. `uv tool install /path/to/saga`. To upgrade: `uv tool install --force saga-
|
|
|
37
40
|
|
|
38
41
|
## Usage
|
|
39
42
|
|
|
40
|
-
From inside the repo you want to review:
|
|
43
|
+
From inside the repo, on the branch you want to review:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
saga
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
From inside the repo, reviewing a different branch:
|
|
41
50
|
|
|
42
51
|
```sh
|
|
43
52
|
saga --base main --head my-feature -o saga.html --open
|
|
@@ -49,14 +58,32 @@ Run it from anywhere with `--repo`:
|
|
|
49
58
|
saga --repo ~/src/some-project --base main --head my-feature -o out.html
|
|
50
59
|
```
|
|
51
60
|
|
|
61
|
+
### From a GitHub PR URL
|
|
62
|
+
|
|
63
|
+
Point saga straight at a pull request by passing its URL as the first argument.
|
|
64
|
+
No checkout is needed and it works from any directory:
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
saga https://github.com/owner/repo/pull/5
|
|
68
|
+
saga https://github.com/owner/repo/pull/5 --model openai/gpt-4o -o pr5.html
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
This fetches the PR's diff, commits, and branch names with the
|
|
72
|
+
[`gh`](https://cli.github.com) CLI (so `gh` must be installed and authenticated),
|
|
73
|
+
then builds the saga exactly as it would for a local branch. In this mode the PR
|
|
74
|
+
defines the change set, so `--base`, `--head`, and `--repo` are ignored.
|
|
75
|
+
|
|
76
|
+
The optional positional argument is a GitHub PR URL (as above); all flags below
|
|
77
|
+
apply to both modes.
|
|
78
|
+
|
|
52
79
|
| Flag | Default | Meaning |
|
|
53
80
|
| ---------------------- | --------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
54
|
-
| `--base` |
|
|
55
|
-
| `--head` | current branch | Head ref to walk through
|
|
81
|
+
| `--base` | auto-detected | Base ref to diff against (defaults to the repo's default branch, e.g. `origin/main`); local mode only |
|
|
82
|
+
| `--head` | current branch | Head ref to walk through; local mode only |
|
|
56
83
|
| `--intent PATH` | — | Optional plan/spec describing the change's intent, for plan-aware narration and deviation flagging |
|
|
57
84
|
| `--model` | `anthropic/claude-opus-4-8` | `provider/model` string (see [Providers](#providers)); also `$SAGA_MODEL` |
|
|
58
85
|
| `-o, --output` | `saga.html` | Output file |
|
|
59
|
-
| `--repo` | cwd | A path inside the target git repo
|
|
86
|
+
| `--repo` | cwd | A path inside the target git repo; local mode only |
|
|
60
87
|
| `--open` / `--no-open` | on | Open the result in a browser (on by default; `--no-open` to disable) |
|
|
61
88
|
|
|
62
89
|
## Providers
|
|
@@ -70,6 +97,8 @@ the matching API key:
|
|
|
70
97
|
| Anthropic | `anthropic/claude-opus-4-8` | `ANTHROPIC_API_KEY` |
|
|
71
98
|
| OpenAI | `openai/gpt-4o` | `OPENAI_API_KEY` |
|
|
72
99
|
| OpenRouter | `openrouter/anthropic/claude-3.5-sonnet` | `OPENROUTER_API_KEY` |
|
|
100
|
+
| Local | `local/qwen2.5-coder:14b` | none |
|
|
101
|
+
| Claude CLI | `claude-cli` or `claude-cli/sonnet` | none (Claude Code login) |
|
|
73
102
|
|
|
74
103
|
```sh
|
|
75
104
|
export SAGA_MODEL=openai/gpt-4o
|
|
@@ -77,6 +106,47 @@ export OPENAI_API_KEY=sk-…
|
|
|
77
106
|
saga --base main --head my-feature -o saga.html
|
|
78
107
|
```
|
|
79
108
|
|
|
109
|
+
### Local LLMs (Ollama / LM Studio)
|
|
110
|
+
|
|
111
|
+
A `local/` model runs against any OpenAI-compatible local server, with no API
|
|
112
|
+
key. Pull a capable coder model first (`ollama pull qwen2.5-coder:14b`), then:
|
|
113
|
+
|
|
114
|
+
```sh
|
|
115
|
+
saga --model local/qwen2.5-coder:14b --base main --head my-feature
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`local/` defaults to Ollama's endpoint (`http://localhost:11434/v1`). Point it
|
|
119
|
+
at another server — e.g. LM Studio — with `SAGA_LOCAL_BASE_URL`:
|
|
120
|
+
|
|
121
|
+
```sh
|
|
122
|
+
export SAGA_LOCAL_BASE_URL=http://localhost:1234/v1 # LM Studio
|
|
123
|
+
saga --model local/your-loaded-model --base main --head my-feature
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Two caveats: saga requires schema-valid JSON output, so use an instruction-tuned
|
|
127
|
+
model that follows JSON prompting reliably; and the full diff plus a 16k output
|
|
128
|
+
budget can exceed a small model's context window — prefer larger-context models
|
|
129
|
+
and expect weaker narration than a frontier hosted model.
|
|
130
|
+
|
|
131
|
+
### Claude Code CLI (no API key)
|
|
132
|
+
|
|
133
|
+
If you don't have an Anthropic API key but you are logged into the
|
|
134
|
+
[Claude Code](https://claude.com/claude-code) CLI — for example with a Claude
|
|
135
|
+
Pro/Max subscription — the `claude-cli` model routes generation through
|
|
136
|
+
`claude -p` instead of the API, reusing that login:
|
|
137
|
+
|
|
138
|
+
```sh
|
|
139
|
+
saga --model claude-cli --base main --head my-feature
|
|
140
|
+
saga --model claude-cli/sonnet --base main --head my-feature # pin a model
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
This shells out to the `claude` binary (which must be on your `PATH` and logged
|
|
144
|
+
in), constrains its output to saga's schema via `--json-schema`, and runs it as
|
|
145
|
+
a plain transform with no tools. `ANTHROPIC_API_KEY` and `ANTHROPIC_AUTH_TOKEN`
|
|
146
|
+
are dropped for the subprocess so it uses your Claude Code login rather than
|
|
147
|
+
silently billing an API key. It is slower than a direct API call (Claude Code
|
|
148
|
+
boots an agent per run) and subject to your subscription's usage limits.
|
|
149
|
+
|
|
80
150
|
## As a Claude Code skill
|
|
81
151
|
|
|
82
152
|
The `skills/` directory contains two Claude Code skills. To install both:
|
|
@@ -117,7 +187,8 @@ it on GitHub. Requires the [`gh`](https://cli.github.com) CLI, authenticated.
|
|
|
117
187
|
|
|
118
188
|
## How it works
|
|
119
189
|
|
|
120
|
-
1. `diff.py` computes `git diff base...head` (no checkout) and the commit list
|
|
190
|
+
1. `diff.py` computes `git diff base...head` (no checkout) and the commit list —
|
|
191
|
+
or, given a PR URL, fetches the same diff and metadata from GitHub via `gh`.
|
|
121
192
|
2. `model.py` splits the diff into stable-id hunks (`h0, h1, …`).
|
|
122
193
|
3. `generate.py` sends the labeled diff + commits (+ optional intent) to the chosen
|
|
123
194
|
model via `instructor`, which returns chapters as schema-validated JSON. Coverage
|
|
@@ -127,5 +198,4 @@ it on GitHub. Requires the [`gh`](https://cli.github.com) CLI, authenticated.
|
|
|
127
198
|
|
|
128
199
|
## Not included (yet)
|
|
129
200
|
|
|
130
|
-
- Support for Local LLMs via ollama / LM Studio
|
|
131
201
|
- A GitHub Action to auto-generate saga on PRs.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "saga-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Generate a self-contained static HTML saga of a git diff — a chapter-by-chapter guided tour of a change."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -13,16 +13,42 @@
|
|
|
13
13
|
}
|
|
14
14
|
.saga-theme-toggle:hover { border-color: var(--accent); color: var(--text); }
|
|
15
15
|
|
|
16
|
-
.saga-crumbs { font-size: var(--fs-sm); color: var(--text-dim); margin-bottom:
|
|
16
|
+
.saga-crumbs { font-size: var(--fs-sm); color: var(--text-dim); margin-bottom: 8px; }
|
|
17
17
|
.saga-crumbs a { color: var(--accent); text-decoration: none; }
|
|
18
18
|
.saga-crumbs a:hover { color: var(--accent-bright); text-decoration: underline; }
|
|
19
|
-
|
|
20
|
-
.
|
|
19
|
+
|
|
20
|
+
.header h1 { line-height: 1.2; }
|
|
21
|
+
|
|
22
|
+
.saga-summary {
|
|
23
|
+
color: var(--text-dim);
|
|
24
|
+
font-size: var(--fs-md);
|
|
25
|
+
line-height: 1.45;
|
|
26
|
+
margin-top: 4px;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.saga-statusline {
|
|
30
|
+
display: flex;
|
|
31
|
+
justify-content: space-between;
|
|
32
|
+
align-items: baseline;
|
|
33
|
+
gap: 8px 24px;
|
|
34
|
+
flex-wrap: wrap;
|
|
35
|
+
margin-top: 10px;
|
|
36
|
+
}
|
|
21
37
|
|
|
22
38
|
.saga-verdict {
|
|
23
39
|
color: var(--text-dim);
|
|
24
40
|
font-size: var(--fs-sm);
|
|
25
|
-
|
|
41
|
+
font-family: var(--font-mono);
|
|
42
|
+
letter-spacing: var(--tracking-mono);
|
|
43
|
+
}
|
|
44
|
+
.saga-verdict .saga-flag { color: var(--warn); }
|
|
45
|
+
/* diff add/remove hues, matching the diff body's green/red */
|
|
46
|
+
.saga-verdict .saga-add { color: var(--ok); }
|
|
47
|
+
.saga-verdict .saga-del { color: var(--danger); }
|
|
48
|
+
|
|
49
|
+
.saga-meta {
|
|
50
|
+
color: var(--text-faint);
|
|
51
|
+
font-size: var(--fs-xs);
|
|
26
52
|
font-family: var(--font-mono);
|
|
27
53
|
letter-spacing: var(--tracking-mono);
|
|
28
54
|
}
|
|
@@ -86,15 +112,11 @@
|
|
|
86
112
|
border: 1px solid var(--line); color: var(--text-dim); background: var(--surface-2);
|
|
87
113
|
}
|
|
88
114
|
.saga-plan { color: var(--text-dim); }
|
|
89
|
-
.saga-qa { color: var(--ok); border-color: var(--ok-solid); background: var(--ok-tint); }
|
|
90
115
|
.saga-read { color: var(--accent); border-color: var(--accent-solid); background: var(--accent-tint); }
|
|
91
|
-
/*
|
|
92
|
-
.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
/* Low confidence flagged for close review (amber). */
|
|
96
|
-
.saga-low {
|
|
97
|
-
color: var(--warn-ink); background: var(--warn); border-color: var(--warn-solid); font-weight: 600;
|
|
116
|
+
/* Agent attention flags (QA, differs-from-plan, closer-look) share one calm
|
|
117
|
+
amber; the label carries the meaning, not the colour. */
|
|
118
|
+
.saga-qa, .saga-dev, .saga-low {
|
|
119
|
+
color: var(--warn); border-color: var(--warn-solid); background: var(--warn-tint);
|
|
98
120
|
}
|
|
99
121
|
|
|
100
122
|
/* --- chapter reader --- */
|
|
@@ -120,10 +142,10 @@
|
|
|
120
142
|
.saga-chapter-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
|
121
143
|
.saga-chapter-title { font-size: var(--fs-xl); font-weight: 600; margin: 0; }
|
|
122
144
|
|
|
123
|
-
/*
|
|
145
|
+
/* Differs-from-plan banner (amber attention, same family as low-confidence). */
|
|
124
146
|
.saga-deviation {
|
|
125
|
-
background: var(--
|
|
126
|
-
color: var(--
|
|
147
|
+
background: var(--warn-tint); border: 1px solid var(--warn-solid); border-left: 4px solid var(--warn);
|
|
148
|
+
color: var(--warn); border-radius: var(--radius); padding: 12px 16px; margin: 14px 0; font-size: var(--fs-base);
|
|
127
149
|
}
|
|
128
150
|
.saga-lowconf {
|
|
129
151
|
background: var(--warn-tint); border: 1px solid var(--warn-solid); border-left: 4px solid var(--warn);
|
|
@@ -139,7 +161,7 @@
|
|
|
139
161
|
font-family: var(--font-mono); font-size: var(--fs-sm);
|
|
140
162
|
}
|
|
141
163
|
|
|
142
|
-
.saga-qanote { color: var(--
|
|
164
|
+
.saga-qanote { color: var(--warn); font-size: var(--fs-sm); margin: 8px 0; }
|
|
143
165
|
|
|
144
166
|
.saga-readmark {
|
|
145
167
|
display: inline-flex; align-items: center; gap: 6px;
|
|
@@ -303,28 +303,65 @@
|
|
|
303
303
|
chapters = data.chapters || [];
|
|
304
304
|
comments = loadComments();
|
|
305
305
|
initTheme();
|
|
306
|
-
|
|
306
|
+
renderHead();
|
|
307
|
+
renderVerdict(data.verdict, data.stats);
|
|
307
308
|
renderTOC();
|
|
308
309
|
}
|
|
309
310
|
|
|
311
|
+
// --- header (title, summary, provenance) ----------------------------
|
|
312
|
+
|
|
313
|
+
function renderHead() {
|
|
314
|
+
if (data.title) $('saga-title').textContent = data.title;
|
|
315
|
+
const sum = $('saga-summary');
|
|
316
|
+
if (sum && data.summary) { sum.textContent = data.summary; sum.hidden = false; }
|
|
317
|
+
const meta = $('saga-meta');
|
|
318
|
+
if (meta) {
|
|
319
|
+
const parts = [];
|
|
320
|
+
if (data.commit_sha) parts.push(data.commit_sha.slice(0, 7));
|
|
321
|
+
const rel = relativeTime(data.generated_at);
|
|
322
|
+
if (rel) parts.push('generated ' + rel);
|
|
323
|
+
meta.textContent = parts.join(' · ');
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function relativeTime(iso) {
|
|
328
|
+
if (!iso) return '';
|
|
329
|
+
const then = new Date(iso).getTime();
|
|
330
|
+
if (isNaN(then)) return '';
|
|
331
|
+
const secs = Math.round((Date.now() - then) / 1000);
|
|
332
|
+
if (secs < 60) return 'just now';
|
|
333
|
+
const units = [['year', 31536000], ['month', 2592000], ['week', 604800],
|
|
334
|
+
['day', 86400], ['hour', 3600], ['minute', 60]];
|
|
335
|
+
for (const [name, size] of units) {
|
|
336
|
+
const n = Math.floor(secs / size);
|
|
337
|
+
if (n >= 1) return n + ' ' + name + (n === 1 ? '' : 's') + ' ago';
|
|
338
|
+
}
|
|
339
|
+
return 'just now';
|
|
340
|
+
}
|
|
341
|
+
|
|
310
342
|
// --- verdict line --------------------------------------------------
|
|
311
343
|
|
|
312
|
-
function renderVerdict(v) {
|
|
344
|
+
function renderVerdict(v, stats) {
|
|
313
345
|
if (!v) return;
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
if (
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
346
|
+
// Scope segments read neutral; attention flags echo the amber rail so a
|
|
347
|
+
// reviewer can spot what needs a look. All text is generated (no user
|
|
348
|
+
// content), so building innerHTML here is injection-safe.
|
|
349
|
+
const seg = [{ t: v.chapters + (v.chapters === 1 ? ' chapter' : ' chapters') }];
|
|
350
|
+
if (stats && stats.files) seg.push({ t: stats.files + (stats.files === 1 ? ' file' : ' files') });
|
|
351
|
+
if (stats && (stats.added || stats.removed)) {
|
|
352
|
+
seg.push({ html: '<span class="saga-add">+' + (stats.added || 0) + '</span> ' +
|
|
353
|
+
'<span class="saga-del">−' + (stats.removed || 0) + '</span>' });
|
|
354
|
+
}
|
|
355
|
+
if (v.deviations > 0) seg.push({ t: v.deviations + (v.deviations === 1 ? ' differs from plan' : ' differ from plan'), flag: true });
|
|
356
|
+
if (v.low_confidence > 0) seg.push({ t: v.low_confidence + (v.low_confidence === 1 ? ' needs a closer look' : ' need a closer look'), flag: true });
|
|
357
|
+
$('saga-verdict').innerHTML = seg
|
|
358
|
+
.map((s) => (s.html ? s.html : s.flag ? '<span class="saga-flag">' + s.t + '</span>' : s.t))
|
|
359
|
+
.join(' · ');
|
|
360
|
+
// Two-tier status rail: amber when anything is flagged, else green.
|
|
323
361
|
const rail = $('saga-rail');
|
|
324
362
|
if (rail) {
|
|
325
|
-
rail.classList.remove('saga-rail-ok', 'saga-rail-attn'
|
|
326
|
-
if (v.deviations > 0) rail.classList.add('saga-rail-
|
|
327
|
-
else if (v.low_confidence > 0 || v.qa === 'attention') rail.classList.add('saga-rail-attn');
|
|
363
|
+
rail.classList.remove('saga-rail-ok', 'saga-rail-attn');
|
|
364
|
+
if (v.deviations > 0 || v.low_confidence > 0) rail.classList.add('saga-rail-attn');
|
|
328
365
|
else rail.classList.add('saga-rail-ok');
|
|
329
366
|
}
|
|
330
367
|
}
|
|
@@ -334,9 +371,9 @@
|
|
|
334
371
|
function badges(ch, read) {
|
|
335
372
|
const out = [];
|
|
336
373
|
if (ch.plan_step) out.push('<span class="saga-badge saga-plan">' + esc(ch.plan_step) + '</span>');
|
|
337
|
-
if (ch.deviation) out.push('<span class="saga-badge saga-dev"
|
|
338
|
-
if (ch.confidence === 'low') out.push('<span class="saga-badge saga-low">
|
|
339
|
-
if (ch.qa
|
|
374
|
+
if (ch.deviation) out.push('<span class="saga-badge saga-dev">Differs from plan</span>');
|
|
375
|
+
if (ch.confidence === 'low') out.push('<span class="saga-badge saga-low">Needs a closer look</span>');
|
|
376
|
+
if (ch.qa) out.push('<span class="saga-badge saga-qa">⚠ QA</span>');
|
|
340
377
|
if (read) out.push('<span class="saga-badge saga-read">✓ Read</span>');
|
|
341
378
|
return out.join('');
|
|
342
379
|
}
|
|
@@ -390,13 +427,13 @@
|
|
|
390
427
|
reader.hidden = false;
|
|
391
428
|
|
|
392
429
|
const devBanner = ch.deviation
|
|
393
|
-
? '<div class="saga-deviation"><strong
|
|
430
|
+
? '<div class="saga-deviation"><strong>Differs from the plan.</strong> ' + esc(ch.deviation) + ' Worth confirming this was intentional.</div>'
|
|
394
431
|
: '';
|
|
395
432
|
const lowBanner = ch.confidence === 'low'
|
|
396
|
-
? '<div class="saga-lowconf">
|
|
433
|
+
? '<div class="saga-lowconf"><strong>Needs a closer look.</strong> The walkthrough is unsure here — read the diff rather than just trusting the summary.</div>'
|
|
397
434
|
: '';
|
|
398
|
-
const qaLine = ch.qa
|
|
399
|
-
? '<div class="saga-qanote"
|
|
435
|
+
const qaLine = ch.qa
|
|
436
|
+
? '<div class="saga-qanote">⚠ Manual QA: ' + esc(ch.qa) + '</div>'
|
|
400
437
|
: '';
|
|
401
438
|
|
|
402
439
|
// Class-based (not id-based) so the same nav can render at top and bottom.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/* Saga — design tokens ("instrument ink").
|
|
2
2
|
*
|
|
3
3
|
* Single source of truth for the saga page. A cool CYAN accent means
|
|
4
|
-
* "a human can act here" (links, focus), and a
|
|
5
|
-
* — green (clear) · amber (attention)
|
|
6
|
-
*
|
|
4
|
+
* "a human can act here" (links, focus), and a two-light status system
|
|
5
|
+
* — green (clear) · amber (attention) — signals the agent's own state and is
|
|
6
|
+
* never used as chrome. Red is reserved for errors and destructive actions.
|
|
7
7
|
*
|
|
8
8
|
* Every colour is a light-dark(light, dark) pair. The page follows the OS by
|
|
9
9
|
* default (color-scheme: light dark); the reader's toggle forces a scheme by
|
|
@@ -49,12 +49,11 @@
|
|
|
49
49
|
--warn-ink: light-dark(#ffffff, #1b1200); /* text on a solid amber fill */
|
|
50
50
|
--warn-tint: light-dark(rgba(154,103,0,0.12), rgba(224,168,62,0.15));
|
|
51
51
|
|
|
52
|
-
--danger: light-dark(#c62828, #f2685f); /*
|
|
52
|
+
--danger: light-dark(#c62828, #f2685f); /* errors, destructive */
|
|
53
53
|
--danger-bright: light-dark(#a51f1f, #ff9c94);
|
|
54
54
|
--danger-ink: light-dark(#5c1512, #ffe1de); /* text on a loud red fill */
|
|
55
55
|
--danger-tint: light-dark(rgba(198,40,40,0.10), rgba(242,104,95,0.14));
|
|
56
|
-
--danger-loud:
|
|
57
|
-
--danger-loud-bg:light-dark(#fdecea, #3d1518); /* loud deviation panel bg */
|
|
56
|
+
--danger-loud-bg:light-dark(#fdecea, #3d1518); /* error panel bg */
|
|
58
57
|
--danger-loud-border:light-dark(#c62828, #f2685f);
|
|
59
58
|
|
|
60
59
|
/* --- diff shading (derived from status hues) --- */
|
|
@@ -129,7 +128,7 @@
|
|
|
129
128
|
/* --- signature: the status rail ------------------------------------
|
|
130
129
|
* A thin backlit edge across the top of the page. It shifts colour to
|
|
131
130
|
* the most important state present:
|
|
132
|
-
* accent (idle) · green (clear) · amber (attention)
|
|
131
|
+
* accent (idle) · green (clear) · amber (attention).
|
|
133
132
|
*/
|
|
134
133
|
.saga-rail {
|
|
135
134
|
height: 2px;
|
|
@@ -138,4 +137,3 @@
|
|
|
138
137
|
}
|
|
139
138
|
.saga-rail-ok { --rail: var(--ok); }
|
|
140
139
|
.saga-rail-attn { --rail: var(--warn); }
|
|
141
|
-
.saga-rail-dev { --rail: var(--danger); }
|