diff --git a/Taskfile.yml b/Taskfile.yml index b5e9007..1263462 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -110,7 +110,11 @@ tasks: # harness instead of erroring. sh '{{.TASKFILE_DIR}}/bin/check-surfaces' '{{.TASKFILE_DIR}}' || fail=1 - # 5. Plugin manifest version must match the latest tag, or installers + # 5. A documented session_log call must be one the tool would accept: + # session_id is its only required field, and nine skills omitted it. + sh '{{.TASKFILE_DIR}}/bin/check-session-log' '{{.TASKFILE_DIR}}' || fail=1 + + # 6. Plugin manifest version must match the latest tag, or installers # never see the update (Claude Code gates updates on this field). if [ -f '{{.PLUGIN_MANIFEST}}' ]; then manifest_v=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' '{{.PLUGIN_MANIFEST}}' | head -1) diff --git a/bin/check-session-log b/bin/check-session-log new file mode 100755 index 0000000..cb8770a --- /dev/null +++ b/bin/check-session-log @@ -0,0 +1,39 @@ +#!/bin/sh +# check-session-log — a skill that documents a session_log call must document a +# call the tool would actually accept. +# +# Nine skills documented the payload and none named `session_id`, which is the +# tool's only REQUIRED field. An agent following any of them either failed the +# call or invented an id — and invented ids do not group, so the pass-rate the +# endpoint computes was being assembled from entries that could not be joined +# (#36, found by a pressure test against `debug`). +# +# Also rejected: `duration_ms: `. An agent does not observe a clock. +# A documented field nobody can honestly populate gets fabricated or silently +# dropped, and fabricated numbers in a metrics stream are worse than no numbers. +# +# Usage: check-session-log [checkout-root] + +set -u + +ROOT=${1:-$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)} +fail=0 + +for dir in "$ROOT"/skills/*/; do + [ -f "$dir/SKILL.md" ] || continue + name=$(basename "$dir") + + grep -q 'session_log' "$dir/SKILL.md" || continue + + if ! grep -q 'session_id' "$dir/SKILL.md"; then + echo "ERROR: skills/$name documents session_log without session_id — the tool's only required field" >&2 + fail=1 + fi + + if grep -q 'duration_ms:[[:space:]]*<' "$dir/SKILL.md"; then + echo "ERROR: skills/$name documents duration_ms as a placeholder an agent cannot measure — omit it or pass a real measurement" >&2 + fail=1 + fi +done + +exit $fail diff --git a/skills/cad-babysit/SKILL.md b/skills/cad-babysit/SKILL.md index 75f3ab4..7369423 100644 --- a/skills/cad-babysit/SKILL.md +++ b/skills/cad-babysit/SKILL.md @@ -234,8 +234,8 @@ wrong" — it means "nothing on the known list is wrong." Call `session_log` once at the end of every phase to record the outcome. **At end of each phase:** -- `session_log` with `{skill: "cad-babysit", phase: "", final_status: "pass" | - "fail" | "skip", message: "", duration_ms: , project_root: +- `session_log` with `{session_id: "", skill: "cad-babysit", phase: "", final_status: "pass" | + "fail" | "skip", message: "", project_root: ""}` **Phases for this skill:** query, read-comments, read-actions-logs, classify-fault-vs-outcome, diff --git a/skills/code-review/SKILL.md b/skills/code-review/SKILL.md index 7f05048..9ef66d3 100644 --- a/skills/code-review/SKILL.md +++ b/skills/code-review/SKILL.md @@ -253,7 +253,7 @@ Pass-rate is computed downstream by the `/pass-rate` HTTP endpoint, which treats `pass` as success, `fail` as failure, `skip` as neither. **At end of each phase:** -- `session_log` with `{skill: "code-review", phase: "", final_status: "pass" | "fail" | "skip", message: "", duration_ms: , project_root: ""}` +- `session_log` with `{session_id: "", skill: "code-review", phase: "", final_status: "pass" | "fail" | "skip", message: "", project_root: ""}` **Phases for this skill:** phase-0-iron-laws, phase-1-context, phase-2-architectural-analysis, phase-3-go-checklist, phase-4-code-smell-detection, phase-5-test-quality diff --git a/skills/debug/SKILL.md b/skills/debug/SKILL.md index 5052289..290da99 100644 --- a/skills/debug/SKILL.md +++ b/skills/debug/SKILL.md @@ -154,7 +154,7 @@ Pass-rate is computed downstream by the `/pass-rate` HTTP endpoint, which treats `pass` as success, `fail` as failure, `skip` as neither. **At end of each phase:** -- `session_log` with `{skill: "debug", phase: "", final_status: "pass" | "fail" | "skip", message: "", duration_ms: , project_root: ""}` +- `session_log` with `{session_id: "", skill: "debug", phase: "", final_status: "pass" | "fail" | "skip", message: "", project_root: ""}` **Phases for this skill:** read-failure, generate-hypotheses, output diff --git a/skills/experiment-spec/SKILL.md b/skills/experiment-spec/SKILL.md index f4782f4..1654cda 100644 --- a/skills/experiment-spec/SKILL.md +++ b/skills/experiment-spec/SKILL.md @@ -185,7 +185,7 @@ If silhouette ≤ 0.20: **After experiment concludes:** - `brain_write` to `jepa-fx/failures/` with any new failure modes discovered -- `session_log` with outcome +- `session_log` with `{session_id: "", skill: "experiment-spec", phase: "conclude", final_status: "pass" | "fail" | "skip", message: ""}` ## Cross-References diff --git a/skills/feature-spec/SKILL.md b/skills/feature-spec/SKILL.md index 29c5628..572af74 100644 --- a/skills/feature-spec/SKILL.md +++ b/skills/feature-spec/SKILL.md @@ -245,7 +245,7 @@ Pass-rate is computed downstream by the `/pass-rate` HTTP endpoint, which treats `pass` as success, `fail` as failure, `skip` as neither. **At end of each phase:** -- `session_log` with `{skill: "feature-spec", phase: "", final_status: "pass" | "fail" | "skip", message: "", duration_ms: , project_root: ""}` +- `session_log` with `{session_id: "", skill: "feature-spec", phase: "", final_status: "pass" | "fail" | "skip", message: "", project_root: ""}` **Phases for this skill:** draft diff --git a/skills/session-retrospective/SKILL.md b/skills/session-retrospective/SKILL.md index bbb7f77..1c090d9 100644 --- a/skills/session-retrospective/SKILL.md +++ b/skills/session-retrospective/SKILL.md @@ -116,7 +116,7 @@ Pass-rate is computed downstream by the `/pass-rate` HTTP endpoint, which treats `pass` as success, `fail` as failure, `skip` as neither. **At end of each phase:** -- `session_log` with `{skill: "session-retrospective", phase: "", final_status: "pass" | "fail" | "skip", message: "", duration_ms: , project_root: ""}` +- `session_log` with `{session_id: "", skill: "session-retrospective", phase: "", final_status: "pass" | "fail" | "skip", message: "", project_root: ""}` **Phases for this skill:** surfaced diff --git a/skills/spec-driven-dev/SKILL.md b/skills/spec-driven-dev/SKILL.md index 39e9dba..9634193 100644 --- a/skills/spec-driven-dev/SKILL.md +++ b/skills/spec-driven-dev/SKILL.md @@ -332,7 +332,7 @@ Pass-rate is computed downstream by the `/pass-rate` HTTP endpoint, which treats `pass` as success, `fail` as failure, `skip` as neither. **At end of each phase:** -- `session_log` with `{skill: "spec-driven-dev", phase: "", final_status: "pass" | "fail" | "skip", message: "", duration_ms: , project_root: ""}` +- `session_log` with `{session_id: "", skill: "spec-driven-dev", phase: "", final_status: "pass" | "fail" | "skip", message: "", project_root: ""}` **Phases for this skill:** specify, plan, tasks, implement diff --git a/skills/tdd/SKILL.md b/skills/tdd/SKILL.md index 3b7f09c..14840f0 100644 --- a/skills/tdd/SKILL.md +++ b/skills/tdd/SKILL.md @@ -406,13 +406,13 @@ Pass-rate is computed downstream by the `/pass-rate` HTTP endpoint, which treats `pass` as success, `fail` as failure, `skip` as neither. **At end of `red` phase:** -- `session_log` with `{skill: "tdd", phase: "red", final_status: "pass" | "fail" | "skip", message: "", duration_ms: , project_root: ""}` +- `session_log` with `{session_id: "", skill: "tdd", phase: "red", final_status: "pass" | "fail" | "skip", message: "", project_root: ""}` **At end of `green` phase:** -- `session_log` with `{skill: "tdd", phase: "green", final_status: "pass" | "fail" | "skip", message: "", duration_ms: , project_root: ""}` +- `session_log` with `{session_id: "", skill: "tdd", phase: "green", final_status: "pass" | "fail" | "skip", message: "", project_root: ""}` **At end of `refactor` phase:** -- `session_log` with `{skill: "tdd", phase: "refactor", final_status: "pass" | "fail" | "skip", message: "", duration_ms: , project_root: ""}` +- `session_log` with `{session_id: "", skill: "tdd", phase: "refactor", final_status: "pass" | "fail" | "skip", message: "", project_root: ""}` **Status semantics:** - `pass` — the phase's intended outcome was reached (red: test fails as expected; green: test passes; refactor: tests still pass after refactor). diff --git a/skills/trainer/SKILL.md b/skills/trainer/SKILL.md index 8d8db67..b2e6bdc 100644 --- a/skills/trainer/SKILL.md +++ b/skills/trainer/SKILL.md @@ -231,10 +231,10 @@ Pass-rate is computed downstream by the `/pass-rate` HTTP endpoint, which treats `pass` as success, `fail` as failure, `skip` as neither. **At end of `read` phase:** -- `session_log` with `{skill: "trainer", phase: "read", final_status: "pass" | "fail" | "skip", message: "", duration_ms: , project_root: ""}` +- `session_log` with `{session_id: "", skill: "trainer", phase: "read", final_status: "pass" | "fail" | "skip", message: "", project_root: ""}` **At end of `write` phase:** -- `session_log` with `{skill: "trainer", phase: "write", final_status: "pass" | "fail" | "skip", message: "", duration_ms: , project_root: ""}` +- `session_log` with `{session_id: "", skill: "trainer", phase: "write", final_status: "pass" | "fail" | "skip", message: "", project_root: ""}` **Phases for this skill:** read, write diff --git a/tests/check_session_log_test.sh b/tests/check_session_log_test.sh new file mode 100644 index 0000000..c29a214 --- /dev/null +++ b/tests/check_session_log_test.sh @@ -0,0 +1,67 @@ +#!/bin/sh +# Tests for bin/check-session-log. +# +# The regression: nine skills documented a session_log payload and not one of +# them named `session_id` — the tool's ONLY required field. Every documented +# call was incomplete, so an agent following any of them either failed the call +# or invented an id, and invented ids do not group. Found by a pressure test +# against `debug`, then found to be fleet-wide (#36). +# +# Also gated: `duration_ms: `, which an agent cannot measure. A +# documented field nobody can honestly populate gets fabricated or silently +# dropped; both are worse than omitting it. + +set -u + +ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +SUT="$ROOT/bin/check-session-log" + +pass=0 +fail=0 +WORK=${TMPDIR:-/tmp}/check_session_log_test.$$ + +if [ ! -f "$SUT" ]; then + printf 'FAIL: %s does not exist\n' "$SUT" + exit 1 +fi + +cleanup() { rm -rf "$WORK"; } +trap cleanup EXIT INT TERM + +# fixture — a checkout with one skill whose SKILL.md is . +fixture() { + _f="$WORK/$1" + mkdir -p "$_f/skills/alpha" + printf -- '---\nname: alpha\ndescription: a\n---\n\n%s\n' "$2" > "$_f/skills/alpha/SKILL.md" + printf '%s' "$_f" +} + +# expect