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.
Files changed (41) hide show
  1. saga_cli-0.3.0/.github/CODEOWNERS +3 -0
  2. {saga_cli-0.1.0 → saga_cli-0.3.0}/PKG-INFO +78 -8
  3. {saga_cli-0.1.0 → saga_cli-0.3.0}/README.md +77 -7
  4. {saga_cli-0.1.0 → saga_cli-0.3.0}/pyproject.toml +1 -1
  5. {saga_cli-0.1.0 → saga_cli-0.3.0}/saga/assets/saga.css +38 -16
  6. {saga_cli-0.1.0 → saga_cli-0.3.0}/saga/assets/saga.js +58 -21
  7. {saga_cli-0.1.0 → saga_cli-0.3.0}/saga/assets/tokens.css +6 -8
  8. saga_cli-0.3.0/saga/cli.py +186 -0
  9. saga_cli-0.3.0/saga/diff.py +169 -0
  10. saga_cli-0.3.0/saga/generate.py +345 -0
  11. {saga_cli-0.1.0 → saga_cli-0.3.0}/saga/model.py +11 -5
  12. saga_cli-0.3.0/saga/prompts/saga.md +59 -0
  13. {saga_cli-0.1.0 → saga_cli-0.3.0}/saga/render.py +38 -13
  14. saga_cli-0.3.0/tests/cassettes/generate_anthropic.yaml +162 -0
  15. {saga_cli-0.1.0 → saga_cli-0.3.0}/tests/test_cli.py +84 -5
  16. saga_cli-0.3.0/tests/test_diff.py +217 -0
  17. saga_cli-0.3.0/tests/test_generate.py +418 -0
  18. {saga_cli-0.1.0 → saga_cli-0.3.0}/tests/test_model.py +5 -3
  19. {saga_cli-0.1.0 → saga_cli-0.3.0}/tests/test_render.py +29 -5
  20. {saga_cli-0.1.0 → saga_cli-0.3.0}/uv.lock +1 -1
  21. saga_cli-0.1.0/saga/cli.py +0 -100
  22. saga_cli-0.1.0/saga/diff.py +0 -79
  23. saga_cli-0.1.0/saga/generate.py +0 -182
  24. saga_cli-0.1.0/saga/prompts/saga.md +0 -50
  25. saga_cli-0.1.0/tests/cassettes/generate_anthropic.yaml +0 -168
  26. saga_cli-0.1.0/tests/test_diff.py +0 -95
  27. saga_cli-0.1.0/tests/test_generate.py +0 -153
  28. {saga_cli-0.1.0 → saga_cli-0.3.0}/.github/workflows/ci.yml +0 -0
  29. {saga_cli-0.1.0 → saga_cli-0.3.0}/.gitignore +0 -0
  30. {saga_cli-0.1.0 → saga_cli-0.3.0}/CONTRIBUTING.md +0 -0
  31. {saga_cli-0.1.0 → saga_cli-0.3.0}/LICENSE +0 -0
  32. {saga_cli-0.1.0 → saga_cli-0.3.0}/RELEASING.md +0 -0
  33. {saga_cli-0.1.0 → saga_cli-0.3.0}/docs/example.html +0 -0
  34. {saga_cli-0.1.0 → saga_cli-0.3.0}/saga/__init__.py +0 -0
  35. {saga_cli-0.1.0 → saga_cli-0.3.0}/saga/__main__.py +0 -0
  36. {saga_cli-0.1.0 → saga_cli-0.3.0}/saga/assets/base.css +0 -0
  37. {saga_cli-0.1.0 → saga_cli-0.3.0}/saga/comments.py +0 -0
  38. {saga_cli-0.1.0 → saga_cli-0.3.0}/skills/generate_saga/SKILL.md +0 -0
  39. {saga_cli-0.1.0 → saga_cli-0.3.0}/skills/read_saga_comments/SKILL.md +0 -0
  40. {saga_cli-0.1.0 → saga_cli-0.3.0}/tests/conftest.py +0 -0
  41. {saga_cli-0.1.0 → saga_cli-0.3.0}/tests/test_comments.py +0 -0
@@ -0,0 +1,3 @@
1
+ # Every change requires review from the maintainer.
2
+ # See https://docs.github.com/articles/about-code-owners
3
+ * @JakeBeresford
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: saga-cli
3
- Version: 0.1.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` | `main` | Base ref to diff against |
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` | `main` | Base ref to diff against |
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.1.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: 10px; }
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
- .saga-crumb-sep { margin: 0 8px; color: var(--text-faint); }
20
- .saga-crumb-current { color: var(--text); }
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
- margin-top: 6px;
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
- /* Deviations must be loud. */
92
- .saga-dev {
93
- color: var(--danger-ink); background: var(--danger-loud); border-color: var(--danger); font-weight: 600;
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
- /* Loud deviation banner. */
145
+ /* Differs-from-plan banner (amber attention, same family as low-confidence). */
124
146
  .saga-deviation {
125
- background: var(--danger-loud-bg); border: 1px solid var(--danger); border-left: 4px solid var(--danger);
126
- color: var(--danger-ink); border-radius: var(--radius); padding: 12px 16px; margin: 14px 0; font-size: var(--fs-base);
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(--ok); font-size: var(--fs-sm); margin: 8px 0; }
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
- renderVerdict(data.verdict);
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
- const parts = [
315
- v.chapters + (v.chapters === 1 ? ' chapter' : ' chapters'),
316
- v.deviations + (v.deviations === 1 ? ' deviation' : ' deviations'),
317
- v.low_confidence + ' low-confidence',
318
- ];
319
- if (v.qa && v.qa !== 'n/a') parts.push('QA ' + v.qa);
320
- $('saga-verdict').textContent = parts.join(' · ');
321
- // Shift the top status rail to the loudest state present:
322
- // deviation (red) > attention (amber) > clear (green).
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', 'saga-rail-dev');
326
- if (v.deviations > 0) rail.classList.add('saga-rail-dev');
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">⚠ Deviation</span>');
338
- if (ch.confidence === 'low') out.push('<span class="saga-badge saga-low">Low confidence</span>');
339
- if (ch.qa && ch.qa.status === 'green') out.push('<span class="saga-badge saga-qa">✓ QA</span>');
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>⚠ Deviation from plan.</strong> ' + esc(ch.deviation) + '</div>'
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">Low confidencethis chapter needs close review.</div>'
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 && ch.qa.note
399
- ? '<div class="saga-qanote">' + (ch.qa.status === 'green' ? '✓ ' : '') + esc(ch.qa.note) + '</div>'
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 strict three-light status system
5
- * — green (clear) · amber (attention) · red (deviation) is reserved for the
6
- * agent's own state and never used as chrome.
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); /* deviation */
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: light-dark(#fbe3e1, #b62324); /* loud deviation fill */
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) · red (deviation).
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); }