Compare commits

...
8 Commits
Author SHA1 Message Date
mathias 1d368d4b5d docs(skills): drop the Mode 2 routing notes, fix the contributing steps
CI / Check / Test (push) Successful in 1s
CI / Tag release (push) Successful in 1s
Seven skills carried a `## Mode 2 Routing Note` citing a routing pod ("Plan 6")
that does not exist. Several admitted in their own text that nothing about the
skill changes between modes — so it was roughly ten lines of context on every
load for zero behavioural effect. The content is not worthless, just not
per-skill: it now lives as one table in README.md, with the caveat that nothing
depends on it.

The related line inside the session_log sections stays, because that one does
motivate behaviour — but it no longer cites a component that has never shipped.
It now says what is true: pass-rate is what a future routing decision would be
based on, and a skill that never logs contributes no evidence either way.

While in README: the contributing steps were stale enough to fail CI for anyone
who followed them. They said to add a row to SKILLS_INDEX.md by hand — the table
is generated now, and `task check` fails on a hand-edited diff — and listed none
of the four frontmatter keys the gates require. Step 4 now names the
pressure-test bar from #19 rather than leaving it to be discovered.

Closes #36
Bump-Type: patch
2026-08-29 14:50:14 +02:00
mathias a565b04e91 chore(release): plugin.json version 0.13.3
Skip-Release: true
2026-08-29 12:49:56 +00:00
mathias b616c51a2c fix(skills): document a session_log call the tool would actually accept
CI / Check / Test (push) Successful in 1s
CI / Tag release (push) Successful in 1s
Nine skills documented the payload. None named `session_id`, which is the
tool's only required field. So every documented call was one the tool rejects —
an agent following any of them either failed the call or invented an id, and
invented ids do not group, which means the pass-rate the /pass-rate endpoint
computes was being assembled from entries that cannot be joined.

Found by the #19 pressure test against `debug`, which invented
`debug-signed-artifact-missing-2026-08-29` and said so: "my three entries are
only groupable by luck of naming". The grep afterwards showed it was fleet-wide,
not a `debug` bug.

Also removed: `duration_ms: <wall-clock>` in eight of them. An agent does not
observe a clock. The same run omitted it "rather than fabricate", which is the
good outcome; the bad outcome is the run that fabricates, and a made-up number
in a metrics stream is worse than a missing one. It is an optional field and is
now simply absent.

bin/check-session-log gates both, with tests. This is exactly the case #19 has
in mind for a small tool over prose: the rule is a grep, so it should be a gate,
not a note asking people to remember.

Closes part of #36
Bump-Type: patch
2026-08-29 14:49:20 +02:00
mathias b8bacfa7b3 chore(release): plugin.json version 0.13.2
Skip-Release: true
2026-08-29 12:22:35 +00:00
mathias c377c23e21 fix(mcp): correct the parked reason — the tokens work, the delivery does not
CI / Check / Test (push) Successful in 2s
CI / Tag release (push) Successful in 1s
The previous note blamed infra#224 for cad and gitea-mcp being unreachable.
Both claims were wrong. infra#224 is closed, and closed deliberately: rather
than widening koala-homelab's grants, a second scoped token was provisioned at
~/.op-agent-write.env for AgentSecrets. Sourcing it makes `op vault list`
return AgentSecrets, and an MCP initialize against both endpoints returns 200 —
serverInfo gitea-mcp 0.1.0 and cad aed2aaf. Nothing about the services or the
proxying is blocked.

What is actually blocked is delivery into the session environment. By design no
single identity on koala reads both vaults, and the session env comes from one
`op run --env-file ~/.op-env` under the HomeLab-only token. A mixed-vault file
does not degrade gracefully: tested on a two-line copy, `op run` aborts on the
unresolvable reference and BRAIN_MCP_TOKEN goes unresolved with it — so
appending to ~/.op-env would take out the MCP servers that currently work.

Refs #32, #33
Bump-Type: patch
2026-08-29 14:22:17 +02:00
mathias 8f9d5dd8bd chore(release): plugin.json version 0.13.1
Skip-Release: true
2026-08-29 12:19:31 +00:00
mathias 7ba01abdbe chore(mcp): park cad and gitea-mcp entries, blocked on vault visibility
CI / Tag release (push) Successful in 1s
CI / Check / Test (push) Successful in 1s
Both services are now proxied from koala and reachable: tailscale serve 8445 ->
127.0.0.1:31877 (cad) and 8446 -> 127.0.0.1:30340 (gitea-mcp). Both answer 401
unauthenticated, so the path is right and auth is enforced.

They stay out of mcpServers because neither token resolves on this host. The
static tokens live in the 1Password AgentSecrets vault; koala's op identity sees
HomeLab only:

  op vault list                    -> HomeLab, and nothing else
  op://HomeLab/CAD_STATIC_TOKEN    -> could not find item
  op://HomeLab/gitea-mcp-shared-token -> could not find item

That is infra#224, not the OAuth-discovery bug #32 and #33 both flagged as the
likely blocker. A live entry that cannot authenticate would fail on every
session start, so the wiring is recorded rather than enabled.

The item names come from the ExternalSecrets, read by reference only — no
secret value was retrieved, printed, or transformed at any point.

Refs #32, #33
Bump-Type: patch
2026-08-29 14:18:38 +02:00
mathias 9a81a707af chore(release): plugin.json version 0.13.0
Skip-Release: true
2026-08-29 12:10:37 +00:00
15 changed files with 190 additions and 58 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "skills",
"description": "Mathias's engineering skills library, plus the hooks and MCP servers that make it enforceable: TDD, clean code, debugging, specs, discovery, CI, and secret hygiene.",
"version": "0.12.1",
"version": "0.13.3",
"author": {
"name": "Mathias",
"url": "https://git.d-ma.be/mathias"
+18 -1
View File
@@ -12,5 +12,22 @@
"type": "http",
"url": "https://koala.tail30472e.ts.net:8444/mcp"
}
},
"_parked": {
"_reason": "cad and gitea-mcp are proxied, reachable and AUTHENTICATING: tailscale serve 8445 -> 127.0.0.1:31877 and 8446 -> 127.0.0.1:30340; an MCP initialize with the correct bearer returns 200 from both (serverInfo gitea-mcp 0.1.0, cad aed2aaf). They stay parked only because nothing puts those two tokens into the session environment. Both live in the 1Password AgentSecrets vault, and by deliberate design (infra#224, closed) no single identity on koala reads both vaults: koala-homelab is HomeLab-only and resolves ~/.op-env, koala-agentsecrets-write is AgentSecrets-only and lives in ~/.op-agent-write.env. Adding an AgentSecrets reference to ~/.op-env does NOT degrade gracefully -- `op run` fails wholesale and every other token, BRAIN_MCP_TOKEN included, goes unresolved with it. Enabling needs a read-scoped AgentSecrets identity in the session-env path; see mathias/skills#32, #33.",
"cad": {
"type": "http",
"url": "https://koala.tail30472e.ts.net:8445/mcp",
"headers": {
"Authorization": "Bearer ${CAD_MCP_TOKEN}"
}
},
"gitea": {
"type": "http",
"url": "https://koala.tail30472e.ts.net:8446/mcp",
"headers": {
"Authorization": "Bearer ${GITEA_MCP_STATIC_TOKEN}"
}
}
}
}
}
+40 -5
View File
@@ -130,13 +130,48 @@ before `task skills:install`; default is `main` for always-current.
## Contributing a skill
1. Add a directory `<short-kebab-name>/`.
2. Create `<short-kebab-name>/SKILL.md` — start with one-paragraph
purpose, then triggers ("use when …"), then mechanics.
3. Add a row to `SKILLS_INDEX.md` (description + use-when).
4. Commit directly to `main`. Conventional-commit prefix
1. Add a directory `skills/<short-kebab-name>/`.
2. Create `skills/<short-kebab-name>/SKILL.md` with this frontmatter — all four
keys are gated by `task check`, and the index table is generated from the
last three:
```yaml
---
name: <short-kebab-name>
surfaces: [claude-code, library] # any of claude-code | library | chat
summary: "<one line for the index table>"
use_when: "<the trigger, for the index table>"
description: <the paragraph the model selects on>
---
```
Then: one-paragraph purpose, triggers ("use when …"), mechanics.
3. Run `task index` and commit the regenerated `SKILLS_INDEX.md`. Do **not**
hand-edit the table — `task check` regenerates it and fails on a diff.
4. Pressure-test it before shipping: run the scenario against a subagent that
does *not* have the skill, write the skill against what that run actually
did, then re-run with it. See `superpowers-writing-skills` and #19.
5. Commit directly to `main`. Conventional-commit prefix
(`feat:` / `fix:` / `docs:`). Add `Bump-Type: minor` footer when
adding a new skill or expanding scope; default patch covers edits.
This repo is markdown-only and public — never include secrets, client
names, or anything under NDA in a skill.
## Mode 2 routing candidates
These notes used to sit in each SKILL.md as a `## Mode 2 Routing Note` section.
They changed nothing about how the skill is invoked — several said so outright —
and cost context on every load, so they live here instead. Nothing in the repo
depends on this table; it is a record of which skills looked mechanical enough
to route to a local model, should a routing layer ever exist.
| Skill | Note |
|---|---|
| `cad-babysit` | Phases 2-4 (reading comments/logs, classifying fault vs outcome) are judgment-heavy and should stay Mode 1. Phase 1 (running `cad stuck` and grouping its output) is mechanical and a Mode 2 routing candidate once Plan 6 ships the routing pod. |
| `code-review` | This skill is invoked identically in Mode 1 (cloud Claude) and Mode 2 (client-local with supervisor routing). The routing layer (Plan 6) does not exist yet; treat this skill as Mode 1 only for now. Findings format and severity classification are unchanged across modes. |
| `debug` | This skill produces high-volume mechanical output (hypothesis enumeration) and is a candidate for Mode 2 routing to a local model in the future. Until Plan 6 ships the routing pod, treat as Mode 1 only. The hypothesis format and discipline are identical regardless of which model generates them. |
| `feature-spec` | Spec writing requires interpretation of intent and tradeoff reasoning — work where Claude generally outperforms a 9-13B local model. This skill is intentionally **not** a Mode 2 routing candidate. In Mode 2, the routing layer should leave `feature-spec` calls on Claude. |
| `session-retrospective` | Reading long session logs and pulling themes is high-volume, mechanical pattern-recognition work — a strong Mode 2 routing candidate. Until Plan 6 ships the routing pod, treat as Mode 1 only. |
| `tdd` | This skill is invoked identically whether the agent is running in Mode 1 (cloud Claude, no routing) or Mode 2 (client-local, supervisor routing layer). The routing pod (Plan 6) does not exist yet; until it does, treat this skill as Mode 1 only. The discipline does not change between modes — only the model behind the call. |
| `trainer` | Trainer is the same shape as `session-retrospective` — long-context reading, mechanical filtering — and is a Mode 2 routing candidate. Until Plan 6, treat as Mode 1 only. |
+5 -1
View File
@@ -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)
+39
View File
@@ -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: <wall-clock>`. 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
+2 -8
View File
@@ -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: "<phase-name>", final_status: "pass" |
"fail" | "skip", message: "<one-line summary>", duration_ms: <wall-clock>, project_root:
- `session_log` with `{session_id: "<this session's id>", skill: "cad-babysit", phase: "<phase-name>", final_status: "pass" |
"fail" | "skip", message: "<one-line summary>", project_root:
"<absolute path>"}`
**Phases for this skill:** query, read-comments, read-actions-logs, classify-fault-vs-outcome,
@@ -247,12 +247,6 @@ decide
- `fail` — the phase could not be completed (e.g. `cad stuck` errored, Gitea was unreachable).
- `skip` — phase intentionally skipped (e.g. Phase 3 skipped because Phase 2 was conclusive).
## Mode 2 Routing Note
Phases 2-4 (reading comments/logs, classifying fault vs outcome) are judgment-heavy and should
stay Mode 1. Phase 1 (running `cad stuck` and grouping its output) is mechanical and a Mode 2
routing candidate once Plan 6 ships the routing pod.
## Cross-References
- Depends on `mathias/cad#14`'s `cad stuck` query — this skill is the ritual around it, not a
+2 -6
View File
@@ -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: "<phase-name>", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", duration_ms: <wall-clock>, project_root: "<absolute path>"}`
- `session_log` with `{session_id: "<this session's id>", skill: "code-review", phase: "<phase-name>", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", project_root: "<absolute path>"}`
**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
@@ -262,7 +262,7 @@ treats `pass` as success, `fail` as failure, `skip` as neither.
- `fail` — the phase's intended outcome was NOT reached.
- `skip` — phase was skipped intentionally.
**Why this matters:** the routing pod (Plan 6) reads pass-rate to decide whether to route a future call to a local model. If your skill never logs, the routing pod sees no data.
**Why this matters:** pass-rate is what a future routing decision would be based on — whether a given call can be served by a local model instead of a cloud one. A skill that never logs contributes no evidence either way.
## Common Go Anti-Patterns
@@ -285,7 +285,3 @@ treats `pass` as success, `fail` as failure, `skip` as neither.
- Load `refactoring` skill when recommending fixes
- Load `test-design` skill for reviewing test quality
- Load `solid` skill for SOLID principle violations
## Mode 2 Routing Note
This skill is invoked identically in Mode 1 (cloud Claude) and Mode 2 (client-local with supervisor routing). The routing layer (Plan 6) does not exist yet; treat this skill as Mode 1 only for now. Findings format and severity classification are unchanged across modes.
+2 -6
View File
@@ -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: "<phase-name>", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", duration_ms: <wall-clock>, project_root: "<absolute path>"}`
- `session_log` with `{session_id: "<this session's id>", skill: "debug", phase: "<phase-name>", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", project_root: "<absolute path>"}`
**Phases for this skill:** read-failure, generate-hypotheses, output
@@ -163,11 +163,7 @@ treats `pass` as success, `fail` as failure, `skip` as neither.
- `fail` — the phase's intended outcome was NOT reached.
- `skip` — phase was skipped intentionally.
**Why this matters:** the routing pod (Plan 6) reads pass-rate to decide whether to route a future call to a local model. If your skill never logs, the routing pod sees no data.
## Mode 2 Routing Note
This skill produces high-volume mechanical output (hypothesis enumeration) and is a candidate for Mode 2 routing to a local model in the future. Until Plan 6 ships the routing pod, treat as Mode 1 only. The hypothesis format and discipline are identical regardless of which model generates them.
**Why this matters:** pass-rate is what a future routing decision would be based on — whether a given call can be served by a local model instead of a cloud one. A skill that never logs contributes no evidence either way.
## Cross-References
+1 -1
View File
@@ -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: "<this session's id>", skill: "experiment-spec", phase: "conclude", final_status: "pass" | "fail" | "skip", message: "<one-line outcome>"}`
## Cross-References
+2 -6
View File
@@ -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: "<phase-name>", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", duration_ms: <wall-clock>, project_root: "<absolute path>"}`
- `session_log` with `{session_id: "<this session's id>", skill: "feature-spec", phase: "<phase-name>", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", project_root: "<absolute path>"}`
**Phases for this skill:** draft
@@ -254,11 +254,7 @@ treats `pass` as success, `fail` as failure, `skip` as neither.
- `fail` — the phase's intended outcome was NOT reached (spec rejected, blocked on unresolved questions).
- `skip` — phase was skipped intentionally.
**Why this matters:** the routing pod (Plan 6) reads pass-rate to decide whether to route a future call to a local model. If your skill never logs, the routing pod sees no data.
## Mode 2 Routing Note
Spec writing requires interpretation of intent and tradeoff reasoning — work where Claude generally outperforms a 9-13B local model. This skill is intentionally **not** a Mode 2 routing candidate. In Mode 2, the routing layer should leave `feature-spec` calls on Claude.
**Why this matters:** pass-rate is what a future routing decision would be based on — whether a given call can be served by a local model instead of a cloud one. A skill that never logs contributes no evidence either way.
## Cross-References
+2 -6
View File
@@ -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: "<phase-name>", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", duration_ms: <wall-clock>, project_root: "<absolute path>"}`
- `session_log` with `{session_id: "<this session's id>", skill: "session-retrospective", phase: "<phase-name>", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", project_root: "<absolute path>"}`
**Phases for this skill:** surfaced
@@ -125,11 +125,7 @@ treats `pass` as success, `fail` as failure, `skip` as neither.
- `fail` — the phase's intended outcome was NOT reached (retrospective could not produce coherent candidates).
- `skip` — phase was skipped intentionally (no novel learnings worth surfacing).
**Why this matters:** the routing pod (Plan 6) reads pass-rate to decide whether to route a future call to a local model. If your skill never logs, the routing pod sees no data.
## Mode 2 Routing Note
Reading long session logs and pulling themes is high-volume, mechanical pattern-recognition work — a strong Mode 2 routing candidate. Until Plan 6 ships the routing pod, treat as Mode 1 only.
**Why this matters:** pass-rate is what a future routing decision would be based on — whether a given call can be served by a local model instead of a cloud one. A skill that never logs contributes no evidence either way.
## Cross-References
+2 -2
View File
@@ -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: "<phase-name>", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", duration_ms: <wall-clock>, project_root: "<absolute path>"}`
- `session_log` with `{session_id: "<this session's id>", skill: "spec-driven-dev", phase: "<phase-name>", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", project_root: "<absolute path>"}`
**Phases for this skill:** specify, plan, tasks, implement
@@ -341,7 +341,7 @@ treats `pass` as success, `fail` as failure, `skip` as neither.
- `fail` — the phase's intended outcome was NOT reached (gate blocked, rework required).
- `skip` — phase was skipped intentionally.
**Why this matters:** the routing pod (Plan 6) reads pass-rate to decide whether to route a future call to a local model. If your skill never logs, the routing pod sees no data.
**Why this matters:** pass-rate is what a future routing decision would be based on — whether a given call can be served by a local model instead of a cloud one. A skill that never logs contributes no evidence either way.
## Cross-References
+4 -8
View File
@@ -406,20 +406,20 @@ 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: "<one-line summary>", duration_ms: <wall-clock>, project_root: "<absolute path>"}`
- `session_log` with `{session_id: "<this session's id>", skill: "tdd", phase: "red", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", project_root: "<absolute path>"}`
**At end of `green` phase:**
- `session_log` with `{skill: "tdd", phase: "green", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", duration_ms: <wall-clock>, project_root: "<absolute path>"}`
- `session_log` with `{session_id: "<this session's id>", skill: "tdd", phase: "green", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", project_root: "<absolute path>"}`
**At end of `refactor` phase:**
- `session_log` with `{skill: "tdd", phase: "refactor", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", duration_ms: <wall-clock>, project_root: "<absolute path>"}`
- `session_log` with `{session_id: "<this session's id>", skill: "tdd", phase: "refactor", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", project_root: "<absolute path>"}`
**Status semantics:**
- `pass` — the phase's intended outcome was reached (red: test fails as expected; green: test passes; refactor: tests still pass after refactor).
- `fail` — the phase's intended outcome was NOT reached (test compiled when it shouldn't, test still fails after green attempt, refactor broke tests).
- `skip` — phase was skipped intentionally (e.g. refactor not warranted).
**Why this matters:** the routing pod (Plan 6) reads pass-rate to decide whether to route a future `tdd` call to a local model. If your skill never logs, the routing pod sees no data and may default-route or default-not-route in a way that doesn't reflect real performance.
**Why this matters:** pass-rate is what a future routing decision would be based on — whether a `tdd` call can be served by a local model instead of a cloud one. A skill that never logs contributes no evidence either way, and a decision made on no evidence is a coin flip wearing a metric.
## Final Rule
@@ -429,7 +429,3 @@ Otherwise → not TDD
```
No exceptions without Mathias's permission.
## Mode 2 Routing Note
This skill is invoked identically whether the agent is running in Mode 1 (cloud Claude, no routing) or Mode 2 (client-local, supervisor routing layer). The routing pod (Plan 6) does not exist yet; until it does, treat this skill as Mode 1 only. The discipline does not change between modes — only the model behind the call.
+3 -7
View File
@@ -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: "<one-line summary>", duration_ms: <wall-clock>, project_root: "<absolute path>"}`
- `session_log` with `{session_id: "<this session's id>", skill: "trainer", phase: "read", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", project_root: "<absolute path>"}`
**At end of `write` phase:**
- `session_log` with `{skill: "trainer", phase: "write", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", duration_ms: <wall-clock>, project_root: "<absolute path>"}`
- `session_log` with `{session_id: "<this session's id>", skill: "trainer", phase: "write", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", project_root: "<absolute path>"}`
**Phases for this skill:** read, write
@@ -243,11 +243,7 @@ treats `pass` as success, `fail` as failure, `skip` as neither.
- `fail` — the phase's intended outcome was NOT reached.
- `skip` — phase was skipped intentionally (e.g. no candidates passed the gate, write skipped).
**Why this matters:** the routing pod (Plan 6) reads pass-rate to decide whether to route a future call to a local model. If your skill never logs, the routing pod sees no data.
## Mode 2 Routing Note
Trainer is the same shape as `session-retrospective` — long-context reading, mechanical filtering — and is a Mode 2 routing candidate. Until Plan 6, treat as Mode 1 only.
**Why this matters:** pass-rate is what a future routing decision would be based on — whether a given call can be served by a local model instead of a cloud one. A skill that never logs contributes no evidence either way.
## Cross-References
+67
View File
@@ -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: <wall-clock>`, 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 <name> <body> — a checkout with one skill whose SKILL.md is <body>.
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 <rc> <label> <fixture> [needle]
expect() {
_want=$1; _label=$2; _fx=$3; _needle=${4:-}
_out=$(sh "$SUT" "$_fx" 2>&1); _rc=$?
if [ "$_rc" -ne "$_want" ]; then
fail=$((fail + 1)); printf ' FAIL %s (want rc=%s got %s)\n%s\n' "$_label" "$_want" "$_rc" "$_out"; return
fi
if [ -n "$_needle" ] && ! printf '%s' "$_out" | grep -q "$_needle"; then
fail=$((fail + 1)); printf ' FAIL %s (rc ok, report never says %s)\n%s\n' "$_label" "$_needle" "$_out"; return
fi
pass=$((pass + 1)); printf ' ok %s\n' "$_label"
}
expect 0 'a skill that never mentions session_log is fine' \
"$(fixture quiet 'Just a skill. No logging here.')"
expect 0 'a documented call naming session_id passes' \
"$(fixture good 'Call `session_log` with `{session_id: "<id>", skill: "alpha", phase: "red", final_status: "pass"}`')"
expect 1 'a documented call omitting session_id fails' \
"$(fixture bad 'Call `session_log` with `{skill: "alpha", phase: "red", final_status: "pass"}`')" \
'session_id'
expect 1 'documenting duration_ms as wall-clock fails' \
"$(fixture clock 'Call `session_log` with `{session_id: "<id>", skill: "alpha", duration_ms: <wall-clock>}`')" \
'duration_ms'
printf '\ncheck_session_log: %d passed, %d failed\n' "$pass" "$fail"
[ "$fail" -eq 0 ]