Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
42 changes: 41 additions & 1 deletion CHANGELOG.md

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,28 @@ is. A preference for CI is never a reason for something to go unverified, and CI
that has fallen behind the surface it is supposed to cover is not an authority —
keep it current, and keep it running on the hosts it declares.

**A hook change lands on every host the repository declares, or it says why not.**
The hosts are listed in `skills/taskflow/references/runtime.md`, and a hook that
is wired on one of them and silently absent on another is a defect, not a
staging decision: the absent host simply does no checking, and nothing reports
that it stopped. So a hook is not finished until either its behaviour is wired on
every declared host, or `runtime.md` states for each host which of the two it is
and why.

Two words are not interchangeable, and conflating them is how Codex went a month
with no gate while this file recorded it as a limitation of the host:

- **not wired** — the host supports the event and the reply shape, and this
repository has not connected it. That is work, not a constraint.
- **not supported** — the host has no such event, or no way to see what the hook
needs. That is a fact about the host, and it must name the mechanism it lacks.

A host recorded as *not supported* carries the host's own documentation as
evidence, and is re-checked when wiring changes. A host recorded as *not wired*
belongs in the host's column as outstanding work — and if it stays there across
releases, this file is where that has to be visible rather than discovered by a
user whose edits were never checked.

Release-time checks are their own list, in `RELEASE.md`, which states which of
them CI covers and which it cannot.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Plan
> Task version: v1
> Status: completed

## Approval
- Status: approved
- Approved by: user
- Approved at: 2026-01-01 00:00 +0000
- Approved version: v1
- Approved scope: PRD

## Steps

### Step 1 — 修掉并发写
- Status: done

### Step 2 — 重写解析器
- Status: done

### Step 3 — 补迁移
- Status: done

## Verification / Review

- 2026-01-03 Step 1: ok
- 2026-01-03 Step 2: ok
- 2026-01-03 Step 3: ok
50 changes: 50 additions & 0 deletions TaskFlowDocs/achieved/2026-10-29-midturn-presence/measure-drift.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import json, sys, os, statistics

def analyze(path):
edits = 0 # Edit/Write tool_use on repo files
progress = 0 # hooks/task progress calls
other_task = 0 # any other hooks/task call
turns_with_edits = 0
cur_edits = 0
seen = False
with open(path, encoding='utf-8', errors='replace') as f:
for line in f:
line = line.strip()
if not line: continue
try: rec = json.loads(line)
except Exception: continue
t = rec.get('type')
if t == 'user':
c = (rec.get('message') or {}).get('content')
is_prompt = False
if isinstance(c, str): is_prompt = True
elif isinstance(c, list):
has_tr = any(isinstance(x, dict) and x.get('type') == 'tool_result' for x in c)
has_tx = any(isinstance(x, dict) and x.get('type') == 'text' for x in c)
is_prompt = has_tx and not has_tr
if is_prompt:
if seen and cur_edits: turns_with_edits += 1
seen = True; cur_edits = 0
elif t == 'assistant':
c = (rec.get('message') or {}).get('content')
if not isinstance(c, list): continue
for blk in c:
if not isinstance(blk, dict) or blk.get('type') != 'tool_use': continue
name = blk.get('name') or ''
inp = blk.get('input') or {}
if name in ('Edit','Write','NotebookEdit'):
fp = str(inp.get('file_path',''))
if '/TaskFlowDocs/' in fp or fp.endswith('/plan.md'):
continue # doc edits aren't implementation
edits += 1; cur_edits += 1
elif name == 'Bash':
cmd = str(inp.get('command',''))
if 'hooks/task progress' in cmd: progress += 1
elif 'hooks/task ' in cmd: other_task += 1
if seen and cur_edits: turns_with_edits += 1
return edits, progress, other_task, turns_with_edits

print(f"{'session':<12} {'code-edits':>10} {'progress':>9} {'task-other':>10} {'turns-w-edits':>13}")
for p in sys.argv[1:]:
e, pr, ot, tw = analyze(p)
print(f"{os.path.basename(p)[:12]:<12} {e:>10} {pr:>9} {ot:>10} {tw:>13}")
46 changes: 46 additions & 0 deletions TaskFlowDocs/achieved/2026-10-29-midturn-presence/measure-turns.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import json, sys, statistics, os

def analyze(path):
turns = [] # tool calls per user turn
cur = 0
seen_user = False
with open(path, encoding='utf-8', errors='replace') as f:
for line in f:
line = line.strip()
if not line:
continue
try:
rec = json.loads(line)
except Exception:
continue
if rec.get('type') == 'user':
msg = rec.get('message') or {}
content = msg.get('content')
# a real prompt, not a tool_result
is_prompt = False
if isinstance(content, str):
is_prompt = True
elif isinstance(content, list):
parts = [c for c in content if isinstance(c, dict) and c.get('type') == 'text']
has_tr = any(isinstance(c, dict) and c.get('type') == 'tool_result' for c in content)
is_prompt = bool(parts) and not has_tr
if is_prompt:
if seen_user:
turns.append(cur)
seen_user = True
cur = 0
elif rec.get('type') == 'assistant':
msg = rec.get('message') or {}
content = msg.get('content')
if isinstance(content, list):
cur += sum(1 for c in content if isinstance(c, dict) and c.get('type') == 'tool_use')
if seen_user:
turns.append(cur)
return turns

for p in sys.argv[1:]:
t = analyze(p)
if not t:
print(f"{os.path.basename(p)[:12]} (no turns)")
continue
print(f"{os.path.basename(p)[:12]} turns={len(t):<4} median={statistics.median(t):<6} mean={statistics.mean(t):<6.1f} max={max(t):<4} zero-tool-turns={sum(1 for x in t if x==0)}")
Loading
Loading