@edgehero/pi-dispatch-admin 1.2.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 +1061 -486
- package/package.json +1 -1
- package/skills/operate-pi-dispatch/SKILL.md +66 -6
package/dist/index.mjs
CHANGED
|
@@ -7611,7 +7611,7 @@ function isForgeKind(kind) {
|
|
|
7611
7611
|
function forgeSpec(kind) {
|
|
7612
7612
|
return isForgeKind(kind) ? FORGES[kind] : void 0;
|
|
7613
7613
|
}
|
|
7614
|
-
var FORGES, FORGE_KINDS, RUN_KINDS, MINTED_TOKEN_VARS;
|
|
7614
|
+
var FORGES, FORGE_KINDS, RUN_KINDS, MINTED_TOKEN_VARS, FORGE_HOST_VARS;
|
|
7615
7615
|
var init_forges = __esm({
|
|
7616
7616
|
"../worker/src/forges.mjs"() {
|
|
7617
7617
|
FORGES = {
|
|
@@ -7677,6 +7677,13 @@ var init_forges = __esm({
|
|
|
7677
7677
|
FORGE_KINDS = Object.freeze(Object.keys(FORGES));
|
|
7678
7678
|
RUN_KINDS = Object.freeze(["local", ...FORGE_KINDS]);
|
|
7679
7679
|
MINTED_TOKEN_VARS = new Set(FORGE_KINDS.flatMap((kind) => FORGES[kind].tokenVars));
|
|
7680
|
+
FORGE_HOST_VARS = new Set(FORGE_KINDS.map((kind) => FORGES[kind].hostVar).filter((name) => typeof name === "string"));
|
|
7681
|
+
}
|
|
7682
|
+
});
|
|
7683
|
+
|
|
7684
|
+
// ../worker/src/secret-profiles.mjs
|
|
7685
|
+
var init_secret_profiles = __esm({
|
|
7686
|
+
"../worker/src/secret-profiles.mjs"() {
|
|
7680
7687
|
}
|
|
7681
7688
|
});
|
|
7682
7689
|
|
|
@@ -7701,13 +7708,16 @@ function defaultGraphDir(env = process.env) {
|
|
|
7701
7708
|
function defaultSettingsFile() {
|
|
7702
7709
|
return `${process.env.TMPDIR ?? process.env.TEMP ?? "/tmp"}/pi-dispatch/settings.json`.replace(/\\/g, "/");
|
|
7703
7710
|
}
|
|
7704
|
-
var CHAIN_DEPTH_MAX_DEFAULT, CHAIN_MAX_PER_JOB_DEFAULT;
|
|
7711
|
+
var CHAIN_DEPTH_MAX_DEFAULT, CHAIN_MAX_PER_JOB_DEFAULT, WORKER_ONLY_SECRET_VARS, EGRESS_ENV_VARS;
|
|
7705
7712
|
var init_config2 = __esm({
|
|
7706
7713
|
"../worker/src/config.mjs"() {
|
|
7707
7714
|
init_egress();
|
|
7708
7715
|
init_forges();
|
|
7716
|
+
init_secret_profiles();
|
|
7709
7717
|
CHAIN_DEPTH_MAX_DEFAULT = 1;
|
|
7710
7718
|
CHAIN_MAX_PER_JOB_DEFAULT = 2;
|
|
7719
|
+
WORKER_ONLY_SECRET_VARS = /* @__PURE__ */ new Set(["GITHUB_APP_PRIVATE_KEY"]);
|
|
7720
|
+
EGRESS_ENV_VARS = /* @__PURE__ */ new Set(["HTTPS_PROXY", "HTTP_PROXY", "NO_PROXY", "NODE_USE_ENV_PROXY"]);
|
|
7711
7721
|
}
|
|
7712
7722
|
});
|
|
7713
7723
|
|
|
@@ -7752,6 +7762,21 @@ function validateOverlay(candidate, log) {
|
|
|
7752
7762
|
if (!isIntInRange(value, 1, 10)) return { invalid: "concurrency must be an integer 1-10" };
|
|
7753
7763
|
overlay[key] = value;
|
|
7754
7764
|
break;
|
|
7765
|
+
case "secretProfiles": {
|
|
7766
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
7767
|
+
return { invalid: "secretProfiles must be an object mapping profile names to resolver paths" };
|
|
7768
|
+
}
|
|
7769
|
+
for (const name of Object.keys(value)) {
|
|
7770
|
+
if (!/^[A-Za-z0-9._-]+$/.test(name)) {
|
|
7771
|
+
return { invalid: "secretProfiles names may use letters, digits, dot, dash and underscore only" };
|
|
7772
|
+
}
|
|
7773
|
+
if (typeof value[name] !== "string" || value[name].trim() === "") {
|
|
7774
|
+
return { invalid: `secretProfiles.${name} must be a non-empty absolute path` };
|
|
7775
|
+
}
|
|
7776
|
+
}
|
|
7777
|
+
overlay[key] = { ...value };
|
|
7778
|
+
break;
|
|
7779
|
+
}
|
|
7755
7780
|
case "softHoldPct":
|
|
7756
7781
|
if (!isIntInRange(value, 1, 99)) return { invalid: "softHoldPct must be an integer 1-99" };
|
|
7757
7782
|
overlay[key] = value;
|
|
@@ -7861,10 +7886,399 @@ var init_budget = __esm({
|
|
|
7861
7886
|
}
|
|
7862
7887
|
});
|
|
7863
7888
|
|
|
7864
|
-
// ../worker/src/
|
|
7889
|
+
// ../worker/src/pause-windows.mjs
|
|
7865
7890
|
function isNonEmptyString2(value) {
|
|
7866
7891
|
return typeof value === "string" && value.trim() !== "";
|
|
7867
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
|
+
|
|
8255
|
+
// ../worker/src/reserved-env.mjs
|
|
8256
|
+
var CONTAINER_ENV_NAMES;
|
|
8257
|
+
var init_reserved_env = __esm({
|
|
8258
|
+
"../worker/src/reserved-env.mjs"() {
|
|
8259
|
+
CONTAINER_ENV_NAMES = /* @__PURE__ */ new Set([
|
|
8260
|
+
"PI_PROVIDER",
|
|
8261
|
+
"PI_MODEL",
|
|
8262
|
+
"PI_MAX_TURNS",
|
|
8263
|
+
"PI_MAX_TOKENS",
|
|
8264
|
+
"PI_JOB_ID",
|
|
8265
|
+
"PI_GLOBAL_ALLOW_EXTENSIONS",
|
|
8266
|
+
"PI_PACKAGES",
|
|
8267
|
+
"PI_SESSION_FILE",
|
|
8268
|
+
"PI_FLOW",
|
|
8269
|
+
"PI_COMMAND",
|
|
8270
|
+
"PI_OFFLINE",
|
|
8271
|
+
"PLAYWRIGHT_BROWSERS_PATH",
|
|
8272
|
+
"PLAYWRIGHT_MCP_BROWSER",
|
|
8273
|
+
"PLAYWRIGHT_MCP_SANDBOX"
|
|
8274
|
+
]);
|
|
8275
|
+
}
|
|
8276
|
+
});
|
|
8277
|
+
|
|
8278
|
+
// ../worker/src/triggers.mjs
|
|
8279
|
+
function isNonEmptyString4(value) {
|
|
8280
|
+
return typeof value === "string" && value.trim() !== "";
|
|
8281
|
+
}
|
|
7868
8282
|
function parseTriggers(text, path) {
|
|
7869
8283
|
let parsed;
|
|
7870
8284
|
try {
|
|
@@ -7892,7 +8306,7 @@ function normalizeTrigger(entry, index, path, state) {
|
|
|
7892
8306
|
throw configError(`${at}: "run" must be an object: ${path}`);
|
|
7893
8307
|
}
|
|
7894
8308
|
if (!ON_TYPES.has(on.type)) {
|
|
7895
|
-
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}`);
|
|
7896
8310
|
}
|
|
7897
8311
|
if (!RUN_KINDS.includes(run.kind)) {
|
|
7898
8312
|
throw configError(`${at}: run.kind must be one of ${RUN_KINDS.join("|")} (got ${JSON.stringify(run.kind)}): ${path}`);
|
|
@@ -7906,14 +8320,18 @@ function normalizeTrigger(entry, index, path, state) {
|
|
|
7906
8320
|
if (!isForgeKind(run.kind)) {
|
|
7907
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}`);
|
|
7908
8322
|
}
|
|
7909
|
-
|
|
7910
|
-
if (on.type === "
|
|
7911
|
-
|
|
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;
|
|
7912
8330
|
}
|
|
7913
8331
|
function normalizeCron(on, run, index, path, state) {
|
|
7914
8332
|
const at = `trigger at index ${index}`;
|
|
7915
8333
|
const id = on.id;
|
|
7916
|
-
if (!
|
|
8334
|
+
if (!isNonEmptyString4(id)) {
|
|
7917
8335
|
throw configError(`${at}: cron on.id must be a non-empty string: ${path}`);
|
|
7918
8336
|
}
|
|
7919
8337
|
if (id.includes(":")) {
|
|
@@ -7927,7 +8345,7 @@ function normalizeCron(on, run, index, path, state) {
|
|
|
7927
8345
|
}
|
|
7928
8346
|
state.seenCronIds.add(id);
|
|
7929
8347
|
const pattern = on.pattern;
|
|
7930
|
-
if (!
|
|
8348
|
+
if (!isNonEmptyString4(pattern)) {
|
|
7931
8349
|
throw configError(`cron trigger "${id}": on.pattern must be a non-empty string: ${path}`);
|
|
7932
8350
|
}
|
|
7933
8351
|
const fieldCount = pattern.trim().split(/\s+/).length;
|
|
@@ -7935,13 +8353,13 @@ function normalizeCron(on, run, index, path, state) {
|
|
|
7935
8353
|
throw configError(`cron trigger "${id}": on.pattern must have 5 or 6 space-separated fields, got ${fieldCount}: ${path}`);
|
|
7936
8354
|
}
|
|
7937
8355
|
const command = validateCommand(run, `cron trigger "${id}"`, path, { onType: "cron" });
|
|
7938
|
-
if (!
|
|
8356
|
+
if (!isNonEmptyString4(run.folder)) {
|
|
7939
8357
|
throw configError(`cron trigger "${id}": run.folder must be a non-empty string: ${path}`);
|
|
7940
8358
|
}
|
|
7941
|
-
if (command === void 0 && !
|
|
8359
|
+
if (command === void 0 && !isNonEmptyString4(run.flow)) {
|
|
7942
8360
|
throw configError(`cron trigger "${id}": run.flow must be a non-empty string (or use run.command): ${path}`);
|
|
7943
8361
|
}
|
|
7944
|
-
if (command === void 0 && !
|
|
8362
|
+
if (command === void 0 && !isNonEmptyString4(run.task)) {
|
|
7945
8363
|
throw configError(`cron trigger "${id}": run.task must be a non-empty string: ${path}`);
|
|
7946
8364
|
}
|
|
7947
8365
|
if (run.github !== void 0 && typeof run.github !== "boolean") {
|
|
@@ -7953,9 +8371,14 @@ function normalizeCron(on, run, index, path, state) {
|
|
|
7953
8371
|
validateInstructions(run, `cron trigger "${id}"`, path, { cron: true });
|
|
7954
8372
|
const resume = validateResumeFlag(run, `cron trigger "${id}"`, path);
|
|
7955
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" });
|
|
8377
|
+
const secrets = validateSecrets(run, `cron trigger "${id}"`, path);
|
|
8378
|
+
const secretsProfile = validateSecretsProfile(run, `cron trigger "${id}"`, path);
|
|
7956
8379
|
return {
|
|
7957
8380
|
on: { type: "cron", id, pattern },
|
|
7958
|
-
run: { kind: "local", folder: run.folder, flow: run.flow, task: run.task, provider: run.provider, model: run.model, maxTurns: run.maxTurns, github: run.github, packages, image, resume, ...command !== void 0 && { command }, ...skillsDir !== void 0 && { skillsDir } }
|
|
8381
|
+
run: { kind: "local", folder: run.folder, flow: run.flow, task: run.task, provider: run.provider, model: run.model, maxTurns: run.maxTurns, github: run.github, packages, image, resume, ...command !== void 0 && { command }, ...skillsDir !== void 0 && { skillsDir }, ...secrets !== void 0 && { secrets }, ...secretsProfile !== void 0 && { secretsProfile } }
|
|
7959
8382
|
};
|
|
7960
8383
|
}
|
|
7961
8384
|
function validatePackagesFlag(run, at, path) {
|
|
@@ -8020,7 +8443,7 @@ function validateCommand(run, at, path, { onType }) {
|
|
|
8020
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}`);
|
|
8021
8444
|
}
|
|
8022
8445
|
if (raw === void 0) return void 0;
|
|
8023
|
-
if (!
|
|
8446
|
+
if (!isNonEmptyString4(raw)) {
|
|
8024
8447
|
throw configError(`${at}: run.command must be a non-empty string -- the registered command name, optionally followed by its arguments: ${path}`);
|
|
8025
8448
|
}
|
|
8026
8449
|
if (raw !== raw.trim()) {
|
|
@@ -8057,6 +8480,72 @@ function validatePredicate(on, index, path, requirePositive) {
|
|
|
8057
8480
|
}
|
|
8058
8481
|
return { any: on.any, all: on.all, none: on.none };
|
|
8059
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
|
+
}
|
|
8060
8549
|
function validateRepository(run, onType, at, path) {
|
|
8061
8550
|
const needed = run.kind === "azure" && (onType === "label" || onType === "comment");
|
|
8062
8551
|
const raw = run.repository;
|
|
@@ -8067,7 +8556,7 @@ function validateRepository(run, onType, at, path) {
|
|
|
8067
8556
|
if (!needed) {
|
|
8068
8557
|
throw configError(`${at}: run.repository is only meaningful on an azure label or comment trigger (a pull request names its own repository): ${path}`);
|
|
8069
8558
|
}
|
|
8070
|
-
if (!
|
|
8559
|
+
if (!isNonEmptyString4(raw) || raw !== raw.trim() || raw.includes("/")) {
|
|
8071
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}`);
|
|
8072
8561
|
}
|
|
8073
8562
|
return raw;
|
|
@@ -8086,13 +8575,67 @@ function validateReplicas(run, at, path) {
|
|
|
8086
8575
|
}
|
|
8087
8576
|
return replicas;
|
|
8088
8577
|
}
|
|
8578
|
+
function validateSecrets(run, at, path) {
|
|
8579
|
+
const secrets = run.secrets;
|
|
8580
|
+
if (secrets === void 0) return void 0;
|
|
8581
|
+
if (secrets === null || typeof secrets !== "object" || Array.isArray(secrets)) {
|
|
8582
|
+
throw configError(`${at}: run.secrets must be an object mapping environment variable names to references when present (got ${JSON.stringify(secrets)}): ${path}`);
|
|
8583
|
+
}
|
|
8584
|
+
const names = Object.keys(secrets);
|
|
8585
|
+
if (names.length === 0) {
|
|
8586
|
+
throw configError(`${at}: run.secrets is empty -- an empty map is a field that does nothing, and this trigger reads as though it binds a secret: ${path}`);
|
|
8587
|
+
}
|
|
8588
|
+
if (names.length > SECRETS_MAX) {
|
|
8589
|
+
throw configError(`${at}: run.secrets names ${names.length} variables, over the ${SECRETS_MAX} cap -- each one is resolved before the container starts, holding a concurrency slot while it runs: ${path}`);
|
|
8590
|
+
}
|
|
8591
|
+
for (const name of names) {
|
|
8592
|
+
if (!ENV_NAME.test(name)) {
|
|
8593
|
+
throw configError(`${at}: run.secrets key ${JSON.stringify(name)} is not an environment variable name (letters, digits and underscore, not starting with a digit): ${path}`);
|
|
8594
|
+
}
|
|
8595
|
+
if (RESERVED_ENV_NAMES.has(name)) {
|
|
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}`);
|
|
8597
|
+
}
|
|
8598
|
+
const reference = secrets[name];
|
|
8599
|
+
if (!isNonEmptyString4(reference)) {
|
|
8600
|
+
throw configError(`${at}: run.secrets.${name} must be a non-empty string reference for your resolver to read (got ${JSON.stringify(reference)}): ${path}`);
|
|
8601
|
+
}
|
|
8602
|
+
if (reference !== reference.trim()) {
|
|
8603
|
+
throw configError(`${at}: run.secrets.${name} must not have leading or trailing whitespace -- the file is the reviewed artifact and silently trimming it would make the file disagree with what the resolver is asked for: ${path}`);
|
|
8604
|
+
}
|
|
8605
|
+
if (reference.startsWith("-")) {
|
|
8606
|
+
throw configError(`${at}: run.secrets.${name} must not start with "-" -- it is passed as the resolver's first argument, where a leading dash parses as a flag: ${path}`);
|
|
8607
|
+
}
|
|
8608
|
+
}
|
|
8609
|
+
if (run.resume === true) {
|
|
8610
|
+
throw configError(`${at}: run.secrets and run.resume cannot be combined -- a resumed job replays a transcript kept on host disk, and any command the agent ran that echoed a resolved value wrote it into that transcript, which is then prefilled into every later job on the same key: ${path}`);
|
|
8611
|
+
}
|
|
8612
|
+
return { ...secrets };
|
|
8613
|
+
}
|
|
8614
|
+
function validateSecretsProfile(run, at, path) {
|
|
8615
|
+
const profile = run.secretsProfile;
|
|
8616
|
+
if (profile === void 0) return void 0;
|
|
8617
|
+
if (!isNonEmptyString4(profile)) {
|
|
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}`);
|
|
8619
|
+
}
|
|
8620
|
+
if (!ID_CHARSET.test(profile)) {
|
|
8621
|
+
throw configError(`${at}: run.secretsProfile ${JSON.stringify(profile)} may use letters, digits, dot, dash and underscore only -- PI_SECRET_PROFILES is a comma-separated list of name:path pairs, so a name carrying either separator cannot be declared: ${path}`);
|
|
8622
|
+
}
|
|
8623
|
+
if (run.secrets === void 0) {
|
|
8624
|
+
throw configError(`${at}: run.secretsProfile is set but run.secrets names nothing -- a profile that resolves no references is a field that does nothing: ${path}`);
|
|
8625
|
+
}
|
|
8626
|
+
return profile;
|
|
8627
|
+
}
|
|
8089
8628
|
function normalizeLabel(on, run, index, path) {
|
|
8090
8629
|
const at = `trigger at index ${index}`;
|
|
8091
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" });
|
|
8092
8634
|
const command = validateCommand(run, at, path, { onType: "label" });
|
|
8093
|
-
if (command === void 0 && !
|
|
8635
|
+
if (command === void 0 && !isNonEmptyString4(run.flow)) {
|
|
8094
8636
|
throw configError(`${at}: label trigger run.flow must be a non-empty string (or use run.command): ${path}`);
|
|
8095
8637
|
}
|
|
8638
|
+
validateFlowName(run, at, path);
|
|
8096
8639
|
const packages = validatePackagesFlag(run, at, path);
|
|
8097
8640
|
const image = validateImageRef(run, at, path);
|
|
8098
8641
|
const skillsDir = validateSkillsDir(run, at, path);
|
|
@@ -8100,20 +8643,26 @@ function normalizeLabel(on, run, index, path) {
|
|
|
8100
8643
|
const resume = validateResumeFlag(run, at, path);
|
|
8101
8644
|
const repository = validateRepository(run, "label", at, path);
|
|
8102
8645
|
const replicas = validateReplicas(run, at, path);
|
|
8646
|
+
const secrets = validateSecrets(run, at, path);
|
|
8647
|
+
const secretsProfile = validateSecretsProfile(run, at, path);
|
|
8103
8648
|
return {
|
|
8104
8649
|
on: { type: "label", any: predicate.any, all: predicate.all, none: predicate.none },
|
|
8105
|
-
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 } }
|
|
8650
|
+
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 } }
|
|
8106
8651
|
};
|
|
8107
8652
|
}
|
|
8108
8653
|
function normalizeComment(on, run, index, path, state) {
|
|
8109
8654
|
const at = `trigger at index ${index}`;
|
|
8110
|
-
if (!
|
|
8655
|
+
if (!isNonEmptyString4(on.phrase)) {
|
|
8111
8656
|
throw configError(`${at}: comment trigger on.phrase must be a non-empty string: ${path}`);
|
|
8112
8657
|
}
|
|
8658
|
+
validateNumber(on, at, path, { onType: "comment" });
|
|
8659
|
+
validateOnce(on, run, at, path, { onType: "comment" });
|
|
8660
|
+
validateDisarmed(on, at, path, { onType: "comment" });
|
|
8113
8661
|
const command = validateCommand(run, at, path, { onType: "comment" });
|
|
8114
|
-
if (command === void 0 && !
|
|
8662
|
+
if (command === void 0 && !isNonEmptyString4(run.flow)) {
|
|
8115
8663
|
throw configError(`${at}: comment trigger run.flow (the default flow) must be a non-empty string (or use run.command): ${path}`);
|
|
8116
8664
|
}
|
|
8665
|
+
validateFlowName(run, at, path);
|
|
8117
8666
|
state.commentCounts[run.kind] = (state.commentCounts[run.kind] ?? 0) + 1;
|
|
8118
8667
|
if (state.commentCounts[run.kind] > 1) {
|
|
8119
8668
|
throw configError(`${at}: at most one ${run.kind} comment trigger is allowed: ${path}`);
|
|
@@ -8125,9 +8674,59 @@ function normalizeComment(on, run, index, path, state) {
|
|
|
8125
8674
|
const resume = validateResumeFlag(run, at, path);
|
|
8126
8675
|
const repository = validateRepository(run, "comment", at, path);
|
|
8127
8676
|
const replicas = validateReplicas(run, at, path);
|
|
8677
|
+
const secrets = validateSecrets(run, at, path);
|
|
8678
|
+
const secretsProfile = validateSecretsProfile(run, at, path);
|
|
8128
8679
|
return {
|
|
8129
8680
|
on: { type: "comment", phrase: on.phrase },
|
|
8130
|
-
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 } }
|
|
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 } }
|
|
8682
|
+
};
|
|
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 } }
|
|
8131
8730
|
};
|
|
8132
8731
|
}
|
|
8133
8732
|
function normalizePullRequest(on, run, index, path) {
|
|
@@ -8143,17 +8742,29 @@ function normalizePullRequest(on, run, index, path) {
|
|
|
8143
8742
|
throw configError(`${at}: pull_request on.action has an unsupported ${run.kind} action ${JSON.stringify(a)} (expected ${expected}): ${path}`);
|
|
8144
8743
|
}
|
|
8145
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
|
+
}
|
|
8146
8750
|
const reviewState = validateReviewState(on, actions, run, at, path);
|
|
8147
8751
|
const labelAction = forgeSpec(run.kind)?.prLabelAction;
|
|
8148
8752
|
const requirePositive = typeof labelAction === "string" && actions.includes(labelAction);
|
|
8149
8753
|
if (run.kind === "azure" && (on.any !== void 0 || on.all !== void 0 || on.none !== void 0)) {
|
|
8150
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}`);
|
|
8151
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
|
+
}
|
|
8152
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" });
|
|
8153
8763
|
const command = validateCommand(run, at, path, { onType: "pull_request" });
|
|
8154
|
-
if (command === void 0 && !
|
|
8764
|
+
if (command === void 0 && !isNonEmptyString4(run.flow)) {
|
|
8155
8765
|
throw configError(`${at}: pull_request trigger run.flow must be a non-empty string (or use run.command): ${path}`);
|
|
8156
8766
|
}
|
|
8767
|
+
validateFlowName(run, at, path);
|
|
8157
8768
|
const packages = validatePackagesFlag(run, at, path);
|
|
8158
8769
|
const image = validateImageRef(run, at, path);
|
|
8159
8770
|
const skillsDir = validateSkillsDir(run, at, path);
|
|
@@ -8161,6 +8772,8 @@ function normalizePullRequest(on, run, index, path) {
|
|
|
8161
8772
|
const resume = validateResumeFlag(run, at, path);
|
|
8162
8773
|
validateRepository(run, "pull_request", at, path);
|
|
8163
8774
|
const replicas = validateReplicas(run, at, path);
|
|
8775
|
+
const secrets = validateSecrets(run, at, path);
|
|
8776
|
+
const secretsProfile = validateSecretsProfile(run, at, path);
|
|
8164
8777
|
return {
|
|
8165
8778
|
on: {
|
|
8166
8779
|
type: "pull_request",
|
|
@@ -8170,9 +8783,12 @@ function normalizePullRequest(on, run, index, path) {
|
|
|
8170
8783
|
...reviewState !== void 0 && { reviewState },
|
|
8171
8784
|
any: predicate.any,
|
|
8172
8785
|
all: predicate.all,
|
|
8173
|
-
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 }
|
|
8174
8790
|
},
|
|
8175
|
-
run: { kind: run.kind, flow: run.flow, packages, image, resume, replicas, ...command !== void 0 && { command }, ...skillsDir !== void 0 && { skillsDir }, ...instructions !== void 0 && { instructions } }
|
|
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 } }
|
|
8176
8792
|
};
|
|
8177
8793
|
}
|
|
8178
8794
|
function validateReviewState(on, actions, run, at, path) {
|
|
@@ -8194,366 +8810,58 @@ function validateReviewState(on, actions, run, at, path) {
|
|
|
8194
8810
|
}
|
|
8195
8811
|
return [...on.reviewState];
|
|
8196
8812
|
}
|
|
8197
|
-
var ON_TYPES, PR_ACTIONS, REVIEW_STATES, REVIEW_ACTION, ID_CHARSET, REPLICAS_MAX, 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;
|
|
8198
8814
|
var init_triggers = __esm({
|
|
8199
8815
|
"../worker/src/triggers.mjs"() {
|
|
8200
8816
|
init_config2();
|
|
8817
|
+
init_flow_gate();
|
|
8201
8818
|
init_forges();
|
|
8202
|
-
|
|
8819
|
+
init_reserved_env();
|
|
8820
|
+
ON_TYPES = /* @__PURE__ */ new Set(["cron", "label", "comment", "pull_request", "issue"]);
|
|
8821
|
+
DISARMED_TYPE = "disarmed";
|
|
8203
8822
|
PR_ACTIONS = {
|
|
8204
|
-
github: /* @__PURE__ */ new Set(["labeled", "opened", "synchronize", "reopened", "review_submitted"]),
|
|
8823
|
+
github: /* @__PURE__ */ new Set(["labeled", "opened", "synchronize", "reopened", "review_submitted", "closed"]),
|
|
8205
8824
|
// GitLab's `approved` is its review gate (a member approved the MR). It is NOT github's
|
|
8206
8825
|
// `review_submitted` renamed: `approved` is one verdict, `review_submitted` is every verdict, which is
|
|
8207
|
-
// what `on.reviewState` below exists to narrow.
|
|
8208
|
-
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"]),
|
|
8209
8828
|
// Forgejo's own spellings. `label_updated` is its `labeled` and `synchronized` its `synchronize` -- a
|
|
8210
8829
|
// one-letter difference that an operator would otherwise discover as a trigger that loads clean and
|
|
8211
8830
|
// never fires. `label_cleared` is deliberately ABSENT and always will be: REMOVING a label must never
|
|
8212
8831
|
// start a paid run, and it has no GitHub counterpart to inherit that rule from.
|
|
8213
|
-
forgejo: /* @__PURE__ */ new Set(["label_updated", "opened", "synchronized", "reopened"]),
|
|
8832
|
+
forgejo: /* @__PURE__ */ new Set(["label_updated", "opened", "synchronized", "reopened", "closed"]),
|
|
8214
8833
|
// Azure's Service Hook events reduced to the two that leave something to act on. `git.pullrequest.merged`
|
|
8215
|
-
// is omitted for the
|
|
8216
|
-
//
|
|
8217
|
-
//
|
|
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.
|
|
8218
8840
|
azure: /* @__PURE__ */ new Set(["created", "updated"])
|
|
8219
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
|
+
};
|
|
8220
8848
|
REVIEW_STATES = /* @__PURE__ */ new Set(["approved", "changes_requested", "commented"]);
|
|
8221
8849
|
REVIEW_ACTION = "review_submitted";
|
|
8222
8850
|
ID_CHARSET = /^[A-Za-z0-9._-]+$/;
|
|
8223
8851
|
REPLICAS_MAX = 3;
|
|
8852
|
+
SECRETS_MAX = 16;
|
|
8853
|
+
ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
8854
|
+
RESERVED_ENV_NAMES = /* @__PURE__ */ new Set([...MINTED_TOKEN_VARS, ...FORGE_HOST_VARS, ...WORKER_ONLY_SECRET_VARS, ...EGRESS_ENV_VARS, ...CONTAINER_ENV_NAMES]);
|
|
8224
8855
|
INSTRUCTIONS_MAX = 2e3;
|
|
8225
8856
|
}
|
|
8226
8857
|
});
|
|
8227
8858
|
|
|
8228
|
-
// ../worker/src/pause-windows.mjs
|
|
8229
|
-
function isNonEmptyString3(value) {
|
|
8230
|
-
return typeof value === "string" && value.trim() !== "";
|
|
8231
|
-
}
|
|
8232
|
-
function parseHHMM(value, label, at, path) {
|
|
8233
|
-
const m = isNonEmptyString3(value) ? /^([0-9]{1,2}):([0-9]{2})$/.exec(value.trim()) : null;
|
|
8234
|
-
if (!m) throw configError(`${at}: ${label} must be "HH:MM": ${JSON.stringify(value)} (${path})`);
|
|
8235
|
-
const h = Number(m[1]);
|
|
8236
|
-
const min = Number(m[2]);
|
|
8237
|
-
if (h > 23 || min > 59) throw configError(`${at}: ${label} out of range 00:00-23:59: ${JSON.stringify(value)} (${path})`);
|
|
8238
|
-
return h * 60 + min;
|
|
8239
|
-
}
|
|
8240
|
-
function assertDate(value, label, at, path) {
|
|
8241
|
-
const m = /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.exec(String(value).trim());
|
|
8242
|
-
const mo = m && Number(m[2]);
|
|
8243
|
-
const d = m && Number(m[3]);
|
|
8244
|
-
if (!m || mo < 1 || mo > 12 || d < 1 || d > 31) {
|
|
8245
|
-
throw configError(`${at}: ${label} must be "YYYY-MM-DD": ${JSON.stringify(value)} (${path})`);
|
|
8246
|
-
}
|
|
8247
|
-
}
|
|
8248
|
-
function validateTz(tz, at, path) {
|
|
8249
|
-
try {
|
|
8250
|
-
new Intl.DateTimeFormat("en-US", { timeZone: tz });
|
|
8251
|
-
return tz;
|
|
8252
|
-
} catch {
|
|
8253
|
-
throw configError(`${at}: tz is not a valid IANA timezone: ${JSON.stringify(tz)} (${path})`);
|
|
8254
|
-
}
|
|
8255
|
-
}
|
|
8256
|
-
function parsePauseWindows(text, path) {
|
|
8257
|
-
let parsed;
|
|
8258
|
-
try {
|
|
8259
|
-
parsed = JSON.parse(text);
|
|
8260
|
-
} catch (error) {
|
|
8261
|
-
throw configError(`pause-windows file is not valid JSON: ${path} (${error.message})`);
|
|
8262
|
-
}
|
|
8263
|
-
const windows = parsed?.windows;
|
|
8264
|
-
if (!Array.isArray(windows)) {
|
|
8265
|
-
throw configError(`pause-windows file must have a "windows" array: ${path}`);
|
|
8266
|
-
}
|
|
8267
|
-
return windows.map((w, index) => normalizeWindow(w, index, path));
|
|
8268
|
-
}
|
|
8269
|
-
function normalizeWindow(w, index, path) {
|
|
8270
|
-
const at = `pause window at index ${index}`;
|
|
8271
|
-
if (w === null || typeof w !== "object") throw configError(`${at}: must be an object: ${path}`);
|
|
8272
|
-
if (!isNonEmptyString3(w.scope)) throw configError(`${at}: scope must be a non-empty string: ${path}`);
|
|
8273
|
-
const fromMin = parseHHMM(w.from, "from", at, path);
|
|
8274
|
-
const toMin = parseHHMM(w.to, "to", at, path);
|
|
8275
|
-
if (fromMin === toMin) {
|
|
8276
|
-
throw configError(`${at}: from and to must differ (a 24h pause is not expressible; remove the trigger): ${path}`);
|
|
8277
|
-
}
|
|
8278
|
-
const tz = w.tz === void 0 ? "UTC" : validateTz(w.tz, at, path);
|
|
8279
|
-
let days = null;
|
|
8280
|
-
if (w.days !== void 0) {
|
|
8281
|
-
if (!Array.isArray(w.days) || w.days.length === 0) {
|
|
8282
|
-
throw configError(`${at}: days must be a non-empty array of weekday names (mon..sun): ${path}`);
|
|
8283
|
-
}
|
|
8284
|
-
days = w.days.map((d) => {
|
|
8285
|
-
const key = String(d).trim().toLowerCase().slice(0, 3);
|
|
8286
|
-
if (!DAY_SET.has(key)) throw configError(`${at}: unknown weekday ${JSON.stringify(d)} (use mon..sun): ${path}`);
|
|
8287
|
-
return key;
|
|
8288
|
-
});
|
|
8289
|
-
}
|
|
8290
|
-
let dateFrom = null;
|
|
8291
|
-
let dateTo = null;
|
|
8292
|
-
if (w.dateFrom !== void 0) {
|
|
8293
|
-
assertDate(w.dateFrom, "dateFrom", at, path);
|
|
8294
|
-
dateFrom = String(w.dateFrom).trim();
|
|
8295
|
-
}
|
|
8296
|
-
if (w.dateTo !== void 0) {
|
|
8297
|
-
assertDate(w.dateTo, "dateTo", at, path);
|
|
8298
|
-
dateTo = String(w.dateTo).trim();
|
|
8299
|
-
}
|
|
8300
|
-
if (dateFrom && dateTo && dateFrom > dateTo) throw configError(`${at}: dateFrom must be <= dateTo: ${path}`);
|
|
8301
|
-
const norm = { scope: w.scope.trim(), from: w.from.trim(), to: w.to.trim(), fromMin, toMin, tz };
|
|
8302
|
-
if (days) norm.days = days;
|
|
8303
|
-
if (dateFrom) norm.dateFrom = dateFrom;
|
|
8304
|
-
if (dateTo) norm.dateTo = dateTo;
|
|
8305
|
-
return norm;
|
|
8306
|
-
}
|
|
8307
|
-
function zonedParts(tz, ms) {
|
|
8308
|
-
const fmt2 = new Intl.DateTimeFormat("en-US", {
|
|
8309
|
-
timeZone: tz,
|
|
8310
|
-
hour12: false,
|
|
8311
|
-
year: "numeric",
|
|
8312
|
-
month: "2-digit",
|
|
8313
|
-
day: "2-digit",
|
|
8314
|
-
hour: "2-digit",
|
|
8315
|
-
minute: "2-digit",
|
|
8316
|
-
weekday: "short"
|
|
8317
|
-
});
|
|
8318
|
-
const p = {};
|
|
8319
|
-
for (const part of fmt2.formatToParts(new Date(ms))) p[part.type] = part.value;
|
|
8320
|
-
let hh = Number(p.hour);
|
|
8321
|
-
if (hh === 24) hh = 0;
|
|
8322
|
-
return { y: Number(p.year), mo: Number(p.month), d: Number(p.day), hh, mi: Number(p.minute), dow: WEEKDAY_INDEX[p.weekday] };
|
|
8323
|
-
}
|
|
8324
|
-
function wallToMs(tz, y, mo, d, hh, mi) {
|
|
8325
|
-
const guess = Date.UTC(y, mo - 1, d, hh, mi);
|
|
8326
|
-
const shown = zonedParts(tz, guess);
|
|
8327
|
-
const shownAsUtc = Date.UTC(shown.y, shown.mo - 1, shown.d, shown.hh, shown.mi);
|
|
8328
|
-
return guess - (shownAsUtc - guess);
|
|
8329
|
-
}
|
|
8330
|
-
function shiftDay(tz, parts, delta) {
|
|
8331
|
-
const noon = wallToMs(tz, parts.y, parts.mo, parts.d, 12, 0);
|
|
8332
|
-
return zonedParts(tz, noon + delta * 864e5);
|
|
8333
|
-
}
|
|
8334
|
-
function ymd(parts) {
|
|
8335
|
-
return `${String(parts.y).padStart(4, "0")}-${String(parts.mo).padStart(2, "0")}-${String(parts.d).padStart(2, "0")}`;
|
|
8336
|
-
}
|
|
8337
|
-
function qualifies(w, parts) {
|
|
8338
|
-
if (w.days && !w.days.includes(DAYS[parts.dow])) return false;
|
|
8339
|
-
const date = ymd(parts);
|
|
8340
|
-
if (w.dateFrom && date < w.dateFrom) return false;
|
|
8341
|
-
if (w.dateTo && date > w.dateTo) return false;
|
|
8342
|
-
return true;
|
|
8343
|
-
}
|
|
8344
|
-
function windowEndAt(w, nowMs) {
|
|
8345
|
-
const p = zonedParts(w.tz, nowMs);
|
|
8346
|
-
const nowMin = p.hh * 60 + p.mi;
|
|
8347
|
-
const endAt = (dateParts) => wallToMs(w.tz, dateParts.y, dateParts.mo, dateParts.d, Math.floor(w.toMin / 60), w.toMin % 60);
|
|
8348
|
-
if (w.fromMin < w.toMin) {
|
|
8349
|
-
if (nowMin >= w.fromMin && nowMin < w.toMin && qualifies(w, p)) return endAt(p);
|
|
8350
|
-
return null;
|
|
8351
|
-
}
|
|
8352
|
-
if (nowMin >= w.fromMin && qualifies(w, p)) return endAt(shiftDay(w.tz, p, 1));
|
|
8353
|
-
if (nowMin < w.toMin && qualifies(w, shiftDay(w.tz, p, -1))) return endAt(p);
|
|
8354
|
-
return null;
|
|
8355
|
-
}
|
|
8356
|
-
var DAYS, DAY_SET, WEEKDAY_INDEX;
|
|
8357
|
-
var init_pause_windows = __esm({
|
|
8358
|
-
"../worker/src/pause-windows.mjs"() {
|
|
8359
|
-
init_config2();
|
|
8360
|
-
DAYS = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
|
|
8361
|
-
DAY_SET = new Set(DAYS);
|
|
8362
|
-
WEEKDAY_INDEX = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 };
|
|
8363
|
-
}
|
|
8364
|
-
});
|
|
8365
|
-
|
|
8366
|
-
// ../worker/src/subscriptions.mjs
|
|
8367
|
-
function isNonEmptyString4(value) {
|
|
8368
|
-
return typeof value === "string" && value.trim() !== "";
|
|
8369
|
-
}
|
|
8370
|
-
function normalizeGlobs(value, label, at, path) {
|
|
8371
|
-
if (!Array.isArray(value) || value.length === 0) {
|
|
8372
|
-
throw configError(`${at}: ${label} must be a non-empty array of model globs: ${path}`);
|
|
8373
|
-
}
|
|
8374
|
-
return value.map((glob) => {
|
|
8375
|
-
if (typeof glob !== "string" || !MODEL_GLOB_RE.test(glob.trim())) {
|
|
8376
|
-
throw configError(`${at}: ${label} glob ${JSON.stringify(glob)} must match ${MODEL_GLOB_RE} (model-id charset plus "*", max 64 chars): ${path}`);
|
|
8377
|
-
}
|
|
8378
|
-
return glob.trim();
|
|
8379
|
-
});
|
|
8380
|
-
}
|
|
8381
|
-
function normalizeBool(value, label, at, path, fallback) {
|
|
8382
|
-
if (value === void 0) return fallback;
|
|
8383
|
-
if (typeof value !== "boolean") throw configError(`${at}: ${label} must be a boolean: ${path}`);
|
|
8384
|
-
return value;
|
|
8385
|
-
}
|
|
8386
|
-
function parseSubscriptions(text, path) {
|
|
8387
|
-
let parsed;
|
|
8388
|
-
try {
|
|
8389
|
-
parsed = JSON.parse(text);
|
|
8390
|
-
} catch (error) {
|
|
8391
|
-
throw configError(`subscriptions file is not valid JSON: ${path} (${error.message})`);
|
|
8392
|
-
}
|
|
8393
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
8394
|
-
throw configError(`subscriptions file must be a JSON object: ${path}`);
|
|
8395
|
-
}
|
|
8396
|
-
const version = parsed.version;
|
|
8397
|
-
if (!Number.isInteger(version) || version < 1) {
|
|
8398
|
-
throw configError(`subscriptions file must declare an integer "version" >= 1 (got ${JSON.stringify(version)}): ${path}`);
|
|
8399
|
-
}
|
|
8400
|
-
if (version > SUBSCRIPTIONS_VERSION) {
|
|
8401
|
-
throw configError(`subscriptions file written by a newer pi-dispatch (version ${version}; this build understands ${SUBSCRIPTIONS_VERSION}): ${path}`);
|
|
8402
|
-
}
|
|
8403
|
-
const entries = parsed.subscriptions;
|
|
8404
|
-
if (!Array.isArray(entries)) {
|
|
8405
|
-
throw configError(`subscriptions file must have a "subscriptions" array: ${path}`);
|
|
8406
|
-
}
|
|
8407
|
-
const seenIds = /* @__PURE__ */ new Map();
|
|
8408
|
-
return { version, subscriptions: entries.map((entry, index) => normalizeSubscription(entry, index, path, seenIds)) };
|
|
8409
|
-
}
|
|
8410
|
-
function normalizeSubscription(entry, index, path, seenIds) {
|
|
8411
|
-
const at = `subscription at index ${index}`;
|
|
8412
|
-
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
|
|
8413
|
-
throw configError(`${at}: must be an object: ${path}`);
|
|
8414
|
-
}
|
|
8415
|
-
if (!isNonEmptyString4(entry.id)) throw configError(`${at}: id must be a non-empty string: ${path}`);
|
|
8416
|
-
const id = entry.id.trim();
|
|
8417
|
-
if (seenIds.has(id)) {
|
|
8418
|
-
throw configError(`${at}: id ${JSON.stringify(id)} is already used by the subscription at index ${seenIds.get(id)}: ${path}`);
|
|
8419
|
-
}
|
|
8420
|
-
seenIds.set(id, index);
|
|
8421
|
-
if (!isNonEmptyString4(entry.vendor)) throw configError(`${at}: vendor must be a non-empty string: ${path}`);
|
|
8422
|
-
if (!isNonEmptyString4(entry.provider)) throw configError(`${at}: provider must be a non-empty string (the pi-ai provider id this plan covers): ${path}`);
|
|
8423
|
-
const models = entry.models === void 0 ? ["*"] : normalizeGlobs(entry.models, "models", at, path);
|
|
8424
|
-
const price = entry.price;
|
|
8425
|
-
if (price === null || typeof price !== "object" || Array.isArray(price)) {
|
|
8426
|
-
throw configError(`${at}: price must be an object { amount, currency, per }: ${path}`);
|
|
8427
|
-
}
|
|
8428
|
-
if (typeof price.amount !== "number" || !Number.isFinite(price.amount) || price.amount <= 0) {
|
|
8429
|
-
throw configError(`${at}: price.amount must be a number > 0: ${path}`);
|
|
8430
|
-
}
|
|
8431
|
-
if (typeof price.currency !== "string" || !/^[A-Z]{3}$/.test(price.currency)) {
|
|
8432
|
-
throw configError(`${at}: price.currency must be a 3-letter uppercase code (e.g. "USD"): ${path}`);
|
|
8433
|
-
}
|
|
8434
|
-
if (price.per !== "month") {
|
|
8435
|
-
throw configError(`${at}: price.per must be "month" (got ${JSON.stringify(price.per)}): ${path}`);
|
|
8436
|
-
}
|
|
8437
|
-
const sharedWithOtherProducts = normalizeBool(entry.sharedWithOtherProducts, "sharedWithOtherProducts", at, path, false);
|
|
8438
|
-
const hypothetical = normalizeBool(entry.hypothetical, "hypothetical", at, path, false);
|
|
8439
|
-
let counterfactualModel = null;
|
|
8440
|
-
if (entry.counterfactualModel !== void 0 && entry.counterfactualModel !== null) {
|
|
8441
|
-
const cm = entry.counterfactualModel;
|
|
8442
|
-
if (typeof cm !== "object" || Array.isArray(cm) || !isNonEmptyString4(cm.provider) || !isNonEmptyString4(cm.id)) {
|
|
8443
|
-
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}`);
|
|
8444
|
-
}
|
|
8445
|
-
counterfactualModel = { provider: cm.provider.trim(), id: cm.id.trim() };
|
|
8446
|
-
}
|
|
8447
|
-
let windows = [];
|
|
8448
|
-
if (entry.windows !== void 0) {
|
|
8449
|
-
if (!Array.isArray(entry.windows)) throw configError(`${at}: windows must be an array: ${path}`);
|
|
8450
|
-
windows = entry.windows.map((w, windowIndex) => normalizeWindow2(w, windowIndex, index, path));
|
|
8451
|
-
}
|
|
8452
|
-
return {
|
|
8453
|
-
id,
|
|
8454
|
-
vendor: entry.vendor.trim(),
|
|
8455
|
-
provider: entry.provider.trim(),
|
|
8456
|
-
models,
|
|
8457
|
-
price: { amount: price.amount, currency: price.currency, per: "month" },
|
|
8458
|
-
sharedWithOtherProducts,
|
|
8459
|
-
hypothetical,
|
|
8460
|
-
counterfactualModel,
|
|
8461
|
-
windows
|
|
8462
|
-
};
|
|
8463
|
-
}
|
|
8464
|
-
function normalizeWindow2(w, windowIndex, subIndex, path) {
|
|
8465
|
-
const at = `window at index ${windowIndex} of subscription at index ${subIndex}`;
|
|
8466
|
-
if (w === null || typeof w !== "object" || Array.isArray(w)) {
|
|
8467
|
-
throw configError(`${at}: must be an object: ${path}`);
|
|
8468
|
-
}
|
|
8469
|
-
if (!WINDOW_PERS.has(w.per)) {
|
|
8470
|
-
throw configError(`${at}: per must be one of 5h|7d|30d|month (got ${JSON.stringify(w.per)}): ${path}`);
|
|
8471
|
-
}
|
|
8472
|
-
const rolling = normalizeBool(w.rolling, "rolling", at, path, false);
|
|
8473
|
-
let unit = null;
|
|
8474
|
-
if (w.unit !== void 0 && w.unit !== null) {
|
|
8475
|
-
if (!WINDOW_UNITS.has(w.unit)) {
|
|
8476
|
-
throw configError(`${at}: unit must be one of tokens|requests|prompts|credits, or null (vendor undisclosed): ${path}`);
|
|
8477
|
-
}
|
|
8478
|
-
unit = w.unit;
|
|
8479
|
-
}
|
|
8480
|
-
let limit = null;
|
|
8481
|
-
if (w.limit !== void 0 && w.limit !== null) {
|
|
8482
|
-
if (typeof w.limit === "number") {
|
|
8483
|
-
if (!Number.isFinite(w.limit) || w.limit <= 0) throw configError(`${at}: limit must be a number > 0, [min, max], or null (vendor undisclosed): ${path}`);
|
|
8484
|
-
limit = w.limit;
|
|
8485
|
-
} else if (Array.isArray(w.limit)) {
|
|
8486
|
-
const [min, max] = w.limit;
|
|
8487
|
-
if (w.limit.length !== 2 || typeof min !== "number" || typeof max !== "number" || !Number.isFinite(min) || !Number.isFinite(max) || min <= 0 || max <= 0 || min >= max) {
|
|
8488
|
-
throw configError(`${at}: limit range must be [min, max] with both > 0 and min < max: ${path}`);
|
|
8489
|
-
}
|
|
8490
|
-
limit = [min, max];
|
|
8491
|
-
} else {
|
|
8492
|
-
throw configError(`${at}: limit must be a number > 0, [min, max], or null (vendor undisclosed): ${path}`);
|
|
8493
|
-
}
|
|
8494
|
-
}
|
|
8495
|
-
const scope = w.scope === void 0 || w.scope === null ? null : normalizeGlobs(w.scope, "scope", at, path);
|
|
8496
|
-
return { per: w.per, rolling, unit, limit, scope };
|
|
8497
|
-
}
|
|
8498
|
-
var SUBSCRIPTIONS_VERSION, WINDOW_PERS, WINDOW_UNITS, MODEL_GLOB_RE;
|
|
8499
|
-
var init_subscriptions = __esm({
|
|
8500
|
-
"../worker/src/subscriptions.mjs"() {
|
|
8501
|
-
init_config2();
|
|
8502
|
-
SUBSCRIPTIONS_VERSION = 1;
|
|
8503
|
-
WINDOW_PERS = /* @__PURE__ */ new Set(["5h", "7d", "30d", "month"]);
|
|
8504
|
-
WINDOW_UNITS = /* @__PURE__ */ new Set(["tokens", "requests", "prompts", "credits"]);
|
|
8505
|
-
MODEL_GLOB_RE = /^[A-Za-z0-9*._:/-]{1,64}$/;
|
|
8506
|
-
}
|
|
8507
|
-
});
|
|
8508
|
-
|
|
8509
|
-
// ../worker/src/connection.mjs
|
|
8510
|
-
import { Redis } from "ioredis";
|
|
8511
|
-
function parseConnection(url, { failFast = false } = {}) {
|
|
8512
|
-
const u = new URL(url);
|
|
8513
|
-
return {
|
|
8514
|
-
host: u.hostname || "127.0.0.1",
|
|
8515
|
-
port: Number(u.port || 6379),
|
|
8516
|
-
...u.password ? { password: u.password } : {},
|
|
8517
|
-
...u.username ? { username: u.username } : {},
|
|
8518
|
-
...u.pathname && u.pathname !== "/" ? { db: Number(u.pathname.slice(1)) } : {},
|
|
8519
|
-
maxRetriesPerRequest: null,
|
|
8520
|
-
// required for BullMQ blocking connections
|
|
8521
|
-
...failFast ? {
|
|
8522
|
-
connectTimeout: 2e3,
|
|
8523
|
-
enableOfflineQueue: false,
|
|
8524
|
-
// don't buffer commands while disconnected -- error now
|
|
8525
|
-
retryStrategy: (attempts) => attempts > 2 ? null : 200
|
|
8526
|
-
// give up after ~2 tries
|
|
8527
|
-
} : {}
|
|
8528
|
-
};
|
|
8529
|
-
}
|
|
8530
|
-
function makeRedisClient(url) {
|
|
8531
|
-
return new Redis(url, { maxRetriesPerRequest: null });
|
|
8532
|
-
}
|
|
8533
|
-
var init_connection = __esm({
|
|
8534
|
-
"../worker/src/connection.mjs"() {
|
|
8535
|
-
}
|
|
8536
|
-
});
|
|
8537
|
-
|
|
8538
|
-
// ../worker/src/job-id.mjs
|
|
8539
|
-
import { createHash } from "node:crypto";
|
|
8540
|
-
function localJobId({ folder, flow, task, minute }) {
|
|
8541
|
-
const digest = createHash("sha256").update([folder, flow ?? "", task ?? "", minute].join("\0")).digest("hex");
|
|
8542
|
-
return `local-${digest.slice(0, 16)}`;
|
|
8543
|
-
}
|
|
8544
|
-
var init_job_id = __esm({
|
|
8545
|
-
"../worker/src/job-id.mjs"() {
|
|
8546
|
-
init_config2();
|
|
8547
|
-
init_forges();
|
|
8548
|
-
}
|
|
8549
|
-
});
|
|
8550
|
-
|
|
8551
8859
|
// ../worker/src/queue.mjs
|
|
8552
8860
|
import { Queue } from "bullmq";
|
|
8553
8861
|
function makeQueue(connection) {
|
|
8554
8862
|
return new Queue(QUEUE, { connection });
|
|
8555
8863
|
}
|
|
8556
|
-
async function enqueueLocalJob(queue, { folder, flow, task, command, provider, model, maxTurns, image, skillsDir, chainDepth, parentJobId, jobId, now = /* @__PURE__ */ new Date() }) {
|
|
8864
|
+
async function enqueueLocalJob(queue, { folder, flow, task, command, provider, model, maxTurns, image, skillsDir, secrets, secretsProfile, chainDepth, parentJobId, jobId, now = /* @__PURE__ */ new Date() }) {
|
|
8557
8865
|
const minute = now.toISOString().slice(0, 16);
|
|
8558
8866
|
const id = jobId ?? localJobId({ folder, flow: command !== void 0 ? `cmd:${command}` : flow, task, minute });
|
|
8559
8867
|
const data = {
|
|
@@ -8574,6 +8882,11 @@ async function enqueueLocalJob(queue, { folder, flow, task, command, provider, m
|
|
|
8574
8882
|
// than inside `trigger` because a worker-host path is an execution knob, not a fact about the
|
|
8575
8883
|
// delivery -- and `trigger` is the object copied into /job/event.json.
|
|
8576
8884
|
...skillsDir !== void 0 && { skillsDir },
|
|
8885
|
+
// REQ-TRIGGER-SECRETS, on the forge path's terms: references only, resolved by the worker at job
|
|
8886
|
+
// start. A cron trigger may bind secrets (a nightly deploy is the obvious user), which is where
|
|
8887
|
+
// this differs from `replicas` -- that one is refused on a local job and this one is not.
|
|
8888
|
+
...secrets !== void 0 && { secrets },
|
|
8889
|
+
...secretsProfile !== void 0 && { secretsProfile },
|
|
8577
8890
|
...chainDepth !== void 0 && { chainDepth },
|
|
8578
8891
|
...parentJobId !== void 0 && { parentJobId }
|
|
8579
8892
|
};
|
|
@@ -8586,59 +8899,18 @@ async function enqueueLocalJob(queue, { folder, flow, task, command, provider, m
|
|
|
8586
8899
|
});
|
|
8587
8900
|
return id;
|
|
8588
8901
|
}
|
|
8589
|
-
var QUEUE, SEMANTIC_WINDOW_MS;
|
|
8902
|
+
var PR_CLOSE_WORDS, QUEUE, SEMANTIC_WINDOW_MS;
|
|
8590
8903
|
var init_queue = __esm({
|
|
8591
8904
|
"../worker/src/queue.mjs"() {
|
|
8592
8905
|
init_job_id();
|
|
8593
8906
|
init_forges();
|
|
8907
|
+
init_triggers();
|
|
8908
|
+
PR_CLOSE_WORDS = new Set(Object.values(PR_CLOSE_ACTIONS));
|
|
8594
8909
|
QUEUE = "pi-jobs";
|
|
8595
8910
|
SEMANTIC_WINDOW_MS = 10 * 60 * 1e3;
|
|
8596
8911
|
}
|
|
8597
8912
|
});
|
|
8598
8913
|
|
|
8599
|
-
// ../worker/src/flow-gate.mjs
|
|
8600
|
-
import { execFile } from "node:child_process";
|
|
8601
|
-
import { promisify } from "node:util";
|
|
8602
|
-
async function readFlowGate({ folder, flow, sha, git = defaultGit }) {
|
|
8603
|
-
if (typeof sha !== "string" || sha.trim() === "") return { gate: "deny" };
|
|
8604
|
-
if (typeof flow !== "string" || !SKILL_NAME_RE.test(flow)) return { gate: "deny" };
|
|
8605
|
-
const path = `.pi/skills/${flow}/SKILL.md`;
|
|
8606
|
-
try {
|
|
8607
|
-
const lsTreeZ = await git(folder, ["ls-tree", "-z", sha, path]);
|
|
8608
|
-
const record = lsTreeZ.split("\0").find((r) => r);
|
|
8609
|
-
if (!record) return { gate: "no-skill" };
|
|
8610
|
-
const tab = record.indexOf(" ");
|
|
8611
|
-
if (tab === -1) return { gate: "deny" };
|
|
8612
|
-
const [mode, type, oid] = record.slice(0, tab).split(/\s+/);
|
|
8613
|
-
if (mode !== "100644" || type !== "blob") return { gate: "deny" };
|
|
8614
|
-
const content = await git(folder, ["cat-file", "blob", oid], { raw: true });
|
|
8615
|
-
return { gate: aiTriggerAllows(content) ? "allow" : "deny" };
|
|
8616
|
-
} catch {
|
|
8617
|
-
return { gate: "deny" };
|
|
8618
|
-
}
|
|
8619
|
-
}
|
|
8620
|
-
function aiTriggerAllows(buf) {
|
|
8621
|
-
const text = buf.toString("utf8").replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
|
|
8622
|
-
const block = /^---\n([\s\S]*?)\n---(?:\n|$)/.exec(text);
|
|
8623
|
-
if (!block) return false;
|
|
8624
|
-
return /^ai-trigger:\s*("?)allow\1\s*$/m.test(block[1]);
|
|
8625
|
-
}
|
|
8626
|
-
async function defaultGit(gitDir, args, { raw = false } = {}) {
|
|
8627
|
-
const hardened = ["-c", "core.hooksPath=/dev/null", "-c", "core.fsmonitor=false", "--no-pager", "-C", gitDir, ...args];
|
|
8628
|
-
const { stdout } = await exec("git", hardened, {
|
|
8629
|
-
encoding: raw ? "buffer" : "utf8",
|
|
8630
|
-
maxBuffer: 16 * 1024 * 1024
|
|
8631
|
-
});
|
|
8632
|
-
return stdout;
|
|
8633
|
-
}
|
|
8634
|
-
var exec, SKILL_NAME_RE;
|
|
8635
|
-
var init_flow_gate = __esm({
|
|
8636
|
-
"../worker/src/flow-gate.mjs"() {
|
|
8637
|
-
exec = promisify(execFile);
|
|
8638
|
-
SKILL_NAME_RE = /^[a-z0-9](?:[a-z0-9_-]{0,62}[a-z0-9])?$/;
|
|
8639
|
-
}
|
|
8640
|
-
});
|
|
8641
|
-
|
|
8642
8914
|
// ../worker/src/git-dirty.mjs
|
|
8643
8915
|
import { execFileSync } from "node:child_process";
|
|
8644
8916
|
function gitDirty(folder, { exec: exec4 = execFileSync } = {}) {
|
|
@@ -9216,6 +9488,16 @@ function buildGraphModel({ triggers, schedulers, folderSkills, injectedSkills, o
|
|
|
9216
9488
|
// (guarded on `typeof t.flow === "string"`) correctly draws nothing for it.
|
|
9217
9489
|
command: t.command ?? null,
|
|
9218
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,
|
|
9219
9501
|
folderKey: group?.key ?? null,
|
|
9220
9502
|
runs: Number.isInteger(stats?.runs) ? stats.runs : 0,
|
|
9221
9503
|
lastOutcome: stats?.lastOutcome ?? null,
|
|
@@ -9353,8 +9635,16 @@ function triggerMatchLabel(t) {
|
|
|
9353
9635
|
return selectorLabel(t);
|
|
9354
9636
|
case "comment":
|
|
9355
9637
|
return `"${t.phrase ?? "-"}"`;
|
|
9356
|
-
case "pull_request":
|
|
9357
|
-
|
|
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
|
+
}
|
|
9358
9648
|
default:
|
|
9359
9649
|
return "(unknown)";
|
|
9360
9650
|
}
|
|
@@ -9961,8 +10251,110 @@ var init_costs = __esm({
|
|
|
9961
10251
|
}
|
|
9962
10252
|
});
|
|
9963
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
|
+
|
|
9964
10356
|
// src/read-model.mjs
|
|
9965
|
-
import * as
|
|
10357
|
+
import * as nodeFs3 from "node:fs";
|
|
9966
10358
|
import { join as join2, delimiter, sep } from "node:path";
|
|
9967
10359
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
9968
10360
|
function resolvePaths(env = process.env) {
|
|
@@ -10135,14 +10527,14 @@ function folderUnderRoots(folder, roots) {
|
|
|
10135
10527
|
if (!Array.isArray(roots) || roots.length === 0) return false;
|
|
10136
10528
|
let realTarget;
|
|
10137
10529
|
try {
|
|
10138
|
-
realTarget =
|
|
10530
|
+
realTarget = nodeFs3.realpathSync(folder);
|
|
10139
10531
|
} catch {
|
|
10140
10532
|
return false;
|
|
10141
10533
|
}
|
|
10142
10534
|
for (const root of roots) {
|
|
10143
10535
|
let realRoot;
|
|
10144
10536
|
try {
|
|
10145
|
-
realRoot =
|
|
10537
|
+
realRoot = nodeFs3.realpathSync(root);
|
|
10146
10538
|
} catch {
|
|
10147
10539
|
continue;
|
|
10148
10540
|
}
|
|
@@ -10153,7 +10545,7 @@ function folderUnderRoots(folder, roots) {
|
|
|
10153
10545
|
function hourKey(nowMs) {
|
|
10154
10546
|
return `dispatch-run:${new Date(nowMs).toISOString().slice(0, 13).replace("T", "-")}`;
|
|
10155
10547
|
}
|
|
10156
|
-
function writeSettings({ settingsFile, mutate, fs =
|
|
10548
|
+
function writeSettings({ settingsFile, mutate, fs = nodeFs3 }) {
|
|
10157
10549
|
const res = readOverlay(settingsFile, { fs });
|
|
10158
10550
|
const base = res.overlay ?? {};
|
|
10159
10551
|
const next = mutate({ ...base });
|
|
@@ -10161,27 +10553,7 @@ function writeSettings({ settingsFile, mutate, fs = nodeFs2 }) {
|
|
|
10161
10553
|
if (w.invalid) return { invalid: w.invalid };
|
|
10162
10554
|
return res.invalid ? { ok: true, overlay: next, rebuiltFrom: res.invalid } : { ok: true, overlay: next };
|
|
10163
10555
|
}
|
|
10164
|
-
function
|
|
10165
|
-
let current = [];
|
|
10166
|
-
try {
|
|
10167
|
-
const raw = JSON.parse(fs.readFileSync(triggersPath, "utf8"));
|
|
10168
|
-
if (Array.isArray(raw?.triggers)) current = raw.triggers;
|
|
10169
|
-
} catch {
|
|
10170
|
-
}
|
|
10171
|
-
const next = mutate(current.map((t) => ({ ...t })));
|
|
10172
|
-
const text = `${JSON.stringify({ triggers: next }, null, 2)}
|
|
10173
|
-
`;
|
|
10174
|
-
try {
|
|
10175
|
-
parseTriggers(text, triggersPath);
|
|
10176
|
-
} catch (e) {
|
|
10177
|
-
return { invalid: e?.message ?? String(e) };
|
|
10178
|
-
}
|
|
10179
|
-
const tmp = `${triggersPath}.tmp`;
|
|
10180
|
-
fs.writeFileSync(tmp, text, { mode: 420 });
|
|
10181
|
-
fs.renameSync(tmp, triggersPath);
|
|
10182
|
-
return { ok: true };
|
|
10183
|
-
}
|
|
10184
|
-
function readPauseWindows({ pauseWindowsPath, fs = nodeFs2 }) {
|
|
10556
|
+
function readPauseWindows({ pauseWindowsPath, fs = nodeFs3 }) {
|
|
10185
10557
|
let text;
|
|
10186
10558
|
try {
|
|
10187
10559
|
text = fs.readFileSync(pauseWindowsPath, "utf8");
|
|
@@ -10194,7 +10566,7 @@ function readPauseWindows({ pauseWindowsPath, fs = nodeFs2 }) {
|
|
|
10194
10566
|
return { invalid: e?.message ?? String(e) };
|
|
10195
10567
|
}
|
|
10196
10568
|
}
|
|
10197
|
-
function writePauseWindows({ pauseWindowsPath, mutate, fs =
|
|
10569
|
+
function writePauseWindows({ pauseWindowsPath, mutate, fs = nodeFs3 }) {
|
|
10198
10570
|
let current = [];
|
|
10199
10571
|
try {
|
|
10200
10572
|
const raw = JSON.parse(fs.readFileSync(pauseWindowsPath, "utf8"));
|
|
@@ -10214,7 +10586,7 @@ function writePauseWindows({ pauseWindowsPath, mutate, fs = nodeFs2 }) {
|
|
|
10214
10586
|
fs.renameSync(tmp, pauseWindowsPath);
|
|
10215
10587
|
return { ok: true };
|
|
10216
10588
|
}
|
|
10217
|
-
function readSubscriptions({ subscriptionsPath, fs =
|
|
10589
|
+
function readSubscriptions({ subscriptionsPath, fs = nodeFs3 }) {
|
|
10218
10590
|
if (subscriptionsPath === null || subscriptionsPath === void 0) return { missing: true };
|
|
10219
10591
|
let text;
|
|
10220
10592
|
try {
|
|
@@ -10296,7 +10668,7 @@ function withTimeout(promise, ms, fallback) {
|
|
|
10296
10668
|
});
|
|
10297
10669
|
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
10298
10670
|
}
|
|
10299
|
-
function listRuns({ logsDir, limit = 10, fs =
|
|
10671
|
+
function listRuns({ logsDir, limit = 10, fs = nodeFs3 }) {
|
|
10300
10672
|
const cap = clampLimit(limit);
|
|
10301
10673
|
let names;
|
|
10302
10674
|
try {
|
|
@@ -10314,7 +10686,7 @@ function listRuns({ logsDir, limit = 10, fs = nodeFs2 }) {
|
|
|
10314
10686
|
records.sort(byEndedAtDesc);
|
|
10315
10687
|
return records.slice(0, cap);
|
|
10316
10688
|
}
|
|
10317
|
-
function scanRunRecords({ logsDir, sinceMs, nowMs = Date.now(), fs =
|
|
10689
|
+
function scanRunRecords({ logsDir, sinceMs, nowMs = Date.now(), fs = nodeFs3 }) {
|
|
10318
10690
|
const oldestMs = nowMs - SCAN_WINDOW_MAX_DAYS * 24 * 60 * 60 * 1e3;
|
|
10319
10691
|
const cutoffMs = Math.max(Number.isFinite(sinceMs) ? sinceMs : oldestMs, oldestMs);
|
|
10320
10692
|
let names;
|
|
@@ -10335,27 +10707,26 @@ function scanRunRecords({ logsDir, sinceMs, nowMs = Date.now(), fs = nodeFs2 })
|
|
|
10335
10707
|
}
|
|
10336
10708
|
return records;
|
|
10337
10709
|
}
|
|
10338
|
-
function readRun({ logsDir, jobId, fs =
|
|
10710
|
+
function readRun({ logsDir, jobId, fs = nodeFs3 }) {
|
|
10339
10711
|
return readJsonFile(join2(logsDir, `${sanitizeJobId(jobId)}.json`), fs);
|
|
10340
10712
|
}
|
|
10341
|
-
function readLogTail({ logsDir, jobId, lines = 200, fs =
|
|
10713
|
+
function readLogTail({ logsDir, jobId, lines = 200, fs = nodeFs3 }) {
|
|
10342
10714
|
let text;
|
|
10343
10715
|
try {
|
|
10344
10716
|
text = fs.readFileSync(join2(logsDir, `${sanitizeJobId(jobId)}.log`), "utf8");
|
|
10345
10717
|
} catch {
|
|
10346
10718
|
return { missing: true };
|
|
10347
10719
|
}
|
|
10348
|
-
const all = text.split("\n");
|
|
10349
|
-
if (all.length > 0 && all[all.length - 1] === "") all.pop();
|
|
10720
|
+
const all = text.split("\n").filter((line) => line !== "");
|
|
10350
10721
|
const cap = clampLines(lines);
|
|
10351
10722
|
return { lines: all.slice(Math.max(0, all.length - cap)) };
|
|
10352
10723
|
}
|
|
10353
|
-
function readSettingsView({ settingsFile, fs =
|
|
10724
|
+
function readSettingsView({ settingsFile, fs = nodeFs3 }) {
|
|
10354
10725
|
const result = readOverlay(settingsFile, { fs });
|
|
10355
10726
|
if (result.invalid) return { path: settingsFile, invalid: result.invalid };
|
|
10356
10727
|
return { path: settingsFile, overlay: result.overlay };
|
|
10357
10728
|
}
|
|
10358
|
-
function readTriggers({ triggersPath, fs =
|
|
10729
|
+
function readTriggers({ triggersPath, fs = nodeFs3 }) {
|
|
10359
10730
|
let text;
|
|
10360
10731
|
try {
|
|
10361
10732
|
text = fs.readFileSync(triggersPath, "utf8");
|
|
@@ -10393,6 +10764,8 @@ function normalizeTriggerForDisplay(entry) {
|
|
|
10393
10764
|
const resume = run.resume === true;
|
|
10394
10765
|
const forge = typeof run.kind === "string" && run.kind.trim() !== "" ? run.kind : null;
|
|
10395
10766
|
const replicas = Number.isInteger(run.replicas) && run.replicas > 1 ? run.replicas : null;
|
|
10767
|
+
const secrets = run.secrets !== null && typeof run.secrets === "object" && !Array.isArray(run.secrets) ? Object.keys(run.secrets).length : 0;
|
|
10768
|
+
const secretsProfile = typeof run.secretsProfile === "string" && run.secretsProfile.trim() !== "" ? run.secretsProfile : null;
|
|
10396
10769
|
switch (on.type) {
|
|
10397
10770
|
case "cron":
|
|
10398
10771
|
return {
|
|
@@ -10409,19 +10782,25 @@ function normalizeTriggerForDisplay(entry) {
|
|
|
10409
10782
|
image,
|
|
10410
10783
|
skillsDir,
|
|
10411
10784
|
instructions,
|
|
10412
|
-
resume
|
|
10785
|
+
resume,
|
|
10786
|
+
secrets,
|
|
10787
|
+
secretsProfile
|
|
10413
10788
|
};
|
|
10414
10789
|
case "label":
|
|
10415
|
-
return { type: "label", any: normalizeSelector(on.any), all: normalizeSelector(on.all), none: normalizeSelector(on.none), flow, command, packages, image, skillsDir, instructions, resume, replicas, forge };
|
|
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 };
|
|
10416
10791
|
case "comment":
|
|
10417
|
-
return { type: "comment", phrase: typeof on.phrase === "string" ? on.phrase : null, flow, command, packages, image, skillsDir, instructions, resume, replicas, forge };
|
|
10418
|
-
case "pull_request":
|
|
10792
|
+
return { type: "comment", phrase: typeof on.phrase === "string" ? on.phrase : null, flow, command, packages, image, skillsDir, instructions, resume, secrets, secretsProfile, replicas, forge };
|
|
10793
|
+
case "pull_request": {
|
|
10794
|
+
const prDisarmed = on.disarmed !== null && typeof on.disarmed === "object" && !Array.isArray(on.disarmed) ? on.disarmed : null;
|
|
10419
10795
|
return {
|
|
10420
10796
|
type: "pull_request",
|
|
10421
10797
|
action: normalizeSelector(on.action),
|
|
10422
10798
|
any: normalizeSelector(on.any),
|
|
10423
10799
|
all: normalizeSelector(on.all),
|
|
10424
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 },
|
|
10425
10804
|
flow,
|
|
10426
10805
|
command,
|
|
10427
10806
|
packages,
|
|
@@ -10429,9 +10808,39 @@ function normalizeTriggerForDisplay(entry) {
|
|
|
10429
10808
|
skillsDir,
|
|
10430
10809
|
instructions,
|
|
10431
10810
|
resume,
|
|
10811
|
+
secrets,
|
|
10812
|
+
secretsProfile,
|
|
10432
10813
|
replicas,
|
|
10433
10814
|
forge
|
|
10434
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 },
|
|
10831
|
+
flow,
|
|
10832
|
+
command,
|
|
10833
|
+
packages,
|
|
10834
|
+
image,
|
|
10835
|
+
skillsDir,
|
|
10836
|
+
instructions,
|
|
10837
|
+
resume,
|
|
10838
|
+
secrets,
|
|
10839
|
+
secretsProfile,
|
|
10840
|
+
replicas,
|
|
10841
|
+
forge
|
|
10842
|
+
};
|
|
10843
|
+
}
|
|
10435
10844
|
default:
|
|
10436
10845
|
return null;
|
|
10437
10846
|
}
|
|
@@ -10440,7 +10849,7 @@ function normalizeSelector(value) {
|
|
|
10440
10849
|
if (!Array.isArray(value)) return [];
|
|
10441
10850
|
return value.filter((member) => typeof member === "string");
|
|
10442
10851
|
}
|
|
10443
|
-
function readStagedPackages({ globalPiDir, fs =
|
|
10852
|
+
function readStagedPackages({ globalPiDir, fs = nodeFs3, readManifest: readManifest2 = readStageManifest } = {}) {
|
|
10444
10853
|
const empty = { stagedAt: null, packages: [] };
|
|
10445
10854
|
if (typeof globalPiDir !== "string" || globalPiDir === "") return empty;
|
|
10446
10855
|
let manifest;
|
|
@@ -10648,7 +11057,7 @@ function readFolderSkills({ folder, exec: exec4 = execFileSync2 } = {}) {
|
|
|
10648
11057
|
}
|
|
10649
11058
|
return { head, skills, truncated: truncated || anyUnread, unreachable: null };
|
|
10650
11059
|
}
|
|
10651
|
-
function readInjectedSkills({ skillsDir, fs =
|
|
11060
|
+
function readInjectedSkills({ skillsDir, fs = nodeFs3 } = {}) {
|
|
10652
11061
|
if (typeof skillsDir !== "string" || skillsDir === "") return { skills: [], truncated: false, unreachable: null };
|
|
10653
11062
|
let names;
|
|
10654
11063
|
try {
|
|
@@ -10671,7 +11080,7 @@ function readInjectedSkills({ skillsDir, fs = nodeFs2 } = {}) {
|
|
|
10671
11080
|
}
|
|
10672
11081
|
return { skills, truncated, unreachable: null };
|
|
10673
11082
|
}
|
|
10674
|
-
function readOverlaySkills({ globalPiDir, fs =
|
|
11083
|
+
function readOverlaySkills({ globalPiDir, fs = nodeFs3 } = {}) {
|
|
10675
11084
|
if (typeof globalPiDir !== "string" || globalPiDir === "") return { skills: [], truncated: false, unreachable: null };
|
|
10676
11085
|
const dir = join2(globalPiDir, "skills");
|
|
10677
11086
|
let names;
|
|
@@ -10695,7 +11104,7 @@ function readOverlaySkills({ globalPiDir, fs = nodeFs2 } = {}) {
|
|
|
10695
11104
|
}
|
|
10696
11105
|
return { skills, truncated, unreachable: null };
|
|
10697
11106
|
}
|
|
10698
|
-
function readStagedSkillsList({ globalPiDir, fs =
|
|
11107
|
+
function readStagedSkillsList({ globalPiDir, fs = nodeFs3, readStaged = readStagedSkills } = {}) {
|
|
10699
11108
|
const empty = { skills: [], unenumerable: [], truncated: false };
|
|
10700
11109
|
if (typeof globalPiDir !== "string" || globalPiDir === "") return empty;
|
|
10701
11110
|
let result;
|
|
@@ -10770,7 +11179,7 @@ function forgeRepoTargets({ records } = {}) {
|
|
|
10770
11179
|
function escapeRegExp2(s) {
|
|
10771
11180
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
10772
11181
|
}
|
|
10773
|
-
function listRunIds({ logsDir, fs =
|
|
11182
|
+
function listRunIds({ logsDir, fs = nodeFs3 }) {
|
|
10774
11183
|
let names;
|
|
10775
11184
|
try {
|
|
10776
11185
|
names = fs.readdirSync(logsDir);
|
|
@@ -10815,7 +11224,6 @@ var init_read_model = __esm({
|
|
|
10815
11224
|
init_runtime_settings();
|
|
10816
11225
|
init_run_history();
|
|
10817
11226
|
init_budget();
|
|
10818
|
-
init_triggers();
|
|
10819
11227
|
init_pause_windows();
|
|
10820
11228
|
init_subscriptions();
|
|
10821
11229
|
init_connection();
|
|
@@ -10827,6 +11235,7 @@ var init_read_model = __esm({
|
|
|
10827
11235
|
init_forges();
|
|
10828
11236
|
init_graph_model();
|
|
10829
11237
|
init_costs();
|
|
11238
|
+
init_triggers_file();
|
|
10830
11239
|
DISPATCH_RUN_TTL_SECONDS = 2 * 60 * 60;
|
|
10831
11240
|
SCAN_WINDOW_MAX_DAYS = 92;
|
|
10832
11241
|
GRAPH_LIMITS = Object.freeze({
|
|
@@ -10945,6 +11354,12 @@ function normalizeModel(model) {
|
|
|
10945
11354
|
package: typeof n.package === "string" ? clip(n.package, 80) : null,
|
|
10946
11355
|
tiersUnknown: Array.isArray(n.tiersUnknown) ? [...new Set(n.tiersUnknown.filter((t) => typeof t === "string").map((t) => clip(t, 40)))].sort().slice(0, 4) : [],
|
|
10947
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,
|
|
10948
11363
|
folderKey: typeof n.folderKey === "string" ? n.folderKey : null,
|
|
10949
11364
|
runs: intOr(n.runs, 0),
|
|
10950
11365
|
lastOutcome: typeof n.lastOutcome === "string" ? n.lastOutcome : null,
|
|
@@ -11389,6 +11804,8 @@ function buildTip(n, flags, groupLabel, nowMs) {
|
|
|
11389
11804
|
const nextMs = typeof n.next === "number" ? n.next : typeof n.next === "string" ? Date.parse(n.next) : NaN;
|
|
11390
11805
|
if (Number.isFinite(nextMs) && Number.isFinite(nowMs) && nextMs > nowMs) lines.push(`next ${relSpan(nextMs - nowMs)}`);
|
|
11391
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)");
|
|
11392
11809
|
}
|
|
11393
11810
|
if (n.aiTrigger) lines.push("chainable: ai-trigger allow");
|
|
11394
11811
|
if (n.kind === "skill" && n.loops.length > 0) {
|
|
@@ -11419,6 +11836,7 @@ function nodeState(n, flagNames) {
|
|
|
11419
11836
|
}
|
|
11420
11837
|
if (n.kind === "skill-unverified") return { stroke: PAGE_DIM, dash: "10,4", faded: false };
|
|
11421
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 };
|
|
11422
11840
|
if (flagNames.has("orphan")) return { stroke: CHIP_STROKE, dash: "8,3", faded: true };
|
|
11423
11841
|
return { stroke: CHIP_STROKE, dash: null, faded: false };
|
|
11424
11842
|
}
|
|
@@ -11663,6 +12081,7 @@ var init_graph_html = __esm({
|
|
|
11663
12081
|
label: "#d8bfd8",
|
|
11664
12082
|
comment: "#e7e7ae",
|
|
11665
12083
|
pull_request: "#c0deed",
|
|
12084
|
+
issue: "#c7e9c0",
|
|
11666
12085
|
skill: "#fdd0a2"
|
|
11667
12086
|
});
|
|
11668
12087
|
STATUS_COMPLETED = "#3fb950";
|
|
@@ -11692,6 +12111,7 @@ var init_graph_html = __esm({
|
|
|
11692
12111
|
label: "\u25C8",
|
|
11693
12112
|
comment: "\u275D",
|
|
11694
12113
|
pull_request: "\u21C4",
|
|
12114
|
+
issue: "\u25C9",
|
|
11695
12115
|
skill: "\u0192",
|
|
11696
12116
|
"skill-missing": "!",
|
|
11697
12117
|
"skill-unverified": "?",
|
|
@@ -13106,7 +13526,7 @@ var init_open_browser = __esm({
|
|
|
13106
13526
|
});
|
|
13107
13527
|
|
|
13108
13528
|
// src/deployment-pointer.mjs
|
|
13109
|
-
import * as
|
|
13529
|
+
import * as nodeFs4 from "node:fs";
|
|
13110
13530
|
import { homedir } from "node:os";
|
|
13111
13531
|
import { join as join3, isAbsolute } from "node:path";
|
|
13112
13532
|
function pointerPath(env = process.env) {
|
|
@@ -13138,7 +13558,7 @@ function normalizePointer(raw) {
|
|
|
13138
13558
|
}
|
|
13139
13559
|
return { pointer: { version: raw.version, deploymentDir: raw.deploymentDir, env } };
|
|
13140
13560
|
}
|
|
13141
|
-
function readPointer({ path = pointerPath(), fs =
|
|
13561
|
+
function readPointer({ path = pointerPath(), fs = nodeFs4 } = {}) {
|
|
13142
13562
|
let text;
|
|
13143
13563
|
try {
|
|
13144
13564
|
text = fs.readFileSync(path, "utf8");
|
|
@@ -13171,12 +13591,12 @@ function layerPointer(env, fs) {
|
|
|
13171
13591
|
}
|
|
13172
13592
|
return { applied };
|
|
13173
13593
|
}
|
|
13174
|
-
function applyDeploymentPointer(env = process.env, { fs =
|
|
13594
|
+
function applyDeploymentPointer(env = process.env, { fs = nodeFs4 } = {}) {
|
|
13175
13595
|
if (appliedOnce) return { applied: [] };
|
|
13176
13596
|
appliedOnce = true;
|
|
13177
13597
|
return layerPointer(env, fs);
|
|
13178
13598
|
}
|
|
13179
|
-
function reapplyDeploymentPointer(env = process.env, { fs =
|
|
13599
|
+
function reapplyDeploymentPointer(env = process.env, { fs = nodeFs4 } = {}) {
|
|
13180
13600
|
appliedOnce = true;
|
|
13181
13601
|
return layerPointer(env, fs);
|
|
13182
13602
|
}
|
|
@@ -13185,7 +13605,7 @@ function takePointerNotice() {
|
|
|
13185
13605
|
notice = void 0;
|
|
13186
13606
|
return n;
|
|
13187
13607
|
}
|
|
13188
|
-
function writePointer({ path = pointerPath(), pointer, fs =
|
|
13608
|
+
function writePointer({ path = pointerPath(), pointer, fs = nodeFs4 } = {}) {
|
|
13189
13609
|
const res = normalizePointer(pointer);
|
|
13190
13610
|
if (res.ignored) return { invalid: res.ignored };
|
|
13191
13611
|
const tmp = `${path}.tmp`;
|
|
@@ -38917,17 +39337,25 @@ function triggerLine(t) {
|
|
|
38917
39337
|
const ins = t?.instructions === true ? " [instructions]" : "";
|
|
38918
39338
|
const res = t?.resume === true ? " [resume]" : "";
|
|
38919
39339
|
const rep = t?.replicas > 1 ? ` [x${t.replicas}]` : "";
|
|
39340
|
+
const sec = t?.secrets > 0 ? ` [secrets ${t.secrets}${t.secretsProfile ? ` via ${t.secretsProfile}` : ""}]` : "";
|
|
39341
|
+
const shot = t?.disarmed ? " [spent]" : t?.once === true ? " [once]" : "";
|
|
38920
39342
|
switch (t?.type) {
|
|
38921
39343
|
case "cron":
|
|
38922
|
-
return `cron ${t.id ?? "-"} ${t.pattern ?? "-"} \u2192 ${t.folder ?? "-"}/${flow}${forge}${pkgs}${img}${skl}${ins}${res}${rep}`;
|
|
39344
|
+
return `cron ${t.id ?? "-"} ${t.pattern ?? "-"} \u2192 ${t.folder ?? "-"}/${flow}${forge}${pkgs}${img}${skl}${ins}${res}${rep}${sec}`;
|
|
38923
39345
|
case "label":
|
|
38924
|
-
return `label ${ruleClauses(t) || "(no selector)"} \u2192 ${flow}${forge}${pkgs}${img}${skl}${ins}${res}${rep}`;
|
|
39346
|
+
return `label ${ruleClauses(t) || "(no selector)"} \u2192 ${flow}${forge}${pkgs}${img}${skl}${ins}${res}${rep}${sec}`;
|
|
38925
39347
|
case "comment":
|
|
38926
|
-
return `comment "${t.phrase ?? "-"}" \u2192 ${flow}${forge}${pkgs}${img}${skl}${ins}${res}${rep}`;
|
|
39348
|
+
return `comment "${t.phrase ?? "-"}" \u2192 ${flow}${forge}${pkgs}${img}${skl}${ins}${res}${rep}${sec}`;
|
|
38927
39349
|
case "pull_request": {
|
|
38928
39350
|
const clauses = ruleClauses(t);
|
|
38929
39351
|
const action = `action[${(t.action ?? []).join(",")}]`;
|
|
38930
|
-
|
|
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}`;
|
|
38931
39359
|
}
|
|
38932
39360
|
default:
|
|
38933
39361
|
return "(unknown trigger)";
|
|
@@ -39856,8 +40284,9 @@ function triggerRow(t, sel, inner, styler, sched = null) {
|
|
|
39856
40284
|
const img = t?.image ? " " + styler.fg("accent", `[${t.image}]`) : "";
|
|
39857
40285
|
const res = t?.resume === true ? " " + styler.fg("warning", "[resume]") : "";
|
|
39858
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]") : "";
|
|
39859
40288
|
const health = sched && (sched.overdueMs || sched.stallMax > 0 && sched.stalls >= sched.stallMax) ? " " + styler.fg("warning", sched.overdueMs ? "\u26A0 overdue" : "\u26A0 stalled") : "";
|
|
39860
|
-
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);
|
|
39861
40290
|
}
|
|
39862
40291
|
function matchColored(t, styler) {
|
|
39863
40292
|
switch (t?.type) {
|
|
@@ -39865,10 +40294,15 @@ function matchColored(t, styler) {
|
|
|
39865
40294
|
return styler.fg("text", `${t.id ?? "-"} ${t.pattern ?? "-"}`);
|
|
39866
40295
|
case "comment":
|
|
39867
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
|
+
}
|
|
39868
40301
|
case "label":
|
|
39869
40302
|
case "pull_request": {
|
|
39870
40303
|
const parts = [];
|
|
39871
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}`));
|
|
39872
40306
|
for (const x of t.any ?? []) parts.push(styler.fg("success", x));
|
|
39873
40307
|
for (const x of t.all ?? []) parts.push(styler.fg("success", `+${x}`));
|
|
39874
40308
|
for (const x of t.none ?? []) parts.push(styler.fg("error", `!${x}`));
|
|
@@ -40003,11 +40437,17 @@ function renderTriggerDetail(t, inner, styler, sched = null, staged = null) {
|
|
|
40003
40437
|
}
|
|
40004
40438
|
} else if (t.type === "label" || t.type === "pull_request") {
|
|
40005
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"));
|
|
40006
40442
|
out.push(kv("any of", (t.any ?? []).join(" \xB7 ") || "-", "success"));
|
|
40007
40443
|
out.push(kv("all of", (t.all ?? []).join(" \xB7 ") || "-", "success"));
|
|
40008
40444
|
out.push(kv("none of", (t.none ?? []).join(" \xB7 ") || "-", "error"));
|
|
40009
40445
|
} else if (t.type === "comment") {
|
|
40010
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"));
|
|
40011
40451
|
}
|
|
40012
40452
|
blank();
|
|
40013
40453
|
section("runs");
|
|
@@ -40038,6 +40478,12 @@ function renderTriggerDetail(t, inner, styler, sched = null, staged = null) {
|
|
|
40038
40478
|
keeps("issue text, file contents, tool output, the agent's own reasoning");
|
|
40039
40479
|
keeps("replayed into the next job on the same branch; forks never resume");
|
|
40040
40480
|
}
|
|
40481
|
+
if (t.secrets > 0) {
|
|
40482
|
+
const holds = (text) => out.push(fitLine(styler.fg("border", "\xB7 ") + styler.fg("warning", text), inner, styler));
|
|
40483
|
+
holds(`${t.secrets} vault secret(s) injected${t.secretsProfile ? ` via profile ${t.secretsProfile}` : ""}`);
|
|
40484
|
+
holds("resolved on the host before the container; the job holds no vault credential");
|
|
40485
|
+
holds("the agent can read them, and the forge is on the egress allowlist");
|
|
40486
|
+
}
|
|
40041
40487
|
if (t.replicas > 1) {
|
|
40042
40488
|
const races = (text) => out.push(fitLine(styler.fg("border", "\xB7 ") + styler.fg("warning", text), inner, styler));
|
|
40043
40489
|
races(`each delivery starts ${t.replicas} independent jobs`);
|
|
@@ -40065,33 +40511,38 @@ function trustModel(t) {
|
|
|
40065
40511
|
}
|
|
40066
40512
|
const subject = t?.type === "comment" ? "adversarial comment text" : "adversarial issue/PR text";
|
|
40067
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;
|
|
40068
40516
|
switch (t?.forge) {
|
|
40069
40517
|
case "gitlab":
|
|
40070
|
-
return [
|
|
40518
|
+
return oneShot([
|
|
40071
40519
|
"authorized by the actor's resolved project access level (>= Developer) \u2014 a label is NOT approval here",
|
|
40072
40520
|
"HMAC over the body (19.0+) or a bare X-Gitlab-Token \xB7 dedup by webhook-id",
|
|
40073
40521
|
where
|
|
40074
|
-
];
|
|
40522
|
+
]);
|
|
40075
40523
|
case "forgejo":
|
|
40076
|
-
return [
|
|
40524
|
+
return oneShot([
|
|
40077
40525
|
"authorized by the actor's resolved repository permission (admin|write), on every trigger type",
|
|
40078
40526
|
"HMAC over the body (GitHub-compatible) \xB7 dedup by X-GitHub-Delivery GUID",
|
|
40079
40527
|
where
|
|
40080
|
-
];
|
|
40528
|
+
]);
|
|
40081
40529
|
case "azure":
|
|
40082
|
-
return [
|
|
40530
|
+
return oneShot([
|
|
40083
40531
|
"authorized by the actor's resolved project membership \u2014 work items name the actor only by email",
|
|
40084
40532
|
"NO HMAC: a shared secret that covers no bytes \xB7 dedup by the payload's own id (OQ-015)",
|
|
40085
40533
|
where
|
|
40086
|
-
];
|
|
40534
|
+
]);
|
|
40087
40535
|
default:
|
|
40088
|
-
return [
|
|
40536
|
+
return oneShot([
|
|
40089
40537
|
// A review trigger reads the REVIEWER's author_association, not the PR author's (issue #66), so it
|
|
40090
40538
|
// cannot share the generic line: an operator reading "author gate" would picture the wrong person.
|
|
40091
|
-
|
|
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",
|
|
40092
40543
|
"dedup by X-GitHub-Delivery GUID (redelivery-safe)",
|
|
40093
40544
|
where
|
|
40094
|
-
];
|
|
40545
|
+
]);
|
|
40095
40546
|
}
|
|
40096
40547
|
}
|
|
40097
40548
|
function triggerDetailHints(inner, styler, pendingDelete = false) {
|
|
@@ -40332,7 +40783,7 @@ var init_dashboard = __esm({
|
|
|
40332
40783
|
TAIL_VIEWPORT = 20;
|
|
40333
40784
|
MIN_WIDTH2 = 8;
|
|
40334
40785
|
DRILL_WIDTH = 70;
|
|
40335
|
-
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" };
|
|
40336
40787
|
KIND_WIDTH = 13;
|
|
40337
40788
|
FRAME_CHROME_ROWS = 4;
|
|
40338
40789
|
}
|
|
@@ -40357,7 +40808,7 @@ __export(setup_wizard_exports, {
|
|
|
40357
40808
|
runSetupWizard: () => runSetupWizard,
|
|
40358
40809
|
runtimeDirFor: () => runtimeDirFor
|
|
40359
40810
|
});
|
|
40360
|
-
import * as
|
|
40811
|
+
import * as nodeFs5 from "node:fs";
|
|
40361
40812
|
import { execFileSync as execFileSync3, spawn as nodeSpawn2 } from "node:child_process";
|
|
40362
40813
|
import { homedir as homedir2 } from "node:os";
|
|
40363
40814
|
import { join as join6 } from "node:path";
|
|
@@ -40373,7 +40824,7 @@ async function defaultProbeQueue(url) {
|
|
|
40373
40824
|
async function detectDeployment({
|
|
40374
40825
|
env = process.env,
|
|
40375
40826
|
cwd = process.cwd(),
|
|
40376
|
-
fs =
|
|
40827
|
+
fs = nodeFs5,
|
|
40377
40828
|
probeQueue = defaultProbeQueue
|
|
40378
40829
|
} = {}) {
|
|
40379
40830
|
const pointerRes = readPointer({ path: pointerPath(env), fs });
|
|
@@ -40500,7 +40951,7 @@ async function ensureDocker(ui, notify, { platform, probeDockerFn }) {
|
|
|
40500
40951
|
function npmSpawnOptions(platform, dir) {
|
|
40501
40952
|
return platform === "win32" ? { bin: "npm.cmd", options: { cwd: dir, shell: true } } : { bin: "npm", options: { cwd: dir } };
|
|
40502
40953
|
}
|
|
40503
|
-
function listRepoSkills(cwd, fs =
|
|
40954
|
+
function listRepoSkills(cwd, fs = nodeFs5) {
|
|
40504
40955
|
try {
|
|
40505
40956
|
const root = join6(cwd, ".pi", "skills");
|
|
40506
40957
|
return fs.readdirSync(root).filter((name) => SKILL_NAME_RE.test(name) && fs.existsSync(join6(root, name, "SKILL.md")));
|
|
@@ -40528,7 +40979,7 @@ function ensureDeploymentPackageJson(fs, dir) {
|
|
|
40528
40979
|
}
|
|
40529
40980
|
async function runSetupWizard(paths, ctx, notify, deps = {}) {
|
|
40530
40981
|
const {
|
|
40531
|
-
fs =
|
|
40982
|
+
fs = nodeFs5,
|
|
40532
40983
|
env = process.env,
|
|
40533
40984
|
platform = process.platform,
|
|
40534
40985
|
execPath = process.execPath,
|
|
@@ -40842,7 +41293,7 @@ async function offerFirstTrigger(repoCwd, deployDir, ui, notify, { fs, listRepoS
|
|
|
40842
41293
|
);
|
|
40843
41294
|
}
|
|
40844
41295
|
function registerNudge(pi, deps = {}) {
|
|
40845
|
-
const { fs =
|
|
41296
|
+
const { fs = nodeFs5, env = process.env, homedirFn = homedir2 } = deps;
|
|
40846
41297
|
pi.on("session_start", (event, ctx) => {
|
|
40847
41298
|
try {
|
|
40848
41299
|
if (event?.reason !== "startup" || !ctx?.hasUI) return;
|
|
@@ -40867,8 +41318,8 @@ var init_setup_wizard = __esm({
|
|
|
40867
41318
|
init_deployment_pointer();
|
|
40868
41319
|
init_read_model();
|
|
40869
41320
|
init_index();
|
|
40870
|
-
RUNTIME_VERSION = "1.
|
|
40871
|
-
RECEIVER_VERSION = "1.
|
|
41321
|
+
RUNTIME_VERSION = "1.4.0";
|
|
41322
|
+
RECEIVER_VERSION = "1.3.0";
|
|
40872
41323
|
RUNTIME_PKG = "@edgehero/pi-dispatch";
|
|
40873
41324
|
RECEIVER_PKG = "@edgehero/pi-dispatch-receiver";
|
|
40874
41325
|
NUDGE_MARKER_BASENAME = "pi-dispatch-setup.nudged";
|
|
@@ -40883,10 +41334,108 @@ var init_setup_wizard = __esm({
|
|
|
40883
41334
|
}
|
|
40884
41335
|
});
|
|
40885
41336
|
|
|
41337
|
+
// src/secrets-command.ts
|
|
41338
|
+
var secrets_command_exports = {};
|
|
41339
|
+
__export(secrets_command_exports, {
|
|
41340
|
+
runSecretsCommand: () => runSecretsCommand
|
|
41341
|
+
});
|
|
41342
|
+
function renderProfiles(profiles) {
|
|
41343
|
+
const names = Object.keys(profiles).sort();
|
|
41344
|
+
if (names.length === 0) return "No resolver profiles are declared in the settings overlay.";
|
|
41345
|
+
return ["Resolver profiles (settings overlay):", ...names.map((n) => ` ${n} -> ${profiles[n]}`)].join("\n");
|
|
41346
|
+
}
|
|
41347
|
+
async function runSecretsCommand(paths, ctx, notify, tokens, deps = {}) {
|
|
41348
|
+
const ui = ctx?.ui;
|
|
41349
|
+
const fsSeam = deps.fs ? { fs: deps.fs } : {};
|
|
41350
|
+
const view = readSettingsView({ settingsFile: paths.settingsFile, ...fsSeam });
|
|
41351
|
+
if (view.invalid) {
|
|
41352
|
+
notify?.(`settings overlay is unreadable (${view.invalid}) \u2014 fix it before declaring a profile`, "error");
|
|
41353
|
+
return;
|
|
41354
|
+
}
|
|
41355
|
+
const profiles = { ...view.overlay?.secretProfiles ?? {} };
|
|
41356
|
+
const action = tokens[0] ?? "list";
|
|
41357
|
+
if (action === "list") {
|
|
41358
|
+
notify?.(renderProfiles(profiles), "info");
|
|
41359
|
+
return;
|
|
41360
|
+
}
|
|
41361
|
+
if (typeof ui?.input !== "function" || typeof ui?.confirm !== "function") {
|
|
41362
|
+
notify?.("declaring a profile needs dialogs (newer pi) \u2014 no input/confirm available", "warning");
|
|
41363
|
+
return;
|
|
41364
|
+
}
|
|
41365
|
+
if (action === "add") {
|
|
41366
|
+
const nameAnswer = await ui.input("profile name \u2014 what a trigger's run.secretsProfile will say (e.g. prod)", "default");
|
|
41367
|
+
const name = (nameAnswer ?? "").trim();
|
|
41368
|
+
if (name === "") return;
|
|
41369
|
+
if (!PROFILE_NAME.test(name)) {
|
|
41370
|
+
notify?.("a profile name may use letters, digits, dot, dash and underscore only \u2014 PI_SECRET_PROFILES is a comma-separated list of name:path pairs", "error");
|
|
41371
|
+
return;
|
|
41372
|
+
}
|
|
41373
|
+
const pathAnswer = await ui.input("resolver \u2014 ABSOLUTE path to the script that reads one reference", "/opt/pi/resolve.sh");
|
|
41374
|
+
const path = (pathAnswer ?? "").trim();
|
|
41375
|
+
if (path === "") return;
|
|
41376
|
+
if (!(path.startsWith("/") || /^([A-Za-z]:[\\/]|\\\\)/.test(path))) {
|
|
41377
|
+
notify?.("the resolver needs an ABSOLUTE path: a service manager's working directory is not your shell's", "error");
|
|
41378
|
+
return;
|
|
41379
|
+
}
|
|
41380
|
+
const replacing = profiles[name] ? `
|
|
41381
|
+
|
|
41382
|
+
This REPLACES the current ${name} -> ${profiles[name]}` : "";
|
|
41383
|
+
const ok = await ui.confirm(
|
|
41384
|
+
"Declare a secret resolver",
|
|
41385
|
+
`${paths.settingsFile} gets:
|
|
41386
|
+
${JSON.stringify({ secretProfiles: { ...profiles, [name]: path } }, null, 2)}${replacing}
|
|
41387
|
+
|
|
41388
|
+
The worker EXECUTES this script, as the account it runs as, once per reference. It must also sit inside PI_SECRET_RESOLVER_ROOTS or the worker will refuse it.`
|
|
41389
|
+
);
|
|
41390
|
+
if (!ok) {
|
|
41391
|
+
notify?.("not declared", "info");
|
|
41392
|
+
return;
|
|
41393
|
+
}
|
|
41394
|
+
const res = writeSettings({ settingsFile: paths.settingsFile, mutate: (o) => ({ ...o, secretProfiles: { ...profiles, [name]: path } }), ...fsSeam });
|
|
41395
|
+
if (res.invalid) {
|
|
41396
|
+
notify?.(`rejected: ${res.invalid}`, "error");
|
|
41397
|
+
return;
|
|
41398
|
+
}
|
|
41399
|
+
notify?.(`declared ${name} -> ${path}. A trigger reaches it with "secretsProfile": "${name}" beside its "secrets" map, which is an edit to triggers.json.`, "info");
|
|
41400
|
+
return;
|
|
41401
|
+
}
|
|
41402
|
+
if (action === "remove") {
|
|
41403
|
+
const name = (tokens[1] ?? "").trim();
|
|
41404
|
+
if (!name || !(name in profiles)) {
|
|
41405
|
+
notify?.(`no such profile. ${renderProfiles(profiles)}`, "warning");
|
|
41406
|
+
return;
|
|
41407
|
+
}
|
|
41408
|
+
const ok = await ui.confirm("Remove a secret resolver", `${name} -> ${profiles[name]}
|
|
41409
|
+
|
|
41410
|
+
Every trigger naming it refuses PRE-SPEND after this, rather than running without its secrets.`);
|
|
41411
|
+
if (!ok) {
|
|
41412
|
+
notify?.("not removed", "info");
|
|
41413
|
+
return;
|
|
41414
|
+
}
|
|
41415
|
+
const next = { ...profiles };
|
|
41416
|
+
delete next[name];
|
|
41417
|
+
const res = writeSettings({ settingsFile: paths.settingsFile, mutate: (o) => ({ ...o, secretProfiles: next }), ...fsSeam });
|
|
41418
|
+
if (res.invalid) {
|
|
41419
|
+
notify?.(`rejected: ${res.invalid}`, "error");
|
|
41420
|
+
return;
|
|
41421
|
+
}
|
|
41422
|
+
notify?.(`removed ${name}`, "info");
|
|
41423
|
+
return;
|
|
41424
|
+
}
|
|
41425
|
+
notify?.("usage: /dispatch secrets <list|add|remove <name>>", "warning");
|
|
41426
|
+
}
|
|
41427
|
+
var PROFILE_NAME;
|
|
41428
|
+
var init_secrets_command = __esm({
|
|
41429
|
+
"src/secrets-command.ts"() {
|
|
41430
|
+
init_read_model();
|
|
41431
|
+
PROFILE_NAME = /^[A-Za-z0-9._-]+$/;
|
|
41432
|
+
}
|
|
41433
|
+
});
|
|
41434
|
+
|
|
40886
41435
|
// src/index.ts
|
|
40887
41436
|
import { VERSION } from "@earendil-works/pi-coding-agent";
|
|
40888
41437
|
import { fileURLToPath as fileURLToPath2, pathToFileURL } from "node:url";
|
|
40889
|
-
import * as
|
|
41438
|
+
import * as nodeFs6 from "node:fs";
|
|
40890
41439
|
function computePiVersionAdvisory(version, supported) {
|
|
40891
41440
|
if (version === supported || version === "0.0.0") return void 0;
|
|
40892
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`;
|
|
@@ -41028,7 +41577,7 @@ function registerTools(pi) {
|
|
|
41028
41577
|
pi.registerTool({
|
|
41029
41578
|
name: "dispatch_triggers",
|
|
41030
41579
|
label: "pi-dispatch triggers",
|
|
41031
|
-
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.",
|
|
41032
41581
|
parameters: typebox_exports.Object({}),
|
|
41033
41582
|
async execute() {
|
|
41034
41583
|
const paths = resolvePaths(process.env);
|
|
@@ -41073,7 +41622,7 @@ function registerTools(pi) {
|
|
|
41073
41622
|
pi.registerTool({
|
|
41074
41623
|
name: "dispatch_trigger_add",
|
|
41075
41624
|
label: "pi-dispatch add trigger",
|
|
41076
|
-
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.",
|
|
41077
41626
|
executionMode: "sequential",
|
|
41078
41627
|
parameters: typebox_exports.Object({
|
|
41079
41628
|
kind: typebox_exports.String(),
|
|
@@ -41090,13 +41639,18 @@ function registerTools(pi) {
|
|
|
41090
41639
|
phrase: typebox_exports.Optional(typebox_exports.String()),
|
|
41091
41640
|
labels: typebox_exports.Optional(typebox_exports.Array(typebox_exports.String())),
|
|
41092
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()),
|
|
41093
41647
|
model: typebox_exports.Optional(typebox_exports.String()),
|
|
41094
41648
|
provider: typebox_exports.Optional(typebox_exports.String()),
|
|
41095
41649
|
maxTurns: typebox_exports.Optional(typebox_exports.Integer({ minimum: 1 }))
|
|
41096
41650
|
}),
|
|
41097
41651
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
41098
41652
|
const entry = buildTriggerEntry(params.kind, params);
|
|
41099
|
-
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)`);
|
|
41100
41654
|
const result = await confirmedWrite(
|
|
41101
41655
|
ctx,
|
|
41102
41656
|
{ title: `Add ${params.kind} trigger`, message: `Add to triggers.json:
|
|
@@ -41307,6 +41861,17 @@ function buildTriggerEntry(kind, f) {
|
|
|
41307
41861
|
const on = { type: "pull_request", action: asWords(f.action) };
|
|
41308
41862
|
const any = asWords(f.labels ?? f.any);
|
|
41309
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;
|
|
41310
41875
|
return { on, run: { kind: forge, flow: f.flow } };
|
|
41311
41876
|
}
|
|
41312
41877
|
return null;
|
|
@@ -41358,7 +41923,7 @@ async function dispatch(pi, args, ctx) {
|
|
|
41358
41923
|
if (det.state === "pointer" && !runtimeSkewNoticed) {
|
|
41359
41924
|
const ptrRes = readPointer({ path: pointerPath(process.env) });
|
|
41360
41925
|
const deploymentDir = ptrRes.pointer?.deploymentDir;
|
|
41361
|
-
const installed = deploymentDir ? readInstalledVersion2(
|
|
41926
|
+
const installed = deploymentDir ? readInstalledVersion2(nodeFs6, runtimeDirFor2(deploymentDir)) : void 0;
|
|
41362
41927
|
if (typeof installed === "string" && installed !== RUNTIME_VERSION2) {
|
|
41363
41928
|
runtimeSkewNoticed = true;
|
|
41364
41929
|
notify?.(
|
|
@@ -41458,6 +42023,11 @@ ${renderBudget({ budget, settings: settings2 })}`);
|
|
|
41458
42023
|
applyUnset(paths.settingsFile, tokens, notify);
|
|
41459
42024
|
return;
|
|
41460
42025
|
}
|
|
42026
|
+
case "secrets": {
|
|
42027
|
+
const { runSecretsCommand: runSecretsCommand2 } = await Promise.resolve().then(() => (init_secrets_command(), secrets_command_exports));
|
|
42028
|
+
await runSecretsCommand2(paths, ctx, notify, tokens.slice(1));
|
|
42029
|
+
return;
|
|
42030
|
+
}
|
|
41461
42031
|
case "setup": {
|
|
41462
42032
|
const { runSetupWizard: runSetupWizard2 } = await Promise.resolve().then(() => (init_setup_wizard(), setup_wizard_exports));
|
|
41463
42033
|
await runSetupWizard2(paths, ctx, notify, { openDashboardFn: openDashboard });
|
|
@@ -41512,7 +42082,7 @@ async function assembleGraph(paths) {
|
|
|
41512
42082
|
});
|
|
41513
42083
|
}
|
|
41514
42084
|
function realArtifactDeps() {
|
|
41515
|
-
return { fs:
|
|
42085
|
+
return { fs: nodeFs6, openBrowser, env: process.env, now: () => Date.now(), platform: process.platform };
|
|
41516
42086
|
}
|
|
41517
42087
|
function isHeadlessEnv(env, platform) {
|
|
41518
42088
|
if (env?.SSH_CONNECTION || env?.SSH_TTY) return "SSH session";
|
|
@@ -42038,7 +42608,7 @@ function completeArguments(prefix) {
|
|
|
42038
42608
|
}
|
|
42039
42609
|
return null;
|
|
42040
42610
|
}
|
|
42041
|
-
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;
|
|
42042
42612
|
var init_index = __esm({
|
|
42043
42613
|
"src/index.ts"() {
|
|
42044
42614
|
init_build();
|
|
@@ -42063,7 +42633,7 @@ var init_index = __esm({
|
|
|
42063
42633
|
runtimeSkewNoticed = false;
|
|
42064
42634
|
CHANNEL = "pi-dispatch-admin";
|
|
42065
42635
|
REBUILT_NOTICE = (reason) => `replaced invalid settings file (${reason}) \u2014 other keys were lost`;
|
|
42066
|
-
USAGE = "usage: /dispatch <status|pause|resume|run|runs|logs|budget|insights|triggers|settings|set|unset|setup>";
|
|
42636
|
+
USAGE = "usage: /dispatch <status|pause|resume|run|runs|logs|budget|insights|triggers|settings|set|unset|setup|secrets>";
|
|
42067
42637
|
KNOWN_SUBCOMMANDS = [
|
|
42068
42638
|
"status",
|
|
42069
42639
|
"pause",
|
|
@@ -42077,13 +42647,18 @@ var init_index = __esm({
|
|
|
42077
42647
|
"settings",
|
|
42078
42648
|
"set",
|
|
42079
42649
|
"unset",
|
|
42080
|
-
"setup"
|
|
42650
|
+
"setup",
|
|
42651
|
+
"secrets"
|
|
42081
42652
|
];
|
|
42082
42653
|
FORGE_PROMPT = "forge \u2014 github, gitlab, forgejo or azure";
|
|
42654
|
+
ISSUE_CLOSE_WORD = { github: "closed", gitlab: "close", forgejo: "closed" };
|
|
42083
42655
|
PR_ACTION_VOCAB = {
|
|
42084
|
-
|
|
42085
|
-
|
|
42086
|
-
|
|
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" },
|
|
42087
42662
|
azure: { hint: "created updated", dflt: "updated" }
|
|
42088
42663
|
};
|
|
42089
42664
|
PRICING = { listPricedModels, getPricedModel, isZeroRated, reprice, piAiVersion };
|