@ericrisco/rsc 0.1.33 → 0.1.35

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 (65) hide show
  1. package/README.md +20 -0
  2. package/manifest.json +1 -1
  3. package/package.json +1 -1
  4. package/skills/agent-eval/SKILL.md +1 -1
  5. package/skills/analyze/SKILL.md +1 -1
  6. package/skills/brand-voice/SKILL.md +1 -1
  7. package/skills/brand-voice/references/voice-guide-template.md +14 -0
  8. package/skills/building-agents/SKILL.md +2 -2
  9. package/skills/competitor-watch/SKILL.md +9 -4
  10. package/skills/competitor-watch/references/tracker-schema.md +19 -0
  11. package/skills/constitution/references/constitution-template.md +10 -0
  12. package/skills/content-engine/SKILL.md +1 -1
  13. package/skills/content-engine/references/brief-and-pipeline.md +15 -1
  14. package/skills/course-builder/references/grounding-and-scoping.md +23 -0
  15. package/skills/course-storytelling/references/learner-grounding.md +22 -2
  16. package/skills/decision-records/SKILL.md +14 -1
  17. package/skills/decision-records/references/templates.md +20 -0
  18. package/skills/design/references/brand-grounding.md +11 -1
  19. package/skills/harness/SKILL.md +3 -3
  20. package/skills/harness/references/daily-curation-automation.md +8 -6
  21. package/skills/harness/references/obsidian-scaffolding.md +16 -9
  22. package/skills/harness/references/wiki-article-template.md +31 -14
  23. package/skills/harness/references/wiki-index-template.md +7 -3
  24. package/skills/harness/references/wiki-protocol.md +120 -54
  25. package/skills/harness/references/wiki-worklog-template.md +5 -1
  26. package/skills/instagram-api/SKILL.md +8 -1
  27. package/skills/instagram-api/references/insights-metrics.md +8 -1
  28. package/skills/knowledge-ops/SKILL.md +9 -9
  29. package/skills/knowledge-ops/evals/cases.yaml +1 -1
  30. package/skills/knowledge-ops/references/gardening-playbook.md +18 -8
  31. package/skills/linkedin-api/SKILL.md +10 -1
  32. package/skills/linkedin-strategy/SKILL.md +12 -0
  33. package/skills/linkedin-strategy/references/wiki-records.md +27 -1
  34. package/skills/marketing/references/brand-grounding.md +22 -2
  35. package/skills/medium-strategy/SKILL.md +12 -0
  36. package/skills/medium-strategy/references/wiki-records.md +39 -1
  37. package/skills/medium-writing/SKILL.md +19 -0
  38. package/skills/plan/references/plan-template.md +12 -1
  39. package/skills/roast-me/tools/adapters/__pycache__/__init__.cpython-312.pyc +0 -0
  40. package/skills/roast-me/tools/adapters/__pycache__/base.cpython-312.pyc +0 -0
  41. package/skills/roast-me/tools/adapters/__pycache__/claude.cpython-312.pyc +0 -0
  42. package/skills/roast-me/tools/adapters/__pycache__/codex.cpython-312.pyc +0 -0
  43. package/skills/roast-me/tools/adapters/__pycache__/gemini.cpython-312.pyc +0 -0
  44. package/skills/roast-me/tools/adapters/__pycache__/registry.cpython-312.pyc +0 -0
  45. package/skills/roast-me/tools/adapters/base.py +12 -0
  46. package/skills/roast-me/tools/adapters/codex.py +146 -53
  47. package/skills/roast-me/tools/adapters/gemini.py +102 -81
  48. package/skills/roast-me/tools/extract_prompts.py +13 -14
  49. package/skills/shortform-packaging/SKILL.md +7 -2
  50. package/skills/shortform-packaging/references/package-templates.md +13 -2
  51. package/skills/shortform-strategy/SKILL.md +30 -6
  52. package/skills/shortform-strategy/references/learning-loop-template.md +9 -1
  53. package/skills/specify/references/spec-template.md +12 -1
  54. package/skills/tiktok-api/SKILL.md +6 -2
  55. package/skills/tiktok-api/references/wiki-schema.md +38 -4
  56. package/skills/verify/SKILL.md +11 -1
  57. package/skills/youtube-api/SKILL.md +7 -2
  58. package/skills/youtube-api/references/wiki-schema.md +25 -4
  59. package/skills/youtube-packaging/SKILL.md +10 -2
  60. package/skills/youtube-packaging/references/description-and-chapters.md +70 -2
  61. package/skills/youtube-strategy/SKILL.md +9 -2
  62. package/skills/youtube-strategy/references/wiki-records.md +39 -6
  63. package/skills/youtube-strategy/scripts/verify.sh +4 -0
  64. package/skills/youtube-thumbnails/SKILL.md +10 -5
  65. package/skills/youtube-thumbnails/references/experiment-log-format.md +28 -2
@@ -91,9 +91,19 @@ If the user can't answer a question, that dimension stays incomplete — note th
91
91
 
92
92
  ### Brand wiki article template
93
93
 
94
- Each `02-DOCS/wiki/brand/*.md` article follows the harness wiki format:
94
+ Each `02-DOCS/wiki/brand/*.md` article follows the harness wiki format. It is an OKF v0.1 article: open it with YAML frontmatter carrying a non-empty `type:` (`brand-study`), then the H1 and body:
95
95
 
96
96
  ```markdown
97
+ ---
98
+ type: brand-study
99
+ title: Brand — Value Proposition
100
+ description: The core benefit and what makes this brand different from the named alternatives.
101
+ tags: [brand, value-proposition]
102
+ timestamp: YYYY-MM-DDTHH:MM:SSZ
103
+ topic: brand
104
+ status: stable
105
+ ---
106
+
97
107
  # Brand — Value Proposition
98
108
 
99
109
  > Sources: {user interview, YYYY-MM-DD}
@@ -124,9 +134,19 @@ One paragraph: the core benefit and what makes it different.
124
134
 
125
135
  ### Voice article specifics
126
136
 
127
- `voice.md` must contain the do/don't word lists and link to every raw sample:
137
+ `voice.md` must contain the do/don't word lists and link to every raw sample (same OKF frontmatter; `type: brand-study`):
128
138
 
129
139
  ```markdown
140
+ ---
141
+ type: brand-study
142
+ title: Brand — Tone & Voice
143
+ description: Tone descriptors, do/don't word lists, and the voice fingerprint extracted from real samples.
144
+ tags: [brand, voice, tone]
145
+ timestamp: YYYY-MM-DDTHH:MM:SSZ
146
+ topic: brand
147
+ status: stable
148
+ ---
149
+
130
150
  # Brand — Tone & Voice
131
151
 
132
152
  > Sources: {user interview, YYYY-MM-DD}
@@ -133,6 +133,16 @@ On a monthly/quarterly review:
133
133
  Every session ends with a dated record under `02-DOCS/wiki/medium/decisions/`. Minimum fields — the bet-on metric is mandatory, because a decision with no metric is an opinion, not a checkable bet:
134
134
 
135
135
  ```markdown
136
+ ---
137
+ type: medium-decision
138
+ title: Solo vs publication for AI-tools series
139
+ description: Stay solo + canonical-import the blog series; pitch one Boost-eligible AI pub with a native original.
140
+ tags: [medium, publications, monetization, distribution]
141
+ timestamp: 2026-06-02T00:00:00Z
142
+ topic: medium
143
+ status: stable
144
+ ---
145
+
136
146
  # 2026-06-02 — Solo vs publication for AI-tools series
137
147
 
138
148
  Decision: Stay solo profile + canonical-import the blog series; pitch one Boost-eligible AI pub with a native original.
@@ -140,6 +150,8 @@ Rationale: 400 followers, no editor nomination path; the blog has evergreen sear
140
150
  Bets on metric: member reading time from external/search referral; 1 Boost nomination this quarter.
141
151
  ```
142
152
 
153
+ `type:` is the required OKF v0.1 field; `timestamp` is ISO 8601. The `Decision:` / `Bets on metric:` lines are domain signals `verify.sh` greps for — keep them.
154
+
143
155
  After a review, append a `what-worked.md` entry tied to specific stories/pubs/tags (e.g. "`AI Coding Tools` long-tail + Boost → 3.2h member reading time"). Full templates, layout, and the verify.sh contract: [`references/wiki-records.md`](references/wiki-records.md). Sanity-check the wiki with `scripts/verify.sh 02-DOCS/wiki/medium/`.
144
156
 
145
157
  ## Anti-patterns
@@ -2,6 +2,8 @@
2
2
 
3
3
  This is the persistence layer that makes medium-strategy compound. Read SKILL.md §1 and §9 first; this file is the exact format.
4
4
 
5
+ Every `.md` file written under `02-DOCS/wiki/medium/` is part of the OKF v0.1 bundle: it carries YAML frontmatter with a non-empty `type:` (the only required OKF field), and any cross-references use **standard markdown links** (`[Text](./pubs.md)`), never wikilinks (`[[...]]`). The domain lines `verify.sh` greps for (the dated heading, `Decision:`, the metric/bet field) stay exactly as shown below.
6
+
5
7
  ## Layout under `02-DOCS/wiki/medium/`
6
8
 
7
9
  ```text
@@ -21,6 +23,16 @@ This is the persistence layer that makes medium-strategy compound. Read SKILL.md
21
23
  Every decision file MUST carry three signals: a **date**, a **Decision:** line, and a **named metric/bet**. The metric is mandatory — a decision with no metric is an opinion, not something you can check next quarter.
22
24
 
23
25
  ```markdown
26
+ ---
27
+ type: medium-decision
28
+ title: Solo vs publication for the AI-tools series
29
+ description: Stay solo; canonical-import the evergreen blog posts; pitch one Boost-eligible AI pub.
30
+ tags: [medium, publications, monetization, distribution]
31
+ timestamp: 2026-06-02T00:00:00Z
32
+ topic: medium
33
+ status: stable
34
+ ---
35
+
24
36
  # 2026-06-02 — Solo vs publication for the AI-tools series
25
37
 
26
38
  Decision: Stay solo profile; canonical-import the 6 evergreen blog posts; pitch one Boost-eligible AI publication with a new native original.
@@ -29,11 +41,23 @@ Bets on metric: member reading time from external + search referral over 90 days
29
41
  Status: open
30
42
  ```
31
43
 
32
- When the bet resolves, append a short outcome to the same file (`Status: won — Boost nomination 2026-07; +2.1h member reading time`) rather than editing the original — append-only history.
44
+ `type:` (required) + `timestamp:` (ISO 8601, last meaningful edit) are the OKF surface; the `Status:` line in the body is the domain bet-lifecycle marker — distinct from the frontmatter `status:` (the OKF article lifecycle: draft | stable | stale). When the bet resolves, append a short outcome to the body (`Status: won — Boost nomination 2026-07; +2.1h member reading time`) rather than editing the original — append-only history. A running `decisions.md` log at the root carries one frontmatter block at the top (`type: medium-decision`).
33
45
 
34
46
  ## what-worked.md entry template
35
47
 
48
+ `what-worked.md` is one running file; new review blocks are prepended newest-first under a single frontmatter block at the top.
49
+
36
50
  ```markdown
51
+ ---
52
+ type: medium-record
53
+ title: What worked — Medium
54
+ description: Running log of outcomes tied to specific Medium stories, publications, and tags.
55
+ tags: [medium, what-worked, member-reading-time, boost]
56
+ timestamp: 2026-09-01T00:00:00Z
57
+ topic: medium
58
+ status: stable
59
+ ---
60
+
37
61
  ## 2026-09-01 — Q3 review
38
62
  - `AI Coding Tools` (long-tail) + Boost → 3.2h member reading time on one story. Double down: keep the long-tail tag, pitch the same pub again.
39
63
  - Imported blog post w/ canonical + newsletter push → +5% external bonus visible, but low member conversion. Keep importing; stop expecting member reads from cold list.
@@ -44,6 +68,20 @@ When the bet resolves, append a short outcome to the same file (`Status: won —
44
68
 
45
69
  Keep these lightweight. `pubs.md`: pub name | status (accepted/pitched/dropped) | niche fit | a one-line guideline note. `tags.md`: tag | popular-or-long-tail | did it pull member reading time?
46
70
 
71
+ Both still carry OKF frontmatter (one block at the top of each file):
72
+
73
+ ```markdown
74
+ ---
75
+ type: medium-record
76
+ title: Publications tracker # or: Tag sets tried
77
+ description: Accepted / pitched / dropped Medium publications and their guideline notes.
78
+ tags: [medium, publications] # or [medium, tags, distribution] for tags.md
79
+ timestamp: 2026-09-01T00:00:00Z
80
+ topic: medium
81
+ status: stable
82
+ ---
83
+ ```
84
+
47
85
  ## The verify.sh contract
48
86
 
49
87
  `scripts/verify.sh` is a **structural lint, not a craft judge.** Run it after a session:
@@ -117,6 +117,25 @@ If `02-DOCS/` exists, learn before you write:
117
117
  - Bias toward the title/subtitle patterns and topics that historically beat a 50% read ratio for *this* author; avoid angles that drew clicks but bounced.
118
118
  - After a notable result, append the learned pattern to `02-DOCS/wiki/medium/read-ratio-patterns.md` so the next draft inherits it.
119
119
 
120
+ `read-ratio-patterns.md` lives in the `02-DOCS/wiki/` OKF v0.1 bundle, so it carries YAML frontmatter with a non-empty `type:` — one block at the top, with new pattern blocks prepended newest-first below it. Use `type: medium-record` (the same type the `medium-strategy` skill writes under `02-DOCS/wiki/medium/`, keeping the two skills' wiki content consistent). Create it with this header if it does not yet exist:
121
+
122
+ ```markdown
123
+ ---
124
+ type: medium-record
125
+ title: Read-ratio patterns — Medium
126
+ description: Title/subtitle/topic patterns that historically beat a 50% read ratio for this author.
127
+ tags: [medium, read-ratio, titles, hooks]
128
+ timestamp: 2026-06-02T00:00:00Z
129
+ topic: medium
130
+ status: stable
131
+ ---
132
+
133
+ ## 2026-06-02 — <pattern learned>
134
+ - <title/subtitle archetype + topic that beat the 50% read ratio, with the number observed>
135
+ ```
136
+
137
+ Cross-references use standard markdown links (`[Text](./what-worked.md)`), never wikilinks (`[[...]]`).
138
+
120
139
  If `02-DOCS/` is absent, proceed from the craft defaults above and say so in one line, rather than inventing stats.
121
140
 
122
141
  ## Anti-patterns
@@ -9,9 +9,20 @@ flows, levels — not framework syntax.
9
9
  ---
10
10
 
11
11
  ```markdown
12
+ ---
13
+ type: plan
14
+ title: Plan — <feature name>
15
+ description: The structure-level implementation plan for <feature> — contracts, shapes, flows.
16
+ tags: [sdd, plan]
17
+ timestamp: YYYY-MM-DDTHH:MM:SSZ
18
+ topic: sdd
19
+ slug: <slug>
20
+ status: draft
21
+ ---
22
+
12
23
  # Plan — <feature name>
13
24
 
14
- > Spec: ../specs/<slug>.md · Constitution: ../constitution.md · Status: draft | approved
25
+ > Spec: [../specs/<slug>.md](../specs/<slug>.md) · Constitution: [../constitution.md](../constitution.md) · Status: draft | approved
15
26
  > Last updated: YYYY-MM-DD
16
27
 
17
28
  ## 0. Global Constraints
@@ -17,10 +17,22 @@ An adapter MUST:
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
+ import re
20
21
  from abc import ABC, abstractmethod
21
22
  from pathlib import Path
22
23
  from typing import Any
23
24
 
25
+ #: Shared correction detector — a user message matching this right after a prompt
26
+ #: signals the previous turn went wrong. Single source of truth for every adapter
27
+ #: (and re-used by extract_prompts.py) so the heuristic never drifts between runtimes.
28
+ CORRECTION_RE = re.compile(
29
+ r"\b(no[,.]?\s|wrong|instead|actually|don'?t|shouldn'?t|stop|not that|"
30
+ r"I said|I meant|I asked|that'?s not|please don'?t|why did you|"
31
+ r"you should have|that was wrong|incorrect|try again|redo|"
32
+ r"that broke|you broke|revert|undo)\b",
33
+ re.IGNORECASE,
34
+ )
35
+
24
36
 
25
37
  class BaseAdapter(ABC):
26
38
  """Contract every runtime adapter must satisfy."""
@@ -1,36 +1,71 @@
1
1
  """Codex CLI adapter (@openai/codex).
2
2
 
3
3
  Session files: ~/.codex/sessions/YYYY/MM/DD/rollout-<timestamp>.jsonl
4
- Format: JSON Lines. Each line is an event object.
5
-
6
- CONFIRMED (2026-06, via official CLI reference and community tooling):
7
- - Base path: ~/.codex/sessions/
8
- - Subdirectory layout: year/month/day
9
- - File naming: rollout-<ISO-timestamp>.jsonl
10
- - Format: JSONL, one event per line
11
-
12
- STUBBED (format details):
13
- The internal schema of each JSONL line has not been verified against a live
14
- Codex installation. Based on community-authored session viewers, events appear
15
- to carry {"role": "user"|"assistant", "content": "...", ...}. If the role
16
- field is absent or the schema differs, parse() returns [] gracefully.
17
-
18
- Degradation: if ~/.codex/ does not exist, discover() returns [].
4
+ Format: JSON Lines. Each line is {"timestamp", "type", "payload"}.
5
+
6
+ CONFIRMED (2026-06, against live ~/.codex/sessions on macOS):
7
+ - Base path / layout / naming: ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl
8
+ - Line envelope: {"timestamp": ISO8601, "type": str, "payload": {...}}
9
+ - The genuine user prompt is an `event_msg` line whose payload.type is
10
+ "user_message", carrying the typed text in payload.message. (Injected
11
+ AGENTS.md / developer preambles appear only as response_item/message lines,
12
+ so keying on user_message excludes them.)
13
+ - Assistant turns: event_msg/agent_message (payload.message) and
14
+ response_item/message role=assistant.
15
+ - Tool calls: response_item/function_call (payload.name).
16
+ - Tool results: response_item/function_call_output (payload.output) — shell
17
+ execs include a "Process exited with code N" line; N != 0 (or common error
18
+ markers) means the command failed.
19
+ - Model: turn_context.model / session_meta payload.
20
+
21
+ Degradation: if ~/.codex/ does not exist, discover() returns []; any unexpected
22
+ line shape is skipped, so parse() never raises.
19
23
  """
20
24
 
21
25
  from __future__ import annotations
22
26
 
27
+ import datetime
23
28
  import json
29
+ import re
24
30
  from pathlib import Path
25
31
  from typing import Any
26
32
 
27
- from .base import BaseAdapter
33
+ from .base import CORRECTION_RE, BaseAdapter
34
+
35
+ # A shell tool result failed when it reports a non-zero exit code, or carries a
36
+ # common error marker with no explicit success code. Conservative on purpose —
37
+ # better to miss a soft error than to invent one.
38
+ _EXIT_CODE_RE = re.compile(r"exited with code\s+(\d+)", re.IGNORECASE)
39
+ _ERROR_MARKER_RE = re.compile(
40
+ r"\b(command not found|no such file|permission denied|traceback \(most recent|"
41
+ r"fatal:|panic:|segmentation fault|unhandled exception)\b",
42
+ re.IGNORECASE,
43
+ )
44
+
45
+
46
+ def _looks_like_error(output: str) -> bool:
47
+ if not output:
48
+ return False
49
+ m = _EXIT_CODE_RE.search(output)
50
+ if m:
51
+ return m.group(1) != "0"
52
+ return bool(_ERROR_MARKER_RE.search(output))
53
+
54
+
55
+ def _ts(value: Any) -> float | None:
56
+ if isinstance(value, (int, float)):
57
+ return float(value)
58
+ if isinstance(value, str):
59
+ try:
60
+ return datetime.datetime.fromisoformat(value.replace("Z", "+00:00")).timestamp()
61
+ except (ValueError, AttributeError):
62
+ return None
63
+ return None
28
64
 
29
65
 
30
66
  class CodexAdapter(BaseAdapter):
31
67
  RUNTIME_ID = "codex"
32
68
 
33
- # CONFIRMED: path layout from official CLI docs and community tools.
34
69
  SESSIONS_DIR = Path.home() / ".codex" / "sessions"
35
70
 
36
71
  def discover(self) -> list[Path]:
@@ -50,17 +85,17 @@ class CodexAdapter(BaseAdapter):
50
85
  return sorted(files, key=lambda f: f.stat().st_mtime, reverse=True)
51
86
 
52
87
  def parse(self, path: Path) -> list[dict[str, Any]]:
53
- """Parse one Codex session JSONL file.
54
-
55
- STUBBED schema: assumes {"role": "user", "content": "..."} event lines.
56
- Falls back silently if the schema differs.
57
- """
58
- records: list[dict[str, Any]] = []
88
+ """Parse one Codex session JSONL file into user-prompt records with
89
+ session-aware error/correction context."""
59
90
  try:
60
91
  lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
61
92
  except OSError:
62
93
  return []
63
94
 
95
+ # 1) Flatten the file into an ordered timeline of turns.
96
+ timeline: list[dict[str, Any]] = []
97
+ model = ""
98
+ pending_tool = ""
64
99
  for line in lines:
65
100
  line = line.strip()
66
101
  if not line:
@@ -69,45 +104,103 @@ class CodexAdapter(BaseAdapter):
69
104
  obj = json.loads(line)
70
105
  except json.JSONDecodeError:
71
106
  continue
107
+ if not isinstance(obj, dict):
108
+ continue
72
109
 
73
- # STUBBED: expected schema based on community session viewers.
74
- role = obj.get("role") or obj.get("type", "")
75
- if role != "user":
110
+ top_type = obj.get("type")
111
+ payload = obj.get("payload")
112
+ if not isinstance(payload, dict):
113
+ payload = {}
114
+ ts = _ts(obj.get("timestamp"))
115
+
116
+ # Track the active model from session metadata / turn context.
117
+ if top_type in ("session_meta", "turn_context"):
118
+ m = payload.get("model")
119
+ if isinstance(m, str) and m:
120
+ model = m
76
121
  continue
77
122
 
78
- content = obj.get("content", "")
79
- if isinstance(content, list):
80
- # Some formats use a content array like OpenAI chat API.
81
- parts = [
82
- c.get("text", "") if isinstance(c, dict) else str(c)
83
- for c in content
84
- if isinstance(c, dict) and c.get("type") == "text"
85
- ]
86
- text = "\n".join(parts)
87
- elif isinstance(content, str):
88
- text = content
89
- else:
90
- text = ""
91
-
92
- if not text.strip():
123
+ if top_type == "event_msg":
124
+ p_type = payload.get("type")
125
+ if p_type == "user_message":
126
+ text = payload.get("message", "")
127
+ if isinstance(text, str) and text.strip():
128
+ timeline.append({"kind": "user", "text": text, "ts": ts})
129
+ elif p_type == "agent_message":
130
+ text = payload.get("message", "")
131
+ if isinstance(text, str) and text.strip():
132
+ timeline.append({"kind": "assistant", "text": text, "ts": ts})
93
133
  continue
94
134
 
95
- ts = obj.get("timestamp") or obj.get("created_at")
96
- if isinstance(ts, str):
97
- try:
98
- import datetime
99
- dt = datetime.datetime.fromisoformat(ts.replace("Z", "+00:00"))
100
- ts = dt.timestamp()
101
- except (ValueError, AttributeError):
102
- ts = None
103
- elif not isinstance(ts, (int, float)):
104
- ts = None
135
+ if top_type == "response_item":
136
+ p_type = payload.get("type")
137
+ if p_type == "function_call":
138
+ name = payload.get("name", "")
139
+ pending_tool = name if isinstance(name, str) else ""
140
+ elif p_type == "function_call_output":
141
+ output = payload.get("output", "")
142
+ output = output if isinstance(output, str) else json.dumps(output)
143
+ timeline.append({
144
+ "kind": "tool_output",
145
+ "tool": pending_tool,
146
+ "is_error": _looks_like_error(output),
147
+ "text": output,
148
+ "ts": ts,
149
+ })
150
+ pending_tool = ""
151
+ # response_item/message (assistant/user/developer) is intentionally
152
+ # ignored: genuine prompts come from event_msg/user_message, and the
153
+ # assistant context comes from event_msg/agent_message.
154
+ continue
155
+
156
+ # 2) Walk the timeline; enrich each user prompt with what follows it.
157
+ user_indices = [i for i, t in enumerate(timeline) if t["kind"] == "user"]
158
+ records: list[dict[str, Any]] = []
159
+ for n, idx in enumerate(user_indices):
160
+ turn = timeline[idx]
161
+ next_user = user_indices[n + 1] if n + 1 < len(user_indices) else len(timeline)
162
+
163
+ followed_by_error = False
164
+ error_tool = ""
165
+ error_text = ""
166
+ for j in range(idx + 1, next_user):
167
+ t = timeline[j]
168
+ if t["kind"] == "tool_output" and t.get("is_error"):
169
+ followed_by_error = True
170
+ error_tool = t.get("tool", "")
171
+ error_text = t.get("text", "")
172
+ break
173
+
174
+ followed_by_correction = False
175
+ correction_text = ""
176
+ if next_user < len(timeline):
177
+ nxt = timeline[next_user]["text"]
178
+ if CORRECTION_RE.search(nxt):
179
+ followed_by_correction = True
180
+ correction_text = nxt
181
+
182
+ # context_before: last assistant text strictly before this prompt.
183
+ context_before = ""
184
+ for j in range(idx - 1, -1, -1):
185
+ if timeline[j]["kind"] == "assistant":
186
+ context_before = timeline[j]["text"]
187
+ break
188
+
189
+ error_was_recovered = followed_by_error and not followed_by_correction
105
190
 
106
191
  records.append({
107
192
  "runtime": self.RUNTIME_ID,
108
193
  "session_file": str(path),
109
- "prompt_text": text,
110
- "timestamp": ts,
194
+ "prompt_text": turn["text"],
195
+ "timestamp": turn.get("ts"),
196
+ "model": model,
197
+ "context_before": context_before,
198
+ "followed_by_error": followed_by_error,
199
+ "error_was_recovered": error_was_recovered,
200
+ "followed_by_correction": followed_by_correction,
201
+ "correction_text": correction_text,
202
+ "error_tool": error_tool,
203
+ "error_text": error_text,
111
204
  })
112
205
 
113
206
  return records