balladeer 1.0.15 → 1.0.17

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.
Files changed (77) hide show
  1. package/README.md +2 -0
  2. package/dist/behavior-map-schema.d.ts +107 -0
  3. package/dist/behavior-map-schema.js +232 -0
  4. package/dist/cli.d.ts +6 -1
  5. package/dist/cli.js +74 -6
  6. package/dist/commands/guidance.d.ts +2 -1
  7. package/dist/commands/guidance.js +55 -0
  8. package/dist/commands/judge.d.ts +430 -0
  9. package/dist/commands/judge.js +1713 -0
  10. package/dist/commands/map.d.ts +164 -0
  11. package/dist/commands/map.js +838 -0
  12. package/dist/commands/mcp.js +2 -2
  13. package/dist/commands/offers.d.ts +265 -0
  14. package/dist/commands/offers.js +801 -0
  15. package/dist/commands/risk.d.ts +29 -0
  16. package/dist/commands/risk.js +133 -0
  17. package/dist/copy.d.ts +24 -1
  18. package/dist/copy.js +91 -0
  19. package/dist/guidance-hook.mjs +348 -94
  20. package/dist/headless-agent.d.ts +166 -0
  21. package/dist/headless-agent.js +416 -0
  22. package/dist/hook-trust.d.ts +41 -9
  23. package/dist/hook-trust.js +98 -16
  24. package/dist/judge-brief.d.ts +48 -0
  25. package/dist/judge-brief.js +102 -0
  26. package/dist/judge-hook.d.ts +258 -0
  27. package/dist/judge-hook.js +1278 -0
  28. package/dist/judge-said.d.ts +52 -0
  29. package/dist/judge-said.js +181 -0
  30. package/dist/map-brief.d.ts +16 -0
  31. package/dist/map-brief.js +41 -0
  32. package/dist/offer-brief.d.ts +95 -0
  33. package/dist/offer-brief.js +217 -0
  34. package/dist/owned-process.d.ts +65 -0
  35. package/dist/owned-process.js +146 -0
  36. package/dist/promise-meaning.d.ts +134 -0
  37. package/dist/promise-meaning.js +300 -0
  38. package/dist/relay.d.ts +71 -0
  39. package/dist/relay.js +193 -0
  40. package/dist/remove-earlier.js +4 -2
  41. package/dist/risk/contract.d.ts +145 -0
  42. package/dist/risk/contract.js +74 -0
  43. package/dist/risk/describe.d.ts +7 -0
  44. package/dist/risk/describe.js +45 -0
  45. package/dist/risk/diff.d.ts +26 -0
  46. package/dist/risk/diff.js +174 -0
  47. package/dist/risk/extract.d.ts +43 -0
  48. package/dist/risk/extract.js +336 -0
  49. package/dist/risk/git.d.ts +30 -0
  50. package/dist/risk/git.js +118 -0
  51. package/dist/risk/import-graph.d.ts +41 -0
  52. package/dist/risk/import-graph.js +487 -0
  53. package/dist/risk/index.d.ts +20 -0
  54. package/dist/risk/index.js +20 -0
  55. package/dist/risk/paths.d.ts +16 -0
  56. package/dist/risk/paths.js +73 -0
  57. package/dist/risk/pipeline.d.ts +47 -0
  58. package/dist/risk/pipeline.js +121 -0
  59. package/dist/risk/priors.d.ts +18 -0
  60. package/dist/risk/priors.js +86 -0
  61. package/dist/risk/resources.d.ts +56 -0
  62. package/dist/risk/resources.js +374 -0
  63. package/dist/risk/score.d.ts +95 -0
  64. package/dist/risk/score.js +552 -0
  65. package/dist/risk/symbols.d.ts +35 -0
  66. package/dist/risk/symbols.js +348 -0
  67. package/dist/risk/text.d.ts +43 -0
  68. package/dist/risk/text.js +277 -0
  69. package/dist/risk/validate.d.ts +19 -0
  70. package/dist/risk/validate.js +154 -0
  71. package/dist/scratch-worktree.d.ts +55 -0
  72. package/dist/scratch-worktree.js +160 -0
  73. package/dist/user-scope.d.ts +74 -3
  74. package/dist/user-scope.js +270 -9
  75. package/dist/wire.d.ts +52 -3
  76. package/dist/wire.js +2 -2
  77. package/package.json +1 -1
@@ -16,9 +16,9 @@ var __commonJS = (cb, mod) => function __require2() {
16
16
  };
17
17
  var __copyProps = (to, from, except, desc) => {
18
18
  if (from && typeof from === "object" || typeof from === "function") {
19
- for (let key of __getOwnPropNames(from))
20
- if (!__hasOwnProp.call(to, key) && key !== except)
21
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
19
+ for (let key2 of __getOwnPropNames(from))
20
+ if (!__hasOwnProp.call(to, key2) && key2 !== except)
21
+ __defProp(to, key2, { get: () => from[key2], enumerable: !(desc = __getOwnPropDesc(from, key2)) || desc.enumerable });
22
22
  }
23
23
  return to;
24
24
  };
@@ -366,9 +366,9 @@ var require_toml_parser = __commonJS({
366
366
  var hasOwnProperty = Object.prototype.hasOwnProperty;
367
367
  var defineProperty = Object.defineProperty;
368
368
  var descriptor = { configurable: true, enumerable: true, writable: true, value: void 0 };
369
- function hasKey(obj, key) {
370
- if (hasOwnProperty.call(obj, key)) return true;
371
- if (key === "__proto__") defineProperty(obj, "__proto__", descriptor);
369
+ function hasKey(obj, key2) {
370
+ if (hasOwnProperty.call(obj, key2)) return true;
371
+ if (key2 === "__proto__") defineProperty(obj, "__proto__", descriptor);
372
372
  return false;
373
373
  }
374
374
  var INLINE_TABLE = Symbol("inline-table");
@@ -570,11 +570,11 @@ var require_toml_parser = __commonJS({
570
570
  parseAssign() {
571
571
  return this.callNow(this.parseKeyword, this.recordAssignKeyword);
572
572
  }
573
- recordAssignKeyword(key) {
573
+ recordAssignKeyword(key2) {
574
574
  if (this.state.resultTable) {
575
- this.state.resultTable.push(key);
575
+ this.state.resultTable.push(key2);
576
576
  } else {
577
- this.state.resultTable = [key];
577
+ this.state.resultTable = [key2];
578
578
  }
579
579
  return this.goto(this.parseAssignKeywordPreDot);
580
580
  }
@@ -1779,10 +1779,10 @@ var require_stringify = __commonJS({
1779
1779
  return new Error("Array values can't have mixed types");
1780
1780
  }
1781
1781
  function getInlineKeys(obj) {
1782
- return Object.keys(obj).filter((key) => isInline(obj[key]));
1782
+ return Object.keys(obj).filter((key2) => isInline(obj[key2]));
1783
1783
  }
1784
1784
  function getComplexKeys(obj) {
1785
- return Object.keys(obj).filter((key) => !isInline(obj[key]));
1785
+ return Object.keys(obj).filter((key2) => !isInline(obj[key2]));
1786
1786
  }
1787
1787
  function toJSON(obj) {
1788
1788
  let nobj = Array.isArray(obj) ? [] : Object.prototype.hasOwnProperty.call(obj, "__proto__") ? { ["__proto__"]: void 0 } : {};
@@ -1803,16 +1803,16 @@ var require_stringify = __commonJS({
1803
1803
  complexKeys = getComplexKeys(obj);
1804
1804
  var result = [];
1805
1805
  var inlineIndent = indent || "";
1806
- inlineKeys.forEach((key) => {
1807
- var type = tomlType2(obj[key]);
1806
+ inlineKeys.forEach((key2) => {
1807
+ var type = tomlType2(obj[key2]);
1808
1808
  if (type !== "undefined" && type !== "null") {
1809
- result.push(inlineIndent + stringifyKey(key) + " = " + stringifyAnyInline(obj[key], true));
1809
+ result.push(inlineIndent + stringifyKey(key2) + " = " + stringifyAnyInline(obj[key2], true));
1810
1810
  }
1811
1811
  });
1812
1812
  if (result.length > 0) result.push("");
1813
1813
  var complexIndent = prefix && inlineKeys.length > 0 ? indent + " " : "";
1814
- complexKeys.forEach((key) => {
1815
- result.push(stringifyComplex(prefix, complexIndent, key, obj[key]));
1814
+ complexKeys.forEach((key2) => {
1815
+ result.push(stringifyComplex(prefix, complexIndent, key2, obj[key2]));
1816
1816
  });
1817
1817
  return result.join("\n");
1818
1818
  }
@@ -1857,8 +1857,8 @@ var require_stringify = __commonJS({
1857
1857
  return "table";
1858
1858
  }
1859
1859
  }
1860
- function stringifyKey(key) {
1861
- var keyStr = String(key);
1860
+ function stringifyKey(key2) {
1861
+ var keyStr = String(key2);
1862
1862
  if (/^[-A-Za-z0-9_]+$/.test(keyStr)) {
1863
1863
  return keyStr;
1864
1864
  } else {
@@ -1977,27 +1977,27 @@ var require_stringify = __commonJS({
1977
1977
  function stringifyInlineTable(value) {
1978
1978
  value = toJSON(value);
1979
1979
  var result = [];
1980
- Object.keys(value).forEach((key) => {
1981
- result.push(stringifyKey(key) + " = " + stringifyAnyInline(value[key], false));
1980
+ Object.keys(value).forEach((key2) => {
1981
+ result.push(stringifyKey(key2) + " = " + stringifyAnyInline(value[key2], false));
1982
1982
  });
1983
1983
  return "{ " + result.join(", ") + (result.length > 0 ? " " : "") + "}";
1984
1984
  }
1985
- function stringifyComplex(prefix, indent, key, value) {
1985
+ function stringifyComplex(prefix, indent, key2, value) {
1986
1986
  var valueType = tomlType2(value);
1987
1987
  if (valueType === "array") {
1988
- return stringifyArrayOfTables(prefix, indent, key, value);
1988
+ return stringifyArrayOfTables(prefix, indent, key2, value);
1989
1989
  } else if (valueType === "table") {
1990
- return stringifyComplexTable(prefix, indent, key, value);
1990
+ return stringifyComplexTable(prefix, indent, key2, value);
1991
1991
  } else {
1992
1992
  throw typeError(valueType);
1993
1993
  }
1994
1994
  }
1995
- function stringifyArrayOfTables(prefix, indent, key, values) {
1995
+ function stringifyArrayOfTables(prefix, indent, key2, values) {
1996
1996
  values = toJSON(values);
1997
1997
  validateArray(values);
1998
1998
  var firstValueType = tomlType2(values[0]);
1999
1999
  if (firstValueType !== "table") throw typeError(firstValueType);
2000
- var fullKey = prefix + stringifyKey(key);
2000
+ var fullKey = prefix + stringifyKey(key2);
2001
2001
  var result = "";
2002
2002
  values.forEach((table) => {
2003
2003
  if (result.length > 0) result += "\n";
@@ -2006,8 +2006,8 @@ var require_stringify = __commonJS({
2006
2006
  });
2007
2007
  return result;
2008
2008
  }
2009
- function stringifyComplexTable(prefix, indent, key, value) {
2010
- var fullKey = prefix + stringifyKey(key);
2009
+ function stringifyComplexTable(prefix, indent, key2, value) {
2010
+ var fullKey = prefix + stringifyKey(key2);
2011
2011
  var result = "";
2012
2012
  if (getInlineKeys(value).length > 0) {
2013
2013
  result += indent + "[" + fullKey + "]\n";
@@ -2026,8 +2026,133 @@ var require_toml = __commonJS({
2026
2026
  }
2027
2027
  });
2028
2028
 
2029
+ // src/relay.ts
2030
+ import { createHash } from "node:crypto";
2031
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2032
+ import { join } from "node:path";
2033
+ var RELAY_FIELD = "forThePerson";
2034
+ var MAX_SENTENCE = 400;
2035
+ var MAX_SUBJECT = 200;
2036
+ var MAX_PENDING = 8;
2037
+ function clean(value, limit) {
2038
+ if (typeof value !== "string") return void 0;
2039
+ const text = value.replace(/[\u0000-\u001f\u007f]/g, " ").trim();
2040
+ return text ? text.slice(0, limit) : void 0;
2041
+ }
2042
+ function noticeFrom(value) {
2043
+ if (!value || typeof value !== "object") return void 0;
2044
+ const record = value;
2045
+ const sentence = clean(record.sentence, MAX_SENTENCE);
2046
+ if (!sentence) return void 0;
2047
+ const subjects = (Array.isArray(record.subjects) ? record.subjects : []).map((s) => clean(s, MAX_SUBJECT)).filter((s) => s !== void 0).slice(0, 4);
2048
+ return { sentence, subjects };
2049
+ }
2050
+ function noticeFromToolResponse(response) {
2051
+ const visit = (value, depth) => {
2052
+ if (depth > 4 || value === null || value === void 0) return void 0;
2053
+ if (typeof value === "string") {
2054
+ if (!value.includes(RELAY_FIELD)) return void 0;
2055
+ try {
2056
+ return visit(JSON.parse(value), depth + 1);
2057
+ } catch {
2058
+ return void 0;
2059
+ }
2060
+ }
2061
+ if (Array.isArray(value)) {
2062
+ for (const item of value) {
2063
+ const found = visit(item, depth + 1);
2064
+ if (found) return found;
2065
+ }
2066
+ return void 0;
2067
+ }
2068
+ if (typeof value !== "object") return void 0;
2069
+ const record = value;
2070
+ if (RELAY_FIELD in record) return noticeFrom(record[RELAY_FIELD]);
2071
+ for (const key2 of ["structuredContent", "content", "text", "result", "tool_response"]) {
2072
+ const found = visit(record[key2], depth + 1);
2073
+ if (found) return found;
2074
+ }
2075
+ return void 0;
2076
+ };
2077
+ return visit(response, 0);
2078
+ }
2079
+ function relayNote(notice2) {
2080
+ return `Balladeer just acted on the person's behalf: ${notice2.sentence} Make sure your reply tells them this, in your own words and in about one line.`;
2081
+ }
2082
+ var WORD = /[a-z0-9]+/g;
2083
+ var QUIET = new Set(
2084
+ "a an the and or of to in on for with is are be by as at from that this it its not no".split(" ")
2085
+ );
2086
+ function words(text) {
2087
+ return (text.toLowerCase().match(WORD) ?? []).filter((w) => w.length > 2 && !QUIET.has(w));
2088
+ }
2089
+ function mentions(reply, subject) {
2090
+ const wanted = [...new Set(words(subject))];
2091
+ if (!wanted.length) return true;
2092
+ const said = new Set(words(reply));
2093
+ const found = wanted.filter((w) => said.has(w) || said.has(w.replace(/s$/, ""))).length;
2094
+ return found / wanted.length >= 0.5;
2095
+ }
2096
+ function relayed(reply, notice2) {
2097
+ return notice2.subjects.length === 0 || notice2.subjects.some((s) => mentions(reply, s));
2098
+ }
2099
+ function reminder(notices) {
2100
+ const told = notices.map((n) => n.sentence).join(" ");
2101
+ return `Your reply does not yet tell the person what Balladeer did: ${told} Add it in your own words, in about one line.`;
2102
+ }
2103
+ function pendingPath(directory, sessionId) {
2104
+ return join(directory, `${createHash("sha256").update(sessionId).digest("hex")}.relay.json`);
2105
+ }
2106
+ function readPending(directory, sessionId) {
2107
+ try {
2108
+ const parsed = JSON.parse(readFileSync(pendingPath(directory, sessionId), "utf8"));
2109
+ return parsed.schemaVersion === 1 && Array.isArray(parsed.notices) ? parsed.notices.map(noticeFrom).filter((n) => n !== void 0) : [];
2110
+ } catch {
2111
+ return [];
2112
+ }
2113
+ }
2114
+ function writePending(directory, sessionId, notices) {
2115
+ mkdirSync(directory, { recursive: true, mode: 448 });
2116
+ const path = pendingPath(directory, sessionId);
2117
+ const temporary = `${path}.${process.pid}.tmp`;
2118
+ const body = { schemaVersion: 1, notices: notices.slice(-MAX_PENDING) };
2119
+ writeFileSync(temporary, JSON.stringify(body), { mode: 384 });
2120
+ renameSync(temporary, path);
2121
+ }
2122
+ function isBalladeerTool(name) {
2123
+ return typeof name === "string" && /^mcp__balladeer[a-z0-9_-]*__/i.test(name);
2124
+ }
2125
+ function relayHook(input, directory) {
2126
+ try {
2127
+ const sessionId = typeof input.session_id === "string" ? input.session_id : void 0;
2128
+ if (input.hook_event_name === "PostToolUse") {
2129
+ if (!isBalladeerTool(input.tool_name)) return void 0;
2130
+ const notice2 = noticeFromToolResponse(input.tool_response ?? input.tool_output);
2131
+ if (!notice2) return void 0;
2132
+ if (sessionId && directory)
2133
+ writePending(directory, sessionId, [...readPending(directory, sessionId), notice2]);
2134
+ return JSON.stringify({
2135
+ hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: relayNote(notice2) }
2136
+ });
2137
+ }
2138
+ if (input.hook_event_name === "Stop") {
2139
+ if (!sessionId || !directory) return void 0;
2140
+ const pending = readPending(directory, sessionId);
2141
+ if (!pending.length) return void 0;
2142
+ const reply = typeof input.last_assistant_message === "string" ? input.last_assistant_message : "";
2143
+ const missing = pending.filter((n) => !relayed(reply, n));
2144
+ writePending(directory, sessionId, []);
2145
+ if (!missing.length || input.stop_hook_active === true) return void 0;
2146
+ return JSON.stringify({ decision: "block", reason: reminder(missing) });
2147
+ }
2148
+ return void 0;
2149
+ } catch {
2150
+ return void 0;
2151
+ }
2152
+ }
2153
+
2029
2154
  // src/wire.ts
2030
- var CLI_VERSION = "1.0.15";
2155
+ var CLI_VERSION = "1.0.17";
2031
2156
  var CLIENT_HEADER = "x-balladeer-client";
2032
2157
  var UPDATE_STATE_HEADER = "x-balladeer-update-state";
2033
2158
  var HOOK_HOST_HEADER = "x-balladeer-hook";
@@ -2040,15 +2165,15 @@ import {
2040
2165
  fchmodSync,
2041
2166
  fsyncSync,
2042
2167
  lstatSync,
2043
- mkdirSync,
2168
+ mkdirSync as mkdirSync2,
2044
2169
  openSync,
2045
- readFileSync,
2046
- renameSync,
2170
+ readFileSync as readFileSync2,
2171
+ renameSync as renameSync2,
2047
2172
  unlinkSync,
2048
2173
  writeSync
2049
2174
  } from "node:fs";
2050
2175
  import { homedir } from "node:os";
2051
- import { dirname, join, parse, resolve } from "node:path";
2176
+ import { dirname, join as join2, parse, resolve } from "node:path";
2052
2177
  var StoreError = class extends Error {
2053
2178
  code;
2054
2179
  constructor(code, message, options) {
@@ -2062,11 +2187,11 @@ function configHome(environment = process.env) {
2062
2187
  const explicit = environment.BALLADEER_CONFIG_HOME?.trim();
2063
2188
  if (explicit) return resolve(explicit);
2064
2189
  const xdg = environment.XDG_CONFIG_HOME?.trim();
2065
- if (xdg) return resolve(join(xdg, "balladeer"));
2066
- return resolve(join(homedir(), ".config", "balladeer"));
2190
+ if (xdg) return resolve(join2(xdg, "balladeer"));
2191
+ return resolve(join2(homedir(), ".config", "balladeer"));
2067
2192
  }
2068
2193
  function credentialsPath(environment = process.env) {
2069
- return join(configHome(environment), "credentials.json");
2194
+ return join2(configHome(environment), "credentials.json");
2070
2195
  }
2071
2196
  function assertPrivate(path, kind) {
2072
2197
  const stats = lstatSync(path);
@@ -2098,7 +2223,7 @@ function readCredentials(environment = process.env) {
2098
2223
  let raw;
2099
2224
  try {
2100
2225
  assertPrivate(path, "file");
2101
- raw = readFileSync(path, "utf8");
2226
+ raw = readFileSync2(path, "utf8");
2102
2227
  } catch (error) {
2103
2228
  if (error?.code === "ENOENT") {
2104
2229
  return { ...EMPTY, pendingPairings: [], sessions: [], agents: [] };
@@ -2154,8 +2279,8 @@ function newerVersionPublished() {
2154
2279
 
2155
2280
  // src/self-update.ts
2156
2281
  import { spawn } from "node:child_process";
2157
- import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "node:fs";
2158
- import { delimiter, dirname as dirname2, join as join2 } from "node:path";
2282
+ import { existsSync, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
2283
+ import { delimiter, dirname as dirname2, join as join3 } from "node:path";
2159
2284
 
2160
2285
  // src/release.ts
2161
2286
  var HOOK_SPECIFIER = "balladeer@1";
@@ -2167,24 +2292,24 @@ var DAY_MS = 24 * 60 * 60 * 1e3;
2167
2292
  var SELF_UPDATE_ENVIRONMENT = "BALLADEER_SELF_UPDATE";
2168
2293
  var STATE_PATTERN = /^(?:started|unstartable|installed|failed):[A-Za-z0-9._-]{1,40}$/;
2169
2294
  function stampPath(environment) {
2170
- return join2(configHome(environment), STAMP);
2295
+ return join3(configHome(environment), STAMP);
2171
2296
  }
2172
2297
  function statePath(environment) {
2173
- return join2(configHome(environment), STATE);
2298
+ return join3(configHome(environment), STATE);
2174
2299
  }
2175
2300
  function lastStarted(environment) {
2176
2301
  const path = stampPath(environment);
2177
2302
  if (!existsSync(path)) return 0;
2178
2303
  try {
2179
- const parsed = JSON.parse(readFileSync2(path, "utf8"));
2304
+ const parsed = JSON.parse(readFileSync3(path, "utf8"));
2180
2305
  return typeof parsed.startedAt === "number" ? parsed.startedAt : 0;
2181
2306
  } catch {
2182
2307
  return 0;
2183
2308
  }
2184
2309
  }
2185
2310
  function writePrivate(path, value, environment) {
2186
- mkdirSync2(configHome(environment), { recursive: true, mode: 448 });
2187
- writeFileSync(path, JSON.stringify(value) + "\n", { mode: 384 });
2311
+ mkdirSync3(configHome(environment), { recursive: true, mode: 448 });
2312
+ writeFileSync2(path, JSON.stringify(value) + "\n", { mode: 384 });
2188
2313
  }
2189
2314
  function recordSelfUpdateState(environment, state, at) {
2190
2315
  if (!STATE_PATTERN.test(state)) return;
@@ -2195,7 +2320,7 @@ function recordSelfUpdateState(environment, state, at) {
2195
2320
  }
2196
2321
  function selfUpdateState(environment) {
2197
2322
  try {
2198
- const parsed = JSON.parse(readFileSync2(statePath(environment), "utf8"));
2323
+ const parsed = JSON.parse(readFileSync3(statePath(environment), "utf8"));
2199
2324
  return typeof parsed.state === "string" && STATE_PATTERN.test(parsed.state) ? parsed.state : void 0;
2200
2325
  } catch {
2201
2326
  return void 0;
@@ -2209,9 +2334,9 @@ function resolveLaunch(specifier, environment, execPath = process.execPath, exis
2209
2334
  PATH: [bin, environment.PATH ?? ""].filter((part) => part.length > 0).join(delimiter),
2210
2335
  [SELF_UPDATE_ENVIRONMENT]: "1"
2211
2336
  };
2212
- const script = join2(bin, "..", "lib", "node_modules", "npm", "bin", "npx-cli.js");
2337
+ const script = join3(bin, "..", "lib", "node_modules", "npm", "bin", "npx-cli.js");
2213
2338
  if (exists(script)) return { command: execPath, args: [script, ...tail], environment: child };
2214
- const shim = join2(bin, process.platform === "win32" ? "npx.cmd" : "npx");
2339
+ const shim = join3(bin, process.platform === "win32" ? "npx.cmd" : "npx");
2215
2340
  if (exists(shim)) return { command: shim, args: tail, environment: child };
2216
2341
  return { command: "npx", args: tail, environment: child };
2217
2342
  }
@@ -2249,25 +2374,25 @@ function startSelfUpdateIfDue(newer, deps) {
2249
2374
  }
2250
2375
 
2251
2376
  // src/guidance.ts
2252
- import { createHash, randomBytes } from "node:crypto";
2377
+ import { createHash as createHash2, randomBytes } from "node:crypto";
2253
2378
  import {
2254
2379
  constants,
2255
2380
  closeSync as closeSync2,
2256
2381
  fstatSync,
2257
2382
  lstatSync as lstatSync2,
2258
- mkdirSync as mkdirSync3,
2383
+ mkdirSync as mkdirSync4,
2259
2384
  openSync as openSync2,
2260
- readFileSync as readFileSync3,
2261
- renameSync as renameSync2,
2385
+ readFileSync as readFileSync4,
2386
+ renameSync as renameSync3,
2262
2387
  unlinkSync as unlinkSync2,
2263
- writeFileSync as writeFileSync2
2388
+ writeFileSync as writeFileSync3
2264
2389
  } from "node:fs";
2265
- import { join as join3 } from "node:path";
2390
+ import { join as join4 } from "node:path";
2266
2391
  var GUIDANCE_UNAVAILABLE = "Balladeer could not verify the current workspace guidance and capture mode. Continue the user's authorized work, but do not make unsolicited capture offers or file inferred promises. Do not reuse earlier Quiet/Thorough permission or cached instructions as current. An explicit request to record still requires current Balladeer tool checks and named-human agreement to meaning; never invent approval. Retry current guidance at the next hook boundary.";
2267
2392
  var HEX = /^[a-f0-9]{64}$/;
2268
2393
  var UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i;
2269
2394
  var MAX_BYTES = 4e4;
2270
- var sha = (value) => createHash("sha256").update(value).digest("hex");
2395
+ var sha = (value) => createHash2("sha256").update(value).digest("hex");
2271
2396
  function object(value) {
2272
2397
  return value !== null && typeof value === "object" && !Array.isArray(value);
2273
2398
  }
@@ -2282,13 +2407,13 @@ function guidanceEndpoint(agent) {
2282
2407
  const expected = new URL(agent.controlPlane);
2283
2408
  if (url.origin !== expected.origin || url.protocol !== "https:" || url.pathname !== "/api/mcp" || url.username || url.password || url.hash || !UUID.test(agent.workspaceId) || !UUID.test(agent.repositoryId))
2284
2409
  throw new Error("invalid_guidance_scope");
2285
- for (const [key, value] of [
2410
+ for (const [key2, value] of [
2286
2411
  ["workspaceId", agent.workspaceId],
2287
2412
  ["repositoryId", agent.repositoryId]
2288
2413
  ]) {
2289
- if (url.searchParams.getAll(key).length > 1 || url.searchParams.has(key) && url.searchParams.get(key) !== value)
2414
+ if (url.searchParams.getAll(key2).length > 1 || url.searchParams.has(key2) && url.searchParams.get(key2) !== value)
2290
2415
  throw new Error("invalid_guidance_scope");
2291
- url.searchParams.set(key, value);
2416
+ url.searchParams.set(key2, value);
2292
2417
  }
2293
2418
  url.pathname = "/api/agent-guidance";
2294
2419
  return url;
@@ -2299,7 +2424,7 @@ function readJson(path) {
2299
2424
  fd = openSync2(path, constants.O_RDONLY | constants.O_NOFOLLOW);
2300
2425
  const stat = fstatSync(fd);
2301
2426
  if (!stat.isFile() || stat.size > MAX_BYTES || (stat.mode & 63) !== 0) return void 0;
2302
- return JSON.parse(readFileSync3(fd, "utf8"));
2427
+ return JSON.parse(readFileSync4(fd, "utf8"));
2303
2428
  } catch {
2304
2429
  return void 0;
2305
2430
  } finally {
@@ -2308,7 +2433,7 @@ function readJson(path) {
2308
2433
  }
2309
2434
  function privateDirectory(path) {
2310
2435
  try {
2311
- mkdirSync3(path, { mode: 448 });
2436
+ mkdirSync4(path, { mode: 448 });
2312
2437
  } catch {
2313
2438
  }
2314
2439
  const stat = lstatSync2(path);
@@ -2318,9 +2443,9 @@ function privateDirectory(path) {
2318
2443
  function atomicJson(path, value) {
2319
2444
  const temporary = `${path}.${randomBytes(8).toString("hex")}.tmp`;
2320
2445
  try {
2321
- writeFileSync2(temporary, `${JSON.stringify(value)}
2446
+ writeFileSync3(temporary, `${JSON.stringify(value)}
2322
2447
  `, { flag: "wx", mode: 384 });
2323
- renameSync2(temporary, path);
2448
+ renameSync3(temporary, path);
2324
2449
  } finally {
2325
2450
  try {
2326
2451
  unlinkSync2(temporary);
@@ -2342,16 +2467,16 @@ function guidanceScopeKey(agent) {
2342
2467
  function cacheDirectory(environment) {
2343
2468
  const home = configHome(environment);
2344
2469
  privateDirectory(home);
2345
- const directory = join3(home, "guidance-v1");
2470
+ const directory = join4(home, "guidance-v1");
2346
2471
  privateDirectory(directory);
2347
2472
  return directory;
2348
2473
  }
2349
2474
  function cached(directory, scopeKey, agent) {
2350
2475
  try {
2351
- const pointer = readJson(join3(directory, `${scopeKey}.current.json`));
2476
+ const pointer = readJson(join4(directory, `${scopeKey}.current.json`));
2352
2477
  if (!object(pointer) || typeof pointer.key !== "string" || !HEX.test(pointer.key))
2353
2478
  return void 0;
2354
- const body = readJson(join3(directory, `${pointer.key}.body.json`));
2479
+ const body = readJson(join4(directory, `${pointer.key}.body.json`));
2355
2480
  if (!object(body) || typeof body.etag !== "string" || !/^"[a-f0-9]{64}"$/.test(body.etag))
2356
2481
  return void 0;
2357
2482
  const document = parseGuidance(body.document, agent);
@@ -2427,12 +2552,12 @@ async function loadGuidance(input) {
2427
2552
  if (abort.signal.aborted) throw new Error("guidance_deadline");
2428
2553
  if (directory) {
2429
2554
  try {
2430
- const key = sha(JSON.stringify([scopeKey, document.revision]));
2431
- atomicJson(join3(directory, `${key}.body.json`), { document, etag });
2432
- atomicJson(join3(directory, `${scopeKey}.current.json`), { key });
2433
- if (previous && previous.key !== key) {
2555
+ const key2 = sha(JSON.stringify([scopeKey, document.revision]));
2556
+ atomicJson(join4(directory, `${key2}.body.json`), { document, etag });
2557
+ atomicJson(join4(directory, `${scopeKey}.current.json`), { key: key2 });
2558
+ if (previous && previous.key !== key2) {
2434
2559
  try {
2435
- unlinkSync2(join3(directory, `${previous.key}.body.json`));
2560
+ unlinkSync2(join4(directory, `${previous.key}.body.json`));
2436
2561
  } catch {
2437
2562
  }
2438
2563
  }
@@ -2474,7 +2599,7 @@ function recordGuidanceContext(input, write2) {
2474
2599
  let path;
2475
2600
  let emitted = true;
2476
2601
  try {
2477
- path = join3(cacheDirectory(input.environment), `${contextKey}.context.json`);
2602
+ path = join4(cacheDirectory(input.environment), `${contextKey}.context.json`);
2478
2603
  const previous = readJson(path);
2479
2604
  emitted = always || !object(previous) || previous.status === "unavailable" || previous.digest !== document?.digest || previous.revision !== document?.revision || (previous.runtimeGeneration ?? null) !== (input.runtimeGeneration ?? null);
2480
2605
  } catch {
@@ -2510,14 +2635,14 @@ import {
2510
2635
  existsSync as existsSync3,
2511
2636
  lstatSync as lstatSync4,
2512
2637
  linkSync,
2513
- mkdirSync as mkdirSync5,
2514
- readFileSync as readFileSync6,
2515
- renameSync as renameSync5,
2638
+ mkdirSync as mkdirSync6,
2639
+ readFileSync as readFileSync7,
2640
+ renameSync as renameSync6,
2516
2641
  unlinkSync as unlinkSync5,
2517
- writeFileSync as writeFileSync3
2642
+ writeFileSync as writeFileSync4
2518
2643
  } from "node:fs";
2519
2644
  var import_toml2 = __toESM(require_toml(), 1);
2520
- import { dirname as dirname4, join as join6, relative } from "node:path";
2645
+ import { dirname as dirname4, join as join7, relative } from "node:path";
2521
2646
 
2522
2647
  // src/codex-config.ts
2523
2648
  var import_toml = __toESM(require_toml(), 1);
@@ -2526,26 +2651,26 @@ import {
2526
2651
  existsSync as existsSync2,
2527
2652
  fsyncSync as fsyncSync3,
2528
2653
  lstatSync as lstatSync3,
2529
- mkdirSync as mkdirSync4,
2654
+ mkdirSync as mkdirSync5,
2530
2655
  openSync as openSync4,
2531
- readFileSync as readFileSync5,
2532
- renameSync as renameSync4,
2656
+ readFileSync as readFileSync6,
2657
+ renameSync as renameSync5,
2533
2658
  unlinkSync as unlinkSync4,
2534
2659
  writeSync as writeSync3
2535
2660
  } from "node:fs";
2536
- import { join as join5 } from "node:path";
2661
+ import { join as join6 } from "node:path";
2537
2662
 
2538
2663
  // src/mcp-config.ts
2539
2664
  import {
2540
2665
  closeSync as closeSync3,
2541
2666
  fsyncSync as fsyncSync2,
2542
2667
  openSync as openSync3,
2543
- readFileSync as readFileSync4,
2544
- renameSync as renameSync3,
2668
+ readFileSync as readFileSync5,
2669
+ renameSync as renameSync4,
2545
2670
  unlinkSync as unlinkSync3,
2546
2671
  writeSync as writeSync2
2547
2672
  } from "node:fs";
2548
- import { basename, dirname as dirname3, isAbsolute, join as join4 } from "node:path";
2673
+ import { basename, dirname as dirname3, isAbsolute, join as join5 } from "node:path";
2549
2674
  var MCP_CONFIG_FILE = ".mcp.json";
2550
2675
  function sameOrigin(left, right) {
2551
2676
  try {
@@ -2588,7 +2713,7 @@ function currentEntry(parsed) {
2588
2713
  }
2589
2714
  function readMcpConfig(repositoryRoot) {
2590
2715
  try {
2591
- return JSON.parse(readFileSync4(join4(repositoryRoot, MCP_CONFIG_FILE), "utf8"));
2716
+ return JSON.parse(readFileSync5(join5(repositoryRoot, MCP_CONFIG_FILE), "utf8"));
2592
2717
  } catch {
2593
2718
  return void 0;
2594
2719
  }
@@ -2598,7 +2723,7 @@ function readMcpConfig(repositoryRoot) {
2598
2723
  var CODEX_CONFIG_FILE = ".codex/config.toml";
2599
2724
  function readCodexEntry(root) {
2600
2725
  try {
2601
- const config = (0, import_toml.parse)(readFileSync5(join5(root, CODEX_CONFIG_FILE), "utf8"));
2726
+ const config = (0, import_toml.parse)(readFileSync6(join6(root, CODEX_CONFIG_FILE), "utf8"));
2602
2727
  const servers = config.mcp_servers;
2603
2728
  return servers && typeof servers === "object" && !Array.isArray(servers) && !(servers instanceof Date) ? servers.balladeer : void 0;
2604
2729
  } catch {
@@ -2607,7 +2732,7 @@ function readCodexEntry(root) {
2607
2732
  }
2608
2733
  function hasManagedCodexEntry(root, controlPlane) {
2609
2734
  try {
2610
- const text = readFileSync5(join5(root, CODEX_CONFIG_FILE), "utf8");
2735
+ const text = readFileSync6(join6(root, CODEX_CONFIG_FILE), "utf8");
2611
2736
  const starts = [...text.matchAll(/^# balladeer:mcp:start\r?$/gm)];
2612
2737
  const ends = [...text.matchAll(/^# balladeer:mcp:end\r?$/gm)];
2613
2738
  if (starts.length !== 1 || ends.length !== 1 || !starts[0] || !ends[0] || starts[0].index >= ends[0].index)
@@ -2740,6 +2865,72 @@ Balladeer only keeps promises a check can catch. If a customer would notice some
2740
2865
  slips, say that and I will keep that instead."
2741
2866
 
2742
2867
  Then take the customer-visible half if they give you one, and file that instead.`;
2868
+ var VERIFIER_SHAPE = `### The shape of a check that lasts
2869
+
2870
+ Write a test only when the promise is easily testable: the repository's own test runner reaches the
2871
+ behavior in the ordinary way, its existing fixtures set the situation up, no outside system is
2872
+ needed, and the assertion is one of the promise's cases. Put it in the team's own suite, under the
2873
+ team's runner, with a comment naming the promise id, so a later judge can find it, read it and run
2874
+ it first. If reaching the behavior would mean stand-in modules, a scenario repository or a copy of
2875
+ the product's logic, the promise is not easily testable: write nothing and say so. The pre-push
2876
+ judge below covers it.
2877
+
2878
+ A check exists to catch the failure its owner named, and to keep catching it after the code
2879
+ underneath has been rewritten by people who never read the promise. So build it against the
2880
+ behavior, at the boundary the promise names: the request a caller sends, the command a person runs,
2881
+ the row that ends up written, the page that renders. Reach the product through the entry point its
2882
+ users reach it through, never through a helper you found while reading the source, and never through
2883
+ a copy of the product's logic written into the check. A copy passes forever while the product drifts
2884
+ underneath it.
2885
+
2886
+ Assert what the promise's cases say and nothing more. Each passing case states a property; check
2887
+ that property. Each failing case names an outcome that must not happen; make the check refuse
2888
+ exactly that. Do not pin whole outputs, orderings the promise lets vary, or values the owner never
2889
+ mentioned: every one of those becomes a red the owner is told to act on when the behavior has not
2890
+ broken.
2891
+
2892
+ The refactor control is where a brittle check shows itself. It must be a genuine restructuring of
2893
+ the same behavior, moved, renamed or reimplemented, with the same observable outcome, reached
2894
+ through the same interface the check uses to reach the product. If you cannot write it without also
2895
+ editing the check, the check is bound to an implementation rather than to the promise, and what
2896
+ needs fixing is the check. The known-bad control must be the failure the owner wrote down, not the
2897
+ check sabotaging itself.
2898
+
2899
+ Where the repository's own test runner already reaches the behavior, run it through the declared
2900
+ result protocol rather than writing a second harness: the existing suite is the one the team will
2901
+ keep maintaining. And when a check cannot reach a verdict, say so. A check that could not run reads
2902
+ Unknown, which is honest, and never as a pass, which is not.`;
2903
+ var PRE_PUSH_JUDGE = `### Before you push, try to break it
2904
+
2905
+ The sealed check runs in CI, after the fact. The cheapest moment to catch a broken promise is before
2906
+ the push, on the machine where the change can still be fixed, and the way to catch it is not to
2907
+ reread your own diff. Find the promises the change touches: balladeer affected <paths> where this
2908
+ repository has a touch map, otherwise the leads discover_intent gave you for those paths. For each
2909
+ one that has a linked test or a sealed check, run it first; a test named for the promise is the
2910
+ fastest verdict there is, and the runner's run-one-target command prints the four facts a red check
2911
+ carries.
2912
+
2913
+ Then, for every promise the change touches, checked or not, spawn a fresh agent that has not seen
2914
+ your change or your reasoning, however your host spawns one, and hand it three things and nothing
2915
+ else: the promise as get_promise returns it, the changed files, and the brief below. Not the
2916
+ pull-request description and not your notes. One agent per promise; a judge handed six promises at
2917
+ once holds none of them. If your host cannot spawn one, work through the brief yourself in a fresh
2918
+ context after the work is finished, and say that you did.
2919
+
2920
+ The brief, verbatim: "You are checking one promise against one change. Read the promise's cases
2921
+ first. Then find out, by running things, whether the change keeps it. You may report broken only
2922
+ with a reproduction: a command or test that fails with this change and passes without it, which
2923
+ you have run both ways. You may report kept only after you have exercised each of the promise's
2924
+ passing cases against the changed code. Anything else you report as could not tell, with what
2925
+ stopped you. Do not take the author's word for anything, read code rather than descriptions, and
2926
+ name the promise, never the person."
2927
+
2928
+ What to do with the answer. Broken, with a reproduction: fix the behavior before pushing, keep the
2929
+ reproduction, and offer to carry it into the sealed check as a failing case, which means re-sealing
2930
+ and re-qualifying, so ask before you do. Kept: say so in one line, naming the promise. Could not
2931
+ tell: say that, in those words; it is not a pass. None of this is protection, and none of it reaches
2932
+ Balladeer on its own. Protection is the sealed check qualifying in CI. This is the check you run so
2933
+ that CI has nothing to find.`;
2743
2934
  var SETTLING_QUESTIONS = `### When somebody says "tell me about" one
2744
2935
 
2745
2936
  An id is how a person points at something here. Expand a promise id with get_promise and a proposal
@@ -2888,6 +3079,10 @@ especially: it is Balladeer's word for a proposal, so it reads as jargon whateve
2888
3079
 
2889
3080
  ${SESSION_BEHAVIORS}
2890
3081
 
3082
+ ${VERIFIER_SHAPE}
3083
+
3084
+ ${PRE_PUSH_JUDGE}
3085
+
2891
3086
  ${SETTLING_QUESTIONS}
2892
3087
 
2893
3088
  ${SEALED_FILES}
@@ -2962,7 +3157,7 @@ function readRegular(path, allowMissing = false, writable = true) {
2962
3157
  const stat = lstatSync4(path);
2963
3158
  if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_FILE || writable && (stat.mode & 146) === 0)
2964
3159
  throw new Error("file_not_safely_writable");
2965
- return readFileSync6(path, "utf8");
3160
+ return readFileSync7(path, "utf8");
2966
3161
  }
2967
3162
  function safeParent(root, path) {
2968
3163
  let at = dirname4(path);
@@ -2984,7 +3179,7 @@ function validateProjectGuidanceScope(options) {
2984
3179
  stdio: ["ignore", "pipe", "ignore"],
2985
3180
  timeout: 1e3
2986
3181
  }).trim();
2987
- const path = join6(root, options.hook === "codex" ? ".codex/config.toml" : ".mcp.json");
3182
+ const path = join7(root, options.hook === "codex" ? ".codex/config.toml" : ".mcp.json");
2988
3183
  safeParent(root, path);
2989
3184
  readRegular(path, false, false);
2990
3185
  const entry = options.hook === "codex" ? readCodexEntry(root) : currentEntry(readMcpConfig(root));
@@ -3098,31 +3293,52 @@ function repositoryHints(cwd = process.cwd()) {
3098
3293
  }
3099
3294
 
3100
3295
  // src/hook-trust.ts
3101
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
3102
- import { join as join7 } from "node:path";
3296
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync8, rmSync, writeFileSync as writeFileSync5 } from "node:fs";
3297
+ import { join as join8 } from "node:path";
3298
+ var HOSTS_REQUIRING_TRUST = ["codex"];
3103
3299
  var FILE = "hook-trust.json";
3300
+ var key = (host, set) => set === "guidance" ? host : `${host}:${set}`;
3104
3301
  function read(environment) {
3105
3302
  try {
3106
- const parsed = JSON.parse(readFileSync7(join7(configHome(environment), FILE), "utf8"));
3303
+ const parsed = JSON.parse(readFileSync8(join8(configHome(environment), FILE), "utf8"));
3107
3304
  return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
3108
3305
  } catch {
3109
3306
  return {};
3110
3307
  }
3111
3308
  }
3309
+ function pushCheckAwaitingFile(environment, host) {
3310
+ return join8(configHome(environment), `${host}-push-check-awaiting-first-run`);
3311
+ }
3312
+ function forget(path) {
3313
+ try {
3314
+ rmSync(path, { force: true });
3315
+ } catch {
3316
+ }
3317
+ }
3112
3318
  function write(environment, marks) {
3113
3319
  try {
3114
- mkdirSync6(configHome(environment), { recursive: true, mode: 448 });
3115
- writeFileSync4(join7(configHome(environment), FILE), JSON.stringify(marks) + "\n", {
3320
+ mkdirSync7(configHome(environment), { recursive: true, mode: 448 });
3321
+ writeFileSync5(join8(configHome(environment), FILE), JSON.stringify(marks) + "\n", {
3116
3322
  mode: 384
3117
3323
  });
3324
+ for (const host of HOSTS_REQUIRING_TRUST) {
3325
+ const file = pushCheckAwaitingFile(environment, host);
3326
+ if (awaiting(marks[key(host, "push-check")])) writeFileSync5(file, "", { mode: 384 });
3327
+ else forget(file);
3328
+ }
3118
3329
  } catch {
3119
3330
  }
3120
3331
  }
3121
- function noteHookFired(environment, host, now = Date.now()) {
3332
+ function noteHookFired(environment, host, now = Date.now(), set = "guidance") {
3333
+ if (set === "push-check") forget(pushCheckAwaitingFile(environment, host));
3122
3334
  const marks = read(environment);
3123
- const mark = marks[host];
3335
+ const mark = marks[key(host, set)];
3124
3336
  if (mark?.firedAt !== void 0 && mark.firedAt >= (mark.writtenAt ?? 0)) return;
3125
- write(environment, { ...marks, [host]: { ...mark, firedAt: now } });
3337
+ write(environment, { ...marks, [key(host, set)]: { ...mark, firedAt: now } });
3338
+ }
3339
+ function awaiting(mark) {
3340
+ if (mark?.writtenAt === void 0) return false;
3341
+ return mark.firedAt === void 0 || mark.firedAt < mark.writtenAt;
3126
3342
  }
3127
3343
 
3128
3344
  // src/commands/guidance.ts
@@ -3167,7 +3383,45 @@ function eventFrom(input) {
3167
3383
  function identifier(input) {
3168
3384
  return typeof input === "string" && input.length > 0 && input.length <= 256 ? input : void 0;
3169
3385
  }
3386
+ async function runRelayNote(options) {
3387
+ const input = await new Promise((resolve2) => {
3388
+ const chunks = [];
3389
+ let bytes = 0;
3390
+ let settled = false;
3391
+ const done = (value) => {
3392
+ if (settled) return;
3393
+ settled = true;
3394
+ clearTimeout(timer);
3395
+ options.stdin.removeListener("data", data);
3396
+ resolve2(value);
3397
+ };
3398
+ const data = (chunk) => {
3399
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
3400
+ bytes += buffer.length;
3401
+ if (bytes > 1048576) done(void 0);
3402
+ else chunks.push(buffer);
3403
+ };
3404
+ const timer = setTimeout(() => done(void 0), 1e3);
3405
+ options.stdin.on("data", data);
3406
+ options.stdin.once("end", () => {
3407
+ try {
3408
+ done(JSON.parse(Buffer.concat(chunks).toString("utf8")));
3409
+ } catch {
3410
+ done(void 0);
3411
+ }
3412
+ });
3413
+ options.stdin.once("error", () => done(void 0));
3414
+ });
3415
+ if (!input || typeof input !== "object" || Array.isArray(input)) return 0;
3416
+ const record = input;
3417
+ if (record.hook_event_name !== "PostToolUse") return 0;
3418
+ const out = relayHook(record);
3419
+ if (out) options.write(`${out}
3420
+ `);
3421
+ return 0;
3422
+ }
3170
3423
  async function runGuidance(options) {
3424
+ if (options.event === "PostToolUse") return runRelayNote(options);
3171
3425
  const projectScoped = options.repositoryId !== void 0;
3172
3426
  if (projectScoped && !validateProjectGuidanceScope({
3173
3427
  cwd: options.cwd,