feat: task_completed signal — post-task skill capture (v0.3.2)

Adds an 11th signal type emitted when a run of work (between two
UserPromptSubmit events) crosses three quality gates:
  - >=5 tool calls (TASK_TOOL_MIN)
  - >=3 distinct tool kinds (TASK_DIVERSITY_MIN, filters single-tool
    sweeps like "wrote 5 files")
  - 0 correction signals during the run (filters tasks where the user
    pushed back; correction-during-task disqualifies the recipe)

Payload carries tool_count, tool_kinds, active_skills, active_agents
so the agent can cluster by sorted tool-kind tuple and route through
the existing skill-overlap rule (skill_new vs skill_edit).

Importantly: cross_session_evidence is FALSE on first occurrence,
so resulting skill_new proposals always queue for review — they only
auto-apply when the same multi-tool recipe recurs in a second session
(then the existing rubric kicks in). Post-task creation captures novel
patterns while preserving the rule "auto-apply requires cross-session".

Hook adds state fields: task_tool_count, task_tool_kinds, task_corrections.
All reset on UserPromptSubmit boundary and on session change.

Agent gets one new signal-types-table row and one clustering bullet
referencing the existing skill-overlap rule.

3 new tests (30 passed, 0 failed):
  - 5 tools + 5 kinds + 0 corrections fires task_completed
  - 5 tools + 1 kind (Edit only) does NOT fire (diversity gate)
  - 5 tools + 3 kinds + correction-on-closing-prompt does NOT fire
This commit is contained in:
2026-05-10 22:34:33 +01:00
parent 6d8ff37cb2
commit 7ddda26bb4
3 changed files with 72 additions and 0 deletions
+42
View File
@@ -364,6 +364,48 @@ for i in 1 2 3 4 5; do
done
assert_grep "$ROOT/journal.jsonl" '"active_skills":\["caveman"\]' "active_skills payload includes invoked skill"
# --- Test 24: task_completed fires on diverse multi-tool task ---
echo "Test 24: task_completed after 5 tools / 3 kinds / no corrections"
reset_state
for kind in Bash Read Edit Write Grep; do
echo "{\"hook_event_name\":\"PostToolUse\",\"tool_name\":\"$kind\",\"tool_input\":{},\"session_id\":\"sT\",\"cwd\":\"/tmp/x\"}" \
| HOOK_RUN >/dev/null 2>&1 || true
done
echo '{"hook_event_name":"UserPromptSubmit","prompt":"go on","session_id":"sT","cwd":"/tmp/x"}' \
| HOOK_RUN >/dev/null 2>&1 || true
assert_grep "$ROOT/journal.jsonl" '"type":"task_completed"' "5 tools + 5 kinds + 0 corrections emits task_completed"
# --- Test 25: task_completed suppressed when tool diversity < 3 ---
echo "Test 25: task_completed suppressed on single-tool run"
reset_state
for i in 1 2 3 4 5; do
echo "{\"hook_event_name\":\"PostToolUse\",\"tool_name\":\"Edit\",\"tool_input\":{\"file_path\":\"/tmp/$i\"},\"session_id\":\"sT2\",\"cwd\":\"/tmp/x\"}" \
| HOOK_RUN >/dev/null 2>&1 || true
done
echo '{"hook_event_name":"UserPromptSubmit","prompt":"go on","session_id":"sT2","cwd":"/tmp/x"}' \
| HOOK_RUN >/dev/null 2>&1 || true
if grep -qE '"type":"task_completed"' "$ROOT/journal.jsonl"; then
echo " FAIL: task_completed fired on single-tool task"; FAIL=$((FAIL+1))
else
echo " PASS: task_completed suppressed (low tool diversity)"; PASS=$((PASS+1))
fi
# --- Test 26: task_completed suppressed when correction fires mid-task ---
echo "Test 26: task_completed suppressed after correction"
reset_state
for kind in Bash Read Edit Write Grep; do
echo "{\"hook_event_name\":\"PostToolUse\",\"tool_name\":\"$kind\",\"tool_input\":{},\"session_id\":\"sT3\",\"cwd\":\"/tmp/x\"}" \
| HOOK_RUN >/dev/null 2>&1 || true
done
# Correction phrase resets task_corrections inside the same UserPromptSubmit cycle, so the prior run is disqualified.
echo '{"hook_event_name":"UserPromptSubmit","prompt":"no, undo that","session_id":"sT3","cwd":"/tmp/x"}' \
| HOOK_RUN >/dev/null 2>&1 || true
if grep -qE '"type":"task_completed"' "$ROOT/journal.jsonl"; then
echo " FAIL: task_completed fired despite correction on the closing prompt"; FAIL=$((FAIL+1))
else
echo " PASS: task_completed suppressed by correction"; PASS=$((PASS+1))
fi
echo
echo "Results: $PASS passed, $FAIL failed"
[ "$FAIL" = "0" ]