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.
- package/README.md +2 -0
- package/dist/behavior-map-schema.d.ts +107 -0
- package/dist/behavior-map-schema.js +232 -0
- package/dist/cli.d.ts +6 -1
- package/dist/cli.js +74 -6
- package/dist/commands/guidance.d.ts +2 -1
- package/dist/commands/guidance.js +55 -0
- package/dist/commands/judge.d.ts +430 -0
- package/dist/commands/judge.js +1713 -0
- package/dist/commands/map.d.ts +164 -0
- package/dist/commands/map.js +838 -0
- package/dist/commands/mcp.js +2 -2
- package/dist/commands/offers.d.ts +265 -0
- package/dist/commands/offers.js +801 -0
- package/dist/commands/risk.d.ts +29 -0
- package/dist/commands/risk.js +133 -0
- package/dist/copy.d.ts +24 -1
- package/dist/copy.js +91 -0
- package/dist/guidance-hook.mjs +348 -94
- package/dist/headless-agent.d.ts +166 -0
- package/dist/headless-agent.js +416 -0
- package/dist/hook-trust.d.ts +41 -9
- package/dist/hook-trust.js +98 -16
- package/dist/judge-brief.d.ts +48 -0
- package/dist/judge-brief.js +102 -0
- package/dist/judge-hook.d.ts +258 -0
- package/dist/judge-hook.js +1278 -0
- package/dist/judge-said.d.ts +52 -0
- package/dist/judge-said.js +181 -0
- package/dist/map-brief.d.ts +16 -0
- package/dist/map-brief.js +41 -0
- package/dist/offer-brief.d.ts +95 -0
- package/dist/offer-brief.js +217 -0
- package/dist/owned-process.d.ts +65 -0
- package/dist/owned-process.js +146 -0
- package/dist/promise-meaning.d.ts +134 -0
- package/dist/promise-meaning.js +300 -0
- package/dist/relay.d.ts +71 -0
- package/dist/relay.js +193 -0
- package/dist/remove-earlier.js +4 -2
- package/dist/risk/contract.d.ts +145 -0
- package/dist/risk/contract.js +74 -0
- package/dist/risk/describe.d.ts +7 -0
- package/dist/risk/describe.js +45 -0
- package/dist/risk/diff.d.ts +26 -0
- package/dist/risk/diff.js +174 -0
- package/dist/risk/extract.d.ts +43 -0
- package/dist/risk/extract.js +336 -0
- package/dist/risk/git.d.ts +30 -0
- package/dist/risk/git.js +118 -0
- package/dist/risk/import-graph.d.ts +41 -0
- package/dist/risk/import-graph.js +487 -0
- package/dist/risk/index.d.ts +20 -0
- package/dist/risk/index.js +20 -0
- package/dist/risk/paths.d.ts +16 -0
- package/dist/risk/paths.js +73 -0
- package/dist/risk/pipeline.d.ts +47 -0
- package/dist/risk/pipeline.js +121 -0
- package/dist/risk/priors.d.ts +18 -0
- package/dist/risk/priors.js +86 -0
- package/dist/risk/resources.d.ts +56 -0
- package/dist/risk/resources.js +374 -0
- package/dist/risk/score.d.ts +95 -0
- package/dist/risk/score.js +552 -0
- package/dist/risk/symbols.d.ts +35 -0
- package/dist/risk/symbols.js +348 -0
- package/dist/risk/text.d.ts +43 -0
- package/dist/risk/text.js +277 -0
- package/dist/risk/validate.d.ts +19 -0
- package/dist/risk/validate.js +154 -0
- package/dist/scratch-worktree.d.ts +55 -0
- package/dist/scratch-worktree.js +160 -0
- package/dist/user-scope.d.ts +74 -3
- package/dist/user-scope.js +270 -9
- package/dist/wire.d.ts +52 -3
- package/dist/wire.js +2 -2
- package/package.json +1 -1
package/dist/guidance-hook.mjs
CHANGED
|
@@ -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
|
|
20
|
-
if (!__hasOwnProp.call(to,
|
|
21
|
-
__defProp(to,
|
|
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,
|
|
370
|
-
if (hasOwnProperty.call(obj,
|
|
371
|
-
if (
|
|
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(
|
|
573
|
+
recordAssignKeyword(key2) {
|
|
574
574
|
if (this.state.resultTable) {
|
|
575
|
-
this.state.resultTable.push(
|
|
575
|
+
this.state.resultTable.push(key2);
|
|
576
576
|
} else {
|
|
577
|
-
this.state.resultTable = [
|
|
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((
|
|
1782
|
+
return Object.keys(obj).filter((key2) => isInline(obj[key2]));
|
|
1783
1783
|
}
|
|
1784
1784
|
function getComplexKeys(obj) {
|
|
1785
|
-
return Object.keys(obj).filter((
|
|
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((
|
|
1807
|
-
var type = tomlType2(obj[
|
|
1806
|
+
inlineKeys.forEach((key2) => {
|
|
1807
|
+
var type = tomlType2(obj[key2]);
|
|
1808
1808
|
if (type !== "undefined" && type !== "null") {
|
|
1809
|
-
result.push(inlineIndent + stringifyKey(
|
|
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((
|
|
1815
|
-
result.push(stringifyComplex(prefix, complexIndent,
|
|
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(
|
|
1861
|
-
var keyStr = String(
|
|
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((
|
|
1981
|
-
result.push(stringifyKey(
|
|
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,
|
|
1985
|
+
function stringifyComplex(prefix, indent, key2, value) {
|
|
1986
1986
|
var valueType = tomlType2(value);
|
|
1987
1987
|
if (valueType === "array") {
|
|
1988
|
-
return stringifyArrayOfTables(prefix, indent,
|
|
1988
|
+
return stringifyArrayOfTables(prefix, indent, key2, value);
|
|
1989
1989
|
} else if (valueType === "table") {
|
|
1990
|
-
return stringifyComplexTable(prefix, indent,
|
|
1990
|
+
return stringifyComplexTable(prefix, indent, key2, value);
|
|
1991
1991
|
} else {
|
|
1992
1992
|
throw typeError(valueType);
|
|
1993
1993
|
}
|
|
1994
1994
|
}
|
|
1995
|
-
function stringifyArrayOfTables(prefix, indent,
|
|
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(
|
|
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,
|
|
2010
|
-
var fullKey = prefix + stringifyKey(
|
|
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.
|
|
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(
|
|
2066
|
-
return resolve(
|
|
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
|
|
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 =
|
|
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
|
|
2158
|
-
import { delimiter, dirname as dirname2, join as
|
|
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
|
|
2295
|
+
return join3(configHome(environment), STAMP);
|
|
2171
2296
|
}
|
|
2172
2297
|
function statePath(environment) {
|
|
2173
|
-
return
|
|
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(
|
|
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
|
-
|
|
2187
|
-
|
|
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(
|
|
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 =
|
|
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 =
|
|
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
|
|
2383
|
+
mkdirSync as mkdirSync4,
|
|
2259
2384
|
openSync as openSync2,
|
|
2260
|
-
readFileSync as
|
|
2261
|
-
renameSync as
|
|
2385
|
+
readFileSync as readFileSync4,
|
|
2386
|
+
renameSync as renameSync3,
|
|
2262
2387
|
unlinkSync as unlinkSync2,
|
|
2263
|
-
writeFileSync as
|
|
2388
|
+
writeFileSync as writeFileSync3
|
|
2264
2389
|
} from "node:fs";
|
|
2265
|
-
import { join as
|
|
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) =>
|
|
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 [
|
|
2410
|
+
for (const [key2, value] of [
|
|
2286
2411
|
["workspaceId", agent.workspaceId],
|
|
2287
2412
|
["repositoryId", agent.repositoryId]
|
|
2288
2413
|
]) {
|
|
2289
|
-
if (url.searchParams.getAll(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
2446
|
+
writeFileSync3(temporary, `${JSON.stringify(value)}
|
|
2322
2447
|
`, { flag: "wx", mode: 384 });
|
|
2323
|
-
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
|
2431
|
-
atomicJson(
|
|
2432
|
-
atomicJson(
|
|
2433
|
-
if (previous && previous.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(
|
|
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 =
|
|
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
|
|
2514
|
-
readFileSync as
|
|
2515
|
-
renameSync as
|
|
2638
|
+
mkdirSync as mkdirSync6,
|
|
2639
|
+
readFileSync as readFileSync7,
|
|
2640
|
+
renameSync as renameSync6,
|
|
2516
2641
|
unlinkSync as unlinkSync5,
|
|
2517
|
-
writeFileSync as
|
|
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
|
|
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
|
|
2654
|
+
mkdirSync as mkdirSync5,
|
|
2530
2655
|
openSync as openSync4,
|
|
2531
|
-
readFileSync as
|
|
2532
|
-
renameSync as
|
|
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
|
|
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
|
|
2544
|
-
renameSync as
|
|
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
|
|
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(
|
|
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)(
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
3102
|
-
import { join as
|
|
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(
|
|
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
|
-
|
|
3115
|
-
|
|
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,
|