opencode-goal-mode 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Run the shell guard against the EXTERNAL, third-party-authored corpus built by
4
+ * build-external-corpus.mjs (real tldr-pages commands). This is the honest
5
+ * benchmark: the analyzer authors did not write or curate these commands, so the
6
+ * detection / false-positive numbers reflect real-world behavior, warts and all.
7
+ *
8
+ * It deliberately also reports DISAGREEMENTS between the analyzer and the
9
+ * independent ground-truth labeler, so misses and false positives are auditable
10
+ * rather than averaged away.
11
+ *
12
+ * node benchmarks/external.mjs # summary
13
+ * node benchmarks/external.mjs --json # full machine-readable result
14
+ */
15
+
16
+ import { readFileSync } from "node:fs";
17
+ import { join, dirname } from "node:path";
18
+ import { fileURLToPath } from "node:url";
19
+ import * as current from "../plugins/goal-guard/shell.js";
20
+ import * as legacy from "./legacy-analyzer.mjs";
21
+
22
+ const here = dirname(fileURLToPath(import.meta.url));
23
+
24
+ export function loadExternalCorpus() {
25
+ return JSON.parse(readFileSync(join(here, "external-corpus.json"), "utf8"));
26
+ }
27
+
28
+ function blocked(analyzer, cmd) {
29
+ const a = analyzer.analyzeCommand(cmd);
30
+ return Boolean(a.destructive || a.networkExec);
31
+ }
32
+
33
+ /** Evaluate one analyzer over labeled entries (each {cmd, page, destructive}). */
34
+ function score(analyzer, labeled) {
35
+ let destTotal = 0;
36
+ let destCaught = 0;
37
+ let safeTotal = 0;
38
+ let safeFalsePos = 0;
39
+ const misses = [];
40
+ const falsePositives = [];
41
+ for (const e of labeled) {
42
+ const isBlocked = blocked(analyzer, e.cmd);
43
+ if (e.destructive) {
44
+ destTotal += 1;
45
+ if (isBlocked) destCaught += 1;
46
+ else misses.push({ cmd: e.cmd, page: e.page });
47
+ } else {
48
+ safeTotal += 1;
49
+ if (isBlocked) {
50
+ safeFalsePos += 1;
51
+ falsePositives.push({ cmd: e.cmd, page: e.page });
52
+ }
53
+ }
54
+ }
55
+ return {
56
+ detectionRate: destTotal ? (destCaught / destTotal) * 100 : 0,
57
+ falsePositiveRate: safeTotal ? (safeFalsePos / safeTotal) * 100 : 0,
58
+ destCaught,
59
+ destTotal,
60
+ safeFalsePos,
61
+ safeTotal,
62
+ misses,
63
+ falsePositives,
64
+ };
65
+ }
66
+
67
+ export function runExternalBenchmark() {
68
+ const corpus = loadExternalCorpus();
69
+ // The corpus is written destructive-first then safe (see build-external-corpus.mjs),
70
+ // so the recorded count is the label boundary — no re-running the labeler needed.
71
+ const labeled = corpus.entries.map((e, i) => ({ ...e, destructive: i < corpus.totals.destructiveFound }));
72
+ return {
73
+ source: corpus.source,
74
+ commit: corpus.commit,
75
+ totals: corpus.totals,
76
+ sampleSize: labeled.length,
77
+ legacy: score(legacy, labeled),
78
+ current: score(current, labeled),
79
+ };
80
+ }
81
+
82
+ function pct(n) {
83
+ return `${n.toFixed(1)}%`;
84
+ }
85
+
86
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
87
+ const r = runExternalBenchmark();
88
+ if (process.argv.includes("--json")) {
89
+ console.log(JSON.stringify(r, null, 2));
90
+ } else {
91
+ console.log("External shell-guard benchmark (third-party tldr-pages commands)");
92
+ console.log("================================================================");
93
+ console.log(`Source: ${r.source} @ ${r.commit.slice(0, 12)}`);
94
+ console.log(
95
+ `Sample: ${r.sampleSize} commands ` +
96
+ `(${r.totals.destructiveFound} destructive [all found], ` +
97
+ `${r.totals.safeSampled}/${r.totals.safeFound} safe sampled)`,
98
+ );
99
+ console.log("");
100
+ console.log(`Detection (destructive caught) legacy ${pct(r.legacy.detectionRate)} → current ${pct(r.current.detectionRate)}`);
101
+ console.log(`False positives on safe commands legacy ${pct(r.legacy.falsePositiveRate)} → current ${pct(r.current.falsePositiveRate)}`);
102
+ console.log("");
103
+ console.log(`Current analyzer misses (${r.current.misses.length}):`);
104
+ for (const m of r.current.misses.slice(0, 20)) console.log(` - ${m.cmd} [${m.page}]`);
105
+ if (r.current.misses.length > 20) console.log(` … ${r.current.misses.length - 20} more`);
106
+ console.log(`Current analyzer false positives (${r.current.falsePositives.length}):`);
107
+ for (const f of r.current.falsePositives.slice(0, 20)) console.log(` - ${f.cmd} [${f.page}]`);
108
+ if (r.current.falsePositives.length > 20) console.log(` … ${r.current.falsePositives.length - 20} more`);
109
+ }
110
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * The ORIGINAL regex-based shell classifier, preserved verbatim from the first
3
+ * published version of the plugin (commit 130956d) so the benchmark can compare
4
+ * it apples-to-apples against the current quote-aware analyzer.
5
+ *
6
+ * Do not "improve" this file — its job is to faithfully represent the old
7
+ * behavior that the new analyzer replaced.
8
+ */
9
+
10
+ const MUTATING_BASH_PATTERNS = [
11
+ /(^|&&|;|\|\|)\s*(sudo\s+)?(rm|mv|cp|mkdir|rmdir|touch|ln)\b/i,
12
+ /(^|&&|;|\|\|)\s*(sudo\s+)?(tee|xargs\s+(rm|mv|cp))\b/i,
13
+ /(^|&&|;|\|\|)\s*[^|]*\s(>|>>)\s*(?!\/dev\/null\b)\S+/i,
14
+ /(^|&&|;|\|\|)\s*(perl\s+-pi|sed\s+-i)\b/i,
15
+ /(^|&&|;|\|\|)\s*(npm|pnpm|yarn|bun)\s+(install|ci|add|remove|update)\b/i,
16
+ /(^|&&|;|\|\|)\s*(npm|pnpm|yarn|bun)\s+(run\s+)?(format|fix|lint:fix)\b/i,
17
+ /\b((npx|pnpm\s+exec|yarn)\s+)?(prettier|eslint)\b.*\s(--write|--fix)\b/i,
18
+ /\b(node|python3?)\b.*\b(writeFile|appendFile|copyFile|rename|unlink|rmSync|mkdir|rmdir|openSync)\b/i,
19
+ ];
20
+
21
+ export function looksLikeDestructiveBash(command) {
22
+ const normalized = String(command || "").trim();
23
+ return [
24
+ /(^|&&|;|\|\|)\s*(sudo\s+)?rm\s+-[a-zA-Z]*[rR][a-zA-Z]*[rfRF]?\b/,
25
+ /(^|&&|;|\|\|)\s*(sudo\s+)?rm\s+(--recursive|--force|--recursive\s+--force|-rf|-fr|-r)\b/,
26
+ /(^|&&|;|\|\|)\s*git\s+reset\b/,
27
+ /(^|&&|;|\|\|)\s*git\s+clean\b/,
28
+ /(^|&&|;|\|\|)\s*git\s+checkout\b/,
29
+ /(^|&&|;|\|\|)\s*git\s+restore\b/,
30
+ /(^|&&|;|\|\|)\s*git\s+switch\b/,
31
+ /(^|&&|;|\|\|)\s*git\s+push\b/,
32
+ /(^|&&|;|\|\|)\s*(sudo\s+)?find\b.*\s-delete\b/,
33
+ /(^|&&|;|\|\|)\s*(sudo\s+)?find\b.*\s-exec\s+rm\b/,
34
+ /(^|&&|;|\|\|)\s*(sudo\s+)?dd\b.*\bof=\/dev\//,
35
+ /(^|&&|;|\|\|)\s*(sudo\s+)?mkfs(\.|\s|$)/,
36
+ /(^|&&|;|\|\|)\s*(sudo\s+)?shred\b/,
37
+ /(^|&&|;|\|\|)\s*(sudo\s+)?truncate\b/,
38
+ /(^|&&|;|\|\|)\s*(sudo\s+)?chmod\s+-[a-zA-Z]*[rR][a-zA-Z]*[wW][a-zA-Z]*[xX][a-zA-Z]*\s+\/\b/,
39
+ ].some((pattern) => pattern.test(normalized));
40
+ }
41
+
42
+ export function looksLikeMutatingBash(command) {
43
+ const normalized = String(command || "").trim();
44
+ if (!normalized) return false;
45
+ if (looksLikeDestructiveBash(normalized)) return true;
46
+ return MUTATING_BASH_PATTERNS.some((pattern) => pattern.test(normalized));
47
+ }
48
+
49
+ /** Adapter to the analyzer signal shape used by the benchmark. */
50
+ export function analyzeCommand(command) {
51
+ const destructive = looksLikeDestructiveBash(command);
52
+ const mutating = looksLikeMutatingBash(command);
53
+ return { destructive, mutating, verification: false, networkExec: false, reasons: [] };
54
+ }
@@ -0,0 +1,252 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Reproducible benchmark of the shell-command guard.
4
+ *
5
+ * Compares the original regex classifier (benchmarks/legacy-analyzer.mjs,
6
+ * preserved verbatim from the first release) against the current quote-aware
7
+ * analyzer (plugins/goal-guard/shell.js) on a labeled corpus, and measures
8
+ * detection rate, false-positive rate, and throughput. Writes results.json and
9
+ * the SVG charts embedded in the README.
10
+ *
11
+ * node benchmarks/run.mjs
12
+ */
13
+
14
+ import { writeFileSync, mkdirSync } from "node:fs";
15
+ import { join, dirname } from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+ import { performance } from "node:perf_hooks";
18
+ import { CORPUS } from "./corpus.mjs";
19
+ import * as current from "../plugins/goal-guard/shell.js";
20
+ import * as legacy from "./legacy-analyzer.mjs";
21
+ import { groupedBarChart, horizontalBarChart } from "./charts.mjs";
22
+ import { runTruthfulnessBenchmark } from "./truthfulness.mjs";
23
+ import { runExternalBenchmark } from "./external.mjs";
24
+
25
+ const root = fileURLToPath(new URL("..", import.meta.url));
26
+ const outDir = join(root, "docs", "benchmarks");
27
+ mkdirSync(outDir, { recursive: true });
28
+
29
+ /** A command is "blocked" when flagged destructive or as remote execution. */
30
+ function blocked(analyzer, cmd) {
31
+ const a = analyzer.analyzeCommand(cmd);
32
+ return Boolean(a.destructive || a.networkExec);
33
+ }
34
+
35
+ function evaluate(analyzer) {
36
+ const families = {};
37
+ let destTotal = 0;
38
+ let destCaught = 0;
39
+ let safeTotal = 0;
40
+ let safeFalsePos = 0;
41
+
42
+ for (const { cmd, label, family } of CORPUS) {
43
+ families[family] ??= { destTotal: 0, destCaught: 0, safeTotal: 0, safeFalsePos: 0 };
44
+ const isBlocked = blocked(analyzer, cmd);
45
+ if (label === "destructive") {
46
+ destTotal += 1;
47
+ families[family].destTotal += 1;
48
+ if (isBlocked) {
49
+ destCaught += 1;
50
+ families[family].destCaught += 1;
51
+ }
52
+ } else {
53
+ safeTotal += 1;
54
+ families[family].safeTotal += 1;
55
+ if (isBlocked) {
56
+ safeFalsePos += 1;
57
+ families[family].safeFalsePos += 1;
58
+ }
59
+ }
60
+ }
61
+
62
+ return {
63
+ detectionRate: destTotal ? (destCaught / destTotal) * 100 : 0,
64
+ falsePositiveRate: safeTotal ? (safeFalsePos / safeTotal) * 100 : 0,
65
+ destCaught,
66
+ destTotal,
67
+ safeFalsePos,
68
+ safeTotal,
69
+ families,
70
+ };
71
+ }
72
+
73
+ function throughput(analyzer) {
74
+ const cmds = CORPUS.map((c) => c.cmd);
75
+ // Warm up.
76
+ for (const c of cmds) analyzer.analyzeCommand(c);
77
+ const iterations = 4000;
78
+ const start = performance.now();
79
+ for (let i = 0; i < iterations; i += 1) {
80
+ for (const c of cmds) analyzer.analyzeCommand(c);
81
+ }
82
+ const ms = performance.now() - start;
83
+ const ops = (iterations * cmds.length) / (ms / 1000);
84
+ return Math.round(ops);
85
+ }
86
+
87
+ /** Locale-independent thousands grouping (the host locale may use '.' as separator). */
88
+ function fmt(n) {
89
+ return Math.round(n)
90
+ .toString()
91
+ .replace(/\B(?=(\d{3})+(?!\d))/g, ",");
92
+ }
93
+
94
+ const legacyEval = evaluate(legacy);
95
+ const currentEval = evaluate(current);
96
+ const external = runExternalBenchmark();
97
+ const truthfulness = runTruthfulnessBenchmark();
98
+ const legacyOps = throughput(legacy);
99
+ const currentOps = throughput(current);
100
+ const legacyUs = 1e6 / legacyOps;
101
+ const currentUs = 1e6 / currentOps;
102
+
103
+ const FAMILY_LABELS = {
104
+ classic: "Classic",
105
+ bypass: "Obfuscated",
106
+ "remote-exec": "Remote exec",
107
+ };
108
+ const detFamilies = ["classic", "bypass", "remote-exec"];
109
+
110
+ function familyRate(ev, fam) {
111
+ const f = ev.families[fam];
112
+ return f && f.destTotal ? (f.destCaught / f.destTotal) * 100 : 0;
113
+ }
114
+
115
+ // Trim the per-command miss/false-positive lists to keep results.json readable;
116
+ // the full lists are always available via `node benchmarks/external.mjs --json`.
117
+ const externalSummary = {
118
+ source: external.source,
119
+ commit: external.commit,
120
+ totals: external.totals,
121
+ sampleSize: external.sampleSize,
122
+ legacy: {
123
+ detectionRate: Number(external.legacy.detectionRate.toFixed(1)),
124
+ falsePositiveRate: Number(external.legacy.falsePositiveRate.toFixed(1)),
125
+ destCaught: external.legacy.destCaught,
126
+ destTotal: external.legacy.destTotal,
127
+ safeFalsePos: external.legacy.safeFalsePos,
128
+ safeTotal: external.legacy.safeTotal,
129
+ },
130
+ current: {
131
+ detectionRate: Number(external.current.detectionRate.toFixed(1)),
132
+ falsePositiveRate: Number(external.current.falsePositiveRate.toFixed(1)),
133
+ destCaught: external.current.destCaught,
134
+ destTotal: external.current.destTotal,
135
+ safeFalsePos: external.current.safeFalsePos,
136
+ safeTotal: external.current.safeTotal,
137
+ misses: external.current.misses.map((m) => m.cmd),
138
+ falsePositives: external.current.falsePositives.map((f) => f.cmd),
139
+ },
140
+ };
141
+
142
+ const results = {
143
+ // The honest, third-party benchmark: real commands the analyzer was never
144
+ // fitted to. This is the headline number.
145
+ external: externalSummary,
146
+ // Curated REGRESSION FIXTURES: a hand-authored set of known destructive
147
+ // patterns and their safe look-alikes. These define the patterns the analyzer
148
+ // is built to catch and guard against regressions — they are NOT an unbiased
149
+ // sample, so the 100%/0% here is "passes its own spec", not measured accuracy.
150
+ fixtures: {
151
+ corpusSize: CORPUS.length,
152
+ destructiveCount: CORPUS.filter((c) => c.label === "destructive").length,
153
+ safeCount: CORPUS.filter((c) => c.label === "safe").length,
154
+ legacy: { ...legacyEval, opsPerSec: legacyOps, usPerCommand: Number(legacyUs.toFixed(2)) },
155
+ current: { ...currentEval, opsPerSec: currentOps, usPerCommand: Number(currentUs.toFixed(2)) },
156
+ },
157
+ // Completion-enforcement fixtures (hand-authored policy cases), not a survey.
158
+ completionFixtures: truthfulness,
159
+ };
160
+
161
+ writeFileSync(join(outDir, "results.json"), JSON.stringify(results, null, 2));
162
+
163
+ // Headline chart: detection + false positives on the EXTERNAL third-party corpus.
164
+ writeFileSync(
165
+ join(outDir, "external-scorecard.svg"),
166
+ groupedBarChart({
167
+ title: "Guard accuracy on real third-party commands",
168
+ subtitle: `${external.sampleSize} tldr-pages commands the analyzer was never fitted to. Detection higher = better; false positives lower = better.`,
169
+ groups: ["Detection rate", "False-positive rate"],
170
+ series: [
171
+ { name: "Legacy regex guard", color: "#9aa0a6", values: [external.legacy.detectionRate, external.legacy.falsePositiveRate] },
172
+ { name: "Goal Mode analyzer", color: "#2da44e", values: [external.current.detectionRate, external.current.falsePositiveRate] },
173
+ ],
174
+ }),
175
+ );
176
+
177
+ // Chart 1: detection rate by command family (CURATED regression fixtures).
178
+ writeFileSync(
179
+ join(outDir, "detection-by-family.svg"),
180
+ groupedBarChart({
181
+ title: "Detection by family — curated regression fixtures",
182
+ subtitle: `Curated patterns the analyzer is built to catch (not an unbiased sample). ${results.fixtures.destructiveCount} destructive fixtures.`,
183
+ groups: detFamilies.map((f) => FAMILY_LABELS[f]),
184
+ series: [
185
+ { name: "Legacy regex guard", color: "#9aa0a6", values: detFamilies.map((f) => familyRate(legacyEval, f)) },
186
+ { name: "Goal Mode analyzer", color: "#2da44e", values: detFamilies.map((f) => familyRate(currentEval, f)) },
187
+ ],
188
+ }),
189
+ );
190
+
191
+ // Chart 2: overall scorecard on the CURATED fixtures (passes its own spec).
192
+ writeFileSync(
193
+ join(outDir, "overall-scorecard.svg"),
194
+ groupedBarChart({
195
+ title: "Curated fixtures — passes its own spec",
196
+ subtitle: "Curated regression fixtures, not measured accuracy. See external-scorecard.svg for the real-world number.",
197
+ groups: ["Detection rate", "False-positive rate"],
198
+ series: [
199
+ { name: "Legacy regex guard", color: "#9aa0a6", values: [legacyEval.detectionRate, legacyEval.falsePositiveRate] },
200
+ { name: "Goal Mode analyzer", color: "#2da44e", values: [currentEval.detectionRate, currentEval.falsePositiveRate] },
201
+ ],
202
+ }),
203
+ );
204
+
205
+ // Chart 3: per-command latency — the deeper analysis costs a few microseconds,
206
+ // which is negligible for a tool-call guard. Shown for honesty, not as a "win".
207
+ writeFileSync(
208
+ join(outDir, "latency.svg"),
209
+ horizontalBarChart({
210
+ title: "Per-command analysis latency",
211
+ subtitle: "Microseconds to classify one command. Both are negligible for a tool-call guard.",
212
+ unit: " µs",
213
+ max: Math.max(legacyUs, currentUs) * 1.4,
214
+ rows: [
215
+ { label: "Legacy regex guard", value: legacyUs, display: `${legacyUs.toFixed(2)} µs`, color: "#9aa0a6" },
216
+ { label: "Goal Mode analyzer", value: currentUs, display: `${currentUs.toFixed(2)} µs`, color: "#2da44e" },
217
+ ],
218
+ }),
219
+ );
220
+
221
+ writeFileSync(
222
+ join(outDir, "truthfulness-score.svg"),
223
+ horizontalBarChart({
224
+ title: "Completion-enforcement fixtures",
225
+ subtitle: `${truthfulness.corpusSize} hand-authored policy cases (a spec, not a survey): premature claims blocked, valid ones allowed.`,
226
+ unit: "%",
227
+ max: 100,
228
+ rows: [
229
+ { label: "Truthfulness score", value: truthfulness.score, display: `${truthfulness.score.toFixed(1)}%`, color: "#2da44e" },
230
+ { label: "Decision accuracy", value: truthfulness.decisionAccuracy, display: `${truthfulness.decisionAccuracy.toFixed(1)}%`, color: "#0969da" },
231
+ { label: "Reason accuracy", value: truthfulness.reasonAccuracy, display: `${truthfulness.reasonAccuracy.toFixed(1)}%`, color: "#bf8700" },
232
+ ],
233
+ }),
234
+ );
235
+
236
+ const pct = (n) => `${n.toFixed(1)}%`;
237
+ console.log("Goal Mode shell-guard benchmark");
238
+ console.log("================================");
239
+ console.log("");
240
+ console.log(`HEADLINE — external corpus: ${external.sampleSize} real tldr-pages commands @ ${external.commit.slice(0, 12)}`);
241
+ console.log(` (${external.totals.destructiveFound} destructive [all found] + ${external.totals.safeSampled}/${external.totals.safeFound} safe sampled)`);
242
+ console.log(` Detection legacy ${pct(external.legacy.detectionRate)} → Goal Mode ${pct(external.current.detectionRate)}`);
243
+ console.log(` False positives legacy ${pct(external.legacy.falsePositiveRate)} → Goal Mode ${pct(external.current.falsePositiveRate)}`);
244
+ console.log(` Remaining Goal Mode misses: ${external.current.misses.length} (mostly un-flagged single-target rm — see external.mjs --json)`);
245
+ console.log("");
246
+ console.log(`Curated regression fixtures: ${results.fixtures.corpusSize} commands (defines patterns to catch; not an unbiased sample)`);
247
+ console.log(` Detection legacy ${pct(legacyEval.detectionRate)} → Goal Mode ${pct(currentEval.detectionRate)} (passes its own spec)`);
248
+ console.log(` False pos legacy ${pct(legacyEval.falsePositiveRate)} → Goal Mode ${pct(currentEval.falsePositiveRate)}`);
249
+ console.log(`Completion-enforcement fixtures: ${truthfulness.corpusSize} hand-authored policy cases, all pass (a spec, not a survey)`);
250
+ console.log(`Latency: Goal Mode ${currentUs.toFixed(2)} µs/cmd (${fmt(currentOps)}/s)`);
251
+ console.log("");
252
+ console.log(`Wrote results.json + 5 SVG charts to docs/benchmarks/`);
@@ -0,0 +1,64 @@
1
+ import { BASE_GATES } from "../plugins/goal-guard/agents.js";
2
+ import { DEFAULT_CONFIG } from "../plugins/goal-guard/config.js";
3
+ import { evaluateCompletionClaim } from "../plugins/goal-guard/completion.js";
4
+ import { refreshStickyGates } from "../plugins/goal-guard/gates.js";
5
+ import { createState } from "../plugins/goal-guard/state.js";
6
+ import { FALSE_COMPLETION_CORPUS } from "./completion-corpus.mjs";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ function buildState(def) {
10
+ const state = createState("2026-01-01T00:00:00.000Z");
11
+ Object.assign(state, def.state || {});
12
+ state.latestVerdict = {};
13
+ for (const gate of def.state?.verdicts || []) {
14
+ state.latestVerdict[gate.agent] = { verdict: gate.verdict, at: "2026-01-01T00:00:01.000Z", seq: gate.seq };
15
+ }
16
+ if (!state.goalText && def.state?.contextualGate) state.goalText = def.state.contextualGate;
17
+ refreshStickyGates(state);
18
+ return state;
19
+ }
20
+
21
+ export function evaluateTruthfulnessCase(def) {
22
+ const config = { ...DEFAULT_CONFIG, ...(def.config || {}) };
23
+ const decision = evaluateCompletionClaim(buildState(def), config, def.text);
24
+ const reason = decision.reason || "";
25
+ const decisionCorrect = Boolean(decision.blocked) === Boolean(def.expected.blocked);
26
+ const reasonCorrect = !def.expected.blocked || reason.includes(def.expected.reasonIncludes || "");
27
+ return {
28
+ id: def.id,
29
+ family: def.family,
30
+ expectedBlocked: Boolean(def.expected.blocked),
31
+ actualBlocked: Boolean(decision.blocked),
32
+ decisionCorrect,
33
+ reasonCorrect,
34
+ reason,
35
+ };
36
+ }
37
+
38
+ export function runTruthfulnessBenchmark(corpus = FALSE_COMPLETION_CORPUS) {
39
+ const cases = corpus.map(evaluateTruthfulnessCase);
40
+ const falseCompletionCases = cases.filter((c) => c.expectedBlocked);
41
+ const trueCompletionCases = cases.filter((c) => !c.expectedBlocked);
42
+ const decisionCorrect = cases.filter((c) => c.decisionCorrect).length;
43
+ const reasonCorrect = falseCompletionCases.filter((c) => c.reasonCorrect).length;
44
+ const falseCompletionBlocked = falseCompletionCases.filter((c) => c.actualBlocked).length;
45
+ const trueCompletionAllowed = trueCompletionCases.filter((c) => !c.actualBlocked).length;
46
+ const decisionAccuracy = cases.length ? (decisionCorrect / cases.length) * 100 : 0;
47
+ const reasonAccuracy = falseCompletionCases.length ? (reasonCorrect / falseCompletionCases.length) * 100 : 100;
48
+ return {
49
+ name: "False Completion Dataset",
50
+ corpusSize: cases.length,
51
+ requiredBaseGates: BASE_GATES,
52
+ score: Number(((decisionAccuracy * 0.65 + reasonAccuracy * 0.35)).toFixed(1)),
53
+ decisionAccuracy: Number(decisionAccuracy.toFixed(1)),
54
+ reasonAccuracy: Number(reasonAccuracy.toFixed(1)),
55
+ falseCompletionBlockRate: Number(((falseCompletionBlocked / falseCompletionCases.length) * 100).toFixed(1)),
56
+ validCompletionAllowRate: Number(((trueCompletionAllowed / trueCompletionCases.length) * 100).toFixed(1)),
57
+ cases,
58
+ };
59
+ }
60
+
61
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
62
+ const result = runTruthfulnessBenchmark();
63
+ console.log(JSON.stringify(result, null, 2));
64
+ }
@@ -0,0 +1,27 @@
1
+ ---
2
+ description: Map Goal Contract acceptance criteria to recorded verification evidence and gaps.
3
+ agent: goal
4
+ ---
5
+
6
+ Produce a read-only evidence map for the current Goal Mode session. Do not edit files.
7
+
8
+ Call `goal_evidence_map` first and use its authoritative Goal Guard state,
9
+ including the Goal Contract, recorded evidence, dirty state, reviewer status, and
10
+ any user-provided context. Report unknown or missing details honestly instead of
11
+ inferring evidence that is not recorded.
12
+
13
+ Include:
14
+
15
+ - Acceptance criterion
16
+ - Recorded evidence covering it
17
+ - Reviewer status
18
+ - Verification command/result summary
19
+ - Status: covered, partially covered, missing, or stale
20
+ - Gap or risk
21
+ - Next required action
22
+
23
+ Additional context:
24
+
25
+ ```text
26
+ $ARGUMENTS
27
+ ```
package/commands/goal.md CHANGED
@@ -9,4 +9,19 @@ Start Goal Mode for this request:
9
9
  $ARGUMENTS
10
10
  ```
11
11
 
12
- First create a Goal Contract, ask only essential beginning clarifying questions, delegate discovery/research to subagents, implement in the main agent, verify, run required review cycles, and only finish with `Goal Completed` if all gates pass.
12
+ Run this sequence:
13
+
14
+ 1. **Seed the contract first.** Call the `goal_contract` tool with the original
15
+ request, explicit/inferred requirements, non-goals, and concrete acceptance
16
+ criteria. This activates enforcement, fixes the required specialist review
17
+ gates, and lights up the goal banner in the sidebar. Ask only essential
18
+ clarifying questions before recording it.
19
+ 2. Delegate discovery and research to subagents; implement in the main agent.
20
+ 3. Verify, and record each verification with the `goal_evidence` tool so it maps
21
+ to your acceptance criteria.
22
+ 4. Run the required review cycles. Consult `goal_status` / `goal_evidence_map`
23
+ for the authoritative list of missing or stale gates rather than relying on
24
+ memory.
25
+ 5. Only finish with `Goal Completed` (plus an accurate `Review cycles: N` line)
26
+ once every required gate has a fresh PASS — the guard will rewrite a premature
27
+ claim to `Goal Not Completed`.
@@ -1,7 +1,7 @@
1
1
  <svg xmlns="http://www.w3.org/2000/svg" width="720" height="380" viewBox="0 0 720 380" font-family="-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif">
2
2
  <rect width="720" height="380" fill="#ffffff"/>
3
- <text x="48" y="28" font-size="17" font-weight="700" fill="#1f2328">Destructive-command detection rate by family</text>
4
- <text x="48" y="47" font-size="12" fill="#656d76">Higher is better. Corpus: 48 destructive commands.</text>
3
+ <text x="48" y="28" font-size="17" font-weight="700" fill="#1f2328">Detection by family curated regression fixtures</text>
4
+ <text x="48" y="47" font-size="12" fill="#656d76">Curated patterns the analyzer is built to catch (not an unbiased sample). 48 destructive fixtures.</text>
5
5
  <line x1="48" y1="296.0" x2="700" y2="296.0" stroke="#eaeef2" stroke-width="1"/>
6
6
  <text x="40" y="300.0" font-size="11" text-anchor="end" fill="#656d76">0%</text>
7
7
  <line x1="48" y1="249.6" x2="700" y2="249.6" stroke="#eaeef2" stroke-width="1"/>
@@ -0,0 +1,32 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="720" height="380" viewBox="0 0 720 380" font-family="-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif">
2
+ <rect width="720" height="380" fill="#ffffff"/>
3
+ <text x="48" y="28" font-size="17" font-weight="700" fill="#1f2328">Guard accuracy on real third-party commands</text>
4
+ <text x="48" y="47" font-size="12" fill="#656d76">704 tldr-pages commands the analyzer was never fitted to. Detection higher = better; false positives lower = better.</text>
5
+ <line x1="48" y1="296.0" x2="700" y2="296.0" stroke="#eaeef2" stroke-width="1"/>
6
+ <text x="40" y="300.0" font-size="11" text-anchor="end" fill="#656d76">0%</text>
7
+ <line x1="48" y1="249.6" x2="700" y2="249.6" stroke="#eaeef2" stroke-width="1"/>
8
+ <text x="40" y="253.6" font-size="11" text-anchor="end" fill="#656d76">20%</text>
9
+ <line x1="48" y1="203.2" x2="700" y2="203.2" stroke="#eaeef2" stroke-width="1"/>
10
+ <text x="40" y="207.2" font-size="11" text-anchor="end" fill="#656d76">40%</text>
11
+ <line x1="48" y1="156.8" x2="700" y2="156.8" stroke="#eaeef2" stroke-width="1"/>
12
+ <text x="40" y="160.8" font-size="11" text-anchor="end" fill="#656d76">60%</text>
13
+ <line x1="48" y1="110.4" x2="700" y2="110.4" stroke="#eaeef2" stroke-width="1"/>
14
+ <text x="40" y="114.4" font-size="11" text-anchor="end" fill="#656d76">80%</text>
15
+ <line x1="48" y1="64.0" x2="700" y2="64.0" stroke="#eaeef2" stroke-width="1"/>
16
+ <text x="40" y="68.0" font-size="11" text-anchor="end" fill="#656d76">100%</text>
17
+ <rect x="56.0" y="171.1" width="151.0" height="124.9" rx="3" fill="#9aa0a6"/>
18
+ <text x="131.5" y="166.1" font-size="11" font-weight="600" text-anchor="middle" fill="#1f2328">54%</text>
19
+ <rect x="215.0" y="79.6" width="151.0" height="216.4" rx="3" fill="#2da44e"/>
20
+ <text x="290.5" y="74.6" font-size="11" font-weight="600" text-anchor="middle" fill="#1f2328">93%</text>
21
+ <text x="211.0" y="314.0" font-size="11" text-anchor="middle" fill="#1f2328">Detection rate</text>
22
+ <rect x="382.0" y="295.6" width="151.0" height="0.4" rx="3" fill="#9aa0a6"/>
23
+ <text x="457.5" y="290.6" font-size="11" font-weight="600" text-anchor="middle" fill="#1f2328">0%</text>
24
+ <rect x="541.0" y="295.6" width="151.0" height="0.4" rx="3" fill="#2da44e"/>
25
+ <text x="616.5" y="290.6" font-size="11" font-weight="600" text-anchor="middle" fill="#1f2328">0%</text>
26
+ <text x="537.0" y="314.0" font-size="11" text-anchor="middle" fill="#1f2328">False-positive rate</text>
27
+ <line x1="48" y1="296" x2="700" y2="296" stroke="#d0d7de" stroke-width="1.5"/>
28
+ <rect x="48" y="344" width="12" height="12" rx="2" fill="#9aa0a6"/>
29
+ <text x="66" y="354" font-size="12" fill="#1f2328">Legacy regex guard</text>
30
+ <rect x="201.6" y="344" width="12" height="12" rx="2" fill="#2da44e"/>
31
+ <text x="219.6" y="354" font-size="12" fill="#1f2328">Goal Mode analyzer</text>
32
+ </svg>
@@ -4,10 +4,10 @@
4
4
  <text x="20" y="47" font-size="12" fill="#656d76">Microseconds to classify one command. Both are negligible for a tool-call guard.</text>
5
5
  <text x="218" y="87" font-size="12" text-anchor="end" fill="#1f2328">Legacy regex guard</text>
6
6
  <rect x="230" y="70" width="420" height="22" rx="3" fill="#eaeef2"/>
7
- <rect x="230" y="70" width="202.0" height="22" rx="3" fill="#9aa0a6"/>
8
- <text x="440.0" y="87" font-size="12" font-weight="600" fill="#1f2328">2.62 µs</text>
7
+ <rect x="230" y="70" width="214.5" height="22" rx="3" fill="#9aa0a6"/>
8
+ <text x="452.5" y="87" font-size="12" font-weight="600" fill="#1f2328">0.79 µs</text>
9
9
  <text x="218" y="125" font-size="12" text-anchor="end" fill="#1f2328">Goal Mode analyzer</text>
10
10
  <rect x="230" y="108" width="420" height="22" rx="3" fill="#eaeef2"/>
11
11
  <rect x="230" y="108" width="300.0" height="22" rx="3" fill="#2da44e"/>
12
- <text x="538.0" y="125" font-size="12" font-weight="600" fill="#1f2328">3.89 µs</text>
12
+ <text x="538.0" y="125" font-size="12" font-weight="600" fill="#1f2328">1.11 µs</text>
13
13
  </svg>
@@ -1,7 +1,7 @@
1
1
  <svg xmlns="http://www.w3.org/2000/svg" width="720" height="380" viewBox="0 0 720 380" font-family="-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif">
2
2
  <rect width="720" height="380" fill="#ffffff"/>
3
- <text x="48" y="28" font-size="17" font-weight="700" fill="#1f2328">Overall guard accuracy</text>
4
- <text x="48" y="47" font-size="12" fill="#656d76">Detection rate (higher better) vs false-positive rate (lower better).</text>
3
+ <text x="48" y="28" font-size="17" font-weight="700" fill="#1f2328">Curated fixtures — passes its own spec</text>
4
+ <text x="48" y="47" font-size="12" fill="#656d76">Curated regression fixtures, not measured accuracy. See external-scorecard.svg for the real-world number.</text>
5
5
  <line x1="48" y1="296.0" x2="700" y2="296.0" stroke="#eaeef2" stroke-width="1"/>
6
6
  <text x="40" y="300.0" font-size="11" text-anchor="end" fill="#656d76">0%</text>
7
7
  <line x1="48" y1="249.6" x2="700" y2="249.6" stroke="#eaeef2" stroke-width="1"/>