ulysses-protocol-d6bf2d — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited ulysses-protocol-d6bf2d (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Ulysses is a Thoughtbox-owned debugging protocol.
The invariants live in the server-side Ulysses implementation behind tb.ulysses(...). The durable trace lives in Thoughtbox thoughts and knowledge. Claude Code hooks only enforce the current server state.
Do not use .ulysses/ files or scripts/ulysses.sh as authoritative state. Do not use legacy direct handles like thoughtbox_gateway or thoughtbox_ulysses as the interaction surface when Code Mode is available.
The current public Thoughtbox MCP surface is Code Mode:
thoughtbox_searchthoughtbox_executetb.ulysses({ ... })tb.notebook.create(...) and tb.notebook.addCell(...)Each thoughtbox_execute call should contain at most one state-mutating Ulysses operation. Read-only confirmation calls such as tb.ulysses({ operation: "status" }) are safe to use for state checks.
Example execution wrapper:
async () => {
return await tb.ulysses({
operation: "status",
});
}thoughtbox_search beforeexecuting.
thoughtbox_execute and call tb.ulysses({ operation: ... }) for everyprotocol transition.
S=2; read-only inspection remains allowed.reflect, but only the coordinator calls tb.ulysses.initRequired inputs:
Call:
async () => {
return await tb.ulysses({
operation: "init",
problem: "<problem>",
constraints: ["<optional constraint>"],
});
}Then record a structured Thoughtbox thought summarizing the debugging context.
planRequired inputs:
irreversibleEach validator is a code cell that reads observed data from process.env.TB_OBSERVED_PATH and writes a verdict to process.env.TB_VERDICT_PATH. Use the auto-materialised helper:
import { observed, pass, fail } from "./tb-validate.js";
const d = observed<{ errors: number }>();
d.errors === 0 ? pass("clean run") : fail(`${d.errors} errors`, d);Cells are snapshotted at plan time (source + package.json + tsconfig hashed with sha256). Later edits to the notebook cannot influence the verdict.
Call:
async () => {
return await tb.ulysses({
operation: "plan",
primary: "<primary action>",
recovery: "<recovery action>",
irreversible: false,
primaryValidator: { notebookId: "<id>", cellId: "<id>" },
recoveryValidator: { notebookId: "<id>", cellId: "<id>" },
});
}Do not act before plan is recorded.
outcomeRequired inputs:
observed: any JSON-serialisable value piped into the validator cell bound for the current S phase. The cell's pass/fail verdict — not any agent claim — drives the state machine.details (free-form notes attached to the history event)Call:
async () => {
return await tb.ulysses({
operation: "outcome",
observed: { errorCount: 3, lastLog: "..." },
details: "<what happened>",
});
}State transitions derived from the verdict:
expected, S→0, checkpointunexpected-unfavorable, S→2, recovery pendingvalidator_tampering history eventIf the returned state reaches S=2 with no active_step, stop mutating work and move to reflect.
bind_final_validatorPin a notebook code cell as the predicate that gates complete(resolved). The cell is snapshotted and pinned at bind time.
Call:
async () => {
return await tb.ulysses({
operation: "bind_final_validator",
notebookId: "<id>",
cellId: "<id>",
});
}reflectRequired inputs:
Call:
async () => {
return await tb.ulysses({
operation: "reflect",
hypothesis: "<hypothesis>",
falsification: "<what would disprove it>",
});
}After reflect, you may optionally launch debugger or researcher agents to test competing explanations. They return evidence only. The coordinator records the next plan or outcome.
statusCall:
async () => {
return await tb.ulysses({
operation: "status",
});
}Use the returned server state as the only source of truth.
completeterminalState='resolved' is hard-gated by the final validator if one is bound. The agent must supply observed data; the validator runs against the pinned snapshot, and the terminal is rejected if the validator returns fail or its hash does not match.
Call:
async () => {
return await tb.ulysses({
operation: "complete",
terminalState: "resolved",
observed: { errorCount: 0, passingTests: 42 },
summary: "<transferable learning>",
});
}The other terminals (insufficient_information, environment_compromised) do not run the final validator and accept the existing call shape.
Completion should yield both protocol closure and a reusable knowledge artifact in Thoughtbox.
reflect is mandatory at S=2.complete(resolved) is hard-gated by the final validator when bound.A validator cell is an ordinary code cell in a Thoughtbox notebook. Use the auto-materialised helper for ergonomics:
import { observed, pass, fail } from "./tb-validate.js";
interface Observed {
errorCount: number;
status: "ok" | "degraded" | "down";
}
const d = observed<Observed>();
if (d.status === "ok" && d.errorCount === 0) {
pass("system healthy");
} else {
fail(`status=${d.status}, errors=${d.errorCount}`, d);
}Verdict semantics: pass → assessment expected. fail → assessment unexpected-unfavorable. Anything else (no verdict file, malformed JSON, crash, timeout) → pass=false, reason="malformed_verdict".
Authoring rules:
Use subagents only after reflect when more evidence would help.
Good uses:
Bad uses:
tb.ulyssesthoughtbox_search, thoughtbox_executetb.ulysses({ operation: ... })operations through Code Mode
src/protocol/ulysses-tool.tsreferences/protocol-spec.md~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.