@edgehero/pi-dispatch-admin 1.3.0 → 1.4.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.
- package/README.md +4 -3
- package/dist/index.mjs +816 -473
- package/package.json +1 -1
- package/skills/operate-pi-dispatch/SKILL.md +36 -6
package/dist/index.mjs
CHANGED
|
@@ -7886,6 +7886,372 @@ var init_budget = __esm({
|
|
|
7886
7886
|
}
|
|
7887
7887
|
});
|
|
7888
7888
|
|
|
7889
|
+
// ../worker/src/pause-windows.mjs
|
|
7890
|
+
function isNonEmptyString2(value) {
|
|
7891
|
+
return typeof value === "string" && value.trim() !== "";
|
|
7892
|
+
}
|
|
7893
|
+
function parseHHMM(value, label, at, path) {
|
|
7894
|
+
const m = isNonEmptyString2(value) ? /^([0-9]{1,2}):([0-9]{2})$/.exec(value.trim()) : null;
|
|
7895
|
+
if (!m) throw configError(`${at}: ${label} must be "HH:MM": ${JSON.stringify(value)} (${path})`);
|
|
7896
|
+
const h = Number(m[1]);
|
|
7897
|
+
const min = Number(m[2]);
|
|
7898
|
+
if (h > 23 || min > 59) throw configError(`${at}: ${label} out of range 00:00-23:59: ${JSON.stringify(value)} (${path})`);
|
|
7899
|
+
return h * 60 + min;
|
|
7900
|
+
}
|
|
7901
|
+
function assertDate(value, label, at, path) {
|
|
7902
|
+
const m = /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.exec(String(value).trim());
|
|
7903
|
+
const mo = m && Number(m[2]);
|
|
7904
|
+
const d = m && Number(m[3]);
|
|
7905
|
+
if (!m || mo < 1 || mo > 12 || d < 1 || d > 31) {
|
|
7906
|
+
throw configError(`${at}: ${label} must be "YYYY-MM-DD": ${JSON.stringify(value)} (${path})`);
|
|
7907
|
+
}
|
|
7908
|
+
}
|
|
7909
|
+
function validateTz(tz, at, path) {
|
|
7910
|
+
try {
|
|
7911
|
+
new Intl.DateTimeFormat("en-US", { timeZone: tz });
|
|
7912
|
+
return tz;
|
|
7913
|
+
} catch {
|
|
7914
|
+
throw configError(`${at}: tz is not a valid IANA timezone: ${JSON.stringify(tz)} (${path})`);
|
|
7915
|
+
}
|
|
7916
|
+
}
|
|
7917
|
+
function parsePauseWindows(text, path) {
|
|
7918
|
+
let parsed;
|
|
7919
|
+
try {
|
|
7920
|
+
parsed = JSON.parse(text);
|
|
7921
|
+
} catch (error) {
|
|
7922
|
+
throw configError(`pause-windows file is not valid JSON: ${path} (${error.message})`);
|
|
7923
|
+
}
|
|
7924
|
+
const windows = parsed?.windows;
|
|
7925
|
+
if (!Array.isArray(windows)) {
|
|
7926
|
+
throw configError(`pause-windows file must have a "windows" array: ${path}`);
|
|
7927
|
+
}
|
|
7928
|
+
return windows.map((w, index) => normalizeWindow(w, index, path));
|
|
7929
|
+
}
|
|
7930
|
+
function normalizeWindow(w, index, path) {
|
|
7931
|
+
const at = `pause window at index ${index}`;
|
|
7932
|
+
if (w === null || typeof w !== "object") throw configError(`${at}: must be an object: ${path}`);
|
|
7933
|
+
if (!isNonEmptyString2(w.scope)) throw configError(`${at}: scope must be a non-empty string: ${path}`);
|
|
7934
|
+
const fromMin = parseHHMM(w.from, "from", at, path);
|
|
7935
|
+
const toMin = parseHHMM(w.to, "to", at, path);
|
|
7936
|
+
if (fromMin === toMin) {
|
|
7937
|
+
throw configError(`${at}: from and to must differ (a 24h pause is not expressible; remove the trigger): ${path}`);
|
|
7938
|
+
}
|
|
7939
|
+
const tz = w.tz === void 0 ? "UTC" : validateTz(w.tz, at, path);
|
|
7940
|
+
let days = null;
|
|
7941
|
+
if (w.days !== void 0) {
|
|
7942
|
+
if (!Array.isArray(w.days) || w.days.length === 0) {
|
|
7943
|
+
throw configError(`${at}: days must be a non-empty array of weekday names (mon..sun): ${path}`);
|
|
7944
|
+
}
|
|
7945
|
+
days = w.days.map((d) => {
|
|
7946
|
+
const key = String(d).trim().toLowerCase().slice(0, 3);
|
|
7947
|
+
if (!DAY_SET.has(key)) throw configError(`${at}: unknown weekday ${JSON.stringify(d)} (use mon..sun): ${path}`);
|
|
7948
|
+
return key;
|
|
7949
|
+
});
|
|
7950
|
+
}
|
|
7951
|
+
let dateFrom = null;
|
|
7952
|
+
let dateTo = null;
|
|
7953
|
+
if (w.dateFrom !== void 0) {
|
|
7954
|
+
assertDate(w.dateFrom, "dateFrom", at, path);
|
|
7955
|
+
dateFrom = String(w.dateFrom).trim();
|
|
7956
|
+
}
|
|
7957
|
+
if (w.dateTo !== void 0) {
|
|
7958
|
+
assertDate(w.dateTo, "dateTo", at, path);
|
|
7959
|
+
dateTo = String(w.dateTo).trim();
|
|
7960
|
+
}
|
|
7961
|
+
if (dateFrom && dateTo && dateFrom > dateTo) throw configError(`${at}: dateFrom must be <= dateTo: ${path}`);
|
|
7962
|
+
const norm = { scope: w.scope.trim(), from: w.from.trim(), to: w.to.trim(), fromMin, toMin, tz };
|
|
7963
|
+
if (days) norm.days = days;
|
|
7964
|
+
if (dateFrom) norm.dateFrom = dateFrom;
|
|
7965
|
+
if (dateTo) norm.dateTo = dateTo;
|
|
7966
|
+
return norm;
|
|
7967
|
+
}
|
|
7968
|
+
function zonedParts(tz, ms) {
|
|
7969
|
+
const fmt2 = new Intl.DateTimeFormat("en-US", {
|
|
7970
|
+
timeZone: tz,
|
|
7971
|
+
hour12: false,
|
|
7972
|
+
year: "numeric",
|
|
7973
|
+
month: "2-digit",
|
|
7974
|
+
day: "2-digit",
|
|
7975
|
+
hour: "2-digit",
|
|
7976
|
+
minute: "2-digit",
|
|
7977
|
+
weekday: "short"
|
|
7978
|
+
});
|
|
7979
|
+
const p = {};
|
|
7980
|
+
for (const part of fmt2.formatToParts(new Date(ms))) p[part.type] = part.value;
|
|
7981
|
+
let hh = Number(p.hour);
|
|
7982
|
+
if (hh === 24) hh = 0;
|
|
7983
|
+
return { y: Number(p.year), mo: Number(p.month), d: Number(p.day), hh, mi: Number(p.minute), dow: WEEKDAY_INDEX[p.weekday] };
|
|
7984
|
+
}
|
|
7985
|
+
function wallToMs(tz, y, mo, d, hh, mi) {
|
|
7986
|
+
const guess = Date.UTC(y, mo - 1, d, hh, mi);
|
|
7987
|
+
const shown = zonedParts(tz, guess);
|
|
7988
|
+
const shownAsUtc = Date.UTC(shown.y, shown.mo - 1, shown.d, shown.hh, shown.mi);
|
|
7989
|
+
return guess - (shownAsUtc - guess);
|
|
7990
|
+
}
|
|
7991
|
+
function shiftDay(tz, parts, delta) {
|
|
7992
|
+
const noon = wallToMs(tz, parts.y, parts.mo, parts.d, 12, 0);
|
|
7993
|
+
return zonedParts(tz, noon + delta * 864e5);
|
|
7994
|
+
}
|
|
7995
|
+
function ymd(parts) {
|
|
7996
|
+
return `${String(parts.y).padStart(4, "0")}-${String(parts.mo).padStart(2, "0")}-${String(parts.d).padStart(2, "0")}`;
|
|
7997
|
+
}
|
|
7998
|
+
function qualifies(w, parts) {
|
|
7999
|
+
if (w.days && !w.days.includes(DAYS[parts.dow])) return false;
|
|
8000
|
+
const date = ymd(parts);
|
|
8001
|
+
if (w.dateFrom && date < w.dateFrom) return false;
|
|
8002
|
+
if (w.dateTo && date > w.dateTo) return false;
|
|
8003
|
+
return true;
|
|
8004
|
+
}
|
|
8005
|
+
function windowEndAt(w, nowMs) {
|
|
8006
|
+
const p = zonedParts(w.tz, nowMs);
|
|
8007
|
+
const nowMin = p.hh * 60 + p.mi;
|
|
8008
|
+
const endAt = (dateParts) => wallToMs(w.tz, dateParts.y, dateParts.mo, dateParts.d, Math.floor(w.toMin / 60), w.toMin % 60);
|
|
8009
|
+
if (w.fromMin < w.toMin) {
|
|
8010
|
+
if (nowMin >= w.fromMin && nowMin < w.toMin && qualifies(w, p)) return endAt(p);
|
|
8011
|
+
return null;
|
|
8012
|
+
}
|
|
8013
|
+
if (nowMin >= w.fromMin && qualifies(w, p)) return endAt(shiftDay(w.tz, p, 1));
|
|
8014
|
+
if (nowMin < w.toMin && qualifies(w, shiftDay(w.tz, p, -1))) return endAt(p);
|
|
8015
|
+
return null;
|
|
8016
|
+
}
|
|
8017
|
+
var DAYS, DAY_SET, WEEKDAY_INDEX;
|
|
8018
|
+
var init_pause_windows = __esm({
|
|
8019
|
+
"../worker/src/pause-windows.mjs"() {
|
|
8020
|
+
init_config2();
|
|
8021
|
+
DAYS = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
|
|
8022
|
+
DAY_SET = new Set(DAYS);
|
|
8023
|
+
WEEKDAY_INDEX = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 };
|
|
8024
|
+
}
|
|
8025
|
+
});
|
|
8026
|
+
|
|
8027
|
+
// ../worker/src/subscriptions.mjs
|
|
8028
|
+
function isNonEmptyString3(value) {
|
|
8029
|
+
return typeof value === "string" && value.trim() !== "";
|
|
8030
|
+
}
|
|
8031
|
+
function normalizeGlobs(value, label, at, path) {
|
|
8032
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
8033
|
+
throw configError(`${at}: ${label} must be a non-empty array of model globs: ${path}`);
|
|
8034
|
+
}
|
|
8035
|
+
return value.map((glob) => {
|
|
8036
|
+
if (typeof glob !== "string" || !MODEL_GLOB_RE.test(glob.trim())) {
|
|
8037
|
+
throw configError(`${at}: ${label} glob ${JSON.stringify(glob)} must match ${MODEL_GLOB_RE} (model-id charset plus "*", max 64 chars): ${path}`);
|
|
8038
|
+
}
|
|
8039
|
+
return glob.trim();
|
|
8040
|
+
});
|
|
8041
|
+
}
|
|
8042
|
+
function normalizeBool(value, label, at, path, fallback) {
|
|
8043
|
+
if (value === void 0) return fallback;
|
|
8044
|
+
if (typeof value !== "boolean") throw configError(`${at}: ${label} must be a boolean: ${path}`);
|
|
8045
|
+
return value;
|
|
8046
|
+
}
|
|
8047
|
+
function parseSubscriptions(text, path) {
|
|
8048
|
+
let parsed;
|
|
8049
|
+
try {
|
|
8050
|
+
parsed = JSON.parse(text);
|
|
8051
|
+
} catch (error) {
|
|
8052
|
+
throw configError(`subscriptions file is not valid JSON: ${path} (${error.message})`);
|
|
8053
|
+
}
|
|
8054
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
8055
|
+
throw configError(`subscriptions file must be a JSON object: ${path}`);
|
|
8056
|
+
}
|
|
8057
|
+
const version = parsed.version;
|
|
8058
|
+
if (!Number.isInteger(version) || version < 1) {
|
|
8059
|
+
throw configError(`subscriptions file must declare an integer "version" >= 1 (got ${JSON.stringify(version)}): ${path}`);
|
|
8060
|
+
}
|
|
8061
|
+
if (version > SUBSCRIPTIONS_VERSION) {
|
|
8062
|
+
throw configError(`subscriptions file written by a newer pi-dispatch (version ${version}; this build understands ${SUBSCRIPTIONS_VERSION}): ${path}`);
|
|
8063
|
+
}
|
|
8064
|
+
const entries = parsed.subscriptions;
|
|
8065
|
+
if (!Array.isArray(entries)) {
|
|
8066
|
+
throw configError(`subscriptions file must have a "subscriptions" array: ${path}`);
|
|
8067
|
+
}
|
|
8068
|
+
const seenIds = /* @__PURE__ */ new Map();
|
|
8069
|
+
return { version, subscriptions: entries.map((entry, index) => normalizeSubscription(entry, index, path, seenIds)) };
|
|
8070
|
+
}
|
|
8071
|
+
function normalizeSubscription(entry, index, path, seenIds) {
|
|
8072
|
+
const at = `subscription at index ${index}`;
|
|
8073
|
+
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
|
|
8074
|
+
throw configError(`${at}: must be an object: ${path}`);
|
|
8075
|
+
}
|
|
8076
|
+
if (!isNonEmptyString3(entry.id)) throw configError(`${at}: id must be a non-empty string: ${path}`);
|
|
8077
|
+
const id = entry.id.trim();
|
|
8078
|
+
if (seenIds.has(id)) {
|
|
8079
|
+
throw configError(`${at}: id ${JSON.stringify(id)} is already used by the subscription at index ${seenIds.get(id)}: ${path}`);
|
|
8080
|
+
}
|
|
8081
|
+
seenIds.set(id, index);
|
|
8082
|
+
if (!isNonEmptyString3(entry.vendor)) throw configError(`${at}: vendor must be a non-empty string: ${path}`);
|
|
8083
|
+
if (!isNonEmptyString3(entry.provider)) throw configError(`${at}: provider must be a non-empty string (the pi-ai provider id this plan covers): ${path}`);
|
|
8084
|
+
const models = entry.models === void 0 ? ["*"] : normalizeGlobs(entry.models, "models", at, path);
|
|
8085
|
+
const price = entry.price;
|
|
8086
|
+
if (price === null || typeof price !== "object" || Array.isArray(price)) {
|
|
8087
|
+
throw configError(`${at}: price must be an object { amount, currency, per }: ${path}`);
|
|
8088
|
+
}
|
|
8089
|
+
if (typeof price.amount !== "number" || !Number.isFinite(price.amount) || price.amount <= 0) {
|
|
8090
|
+
throw configError(`${at}: price.amount must be a number > 0: ${path}`);
|
|
8091
|
+
}
|
|
8092
|
+
if (typeof price.currency !== "string" || !/^[A-Z]{3}$/.test(price.currency)) {
|
|
8093
|
+
throw configError(`${at}: price.currency must be a 3-letter uppercase code (e.g. "USD"): ${path}`);
|
|
8094
|
+
}
|
|
8095
|
+
if (price.per !== "month") {
|
|
8096
|
+
throw configError(`${at}: price.per must be "month" (got ${JSON.stringify(price.per)}): ${path}`);
|
|
8097
|
+
}
|
|
8098
|
+
const sharedWithOtherProducts = normalizeBool(entry.sharedWithOtherProducts, "sharedWithOtherProducts", at, path, false);
|
|
8099
|
+
const hypothetical = normalizeBool(entry.hypothetical, "hypothetical", at, path, false);
|
|
8100
|
+
let counterfactualModel = null;
|
|
8101
|
+
if (entry.counterfactualModel !== void 0 && entry.counterfactualModel !== null) {
|
|
8102
|
+
const cm = entry.counterfactualModel;
|
|
8103
|
+
if (typeof cm !== "object" || Array.isArray(cm) || !isNonEmptyString3(cm.provider) || !isNonEmptyString3(cm.id)) {
|
|
8104
|
+
throw configError(`${at}: counterfactualModel must be { provider, id } with non-empty strings (a PRICED pi-ai model to re-price covered runs at API rates): ${path}`);
|
|
8105
|
+
}
|
|
8106
|
+
counterfactualModel = { provider: cm.provider.trim(), id: cm.id.trim() };
|
|
8107
|
+
}
|
|
8108
|
+
let windows = [];
|
|
8109
|
+
if (entry.windows !== void 0) {
|
|
8110
|
+
if (!Array.isArray(entry.windows)) throw configError(`${at}: windows must be an array: ${path}`);
|
|
8111
|
+
windows = entry.windows.map((w, windowIndex) => normalizeWindow2(w, windowIndex, index, path));
|
|
8112
|
+
}
|
|
8113
|
+
return {
|
|
8114
|
+
id,
|
|
8115
|
+
vendor: entry.vendor.trim(),
|
|
8116
|
+
provider: entry.provider.trim(),
|
|
8117
|
+
models,
|
|
8118
|
+
price: { amount: price.amount, currency: price.currency, per: "month" },
|
|
8119
|
+
sharedWithOtherProducts,
|
|
8120
|
+
hypothetical,
|
|
8121
|
+
counterfactualModel,
|
|
8122
|
+
windows
|
|
8123
|
+
};
|
|
8124
|
+
}
|
|
8125
|
+
function normalizeWindow2(w, windowIndex, subIndex, path) {
|
|
8126
|
+
const at = `window at index ${windowIndex} of subscription at index ${subIndex}`;
|
|
8127
|
+
if (w === null || typeof w !== "object" || Array.isArray(w)) {
|
|
8128
|
+
throw configError(`${at}: must be an object: ${path}`);
|
|
8129
|
+
}
|
|
8130
|
+
if (!WINDOW_PERS.has(w.per)) {
|
|
8131
|
+
throw configError(`${at}: per must be one of 5h|7d|30d|month (got ${JSON.stringify(w.per)}): ${path}`);
|
|
8132
|
+
}
|
|
8133
|
+
const rolling = normalizeBool(w.rolling, "rolling", at, path, false);
|
|
8134
|
+
let unit = null;
|
|
8135
|
+
if (w.unit !== void 0 && w.unit !== null) {
|
|
8136
|
+
if (!WINDOW_UNITS.has(w.unit)) {
|
|
8137
|
+
throw configError(`${at}: unit must be one of tokens|requests|prompts|credits, or null (vendor undisclosed): ${path}`);
|
|
8138
|
+
}
|
|
8139
|
+
unit = w.unit;
|
|
8140
|
+
}
|
|
8141
|
+
let limit = null;
|
|
8142
|
+
if (w.limit !== void 0 && w.limit !== null) {
|
|
8143
|
+
if (typeof w.limit === "number") {
|
|
8144
|
+
if (!Number.isFinite(w.limit) || w.limit <= 0) throw configError(`${at}: limit must be a number > 0, [min, max], or null (vendor undisclosed): ${path}`);
|
|
8145
|
+
limit = w.limit;
|
|
8146
|
+
} else if (Array.isArray(w.limit)) {
|
|
8147
|
+
const [min, max] = w.limit;
|
|
8148
|
+
if (w.limit.length !== 2 || typeof min !== "number" || typeof max !== "number" || !Number.isFinite(min) || !Number.isFinite(max) || min <= 0 || max <= 0 || min >= max) {
|
|
8149
|
+
throw configError(`${at}: limit range must be [min, max] with both > 0 and min < max: ${path}`);
|
|
8150
|
+
}
|
|
8151
|
+
limit = [min, max];
|
|
8152
|
+
} else {
|
|
8153
|
+
throw configError(`${at}: limit must be a number > 0, [min, max], or null (vendor undisclosed): ${path}`);
|
|
8154
|
+
}
|
|
8155
|
+
}
|
|
8156
|
+
const scope = w.scope === void 0 || w.scope === null ? null : normalizeGlobs(w.scope, "scope", at, path);
|
|
8157
|
+
return { per: w.per, rolling, unit, limit, scope };
|
|
8158
|
+
}
|
|
8159
|
+
var SUBSCRIPTIONS_VERSION, WINDOW_PERS, WINDOW_UNITS, MODEL_GLOB_RE;
|
|
8160
|
+
var init_subscriptions = __esm({
|
|
8161
|
+
"../worker/src/subscriptions.mjs"() {
|
|
8162
|
+
init_config2();
|
|
8163
|
+
SUBSCRIPTIONS_VERSION = 1;
|
|
8164
|
+
WINDOW_PERS = /* @__PURE__ */ new Set(["5h", "7d", "30d", "month"]);
|
|
8165
|
+
WINDOW_UNITS = /* @__PURE__ */ new Set(["tokens", "requests", "prompts", "credits"]);
|
|
8166
|
+
MODEL_GLOB_RE = /^[A-Za-z0-9*._:/-]{1,64}$/;
|
|
8167
|
+
}
|
|
8168
|
+
});
|
|
8169
|
+
|
|
8170
|
+
// ../worker/src/connection.mjs
|
|
8171
|
+
import { Redis } from "ioredis";
|
|
8172
|
+
function parseConnection(url, { failFast = false } = {}) {
|
|
8173
|
+
const u = new URL(url);
|
|
8174
|
+
return {
|
|
8175
|
+
host: u.hostname || "127.0.0.1",
|
|
8176
|
+
port: Number(u.port || 6379),
|
|
8177
|
+
...u.password ? { password: u.password } : {},
|
|
8178
|
+
...u.username ? { username: u.username } : {},
|
|
8179
|
+
...u.pathname && u.pathname !== "/" ? { db: Number(u.pathname.slice(1)) } : {},
|
|
8180
|
+
maxRetriesPerRequest: null,
|
|
8181
|
+
// required for BullMQ blocking connections
|
|
8182
|
+
...failFast ? {
|
|
8183
|
+
connectTimeout: 2e3,
|
|
8184
|
+
enableOfflineQueue: false,
|
|
8185
|
+
// don't buffer commands while disconnected -- error now
|
|
8186
|
+
retryStrategy: (attempts) => attempts > 2 ? null : 200
|
|
8187
|
+
// give up after ~2 tries
|
|
8188
|
+
} : {}
|
|
8189
|
+
};
|
|
8190
|
+
}
|
|
8191
|
+
function makeRedisClient(url) {
|
|
8192
|
+
return new Redis(url, { maxRetriesPerRequest: null });
|
|
8193
|
+
}
|
|
8194
|
+
var init_connection = __esm({
|
|
8195
|
+
"../worker/src/connection.mjs"() {
|
|
8196
|
+
}
|
|
8197
|
+
});
|
|
8198
|
+
|
|
8199
|
+
// ../worker/src/job-id.mjs
|
|
8200
|
+
import { createHash } from "node:crypto";
|
|
8201
|
+
function localJobId({ folder, flow, task, minute }) {
|
|
8202
|
+
const digest = createHash("sha256").update([folder, flow ?? "", task ?? "", minute].join("\0")).digest("hex");
|
|
8203
|
+
return `local-${digest.slice(0, 16)}`;
|
|
8204
|
+
}
|
|
8205
|
+
var init_job_id = __esm({
|
|
8206
|
+
"../worker/src/job-id.mjs"() {
|
|
8207
|
+
init_config2();
|
|
8208
|
+
init_forges();
|
|
8209
|
+
}
|
|
8210
|
+
});
|
|
8211
|
+
|
|
8212
|
+
// ../worker/src/flow-gate.mjs
|
|
8213
|
+
import { execFile } from "node:child_process";
|
|
8214
|
+
import { promisify } from "node:util";
|
|
8215
|
+
async function readFlowGate({ folder, flow, sha, git = defaultGit }) {
|
|
8216
|
+
if (typeof sha !== "string" || sha.trim() === "") return { gate: "deny" };
|
|
8217
|
+
if (typeof flow !== "string" || !SKILL_NAME_RE.test(flow)) return { gate: "deny" };
|
|
8218
|
+
const path = `.pi/skills/${flow}/SKILL.md`;
|
|
8219
|
+
try {
|
|
8220
|
+
const lsTreeZ = await git(folder, ["ls-tree", "-z", sha, path]);
|
|
8221
|
+
const record = lsTreeZ.split("\0").find((r) => r);
|
|
8222
|
+
if (!record) return { gate: "no-skill" };
|
|
8223
|
+
const tab = record.indexOf(" ");
|
|
8224
|
+
if (tab === -1) return { gate: "deny" };
|
|
8225
|
+
const [mode, type, oid] = record.slice(0, tab).split(/\s+/);
|
|
8226
|
+
if (mode !== "100644" || type !== "blob") return { gate: "deny" };
|
|
8227
|
+
const content = await git(folder, ["cat-file", "blob", oid], { raw: true });
|
|
8228
|
+
return { gate: aiTriggerAllows(content) ? "allow" : "deny" };
|
|
8229
|
+
} catch {
|
|
8230
|
+
return { gate: "deny" };
|
|
8231
|
+
}
|
|
8232
|
+
}
|
|
8233
|
+
function aiTriggerAllows(buf) {
|
|
8234
|
+
const text = buf.toString("utf8").replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
|
|
8235
|
+
const block = /^---\n([\s\S]*?)\n---(?:\n|$)/.exec(text);
|
|
8236
|
+
if (!block) return false;
|
|
8237
|
+
return /^ai-trigger:\s*("?)allow\1\s*$/m.test(block[1]);
|
|
8238
|
+
}
|
|
8239
|
+
async function defaultGit(gitDir, args, { raw = false } = {}) {
|
|
8240
|
+
const hardened = ["-c", "core.hooksPath=/dev/null", "-c", "core.fsmonitor=false", "--no-pager", "-C", gitDir, ...args];
|
|
8241
|
+
const { stdout } = await exec("git", hardened, {
|
|
8242
|
+
encoding: raw ? "buffer" : "utf8",
|
|
8243
|
+
maxBuffer: 16 * 1024 * 1024
|
|
8244
|
+
});
|
|
8245
|
+
return stdout;
|
|
8246
|
+
}
|
|
8247
|
+
var exec, SKILL_NAME_RE;
|
|
8248
|
+
var init_flow_gate = __esm({
|
|
8249
|
+
"../worker/src/flow-gate.mjs"() {
|
|
8250
|
+
exec = promisify(execFile);
|
|
8251
|
+
SKILL_NAME_RE = /^[a-z0-9](?:[a-z0-9_-]{0,62}[a-z0-9])?$/;
|
|
8252
|
+
}
|
|
8253
|
+
});
|
|
8254
|
+
|
|
7889
8255
|
// ../worker/src/reserved-env.mjs
|
|
7890
8256
|
var CONTAINER_ENV_NAMES;
|
|
7891
8257
|
var init_reserved_env = __esm({
|
|
@@ -7910,7 +8276,7 @@ var init_reserved_env = __esm({
|
|
|
7910
8276
|
});
|
|
7911
8277
|
|
|
7912
8278
|
// ../worker/src/triggers.mjs
|
|
7913
|
-
function
|
|
8279
|
+
function isNonEmptyString4(value) {
|
|
7914
8280
|
return typeof value === "string" && value.trim() !== "";
|
|
7915
8281
|
}
|
|
7916
8282
|
function parseTriggers(text, path) {
|
|
@@ -7940,7 +8306,7 @@ function normalizeTrigger(entry, index, path, state) {
|
|
|
7940
8306
|
throw configError(`${at}: "run" must be an object: ${path}`);
|
|
7941
8307
|
}
|
|
7942
8308
|
if (!ON_TYPES.has(on.type)) {
|
|
7943
|
-
throw configError(`${at}: on.type must be one of
|
|
8309
|
+
throw configError(`${at}: on.type must be one of ${[...ON_TYPES].join("|")} (got ${JSON.stringify(on.type)}): ${path}`);
|
|
7944
8310
|
}
|
|
7945
8311
|
if (!RUN_KINDS.includes(run.kind)) {
|
|
7946
8312
|
throw configError(`${at}: run.kind must be one of ${RUN_KINDS.join("|")} (got ${JSON.stringify(run.kind)}): ${path}`);
|
|
@@ -7954,14 +8320,18 @@ function normalizeTrigger(entry, index, path, state) {
|
|
|
7954
8320
|
if (!isForgeKind(run.kind)) {
|
|
7955
8321
|
throw configError(`${at}: a ${on.type} trigger is webhook-driven and produces a forge job; run.kind must be one of ${FORGE_KINDS.join("|")} (got ${JSON.stringify(run.kind)}): ${path}`);
|
|
7956
8322
|
}
|
|
7957
|
-
|
|
7958
|
-
if (on.type === "
|
|
7959
|
-
|
|
8323
|
+
let normalized;
|
|
8324
|
+
if (on.type === "label") normalized = normalizeLabel(on, run, index, path);
|
|
8325
|
+
else if (on.type === "comment") normalized = normalizeComment(on, run, index, path, state);
|
|
8326
|
+
else if (on.type === "issue") normalized = normalizeIssue(on, run, index, path);
|
|
8327
|
+
else normalized = normalizePullRequest(on, run, index, path);
|
|
8328
|
+
if (on.disarmed !== void 0) return { on: { type: DISARMED_TYPE }, run: {} };
|
|
8329
|
+
return normalized;
|
|
7960
8330
|
}
|
|
7961
8331
|
function normalizeCron(on, run, index, path, state) {
|
|
7962
8332
|
const at = `trigger at index ${index}`;
|
|
7963
8333
|
const id = on.id;
|
|
7964
|
-
if (!
|
|
8334
|
+
if (!isNonEmptyString4(id)) {
|
|
7965
8335
|
throw configError(`${at}: cron on.id must be a non-empty string: ${path}`);
|
|
7966
8336
|
}
|
|
7967
8337
|
if (id.includes(":")) {
|
|
@@ -7975,7 +8345,7 @@ function normalizeCron(on, run, index, path, state) {
|
|
|
7975
8345
|
}
|
|
7976
8346
|
state.seenCronIds.add(id);
|
|
7977
8347
|
const pattern = on.pattern;
|
|
7978
|
-
if (!
|
|
8348
|
+
if (!isNonEmptyString4(pattern)) {
|
|
7979
8349
|
throw configError(`cron trigger "${id}": on.pattern must be a non-empty string: ${path}`);
|
|
7980
8350
|
}
|
|
7981
8351
|
const fieldCount = pattern.trim().split(/\s+/).length;
|
|
@@ -7983,13 +8353,13 @@ function normalizeCron(on, run, index, path, state) {
|
|
|
7983
8353
|
throw configError(`cron trigger "${id}": on.pattern must have 5 or 6 space-separated fields, got ${fieldCount}: ${path}`);
|
|
7984
8354
|
}
|
|
7985
8355
|
const command = validateCommand(run, `cron trigger "${id}"`, path, { onType: "cron" });
|
|
7986
|
-
if (!
|
|
8356
|
+
if (!isNonEmptyString4(run.folder)) {
|
|
7987
8357
|
throw configError(`cron trigger "${id}": run.folder must be a non-empty string: ${path}`);
|
|
7988
8358
|
}
|
|
7989
|
-
if (command === void 0 && !
|
|
8359
|
+
if (command === void 0 && !isNonEmptyString4(run.flow)) {
|
|
7990
8360
|
throw configError(`cron trigger "${id}": run.flow must be a non-empty string (or use run.command): ${path}`);
|
|
7991
8361
|
}
|
|
7992
|
-
if (command === void 0 && !
|
|
8362
|
+
if (command === void 0 && !isNonEmptyString4(run.task)) {
|
|
7993
8363
|
throw configError(`cron trigger "${id}": run.task must be a non-empty string: ${path}`);
|
|
7994
8364
|
}
|
|
7995
8365
|
if (run.github !== void 0 && typeof run.github !== "boolean") {
|
|
@@ -8001,6 +8371,9 @@ function normalizeCron(on, run, index, path, state) {
|
|
|
8001
8371
|
validateInstructions(run, `cron trigger "${id}"`, path, { cron: true });
|
|
8002
8372
|
const resume = validateResumeFlag(run, `cron trigger "${id}"`, path);
|
|
8003
8373
|
validateReplicas(run, `cron trigger "${id}"`, path);
|
|
8374
|
+
validateNumber(on, `cron trigger "${id}"`, path, { onType: "cron" });
|
|
8375
|
+
validateOnce(on, run, `cron trigger "${id}"`, path, { onType: "cron" });
|
|
8376
|
+
validateDisarmed(on, `cron trigger "${id}"`, path, { onType: "cron" });
|
|
8004
8377
|
const secrets = validateSecrets(run, `cron trigger "${id}"`, path);
|
|
8005
8378
|
const secretsProfile = validateSecretsProfile(run, `cron trigger "${id}"`, path);
|
|
8006
8379
|
return {
|
|
@@ -8070,7 +8443,7 @@ function validateCommand(run, at, path, { onType }) {
|
|
|
8070
8443
|
throw configError(`${at}: exactly one of run.flow or run.command must be set -- a trigger dispatches either a flow or a registered command, and with both present the file does not say which one runs: ${path}`);
|
|
8071
8444
|
}
|
|
8072
8445
|
if (raw === void 0) return void 0;
|
|
8073
|
-
if (!
|
|
8446
|
+
if (!isNonEmptyString4(raw)) {
|
|
8074
8447
|
throw configError(`${at}: run.command must be a non-empty string -- the registered command name, optionally followed by its arguments: ${path}`);
|
|
8075
8448
|
}
|
|
8076
8449
|
if (raw !== raw.trim()) {
|
|
@@ -8107,6 +8480,72 @@ function validatePredicate(on, index, path, requirePositive) {
|
|
|
8107
8480
|
}
|
|
8108
8481
|
return { any: on.any, all: on.all, none: on.none };
|
|
8109
8482
|
}
|
|
8483
|
+
function validateNumber(on, at, path, { capable = false, onType } = {}) {
|
|
8484
|
+
const number = on.number;
|
|
8485
|
+
if (number === void 0) return void 0;
|
|
8486
|
+
if (!capable) {
|
|
8487
|
+
if (onType === "cron") {
|
|
8488
|
+
throw configError(`${at}: on.number is not available on a cron trigger -- a schedule fires on time, not on an item, so there is no delivery for a number to narrow: ${path}`);
|
|
8489
|
+
}
|
|
8490
|
+
throw configError(`${at}: on.number is not yet covered for ${onType} triggers here -- only the close routes read it, so on a rule they never serve it would sit in the file looking configured; narrowing labels, comments or non-close pull_request rules is a gap to close, not a limit: ${path}`);
|
|
8491
|
+
}
|
|
8492
|
+
if (!Number.isInteger(number) || number < 1) {
|
|
8493
|
+
throw configError(`${at}: on.number must be an integer >= 1 when present -- the item number the forge itself assigns (on GitLab, the iid) (got ${JSON.stringify(number)}): ${path}`);
|
|
8494
|
+
}
|
|
8495
|
+
return number;
|
|
8496
|
+
}
|
|
8497
|
+
function validateOnce(on, run, at, path, { capable = false, onType } = {}) {
|
|
8498
|
+
const once = on.once;
|
|
8499
|
+
if (once === void 0) return void 0;
|
|
8500
|
+
if (!capable) {
|
|
8501
|
+
if (onType === "cron") {
|
|
8502
|
+
throw configError(`${at}: on.once is not available on a cron trigger -- a one-shot that can re-arm is a schedule, and a cron job carries no matched delivery for the disarm to name; delete the entry when the work is done: ${path}`);
|
|
8503
|
+
}
|
|
8504
|
+
const closeWords = Object.entries(PR_CLOSE_ACTIONS).map(([k, w]) => `${k} has ${JSON.stringify(w)}`).join(", ");
|
|
8505
|
+
throw configError(`${at}: on.once is not yet covered here -- the disarm writes back to the one entry a delivery matched, and only a close delivery names the single item that spends it; use on.type "issue", or a pull_request rule whose only action is the close word (${closeWords}; azure has no close trigger yet): ${path}`);
|
|
8506
|
+
}
|
|
8507
|
+
if (typeof once !== "boolean") {
|
|
8508
|
+
throw configError(`${at}: on.once must be true or false when present -- a truthy string arming a one-shot is exactly the drift this validator exists to refuse (got ${JSON.stringify(once)}): ${path}`);
|
|
8509
|
+
}
|
|
8510
|
+
if (once === true && on.number === void 0) {
|
|
8511
|
+
throw configError(`${at}: on.once requires on.number -- a one-shot without a named item is spent by whichever close arrives first, and two different items closing inside one dedup window would race for it; the number is also the identity the disarm re-checks before it writes: ${path}`);
|
|
8512
|
+
}
|
|
8513
|
+
if (once === true && run.replicas !== void 0) {
|
|
8514
|
+
throw configError(`${at}: on.once and run.replicas cannot be combined -- "exactly one run" and "N sandboxes race" contradict, and with N run records the disarm no longer says which one spent the trigger: ${path}`);
|
|
8515
|
+
}
|
|
8516
|
+
return once;
|
|
8517
|
+
}
|
|
8518
|
+
function validateDisarmed(on, at, path, { capable = false, onType } = {}) {
|
|
8519
|
+
const disarmed = on.disarmed;
|
|
8520
|
+
if (disarmed === void 0) return void 0;
|
|
8521
|
+
if (!capable) {
|
|
8522
|
+
throw configError(`${at}: on.disarmed marks a spent one-shot, and on.once is not ${onType === "cron" ? "available on a cron trigger" : "yet covered here"}, so there is nothing this entry could have spent: ${path}`);
|
|
8523
|
+
}
|
|
8524
|
+
if (disarmed === null || typeof disarmed !== "object" || Array.isArray(disarmed)) {
|
|
8525
|
+
throw configError(`${at}: on.disarmed must be an object with a non-empty string "at" (and optionally a non-empty string "jobId") -- the worker writes it when the one-shot fires, and a hand-written one disarms the entry deliberately (got ${JSON.stringify(disarmed)}): ${path}`);
|
|
8526
|
+
}
|
|
8527
|
+
for (const key of Object.keys(disarmed)) {
|
|
8528
|
+
if (key !== "at" && key !== "jobId") {
|
|
8529
|
+
throw configError(`${at}: on.disarmed has an unsupported key ${JSON.stringify(key)} (expected at|jobId): ${path}`);
|
|
8530
|
+
}
|
|
8531
|
+
}
|
|
8532
|
+
if (!isNonEmptyString4(disarmed.at)) {
|
|
8533
|
+
throw configError(`${at}: on.disarmed.at must be a non-empty string (the time the one-shot fired): ${path}`);
|
|
8534
|
+
}
|
|
8535
|
+
if (disarmed.jobId !== void 0 && !isNonEmptyString4(disarmed.jobId)) {
|
|
8536
|
+
throw configError(`${at}: on.disarmed.jobId must be a non-empty string when present (the run record it joins to): ${path}`);
|
|
8537
|
+
}
|
|
8538
|
+
if (on.once !== true) {
|
|
8539
|
+
throw configError(`${at}: on.disarmed is only meaningful beside on.once: true -- an entry that was never a one-shot has nothing to spend: ${path}`);
|
|
8540
|
+
}
|
|
8541
|
+
return disarmed;
|
|
8542
|
+
}
|
|
8543
|
+
function validateFlowName(run, at, path) {
|
|
8544
|
+
if (run.flow === void 0) return;
|
|
8545
|
+
if (!SKILL_NAME_RE.test(run.flow)) {
|
|
8546
|
+
throw configError(`${at}: run.flow ${JSON.stringify(run.flow)} fails the skill-name charset (lowercase letters, digits, dash and underscore, 1-64 chars, starting and ending alphanumeric) -- a name outside it can never materialise, so this trigger could only ever fail after the budget slot was reserved: ${path}`);
|
|
8547
|
+
}
|
|
8548
|
+
}
|
|
8110
8549
|
function validateRepository(run, onType, at, path) {
|
|
8111
8550
|
const needed = run.kind === "azure" && (onType === "label" || onType === "comment");
|
|
8112
8551
|
const raw = run.repository;
|
|
@@ -8117,7 +8556,7 @@ function validateRepository(run, onType, at, path) {
|
|
|
8117
8556
|
if (!needed) {
|
|
8118
8557
|
throw configError(`${at}: run.repository is only meaningful on an azure label or comment trigger (a pull request names its own repository): ${path}`);
|
|
8119
8558
|
}
|
|
8120
|
-
if (!
|
|
8559
|
+
if (!isNonEmptyString4(raw) || raw !== raw.trim() || raw.includes("/")) {
|
|
8121
8560
|
throw configError(`${at}: run.repository must be a non-empty repository NAME within the project, with no slashes and no surrounding whitespace (got ${JSON.stringify(raw)}): ${path}`);
|
|
8122
8561
|
}
|
|
8123
8562
|
return raw;
|
|
@@ -8157,7 +8596,7 @@ function validateSecrets(run, at, path) {
|
|
|
8157
8596
|
throw configError(`${at}: run.secrets key ${JSON.stringify(name)} is a variable the worker sets itself -- the job container would receive the worker's value, not this trigger's, and the trigger would look like it worked: ${path}`);
|
|
8158
8597
|
}
|
|
8159
8598
|
const reference = secrets[name];
|
|
8160
|
-
if (!
|
|
8599
|
+
if (!isNonEmptyString4(reference)) {
|
|
8161
8600
|
throw configError(`${at}: run.secrets.${name} must be a non-empty string reference for your resolver to read (got ${JSON.stringify(reference)}): ${path}`);
|
|
8162
8601
|
}
|
|
8163
8602
|
if (reference !== reference.trim()) {
|
|
@@ -8175,7 +8614,7 @@ function validateSecrets(run, at, path) {
|
|
|
8175
8614
|
function validateSecretsProfile(run, at, path) {
|
|
8176
8615
|
const profile = run.secretsProfile;
|
|
8177
8616
|
if (profile === void 0) return void 0;
|
|
8178
|
-
if (!
|
|
8617
|
+
if (!isNonEmptyString4(profile)) {
|
|
8179
8618
|
throw configError(`${at}: run.secretsProfile must be a non-empty string naming one of the resolver profiles this deployment declares (got ${JSON.stringify(profile)}): ${path}`);
|
|
8180
8619
|
}
|
|
8181
8620
|
if (!ID_CHARSET.test(profile)) {
|
|
@@ -8189,10 +8628,14 @@ function validateSecretsProfile(run, at, path) {
|
|
|
8189
8628
|
function normalizeLabel(on, run, index, path) {
|
|
8190
8629
|
const at = `trigger at index ${index}`;
|
|
8191
8630
|
const predicate = validatePredicate(on, index, path, true);
|
|
8631
|
+
validateNumber(on, at, path, { onType: "label" });
|
|
8632
|
+
validateOnce(on, run, at, path, { onType: "label" });
|
|
8633
|
+
validateDisarmed(on, at, path, { onType: "label" });
|
|
8192
8634
|
const command = validateCommand(run, at, path, { onType: "label" });
|
|
8193
|
-
if (command === void 0 && !
|
|
8635
|
+
if (command === void 0 && !isNonEmptyString4(run.flow)) {
|
|
8194
8636
|
throw configError(`${at}: label trigger run.flow must be a non-empty string (or use run.command): ${path}`);
|
|
8195
8637
|
}
|
|
8638
|
+
validateFlowName(run, at, path);
|
|
8196
8639
|
const packages = validatePackagesFlag(run, at, path);
|
|
8197
8640
|
const image = validateImageRef(run, at, path);
|
|
8198
8641
|
const skillsDir = validateSkillsDir(run, at, path);
|
|
@@ -8209,13 +8652,17 @@ function normalizeLabel(on, run, index, path) {
|
|
|
8209
8652
|
}
|
|
8210
8653
|
function normalizeComment(on, run, index, path, state) {
|
|
8211
8654
|
const at = `trigger at index ${index}`;
|
|
8212
|
-
if (!
|
|
8655
|
+
if (!isNonEmptyString4(on.phrase)) {
|
|
8213
8656
|
throw configError(`${at}: comment trigger on.phrase must be a non-empty string: ${path}`);
|
|
8214
8657
|
}
|
|
8658
|
+
validateNumber(on, at, path, { onType: "comment" });
|
|
8659
|
+
validateOnce(on, run, at, path, { onType: "comment" });
|
|
8660
|
+
validateDisarmed(on, at, path, { onType: "comment" });
|
|
8215
8661
|
const command = validateCommand(run, at, path, { onType: "comment" });
|
|
8216
|
-
if (command === void 0 && !
|
|
8662
|
+
if (command === void 0 && !isNonEmptyString4(run.flow)) {
|
|
8217
8663
|
throw configError(`${at}: comment trigger run.flow (the default flow) must be a non-empty string (or use run.command): ${path}`);
|
|
8218
8664
|
}
|
|
8665
|
+
validateFlowName(run, at, path);
|
|
8219
8666
|
state.commentCounts[run.kind] = (state.commentCounts[run.kind] ?? 0) + 1;
|
|
8220
8667
|
if (state.commentCounts[run.kind] > 1) {
|
|
8221
8668
|
throw configError(`${at}: at most one ${run.kind} comment trigger is allowed: ${path}`);
|
|
@@ -8234,6 +8681,54 @@ function normalizeComment(on, run, index, path, state) {
|
|
|
8234
8681
|
run: { kind: run.kind, flow: run.flow, packages, image, resume, replicas, ...command !== void 0 && { command }, ...skillsDir !== void 0 && { skillsDir }, ...instructions !== void 0 && { instructions }, ...repository !== void 0 && { repository }, ...secrets !== void 0 && { secrets }, ...secretsProfile !== void 0 && { secretsProfile } }
|
|
8235
8682
|
};
|
|
8236
8683
|
}
|
|
8684
|
+
function normalizeIssue(on, run, index, path) {
|
|
8685
|
+
const at = `trigger at index ${index}`;
|
|
8686
|
+
if (run.kind === "azure") {
|
|
8687
|
+
throw configError(`${at}: an issue trigger is not yet covered for azure -- a work item's close is a System.State transition whose terminal names vary by process template (Agile "Closed", Scrum "Done"), and the projected subset carries only System.Tags, so nothing in the delivery says the item closed; widening the payload subset is the gap to close, not a limit: ${path}`);
|
|
8688
|
+
}
|
|
8689
|
+
const actions = on.action;
|
|
8690
|
+
if (!Array.isArray(actions) || actions.length === 0) {
|
|
8691
|
+
throw configError(`${at}: issue on.action must be a non-empty array: ${path}`);
|
|
8692
|
+
}
|
|
8693
|
+
const allowed = ISSUE_ACTIONS[run.kind];
|
|
8694
|
+
const expected = [...allowed].join("|");
|
|
8695
|
+
for (const a of actions) {
|
|
8696
|
+
if (!allowed.has(a)) {
|
|
8697
|
+
throw configError(`${at}: issue on.action has an unsupported ${run.kind} action ${JSON.stringify(a)} (expected ${expected}): ${path}`);
|
|
8698
|
+
}
|
|
8699
|
+
}
|
|
8700
|
+
if (on.any !== void 0 || on.all !== void 0 || on.none !== void 0) {
|
|
8701
|
+
throw configError(`${at}: an issue trigger cannot carry a label predicate -- it fires on a lifecycle action, not a label diff, so any/all/none could never match; a label-predicated rule is on.type "label": ${path}`);
|
|
8702
|
+
}
|
|
8703
|
+
const number = validateNumber(on, at, path, { capable: true, onType: "issue" });
|
|
8704
|
+
const once = validateOnce(on, run, at, path, { capable: true, onType: "issue" });
|
|
8705
|
+
validateDisarmed(on, at, path, { capable: true, onType: "issue" });
|
|
8706
|
+
const command = validateCommand(run, at, path, { onType: "issue" });
|
|
8707
|
+
if (command === void 0 && !isNonEmptyString4(run.flow)) {
|
|
8708
|
+
throw configError(`${at}: issue trigger run.flow must be a non-empty string (or use run.command): ${path}`);
|
|
8709
|
+
}
|
|
8710
|
+
validateFlowName(run, at, path);
|
|
8711
|
+
const packages = validatePackagesFlag(run, at, path);
|
|
8712
|
+
const image = validateImageRef(run, at, path);
|
|
8713
|
+
const skillsDir = validateSkillsDir(run, at, path);
|
|
8714
|
+
const instructions = validateInstructions(run, at, path);
|
|
8715
|
+
const resume = validateResumeFlag(run, at, path);
|
|
8716
|
+
validateRepository(run, "issue", at, path);
|
|
8717
|
+
const replicas = validateReplicas(run, at, path);
|
|
8718
|
+
const secrets = validateSecrets(run, at, path);
|
|
8719
|
+
const secretsProfile = validateSecretsProfile(run, at, path);
|
|
8720
|
+
return {
|
|
8721
|
+
on: {
|
|
8722
|
+
type: "issue",
|
|
8723
|
+
action: [...actions],
|
|
8724
|
+
// Absent rather than present-and-undefined, reviewState's rule below: an unnarrowed rule's
|
|
8725
|
+
// normalized shape must not grow keys.
|
|
8726
|
+
...number !== void 0 && { number },
|
|
8727
|
+
...once !== void 0 && { once }
|
|
8728
|
+
},
|
|
8729
|
+
run: { kind: run.kind, flow: run.flow, packages, image, resume, replicas, ...command !== void 0 && { command }, ...skillsDir !== void 0 && { skillsDir }, ...instructions !== void 0 && { instructions }, ...secrets !== void 0 && { secrets }, ...secretsProfile !== void 0 && { secretsProfile } }
|
|
8730
|
+
};
|
|
8731
|
+
}
|
|
8237
8732
|
function normalizePullRequest(on, run, index, path) {
|
|
8238
8733
|
const at = `trigger at index ${index}`;
|
|
8239
8734
|
const actions = on.action;
|
|
@@ -8247,17 +8742,29 @@ function normalizePullRequest(on, run, index, path) {
|
|
|
8247
8742
|
throw configError(`${at}: pull_request on.action has an unsupported ${run.kind} action ${JSON.stringify(a)} (expected ${expected}): ${path}`);
|
|
8248
8743
|
}
|
|
8249
8744
|
}
|
|
8745
|
+
const closeWord = PR_CLOSE_ACTIONS[run.kind];
|
|
8746
|
+
const isClose = closeWord !== void 0 && actions.includes(closeWord);
|
|
8747
|
+
if (isClose && actions.some((a) => a !== closeWord)) {
|
|
8748
|
+
throw configError(`${at}: pull_request on.action cannot mix ${JSON.stringify(closeWord)} with other actions -- a close rule is gated on the closer's write access and every other action on the author's, and one rule cannot gate on two different actors; split it into two entries: ${path}`);
|
|
8749
|
+
}
|
|
8250
8750
|
const reviewState = validateReviewState(on, actions, run, at, path);
|
|
8251
8751
|
const labelAction = forgeSpec(run.kind)?.prLabelAction;
|
|
8252
8752
|
const requirePositive = typeof labelAction === "string" && actions.includes(labelAction);
|
|
8253
8753
|
if (run.kind === "azure" && (on.any !== void 0 || on.all !== void 0 || on.none !== void 0)) {
|
|
8254
8754
|
throw configError(`${at}: an azure pull_request trigger cannot carry a label predicate -- Azure DevOps attaches tags to work items, never to pull requests, so any/all/none could never match: ${path}`);
|
|
8255
8755
|
}
|
|
8756
|
+
if (isClose && (on.any !== void 0 || on.all !== void 0 || on.none !== void 0)) {
|
|
8757
|
+
throw configError(`${at}: a close pull_request rule cannot carry a label predicate -- the close route matches the action and on.number alone, so any/all/none would sit in the file looking configured and never match anything: ${path}`);
|
|
8758
|
+
}
|
|
8256
8759
|
const predicate = validatePredicate(on, index, path, requirePositive);
|
|
8760
|
+
const number = validateNumber(on, at, path, { capable: isClose, onType: "pull_request" });
|
|
8761
|
+
const once = validateOnce(on, run, at, path, { capable: isClose, onType: "pull_request" });
|
|
8762
|
+
validateDisarmed(on, at, path, { capable: isClose, onType: "pull_request" });
|
|
8257
8763
|
const command = validateCommand(run, at, path, { onType: "pull_request" });
|
|
8258
|
-
if (command === void 0 && !
|
|
8764
|
+
if (command === void 0 && !isNonEmptyString4(run.flow)) {
|
|
8259
8765
|
throw configError(`${at}: pull_request trigger run.flow must be a non-empty string (or use run.command): ${path}`);
|
|
8260
8766
|
}
|
|
8767
|
+
validateFlowName(run, at, path);
|
|
8261
8768
|
const packages = validatePackagesFlag(run, at, path);
|
|
8262
8769
|
const image = validateImageRef(run, at, path);
|
|
8263
8770
|
const skillsDir = validateSkillsDir(run, at, path);
|
|
@@ -8276,7 +8783,10 @@ function normalizePullRequest(on, run, index, path) {
|
|
|
8276
8783
|
...reviewState !== void 0 && { reviewState },
|
|
8277
8784
|
any: predicate.any,
|
|
8278
8785
|
all: predicate.all,
|
|
8279
|
-
none: predicate.none
|
|
8786
|
+
none: predicate.none,
|
|
8787
|
+
// Same rule for the close-narrowing pair (issue #231): only a close-only rule can carry them.
|
|
8788
|
+
...number !== void 0 && { number },
|
|
8789
|
+
...once !== void 0 && { once }
|
|
8280
8790
|
},
|
|
8281
8791
|
run: { kind: run.kind, flow: run.flow, packages, image, resume, replicas, ...command !== void 0 && { command }, ...skillsDir !== void 0 && { skillsDir }, ...instructions !== void 0 && { instructions }, ...secrets !== void 0 && { secrets }, ...secretsProfile !== void 0 && { secretsProfile } }
|
|
8282
8792
|
};
|
|
@@ -8300,30 +8810,41 @@ function validateReviewState(on, actions, run, at, path) {
|
|
|
8300
8810
|
}
|
|
8301
8811
|
return [...on.reviewState];
|
|
8302
8812
|
}
|
|
8303
|
-
var ON_TYPES, PR_ACTIONS, REVIEW_STATES, REVIEW_ACTION, ID_CHARSET, REPLICAS_MAX, SECRETS_MAX, ENV_NAME, RESERVED_ENV_NAMES, INSTRUCTIONS_MAX;
|
|
8813
|
+
var ON_TYPES, DISARMED_TYPE, PR_ACTIONS, PR_CLOSE_ACTIONS, ISSUE_ACTIONS, REVIEW_STATES, REVIEW_ACTION, ID_CHARSET, REPLICAS_MAX, SECRETS_MAX, ENV_NAME, RESERVED_ENV_NAMES, INSTRUCTIONS_MAX;
|
|
8304
8814
|
var init_triggers = __esm({
|
|
8305
8815
|
"../worker/src/triggers.mjs"() {
|
|
8306
8816
|
init_config2();
|
|
8817
|
+
init_flow_gate();
|
|
8307
8818
|
init_forges();
|
|
8308
8819
|
init_reserved_env();
|
|
8309
|
-
ON_TYPES = /* @__PURE__ */ new Set(["cron", "label", "comment", "pull_request"]);
|
|
8820
|
+
ON_TYPES = /* @__PURE__ */ new Set(["cron", "label", "comment", "pull_request", "issue"]);
|
|
8821
|
+
DISARMED_TYPE = "disarmed";
|
|
8310
8822
|
PR_ACTIONS = {
|
|
8311
|
-
github: /* @__PURE__ */ new Set(["labeled", "opened", "synchronize", "reopened", "review_submitted"]),
|
|
8823
|
+
github: /* @__PURE__ */ new Set(["labeled", "opened", "synchronize", "reopened", "review_submitted", "closed"]),
|
|
8312
8824
|
// GitLab's `approved` is its review gate (a member approved the MR). It is NOT github's
|
|
8313
8825
|
// `review_submitted` renamed: `approved` is one verdict, `review_submitted` is every verdict, which is
|
|
8314
|
-
// what `on.reviewState` below exists to narrow.
|
|
8315
|
-
gitlab: /* @__PURE__ */ new Set(["open", "update", "reopen", "approved"]),
|
|
8826
|
+
// what `on.reviewState` below exists to narrow. `close` is GitLab's own spelling of the close action.
|
|
8827
|
+
gitlab: /* @__PURE__ */ new Set(["open", "update", "reopen", "approved", "close"]),
|
|
8316
8828
|
// Forgejo's own spellings. `label_updated` is its `labeled` and `synchronized` its `synchronize` -- a
|
|
8317
8829
|
// one-letter difference that an operator would otherwise discover as a trigger that loads clean and
|
|
8318
8830
|
// never fires. `label_cleared` is deliberately ABSENT and always will be: REMOVING a label must never
|
|
8319
8831
|
// start a paid run, and it has no GitHub counterpart to inherit that rule from.
|
|
8320
|
-
forgejo: /* @__PURE__ */ new Set(["label_updated", "opened", "synchronized", "reopened"]),
|
|
8832
|
+
forgejo: /* @__PURE__ */ new Set(["label_updated", "opened", "synchronized", "reopened", "closed"]),
|
|
8321
8833
|
// Azure's Service Hook events reduced to the two that leave something to act on. `git.pullrequest.merged`
|
|
8322
|
-
// is omitted for the
|
|
8323
|
-
//
|
|
8324
|
-
//
|
|
8834
|
+
// is omitted for the reason the close words' own comment above records, and there is no close word here at
|
|
8835
|
+
// all: an abandon arrives as `git.pullrequest.updated` with nothing in the projected subset to tell it from
|
|
8836
|
+
// any other update, so an azure close rule could only ever fire on the wrong events or never. Widening
|
|
8837
|
+
// INT-AZURE-PAYLOAD-SUBSET (a PR status field) is the gap to close before this set can grow. There is no
|
|
8838
|
+
// label action either -- Azure attaches tags to WORK ITEMS, never to pull requests -- which is why azure's
|
|
8839
|
+
// `prLabelAction` is null and a predicated PR rule is refused below.
|
|
8325
8840
|
azure: /* @__PURE__ */ new Set(["created", "updated"])
|
|
8326
8841
|
};
|
|
8842
|
+
PR_CLOSE_ACTIONS = { github: "closed", gitlab: "close", forgejo: "closed" };
|
|
8843
|
+
ISSUE_ACTIONS = {
|
|
8844
|
+
github: /* @__PURE__ */ new Set(["closed"]),
|
|
8845
|
+
gitlab: /* @__PURE__ */ new Set(["close"]),
|
|
8846
|
+
forgejo: /* @__PURE__ */ new Set(["closed"])
|
|
8847
|
+
};
|
|
8327
8848
|
REVIEW_STATES = /* @__PURE__ */ new Set(["approved", "changes_requested", "commented"]);
|
|
8328
8849
|
REVIEW_ACTION = "review_submitted";
|
|
8329
8850
|
ID_CHARSET = /^[A-Za-z0-9._-]+$/;
|
|
@@ -8335,329 +8856,6 @@ var init_triggers = __esm({
|
|
|
8335
8856
|
}
|
|
8336
8857
|
});
|
|
8337
8858
|
|
|
8338
|
-
// ../worker/src/pause-windows.mjs
|
|
8339
|
-
function isNonEmptyString3(value) {
|
|
8340
|
-
return typeof value === "string" && value.trim() !== "";
|
|
8341
|
-
}
|
|
8342
|
-
function parseHHMM(value, label, at, path) {
|
|
8343
|
-
const m = isNonEmptyString3(value) ? /^([0-9]{1,2}):([0-9]{2})$/.exec(value.trim()) : null;
|
|
8344
|
-
if (!m) throw configError(`${at}: ${label} must be "HH:MM": ${JSON.stringify(value)} (${path})`);
|
|
8345
|
-
const h = Number(m[1]);
|
|
8346
|
-
const min = Number(m[2]);
|
|
8347
|
-
if (h > 23 || min > 59) throw configError(`${at}: ${label} out of range 00:00-23:59: ${JSON.stringify(value)} (${path})`);
|
|
8348
|
-
return h * 60 + min;
|
|
8349
|
-
}
|
|
8350
|
-
function assertDate(value, label, at, path) {
|
|
8351
|
-
const m = /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.exec(String(value).trim());
|
|
8352
|
-
const mo = m && Number(m[2]);
|
|
8353
|
-
const d = m && Number(m[3]);
|
|
8354
|
-
if (!m || mo < 1 || mo > 12 || d < 1 || d > 31) {
|
|
8355
|
-
throw configError(`${at}: ${label} must be "YYYY-MM-DD": ${JSON.stringify(value)} (${path})`);
|
|
8356
|
-
}
|
|
8357
|
-
}
|
|
8358
|
-
function validateTz(tz, at, path) {
|
|
8359
|
-
try {
|
|
8360
|
-
new Intl.DateTimeFormat("en-US", { timeZone: tz });
|
|
8361
|
-
return tz;
|
|
8362
|
-
} catch {
|
|
8363
|
-
throw configError(`${at}: tz is not a valid IANA timezone: ${JSON.stringify(tz)} (${path})`);
|
|
8364
|
-
}
|
|
8365
|
-
}
|
|
8366
|
-
function parsePauseWindows(text, path) {
|
|
8367
|
-
let parsed;
|
|
8368
|
-
try {
|
|
8369
|
-
parsed = JSON.parse(text);
|
|
8370
|
-
} catch (error) {
|
|
8371
|
-
throw configError(`pause-windows file is not valid JSON: ${path} (${error.message})`);
|
|
8372
|
-
}
|
|
8373
|
-
const windows = parsed?.windows;
|
|
8374
|
-
if (!Array.isArray(windows)) {
|
|
8375
|
-
throw configError(`pause-windows file must have a "windows" array: ${path}`);
|
|
8376
|
-
}
|
|
8377
|
-
return windows.map((w, index) => normalizeWindow(w, index, path));
|
|
8378
|
-
}
|
|
8379
|
-
function normalizeWindow(w, index, path) {
|
|
8380
|
-
const at = `pause window at index ${index}`;
|
|
8381
|
-
if (w === null || typeof w !== "object") throw configError(`${at}: must be an object: ${path}`);
|
|
8382
|
-
if (!isNonEmptyString3(w.scope)) throw configError(`${at}: scope must be a non-empty string: ${path}`);
|
|
8383
|
-
const fromMin = parseHHMM(w.from, "from", at, path);
|
|
8384
|
-
const toMin = parseHHMM(w.to, "to", at, path);
|
|
8385
|
-
if (fromMin === toMin) {
|
|
8386
|
-
throw configError(`${at}: from and to must differ (a 24h pause is not expressible; remove the trigger): ${path}`);
|
|
8387
|
-
}
|
|
8388
|
-
const tz = w.tz === void 0 ? "UTC" : validateTz(w.tz, at, path);
|
|
8389
|
-
let days = null;
|
|
8390
|
-
if (w.days !== void 0) {
|
|
8391
|
-
if (!Array.isArray(w.days) || w.days.length === 0) {
|
|
8392
|
-
throw configError(`${at}: days must be a non-empty array of weekday names (mon..sun): ${path}`);
|
|
8393
|
-
}
|
|
8394
|
-
days = w.days.map((d) => {
|
|
8395
|
-
const key = String(d).trim().toLowerCase().slice(0, 3);
|
|
8396
|
-
if (!DAY_SET.has(key)) throw configError(`${at}: unknown weekday ${JSON.stringify(d)} (use mon..sun): ${path}`);
|
|
8397
|
-
return key;
|
|
8398
|
-
});
|
|
8399
|
-
}
|
|
8400
|
-
let dateFrom = null;
|
|
8401
|
-
let dateTo = null;
|
|
8402
|
-
if (w.dateFrom !== void 0) {
|
|
8403
|
-
assertDate(w.dateFrom, "dateFrom", at, path);
|
|
8404
|
-
dateFrom = String(w.dateFrom).trim();
|
|
8405
|
-
}
|
|
8406
|
-
if (w.dateTo !== void 0) {
|
|
8407
|
-
assertDate(w.dateTo, "dateTo", at, path);
|
|
8408
|
-
dateTo = String(w.dateTo).trim();
|
|
8409
|
-
}
|
|
8410
|
-
if (dateFrom && dateTo && dateFrom > dateTo) throw configError(`${at}: dateFrom must be <= dateTo: ${path}`);
|
|
8411
|
-
const norm = { scope: w.scope.trim(), from: w.from.trim(), to: w.to.trim(), fromMin, toMin, tz };
|
|
8412
|
-
if (days) norm.days = days;
|
|
8413
|
-
if (dateFrom) norm.dateFrom = dateFrom;
|
|
8414
|
-
if (dateTo) norm.dateTo = dateTo;
|
|
8415
|
-
return norm;
|
|
8416
|
-
}
|
|
8417
|
-
function zonedParts(tz, ms) {
|
|
8418
|
-
const fmt2 = new Intl.DateTimeFormat("en-US", {
|
|
8419
|
-
timeZone: tz,
|
|
8420
|
-
hour12: false,
|
|
8421
|
-
year: "numeric",
|
|
8422
|
-
month: "2-digit",
|
|
8423
|
-
day: "2-digit",
|
|
8424
|
-
hour: "2-digit",
|
|
8425
|
-
minute: "2-digit",
|
|
8426
|
-
weekday: "short"
|
|
8427
|
-
});
|
|
8428
|
-
const p = {};
|
|
8429
|
-
for (const part of fmt2.formatToParts(new Date(ms))) p[part.type] = part.value;
|
|
8430
|
-
let hh = Number(p.hour);
|
|
8431
|
-
if (hh === 24) hh = 0;
|
|
8432
|
-
return { y: Number(p.year), mo: Number(p.month), d: Number(p.day), hh, mi: Number(p.minute), dow: WEEKDAY_INDEX[p.weekday] };
|
|
8433
|
-
}
|
|
8434
|
-
function wallToMs(tz, y, mo, d, hh, mi) {
|
|
8435
|
-
const guess = Date.UTC(y, mo - 1, d, hh, mi);
|
|
8436
|
-
const shown = zonedParts(tz, guess);
|
|
8437
|
-
const shownAsUtc = Date.UTC(shown.y, shown.mo - 1, shown.d, shown.hh, shown.mi);
|
|
8438
|
-
return guess - (shownAsUtc - guess);
|
|
8439
|
-
}
|
|
8440
|
-
function shiftDay(tz, parts, delta) {
|
|
8441
|
-
const noon = wallToMs(tz, parts.y, parts.mo, parts.d, 12, 0);
|
|
8442
|
-
return zonedParts(tz, noon + delta * 864e5);
|
|
8443
|
-
}
|
|
8444
|
-
function ymd(parts) {
|
|
8445
|
-
return `${String(parts.y).padStart(4, "0")}-${String(parts.mo).padStart(2, "0")}-${String(parts.d).padStart(2, "0")}`;
|
|
8446
|
-
}
|
|
8447
|
-
function qualifies(w, parts) {
|
|
8448
|
-
if (w.days && !w.days.includes(DAYS[parts.dow])) return false;
|
|
8449
|
-
const date = ymd(parts);
|
|
8450
|
-
if (w.dateFrom && date < w.dateFrom) return false;
|
|
8451
|
-
if (w.dateTo && date > w.dateTo) return false;
|
|
8452
|
-
return true;
|
|
8453
|
-
}
|
|
8454
|
-
function windowEndAt(w, nowMs) {
|
|
8455
|
-
const p = zonedParts(w.tz, nowMs);
|
|
8456
|
-
const nowMin = p.hh * 60 + p.mi;
|
|
8457
|
-
const endAt = (dateParts) => wallToMs(w.tz, dateParts.y, dateParts.mo, dateParts.d, Math.floor(w.toMin / 60), w.toMin % 60);
|
|
8458
|
-
if (w.fromMin < w.toMin) {
|
|
8459
|
-
if (nowMin >= w.fromMin && nowMin < w.toMin && qualifies(w, p)) return endAt(p);
|
|
8460
|
-
return null;
|
|
8461
|
-
}
|
|
8462
|
-
if (nowMin >= w.fromMin && qualifies(w, p)) return endAt(shiftDay(w.tz, p, 1));
|
|
8463
|
-
if (nowMin < w.toMin && qualifies(w, shiftDay(w.tz, p, -1))) return endAt(p);
|
|
8464
|
-
return null;
|
|
8465
|
-
}
|
|
8466
|
-
var DAYS, DAY_SET, WEEKDAY_INDEX;
|
|
8467
|
-
var init_pause_windows = __esm({
|
|
8468
|
-
"../worker/src/pause-windows.mjs"() {
|
|
8469
|
-
init_config2();
|
|
8470
|
-
DAYS = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
|
|
8471
|
-
DAY_SET = new Set(DAYS);
|
|
8472
|
-
WEEKDAY_INDEX = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 };
|
|
8473
|
-
}
|
|
8474
|
-
});
|
|
8475
|
-
|
|
8476
|
-
// ../worker/src/subscriptions.mjs
|
|
8477
|
-
function isNonEmptyString4(value) {
|
|
8478
|
-
return typeof value === "string" && value.trim() !== "";
|
|
8479
|
-
}
|
|
8480
|
-
function normalizeGlobs(value, label, at, path) {
|
|
8481
|
-
if (!Array.isArray(value) || value.length === 0) {
|
|
8482
|
-
throw configError(`${at}: ${label} must be a non-empty array of model globs: ${path}`);
|
|
8483
|
-
}
|
|
8484
|
-
return value.map((glob) => {
|
|
8485
|
-
if (typeof glob !== "string" || !MODEL_GLOB_RE.test(glob.trim())) {
|
|
8486
|
-
throw configError(`${at}: ${label} glob ${JSON.stringify(glob)} must match ${MODEL_GLOB_RE} (model-id charset plus "*", max 64 chars): ${path}`);
|
|
8487
|
-
}
|
|
8488
|
-
return glob.trim();
|
|
8489
|
-
});
|
|
8490
|
-
}
|
|
8491
|
-
function normalizeBool(value, label, at, path, fallback) {
|
|
8492
|
-
if (value === void 0) return fallback;
|
|
8493
|
-
if (typeof value !== "boolean") throw configError(`${at}: ${label} must be a boolean: ${path}`);
|
|
8494
|
-
return value;
|
|
8495
|
-
}
|
|
8496
|
-
function parseSubscriptions(text, path) {
|
|
8497
|
-
let parsed;
|
|
8498
|
-
try {
|
|
8499
|
-
parsed = JSON.parse(text);
|
|
8500
|
-
} catch (error) {
|
|
8501
|
-
throw configError(`subscriptions file is not valid JSON: ${path} (${error.message})`);
|
|
8502
|
-
}
|
|
8503
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
8504
|
-
throw configError(`subscriptions file must be a JSON object: ${path}`);
|
|
8505
|
-
}
|
|
8506
|
-
const version = parsed.version;
|
|
8507
|
-
if (!Number.isInteger(version) || version < 1) {
|
|
8508
|
-
throw configError(`subscriptions file must declare an integer "version" >= 1 (got ${JSON.stringify(version)}): ${path}`);
|
|
8509
|
-
}
|
|
8510
|
-
if (version > SUBSCRIPTIONS_VERSION) {
|
|
8511
|
-
throw configError(`subscriptions file written by a newer pi-dispatch (version ${version}; this build understands ${SUBSCRIPTIONS_VERSION}): ${path}`);
|
|
8512
|
-
}
|
|
8513
|
-
const entries = parsed.subscriptions;
|
|
8514
|
-
if (!Array.isArray(entries)) {
|
|
8515
|
-
throw configError(`subscriptions file must have a "subscriptions" array: ${path}`);
|
|
8516
|
-
}
|
|
8517
|
-
const seenIds = /* @__PURE__ */ new Map();
|
|
8518
|
-
return { version, subscriptions: entries.map((entry, index) => normalizeSubscription(entry, index, path, seenIds)) };
|
|
8519
|
-
}
|
|
8520
|
-
function normalizeSubscription(entry, index, path, seenIds) {
|
|
8521
|
-
const at = `subscription at index ${index}`;
|
|
8522
|
-
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
|
|
8523
|
-
throw configError(`${at}: must be an object: ${path}`);
|
|
8524
|
-
}
|
|
8525
|
-
if (!isNonEmptyString4(entry.id)) throw configError(`${at}: id must be a non-empty string: ${path}`);
|
|
8526
|
-
const id = entry.id.trim();
|
|
8527
|
-
if (seenIds.has(id)) {
|
|
8528
|
-
throw configError(`${at}: id ${JSON.stringify(id)} is already used by the subscription at index ${seenIds.get(id)}: ${path}`);
|
|
8529
|
-
}
|
|
8530
|
-
seenIds.set(id, index);
|
|
8531
|
-
if (!isNonEmptyString4(entry.vendor)) throw configError(`${at}: vendor must be a non-empty string: ${path}`);
|
|
8532
|
-
if (!isNonEmptyString4(entry.provider)) throw configError(`${at}: provider must be a non-empty string (the pi-ai provider id this plan covers): ${path}`);
|
|
8533
|
-
const models = entry.models === void 0 ? ["*"] : normalizeGlobs(entry.models, "models", at, path);
|
|
8534
|
-
const price = entry.price;
|
|
8535
|
-
if (price === null || typeof price !== "object" || Array.isArray(price)) {
|
|
8536
|
-
throw configError(`${at}: price must be an object { amount, currency, per }: ${path}`);
|
|
8537
|
-
}
|
|
8538
|
-
if (typeof price.amount !== "number" || !Number.isFinite(price.amount) || price.amount <= 0) {
|
|
8539
|
-
throw configError(`${at}: price.amount must be a number > 0: ${path}`);
|
|
8540
|
-
}
|
|
8541
|
-
if (typeof price.currency !== "string" || !/^[A-Z]{3}$/.test(price.currency)) {
|
|
8542
|
-
throw configError(`${at}: price.currency must be a 3-letter uppercase code (e.g. "USD"): ${path}`);
|
|
8543
|
-
}
|
|
8544
|
-
if (price.per !== "month") {
|
|
8545
|
-
throw configError(`${at}: price.per must be "month" (got ${JSON.stringify(price.per)}): ${path}`);
|
|
8546
|
-
}
|
|
8547
|
-
const sharedWithOtherProducts = normalizeBool(entry.sharedWithOtherProducts, "sharedWithOtherProducts", at, path, false);
|
|
8548
|
-
const hypothetical = normalizeBool(entry.hypothetical, "hypothetical", at, path, false);
|
|
8549
|
-
let counterfactualModel = null;
|
|
8550
|
-
if (entry.counterfactualModel !== void 0 && entry.counterfactualModel !== null) {
|
|
8551
|
-
const cm = entry.counterfactualModel;
|
|
8552
|
-
if (typeof cm !== "object" || Array.isArray(cm) || !isNonEmptyString4(cm.provider) || !isNonEmptyString4(cm.id)) {
|
|
8553
|
-
throw configError(`${at}: counterfactualModel must be { provider, id } with non-empty strings (a PRICED pi-ai model to re-price covered runs at API rates): ${path}`);
|
|
8554
|
-
}
|
|
8555
|
-
counterfactualModel = { provider: cm.provider.trim(), id: cm.id.trim() };
|
|
8556
|
-
}
|
|
8557
|
-
let windows = [];
|
|
8558
|
-
if (entry.windows !== void 0) {
|
|
8559
|
-
if (!Array.isArray(entry.windows)) throw configError(`${at}: windows must be an array: ${path}`);
|
|
8560
|
-
windows = entry.windows.map((w, windowIndex) => normalizeWindow2(w, windowIndex, index, path));
|
|
8561
|
-
}
|
|
8562
|
-
return {
|
|
8563
|
-
id,
|
|
8564
|
-
vendor: entry.vendor.trim(),
|
|
8565
|
-
provider: entry.provider.trim(),
|
|
8566
|
-
models,
|
|
8567
|
-
price: { amount: price.amount, currency: price.currency, per: "month" },
|
|
8568
|
-
sharedWithOtherProducts,
|
|
8569
|
-
hypothetical,
|
|
8570
|
-
counterfactualModel,
|
|
8571
|
-
windows
|
|
8572
|
-
};
|
|
8573
|
-
}
|
|
8574
|
-
function normalizeWindow2(w, windowIndex, subIndex, path) {
|
|
8575
|
-
const at = `window at index ${windowIndex} of subscription at index ${subIndex}`;
|
|
8576
|
-
if (w === null || typeof w !== "object" || Array.isArray(w)) {
|
|
8577
|
-
throw configError(`${at}: must be an object: ${path}`);
|
|
8578
|
-
}
|
|
8579
|
-
if (!WINDOW_PERS.has(w.per)) {
|
|
8580
|
-
throw configError(`${at}: per must be one of 5h|7d|30d|month (got ${JSON.stringify(w.per)}): ${path}`);
|
|
8581
|
-
}
|
|
8582
|
-
const rolling = normalizeBool(w.rolling, "rolling", at, path, false);
|
|
8583
|
-
let unit = null;
|
|
8584
|
-
if (w.unit !== void 0 && w.unit !== null) {
|
|
8585
|
-
if (!WINDOW_UNITS.has(w.unit)) {
|
|
8586
|
-
throw configError(`${at}: unit must be one of tokens|requests|prompts|credits, or null (vendor undisclosed): ${path}`);
|
|
8587
|
-
}
|
|
8588
|
-
unit = w.unit;
|
|
8589
|
-
}
|
|
8590
|
-
let limit = null;
|
|
8591
|
-
if (w.limit !== void 0 && w.limit !== null) {
|
|
8592
|
-
if (typeof w.limit === "number") {
|
|
8593
|
-
if (!Number.isFinite(w.limit) || w.limit <= 0) throw configError(`${at}: limit must be a number > 0, [min, max], or null (vendor undisclosed): ${path}`);
|
|
8594
|
-
limit = w.limit;
|
|
8595
|
-
} else if (Array.isArray(w.limit)) {
|
|
8596
|
-
const [min, max] = w.limit;
|
|
8597
|
-
if (w.limit.length !== 2 || typeof min !== "number" || typeof max !== "number" || !Number.isFinite(min) || !Number.isFinite(max) || min <= 0 || max <= 0 || min >= max) {
|
|
8598
|
-
throw configError(`${at}: limit range must be [min, max] with both > 0 and min < max: ${path}`);
|
|
8599
|
-
}
|
|
8600
|
-
limit = [min, max];
|
|
8601
|
-
} else {
|
|
8602
|
-
throw configError(`${at}: limit must be a number > 0, [min, max], or null (vendor undisclosed): ${path}`);
|
|
8603
|
-
}
|
|
8604
|
-
}
|
|
8605
|
-
const scope = w.scope === void 0 || w.scope === null ? null : normalizeGlobs(w.scope, "scope", at, path);
|
|
8606
|
-
return { per: w.per, rolling, unit, limit, scope };
|
|
8607
|
-
}
|
|
8608
|
-
var SUBSCRIPTIONS_VERSION, WINDOW_PERS, WINDOW_UNITS, MODEL_GLOB_RE;
|
|
8609
|
-
var init_subscriptions = __esm({
|
|
8610
|
-
"../worker/src/subscriptions.mjs"() {
|
|
8611
|
-
init_config2();
|
|
8612
|
-
SUBSCRIPTIONS_VERSION = 1;
|
|
8613
|
-
WINDOW_PERS = /* @__PURE__ */ new Set(["5h", "7d", "30d", "month"]);
|
|
8614
|
-
WINDOW_UNITS = /* @__PURE__ */ new Set(["tokens", "requests", "prompts", "credits"]);
|
|
8615
|
-
MODEL_GLOB_RE = /^[A-Za-z0-9*._:/-]{1,64}$/;
|
|
8616
|
-
}
|
|
8617
|
-
});
|
|
8618
|
-
|
|
8619
|
-
// ../worker/src/connection.mjs
|
|
8620
|
-
import { Redis } from "ioredis";
|
|
8621
|
-
function parseConnection(url, { failFast = false } = {}) {
|
|
8622
|
-
const u = new URL(url);
|
|
8623
|
-
return {
|
|
8624
|
-
host: u.hostname || "127.0.0.1",
|
|
8625
|
-
port: Number(u.port || 6379),
|
|
8626
|
-
...u.password ? { password: u.password } : {},
|
|
8627
|
-
...u.username ? { username: u.username } : {},
|
|
8628
|
-
...u.pathname && u.pathname !== "/" ? { db: Number(u.pathname.slice(1)) } : {},
|
|
8629
|
-
maxRetriesPerRequest: null,
|
|
8630
|
-
// required for BullMQ blocking connections
|
|
8631
|
-
...failFast ? {
|
|
8632
|
-
connectTimeout: 2e3,
|
|
8633
|
-
enableOfflineQueue: false,
|
|
8634
|
-
// don't buffer commands while disconnected -- error now
|
|
8635
|
-
retryStrategy: (attempts) => attempts > 2 ? null : 200
|
|
8636
|
-
// give up after ~2 tries
|
|
8637
|
-
} : {}
|
|
8638
|
-
};
|
|
8639
|
-
}
|
|
8640
|
-
function makeRedisClient(url) {
|
|
8641
|
-
return new Redis(url, { maxRetriesPerRequest: null });
|
|
8642
|
-
}
|
|
8643
|
-
var init_connection = __esm({
|
|
8644
|
-
"../worker/src/connection.mjs"() {
|
|
8645
|
-
}
|
|
8646
|
-
});
|
|
8647
|
-
|
|
8648
|
-
// ../worker/src/job-id.mjs
|
|
8649
|
-
import { createHash } from "node:crypto";
|
|
8650
|
-
function localJobId({ folder, flow, task, minute }) {
|
|
8651
|
-
const digest = createHash("sha256").update([folder, flow ?? "", task ?? "", minute].join("\0")).digest("hex");
|
|
8652
|
-
return `local-${digest.slice(0, 16)}`;
|
|
8653
|
-
}
|
|
8654
|
-
var init_job_id = __esm({
|
|
8655
|
-
"../worker/src/job-id.mjs"() {
|
|
8656
|
-
init_config2();
|
|
8657
|
-
init_forges();
|
|
8658
|
-
}
|
|
8659
|
-
});
|
|
8660
|
-
|
|
8661
8859
|
// ../worker/src/queue.mjs
|
|
8662
8860
|
import { Queue } from "bullmq";
|
|
8663
8861
|
function makeQueue(connection) {
|
|
@@ -8701,59 +8899,18 @@ async function enqueueLocalJob(queue, { folder, flow, task, command, provider, m
|
|
|
8701
8899
|
});
|
|
8702
8900
|
return id;
|
|
8703
8901
|
}
|
|
8704
|
-
var QUEUE, SEMANTIC_WINDOW_MS;
|
|
8902
|
+
var PR_CLOSE_WORDS, QUEUE, SEMANTIC_WINDOW_MS;
|
|
8705
8903
|
var init_queue = __esm({
|
|
8706
8904
|
"../worker/src/queue.mjs"() {
|
|
8707
8905
|
init_job_id();
|
|
8708
8906
|
init_forges();
|
|
8907
|
+
init_triggers();
|
|
8908
|
+
PR_CLOSE_WORDS = new Set(Object.values(PR_CLOSE_ACTIONS));
|
|
8709
8909
|
QUEUE = "pi-jobs";
|
|
8710
8910
|
SEMANTIC_WINDOW_MS = 10 * 60 * 1e3;
|
|
8711
8911
|
}
|
|
8712
8912
|
});
|
|
8713
8913
|
|
|
8714
|
-
// ../worker/src/flow-gate.mjs
|
|
8715
|
-
import { execFile } from "node:child_process";
|
|
8716
|
-
import { promisify } from "node:util";
|
|
8717
|
-
async function readFlowGate({ folder, flow, sha, git = defaultGit }) {
|
|
8718
|
-
if (typeof sha !== "string" || sha.trim() === "") return { gate: "deny" };
|
|
8719
|
-
if (typeof flow !== "string" || !SKILL_NAME_RE.test(flow)) return { gate: "deny" };
|
|
8720
|
-
const path = `.pi/skills/${flow}/SKILL.md`;
|
|
8721
|
-
try {
|
|
8722
|
-
const lsTreeZ = await git(folder, ["ls-tree", "-z", sha, path]);
|
|
8723
|
-
const record = lsTreeZ.split("\0").find((r) => r);
|
|
8724
|
-
if (!record) return { gate: "no-skill" };
|
|
8725
|
-
const tab = record.indexOf(" ");
|
|
8726
|
-
if (tab === -1) return { gate: "deny" };
|
|
8727
|
-
const [mode, type, oid] = record.slice(0, tab).split(/\s+/);
|
|
8728
|
-
if (mode !== "100644" || type !== "blob") return { gate: "deny" };
|
|
8729
|
-
const content = await git(folder, ["cat-file", "blob", oid], { raw: true });
|
|
8730
|
-
return { gate: aiTriggerAllows(content) ? "allow" : "deny" };
|
|
8731
|
-
} catch {
|
|
8732
|
-
return { gate: "deny" };
|
|
8733
|
-
}
|
|
8734
|
-
}
|
|
8735
|
-
function aiTriggerAllows(buf) {
|
|
8736
|
-
const text = buf.toString("utf8").replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
|
|
8737
|
-
const block = /^---\n([\s\S]*?)\n---(?:\n|$)/.exec(text);
|
|
8738
|
-
if (!block) return false;
|
|
8739
|
-
return /^ai-trigger:\s*("?)allow\1\s*$/m.test(block[1]);
|
|
8740
|
-
}
|
|
8741
|
-
async function defaultGit(gitDir, args, { raw = false } = {}) {
|
|
8742
|
-
const hardened = ["-c", "core.hooksPath=/dev/null", "-c", "core.fsmonitor=false", "--no-pager", "-C", gitDir, ...args];
|
|
8743
|
-
const { stdout } = await exec("git", hardened, {
|
|
8744
|
-
encoding: raw ? "buffer" : "utf8",
|
|
8745
|
-
maxBuffer: 16 * 1024 * 1024
|
|
8746
|
-
});
|
|
8747
|
-
return stdout;
|
|
8748
|
-
}
|
|
8749
|
-
var exec, SKILL_NAME_RE;
|
|
8750
|
-
var init_flow_gate = __esm({
|
|
8751
|
-
"../worker/src/flow-gate.mjs"() {
|
|
8752
|
-
exec = promisify(execFile);
|
|
8753
|
-
SKILL_NAME_RE = /^[a-z0-9](?:[a-z0-9_-]{0,62}[a-z0-9])?$/;
|
|
8754
|
-
}
|
|
8755
|
-
});
|
|
8756
|
-
|
|
8757
8914
|
// ../worker/src/git-dirty.mjs
|
|
8758
8915
|
import { execFileSync } from "node:child_process";
|
|
8759
8916
|
function gitDirty(folder, { exec: exec4 = execFileSync } = {}) {
|
|
@@ -9331,6 +9488,16 @@ function buildGraphModel({ triggers, schedulers, folderSkills, injectedSkills, o
|
|
|
9331
9488
|
// (guarded on `typeof t.flow === "string"`) correctly draws nothing for it.
|
|
9332
9489
|
command: t.command ?? null,
|
|
9333
9490
|
replicas: t.replicas ?? null,
|
|
9491
|
+
// The one-shot facts (issue #231), carried the way `cost` is: node FACTS, never new vocabulary
|
|
9492
|
+
// -- the #188 rule, and GRAPH_FLAGS/GRAPH_NODE_KINDS are byte-unchanged by this issue too. A
|
|
9493
|
+
// spent entry still arrives here because the display normalizer reads the RAW file (the spent
|
|
9494
|
+
// row must stay visible), so the two states a renderer has to keep apart ride as one boolean
|
|
9495
|
+
// and one mark: `once` is true only while ARMED -- a spent one-shot is not "a rule that will
|
|
9496
|
+
// fire once", and letting both read true would ask every consumer to re-derive the difference
|
|
9497
|
+
// -- and `disarmed` is the worker's mark verbatim ({ at, jobId? }), null like every absent
|
|
9498
|
+
// fact on this literal.
|
|
9499
|
+
once: t.once === true && !t.disarmed,
|
|
9500
|
+
disarmed: t.disarmed !== null && typeof t.disarmed === "object" ? t.disarmed : null,
|
|
9334
9501
|
folderKey: group?.key ?? null,
|
|
9335
9502
|
runs: Number.isInteger(stats?.runs) ? stats.runs : 0,
|
|
9336
9503
|
lastOutcome: stats?.lastOutcome ?? null,
|
|
@@ -9468,8 +9635,16 @@ function triggerMatchLabel(t) {
|
|
|
9468
9635
|
return selectorLabel(t);
|
|
9469
9636
|
case "comment":
|
|
9470
9637
|
return `"${t.phrase ?? "-"}"`;
|
|
9471
|
-
case "pull_request":
|
|
9472
|
-
|
|
9638
|
+
case "pull_request": {
|
|
9639
|
+
const action = `action[${(Array.isArray(t.action) ? t.action : []).join(",")}]`;
|
|
9640
|
+
const num = Number.isInteger(t.number) ? ` #${t.number}` : "";
|
|
9641
|
+
return t.disarmed ? `${action}${num} (spent)` : `${action}${num}`;
|
|
9642
|
+
}
|
|
9643
|
+
case "issue": {
|
|
9644
|
+
const action = `action[${(Array.isArray(t.action) ? t.action : []).join(",")}]`;
|
|
9645
|
+
const base = Number.isInteger(t.number) ? `${action} #${t.number}` : action;
|
|
9646
|
+
return t.disarmed ? `${base} (spent)` : base;
|
|
9647
|
+
}
|
|
9473
9648
|
default:
|
|
9474
9649
|
return "(unknown)";
|
|
9475
9650
|
}
|
|
@@ -10076,8 +10251,110 @@ var init_costs = __esm({
|
|
|
10076
10251
|
}
|
|
10077
10252
|
});
|
|
10078
10253
|
|
|
10254
|
+
// ../worker/src/triggers-file.mjs
|
|
10255
|
+
import nodeFs2 from "node:fs";
|
|
10256
|
+
function lockPathFor(triggersPath) {
|
|
10257
|
+
return `${triggersPath}.lock`;
|
|
10258
|
+
}
|
|
10259
|
+
function takeLock(triggersPath, fs, log) {
|
|
10260
|
+
const lock = lockPathFor(triggersPath);
|
|
10261
|
+
let sweptAgeMs = null;
|
|
10262
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
10263
|
+
try {
|
|
10264
|
+
const fd = fs.openSync(lock, "wx");
|
|
10265
|
+
if (sweptAgeMs !== null) log("triggers_lock_stale_taken", { ageMs: sweptAgeMs });
|
|
10266
|
+
return fd;
|
|
10267
|
+
} catch (err) {
|
|
10268
|
+
if (err?.code !== "EEXIST") throw err;
|
|
10269
|
+
let mtimeMs;
|
|
10270
|
+
try {
|
|
10271
|
+
mtimeMs = fs.statSync(lock).mtimeMs;
|
|
10272
|
+
} catch {
|
|
10273
|
+
continue;
|
|
10274
|
+
}
|
|
10275
|
+
if (Date.now() - mtimeMs <= LOCK_STALE_MS) return null;
|
|
10276
|
+
try {
|
|
10277
|
+
fs.unlinkSync(lock);
|
|
10278
|
+
} catch {
|
|
10279
|
+
}
|
|
10280
|
+
sweptAgeMs = Math.round(Date.now() - mtimeMs);
|
|
10281
|
+
}
|
|
10282
|
+
}
|
|
10283
|
+
return null;
|
|
10284
|
+
}
|
|
10285
|
+
function releaseLock(fd, triggersPath, fs, log) {
|
|
10286
|
+
fs.closeSync(fd);
|
|
10287
|
+
try {
|
|
10288
|
+
fs.unlinkSync(lockPathFor(triggersPath));
|
|
10289
|
+
} catch {
|
|
10290
|
+
log("triggers_lock_stuck", {});
|
|
10291
|
+
}
|
|
10292
|
+
}
|
|
10293
|
+
function serialize(triggersArray) {
|
|
10294
|
+
return `${JSON.stringify({ triggers: triggersArray }, null, 2)}
|
|
10295
|
+
`;
|
|
10296
|
+
}
|
|
10297
|
+
function renameIntoPlace(fs, tmp, dest) {
|
|
10298
|
+
try {
|
|
10299
|
+
fs.renameSync(tmp, dest);
|
|
10300
|
+
} catch (err) {
|
|
10301
|
+
if (err?.code !== "EPERM") throw err;
|
|
10302
|
+
fs.renameSync(tmp, dest);
|
|
10303
|
+
}
|
|
10304
|
+
}
|
|
10305
|
+
function tmpPathFor(triggersPath) {
|
|
10306
|
+
return `${triggersPath}.${process.pid}.${tmpSeq++}.tmp`;
|
|
10307
|
+
}
|
|
10308
|
+
function discardTmp(fs, tmp) {
|
|
10309
|
+
try {
|
|
10310
|
+
fs.unlinkSync(tmp);
|
|
10311
|
+
} catch {
|
|
10312
|
+
}
|
|
10313
|
+
}
|
|
10314
|
+
function writeTriggers({ triggersPath, mutate, fs = nodeFs2, log = () => {
|
|
10315
|
+
} }) {
|
|
10316
|
+
const fd = takeLock(triggersPath, fs, log);
|
|
10317
|
+
if (fd === null) {
|
|
10318
|
+
return { invalid: `triggers file locked (another write in progress): ${lockPathFor(triggersPath)}` };
|
|
10319
|
+
}
|
|
10320
|
+
try {
|
|
10321
|
+
let current = [];
|
|
10322
|
+
try {
|
|
10323
|
+
const raw = JSON.parse(fs.readFileSync(triggersPath, "utf8"));
|
|
10324
|
+
if (Array.isArray(raw?.triggers)) current = raw.triggers;
|
|
10325
|
+
} catch {
|
|
10326
|
+
}
|
|
10327
|
+
const next = mutate(current.map((t) => ({ ...t })));
|
|
10328
|
+
const text = serialize(next);
|
|
10329
|
+
try {
|
|
10330
|
+
parseTriggers(text, triggersPath);
|
|
10331
|
+
} catch (e) {
|
|
10332
|
+
return { invalid: e?.message ?? String(e) };
|
|
10333
|
+
}
|
|
10334
|
+
const tmp = tmpPathFor(triggersPath);
|
|
10335
|
+
try {
|
|
10336
|
+
fs.writeFileSync(tmp, text, { mode: 420 });
|
|
10337
|
+
renameIntoPlace(fs, tmp, triggersPath);
|
|
10338
|
+
} catch (err) {
|
|
10339
|
+
discardTmp(fs, tmp);
|
|
10340
|
+
throw err;
|
|
10341
|
+
}
|
|
10342
|
+
return { ok: true };
|
|
10343
|
+
} finally {
|
|
10344
|
+
releaseLock(fd, triggersPath, fs, log);
|
|
10345
|
+
}
|
|
10346
|
+
}
|
|
10347
|
+
var LOCK_STALE_MS, tmpSeq;
|
|
10348
|
+
var init_triggers_file = __esm({
|
|
10349
|
+
"../worker/src/triggers-file.mjs"() {
|
|
10350
|
+
init_triggers();
|
|
10351
|
+
LOCK_STALE_MS = 1e4;
|
|
10352
|
+
tmpSeq = 0;
|
|
10353
|
+
}
|
|
10354
|
+
});
|
|
10355
|
+
|
|
10079
10356
|
// src/read-model.mjs
|
|
10080
|
-
import * as
|
|
10357
|
+
import * as nodeFs3 from "node:fs";
|
|
10081
10358
|
import { join as join2, delimiter, sep } from "node:path";
|
|
10082
10359
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
10083
10360
|
function resolvePaths(env = process.env) {
|
|
@@ -10250,14 +10527,14 @@ function folderUnderRoots(folder, roots) {
|
|
|
10250
10527
|
if (!Array.isArray(roots) || roots.length === 0) return false;
|
|
10251
10528
|
let realTarget;
|
|
10252
10529
|
try {
|
|
10253
|
-
realTarget =
|
|
10530
|
+
realTarget = nodeFs3.realpathSync(folder);
|
|
10254
10531
|
} catch {
|
|
10255
10532
|
return false;
|
|
10256
10533
|
}
|
|
10257
10534
|
for (const root of roots) {
|
|
10258
10535
|
let realRoot;
|
|
10259
10536
|
try {
|
|
10260
|
-
realRoot =
|
|
10537
|
+
realRoot = nodeFs3.realpathSync(root);
|
|
10261
10538
|
} catch {
|
|
10262
10539
|
continue;
|
|
10263
10540
|
}
|
|
@@ -10268,7 +10545,7 @@ function folderUnderRoots(folder, roots) {
|
|
|
10268
10545
|
function hourKey(nowMs) {
|
|
10269
10546
|
return `dispatch-run:${new Date(nowMs).toISOString().slice(0, 13).replace("T", "-")}`;
|
|
10270
10547
|
}
|
|
10271
|
-
function writeSettings({ settingsFile, mutate, fs =
|
|
10548
|
+
function writeSettings({ settingsFile, mutate, fs = nodeFs3 }) {
|
|
10272
10549
|
const res = readOverlay(settingsFile, { fs });
|
|
10273
10550
|
const base = res.overlay ?? {};
|
|
10274
10551
|
const next = mutate({ ...base });
|
|
@@ -10276,27 +10553,7 @@ function writeSettings({ settingsFile, mutate, fs = nodeFs2 }) {
|
|
|
10276
10553
|
if (w.invalid) return { invalid: w.invalid };
|
|
10277
10554
|
return res.invalid ? { ok: true, overlay: next, rebuiltFrom: res.invalid } : { ok: true, overlay: next };
|
|
10278
10555
|
}
|
|
10279
|
-
function
|
|
10280
|
-
let current = [];
|
|
10281
|
-
try {
|
|
10282
|
-
const raw = JSON.parse(fs.readFileSync(triggersPath, "utf8"));
|
|
10283
|
-
if (Array.isArray(raw?.triggers)) current = raw.triggers;
|
|
10284
|
-
} catch {
|
|
10285
|
-
}
|
|
10286
|
-
const next = mutate(current.map((t) => ({ ...t })));
|
|
10287
|
-
const text = `${JSON.stringify({ triggers: next }, null, 2)}
|
|
10288
|
-
`;
|
|
10289
|
-
try {
|
|
10290
|
-
parseTriggers(text, triggersPath);
|
|
10291
|
-
} catch (e) {
|
|
10292
|
-
return { invalid: e?.message ?? String(e) };
|
|
10293
|
-
}
|
|
10294
|
-
const tmp = `${triggersPath}.tmp`;
|
|
10295
|
-
fs.writeFileSync(tmp, text, { mode: 420 });
|
|
10296
|
-
fs.renameSync(tmp, triggersPath);
|
|
10297
|
-
return { ok: true };
|
|
10298
|
-
}
|
|
10299
|
-
function readPauseWindows({ pauseWindowsPath, fs = nodeFs2 }) {
|
|
10556
|
+
function readPauseWindows({ pauseWindowsPath, fs = nodeFs3 }) {
|
|
10300
10557
|
let text;
|
|
10301
10558
|
try {
|
|
10302
10559
|
text = fs.readFileSync(pauseWindowsPath, "utf8");
|
|
@@ -10309,7 +10566,7 @@ function readPauseWindows({ pauseWindowsPath, fs = nodeFs2 }) {
|
|
|
10309
10566
|
return { invalid: e?.message ?? String(e) };
|
|
10310
10567
|
}
|
|
10311
10568
|
}
|
|
10312
|
-
function writePauseWindows({ pauseWindowsPath, mutate, fs =
|
|
10569
|
+
function writePauseWindows({ pauseWindowsPath, mutate, fs = nodeFs3 }) {
|
|
10313
10570
|
let current = [];
|
|
10314
10571
|
try {
|
|
10315
10572
|
const raw = JSON.parse(fs.readFileSync(pauseWindowsPath, "utf8"));
|
|
@@ -10329,7 +10586,7 @@ function writePauseWindows({ pauseWindowsPath, mutate, fs = nodeFs2 }) {
|
|
|
10329
10586
|
fs.renameSync(tmp, pauseWindowsPath);
|
|
10330
10587
|
return { ok: true };
|
|
10331
10588
|
}
|
|
10332
|
-
function readSubscriptions({ subscriptionsPath, fs =
|
|
10589
|
+
function readSubscriptions({ subscriptionsPath, fs = nodeFs3 }) {
|
|
10333
10590
|
if (subscriptionsPath === null || subscriptionsPath === void 0) return { missing: true };
|
|
10334
10591
|
let text;
|
|
10335
10592
|
try {
|
|
@@ -10411,7 +10668,7 @@ function withTimeout(promise, ms, fallback) {
|
|
|
10411
10668
|
});
|
|
10412
10669
|
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
10413
10670
|
}
|
|
10414
|
-
function listRuns({ logsDir, limit = 10, fs =
|
|
10671
|
+
function listRuns({ logsDir, limit = 10, fs = nodeFs3 }) {
|
|
10415
10672
|
const cap = clampLimit(limit);
|
|
10416
10673
|
let names;
|
|
10417
10674
|
try {
|
|
@@ -10429,7 +10686,7 @@ function listRuns({ logsDir, limit = 10, fs = nodeFs2 }) {
|
|
|
10429
10686
|
records.sort(byEndedAtDesc);
|
|
10430
10687
|
return records.slice(0, cap);
|
|
10431
10688
|
}
|
|
10432
|
-
function scanRunRecords({ logsDir, sinceMs, nowMs = Date.now(), fs =
|
|
10689
|
+
function scanRunRecords({ logsDir, sinceMs, nowMs = Date.now(), fs = nodeFs3 }) {
|
|
10433
10690
|
const oldestMs = nowMs - SCAN_WINDOW_MAX_DAYS * 24 * 60 * 60 * 1e3;
|
|
10434
10691
|
const cutoffMs = Math.max(Number.isFinite(sinceMs) ? sinceMs : oldestMs, oldestMs);
|
|
10435
10692
|
let names;
|
|
@@ -10450,27 +10707,26 @@ function scanRunRecords({ logsDir, sinceMs, nowMs = Date.now(), fs = nodeFs2 })
|
|
|
10450
10707
|
}
|
|
10451
10708
|
return records;
|
|
10452
10709
|
}
|
|
10453
|
-
function readRun({ logsDir, jobId, fs =
|
|
10710
|
+
function readRun({ logsDir, jobId, fs = nodeFs3 }) {
|
|
10454
10711
|
return readJsonFile(join2(logsDir, `${sanitizeJobId(jobId)}.json`), fs);
|
|
10455
10712
|
}
|
|
10456
|
-
function readLogTail({ logsDir, jobId, lines = 200, fs =
|
|
10713
|
+
function readLogTail({ logsDir, jobId, lines = 200, fs = nodeFs3 }) {
|
|
10457
10714
|
let text;
|
|
10458
10715
|
try {
|
|
10459
10716
|
text = fs.readFileSync(join2(logsDir, `${sanitizeJobId(jobId)}.log`), "utf8");
|
|
10460
10717
|
} catch {
|
|
10461
10718
|
return { missing: true };
|
|
10462
10719
|
}
|
|
10463
|
-
const all = text.split("\n");
|
|
10464
|
-
if (all.length > 0 && all[all.length - 1] === "") all.pop();
|
|
10720
|
+
const all = text.split("\n").filter((line) => line !== "");
|
|
10465
10721
|
const cap = clampLines(lines);
|
|
10466
10722
|
return { lines: all.slice(Math.max(0, all.length - cap)) };
|
|
10467
10723
|
}
|
|
10468
|
-
function readSettingsView({ settingsFile, fs =
|
|
10724
|
+
function readSettingsView({ settingsFile, fs = nodeFs3 }) {
|
|
10469
10725
|
const result = readOverlay(settingsFile, { fs });
|
|
10470
10726
|
if (result.invalid) return { path: settingsFile, invalid: result.invalid };
|
|
10471
10727
|
return { path: settingsFile, overlay: result.overlay };
|
|
10472
10728
|
}
|
|
10473
|
-
function readTriggers({ triggersPath, fs =
|
|
10729
|
+
function readTriggers({ triggersPath, fs = nodeFs3 }) {
|
|
10474
10730
|
let text;
|
|
10475
10731
|
try {
|
|
10476
10732
|
text = fs.readFileSync(triggersPath, "utf8");
|
|
@@ -10534,13 +10790,44 @@ function normalizeTriggerForDisplay(entry) {
|
|
|
10534
10790
|
return { type: "label", any: normalizeSelector(on.any), all: normalizeSelector(on.all), none: normalizeSelector(on.none), flow, command, packages, image, skillsDir, instructions, resume, secrets, secretsProfile, replicas, forge };
|
|
10535
10791
|
case "comment":
|
|
10536
10792
|
return { type: "comment", phrase: typeof on.phrase === "string" ? on.phrase : null, flow, command, packages, image, skillsDir, instructions, resume, secrets, secretsProfile, replicas, forge };
|
|
10537
|
-
case "pull_request":
|
|
10793
|
+
case "pull_request": {
|
|
10794
|
+
const prDisarmed = on.disarmed !== null && typeof on.disarmed === "object" && !Array.isArray(on.disarmed) ? on.disarmed : null;
|
|
10538
10795
|
return {
|
|
10539
10796
|
type: "pull_request",
|
|
10540
10797
|
action: normalizeSelector(on.action),
|
|
10541
10798
|
any: normalizeSelector(on.any),
|
|
10542
10799
|
all: normalizeSelector(on.all),
|
|
10543
10800
|
none: normalizeSelector(on.none),
|
|
10801
|
+
...Number.isInteger(on.number) && on.number >= 1 && { number: on.number },
|
|
10802
|
+
...on.once === true && { once: true },
|
|
10803
|
+
...prDisarmed !== null && { disarmed: prDisarmed },
|
|
10804
|
+
flow,
|
|
10805
|
+
command,
|
|
10806
|
+
packages,
|
|
10807
|
+
image,
|
|
10808
|
+
skillsDir,
|
|
10809
|
+
instructions,
|
|
10810
|
+
resume,
|
|
10811
|
+
secrets,
|
|
10812
|
+
secretsProfile,
|
|
10813
|
+
replicas,
|
|
10814
|
+
forge
|
|
10815
|
+
};
|
|
10816
|
+
}
|
|
10817
|
+
case "issue": {
|
|
10818
|
+
const disarmed = on.disarmed !== null && typeof on.disarmed === "object" && !Array.isArray(on.disarmed) ? on.disarmed : null;
|
|
10819
|
+
return {
|
|
10820
|
+
type: "issue",
|
|
10821
|
+
action: normalizeSelector(on.action),
|
|
10822
|
+
// `null` is the every-item sentinel, this function's own flow/model/phrase convention; the loader
|
|
10823
|
+
// refused anything but an integer >= 1, so the display mirrors rather than re-validates.
|
|
10824
|
+
number: Number.isInteger(on.number) && on.number >= 1 ? on.number : null,
|
|
10825
|
+
// An opt-IN, `=== true` like `resume` above and NOT `!== false`: the rendering mistake that costs
|
|
10826
|
+
// is a one-shot with no marker, never a standing rule with a spurious one.
|
|
10827
|
+
once: on.once === true,
|
|
10828
|
+
// Absent, not null, when unspent: `disarmed` is a mark the worker adds, and a key that is usually
|
|
10829
|
+
// missing reads truer than a fifth null sentinel -- consumers test presence, not value.
|
|
10830
|
+
...disarmed !== null && { disarmed },
|
|
10544
10831
|
flow,
|
|
10545
10832
|
command,
|
|
10546
10833
|
packages,
|
|
@@ -10553,6 +10840,7 @@ function normalizeTriggerForDisplay(entry) {
|
|
|
10553
10840
|
replicas,
|
|
10554
10841
|
forge
|
|
10555
10842
|
};
|
|
10843
|
+
}
|
|
10556
10844
|
default:
|
|
10557
10845
|
return null;
|
|
10558
10846
|
}
|
|
@@ -10561,7 +10849,7 @@ function normalizeSelector(value) {
|
|
|
10561
10849
|
if (!Array.isArray(value)) return [];
|
|
10562
10850
|
return value.filter((member) => typeof member === "string");
|
|
10563
10851
|
}
|
|
10564
|
-
function readStagedPackages({ globalPiDir, fs =
|
|
10852
|
+
function readStagedPackages({ globalPiDir, fs = nodeFs3, readManifest: readManifest2 = readStageManifest } = {}) {
|
|
10565
10853
|
const empty = { stagedAt: null, packages: [] };
|
|
10566
10854
|
if (typeof globalPiDir !== "string" || globalPiDir === "") return empty;
|
|
10567
10855
|
let manifest;
|
|
@@ -10769,7 +11057,7 @@ function readFolderSkills({ folder, exec: exec4 = execFileSync2 } = {}) {
|
|
|
10769
11057
|
}
|
|
10770
11058
|
return { head, skills, truncated: truncated || anyUnread, unreachable: null };
|
|
10771
11059
|
}
|
|
10772
|
-
function readInjectedSkills({ skillsDir, fs =
|
|
11060
|
+
function readInjectedSkills({ skillsDir, fs = nodeFs3 } = {}) {
|
|
10773
11061
|
if (typeof skillsDir !== "string" || skillsDir === "") return { skills: [], truncated: false, unreachable: null };
|
|
10774
11062
|
let names;
|
|
10775
11063
|
try {
|
|
@@ -10792,7 +11080,7 @@ function readInjectedSkills({ skillsDir, fs = nodeFs2 } = {}) {
|
|
|
10792
11080
|
}
|
|
10793
11081
|
return { skills, truncated, unreachable: null };
|
|
10794
11082
|
}
|
|
10795
|
-
function readOverlaySkills({ globalPiDir, fs =
|
|
11083
|
+
function readOverlaySkills({ globalPiDir, fs = nodeFs3 } = {}) {
|
|
10796
11084
|
if (typeof globalPiDir !== "string" || globalPiDir === "") return { skills: [], truncated: false, unreachable: null };
|
|
10797
11085
|
const dir = join2(globalPiDir, "skills");
|
|
10798
11086
|
let names;
|
|
@@ -10816,7 +11104,7 @@ function readOverlaySkills({ globalPiDir, fs = nodeFs2 } = {}) {
|
|
|
10816
11104
|
}
|
|
10817
11105
|
return { skills, truncated, unreachable: null };
|
|
10818
11106
|
}
|
|
10819
|
-
function readStagedSkillsList({ globalPiDir, fs =
|
|
11107
|
+
function readStagedSkillsList({ globalPiDir, fs = nodeFs3, readStaged = readStagedSkills } = {}) {
|
|
10820
11108
|
const empty = { skills: [], unenumerable: [], truncated: false };
|
|
10821
11109
|
if (typeof globalPiDir !== "string" || globalPiDir === "") return empty;
|
|
10822
11110
|
let result;
|
|
@@ -10891,7 +11179,7 @@ function forgeRepoTargets({ records } = {}) {
|
|
|
10891
11179
|
function escapeRegExp2(s) {
|
|
10892
11180
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
10893
11181
|
}
|
|
10894
|
-
function listRunIds({ logsDir, fs =
|
|
11182
|
+
function listRunIds({ logsDir, fs = nodeFs3 }) {
|
|
10895
11183
|
let names;
|
|
10896
11184
|
try {
|
|
10897
11185
|
names = fs.readdirSync(logsDir);
|
|
@@ -10936,7 +11224,6 @@ var init_read_model = __esm({
|
|
|
10936
11224
|
init_runtime_settings();
|
|
10937
11225
|
init_run_history();
|
|
10938
11226
|
init_budget();
|
|
10939
|
-
init_triggers();
|
|
10940
11227
|
init_pause_windows();
|
|
10941
11228
|
init_subscriptions();
|
|
10942
11229
|
init_connection();
|
|
@@ -10948,6 +11235,7 @@ var init_read_model = __esm({
|
|
|
10948
11235
|
init_forges();
|
|
10949
11236
|
init_graph_model();
|
|
10950
11237
|
init_costs();
|
|
11238
|
+
init_triggers_file();
|
|
10951
11239
|
DISPATCH_RUN_TTL_SECONDS = 2 * 60 * 60;
|
|
10952
11240
|
SCAN_WINDOW_MAX_DAYS = 92;
|
|
10953
11241
|
GRAPH_LIMITS = Object.freeze({
|
|
@@ -11066,6 +11354,12 @@ function normalizeModel(model) {
|
|
|
11066
11354
|
package: typeof n.package === "string" ? clip(n.package, 80) : null,
|
|
11067
11355
|
tiersUnknown: Array.isArray(n.tiersUnknown) ? [...new Set(n.tiersUnknown.filter((t) => typeof t === "string").map((t) => clip(t, 40)))].sort().slice(0, 4) : [],
|
|
11068
11356
|
replicas: intOr(n.replicas, null),
|
|
11357
|
+
// One-shot facts (#231): `once` is the armed state, and the disarm mark is narrowed to its
|
|
11358
|
+
// instant alone. The mark's jobId stays server-side on purpose: the page answers "spent, and
|
|
11359
|
+
// when", and provenance beyond that belongs to the operator's own console -- a shareable
|
|
11360
|
+
// file:// page should carry no more of a deployment's run history than the question needs.
|
|
11361
|
+
once: n.once === true,
|
|
11362
|
+
disarmed: n.disarmed !== null && n.disarmed !== void 0 && typeof n.disarmed === "object" && !Array.isArray(n.disarmed) ? { at: typeof n.disarmed.at === "string" ? clip(n.disarmed.at, 40) : Number.isFinite(n.disarmed.at) ? n.disarmed.at : null } : null,
|
|
11069
11363
|
folderKey: typeof n.folderKey === "string" ? n.folderKey : null,
|
|
11070
11364
|
runs: intOr(n.runs, 0),
|
|
11071
11365
|
lastOutcome: typeof n.lastOutcome === "string" ? n.lastOutcome : null,
|
|
@@ -11510,6 +11804,8 @@ function buildTip(n, flags, groupLabel, nowMs) {
|
|
|
11510
11804
|
const nextMs = typeof n.next === "number" ? n.next : typeof n.next === "string" ? Date.parse(n.next) : NaN;
|
|
11511
11805
|
if (Number.isFinite(nextMs) && Number.isFinite(nowMs) && nextMs > nowMs) lines.push(`next ${relSpan(nextMs - nowMs)}`);
|
|
11512
11806
|
}
|
|
11807
|
+
if (n.disarmed !== null) lines.push(`one-shot, spent${n.disarmed.at !== null ? ` ${n.disarmed.at}` : ""}`);
|
|
11808
|
+
else if (n.once) lines.push("one-shot (armed)");
|
|
11513
11809
|
}
|
|
11514
11810
|
if (n.aiTrigger) lines.push("chainable: ai-trigger allow");
|
|
11515
11811
|
if (n.kind === "skill" && n.loops.length > 0) {
|
|
@@ -11540,6 +11836,7 @@ function nodeState(n, flagNames) {
|
|
|
11540
11836
|
}
|
|
11541
11837
|
if (n.kind === "skill-unverified") return { stroke: PAGE_DIM, dash: "10,4", faded: false };
|
|
11542
11838
|
if (n.kind === "skill-not-at-head") return { stroke: PAGE_AMBER, dash: "10,4", faded: false };
|
|
11839
|
+
if (n.kind === "trigger" && n.disarmed !== null) return { stroke: CHIP_STROKE, dash: "8,3", faded: true };
|
|
11543
11840
|
if (flagNames.has("orphan")) return { stroke: CHIP_STROKE, dash: "8,3", faded: true };
|
|
11544
11841
|
return { stroke: CHIP_STROKE, dash: null, faded: false };
|
|
11545
11842
|
}
|
|
@@ -11784,6 +12081,7 @@ var init_graph_html = __esm({
|
|
|
11784
12081
|
label: "#d8bfd8",
|
|
11785
12082
|
comment: "#e7e7ae",
|
|
11786
12083
|
pull_request: "#c0deed",
|
|
12084
|
+
issue: "#c7e9c0",
|
|
11787
12085
|
skill: "#fdd0a2"
|
|
11788
12086
|
});
|
|
11789
12087
|
STATUS_COMPLETED = "#3fb950";
|
|
@@ -11813,6 +12111,7 @@ var init_graph_html = __esm({
|
|
|
11813
12111
|
label: "\u25C8",
|
|
11814
12112
|
comment: "\u275D",
|
|
11815
12113
|
pull_request: "\u21C4",
|
|
12114
|
+
issue: "\u25C9",
|
|
11816
12115
|
skill: "\u0192",
|
|
11817
12116
|
"skill-missing": "!",
|
|
11818
12117
|
"skill-unverified": "?",
|
|
@@ -13227,7 +13526,7 @@ var init_open_browser = __esm({
|
|
|
13227
13526
|
});
|
|
13228
13527
|
|
|
13229
13528
|
// src/deployment-pointer.mjs
|
|
13230
|
-
import * as
|
|
13529
|
+
import * as nodeFs4 from "node:fs";
|
|
13231
13530
|
import { homedir } from "node:os";
|
|
13232
13531
|
import { join as join3, isAbsolute } from "node:path";
|
|
13233
13532
|
function pointerPath(env = process.env) {
|
|
@@ -13259,7 +13558,7 @@ function normalizePointer(raw) {
|
|
|
13259
13558
|
}
|
|
13260
13559
|
return { pointer: { version: raw.version, deploymentDir: raw.deploymentDir, env } };
|
|
13261
13560
|
}
|
|
13262
|
-
function readPointer({ path = pointerPath(), fs =
|
|
13561
|
+
function readPointer({ path = pointerPath(), fs = nodeFs4 } = {}) {
|
|
13263
13562
|
let text;
|
|
13264
13563
|
try {
|
|
13265
13564
|
text = fs.readFileSync(path, "utf8");
|
|
@@ -13292,12 +13591,12 @@ function layerPointer(env, fs) {
|
|
|
13292
13591
|
}
|
|
13293
13592
|
return { applied };
|
|
13294
13593
|
}
|
|
13295
|
-
function applyDeploymentPointer(env = process.env, { fs =
|
|
13594
|
+
function applyDeploymentPointer(env = process.env, { fs = nodeFs4 } = {}) {
|
|
13296
13595
|
if (appliedOnce) return { applied: [] };
|
|
13297
13596
|
appliedOnce = true;
|
|
13298
13597
|
return layerPointer(env, fs);
|
|
13299
13598
|
}
|
|
13300
|
-
function reapplyDeploymentPointer(env = process.env, { fs =
|
|
13599
|
+
function reapplyDeploymentPointer(env = process.env, { fs = nodeFs4 } = {}) {
|
|
13301
13600
|
appliedOnce = true;
|
|
13302
13601
|
return layerPointer(env, fs);
|
|
13303
13602
|
}
|
|
@@ -13306,7 +13605,7 @@ function takePointerNotice() {
|
|
|
13306
13605
|
notice = void 0;
|
|
13307
13606
|
return n;
|
|
13308
13607
|
}
|
|
13309
|
-
function writePointer({ path = pointerPath(), pointer, fs =
|
|
13608
|
+
function writePointer({ path = pointerPath(), pointer, fs = nodeFs4 } = {}) {
|
|
13310
13609
|
const res = normalizePointer(pointer);
|
|
13311
13610
|
if (res.ignored) return { invalid: res.ignored };
|
|
13312
13611
|
const tmp = `${path}.tmp`;
|
|
@@ -39039,6 +39338,7 @@ function triggerLine(t) {
|
|
|
39039
39338
|
const res = t?.resume === true ? " [resume]" : "";
|
|
39040
39339
|
const rep = t?.replicas > 1 ? ` [x${t.replicas}]` : "";
|
|
39041
39340
|
const sec = t?.secrets > 0 ? ` [secrets ${t.secrets}${t.secretsProfile ? ` via ${t.secretsProfile}` : ""}]` : "";
|
|
39341
|
+
const shot = t?.disarmed ? " [spent]" : t?.once === true ? " [once]" : "";
|
|
39042
39342
|
switch (t?.type) {
|
|
39043
39343
|
case "cron":
|
|
39044
39344
|
return `cron ${t.id ?? "-"} ${t.pattern ?? "-"} \u2192 ${t.folder ?? "-"}/${flow}${forge}${pkgs}${img}${skl}${ins}${res}${rep}${sec}`;
|
|
@@ -39049,7 +39349,13 @@ function triggerLine(t) {
|
|
|
39049
39349
|
case "pull_request": {
|
|
39050
39350
|
const clauses = ruleClauses(t);
|
|
39051
39351
|
const action = `action[${(t.action ?? []).join(",")}]`;
|
|
39052
|
-
|
|
39352
|
+
const num = Number.isInteger(t.number) ? ` #${t.number}` : "";
|
|
39353
|
+
return `pull_request ${action}${num}${clauses ? ` ${clauses}` : ""} \u2192 ${flow}${forge}${pkgs}${img}${skl}${ins}${res}${rep}${sec}${shot}`;
|
|
39354
|
+
}
|
|
39355
|
+
case "issue": {
|
|
39356
|
+
const action = `action[${(t.action ?? []).join(",")}]`;
|
|
39357
|
+
const num = Number.isInteger(t.number) ? ` #${t.number}` : "";
|
|
39358
|
+
return `issue ${action}${num} \u2192 ${flow}${forge}${pkgs}${img}${skl}${ins}${res}${rep}${sec}${shot}`;
|
|
39053
39359
|
}
|
|
39054
39360
|
default:
|
|
39055
39361
|
return "(unknown trigger)";
|
|
@@ -39978,8 +40284,9 @@ function triggerRow(t, sel, inner, styler, sched = null) {
|
|
|
39978
40284
|
const img = t?.image ? " " + styler.fg("accent", `[${t.image}]`) : "";
|
|
39979
40285
|
const res = t?.resume === true ? " " + styler.fg("warning", "[resume]") : "";
|
|
39980
40286
|
const rep = t?.replicas > 1 ? " " + styler.fg("warning", `[x${t.replicas}]`) : "";
|
|
40287
|
+
const shot = t?.disarmed ? " " + styler.fg("dim", "[spent]") : t?.once === true ? " " + styler.fg("accent", "[once]") : "";
|
|
39981
40288
|
const health = sched && (sched.overdueMs || sched.stallMax > 0 && sched.stalls >= sched.stallMax) ? " " + styler.fg("warning", sched.overdueMs ? "\u26A0 overdue" : "\u26A0 stalled") : "";
|
|
39982
|
-
return fitLine(`${cursor} ${badge} ${matchColored(t, styler)} ${targetColored(t, styler)}${pkgs}${img}${res}${rep}${health}`, inner, styler);
|
|
40289
|
+
return fitLine(`${cursor} ${badge} ${matchColored(t, styler)} ${targetColored(t, styler)}${pkgs}${img}${res}${rep}${shot}${health}`, inner, styler);
|
|
39983
40290
|
}
|
|
39984
40291
|
function matchColored(t, styler) {
|
|
39985
40292
|
switch (t?.type) {
|
|
@@ -39987,10 +40294,15 @@ function matchColored(t, styler) {
|
|
|
39987
40294
|
return styler.fg("text", `${t.id ?? "-"} ${t.pattern ?? "-"}`);
|
|
39988
40295
|
case "comment":
|
|
39989
40296
|
return styler.fg("text", `"${t.phrase ?? "-"}"`);
|
|
40297
|
+
case "issue": {
|
|
40298
|
+
const num = Number.isInteger(t.number) ? " " + styler.fg("success", `#${t.number}`) : "";
|
|
40299
|
+
return styler.fg("muted", `[${(t.action ?? []).join(",")}]`) + num;
|
|
40300
|
+
}
|
|
39990
40301
|
case "label":
|
|
39991
40302
|
case "pull_request": {
|
|
39992
40303
|
const parts = [];
|
|
39993
40304
|
if (t.type === "pull_request") parts.push(styler.fg("muted", `[${(t.action ?? []).join(",")}]`));
|
|
40305
|
+
if (t.type === "pull_request" && Number.isInteger(t.number)) parts.push(styler.fg("success", `#${t.number}`));
|
|
39994
40306
|
for (const x of t.any ?? []) parts.push(styler.fg("success", x));
|
|
39995
40307
|
for (const x of t.all ?? []) parts.push(styler.fg("success", `+${x}`));
|
|
39996
40308
|
for (const x of t.none ?? []) parts.push(styler.fg("error", `!${x}`));
|
|
@@ -40125,11 +40437,17 @@ function renderTriggerDetail(t, inner, styler, sched = null, staged = null) {
|
|
|
40125
40437
|
}
|
|
40126
40438
|
} else if (t.type === "label" || t.type === "pull_request") {
|
|
40127
40439
|
if (t.type === "pull_request") out.push(kv("PR actions", (t.action ?? []).join(", ") || "-"));
|
|
40440
|
+
if (t.type === "pull_request" && Number.isInteger(t.number)) out.push(kv("item", `#${t.number}`, "success"));
|
|
40441
|
+
if (t.once === true) out.push(kv("one-shot", t.disarmed ? `spent ${t.disarmed.at ?? "-"}${t.disarmed.jobId ? ` by ${t.disarmed.jobId}` : ""}` : "armed", t.disarmed ? "dim" : "accent"));
|
|
40128
40442
|
out.push(kv("any of", (t.any ?? []).join(" \xB7 ") || "-", "success"));
|
|
40129
40443
|
out.push(kv("all of", (t.all ?? []).join(" \xB7 ") || "-", "success"));
|
|
40130
40444
|
out.push(kv("none of", (t.none ?? []).join(" \xB7 ") || "-", "error"));
|
|
40131
40445
|
} else if (t.type === "comment") {
|
|
40132
40446
|
out.push(kv("phrase", `"${t.phrase ?? "-"}"`));
|
|
40447
|
+
} else if (t.type === "issue") {
|
|
40448
|
+
out.push(kv("issue actions", (t.action ?? []).join(", ") || "-"));
|
|
40449
|
+
if (Number.isInteger(t.number)) out.push(kv("item", `#${t.number}`, "success"));
|
|
40450
|
+
if (t.once === true) out.push(kv("one-shot", t.disarmed ? `spent ${t.disarmed.at ?? "-"}${t.disarmed.jobId ? ` by ${t.disarmed.jobId}` : ""}` : "armed", t.disarmed ? "dim" : "accent"));
|
|
40133
40451
|
}
|
|
40134
40452
|
blank();
|
|
40135
40453
|
section("runs");
|
|
@@ -40193,33 +40511,38 @@ function trustModel(t) {
|
|
|
40193
40511
|
}
|
|
40194
40512
|
const subject = t?.type === "comment" ? "adversarial comment text" : "adversarial issue/PR text";
|
|
40195
40513
|
const where = `lives in the receiver \xB7 task is ${subject}`;
|
|
40514
|
+
const closeCapable = t?.type === "issue" || t?.type === "pull_request" && (t?.action ?? []).some((a) => a === "closed" || a === "close");
|
|
40515
|
+
const oneShot = (lines) => closeCapable && (t?.once === true || t?.disarmed) ? [...lines, "one-shot: the WORKER writes on.disarmed into triggers.json, strictly after the run record \u2014 spent always has its run"] : lines;
|
|
40196
40516
|
switch (t?.forge) {
|
|
40197
40517
|
case "gitlab":
|
|
40198
|
-
return [
|
|
40518
|
+
return oneShot([
|
|
40199
40519
|
"authorized by the actor's resolved project access level (>= Developer) \u2014 a label is NOT approval here",
|
|
40200
40520
|
"HMAC over the body (19.0+) or a bare X-Gitlab-Token \xB7 dedup by webhook-id",
|
|
40201
40521
|
where
|
|
40202
|
-
];
|
|
40522
|
+
]);
|
|
40203
40523
|
case "forgejo":
|
|
40204
|
-
return [
|
|
40524
|
+
return oneShot([
|
|
40205
40525
|
"authorized by the actor's resolved repository permission (admin|write), on every trigger type",
|
|
40206
40526
|
"HMAC over the body (GitHub-compatible) \xB7 dedup by X-GitHub-Delivery GUID",
|
|
40207
40527
|
where
|
|
40208
|
-
];
|
|
40528
|
+
]);
|
|
40209
40529
|
case "azure":
|
|
40210
|
-
return [
|
|
40530
|
+
return oneShot([
|
|
40211
40531
|
"authorized by the actor's resolved project membership \u2014 work items name the actor only by email",
|
|
40212
40532
|
"NO HMAC: a shared secret that covers no bytes \xB7 dedup by the payload's own id (OQ-015)",
|
|
40213
40533
|
where
|
|
40214
|
-
];
|
|
40534
|
+
]);
|
|
40215
40535
|
default:
|
|
40216
|
-
return [
|
|
40536
|
+
return oneShot([
|
|
40217
40537
|
// A review trigger reads the REVIEWER's author_association, not the PR author's (issue #66), so it
|
|
40218
40538
|
// cannot share the generic line: an operator reading "author gate" would picture the wrong person.
|
|
40219
|
-
|
|
40539
|
+
// A close rule reads the CLOSER's resolved permission (issue #231), for the inversion's sharper
|
|
40540
|
+
// sibling: GitHub sends NO association for a close at all, so the collaborator-label line would
|
|
40541
|
+
// name a gate this rule never runs — and a wrong trust model is worse than an absent one.
|
|
40542
|
+
closeCapable ? "authorized by the CLOSER's resolved repository permission (admin|write) + HMAC \u2014 a close names no author_association" : t?.type === "comment" ? "authorized by a collaborator comment (author_association) + HMAC" : t?.type === "pull_request" && (t?.action ?? []).includes("review_submitted") ? "authorized by the REVIEWER's author_association (not the PR author's) + HMAC" : "authorized by a collaborator's label + HMAC webhook + author gate",
|
|
40220
40543
|
"dedup by X-GitHub-Delivery GUID (redelivery-safe)",
|
|
40221
40544
|
where
|
|
40222
|
-
];
|
|
40545
|
+
]);
|
|
40223
40546
|
}
|
|
40224
40547
|
}
|
|
40225
40548
|
function triggerDetailHints(inner, styler, pendingDelete = false) {
|
|
@@ -40460,7 +40783,7 @@ var init_dashboard = __esm({
|
|
|
40460
40783
|
TAIL_VIEWPORT = 20;
|
|
40461
40784
|
MIN_WIDTH2 = 8;
|
|
40462
40785
|
DRILL_WIDTH = 70;
|
|
40463
|
-
KIND_COLOR = { cron: "accent", label: "syntaxType", comment: "syntaxKeyword", pull_request: "syntaxFunction" };
|
|
40786
|
+
KIND_COLOR = { cron: "accent", label: "syntaxType", comment: "syntaxKeyword", pull_request: "syntaxFunction", issue: "syntaxString" };
|
|
40464
40787
|
KIND_WIDTH = 13;
|
|
40465
40788
|
FRAME_CHROME_ROWS = 4;
|
|
40466
40789
|
}
|
|
@@ -40485,7 +40808,7 @@ __export(setup_wizard_exports, {
|
|
|
40485
40808
|
runSetupWizard: () => runSetupWizard,
|
|
40486
40809
|
runtimeDirFor: () => runtimeDirFor
|
|
40487
40810
|
});
|
|
40488
|
-
import * as
|
|
40811
|
+
import * as nodeFs5 from "node:fs";
|
|
40489
40812
|
import { execFileSync as execFileSync3, spawn as nodeSpawn2 } from "node:child_process";
|
|
40490
40813
|
import { homedir as homedir2 } from "node:os";
|
|
40491
40814
|
import { join as join6 } from "node:path";
|
|
@@ -40501,7 +40824,7 @@ async function defaultProbeQueue(url) {
|
|
|
40501
40824
|
async function detectDeployment({
|
|
40502
40825
|
env = process.env,
|
|
40503
40826
|
cwd = process.cwd(),
|
|
40504
|
-
fs =
|
|
40827
|
+
fs = nodeFs5,
|
|
40505
40828
|
probeQueue = defaultProbeQueue
|
|
40506
40829
|
} = {}) {
|
|
40507
40830
|
const pointerRes = readPointer({ path: pointerPath(env), fs });
|
|
@@ -40628,7 +40951,7 @@ async function ensureDocker(ui, notify, { platform, probeDockerFn }) {
|
|
|
40628
40951
|
function npmSpawnOptions(platform, dir) {
|
|
40629
40952
|
return platform === "win32" ? { bin: "npm.cmd", options: { cwd: dir, shell: true } } : { bin: "npm", options: { cwd: dir } };
|
|
40630
40953
|
}
|
|
40631
|
-
function listRepoSkills(cwd, fs =
|
|
40954
|
+
function listRepoSkills(cwd, fs = nodeFs5) {
|
|
40632
40955
|
try {
|
|
40633
40956
|
const root = join6(cwd, ".pi", "skills");
|
|
40634
40957
|
return fs.readdirSync(root).filter((name) => SKILL_NAME_RE.test(name) && fs.existsSync(join6(root, name, "SKILL.md")));
|
|
@@ -40656,7 +40979,7 @@ function ensureDeploymentPackageJson(fs, dir) {
|
|
|
40656
40979
|
}
|
|
40657
40980
|
async function runSetupWizard(paths, ctx, notify, deps = {}) {
|
|
40658
40981
|
const {
|
|
40659
|
-
fs =
|
|
40982
|
+
fs = nodeFs5,
|
|
40660
40983
|
env = process.env,
|
|
40661
40984
|
platform = process.platform,
|
|
40662
40985
|
execPath = process.execPath,
|
|
@@ -40970,7 +41293,7 @@ async function offerFirstTrigger(repoCwd, deployDir, ui, notify, { fs, listRepoS
|
|
|
40970
41293
|
);
|
|
40971
41294
|
}
|
|
40972
41295
|
function registerNudge(pi, deps = {}) {
|
|
40973
|
-
const { fs =
|
|
41296
|
+
const { fs = nodeFs5, env = process.env, homedirFn = homedir2 } = deps;
|
|
40974
41297
|
pi.on("session_start", (event, ctx) => {
|
|
40975
41298
|
try {
|
|
40976
41299
|
if (event?.reason !== "startup" || !ctx?.hasUI) return;
|
|
@@ -40995,8 +41318,8 @@ var init_setup_wizard = __esm({
|
|
|
40995
41318
|
init_deployment_pointer();
|
|
40996
41319
|
init_read_model();
|
|
40997
41320
|
init_index();
|
|
40998
|
-
RUNTIME_VERSION = "1.
|
|
40999
|
-
RECEIVER_VERSION = "1.
|
|
41321
|
+
RUNTIME_VERSION = "1.4.0";
|
|
41322
|
+
RECEIVER_VERSION = "1.3.0";
|
|
41000
41323
|
RUNTIME_PKG = "@edgehero/pi-dispatch";
|
|
41001
41324
|
RECEIVER_PKG = "@edgehero/pi-dispatch-receiver";
|
|
41002
41325
|
NUDGE_MARKER_BASENAME = "pi-dispatch-setup.nudged";
|
|
@@ -41112,7 +41435,7 @@ var init_secrets_command = __esm({
|
|
|
41112
41435
|
// src/index.ts
|
|
41113
41436
|
import { VERSION } from "@earendil-works/pi-coding-agent";
|
|
41114
41437
|
import { fileURLToPath as fileURLToPath2, pathToFileURL } from "node:url";
|
|
41115
|
-
import * as
|
|
41438
|
+
import * as nodeFs6 from "node:fs";
|
|
41116
41439
|
function computePiVersionAdvisory(version, supported) {
|
|
41117
41440
|
if (version === supported || version === "0.0.0") return void 0;
|
|
41118
41441
|
return `pi-dispatch admin: running on pi ${version}, tested with pi ${supported} \u2014 things should work; report anything odd, and check for a newer @edgehero/pi-dispatch-admin`;
|
|
@@ -41254,7 +41577,7 @@ function registerTools(pi) {
|
|
|
41254
41577
|
pi.registerTool({
|
|
41255
41578
|
name: "dispatch_triggers",
|
|
41256
41579
|
label: "pi-dispatch triggers",
|
|
41257
|
-
description: "Read-only. Lists the configured triggers as `{ index, ...trigger }` entries. Use the `index` to target a specific trigger with dispatch_trigger_edit or dispatch_trigger_delete.",
|
|
41580
|
+
description: "Read-only. Lists the configured triggers as `{ index, ...trigger }` entries. Use the `index` to target a specific trigger with dispatch_trigger_edit or dispatch_trigger_delete. A close-capable entry (kind issue, or a pull_request on the forge's close word) may carry `on.number` (narrowed to one item), `on.once` (a one-shot), and `on.disarmed` { at, jobId? } \u2014 the worker's mark that the one-shot fired; a disarmed entry still lists here but matches nothing until an operator deletes the key to re-arm it.",
|
|
41258
41581
|
parameters: typebox_exports.Object({}),
|
|
41259
41582
|
async execute() {
|
|
41260
41583
|
const paths = resolvePaths(process.env);
|
|
@@ -41299,7 +41622,7 @@ function registerTools(pi) {
|
|
|
41299
41622
|
pi.registerTool({
|
|
41300
41623
|
name: "dispatch_trigger_add",
|
|
41301
41624
|
label: "pi-dispatch add trigger",
|
|
41302
|
-
description: "Adds a trigger to triggers.json and applies it live. The operator MUST approve a confirm dialog showing the entry; with no interactive operator it is refused. `flow` is the .pi/skills/<name> skill the job runs (its SKILL.md is the agent's instructions). `kind` = cron|label|comment|pull_request. cron (local) needs id, pattern, `folder` (absolute host path the job runs in), `flow`, and `task` (the prompt text handed to the agent), and may set optional model/provider/maxTurns for that schedule (omit = deployment default). label needs labels[]+flow; comment needs phrase+flow; pull_request needs action[] (+ optional labels[]) + flow. Webhook triggers take an optional `forge` = github (default) | gitlab | forgejo | azure, which also decides which action words pull_request accepts: github is labeled|opened|synchronize|reopened|review_submitted, gitlab is open|update|reopen|approved, forgejo is label_updated|opened|synchronized|reopened, azure is created|updated. An azure label or comment trigger must also set `repository` (a work item belongs to a project, not a repository), and an azure pull_request trigger may not carry labels[] at all. A github review_submitted trigger may also set reviewState[] (approved|changes_requested|commented) to narrow which verdicts fire; omitted, all three do. For webhook triggers the repo and the task come from the triggering issue/PR event \u2014 set only the match + flow \u2014 and they run under the deployment default model.",
|
|
41625
|
+
description: "Adds a trigger to triggers.json and applies it live. The operator MUST approve a confirm dialog showing the entry; with no interactive operator it is refused. `flow` is the .pi/skills/<name> skill the job runs (its SKILL.md is the agent's instructions). `kind` = cron|label|comment|pull_request|issue. cron (local) needs id, pattern, `folder` (absolute host path the job runs in), `flow`, and `task` (the prompt text handed to the agent), and may set optional model/provider/maxTurns for that schedule (omit = deployment default). label needs labels[]+flow; comment needs phrase+flow; pull_request needs action[] (+ optional labels[]) + flow. issue fires when an ISSUE closes: it needs flow, its action[] defaults to the forge's close word (github/forgejo closed, gitlab close; azure has no close trigger), and it may set `number` (the forge's own item number, narrowing the rule to that one issue) and `once: true` (a one-shot: it fires once, the run is recorded, then the worker disarms the entry by writing on.disarmed; once requires number). A close-only pull_request rule accepts the same number/once narrowing. Webhook triggers take an optional `forge` = github (default) | gitlab | forgejo | azure, which also decides which action words pull_request accepts: github is labeled|opened|synchronize|reopened|review_submitted|closed, gitlab is open|update|reopen|approved|close, forgejo is label_updated|opened|synchronized|reopened|closed, azure is created|updated (no close word). The close word rides alone: it cannot be mixed with other actions in one entry. An azure label or comment trigger must also set `repository` (a work item belongs to a project, not a repository), and an azure pull_request trigger may not carry labels[] at all. A github review_submitted trigger may also set reviewState[] (approved|changes_requested|commented) to narrow which verdicts fire; omitted, all three do. For webhook triggers the repo and the task come from the triggering issue/PR event \u2014 set only the match + flow \u2014 and they run under the deployment default model.",
|
|
41303
41626
|
executionMode: "sequential",
|
|
41304
41627
|
parameters: typebox_exports.Object({
|
|
41305
41628
|
kind: typebox_exports.String(),
|
|
@@ -41316,13 +41639,18 @@ function registerTools(pi) {
|
|
|
41316
41639
|
phrase: typebox_exports.Optional(typebox_exports.String()),
|
|
41317
41640
|
labels: typebox_exports.Optional(typebox_exports.Array(typebox_exports.String())),
|
|
41318
41641
|
action: typebox_exports.Optional(typebox_exports.Array(typebox_exports.String())),
|
|
41642
|
+
// The close-trigger narrowings (issue #231), read only by the close-capable arms of
|
|
41643
|
+
// buildTriggerEntry and refused fail-loud by the shared validator everywhere else -- the
|
|
41644
|
+
// `repository` posture above, restated for the two fields whose whole point is narrowing spend.
|
|
41645
|
+
number: typebox_exports.Optional(typebox_exports.Integer({ minimum: 1 })),
|
|
41646
|
+
once: typebox_exports.Optional(typebox_exports.Boolean()),
|
|
41319
41647
|
model: typebox_exports.Optional(typebox_exports.String()),
|
|
41320
41648
|
provider: typebox_exports.Optional(typebox_exports.String()),
|
|
41321
41649
|
maxTurns: typebox_exports.Optional(typebox_exports.Integer({ minimum: 1 }))
|
|
41322
41650
|
}),
|
|
41323
41651
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
41324
41652
|
const entry = buildTriggerEntry(params.kind, params);
|
|
41325
|
-
if (!entry) throw new Error(`unknown trigger kind '${params.kind}' (cron|label|comment|pull_request)`);
|
|
41653
|
+
if (!entry) throw new Error(`unknown trigger kind '${params.kind}' (cron|label|comment|pull_request|issue)`);
|
|
41326
41654
|
const result = await confirmedWrite(
|
|
41327
41655
|
ctx,
|
|
41328
41656
|
{ title: `Add ${params.kind} trigger`, message: `Add to triggers.json:
|
|
@@ -41533,6 +41861,17 @@ function buildTriggerEntry(kind, f) {
|
|
|
41533
41861
|
const on = { type: "pull_request", action: asWords(f.action) };
|
|
41534
41862
|
const any = asWords(f.labels ?? f.any);
|
|
41535
41863
|
if (any.length > 0) on.any = any;
|
|
41864
|
+
const prNumber = optInt(f.number);
|
|
41865
|
+
if (prNumber !== void 0) on.number = prNumber;
|
|
41866
|
+
if (typeof f.once === "boolean") on.once = f.once;
|
|
41867
|
+
return { on, run: { kind: forge, flow: f.flow } };
|
|
41868
|
+
}
|
|
41869
|
+
if (kind === "issue") {
|
|
41870
|
+
const action = asWords(f.action);
|
|
41871
|
+
const on = { type: "issue", action: action.length > 0 ? action : [ISSUE_CLOSE_WORD[forge] ?? "closed"] };
|
|
41872
|
+
const number = optInt(f.number);
|
|
41873
|
+
if (number !== void 0) on.number = number;
|
|
41874
|
+
if (typeof f.once === "boolean") on.once = f.once;
|
|
41536
41875
|
return { on, run: { kind: forge, flow: f.flow } };
|
|
41537
41876
|
}
|
|
41538
41877
|
return null;
|
|
@@ -41584,7 +41923,7 @@ async function dispatch(pi, args, ctx) {
|
|
|
41584
41923
|
if (det.state === "pointer" && !runtimeSkewNoticed) {
|
|
41585
41924
|
const ptrRes = readPointer({ path: pointerPath(process.env) });
|
|
41586
41925
|
const deploymentDir = ptrRes.pointer?.deploymentDir;
|
|
41587
|
-
const installed = deploymentDir ? readInstalledVersion2(
|
|
41926
|
+
const installed = deploymentDir ? readInstalledVersion2(nodeFs6, runtimeDirFor2(deploymentDir)) : void 0;
|
|
41588
41927
|
if (typeof installed === "string" && installed !== RUNTIME_VERSION2) {
|
|
41589
41928
|
runtimeSkewNoticed = true;
|
|
41590
41929
|
notify?.(
|
|
@@ -41743,7 +42082,7 @@ async function assembleGraph(paths) {
|
|
|
41743
42082
|
});
|
|
41744
42083
|
}
|
|
41745
42084
|
function realArtifactDeps() {
|
|
41746
|
-
return { fs:
|
|
42085
|
+
return { fs: nodeFs6, openBrowser, env: process.env, now: () => Date.now(), platform: process.platform };
|
|
41747
42086
|
}
|
|
41748
42087
|
function isHeadlessEnv(env, platform) {
|
|
41749
42088
|
if (env?.SSH_CONNECTION || env?.SSH_TTY) return "SSH session";
|
|
@@ -42269,7 +42608,7 @@ function completeArguments(prefix) {
|
|
|
42269
42608
|
}
|
|
42270
42609
|
return null;
|
|
42271
42610
|
}
|
|
42272
|
-
var USED_API, SUPPORTED_PI_VERSION, piVersionAdvisory, piVersionCheckDone, runtimeSkewNoticed, CHANNEL, REBUILT_NOTICE, USAGE, KNOWN_SUBCOMMANDS, FORGE_PROMPT, PR_ACTION_VOCAB, PRICING, INSIGHTS_USAGE, VIEWPORT_LINES;
|
|
42611
|
+
var USED_API, SUPPORTED_PI_VERSION, piVersionAdvisory, piVersionCheckDone, runtimeSkewNoticed, CHANNEL, REBUILT_NOTICE, USAGE, KNOWN_SUBCOMMANDS, FORGE_PROMPT, ISSUE_CLOSE_WORD, PR_ACTION_VOCAB, PRICING, INSIGHTS_USAGE, VIEWPORT_LINES;
|
|
42273
42612
|
var init_index = __esm({
|
|
42274
42613
|
"src/index.ts"() {
|
|
42275
42614
|
init_build();
|
|
@@ -42312,10 +42651,14 @@ var init_index = __esm({
|
|
|
42312
42651
|
"secrets"
|
|
42313
42652
|
];
|
|
42314
42653
|
FORGE_PROMPT = "forge \u2014 github, gitlab, forgejo or azure";
|
|
42654
|
+
ISSUE_CLOSE_WORD = { github: "closed", gitlab: "close", forgejo: "closed" };
|
|
42315
42655
|
PR_ACTION_VOCAB = {
|
|
42316
|
-
|
|
42317
|
-
|
|
42318
|
-
|
|
42656
|
+
// The close words ride the hint too (issue #231): the dialog passes whatever is typed through the
|
|
42657
|
+
// shared validator, so a close-only rule IS authorable here, and a hint that omits the word reads
|
|
42658
|
+
// as the word not existing. The loader refuses a list mixing a close word with any other action.
|
|
42659
|
+
github: { hint: "labeled opened synchronize reopened review_submitted closed", dflt: "labeled" },
|
|
42660
|
+
gitlab: { hint: "open update reopen approved close", dflt: "update" },
|
|
42661
|
+
forgejo: { hint: "label_updated opened synchronized reopened closed", dflt: "label_updated" },
|
|
42319
42662
|
azure: { hint: "created updated", dflt: "updated" }
|
|
42320
42663
|
};
|
|
42321
42664
|
PRICING = { listPricedModels, getPricedModel, isZeroRated, reprice, piAiVersion };
|