Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
8cd7974
docs(spec): task grain and review cadence — largest-ownable tasks, fr…
SSFSKIM Aug 20, 2026
f2f7f06
feat(controlled-track): largest-ownable tasks, frontier review, resum…
SSFSKIM Aug 20, 2026
963a45d
docs(spec)+skill: v1.1 — independent review adopted (ledger executed …
SSFSKIM Aug 20, 2026
8205fdf
docs(spec): v1.2 — acceptance 4 admits the split-at-overlap reading; …
SSFSKIM Aug 20, 2026
a21795a
fix(review): concurrent-review predicate; sde-telemetry dedupes usage…
SSFSKIM Aug 20, 2026
208d240
docs(spec): baseline table carries span and active time per role (col…
SSFSKIM Aug 20, 2026
06357e8
chore: v7.59.0 — task grain and review cadence (largest-ownable tasks…
SSFSKIM Aug 20, 2026
48c425a
docs(spec): table note — both measures carried
SSFSKIM Aug 20, 2026
b02dc70
fix(review): deferred-review tree clause; sde-telemetry classifies ta…
SSFSKIM Aug 20, 2026
581341c
docs(spec): opening claim aligned with the counted baseline
SSFSKIM Aug 20, 2026
c17c7e4
fix(review): fix-range ledger + task-scoped re-review packages; DONE …
SSFSKIM Aug 20, 2026
d0d2d95
fix(review): re-review messages refresh the reviewer's report, checko…
SSFSKIM Aug 21, 2026
1c558ea
docs(spec): outcomes and retrospective — task grain and review cadenc…
SSFSKIM Aug 21, 2026
f3f2138
docs(spec): v1.7 — historical-spec before/after comparison (8 specs, …
SSFSKIM Aug 21, 2026
378a05b
Merge remote-tracking branch 'origin/main' into task-grain-cadence
SSFSKIM Aug 21, 2026
158be82
chore: v7.60.0 — rebump after PR #80 took 7.59.0 (merge of origin/main)
SSFSKIM Aug 21, 2026
a4bea7c
fix(review): telemetry role order + resume-aware idle rule; reviewer …
SSFSKIM Aug 21, 2026
6db4ce9
docs(spec): retrospective names v7.60.0 and the resolved PR #80 colli…
SSFSKIM Aug 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
{
"name": "doperpowers",
"description": "Emerges by humans, Converges by Agent",
"version": "7.59.0",
"version": "7.60.0",
"source": "./",
"author": {
"name": "SSFSKIM",
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "doperpowers",
"description": "Emerges by humans, Converges by Agents",
"version": "7.59.0",
"version": "7.60.0",
"author": {
"name": "SSFSKIM",
"email": "supremekim17@gmail.com"
Expand Down
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "doperpowers",
"version": "7.59.0",
"version": "7.60.0",
"description": "A two-track software-development methodology for coding agents: a human-gated controlled track (brainstorm, plan, TDD, review, ship) plus an autonomous board loop for unattended, well-scoped work.",
"author": {
"name": "SSFSKIM",
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Because every skill declares when it applies, you don't invoke any of this by ha
Both tracks enforce the same non-negotiables — design before code, tests before implementation, evidence before "done." They differ only in where the human sits.

**Controlled** — for work where taste and intent matter.
The agent refuses to jump straight to code. It interviews you (`brainstorming`), turns the conversation into a living design spec (`execspec`), breaks that into tasks small enough for an unsupervised junior to follow (`writing-plans`), then executes each one through a fresh subagent with two-stage review — spec compliance, then code quality (`subagent-driven-execution`). You approve the design; independent reviews gate the rest.
The agent refuses to jump straight to code. It interviews you (`brainstorming`), turns the conversation into a living design spec (`execspec`), breaks that into tasks an executor can own from one self-contained brief (`writing-plans`), then executes each one through a fresh subagent, reviewed at dependency frontiers — spec compliance, then code quality (`subagent-driven-execution`). You approve the design; independent reviews gate the rest.

**Autonomous** — for work that's already well-scoped.
A single self-contained plan (`execplan`) front-loads every decision so the agent can run to the letter without mid-flight questions. At larger scale, the board loop takes over: tickets live as GitHub issues (`issue-tracker`), workers pick them up and build (`executing`), a review loop lands the PRs (`qa-loops`), and durable background sessions keep it all running (`orchestrating-daemons`). Product feedback can even feed the board directly (`triaging-feedback`).
Expand Down Expand Up @@ -74,7 +74,7 @@ Twenty-three skills, grouped by what they're for. Each one auto-triggers from it

**Build it**
- `test-driven-development` — RED → GREEN → REFACTOR, no code before a failing test
- `subagent-driven-execution` — one fresh subagent per task, two-stage review
- `subagent-driven-execution` — one fresh subagent per task, reviews at dependency frontiers
- `execplan` — the autonomous single-plan track, gates front-loaded
- `using-git-worktrees` — isolated workspaces so parallel work never clashes

Expand All @@ -101,8 +101,8 @@ Twenty-three skills, grouped by what they're for. Each one auto-triggers from it

1. **brainstorming** — Activates before writing code. Refines rough ideas through questions, explores alternatives, presents the design in sections short enough to actually read.
2. **using-git-worktrees** — Activates after design approval. Creates an isolated workspace on a new branch and verifies a clean test baseline.
3. **writing-plans** — Breaks the approved design into tasks of a few minutes each, every one with exact file paths, complete code, and verification steps.
4. **subagent-driven-execution** — Dispatches a fresh subagent per task with two-stage review.
3. **writing-plans** — Breaks the approved design into tasks an executor can own from one brief, every one with exact file paths, complete code, and verification steps.
4. **subagent-driven-execution** — Dispatches a fresh subagent per task, reviews at dependency frontiers, and fixes by resuming the executor.
5. **test-driven-development** — Enforces the RED-GREEN-REFACTOR cycle throughout and deletes any code written before its test.
6. **verification-before-completion** — Before anything is called done, runs the check and shows the output; evidence, not assertions.
7. **finishing-a-development-branch** — Verifies tests, presents merge/PR/keep/discard, cleans up the worktree.
Expand Down

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "doperpowers",
"version": "7.59.0",
"version": "7.60.0",
"description": "Doperpowers skills and runtime bootstrap for coding agents",
"type": "module",
"keywords": [
Expand Down
212 changes: 212 additions & 0 deletions scripts/sde-telemetry
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
#!/usr/bin/env python3
"""sde-telemetry — post-hoc metrics for a controlled-track (SDE) run.

For each session transcript given, prints: wall-clock span, dispatch counts
by role, and token totals by model for the controller and its subagents.
Token totals are deduplicated by message id (one assistant response is
written as several rows repeating the same usage). Durations are reported
both as the transcript's span and as active time — the span with idle gaps
excluded: gaps over 15 minutes that precede a new user message (a resume)
rather than a tool result, which is one slow tool call and stays counted.
Subagent transcripts are read from the sibling directory
<transcript-dir>/<session-id>/subagents/.

Usage: sde-telemetry SESSION_JSONL [SESSION_JSONL ...]
"""
import collections
import datetime
import glob
import json
import os
import re
import sys

# A gap this long is the agent idling between resumes, unless a tool result
# follows it — then it is one slow tool call, which counts as active.
IDLE_GAP = datetime.timedelta(minutes=15)

# Tried in order: the narrow roles before the broad "task N" executor match,
# so "Task 2: fix pagination" reads as a fixer rather than an executor.
ROLE_PATTERNS = [
(re.compile(r"re-review|review task|^t\d+ reviewer|^task \d+:\s*review",
re.I), "task-reviewer"),
(re.compile(r"^t\d+ fixer|\bfix", re.I), "fixer"),
(re.compile(r"^(implement|execute|run) task|^task \d+:"
r"|^t\d+ (implementer|executor)", re.I), "executor"),
]


def role_of(description):
for pattern, role in ROLE_PATTERNS:
if pattern.search(description or ""):
return role
return "other"


def is_tool_result(entry):
"""True when this entry is a tool result — the gap before it was one call.

Human turns and tool results share `type: "user"`: a tool result carries a
`tool_result` content block, while a resume is a string or plain text
blocks. Everything else — assistant entries and the sidecar entries a
human turn arrives with (attachments, queued messages, hook output) —
is not a tool result, so a gap before one is the agent waiting.
"""
if entry.get("type") != "user":
return False
content = (entry.get("message") or {}).get("content")
return isinstance(content, list) and any(
isinstance(block, dict) and block.get("type") == "tool_result"
for block in content
)


def scan_transcript(path):
"""Return (token totals by model, first timestamp, last timestamp, active time).

Usage is deduped by (model, message id), keeping the last snapshot seen
for that id: one assistant response is written as several rows — thinking,
tool use — that share a message id and repeat its usage.
"""
usage_by_id = {}
anonymous = 0
times = []
with open(path) as fh:
for line in fh:
try:
entry = json.loads(line)
except json.JSONDecodeError:
continue
ts = entry.get("timestamp")
if ts:
times.append((datetime.datetime.fromisoformat(ts.replace("Z", "+00:00")),
is_tool_result(entry)))
message = entry.get("message")
if isinstance(message, dict) and isinstance(message.get("usage"), dict):
model = message.get("model") or "unknown"
message_id = message.get("id")
if message_id is None:
anonymous += 1
message_id = ("no-id", anonymous)
usage_by_id[(model, message_id)] = message["usage"]

totals = collections.defaultdict(collections.Counter)
for (model, _), usage in usage_by_id.items():
for key in (
"input_tokens",
"output_tokens",
"cache_read_input_tokens",
"cache_creation_input_tokens",
):
totals[model][key] += usage.get(key) or 0

times.sort()
first = times[0][0] if times else None
last = times[-1][0] if times else None
return totals, first, last, active_time(times)


def active_time(times):
"""Sum of the segment spans, splitting at each idle gap.

`times` is (timestamp, is_tool_result) per entry. A gap over IDLE_GAP is
idle unless the entry after it is a tool result: then the gap is one slow
tool call (a 20-minute suite), which is work, not waiting. Every other
long gap ends in a new human turn — a resume — so it is excluded.
"""
active = datetime.timedelta()
if not times:
return active
segment_start = previous = times[0][0]
for t, tool_result in times[1:]:
if not tool_result and t - previous > IDLE_GAP:
active += previous - segment_start
segment_start = t
previous = t
return active + previous - segment_start


def merge(into, totals):
for model, counter in totals.items():
into[model].update(counter)


def fmt_totals(totals, indent=" "):
lines = []
for model in sorted(totals):
c = totals[model]
lines.append(
f"{indent}{model}: in={c['input_tokens']:,} out={c['output_tokens']:,} "
f"cache_read={c['cache_read_input_tokens']:,} "
f"cache_write={c['cache_creation_input_tokens']:,}"
)
return "\n".join(lines) or f"{indent}(none)"


def report(session_jsonl):
session_jsonl = os.path.abspath(session_jsonl)
session_id = os.path.splitext(os.path.basename(session_jsonl))[0]
print(f"=== session {session_id} ===")

controller_totals, first, last, controller_active = scan_transcript(session_jsonl)
if first and last:
span = last - first
print(f"wall-clock: {first.isoformat()} -> {last.isoformat()} "
f"(span {span}, active {controller_active})")

subagent_dir = os.path.join(os.path.dirname(session_jsonl), session_id, "subagents")
role_counts = collections.Counter()
role_totals = collections.defaultdict(lambda: collections.defaultdict(collections.Counter))
role_span = collections.defaultdict(datetime.timedelta)
role_active = collections.defaultdict(datetime.timedelta)
dispatches = []
for meta_path in sorted(glob.glob(os.path.join(subagent_dir, "*.meta.json"))):
with open(meta_path) as fh:
meta = json.load(fh)
description = meta.get("description", "")
role = role_of(description)
role_counts[role] += 1
agent_jsonl = meta_path.replace(".meta.json", ".jsonl")
if os.path.exists(agent_jsonl):
totals, a_first, a_last, a_active = scan_transcript(agent_jsonl)
merge(role_totals[role], totals)
duration = (a_last - a_first) if a_first and a_last else None
if duration is not None:
role_span[role] += duration
role_active[role] += a_active
out_tokens = sum(c["output_tokens"] for c in totals.values())
dispatches.append(
(a_first, role, meta.get("model") or "?", out_tokens, description, duration, a_active)
)

print(f"dispatches: {sum(role_counts.values())} total "
f"({', '.join(f'{r}={n}' for r, n in sorted(role_counts.items())) or 'none'})")
print("controller tokens:")
print(fmt_totals(controller_totals))
for role in sorted(role_totals):
print(f"{role} tokens ({role_counts[role]} dispatches):")
print(fmt_totals(role_totals[role]))
print(f" time: span={role_span[role]} active={role_active[role]}")
if dispatches:
print("dispatch list (start time, span, active, model, output tokens):")
for start, role, model, out_tokens, description, duration, active in sorted(
dispatches, key=lambda d: (d[0] is None, d[0])
):
when = start.strftime("%m-%d %H:%M") if start else "?"
dur = str(duration).split(".")[0] if duration is not None else "?"
act = str(active).split(".")[0]
print(f" {when} {dur:>8} {act:>8} {model:6} out={out_tokens:>7,} [{role}] {description}")
print()


def main(argv):
if len(argv) < 2:
print(__doc__.strip(), file=sys.stderr)
return 2
for path in argv[1:]:
report(path)
return 0


if __name__ == "__main__":
sys.exit(main(sys.argv))
Loading