@edgehero/pi-dispatch-admin 0.6.0 → 0.7.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 +6 -0
- package/dist/index.mjs +2259 -51
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -7689,12 +7689,18 @@ function defaultSandboxDir(env = process.env) {
|
|
|
7689
7689
|
function defaultLogsDir() {
|
|
7690
7690
|
return `${process.env.TMPDIR ?? process.env.TEMP ?? "/tmp"}/pi-dispatch/logs`.replace(/\\/g, "/");
|
|
7691
7691
|
}
|
|
7692
|
+
function defaultGraphDir(env = process.env) {
|
|
7693
|
+
return `${env.TMPDIR ?? env.TEMP ?? "/tmp"}/pi-dispatch/graph`.replace(/\\/g, "/");
|
|
7694
|
+
}
|
|
7692
7695
|
function defaultSettingsFile() {
|
|
7693
7696
|
return `${process.env.TMPDIR ?? process.env.TEMP ?? "/tmp"}/pi-dispatch/settings.json`.replace(/\\/g, "/");
|
|
7694
7697
|
}
|
|
7698
|
+
var CHAIN_DEPTH_MAX_DEFAULT, CHAIN_MAX_PER_JOB_DEFAULT;
|
|
7695
7699
|
var init_config2 = __esm({
|
|
7696
7700
|
"../worker/src/config.mjs"() {
|
|
7697
7701
|
init_forges();
|
|
7702
|
+
CHAIN_DEPTH_MAX_DEFAULT = 1;
|
|
7703
|
+
CHAIN_MAX_PER_JOB_DEFAULT = 2;
|
|
7698
7704
|
}
|
|
7699
7705
|
});
|
|
7700
7706
|
|
|
@@ -8262,7 +8268,7 @@ function normalizeWindow(w, index, path) {
|
|
|
8262
8268
|
return norm;
|
|
8263
8269
|
}
|
|
8264
8270
|
function zonedParts(tz, ms) {
|
|
8265
|
-
const
|
|
8271
|
+
const fmt2 = new Intl.DateTimeFormat("en-US", {
|
|
8266
8272
|
timeZone: tz,
|
|
8267
8273
|
hour12: false,
|
|
8268
8274
|
year: "numeric",
|
|
@@ -8273,7 +8279,7 @@ function zonedParts(tz, ms) {
|
|
|
8273
8279
|
weekday: "short"
|
|
8274
8280
|
});
|
|
8275
8281
|
const p = {};
|
|
8276
|
-
for (const part of
|
|
8282
|
+
for (const part of fmt2.formatToParts(new Date(ms))) p[part.type] = part.value;
|
|
8277
8283
|
let hh = Number(p.hour);
|
|
8278
8284
|
if (hh === 24) hh = 0;
|
|
8279
8285
|
return { y: Number(p.year), mo: Number(p.month), d: Number(p.day), hh, mi: Number(p.minute), dow: WEEKDAY_INDEX[p.weekday] };
|
|
@@ -8594,9 +8600,9 @@ var init_flow_gate = __esm({
|
|
|
8594
8600
|
|
|
8595
8601
|
// ../worker/src/git-dirty.mjs
|
|
8596
8602
|
import { execFileSync } from "node:child_process";
|
|
8597
|
-
function gitDirty(folder, { exec:
|
|
8603
|
+
function gitDirty(folder, { exec: exec4 = execFileSync } = {}) {
|
|
8598
8604
|
try {
|
|
8599
|
-
const out =
|
|
8605
|
+
const out = exec4("git", ["-C", folder, "status", "--porcelain"], { encoding: "utf8" });
|
|
8600
8606
|
return out.trim().length > 0;
|
|
8601
8607
|
} catch {
|
|
8602
8608
|
return null;
|
|
@@ -8748,6 +8754,493 @@ var init_packages = __esm({
|
|
|
8748
8754
|
}
|
|
8749
8755
|
});
|
|
8750
8756
|
|
|
8757
|
+
// ../worker/src/materialize.mjs
|
|
8758
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
8759
|
+
import { promisify as promisify3 } from "node:util";
|
|
8760
|
+
function isWindowsDeviceName(segment) {
|
|
8761
|
+
const stem = segment.split(".")[0].toLowerCase();
|
|
8762
|
+
return WINDOWS_DEVICE_NAMES.has(stem);
|
|
8763
|
+
}
|
|
8764
|
+
function classifyPiPath(path) {
|
|
8765
|
+
if (path === APPEND_SYSTEM) return { outRel: "pi/APPEND_SYSTEM.md", skill: null };
|
|
8766
|
+
if (!path.startsWith(SKILLS_PREFIX)) return null;
|
|
8767
|
+
const segments = path.slice(SKILLS_PREFIX.length).split("/");
|
|
8768
|
+
if (segments.length < 2 || segments.length > 1 + PI_LIMITS.maxTailSegments) return null;
|
|
8769
|
+
const [name, ...tail] = segments;
|
|
8770
|
+
if (!SKILL_NAME_RE.test(name)) return null;
|
|
8771
|
+
for (const seg of tail) {
|
|
8772
|
+
if (!PI_SEGMENT_RE.test(seg)) return null;
|
|
8773
|
+
if (isWindowsDeviceName(seg)) return null;
|
|
8774
|
+
}
|
|
8775
|
+
const outRel = ["pi", "skills", name, ...tail].join("/");
|
|
8776
|
+
if (outRel.length > PI_LIMITS.maxOutRelChars) return null;
|
|
8777
|
+
return { outRel, skill: name };
|
|
8778
|
+
}
|
|
8779
|
+
function selectEntries(lsTreeZ) {
|
|
8780
|
+
const entries = [];
|
|
8781
|
+
let skipped = 0;
|
|
8782
|
+
for (const record of lsTreeZ.split("\0")) {
|
|
8783
|
+
if (!record) continue;
|
|
8784
|
+
const tab = record.indexOf(" ");
|
|
8785
|
+
if (tab === -1) continue;
|
|
8786
|
+
const [mode, type, oid, sizeField] = record.slice(0, tab).split(/\s+/);
|
|
8787
|
+
const path = record.slice(tab + 1);
|
|
8788
|
+
if (mode !== "100644" || type !== "blob") {
|
|
8789
|
+
skipped++;
|
|
8790
|
+
continue;
|
|
8791
|
+
}
|
|
8792
|
+
const size = Number(sizeField);
|
|
8793
|
+
if (!Number.isSafeInteger(size) || size < 0) {
|
|
8794
|
+
throw new Error(`git ls-tree: unreadable object size (is -l still in the argv?) for ${type} ${mode} ${oid}`);
|
|
8795
|
+
}
|
|
8796
|
+
const classified = classifyPiPath(path);
|
|
8797
|
+
if (!classified) {
|
|
8798
|
+
skipped++;
|
|
8799
|
+
continue;
|
|
8800
|
+
}
|
|
8801
|
+
entries.push({ oid, path, size, outRel: classified.outRel, skill: classified.skill });
|
|
8802
|
+
}
|
|
8803
|
+
return { entries, skipped };
|
|
8804
|
+
}
|
|
8805
|
+
function keepOnlyDeclaredSkills(entries) {
|
|
8806
|
+
const declared = /* @__PURE__ */ new Set();
|
|
8807
|
+
for (const e of entries) {
|
|
8808
|
+
if (e.skill !== null && e.outRel.endsWith(`/${SKILL_FILE}`)) declared.add(e.skill);
|
|
8809
|
+
}
|
|
8810
|
+
const kept = entries.filter((e) => e.skill === null || declared.has(e.skill));
|
|
8811
|
+
return { kept, skipped: entries.length - kept.length };
|
|
8812
|
+
}
|
|
8813
|
+
var exec2, PI_DIR, APPEND_SYSTEM, SKILLS_PREFIX, SKILL_FILE, PI_LIMITS, LS_TREE_MAX_BYTES, PI_SEGMENT_RE, WINDOWS_DEVICE_NAMES;
|
|
8814
|
+
var init_materialize = __esm({
|
|
8815
|
+
"../worker/src/materialize.mjs"() {
|
|
8816
|
+
init_flow_gate();
|
|
8817
|
+
exec2 = promisify3(execFile3);
|
|
8818
|
+
PI_DIR = ".pi";
|
|
8819
|
+
APPEND_SYSTEM = `${PI_DIR}/APPEND_SYSTEM.md`;
|
|
8820
|
+
SKILLS_PREFIX = `${PI_DIR}/skills/`;
|
|
8821
|
+
SKILL_FILE = "SKILL.md";
|
|
8822
|
+
PI_LIMITS = Object.freeze({
|
|
8823
|
+
maxFiles: 256,
|
|
8824
|
+
// ~50 typical skills; also bounds cat-file spawns per job
|
|
8825
|
+
maxFilesPerSkill: 64,
|
|
8826
|
+
// SKILL.md + references/ + scripts/ + assets/ runs to a handful
|
|
8827
|
+
maxFileBytes: 1 << 20,
|
|
8828
|
+
// 1 MiB: ~250k words of markdown. Larger is a dataset, not a skill.
|
|
8829
|
+
maxTotalBytes: 8 << 20,
|
|
8830
|
+
// 8 MiB: trivial beside the clone, and it bounds host disk under retention
|
|
8831
|
+
maxTailSegments: 4,
|
|
8832
|
+
// references/x.md is 2, scripts/lib/util.sh is 3
|
|
8833
|
+
maxOutRelChars: 200
|
|
8834
|
+
// Windows MAX_PATH is 260 and jobDir is an OS temp path (~40-70)
|
|
8835
|
+
});
|
|
8836
|
+
LS_TREE_MAX_BYTES = 1 << 20;
|
|
8837
|
+
PI_SEGMENT_RE = /^[a-z0-9](?:[a-z0-9_.-]{0,62}[a-z0-9])?$/i;
|
|
8838
|
+
WINDOWS_DEVICE_NAMES = /* @__PURE__ */ new Set([
|
|
8839
|
+
"con",
|
|
8840
|
+
"prn",
|
|
8841
|
+
"aux",
|
|
8842
|
+
"nul",
|
|
8843
|
+
"com1",
|
|
8844
|
+
"com2",
|
|
8845
|
+
"com3",
|
|
8846
|
+
"com4",
|
|
8847
|
+
"com5",
|
|
8848
|
+
"com6",
|
|
8849
|
+
"com7",
|
|
8850
|
+
"com8",
|
|
8851
|
+
"com9",
|
|
8852
|
+
"lpt1",
|
|
8853
|
+
"lpt2",
|
|
8854
|
+
"lpt3",
|
|
8855
|
+
"lpt4",
|
|
8856
|
+
"lpt5",
|
|
8857
|
+
"lpt6",
|
|
8858
|
+
"lpt7",
|
|
8859
|
+
"lpt8",
|
|
8860
|
+
"lpt9"
|
|
8861
|
+
]);
|
|
8862
|
+
}
|
|
8863
|
+
});
|
|
8864
|
+
|
|
8865
|
+
// src/graph-model.mjs
|
|
8866
|
+
function parseSkillMeta(text) {
|
|
8867
|
+
const empty = { name: null, description: null };
|
|
8868
|
+
if (typeof text !== "string") return empty;
|
|
8869
|
+
const normalized = text.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
|
|
8870
|
+
const block = FRONTMATTER_BLOCK_RE.exec(normalized);
|
|
8871
|
+
if (!block) return empty;
|
|
8872
|
+
return {
|
|
8873
|
+
name: frontmatterValue(block[1], "name"),
|
|
8874
|
+
description: frontmatterValue(block[1], "description")
|
|
8875
|
+
};
|
|
8876
|
+
}
|
|
8877
|
+
function frontmatterValue(block, key) {
|
|
8878
|
+
const m = new RegExp(`^${key}:[ \\t]*(.*)$`, "m").exec(block);
|
|
8879
|
+
if (!m) return null;
|
|
8880
|
+
let value = m[1].trim();
|
|
8881
|
+
if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) value = value.slice(1, -1);
|
|
8882
|
+
if (value === "") return null;
|
|
8883
|
+
return value.length > META_VALUE_MAX_CHARS ? `${value.slice(0, META_VALUE_MAX_CHARS)}\u2026` : value;
|
|
8884
|
+
}
|
|
8885
|
+
function findSiblingMentions(text, siblingNames) {
|
|
8886
|
+
if (typeof text !== "string" || !Array.isArray(siblingNames)) return [];
|
|
8887
|
+
const mentions = [];
|
|
8888
|
+
for (const name of siblingNames) {
|
|
8889
|
+
if (typeof name !== "string" || name === "") continue;
|
|
8890
|
+
const re = new RegExp(`(?<![a-z0-9_-])${escapeRegExp(name)}(?![a-z0-9_-])`, "g");
|
|
8891
|
+
let found = false;
|
|
8892
|
+
let strong = false;
|
|
8893
|
+
let match;
|
|
8894
|
+
let occurrences = 0;
|
|
8895
|
+
while ((match = re.exec(text)) !== null && occurrences++ < 32) {
|
|
8896
|
+
found = true;
|
|
8897
|
+
const windowStart = Math.max(0, match.index - CHAIN_VOCAB_RADIUS);
|
|
8898
|
+
const windowEnd = Math.min(text.length, match.index + name.length + CHAIN_VOCAB_RADIUS);
|
|
8899
|
+
if (CHAIN_VOCAB_RE.test(text.slice(windowStart, windowEnd))) {
|
|
8900
|
+
strong = true;
|
|
8901
|
+
break;
|
|
8902
|
+
}
|
|
8903
|
+
}
|
|
8904
|
+
if (found) mentions.push({ name, strong });
|
|
8905
|
+
}
|
|
8906
|
+
return mentions;
|
|
8907
|
+
}
|
|
8908
|
+
function escapeRegExp(s) {
|
|
8909
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
8910
|
+
}
|
|
8911
|
+
function findLoopHints(text) {
|
|
8912
|
+
if (typeof text !== "string") return [];
|
|
8913
|
+
const normalized = text.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
|
|
8914
|
+
const body = normalized.replace(FRONTMATTER_BLOCK_RE, "");
|
|
8915
|
+
const hints = [];
|
|
8916
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8917
|
+
let match;
|
|
8918
|
+
LOOP_HINT_RE.lastIndex = 0;
|
|
8919
|
+
while ((match = LOOP_HINT_RE.exec(body)) !== null && hints.length < LOOP_HINT_MAX) {
|
|
8920
|
+
const hint = match[0].trim();
|
|
8921
|
+
const key = hint.toLowerCase();
|
|
8922
|
+
if (seen.has(key)) continue;
|
|
8923
|
+
seen.add(key);
|
|
8924
|
+
hints.push({ hint });
|
|
8925
|
+
}
|
|
8926
|
+
return hints;
|
|
8927
|
+
}
|
|
8928
|
+
function buildGraphModel({ triggers, schedulers, folderSkills, injectedSkills, foldersTruncated, forgeRepos, cronStats, runJoin, chainEdges, caps, nowMs } = {}) {
|
|
8929
|
+
const triggerList2 = Array.isArray(triggers?.triggers) ? triggers.triggers : [];
|
|
8930
|
+
const schedulerList = Array.isArray(schedulers) ? schedulers : [];
|
|
8931
|
+
const folders = folderSkills && typeof folderSkills === "object" ? folderSkills : {};
|
|
8932
|
+
const injected = injectedSkills && typeof injectedSkills === "object" ? injectedSkills : {};
|
|
8933
|
+
const statsById = cronStats?.byId && typeof cronStats.byId === "object" ? cronStats.byId : {};
|
|
8934
|
+
const statsByIndex = runJoin?.byIndex && typeof runJoin.byIndex === "object" ? runJoin.byIndex : {};
|
|
8935
|
+
const observed = Array.isArray(chainEdges?.edges) ? chainEdges.edges : [];
|
|
8936
|
+
const model = {
|
|
8937
|
+
folders: [],
|
|
8938
|
+
nodes: [],
|
|
8939
|
+
edges: [],
|
|
8940
|
+
flags: [],
|
|
8941
|
+
caps: {
|
|
8942
|
+
chainDepthMax: Number.isInteger(caps?.chainDepthMax) ? caps.chainDepthMax : null,
|
|
8943
|
+
chainMaxPerJob: Number.isInteger(caps?.chainMaxPerJob) ? caps.chainMaxPerJob : null,
|
|
8944
|
+
sameFolderOnly: true,
|
|
8945
|
+
windowDays: Number.isInteger(caps?.windowDays) ? caps.windowDays : null
|
|
8946
|
+
},
|
|
8947
|
+
meta: {
|
|
8948
|
+
generatedAt: Number.isFinite(nowMs) ? nowMs : null,
|
|
8949
|
+
triggersMissing: triggers?.missing === true,
|
|
8950
|
+
triggersInvalid: typeof triggers?.invalid === "string" ? triggers.invalid : null,
|
|
8951
|
+
unattributedRuns: Number.isInteger(runJoin?.unattributed) ? runJoin.unattributed : 0,
|
|
8952
|
+
chainRefusals: chainEdges?.refusals && typeof chainEdges.refusals === "object" ? chainEdges.refusals : {},
|
|
8953
|
+
truncated: {
|
|
8954
|
+
// collectGraphInputs' cap flag rides its spread into this input (review finding: this was
|
|
8955
|
+
// hardcoded false, so the cap-reached banner could never fire on any surface).
|
|
8956
|
+
folders: foldersTruncated === true,
|
|
8957
|
+
skills: Object.values(folders).some((f) => f?.truncated === true),
|
|
8958
|
+
edges: chainEdges?.truncated === true
|
|
8959
|
+
},
|
|
8960
|
+
droppedObservedEdges: 0,
|
|
8961
|
+
// Injected dirs whose readdir failed (review finding): the OQ-022 badge is the one fact the
|
|
8962
|
+
// injected enumeration exists for, and an unreadable dir must not make it silently vanish.
|
|
8963
|
+
injectedUnreachable: Object.entries(injected).filter(([, r]) => typeof r?.unreachable === "string").map(([dir]) => dir).sort()
|
|
8964
|
+
}
|
|
8965
|
+
};
|
|
8966
|
+
const folderByPath = /* @__PURE__ */ new Map();
|
|
8967
|
+
for (const [path, result] of Object.entries(folders)) {
|
|
8968
|
+
const group = {
|
|
8969
|
+
key: `folder:${path}`,
|
|
8970
|
+
path,
|
|
8971
|
+
label: basenameOf(path),
|
|
8972
|
+
kind: "local",
|
|
8973
|
+
head: typeof result?.head === "string" ? result.head : null,
|
|
8974
|
+
unreachable: typeof result?.unreachable === "string" ? result.unreachable : null,
|
|
8975
|
+
triggerIds: [],
|
|
8976
|
+
skillIds: []
|
|
8977
|
+
};
|
|
8978
|
+
folderByPath.set(path, group);
|
|
8979
|
+
model.folders.push(group);
|
|
8980
|
+
}
|
|
8981
|
+
const forgeGroups = /* @__PURE__ */ new Map();
|
|
8982
|
+
const forgeGroup = (forge) => {
|
|
8983
|
+
const name = typeof forge === "string" && forge !== "" ? forge : "forge";
|
|
8984
|
+
let group = forgeGroups.get(name);
|
|
8985
|
+
if (!group) {
|
|
8986
|
+
const repos = Array.isArray(forgeRepos?.[name]) ? forgeRepos[name] : [];
|
|
8987
|
+
group = { key: `forge:${name}`, path: null, label: name, kind: "forge", head: null, unreachable: "remote-repo", repos, triggerIds: [], skillIds: [] };
|
|
8988
|
+
forgeGroups.set(name, group);
|
|
8989
|
+
model.folders.push(group);
|
|
8990
|
+
}
|
|
8991
|
+
return group;
|
|
8992
|
+
};
|
|
8993
|
+
const skillNode = /* @__PURE__ */ new Map();
|
|
8994
|
+
const addSkill = (group, skill) => {
|
|
8995
|
+
const id = `skill:${group.key}:${skill.name}`;
|
|
8996
|
+
const node = {
|
|
8997
|
+
id,
|
|
8998
|
+
kind: "skill",
|
|
8999
|
+
name: skill.name,
|
|
9000
|
+
folderKey: group.key,
|
|
9001
|
+
isSub: skill.isSub === true,
|
|
9002
|
+
group: skill.isSub === true ? skill.group : null,
|
|
9003
|
+
aiTrigger: skill.aiTrigger === true,
|
|
9004
|
+
meta: skill.meta ?? null,
|
|
9005
|
+
// The prose-loop hints read from the SKILL.md body (findLoopHints): everything a loop does
|
|
9006
|
+
// happens INSIDE this one node's job, which is why consumers group the marker inside the
|
|
9007
|
+
// skill rather than drawing an edge anywhere.
|
|
9008
|
+
loops: Array.isArray(skill.loops) ? skill.loops.filter((l) => typeof l?.hint === "string") : [],
|
|
9009
|
+
unread: skill.unread === true,
|
|
9010
|
+
isFlow: false,
|
|
9011
|
+
// set true when a config edge lands on it
|
|
9012
|
+
mentionedBy: 0
|
|
9013
|
+
};
|
|
9014
|
+
skillNode.set(`${group.key} ${skill.name}`, node);
|
|
9015
|
+
model.nodes.push(node);
|
|
9016
|
+
group.skillIds.push(id);
|
|
9017
|
+
if (node.unread) model.flags.push({ nodeId: id, flag: "unread", detail: "SKILL.md unreadable at enumeration; gate read as closed" });
|
|
9018
|
+
return node;
|
|
9019
|
+
};
|
|
9020
|
+
for (const [path, result] of Object.entries(folders)) {
|
|
9021
|
+
const group = folderByPath.get(path);
|
|
9022
|
+
for (const skill of Array.isArray(result?.skills) ? result.skills : []) {
|
|
9023
|
+
if (typeof skill?.name === "string" && skill.name !== "") addSkill(group, skill);
|
|
9024
|
+
}
|
|
9025
|
+
}
|
|
9026
|
+
for (const [dir, result] of Object.entries(injected)) {
|
|
9027
|
+
for (const skill of Array.isArray(result?.skills) ? result.skills : []) {
|
|
9028
|
+
if (typeof skill?.name !== "string" || skill.name === "") continue;
|
|
9029
|
+
const id = `injected:${dir}:${skill.name}`;
|
|
9030
|
+
model.nodes.push({ id, kind: "injected", name: skill.name, dir, aiTrigger: skill.aiTrigger === true });
|
|
9031
|
+
if (skill.aiTrigger === true) {
|
|
9032
|
+
model.flags.push({ nodeId: id, flag: "injected-ai-trigger", detail: "ai-trigger: allow on an injected skill is a silent no-op (OQ-022)" });
|
|
9033
|
+
}
|
|
9034
|
+
}
|
|
9035
|
+
}
|
|
9036
|
+
const missingNode = (group, name) => {
|
|
9037
|
+
const key = `${group.key} ${name}`;
|
|
9038
|
+
let node = skillNode.get(key);
|
|
9039
|
+
if (node) return node;
|
|
9040
|
+
node = { id: `skill:${group.key}:${name}`, kind: "skill-missing", name, folderKey: group.key, isSub: false, group: null, aiTrigger: false, meta: null, unread: false, isFlow: false, mentionedBy: 0 };
|
|
9041
|
+
skillNode.set(key, node);
|
|
9042
|
+
model.nodes.push(node);
|
|
9043
|
+
group.skillIds.push(node.id);
|
|
9044
|
+
return node;
|
|
9045
|
+
};
|
|
9046
|
+
for (const t of triggerList2) {
|
|
9047
|
+
if (!t || typeof t !== "object" || !Number.isInteger(t.index)) continue;
|
|
9048
|
+
const id = `trigger:${t.index}`;
|
|
9049
|
+
const isCron = t.type === "cron";
|
|
9050
|
+
let group = null;
|
|
9051
|
+
if (isCron) {
|
|
9052
|
+
if (typeof t.folder === "string" && t.folder !== "") {
|
|
9053
|
+
group = folderByPath.get(t.folder) ?? null;
|
|
9054
|
+
if (!group) {
|
|
9055
|
+
group = { key: `folder:${t.folder}`, path: t.folder, label: basenameOf(t.folder), kind: "local", head: null, unreachable: "not-enumerated", triggerIds: [], skillIds: [] };
|
|
9056
|
+
folderByPath.set(t.folder, group);
|
|
9057
|
+
model.folders.push(group);
|
|
9058
|
+
}
|
|
9059
|
+
} else {
|
|
9060
|
+
group = folderByPath.get("") ?? null;
|
|
9061
|
+
if (!group) {
|
|
9062
|
+
group = { key: "folder:(none)", path: null, label: "(no folder)", kind: "local", head: null, unreachable: "no-folder", triggerIds: [], skillIds: [] };
|
|
9063
|
+
folderByPath.set("", group);
|
|
9064
|
+
model.folders.push(group);
|
|
9065
|
+
}
|
|
9066
|
+
}
|
|
9067
|
+
} else {
|
|
9068
|
+
group = forgeGroup(t.forge);
|
|
9069
|
+
}
|
|
9070
|
+
const stats = isCron ? typeof t.id === "string" ? statsById[t.id] : void 0 : statsByIndex[t.index];
|
|
9071
|
+
const sched = isCron ? matchScheduler(schedulerList, t) : null;
|
|
9072
|
+
const node = {
|
|
9073
|
+
id,
|
|
9074
|
+
kind: "trigger",
|
|
9075
|
+
index: t.index,
|
|
9076
|
+
onType: t.type,
|
|
9077
|
+
forge: isCron ? null : t.forge ?? null,
|
|
9078
|
+
cronId: isCron ? t.id ?? null : null,
|
|
9079
|
+
pattern: isCron ? t.pattern ?? null : null,
|
|
9080
|
+
label: triggerMatchLabel(t),
|
|
9081
|
+
flow: t.flow ?? null,
|
|
9082
|
+
replicas: t.replicas ?? null,
|
|
9083
|
+
folderKey: group?.key ?? null,
|
|
9084
|
+
runs: Number.isInteger(stats?.runs) ? stats.runs : 0,
|
|
9085
|
+
lastOutcome: stats?.lastOutcome ?? null,
|
|
9086
|
+
lastEndedAt: stats?.lastEndedAt ?? null,
|
|
9087
|
+
next: sched?.next ?? null,
|
|
9088
|
+
overdueMs: sched?.overdueMs ?? null
|
|
9089
|
+
};
|
|
9090
|
+
model.nodes.push(node);
|
|
9091
|
+
if (group) group.triggerIds.push(id);
|
|
9092
|
+
if (t.type === "pull_request" && Array.isArray(t.action) && t.action.some((a) => a === "opened" || a === "synchronize")) {
|
|
9093
|
+
model.flags.push({ nodeId: id, flag: "pr-spend-loop-risk", detail: "fires on opened/synchronize; a flow that pushes can loop with another bot (OQ-020)" });
|
|
9094
|
+
}
|
|
9095
|
+
if (isCron) model.edges.push({ from: id, to: id, kind: "cron-rearm", label: t.pattern ?? null });
|
|
9096
|
+
if (typeof t.flow === "string" && t.flow !== "") {
|
|
9097
|
+
if (!SKILL_NAME_RE.test(t.flow)) {
|
|
9098
|
+
const target = group ? missingNode(group, clipName(t.flow)) : null;
|
|
9099
|
+
if (target) model.edges.push({ from: id, to: target.id, kind: "config" });
|
|
9100
|
+
model.flags.push({ nodeId: id, flag: "charset-invalid", detail: `run.flow ${JSON.stringify(clipName(t.flow))} fails the skill charset and can never materialise` });
|
|
9101
|
+
continue;
|
|
9102
|
+
}
|
|
9103
|
+
if (isCron && group && group.unreachable === null) {
|
|
9104
|
+
const existing = skillNode.get(`${group.key} ${t.flow}`);
|
|
9105
|
+
if (existing && existing.kind === "skill" && !existing.isSub) {
|
|
9106
|
+
existing.isFlow = true;
|
|
9107
|
+
model.edges.push({ from: id, to: existing.id, kind: "config" });
|
|
9108
|
+
} else {
|
|
9109
|
+
const target = missingNode(group, t.flow);
|
|
9110
|
+
model.edges.push({ from: id, to: target.id, kind: "config" });
|
|
9111
|
+
model.flags.push({ nodeId: id, flag: "no-skill", detail: `.pi/skills/${t.flow}/SKILL.md absent at HEAD` });
|
|
9112
|
+
}
|
|
9113
|
+
} else if (group) {
|
|
9114
|
+
const target = missingNode(group, t.flow);
|
|
9115
|
+
if (target.kind === "skill-missing") target.kind = "skill-unverified";
|
|
9116
|
+
target.isFlow = true;
|
|
9117
|
+
model.edges.push({ from: id, to: target.id, kind: "config" });
|
|
9118
|
+
}
|
|
9119
|
+
}
|
|
9120
|
+
}
|
|
9121
|
+
const localGroups = model.folders.filter((f) => f.kind === "local");
|
|
9122
|
+
for (const edge of observed) {
|
|
9123
|
+
if (typeof edge?.parentFlow !== "string" || typeof edge?.childFlow !== "string") continue;
|
|
9124
|
+
const base = typeof edge.target === "string" && edge.target.startsWith("local:") ? edge.target.slice("local:".length) : null;
|
|
9125
|
+
const matches = base === null ? [] : localGroups.filter((f) => f.label === base);
|
|
9126
|
+
if (matches.length !== 1) {
|
|
9127
|
+
model.meta.droppedObservedEdges++;
|
|
9128
|
+
continue;
|
|
9129
|
+
}
|
|
9130
|
+
const group = matches[0];
|
|
9131
|
+
if (group.unreachable !== null) {
|
|
9132
|
+
model.meta.droppedObservedEdges++;
|
|
9133
|
+
continue;
|
|
9134
|
+
}
|
|
9135
|
+
const from = missingNode(group, edge.parentFlow);
|
|
9136
|
+
const to = missingNode(group, edge.childFlow);
|
|
9137
|
+
model.edges.push({ from: from.id, to: to.id, kind: "observed", count: Number.isInteger(edge.count) ? edge.count : 0, lastEndedAt: edge.lastEndedAt ?? null });
|
|
9138
|
+
}
|
|
9139
|
+
for (const [path, result] of Object.entries(folders)) {
|
|
9140
|
+
const group = folderByPath.get(path);
|
|
9141
|
+
for (const skill of Array.isArray(result?.skills) ? result.skills : []) {
|
|
9142
|
+
if (skill?.isSub === true) continue;
|
|
9143
|
+
const from = skillNode.get(`${group.key} ${skill?.name}`);
|
|
9144
|
+
if (!from) continue;
|
|
9145
|
+
for (const mention of Array.isArray(skill?.mentions) ? skill.mentions : []) {
|
|
9146
|
+
const to = skillNode.get(`${group.key} ${mention?.name}`);
|
|
9147
|
+
if (!to || to.isSub) continue;
|
|
9148
|
+
to.mentionedBy++;
|
|
9149
|
+
model.edges.push({
|
|
9150
|
+
from: from.id,
|
|
9151
|
+
to: to.id,
|
|
9152
|
+
kind: "potential",
|
|
9153
|
+
strong: mention.strong === true,
|
|
9154
|
+
// Eligibility is the exact static half: without ai-trigger: allow on the TARGET, the
|
|
9155
|
+
// outbox gate refuses this edge every time, so a mention alone renders "can never fire".
|
|
9156
|
+
eligible: to.aiTrigger === true
|
|
9157
|
+
});
|
|
9158
|
+
}
|
|
9159
|
+
}
|
|
9160
|
+
}
|
|
9161
|
+
for (const [path] of Object.entries(folders)) {
|
|
9162
|
+
const group = folderByPath.get(path);
|
|
9163
|
+
if (group.unreachable !== null) continue;
|
|
9164
|
+
for (const id of group.skillIds) {
|
|
9165
|
+
const node = model.nodes.find((n) => n.id === id);
|
|
9166
|
+
if (!node || node.kind !== "skill" || node.isSub || node.unread) continue;
|
|
9167
|
+
if (node.isFlow) continue;
|
|
9168
|
+
if (node.aiTrigger) {
|
|
9169
|
+
model.flags.push({ nodeId: id, flag: "ai-reachable-no-trigger", detail: "no trigger names it, but ai-trigger: allow keeps it chain/dispatch_run-reachable" });
|
|
9170
|
+
} else if (node.mentionedBy === 0) {
|
|
9171
|
+
model.flags.push({ nodeId: id, flag: "orphan", detail: "no trigger, no ai-trigger, no mention: dead by every path the system has" });
|
|
9172
|
+
}
|
|
9173
|
+
}
|
|
9174
|
+
}
|
|
9175
|
+
return model;
|
|
9176
|
+
}
|
|
9177
|
+
function triggerMatchLabel(t) {
|
|
9178
|
+
switch (t?.type) {
|
|
9179
|
+
case "cron":
|
|
9180
|
+
return `${t.id ?? "-"} ${t.pattern ?? "-"}`;
|
|
9181
|
+
case "label":
|
|
9182
|
+
return selectorLabel(t);
|
|
9183
|
+
case "comment":
|
|
9184
|
+
return `"${t.phrase ?? "-"}"`;
|
|
9185
|
+
case "pull_request":
|
|
9186
|
+
return `action[${(Array.isArray(t.action) ? t.action : []).join(",")}]`;
|
|
9187
|
+
default:
|
|
9188
|
+
return "(unknown)";
|
|
9189
|
+
}
|
|
9190
|
+
}
|
|
9191
|
+
function selectorLabel(t) {
|
|
9192
|
+
const parts = [];
|
|
9193
|
+
if (Array.isArray(t.any) && t.any.length) parts.push(`any[${t.any.join(",")}]`);
|
|
9194
|
+
if (Array.isArray(t.all) && t.all.length) parts.push(`all[${t.all.join(",")}]`);
|
|
9195
|
+
if (Array.isArray(t.none) && t.none.length) parts.push(`none[${t.none.join(",")}]`);
|
|
9196
|
+
return parts.join(" ") || "(no selector)";
|
|
9197
|
+
}
|
|
9198
|
+
function matchScheduler(schedulers, t) {
|
|
9199
|
+
if (typeof t?.id === "string") {
|
|
9200
|
+
const byId = schedulers.find((s) => s?.key === t.id || s?.name === t.id);
|
|
9201
|
+
if (byId) return byId;
|
|
9202
|
+
}
|
|
9203
|
+
return typeof t?.pattern === "string" ? schedulers.find((s) => s?.pattern === t.pattern) ?? null : null;
|
|
9204
|
+
}
|
|
9205
|
+
function basenameOf(path) {
|
|
9206
|
+
const trimmed = String(path).replace(/[\\/]+$/, "");
|
|
9207
|
+
const at = Math.max(trimmed.lastIndexOf("/"), trimmed.lastIndexOf("\\"));
|
|
9208
|
+
return at === -1 ? trimmed : trimmed.slice(at + 1);
|
|
9209
|
+
}
|
|
9210
|
+
function clipName(name) {
|
|
9211
|
+
const s = String(name);
|
|
9212
|
+
return s.length > 64 ? `${s.slice(0, 64)}\u2026` : s;
|
|
9213
|
+
}
|
|
9214
|
+
var FRONTMATTER_BLOCK_RE, META_VALUE_MAX_CHARS, CHAIN_VOCAB_RE, CHAIN_VOCAB_RADIUS, LOOP_HINT_RE, LOOP_HINT_MAX, GRAPH_EDGE_KINDS, GRAPH_FLAGS;
|
|
9215
|
+
var init_graph_model = __esm({
|
|
9216
|
+
"src/graph-model.mjs"() {
|
|
9217
|
+
init_flow_gate();
|
|
9218
|
+
FRONTMATTER_BLOCK_RE = /^---\n([\s\S]*?)\n---(?:\n|$)/;
|
|
9219
|
+
META_VALUE_MAX_CHARS = 120;
|
|
9220
|
+
CHAIN_VOCAB_RE = /outbox|request-|"flow"|\bchain\b|follow-up/i;
|
|
9221
|
+
CHAIN_VOCAB_RADIUS = 200;
|
|
9222
|
+
LOOP_HINT_RE = /\b(?:until|repeat(?:ing)?|iterate(?:s|d)?|loop(?:s|ing)?|while|for each|keep \w+ing|try again)\b[^.\n]{0,60}/gi;
|
|
9223
|
+
LOOP_HINT_MAX = 3;
|
|
9224
|
+
GRAPH_EDGE_KINDS = Object.freeze(["config", "observed", "potential", "cron-rearm"]);
|
|
9225
|
+
GRAPH_FLAGS = Object.freeze([
|
|
9226
|
+
"no-skill",
|
|
9227
|
+
// config edge target absent at HEAD in an ENUMERATED folder (readFlowGate's precise token)
|
|
9228
|
+
"charset-invalid",
|
|
9229
|
+
// run.flow fails SKILL_NAME_RE: can never materialise, a distinct defect from no-skill
|
|
9230
|
+
"orphan",
|
|
9231
|
+
// no trigger, no ai-trigger, no incoming mention: dead by every path the system has
|
|
9232
|
+
"ai-reachable-no-trigger",
|
|
9233
|
+
// no trigger but ai-trigger: allow -- deliberately chain/dispatch_run-reachable
|
|
9234
|
+
"injected-ai-trigger",
|
|
9235
|
+
// an injected skill carrying ai-trigger: allow is a silent no-op (OQ-022)
|
|
9236
|
+
"unread",
|
|
9237
|
+
// SKILL.md unreadable/oversized at enumeration: facts unknown, gate read as closed
|
|
9238
|
+
"pr-spend-loop-risk"
|
|
9239
|
+
// pull_request on opened/synchronize: the OQ-020 cross-actor spend-loop signature
|
|
9240
|
+
]);
|
|
9241
|
+
}
|
|
9242
|
+
});
|
|
9243
|
+
|
|
8751
9244
|
// src/read-model.mjs
|
|
8752
9245
|
import * as nodeFs2 from "node:fs";
|
|
8753
9246
|
import { join as join2, delimiter, sep } from "node:path";
|
|
@@ -8789,7 +9282,18 @@ function resolvePaths(env = process.env) {
|
|
|
8789
9282
|
dispatchRunPerHour: parseNonNegInt(env.PI_DISPATCH_RUN_PER_HOUR, 3),
|
|
8790
9283
|
// The per-scheduler stall threshold, mirrored from the worker's PI_SCHEDULER_STALL_MAX (default 2) so the
|
|
8791
9284
|
// cron drill-in can show `stalls n/threshold`. Read directly from env for the same reason as above.
|
|
8792
|
-
schedulerStallMax: parseNonNegInt(env.PI_SCHEDULER_STALL_MAX, 2)
|
|
9285
|
+
schedulerStallMax: parseNonNegInt(env.PI_SCHEDULER_STALL_MAX, 2),
|
|
9286
|
+
// The chain caps the graph states (issue #54), on the schedulerStallMax pattern: env read directly
|
|
9287
|
+
// (never loadConfig), DEFAULTS imported from the worker so there is no second literal to drift --
|
|
9288
|
+
// a graph printing "depth <= 1" while the worker enforces 2 would be the exact dishonesty the
|
|
9289
|
+
// GRAPH view exists to remove.
|
|
9290
|
+
chainDepthMax: parseNonNegInt(env.PI_CHAIN_DEPTH_MAX, CHAIN_DEPTH_MAX_DEFAULT),
|
|
9291
|
+
chainMaxPerJob: parseNonNegInt(env.PI_CHAIN_MAX_PER_JOB, CHAIN_MAX_PER_JOB_DEFAULT),
|
|
9292
|
+
// Where the graph HTML artifact lands (issue #54): the worker's own temp-dir default, imported
|
|
9293
|
+
// like defaultLogsDir/defaultSandboxDir above, so the admin and any future worker consumer agree
|
|
9294
|
+
// on the path without loadConfig. Deliberately NOT logsDir -- that directory's filename shape is
|
|
9295
|
+
// contract (INT-RUN-HISTORY-FILE-CONTRACT).
|
|
9296
|
+
graphDir: env.PI_GRAPH_DIR || defaultGraphDir(env)
|
|
8793
9297
|
};
|
|
8794
9298
|
}
|
|
8795
9299
|
function parseNonNegInt(raw, fallback) {
|
|
@@ -8826,9 +9330,9 @@ async function setQueuePaused({ url, paused, makeQueueFn = makeQueue, parseConne
|
|
|
8826
9330
|
});
|
|
8827
9331
|
}
|
|
8828
9332
|
}
|
|
8829
|
-
function revParseHead(folder, { exec:
|
|
9333
|
+
function revParseHead(folder, { exec: exec4 = execFileSync2 } = {}) {
|
|
8830
9334
|
try {
|
|
8831
|
-
const out =
|
|
9335
|
+
const out = exec4("git", ["-C", folder, "rev-parse", "HEAD"], { encoding: "utf8" });
|
|
8832
9336
|
const sha = out.trim();
|
|
8833
9337
|
return sha.length > 0 ? sha : null;
|
|
8834
9338
|
} catch {
|
|
@@ -9143,11 +9647,11 @@ function readTriggers({ triggersPath, fs = nodeFs2 }) {
|
|
|
9143
9647
|
return { invalid: 'triggers file must have a "triggers" array' };
|
|
9144
9648
|
}
|
|
9145
9649
|
const triggers = [];
|
|
9146
|
-
|
|
9650
|
+
entries.forEach((entry, index) => {
|
|
9147
9651
|
const display = normalizeTriggerForDisplay(entry);
|
|
9148
|
-
if (display) triggers.push(display);
|
|
9149
|
-
}
|
|
9150
|
-
return { triggers };
|
|
9652
|
+
if (display) triggers.push({ ...display, index });
|
|
9653
|
+
});
|
|
9654
|
+
return { triggers, count: entries.length };
|
|
9151
9655
|
}
|
|
9152
9656
|
function normalizeTriggerForDisplay(entry) {
|
|
9153
9657
|
if (entry === null || typeof entry !== "object") return null;
|
|
@@ -9229,6 +9733,225 @@ function readStagedPackages({ globalPiDir, fs = nodeFs2, readManifest: readManif
|
|
|
9229
9733
|
function nameAtVersion(pkg) {
|
|
9230
9734
|
return typeof pkg.version === "string" && pkg.version !== "" ? `${pkg.name}@${pkg.version}` : pkg.name;
|
|
9231
9735
|
}
|
|
9736
|
+
function cronRunStats({ records, schedulerIds } = {}) {
|
|
9737
|
+
const byId = {};
|
|
9738
|
+
if (!Array.isArray(records) || !Array.isArray(schedulerIds)) return { byId };
|
|
9739
|
+
for (const id of schedulerIds) {
|
|
9740
|
+
if (typeof id !== "string" || id === "") continue;
|
|
9741
|
+
const re = new RegExp(`^repeat:${escapeRegExp2(id)}:(\\d+)$`);
|
|
9742
|
+
let runs = 0;
|
|
9743
|
+
let lastMillis = -1;
|
|
9744
|
+
let last = null;
|
|
9745
|
+
for (const record of records) {
|
|
9746
|
+
const m = typeof record?.jobId === "string" ? re.exec(record.jobId) : null;
|
|
9747
|
+
if (!m) continue;
|
|
9748
|
+
runs++;
|
|
9749
|
+
const millis = Number(m[1]);
|
|
9750
|
+
if (millis > lastMillis) {
|
|
9751
|
+
lastMillis = millis;
|
|
9752
|
+
last = record;
|
|
9753
|
+
}
|
|
9754
|
+
}
|
|
9755
|
+
byId[id] = {
|
|
9756
|
+
runs,
|
|
9757
|
+
lastOutcome: last?.outcome ?? null,
|
|
9758
|
+
lastEndedAt: last ? last.endedAt ?? last.startedAt ?? null : null
|
|
9759
|
+
};
|
|
9760
|
+
}
|
|
9761
|
+
return { byId };
|
|
9762
|
+
}
|
|
9763
|
+
function joinRunsToTriggers({ records, triggerCount, triggerTypes } = {}) {
|
|
9764
|
+
const byIndex = {};
|
|
9765
|
+
let unattributed = 0;
|
|
9766
|
+
if (!Array.isArray(records)) return { byIndex, unattributed };
|
|
9767
|
+
const count = Number.isInteger(triggerCount) && triggerCount >= 0 ? triggerCount : 0;
|
|
9768
|
+
const types = triggerTypes && typeof triggerTypes === "object" ? triggerTypes : null;
|
|
9769
|
+
for (const record of records) {
|
|
9770
|
+
if (!isForgeKind(record?.kind)) continue;
|
|
9771
|
+
const idx = record?.triggerIndex;
|
|
9772
|
+
if (!Number.isInteger(idx) || idx < 0 || idx >= count) {
|
|
9773
|
+
unattributed++;
|
|
9774
|
+
continue;
|
|
9775
|
+
}
|
|
9776
|
+
if (types && (types[idx] === void 0 || types[idx] !== record.triggerType)) {
|
|
9777
|
+
unattributed++;
|
|
9778
|
+
continue;
|
|
9779
|
+
}
|
|
9780
|
+
const slot = byIndex[idx] ??= { runs: 0, lastOutcome: null, lastEndedAt: null };
|
|
9781
|
+
slot.runs++;
|
|
9782
|
+
const at = record.endedAt ?? record.startedAt ?? null;
|
|
9783
|
+
if (at !== null && (slot.lastEndedAt === null || at > slot.lastEndedAt)) {
|
|
9784
|
+
slot.lastEndedAt = at;
|
|
9785
|
+
slot.lastOutcome = record.outcome ?? null;
|
|
9786
|
+
}
|
|
9787
|
+
}
|
|
9788
|
+
return { byIndex, unattributed };
|
|
9789
|
+
}
|
|
9790
|
+
function observedChainEdges({ records } = {}) {
|
|
9791
|
+
const empty = { edges: [], refusals: {}, truncated: false };
|
|
9792
|
+
if (!Array.isArray(records)) return empty;
|
|
9793
|
+
const byJobId = /* @__PURE__ */ new Map();
|
|
9794
|
+
for (const record of records) {
|
|
9795
|
+
if (typeof record?.jobId === "string" && record.jobId !== "") byJobId.set(record.jobId, record);
|
|
9796
|
+
}
|
|
9797
|
+
const folded = /* @__PURE__ */ new Map();
|
|
9798
|
+
const refusals = {};
|
|
9799
|
+
let truncated = false;
|
|
9800
|
+
for (const child of records) {
|
|
9801
|
+
const parentId = child?.parentJobId;
|
|
9802
|
+
if (typeof parentId !== "string" || parentId === "") continue;
|
|
9803
|
+
const parent = byJobId.get(parentId);
|
|
9804
|
+
if (!parent) continue;
|
|
9805
|
+
if (typeof parent.flow !== "string" || typeof child.flow !== "string") continue;
|
|
9806
|
+
if (parent.target !== child.target) continue;
|
|
9807
|
+
const key = `${parent.flow} ${child.flow} ${parent.target ?? ""}`;
|
|
9808
|
+
let edge = folded.get(key);
|
|
9809
|
+
if (!edge) {
|
|
9810
|
+
if (folded.size >= GRAPH_LIMITS.maxEdges) {
|
|
9811
|
+
truncated = true;
|
|
9812
|
+
continue;
|
|
9813
|
+
}
|
|
9814
|
+
edge = { parentFlow: parent.flow, childFlow: child.flow, target: parent.target ?? null, count: 0, lastEndedAt: null };
|
|
9815
|
+
folded.set(key, edge);
|
|
9816
|
+
}
|
|
9817
|
+
edge.count++;
|
|
9818
|
+
const at = child.endedAt ?? child.startedAt ?? null;
|
|
9819
|
+
if (at !== null && (edge.lastEndedAt === null || at > edge.lastEndedAt)) edge.lastEndedAt = at;
|
|
9820
|
+
}
|
|
9821
|
+
for (const record of records) {
|
|
9822
|
+
if (Number.isInteger(record?.chainRefused) && record.chainRefused > 0 && typeof record?.flow === "string") {
|
|
9823
|
+
const scope = typeof record.target === "string" && record.target.startsWith("local:") ? `${record.target.slice("local:".length)}/${record.flow}` : record.flow;
|
|
9824
|
+
refusals[scope] = (refusals[scope] ?? 0) + record.chainRefused;
|
|
9825
|
+
}
|
|
9826
|
+
}
|
|
9827
|
+
return { edges: [...folded.values()], refusals, truncated };
|
|
9828
|
+
}
|
|
9829
|
+
function readFolderSkills({ folder, exec: exec4 = execFileSync2 } = {}) {
|
|
9830
|
+
const empty = { head: null, skills: [], truncated: false };
|
|
9831
|
+
if (typeof folder !== "string" || folder === "") return { ...empty, unreachable: "no-folder" };
|
|
9832
|
+
const head = revParseHead(folder, { exec: exec4 });
|
|
9833
|
+
if (head === null) return { ...empty, unreachable: "not-a-git-repo" };
|
|
9834
|
+
let listing;
|
|
9835
|
+
try {
|
|
9836
|
+
listing = exec4(
|
|
9837
|
+
"git",
|
|
9838
|
+
["-c", "core.hooksPath=/dev/null", "-c", "core.fsmonitor=false", "--no-pager", "-C", folder, "ls-tree", "-r", "-l", "-z", head, ".pi/"],
|
|
9839
|
+
{ encoding: "utf8", maxBuffer: GRAPH_LS_TREE_MAX_BYTES }
|
|
9840
|
+
);
|
|
9841
|
+
} catch {
|
|
9842
|
+
return { head, skills: [], truncated: false, unreachable: "ls-tree-failed" };
|
|
9843
|
+
}
|
|
9844
|
+
let kept;
|
|
9845
|
+
try {
|
|
9846
|
+
const selected = selectEntries(listing);
|
|
9847
|
+
kept = keepOnlyDeclaredSkills(selected.entries).kept;
|
|
9848
|
+
} catch {
|
|
9849
|
+
return { head, skills: [], truncated: false, unreachable: "listing-unparseable" };
|
|
9850
|
+
}
|
|
9851
|
+
const topLevel = /* @__PURE__ */ new Map();
|
|
9852
|
+
const subs = [];
|
|
9853
|
+
for (const entry of kept) {
|
|
9854
|
+
if (entry.skill === null || !entry.outRel.endsWith("/SKILL.md")) continue;
|
|
9855
|
+
if (entry.outRel === `pi/skills/${entry.skill}/SKILL.md`) {
|
|
9856
|
+
topLevel.set(entry.skill, entry);
|
|
9857
|
+
} else {
|
|
9858
|
+
subs.push({ name: entry.outRel.slice("pi/skills/".length, -"/SKILL.md".length), group: entry.skill });
|
|
9859
|
+
}
|
|
9860
|
+
}
|
|
9861
|
+
const names = [...topLevel.keys()].sort();
|
|
9862
|
+
const truncated = names.length > GRAPH_LIMITS.maxSkillsPerFolder;
|
|
9863
|
+
const keptNames = names.slice(0, GRAPH_LIMITS.maxSkillsPerFolder);
|
|
9864
|
+
const skills = [];
|
|
9865
|
+
let anyUnread = false;
|
|
9866
|
+
for (const name of keptNames) {
|
|
9867
|
+
const entry = topLevel.get(name);
|
|
9868
|
+
let text = null;
|
|
9869
|
+
try {
|
|
9870
|
+
const buf = exec4(
|
|
9871
|
+
"git",
|
|
9872
|
+
["-c", "core.hooksPath=/dev/null", "-c", "core.fsmonitor=false", "--no-pager", "-C", folder, "cat-file", "blob", entry.oid],
|
|
9873
|
+
{ maxBuffer: GRAPH_LIMITS.maxSkillBytes }
|
|
9874
|
+
);
|
|
9875
|
+
text = buf.toString("utf8");
|
|
9876
|
+
} catch {
|
|
9877
|
+
anyUnread = true;
|
|
9878
|
+
}
|
|
9879
|
+
skills.push({
|
|
9880
|
+
name,
|
|
9881
|
+
isSub: false,
|
|
9882
|
+
group: null,
|
|
9883
|
+
// Fail-closed like the gate: an unreadable SKILL.md reads as not-chainable, never as chainable.
|
|
9884
|
+
aiTrigger: text !== null && aiTriggerAllows(text),
|
|
9885
|
+
meta: text !== null ? parseSkillMeta(text) : null,
|
|
9886
|
+
mentions: text !== null ? findSiblingMentions(text.slice(0, GRAPH_LIMITS.maxMentionScanBytes), keptNames.filter((n) => n !== name)) : [],
|
|
9887
|
+
// The prose-loop hints (issue #54's grouped-inside-the-skill visual): scanned over the same
|
|
9888
|
+
// bounded window as the mentions, because both are text evidence of the same trust class.
|
|
9889
|
+
loops: text !== null ? findLoopHints(text.slice(0, GRAPH_LIMITS.maxMentionScanBytes)) : [],
|
|
9890
|
+
unread: text === null
|
|
9891
|
+
});
|
|
9892
|
+
}
|
|
9893
|
+
for (const sub of subs.sort((a, b) => a.name < b.name ? -1 : 1)) {
|
|
9894
|
+
skills.push({ name: sub.name, isSub: true, group: sub.group, aiTrigger: false, meta: null, mentions: [], unread: false });
|
|
9895
|
+
}
|
|
9896
|
+
return { head, skills, truncated: truncated || anyUnread, unreachable: null };
|
|
9897
|
+
}
|
|
9898
|
+
function readInjectedSkills({ skillsDir, fs = nodeFs2 } = {}) {
|
|
9899
|
+
if (typeof skillsDir !== "string" || skillsDir === "") return { skills: [], truncated: false, unreachable: null };
|
|
9900
|
+
let names;
|
|
9901
|
+
try {
|
|
9902
|
+
names = fs.readdirSync(skillsDir);
|
|
9903
|
+
} catch {
|
|
9904
|
+
return { skills: [], truncated: false, unreachable: "unreadable" };
|
|
9905
|
+
}
|
|
9906
|
+
const valid = names.filter((n) => SKILL_NAME_RE.test(n)).sort();
|
|
9907
|
+
const truncated = valid.length > GRAPH_LIMITS.maxSkillsPerFolder;
|
|
9908
|
+
const skills = [];
|
|
9909
|
+
for (const name of valid.slice(0, GRAPH_LIMITS.maxSkillsPerFolder)) {
|
|
9910
|
+
let aiTrigger = false;
|
|
9911
|
+
try {
|
|
9912
|
+
const text = fs.readFileSync(join2(skillsDir, name, "SKILL.md"), "utf8");
|
|
9913
|
+
aiTrigger = aiTriggerAllows(text);
|
|
9914
|
+
} catch {
|
|
9915
|
+
continue;
|
|
9916
|
+
}
|
|
9917
|
+
skills.push({ name, aiTrigger });
|
|
9918
|
+
}
|
|
9919
|
+
return { skills, truncated, unreachable: null };
|
|
9920
|
+
}
|
|
9921
|
+
function collectGraphInputs({ triggers, readFolder = readFolderSkills, readInjected = readInjectedSkills } = {}) {
|
|
9922
|
+
const out = { folderSkills: {}, injectedSkills: {}, foldersTruncated: false };
|
|
9923
|
+
if (!Array.isArray(triggers)) return out;
|
|
9924
|
+
const folders = [];
|
|
9925
|
+
const injectedDirs = [];
|
|
9926
|
+
for (const t of triggers) {
|
|
9927
|
+
if (t?.type === "cron" && typeof t.folder === "string" && t.folder !== "" && !folders.includes(t.folder)) folders.push(t.folder);
|
|
9928
|
+
if (typeof t?.skillsDir === "string" && t.skillsDir !== "" && !injectedDirs.includes(t.skillsDir)) injectedDirs.push(t.skillsDir);
|
|
9929
|
+
}
|
|
9930
|
+
out.foldersTruncated = folders.length > GRAPH_LIMITS.maxFoldersScanned;
|
|
9931
|
+
for (const folder of folders.slice(0, GRAPH_LIMITS.maxFoldersScanned)) {
|
|
9932
|
+
out.folderSkills[folder] = readFolder({ folder });
|
|
9933
|
+
}
|
|
9934
|
+
for (const dir of injectedDirs.slice(0, GRAPH_LIMITS.maxFoldersScanned)) {
|
|
9935
|
+
out.injectedSkills[dir] = readInjected({ skillsDir: dir });
|
|
9936
|
+
}
|
|
9937
|
+
return out;
|
|
9938
|
+
}
|
|
9939
|
+
function forgeRepoTargets({ records } = {}) {
|
|
9940
|
+
const byKind = {};
|
|
9941
|
+
if (!Array.isArray(records)) return byKind;
|
|
9942
|
+
for (const record of records) {
|
|
9943
|
+
if (!isForgeKind(record?.kind) || typeof record?.target !== "string") continue;
|
|
9944
|
+
const repo = record.target.replace(/[#!]\d+$/, "");
|
|
9945
|
+
if (repo === "" || repo === record.target) continue;
|
|
9946
|
+
(byKind[record.kind] ??= /* @__PURE__ */ new Set()).add(repo);
|
|
9947
|
+
}
|
|
9948
|
+
return Object.fromEntries(
|
|
9949
|
+
Object.entries(byKind).map(([kind, set]) => [kind, [...set].sort().slice(0, GRAPH_LIMITS.maxReposListed)])
|
|
9950
|
+
);
|
|
9951
|
+
}
|
|
9952
|
+
function escapeRegExp2(s) {
|
|
9953
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
9954
|
+
}
|
|
9232
9955
|
function listRunIds({ logsDir, fs = nodeFs2 }) {
|
|
9233
9956
|
let names;
|
|
9234
9957
|
try {
|
|
@@ -9267,7 +9990,7 @@ function clampLines(lines) {
|
|
|
9267
9990
|
const n = Number.isFinite(lines) ? Math.floor(lines) : 200;
|
|
9268
9991
|
return Math.min(2e3, Math.max(1, n));
|
|
9269
9992
|
}
|
|
9270
|
-
var DISPATCH_RUN_TTL_SECONDS, SCAN_WINDOW_MAX_DAYS;
|
|
9993
|
+
var DISPATCH_RUN_TTL_SECONDS, SCAN_WINDOW_MAX_DAYS, GRAPH_LIMITS, GRAPH_LS_TREE_MAX_BYTES;
|
|
9271
9994
|
var init_read_model = __esm({
|
|
9272
9995
|
"src/read-model.mjs"() {
|
|
9273
9996
|
init_config2();
|
|
@@ -9282,8 +10005,1058 @@ var init_read_model = __esm({
|
|
|
9282
10005
|
init_flow_gate();
|
|
9283
10006
|
init_git_dirty();
|
|
9284
10007
|
init_packages();
|
|
10008
|
+
init_materialize();
|
|
10009
|
+
init_forges();
|
|
10010
|
+
init_graph_model();
|
|
9285
10011
|
DISPATCH_RUN_TTL_SECONDS = 2 * 60 * 60;
|
|
9286
10012
|
SCAN_WINDOW_MAX_DAYS = 92;
|
|
10013
|
+
GRAPH_LIMITS = Object.freeze({
|
|
10014
|
+
maxFoldersScanned: 16,
|
|
10015
|
+
// distinct cron folders enumerated per graph build; git spawns are the cost
|
|
10016
|
+
maxSkillsPerFolder: 64,
|
|
10017
|
+
// cat-file spawns per folder; mirrors PI_LIMITS.maxFilesPerSkill's altitude
|
|
10018
|
+
maxSkillBytes: 64 * 1024,
|
|
10019
|
+
// one SKILL.md read cap; frontmatter + prose, never a dataset
|
|
10020
|
+
maxMentionScanBytes: 32 * 1024,
|
|
10021
|
+
// sibling-name scan window into each SKILL.md
|
|
10022
|
+
maxEdges: 200,
|
|
10023
|
+
// distinct observed chain edges kept; beyond this the graph says "truncated"
|
|
10024
|
+
windowDays: 30,
|
|
10025
|
+
// run-record window the graph folds; matches the default PI_LOG_RETENTION_DAYS
|
|
10026
|
+
maxReposListed: 5
|
|
10027
|
+
// repos named per forge group, from record targets; a scope label, not an inventory
|
|
10028
|
+
});
|
|
10029
|
+
GRAPH_LS_TREE_MAX_BYTES = 1 << 20;
|
|
10030
|
+
}
|
|
10031
|
+
});
|
|
10032
|
+
|
|
10033
|
+
// src/graph-html.mjs
|
|
10034
|
+
function escapeHtml(s) {
|
|
10035
|
+
return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
10036
|
+
}
|
|
10037
|
+
function embedJson(value) {
|
|
10038
|
+
return JSON.stringify(value).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
10039
|
+
}
|
|
10040
|
+
function fmt(v) {
|
|
10041
|
+
const n = Number.isFinite(v) ? Math.round(v * 10) / 10 : 0;
|
|
10042
|
+
return String(n);
|
|
10043
|
+
}
|
|
10044
|
+
function finOr(v, fallback) {
|
|
10045
|
+
return Number.isFinite(v) ? v : fallback;
|
|
10046
|
+
}
|
|
10047
|
+
function intOr(v, fallback) {
|
|
10048
|
+
return Number.isInteger(v) ? v : fallback;
|
|
10049
|
+
}
|
|
10050
|
+
function strOr(v, fallback) {
|
|
10051
|
+
return typeof v === "string" && v !== "" ? v : fallback;
|
|
10052
|
+
}
|
|
10053
|
+
function clip(s, max) {
|
|
10054
|
+
const t = String(s);
|
|
10055
|
+
return t.length > max ? `${t.slice(0, max)}\u2026` : t;
|
|
10056
|
+
}
|
|
10057
|
+
function normalizeModel(model) {
|
|
10058
|
+
const ok = model !== null && typeof model === "object";
|
|
10059
|
+
const m = ok ? model : {};
|
|
10060
|
+
const caps = {
|
|
10061
|
+
chainDepthMax: intOr(m.caps?.chainDepthMax, null),
|
|
10062
|
+
chainMaxPerJob: intOr(m.caps?.chainMaxPerJob, null),
|
|
10063
|
+
sameFolderOnly: m.caps?.sameFolderOnly !== false,
|
|
10064
|
+
windowDays: intOr(m.caps?.windowDays, null)
|
|
10065
|
+
};
|
|
10066
|
+
const meta = {
|
|
10067
|
+
generatedAt: finOr(m.meta?.generatedAt, null),
|
|
10068
|
+
triggersMissing: m.meta?.triggersMissing === true,
|
|
10069
|
+
triggersInvalid: typeof m.meta?.triggersInvalid === "string" ? m.meta.triggersInvalid : null,
|
|
10070
|
+
unattributedRuns: intOr(m.meta?.unattributedRuns, 0),
|
|
10071
|
+
droppedObservedEdges: intOr(m.meta?.droppedObservedEdges, 0),
|
|
10072
|
+
truncated: {
|
|
10073
|
+
folders: m.meta?.truncated?.folders === true,
|
|
10074
|
+
skills: m.meta?.truncated?.skills === true,
|
|
10075
|
+
edges: m.meta?.truncated?.edges === true
|
|
10076
|
+
}
|
|
10077
|
+
};
|
|
10078
|
+
const folders = [];
|
|
10079
|
+
for (const f of Array.isArray(m.folders) ? m.folders : []) {
|
|
10080
|
+
if (!f || typeof f !== "object" || typeof f.key !== "string") continue;
|
|
10081
|
+
folders.push({
|
|
10082
|
+
key: f.key,
|
|
10083
|
+
label: strOr(f.label, "(folder)"),
|
|
10084
|
+
// The absolute host path rides the model but is RENDERED only under { fullPaths: true } (an
|
|
10085
|
+
// operator opt-in); the default page stays basename-only, so a shared screenshot or artifact
|
|
10086
|
+
// cannot leak home-directory names -- the canary test pins the default.
|
|
10087
|
+
path: typeof f.path === "string" ? f.path : null,
|
|
10088
|
+
kind: f.kind === "forge" ? "forge" : "local",
|
|
10089
|
+
head: typeof f.head === "string" ? f.head : null,
|
|
10090
|
+
unreachable: typeof f.unreachable === "string" ? f.unreachable : null,
|
|
10091
|
+
// Record-derived scope for forge groups ("which repos is this group even about"); already
|
|
10092
|
+
// id-only repo#n-style strings upstream, clipped and capped again here anyway.
|
|
10093
|
+
repos: Array.isArray(f.repos) ? f.repos.slice(0, 5).filter((r) => typeof r === "string").map((r) => clip(r, 60)) : []
|
|
10094
|
+
});
|
|
10095
|
+
}
|
|
10096
|
+
folders.sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
|
|
10097
|
+
const nodes = [];
|
|
10098
|
+
const seen = /* @__PURE__ */ new Set();
|
|
10099
|
+
for (const n of Array.isArray(m.nodes) ? m.nodes : []) {
|
|
10100
|
+
if (!n || typeof n !== "object" || typeof n.id !== "string" || seen.has(n.id)) continue;
|
|
10101
|
+
seen.add(n.id);
|
|
10102
|
+
nodes.push({
|
|
10103
|
+
id: n.id,
|
|
10104
|
+
kind: typeof n.kind === "string" ? n.kind : "skill",
|
|
10105
|
+
name: typeof n.name === "string" ? n.name : null,
|
|
10106
|
+
label: typeof n.label === "string" ? n.label : null,
|
|
10107
|
+
onType: typeof n.onType === "string" ? n.onType : null,
|
|
10108
|
+
pattern: typeof n.pattern === "string" ? n.pattern : null,
|
|
10109
|
+
flow: typeof n.flow === "string" ? n.flow : null,
|
|
10110
|
+
replicas: intOr(n.replicas, null),
|
|
10111
|
+
folderKey: typeof n.folderKey === "string" ? n.folderKey : null,
|
|
10112
|
+
runs: intOr(n.runs, 0),
|
|
10113
|
+
lastOutcome: typeof n.lastOutcome === "string" ? n.lastOutcome : null,
|
|
10114
|
+
lastEndedAt: typeof n.lastEndedAt === "string" || Number.isFinite(n.lastEndedAt) ? n.lastEndedAt : null,
|
|
10115
|
+
isSub: n.isSub === true,
|
|
10116
|
+
group: typeof n.group === "string" ? n.group : null,
|
|
10117
|
+
// Prose-loop hints from the SKILL.md body; capped and clipped so a hostile skill cannot
|
|
10118
|
+
// inflate its own group box into a page-filling banner.
|
|
10119
|
+
loops: Array.isArray(n.loops) ? n.loops.slice(0, 3).flatMap((l) => typeof l?.hint === "string" && l.hint !== "" ? [{ hint: clip(l.hint, 80) }] : []) : [],
|
|
10120
|
+
aiTrigger: n.aiTrigger === true,
|
|
10121
|
+
unread: n.unread === true,
|
|
10122
|
+
description: typeof n.meta?.description === "string" ? clip(n.meta.description, 120) : null
|
|
10123
|
+
});
|
|
10124
|
+
}
|
|
10125
|
+
nodes.sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
10126
|
+
const edges = [];
|
|
10127
|
+
for (const e of Array.isArray(m.edges) ? m.edges : []) {
|
|
10128
|
+
if (!e || typeof e !== "object") continue;
|
|
10129
|
+
if (typeof e.from !== "string" || typeof e.to !== "string") continue;
|
|
10130
|
+
if (!GRAPH_HTML_KINDS.includes(e.kind)) continue;
|
|
10131
|
+
edges.push({
|
|
10132
|
+
from: e.from,
|
|
10133
|
+
to: e.to,
|
|
10134
|
+
kind: e.kind,
|
|
10135
|
+
count: intOr(e.count, null),
|
|
10136
|
+
strong: e.strong === true,
|
|
10137
|
+
eligible: e.eligible === true,
|
|
10138
|
+
label: typeof e.label === "string" ? e.label : null
|
|
10139
|
+
});
|
|
10140
|
+
}
|
|
10141
|
+
edges.sort((a, b) => cmpStr(a.kind, b.kind) || cmpStr(a.from, b.from) || cmpStr(a.to, b.to) || cmpStr(a.label ?? "", b.label ?? "") || (a.count ?? -1) - (b.count ?? -1) || (a.strong ? 1 : 0) - (b.strong ? 1 : 0));
|
|
10142
|
+
const flags = [];
|
|
10143
|
+
for (const f of Array.isArray(m.flags) ? m.flags : []) {
|
|
10144
|
+
if (!f || typeof f !== "object" || typeof f.nodeId !== "string" || typeof f.flag !== "string") continue;
|
|
10145
|
+
flags.push({ nodeId: f.nodeId, flag: f.flag, detail: typeof f.detail === "string" ? clip(f.detail, 160) : null });
|
|
10146
|
+
}
|
|
10147
|
+
flags.sort((a, b) => cmpStr(a.nodeId, b.nodeId) || cmpStr(a.flag, b.flag) || cmpStr(a.detail ?? "", b.detail ?? ""));
|
|
10148
|
+
return { ok, folders, nodes, edges, flags, caps, meta };
|
|
10149
|
+
}
|
|
10150
|
+
function cmpStr(a, b) {
|
|
10151
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
10152
|
+
}
|
|
10153
|
+
function chipLabel(n) {
|
|
10154
|
+
if (n.kind === "trigger") return clip(n.label ?? n.onType ?? "trigger", CHIP_MAX_CHARS);
|
|
10155
|
+
return clip(n.name ?? "(unnamed)", CHIP_MAX_CHARS);
|
|
10156
|
+
}
|
|
10157
|
+
function chipWidth(label) {
|
|
10158
|
+
const raw = LABEL_X + label.length * CHAR_W + 12;
|
|
10159
|
+
const snapped = Math.ceil(raw / GRID) * GRID;
|
|
10160
|
+
return Math.min(NODE_MAX_W, Math.max(NODE_MIN_W, snapped));
|
|
10161
|
+
}
|
|
10162
|
+
function layoutNormalized(norm) {
|
|
10163
|
+
const empty = { nodes: [], wires: [], groups: [], skillGroups: [], viewBox: { x: 0, y: 0, w: 800, h: 600 } };
|
|
10164
|
+
if (!norm.ok && norm.nodes.length === 0) return empty;
|
|
10165
|
+
const groups = [];
|
|
10166
|
+
const groupIndexByKey = /* @__PURE__ */ new Map();
|
|
10167
|
+
const addGroup = (key, label, kind, head, unreachable, path, repos) => {
|
|
10168
|
+
const g = { id: `g${groups.length}`, label, kind, head, unreachable, path: path ?? null, repos: repos ?? [], members: [], x: 0, y: 0, w: 0, h: 0 };
|
|
10169
|
+
groups.push(g);
|
|
10170
|
+
groupIndexByKey.set(key, g);
|
|
10171
|
+
return g;
|
|
10172
|
+
};
|
|
10173
|
+
for (const f of norm.folders) addGroup(f.key, f.label, f.kind, f.head, f.unreachable, f.path, f.repos);
|
|
10174
|
+
const placed = [];
|
|
10175
|
+
const placedByOrig = /* @__PURE__ */ new Map();
|
|
10176
|
+
for (const n of norm.nodes) {
|
|
10177
|
+
const label = chipLabel(n);
|
|
10178
|
+
const p = { id: `n${placed.length}`, kind: n.kind, label, x: 0, y: 0, w: chipWidth(label), h: NODE_H, groupId: null, node: n };
|
|
10179
|
+
placed.push(p);
|
|
10180
|
+
placedByOrig.set(n.id, p);
|
|
10181
|
+
let g = n.folderKey === null ? null : groupIndexByKey.get(n.folderKey) ?? null;
|
|
10182
|
+
if (!g && n.kind === "injected") g = groupIndexByKey.get("~injected") ?? addGroup("~injected", "injected skills (run.skillsDir)", "local", null, null);
|
|
10183
|
+
if (!g) g = groupIndexByKey.get(n.folderKey ?? "~ungrouped") ?? addGroup(n.folderKey ?? "~ungrouped", "ungrouped", "local", null, null);
|
|
10184
|
+
g.members.push(p);
|
|
10185
|
+
p.groupId = g.id;
|
|
10186
|
+
}
|
|
10187
|
+
const wires = [];
|
|
10188
|
+
for (const e of norm.edges) {
|
|
10189
|
+
const f = placedByOrig.get(e.from);
|
|
10190
|
+
const t = placedByOrig.get(e.to);
|
|
10191
|
+
if (!f || !t) continue;
|
|
10192
|
+
wires.push({ id: `w${wires.length}`, kind: e.kind, from: f.id, to: t.id, self: f === t, back: false, edge: e, f, t, d: "", labelX: 0, labelY: 0 });
|
|
10193
|
+
}
|
|
10194
|
+
markBackEdges(placed, wires);
|
|
10195
|
+
const skillGroups = [];
|
|
10196
|
+
let groupY = 0;
|
|
10197
|
+
let maxGroupW = 0;
|
|
10198
|
+
for (const g of groups) {
|
|
10199
|
+
const sgStart = skillGroups.length;
|
|
10200
|
+
const size = layoutGroup(g, wires, skillGroups);
|
|
10201
|
+
g.x = 0;
|
|
10202
|
+
g.y = groupY;
|
|
10203
|
+
g.w = size.w;
|
|
10204
|
+
g.h = size.h;
|
|
10205
|
+
for (const p of g.members) {
|
|
10206
|
+
p.x += g.x;
|
|
10207
|
+
p.y += g.y;
|
|
10208
|
+
}
|
|
10209
|
+
for (let i = sgStart; i < skillGroups.length; i++) {
|
|
10210
|
+
const sg = skillGroups[i];
|
|
10211
|
+
sg.x += g.x;
|
|
10212
|
+
sg.y += g.y;
|
|
10213
|
+
for (const m of sg.markers) {
|
|
10214
|
+
m.x += g.x;
|
|
10215
|
+
m.y += g.y;
|
|
10216
|
+
m.hintX += g.x;
|
|
10217
|
+
m.hintY += g.y;
|
|
10218
|
+
}
|
|
10219
|
+
sg.points = sg.points.map(([px, py]) => [px + g.x, py + g.y]);
|
|
10220
|
+
sg.d = sg.points.map(([px, py], j) => `${j === 0 ? "M" : "L"} ${fmt(px)} ${fmt(py)}`).join(" ");
|
|
10221
|
+
}
|
|
10222
|
+
groupY += g.h + GROUP_GAP;
|
|
10223
|
+
if (g.w > maxGroupW) maxGroupW = g.w;
|
|
10224
|
+
}
|
|
10225
|
+
const parallelCount = /* @__PURE__ */ new Map();
|
|
10226
|
+
for (const w of wires) {
|
|
10227
|
+
const key = `${w.from} ${w.to}`;
|
|
10228
|
+
w.parallel = parallelCount.get(key) ?? 0;
|
|
10229
|
+
parallelCount.set(key, w.parallel + 1);
|
|
10230
|
+
}
|
|
10231
|
+
for (const w of wires) routeWire(w);
|
|
10232
|
+
for (const w of wires) {
|
|
10233
|
+
delete w.f;
|
|
10234
|
+
delete w.t;
|
|
10235
|
+
}
|
|
10236
|
+
const totalH = groups.length > 0 ? groupY - GROUP_GAP : 0;
|
|
10237
|
+
const viewBox = groups.length > 0 ? { x: -VIEW_MARGIN, y: -VIEW_MARGIN, w: maxGroupW + VIEW_MARGIN * 2, h: totalH + VIEW_MARGIN * 2 } : { x: 0, y: 0, w: 800, h: 600 };
|
|
10238
|
+
return { nodes: placed, wires, groups, skillGroups, viewBox };
|
|
10239
|
+
}
|
|
10240
|
+
function markBackEdges(placed, wires) {
|
|
10241
|
+
const out = /* @__PURE__ */ new Map();
|
|
10242
|
+
for (const w of wires) {
|
|
10243
|
+
if (w.self) continue;
|
|
10244
|
+
if (!out.has(w.from)) out.set(w.from, []);
|
|
10245
|
+
out.get(w.from).push(w);
|
|
10246
|
+
}
|
|
10247
|
+
for (const list of out.values()) list.sort((a, b) => cmpStr(a.to, b.to) || cmpStr(a.id, b.id));
|
|
10248
|
+
const state = /* @__PURE__ */ new Map();
|
|
10249
|
+
for (const p of placed) {
|
|
10250
|
+
if (state.has(p.id)) continue;
|
|
10251
|
+
const stack = [{ id: p.id, i: 0 }];
|
|
10252
|
+
state.set(p.id, 1);
|
|
10253
|
+
while (stack.length > 0) {
|
|
10254
|
+
const top = stack[stack.length - 1];
|
|
10255
|
+
const edges = out.get(top.id) ?? [];
|
|
10256
|
+
if (top.i >= edges.length) {
|
|
10257
|
+
state.set(top.id, 2);
|
|
10258
|
+
stack.pop();
|
|
10259
|
+
continue;
|
|
10260
|
+
}
|
|
10261
|
+
const w = edges[top.i++];
|
|
10262
|
+
const s = state.get(w.to);
|
|
10263
|
+
if (s === 1) w.back = true;
|
|
10264
|
+
else if (s !== 2) {
|
|
10265
|
+
state.set(w.to, 1);
|
|
10266
|
+
stack.push({ id: w.to, i: 0 });
|
|
10267
|
+
}
|
|
10268
|
+
}
|
|
10269
|
+
}
|
|
10270
|
+
}
|
|
10271
|
+
function layoutGroup(g, allWires, sgOut) {
|
|
10272
|
+
const members = g.members;
|
|
10273
|
+
if (members.length === 0) return { w: GROUP_PAD_L + NODE_MAX_W + GROUP_PAD_R, h: GROUP_PAD_T + GROUP_PAD_B };
|
|
10274
|
+
const subsByParent = /* @__PURE__ */ new Map();
|
|
10275
|
+
const grid = [];
|
|
10276
|
+
for (const p of members) {
|
|
10277
|
+
if (p.node.isSub && typeof p.node.group === "string") {
|
|
10278
|
+
if (!subsByParent.has(p.node.group)) subsByParent.set(p.node.group, []);
|
|
10279
|
+
subsByParent.get(p.node.group).push(p);
|
|
10280
|
+
} else {
|
|
10281
|
+
grid.push(p);
|
|
10282
|
+
}
|
|
10283
|
+
}
|
|
10284
|
+
const parentByName = /* @__PURE__ */ new Map();
|
|
10285
|
+
for (const p of grid) {
|
|
10286
|
+
if (p.kind === "skill" && p.node.name !== null && !parentByName.has(p.node.name)) parentByName.set(p.node.name, p);
|
|
10287
|
+
}
|
|
10288
|
+
for (const [name, subs] of subsByParent) {
|
|
10289
|
+
if (!parentByName.has(name)) for (const s of subs) grid.push(s);
|
|
10290
|
+
}
|
|
10291
|
+
const supers = /* @__PURE__ */ new Map();
|
|
10292
|
+
for (const p of grid) {
|
|
10293
|
+
if (p.kind !== "skill") continue;
|
|
10294
|
+
const subs = parentByName.get(p.node.name) === p ? subsByParent.get(p.node.name) ?? [] : [];
|
|
10295
|
+
if (p.node.loops.length === 0 && subs.length === 0) continue;
|
|
10296
|
+
supers.set(p.id, computeSkillGroup(p, p.node.loops, subs));
|
|
10297
|
+
}
|
|
10298
|
+
const effW = (p) => supers.has(p.id) ? supers.get(p.id).w : p.w;
|
|
10299
|
+
const effH = (p) => supers.has(p.id) ? supers.get(p.id).h : p.h;
|
|
10300
|
+
const gridSet = new Set(grid.map((p) => p.id));
|
|
10301
|
+
const rankEdges = allWires.filter((w) => !w.self && !w.back && gridSet.has(w.from) && gridSet.has(w.to));
|
|
10302
|
+
const rank = /* @__PURE__ */ new Map();
|
|
10303
|
+
for (const p of grid) rank.set(p.id, p.kind === "trigger" ? 0 : 1);
|
|
10304
|
+
for (let i = 0; i < grid.length; i++) {
|
|
10305
|
+
let changed = false;
|
|
10306
|
+
for (const w of rankEdges) {
|
|
10307
|
+
if (w.t.kind === "trigger") continue;
|
|
10308
|
+
const want = rank.get(w.from) + 1;
|
|
10309
|
+
if (rank.get(w.to) < want) {
|
|
10310
|
+
rank.set(w.to, want);
|
|
10311
|
+
changed = true;
|
|
10312
|
+
}
|
|
10313
|
+
}
|
|
10314
|
+
if (!changed) break;
|
|
10315
|
+
}
|
|
10316
|
+
const maxRank = Math.max(...[...rank.values()]);
|
|
10317
|
+
const rows = [];
|
|
10318
|
+
for (let r = 0; r <= maxRank; r++) rows.push([]);
|
|
10319
|
+
for (const p of grid) rows[rank.get(p.id)].push(p);
|
|
10320
|
+
for (const row of rows) row.sort((a, b) => cmpStr(a.label, b.label) || cmpStr(a.id, b.id));
|
|
10321
|
+
orderRows(rows, rankEdges);
|
|
10322
|
+
const rankMaxW = [];
|
|
10323
|
+
for (let r = 0; r <= maxRank; r++) rankMaxW.push(Math.max(NODE_MAX_W, ...rows[r].map(effW)));
|
|
10324
|
+
const colX = [GROUP_PAD_L];
|
|
10325
|
+
for (let r = 1; r <= maxRank; r++) colX.push(colX[r - 1] + Math.max(RANK_PITCH, rankMaxW[r - 1] + 20));
|
|
10326
|
+
const underIds = /* @__PURE__ */ new Set();
|
|
10327
|
+
for (const w of allWires) {
|
|
10328
|
+
if (w.self && gridSet.has(w.from)) underIds.add(w.from);
|
|
10329
|
+
if (w.back) {
|
|
10330
|
+
if (gridSet.has(w.from)) underIds.add(w.from);
|
|
10331
|
+
if (gridSet.has(w.to)) underIds.add(w.to);
|
|
10332
|
+
}
|
|
10333
|
+
}
|
|
10334
|
+
const maxRows = Math.max(...rows.map((row) => row.length));
|
|
10335
|
+
const rowY = [];
|
|
10336
|
+
let y = GROUP_PAD_T;
|
|
10337
|
+
for (let i = 0; i < maxRows; i++) {
|
|
10338
|
+
rowY.push(y);
|
|
10339
|
+
const band = rows.flatMap((row) => row.length > i ? [row[i]] : []);
|
|
10340
|
+
const bandH = Math.max(NODE_H, ...band.map(effH));
|
|
10341
|
+
const under = band.some((p) => underIds.has(p.id));
|
|
10342
|
+
y += bandH + (under ? UNDER_ROUTE_EXTRA : 0) + ROW_GAP;
|
|
10343
|
+
}
|
|
10344
|
+
for (let r = 0; r <= maxRank; r++) {
|
|
10345
|
+
for (let i = 0; i < rows[r].length; i++) {
|
|
10346
|
+
const p = rows[r][i];
|
|
10347
|
+
if (supers.has(p.id)) {
|
|
10348
|
+
placeSkillGroup(g, p, supers.get(p.id), colX[r], rowY[i], sgOut);
|
|
10349
|
+
} else {
|
|
10350
|
+
p.x = colX[r];
|
|
10351
|
+
p.y = rowY[i];
|
|
10352
|
+
}
|
|
10353
|
+
}
|
|
10354
|
+
}
|
|
10355
|
+
return {
|
|
10356
|
+
w: colX[maxRank] + rankMaxW[maxRank] + GROUP_PAD_R,
|
|
10357
|
+
h: y - ROW_GAP + GROUP_PAD_B
|
|
10358
|
+
};
|
|
10359
|
+
}
|
|
10360
|
+
function computeSkillGroup(p, loops, subs) {
|
|
10361
|
+
let cy = SG_CHIP_Y + NODE_H + 14;
|
|
10362
|
+
const markers = [];
|
|
10363
|
+
for (const l of loops.slice(0, 3)) {
|
|
10364
|
+
markers.push({ x: SG_CHIP_X, y: cy, w: SG_MARKER, h: SG_MARKER, hint: clip(l.hint, SG_HINT_CHARS), hintX: SG_CHIP_X + SG_MARKER + 6, hintY: cy + 24 });
|
|
10365
|
+
cy += SG_MARKER + 8;
|
|
10366
|
+
}
|
|
10367
|
+
const subPlaced = [];
|
|
10368
|
+
for (const s of subs) {
|
|
10369
|
+
const label = clip(s.node.name ?? "(sub)", 16);
|
|
10370
|
+
s.label = label;
|
|
10371
|
+
s.w = Math.min(140, Math.max(80, 12 + label.length * 7));
|
|
10372
|
+
s.h = SUB_CHIP_H;
|
|
10373
|
+
s.nested = true;
|
|
10374
|
+
subPlaced.push({ p: s, x: SG_CHIP_X, y: cy });
|
|
10375
|
+
cy += SUB_CHIP_H + 6;
|
|
10376
|
+
}
|
|
10377
|
+
const wCandidates = [SG_CHIP_X + p.w + 18];
|
|
10378
|
+
if (markers.length > 0) wCandidates.push(SG_CHIP_X + SG_MARKER + 6 + SG_HINT_CHARS * SG_SMALL_CHAR_W + 14);
|
|
10379
|
+
for (const s of subPlaced) wCandidates.push(SG_CHIP_X + s.p.w + 18);
|
|
10380
|
+
const w = Math.max(...wCandidates);
|
|
10381
|
+
const h = cy + 8;
|
|
10382
|
+
const py = SG_CHIP_Y + NODE_H / 2;
|
|
10383
|
+
const points = [
|
|
10384
|
+
[SG_CHIP_X + p.w, py],
|
|
10385
|
+
[w - SG_RING, py],
|
|
10386
|
+
[w - SG_RING, h - SG_RING],
|
|
10387
|
+
[SG_RING, h - SG_RING],
|
|
10388
|
+
[SG_RING, py],
|
|
10389
|
+
[SG_CHIP_X, py]
|
|
10390
|
+
];
|
|
10391
|
+
return { w, h, markers, subPlaced, points };
|
|
10392
|
+
}
|
|
10393
|
+
function placeSkillGroup(g, p, box2, x, y, sgOut) {
|
|
10394
|
+
p.x = x + SG_CHIP_X;
|
|
10395
|
+
p.y = y + SG_CHIP_Y;
|
|
10396
|
+
const markers = box2.markers.map((m) => ({ ...m, x: m.x + x, y: m.y + y, hintX: m.hintX + x, hintY: m.hintY + y }));
|
|
10397
|
+
for (const s of box2.subPlaced) {
|
|
10398
|
+
s.p.x = x + s.x;
|
|
10399
|
+
s.p.y = y + s.y;
|
|
10400
|
+
}
|
|
10401
|
+
sgOut.push({
|
|
10402
|
+
id: `sg${sgOut.length}`,
|
|
10403
|
+
groupId: g.id,
|
|
10404
|
+
nodeId: p.id,
|
|
10405
|
+
label: p.node.name ?? p.label,
|
|
10406
|
+
x,
|
|
10407
|
+
y,
|
|
10408
|
+
w: box2.w,
|
|
10409
|
+
h: box2.h,
|
|
10410
|
+
markers,
|
|
10411
|
+
subIds: box2.subPlaced.map((s) => s.p.id),
|
|
10412
|
+
points: box2.points.map(([px, py]) => [px + x, py + y]),
|
|
10413
|
+
d: ""
|
|
10414
|
+
});
|
|
10415
|
+
}
|
|
10416
|
+
function orderRows(rows, rankEdges) {
|
|
10417
|
+
const pos = /* @__PURE__ */ new Map();
|
|
10418
|
+
const setPos = (row) => row.forEach((p, i) => pos.set(p.id, i));
|
|
10419
|
+
for (const row of rows) setPos(row);
|
|
10420
|
+
const preds = /* @__PURE__ */ new Map();
|
|
10421
|
+
const succs = /* @__PURE__ */ new Map();
|
|
10422
|
+
for (const w of rankEdges) {
|
|
10423
|
+
if (!preds.has(w.to)) preds.set(w.to, []);
|
|
10424
|
+
preds.get(w.to).push(w.from);
|
|
10425
|
+
if (!succs.has(w.from)) succs.set(w.from, []);
|
|
10426
|
+
succs.get(w.from).push(w.to);
|
|
10427
|
+
}
|
|
10428
|
+
const median = (ids) => {
|
|
10429
|
+
const xs = (ids ?? []).map((id) => pos.get(id)).filter((v) => Number.isInteger(v)).sort((a, b) => a - b);
|
|
10430
|
+
return xs.length === 0 ? null : xs[xs.length - 1 >> 1];
|
|
10431
|
+
};
|
|
10432
|
+
const sweep = (nbrs, indices) => {
|
|
10433
|
+
for (const r of indices) {
|
|
10434
|
+
const keyed = rows[r].map((p, i) => ({ p, key: median(nbrs.get(p.id)) ?? i }));
|
|
10435
|
+
keyed.sort((a, b) => a.key - b.key || cmpStr(a.p.label, b.p.label) || cmpStr(a.p.id, b.p.id));
|
|
10436
|
+
rows[r] = keyed.map((k) => k.p);
|
|
10437
|
+
setPos(rows[r]);
|
|
10438
|
+
}
|
|
10439
|
+
};
|
|
10440
|
+
const down = [];
|
|
10441
|
+
const up = [];
|
|
10442
|
+
for (let r = 1; r < rows.length; r++) down.push(r);
|
|
10443
|
+
for (let r = rows.length - 2; r >= 0; r--) up.push(r);
|
|
10444
|
+
sweep(preds, down);
|
|
10445
|
+
sweep(succs, up);
|
|
10446
|
+
}
|
|
10447
|
+
function routeWire(w) {
|
|
10448
|
+
const f = w.f;
|
|
10449
|
+
const t = w.t;
|
|
10450
|
+
const y1 = f.y + NODE_H / 2;
|
|
10451
|
+
const y2 = t.y + NODE_H / 2;
|
|
10452
|
+
const LOOP_OFF = 18;
|
|
10453
|
+
if (w.self) {
|
|
10454
|
+
const x12 = f.x + f.w;
|
|
10455
|
+
const x22 = f.x;
|
|
10456
|
+
const yb = f.y + NODE_H + SELF_LOOP_DROP + w.parallel * 12;
|
|
10457
|
+
w.d = `M ${fmt(x12)} ${fmt(y1)} C ${fmt(x12 + LOOP_OFF)} ${fmt(y1)}, ${fmt(x12 + LOOP_OFF)} ${fmt(yb)}, ${fmt(x12)} ${fmt(yb)} L ${fmt(x22)} ${fmt(yb)} C ${fmt(x22 - LOOP_OFF)} ${fmt(yb)}, ${fmt(x22 - LOOP_OFF)} ${fmt(y2)}, ${fmt(x22)} ${fmt(y2)}`;
|
|
10458
|
+
w.labelX = f.x + f.w / 2;
|
|
10459
|
+
w.labelY = yb + 12;
|
|
10460
|
+
return;
|
|
10461
|
+
}
|
|
10462
|
+
if (w.back) {
|
|
10463
|
+
const x12 = f.x + f.w;
|
|
10464
|
+
const x22 = t.x;
|
|
10465
|
+
const yb = Math.max(f.y, t.y) + NODE_H + SELF_LOOP_DROP + 10 + w.parallel * 12;
|
|
10466
|
+
w.d = `M ${fmt(x12)} ${fmt(y1)} C ${fmt(x12 + LOOP_OFF)} ${fmt(y1)}, ${fmt(x12 + LOOP_OFF)} ${fmt(yb)}, ${fmt(x12)} ${fmt(yb)} L ${fmt(x22)} ${fmt(yb)} C ${fmt(x22 - LOOP_OFF)} ${fmt(yb)}, ${fmt(x22 - LOOP_OFF)} ${fmt(y2)}, ${fmt(x22)} ${fmt(y2)}`;
|
|
10467
|
+
w.labelX = (x12 + x22) / 2;
|
|
10468
|
+
w.labelY = yb + 12;
|
|
10469
|
+
return;
|
|
10470
|
+
}
|
|
10471
|
+
const x1 = f.x + f.w;
|
|
10472
|
+
const x2 = t.x;
|
|
10473
|
+
const dx = Math.max(x2 - x1, 4);
|
|
10474
|
+
const sc = dx < 100 ? 0.75 * (dx / 100) : 0.75;
|
|
10475
|
+
const off = Math.max(dx * sc, 10);
|
|
10476
|
+
const sign = w.parallel % 2 === 1 ? -1 : 1;
|
|
10477
|
+
const steps = Math.ceil(w.parallel / 2);
|
|
10478
|
+
const bow = sign * steps * PARALLEL_BOW;
|
|
10479
|
+
w.d = `M ${fmt(x1)} ${fmt(y1)} C ${fmt(x1 + off)} ${fmt(y1 + bow)}, ${fmt(x2 - off)} ${fmt(y2 + bow)}, ${fmt(x2)} ${fmt(y2)}`;
|
|
10480
|
+
w.labelX = (x1 + x2) / 2;
|
|
10481
|
+
if (x2 - x1 < LABEL_MIN_SPAN) {
|
|
10482
|
+
w.labelY = Math.min(y1, y2) - 12 - w.parallel * 12;
|
|
10483
|
+
} else {
|
|
10484
|
+
w.labelY = (y1 + y2) / 2 - 5 + sign * steps * PARALLEL_LABEL_STEP;
|
|
10485
|
+
}
|
|
10486
|
+
}
|
|
10487
|
+
function relTime(nowMs, v) {
|
|
10488
|
+
const t = typeof v === "number" ? v : typeof v === "string" ? Date.parse(v) : null;
|
|
10489
|
+
if (!Number.isFinite(t) || !Number.isFinite(nowMs)) return null;
|
|
10490
|
+
const s = Math.max(0, Math.floor((nowMs - t) / 1e3));
|
|
10491
|
+
if (s < 60) return `${s}s ago`;
|
|
10492
|
+
const mn = Math.floor(s / 60);
|
|
10493
|
+
if (mn < 60) return `${mn}m ago`;
|
|
10494
|
+
const h = Math.floor(mn / 60);
|
|
10495
|
+
if (h < 48) return `${h}h ago`;
|
|
10496
|
+
return `${Math.floor(h / 24)}d ago`;
|
|
10497
|
+
}
|
|
10498
|
+
function buildTip(n, flags, groupLabel, nowMs) {
|
|
10499
|
+
const lines = [];
|
|
10500
|
+
if (n.kind === "trigger") {
|
|
10501
|
+
lines.push(`trigger \xB7 ${n.onType ?? "?"} \xB7 ${n.label ?? ""}`.trim());
|
|
10502
|
+
if (n.flow !== null) lines.push(`flow: ${n.flow}${n.replicas !== null ? ` \xD7${n.replicas}` : ""}`);
|
|
10503
|
+
} else if (n.kind === "injected") {
|
|
10504
|
+
lines.push(`injected skill \xB7 ${n.name ?? "?"}`);
|
|
10505
|
+
lines.push("trigger-reachable via run.skillsDir, never AI-reachable");
|
|
10506
|
+
} else if (n.kind === "skill-missing") {
|
|
10507
|
+
lines.push(`skill \xB7 ${n.name ?? "?"} \xB7 missing at HEAD`);
|
|
10508
|
+
} else if (n.kind === "skill-unverified") {
|
|
10509
|
+
lines.push(`skill \xB7 ${n.name ?? "?"} \xB7 unverified (repo not readable from this host)`);
|
|
10510
|
+
} else {
|
|
10511
|
+
lines.push(`skill \xB7 ${n.name ?? "?"}${n.isSub ? " \xB7 sub-skill (never a flow)" : ""}`);
|
|
10512
|
+
}
|
|
10513
|
+
if (groupLabel !== null) lines.push(`folder: ${groupLabel}`);
|
|
10514
|
+
if (n.description !== null) lines.push(n.description);
|
|
10515
|
+
if (n.kind === "trigger") {
|
|
10516
|
+
if (n.runs > 0) {
|
|
10517
|
+
const rel = relTime(nowMs, n.lastEndedAt);
|
|
10518
|
+
lines.push(`runs: ${n.runs}${n.lastOutcome !== null ? ` \xB7 last ${n.lastOutcome}${rel !== null ? ` ${rel}` : ""}` : ""}`);
|
|
10519
|
+
} else {
|
|
10520
|
+
lines.push("no runs in window");
|
|
10521
|
+
}
|
|
10522
|
+
}
|
|
10523
|
+
if (n.aiTrigger) lines.push("chainable: ai-trigger allow");
|
|
10524
|
+
if (n.kind === "skill" && n.loops.length > 0) {
|
|
10525
|
+
lines.push(`loops in skill: ${n.loops.map((l) => l.hint).join(" \xB7 ")}`);
|
|
10526
|
+
}
|
|
10527
|
+
for (const f of flags) lines.push(f.detail !== null ? `${f.flag}: ${f.detail}` : f.flag);
|
|
10528
|
+
return lines.join("\n");
|
|
10529
|
+
}
|
|
10530
|
+
function groupTitle(g, fullPaths) {
|
|
10531
|
+
if (g.kind === "forge") {
|
|
10532
|
+
const scope = Array.isArray(g.repos) && g.repos.length > 0 ? ` \xB7 ran against ${g.repos.join(", ")}` : "";
|
|
10533
|
+
return `${g.label}${scope} \xB7 forge \xB7 unverifiable from this host`;
|
|
10534
|
+
}
|
|
10535
|
+
const name = fullPaths === true && typeof g.path === "string" && g.path !== "" ? g.path : g.label;
|
|
10536
|
+
if (g.unreachable !== null && g.unreachable !== void 0 && g.unreachable !== "") return `${name} \xB7 ${g.unreachable}`;
|
|
10537
|
+
if (g.head !== null && g.head !== void 0 && g.head !== "") return `${name} \xB7 HEAD ${String(g.head).slice(0, 7)}`;
|
|
10538
|
+
return name;
|
|
10539
|
+
}
|
|
10540
|
+
function capsLineText(caps) {
|
|
10541
|
+
const d = caps.chainDepthMax ?? "?";
|
|
10542
|
+
const m = caps.chainMaxPerJob ?? "?";
|
|
10543
|
+
const k = caps.windowDays ?? "?";
|
|
10544
|
+
return `chains: depth \u2264 ${d} \xB7 \u2264 ${m} per job \xB7 same folder only \xB7 window ${k}d`;
|
|
10545
|
+
}
|
|
10546
|
+
function nodeState(n, flagNames) {
|
|
10547
|
+
if (n.kind === "skill-missing" || [...flagNames].some((f) => DANGLING_FLAGS.has(f))) {
|
|
10548
|
+
return { stroke: DANGER, dash: "10,4", faded: false };
|
|
10549
|
+
}
|
|
10550
|
+
if (n.kind === "skill-unverified") return { stroke: PAGE_DIM, dash: "10,4", faded: false };
|
|
10551
|
+
if (flagNames.has("orphan")) return { stroke: CHIP_STROKE, dash: "8,3", faded: true };
|
|
10552
|
+
return { stroke: CHIP_STROKE, dash: null, faded: false };
|
|
10553
|
+
}
|
|
10554
|
+
function chipFill(n) {
|
|
10555
|
+
if (n.kind === "trigger") return CHIP_FILL[n.onType] ?? PORT_FILL;
|
|
10556
|
+
return CHIP_FILL.skill;
|
|
10557
|
+
}
|
|
10558
|
+
function chipGlyph(n) {
|
|
10559
|
+
if (n.kind === "trigger") return GLYPH[n.onType] ?? "\u2022";
|
|
10560
|
+
return GLYPH[n.kind] ?? GLYPH.skill;
|
|
10561
|
+
}
|
|
10562
|
+
function statusColor(outcome) {
|
|
10563
|
+
if (outcome === "completed") return STATUS_COMPLETED;
|
|
10564
|
+
if (outcome === "failed") return STATUS_FAILED;
|
|
10565
|
+
return STATUS_OTHER;
|
|
10566
|
+
}
|
|
10567
|
+
function nodeSvg(p, flags, hasIn, hasOut) {
|
|
10568
|
+
const n = p.node;
|
|
10569
|
+
if (p.nested === true) {
|
|
10570
|
+
return [
|
|
10571
|
+
`<g class="gnode" id="${p.id}" transform="translate(${fmt(p.x)},${fmt(p.y)})">`,
|
|
10572
|
+
`<rect width="${fmt(p.w)}" height="${fmt(p.h)}" rx="4" fill="${CHIP_FILL.skill}" stroke="${CHIP_STROKE}" stroke-width="1"/>`,
|
|
10573
|
+
`<text x="8" y="16" font-size="11" fill="${CHIP_LABEL}">${escapeHtml(p.label)}</text>`,
|
|
10574
|
+
"</g>"
|
|
10575
|
+
].join("");
|
|
10576
|
+
}
|
|
10577
|
+
const flagNames = new Set(flags.map((f) => f.flag));
|
|
10578
|
+
const state = nodeState(n, flagNames);
|
|
10579
|
+
const parts = [`<g class="gnode" id="${p.id}" transform="translate(${fmt(p.x)},${fmt(p.y)})">`];
|
|
10580
|
+
parts.push(
|
|
10581
|
+
`<rect width="${fmt(p.w)}" height="${fmt(NODE_H)}" rx="5" fill="${chipFill(n)}"${state.faded ? ' fill-opacity=".5"' : ""} stroke="${state.stroke}" stroke-width="1"${state.dash !== null ? ` stroke-dasharray="${state.dash}"` : ""}/>`
|
|
10582
|
+
);
|
|
10583
|
+
parts.push(`<path d="M 30 1 L 30 29" stroke="${CHIP_STROKE}" stroke-width="1" opacity=".5"/>`);
|
|
10584
|
+
parts.push(`<text x="15" y="20" text-anchor="middle" font-size="13" fill="${CHIP_LABEL}">${escapeHtml(chipGlyph(n))}</text>`);
|
|
10585
|
+
parts.push(`<text x="${LABEL_X}" y="20" font-size="14" fill="${CHIP_LABEL}">${escapeHtml(p.label)}</text>`);
|
|
10586
|
+
if (hasIn) parts.push(`<rect x="-5" y="${fmt(NODE_H / 2 - 5)}" width="10" height="10" rx="3" fill="${PORT_FILL}" stroke="${CHIP_STROKE}" stroke-width="1"/>`);
|
|
10587
|
+
if (hasOut) parts.push(`<rect x="${fmt(p.w - 5)}" y="${fmt(NODE_H / 2 - 5)}" width="10" height="10" rx="3" fill="${PORT_FILL}" stroke="${CHIP_STROKE}" stroke-width="1"/>`);
|
|
10588
|
+
if (n.kind === "trigger") {
|
|
10589
|
+
parts.push(`<rect x="3" y="${fmt(NODE_H + 3)}" width="9" height="9" rx="2" fill="${statusColor(n.runs > 0 ? n.lastOutcome : null)}"/>`);
|
|
10590
|
+
parts.push(`<text x="16" y="${fmt(NODE_H + 11)}" font-size="10" fill="${PAGE_DIM}">${n.runs > 0 ? `${fmt(n.runs)} runs` : "no runs"}</text>`);
|
|
10591
|
+
}
|
|
10592
|
+
const orange = [...flagNames].some((f) => ORANGE_FLAGS.has(f));
|
|
10593
|
+
if (orange) parts.push(`<circle cx="${fmt(p.w - 4)}" cy="-2" r="5" fill="${BADGE_ORANGE}"/>`);
|
|
10594
|
+
if (n.aiTrigger) parts.push(`<circle cx="${fmt(p.w - 4 - (orange ? 14 : 0))}" cy="-2" r="4" fill="none" stroke="${BADGE_GREEN}" stroke-width="2"/>`);
|
|
10595
|
+
parts.push("</g>");
|
|
10596
|
+
return parts.join("");
|
|
10597
|
+
}
|
|
10598
|
+
function wireStyle(w) {
|
|
10599
|
+
if (w.kind === "observed") return { stroke: WIRE_OBSERVED, width: 3, dash: null };
|
|
10600
|
+
if (w.kind === "config") return { stroke: WIRE_CONFIG, width: 2, dash: null };
|
|
10601
|
+
if (w.kind === "potential") return { stroke: w.edge.strong ? PAGE_ACCENT : WIRE_POTENTIAL, width: 2, dash: "6,4" };
|
|
10602
|
+
return { stroke: CHIP_FILL.cron, width: 2, dash: "4,3" };
|
|
10603
|
+
}
|
|
10604
|
+
function wireSvg(w) {
|
|
10605
|
+
const s = wireStyle(w);
|
|
10606
|
+
const parts = [`<g class="gwire" id="${w.id}">`];
|
|
10607
|
+
parts.push(`<path d="${w.d}" fill="none" stroke="${s.stroke}" stroke-width="${fmt(s.width)}"${s.dash !== null ? ` stroke-dasharray="${s.dash}"` : ""}/>`);
|
|
10608
|
+
let label = null;
|
|
10609
|
+
let fill = PAGE_DIM;
|
|
10610
|
+
if (w.kind === "observed" && w.edge.count !== null) {
|
|
10611
|
+
label = `(${w.edge.count}\xD7)`;
|
|
10612
|
+
fill = WIRE_OBSERVED;
|
|
10613
|
+
} else if (w.kind === "potential") {
|
|
10614
|
+
label = "mention";
|
|
10615
|
+
fill = w.edge.strong ? PAGE_ACCENT : WIRE_POTENTIAL;
|
|
10616
|
+
} else if (w.kind === "cron-rearm" && w.edge.label !== null) {
|
|
10617
|
+
label = w.edge.label;
|
|
10618
|
+
fill = CHIP_FILL.cron;
|
|
10619
|
+
}
|
|
10620
|
+
if (label !== null) parts.push(`<text x="${fmt(w.labelX)}" y="${fmt(w.labelY)}" text-anchor="middle" font-size="10" fill="${fill}">${escapeHtml(label)}</text>`);
|
|
10621
|
+
parts.push("</g>");
|
|
10622
|
+
return parts.join("");
|
|
10623
|
+
}
|
|
10624
|
+
function groupSvg(g, fullPaths) {
|
|
10625
|
+
const opacity = g.kind === "forge" ? "0.03" : "0.06";
|
|
10626
|
+
return [
|
|
10627
|
+
`<g class="ggroup">`,
|
|
10628
|
+
`<rect x="${fmt(g.x)}" y="${fmt(g.y)}" width="${fmt(g.w)}" height="${fmt(g.h)}" rx="2" fill="${GROUP_FILL}" fill-opacity="${opacity}" stroke="${PAGE_BORDER}" stroke-width="2"/>`,
|
|
10629
|
+
`<text x="${fmt(g.x + 8)}" y="${fmt(g.y + 18)}" font-size="11" fill="${PAGE_DIM}">${escapeHtml(groupTitle(g, fullPaths))}</text>`,
|
|
10630
|
+
`</g>`
|
|
10631
|
+
].join("");
|
|
10632
|
+
}
|
|
10633
|
+
function skillGroupSvg(sg) {
|
|
10634
|
+
const parts = [`<g class="sgroup" id="${sg.id}">`];
|
|
10635
|
+
parts.push(`<rect x="${fmt(sg.x)}" y="${fmt(sg.y)}" width="${fmt(sg.w)}" height="${fmt(sg.h)}" rx="2" fill="${GROUP_FILL}" fill-opacity="0.08" stroke="${PAGE_BORDER}" stroke-width="2"/>`);
|
|
10636
|
+
parts.push(`<text x="${fmt(sg.x + 8)}" y="${fmt(sg.y + 16)}" font-size="11" fill="${PAGE_DIM}">${escapeHtml(sg.label)}</text>`);
|
|
10637
|
+
parts.push(`<path d="${sg.d}" fill="none" stroke="${WIRE_POTENTIAL}" stroke-width="1.5" stroke-dasharray="4,3"/>`);
|
|
10638
|
+
for (const m of sg.markers) {
|
|
10639
|
+
parts.push(`<rect x="${fmt(m.x)}" y="${fmt(m.y)}" width="${fmt(m.w)}" height="${fmt(m.h)}" rx="6" fill="${CHIP_FILL.skill}" stroke="${CHIP_STROKE}" stroke-width="1"/>`);
|
|
10640
|
+
parts.push(`<text x="${fmt(m.x + m.w / 2)}" y="${fmt(m.y + m.h / 2 + 6)}" text-anchor="middle" font-size="16" fill="${CHIP_LABEL}">\u27F3</text>`);
|
|
10641
|
+
parts.push(`<text x="${fmt(m.hintX)}" y="${fmt(m.hintY)}" font-size="10" fill="${PAGE_DIM}">${escapeHtml(m.hint)}</text>`);
|
|
10642
|
+
}
|
|
10643
|
+
parts.push("</g>");
|
|
10644
|
+
return parts.join("");
|
|
10645
|
+
}
|
|
10646
|
+
function legendSample(stroke, width, dash) {
|
|
10647
|
+
return `<svg width="46" height="10" aria-hidden="true"><path d="M 2 5 C 16 5, 30 5, 44 5" fill="none" stroke="${stroke}" stroke-width="${fmt(width)}"${dash !== null ? ` stroke-dasharray="${dash}"` : ""}/></svg>`;
|
|
10648
|
+
}
|
|
10649
|
+
function legendSwatch(inner) {
|
|
10650
|
+
return `<svg width="16" height="12" aria-hidden="true">${inner}</svg>`;
|
|
10651
|
+
}
|
|
10652
|
+
function legendHtml(norm) {
|
|
10653
|
+
const rows = [];
|
|
10654
|
+
const row = (sample, text) => rows.push(`<div class="row">${sample}<span>${escapeHtml(text)}</span></div>`);
|
|
10655
|
+
rows.push("<h2>edges</h2>");
|
|
10656
|
+
row(legendSample(WIRE_CONFIG, 2, null), "config: the trigger names this flow");
|
|
10657
|
+
row(legendSample(WIRE_OBSERVED, 3, null), "observed \xD7n: chained in run records");
|
|
10658
|
+
row(legendSample(WIRE_POTENTIAL, 2, "6,4"), "potential: a text mention, not a promise");
|
|
10659
|
+
row(legendSample(PAGE_ACCENT, 2, "6,4"), "potential (strong): near chain vocabulary");
|
|
10660
|
+
row(legendSample(CHIP_FILL.cron, 2, "4,3"), "cron re-arm: the schedule itself");
|
|
10661
|
+
rows.push("<h2>badges</h2>");
|
|
10662
|
+
row(legendSwatch(`<circle cx="8" cy="6" r="5" fill="${BADGE_ORANGE}"/>`), "unread / injected ai-trigger / PR spend-loop risk");
|
|
10663
|
+
row(legendSwatch(`<circle cx="8" cy="6" r="4" fill="none" stroke="${BADGE_GREEN}" stroke-width="2"/>`), "chainable: ai-trigger allow");
|
|
10664
|
+
row(legendSwatch(`<rect x="1" y="1" width="14" height="10" rx="2" fill="none" stroke="${CHIP_STROKE}" stroke-dasharray="8,3"/>`), "orphan: no trigger, no ai-trigger, no mention");
|
|
10665
|
+
row(legendSwatch(`<rect x="1" y="1" width="14" height="10" rx="2" fill="none" stroke="${DANGER}" stroke-dasharray="10,4"/>`), "dangling: flow absent or name invalid");
|
|
10666
|
+
row(legendSwatch(`<rect x="1" y="1" width="14" height="10" rx="2" fill="none" stroke="${PAGE_DIM}" stroke-dasharray="10,4"/>`), "unverified: repo not readable from this host");
|
|
10667
|
+
rows.push(`<div class="caps">${escapeHtml(capsLineText(norm.caps))}</div>`);
|
|
10668
|
+
const honesty = [];
|
|
10669
|
+
if (norm.meta.unattributedRuns > 0) honesty.push(`${norm.meta.unattributedRuns} runs unattributed`);
|
|
10670
|
+
if (norm.meta.truncated.folders) honesty.push("folder scan truncated (cap reached)");
|
|
10671
|
+
if (norm.meta.truncated.skills) honesty.push("skill enumeration truncated or partly unread");
|
|
10672
|
+
if (norm.meta.truncated.edges) honesty.push("observed edges truncated (cap reached)");
|
|
10673
|
+
if (norm.meta.droppedObservedEdges > 0) honesty.push(`${norm.meta.droppedObservedEdges} observed edges dropped (no unique folder)`);
|
|
10674
|
+
for (const line of honesty) rows.push(`<div class="honesty">${escapeHtml(line)}</div>`);
|
|
10675
|
+
return `<div id="legend">${rows.join("")}</div>`;
|
|
10676
|
+
}
|
|
10677
|
+
function bannersHtml(norm) {
|
|
10678
|
+
const banners = [];
|
|
10679
|
+
if (!norm.ok) banners.push("no graph model supplied; the page has nothing to draw");
|
|
10680
|
+
if (norm.meta.triggersMissing) banners.push("no triggers file found");
|
|
10681
|
+
if (norm.meta.triggersInvalid !== null) banners.push(`triggers file invalid: ${norm.meta.triggersInvalid}`);
|
|
10682
|
+
if (banners.length === 0) return "";
|
|
10683
|
+
return `<div id="banners">${banners.map((b) => `<div class="banner">${escapeHtml(b)}</div>`).join("")}</div>`;
|
|
10684
|
+
}
|
|
10685
|
+
function buildGraphHtml(model, { now, fullPaths } = {}) {
|
|
10686
|
+
let norm;
|
|
10687
|
+
try {
|
|
10688
|
+
norm = normalizeModel(model);
|
|
10689
|
+
} catch {
|
|
10690
|
+
norm = normalizeModel(null);
|
|
10691
|
+
}
|
|
10692
|
+
const layout = layoutNormalized(norm);
|
|
10693
|
+
const nowMs = Number.isFinite(now) ? now : norm.meta.generatedAt ?? 0;
|
|
10694
|
+
const flagsByOrig = /* @__PURE__ */ new Map();
|
|
10695
|
+
for (const f of norm.flags) {
|
|
10696
|
+
if (!flagsByOrig.has(f.nodeId)) flagsByOrig.set(f.nodeId, []);
|
|
10697
|
+
flagsByOrig.get(f.nodeId).push(f);
|
|
10698
|
+
}
|
|
10699
|
+
const groupById = new Map(layout.groups.map((g) => [g.id, g]));
|
|
10700
|
+
const hasIn = /* @__PURE__ */ new Set();
|
|
10701
|
+
const hasOut = /* @__PURE__ */ new Set();
|
|
10702
|
+
const adj = /* @__PURE__ */ new Map();
|
|
10703
|
+
const touch = (id) => {
|
|
10704
|
+
if (!adj.has(id)) adj.set(id, { nb: /* @__PURE__ */ new Set(), w: /* @__PURE__ */ new Set() });
|
|
10705
|
+
return adj.get(id);
|
|
10706
|
+
};
|
|
10707
|
+
for (const w of layout.wires) {
|
|
10708
|
+
hasOut.add(w.from);
|
|
10709
|
+
hasIn.add(w.to);
|
|
10710
|
+
touch(w.from).nb.add(w.to);
|
|
10711
|
+
touch(w.from).w.add(w.id);
|
|
10712
|
+
touch(w.to).nb.add(w.from);
|
|
10713
|
+
touch(w.to).w.add(w.id);
|
|
10714
|
+
}
|
|
10715
|
+
const graphData = { nodes: {} };
|
|
10716
|
+
const nodeParts = [];
|
|
10717
|
+
for (const p of layout.nodes) {
|
|
10718
|
+
const n = p.node;
|
|
10719
|
+
const flags = norm.nodes.length > 0 ? findFlags(flagsByOrig, norm.nodes, p) : [];
|
|
10720
|
+
const group = groupById.get(p.groupId);
|
|
10721
|
+
const drawIn = hasIn.has(p.id) || n.kind !== "trigger" && n.kind !== "injected";
|
|
10722
|
+
const drawOut = hasOut.has(p.id) || n.kind === "trigger";
|
|
10723
|
+
nodeParts.push(nodeSvg(p, flags, drawIn, drawOut));
|
|
10724
|
+
const a = adj.get(p.id);
|
|
10725
|
+
graphData.nodes[p.id] = {
|
|
10726
|
+
tip: buildTip(n, flags, group ? group.label : null, nowMs),
|
|
10727
|
+
nb: a ? [...a.nb].sort() : [],
|
|
10728
|
+
w: a ? [...a.w].sort() : []
|
|
10729
|
+
};
|
|
10730
|
+
}
|
|
10731
|
+
const svgBody = [
|
|
10732
|
+
layout.groups.map((g) => groupSvg(g, fullPaths)).join(""),
|
|
10733
|
+
layout.skillGroups.map(skillGroupSvg).join(""),
|
|
10734
|
+
layout.wires.map(wireSvg).join(""),
|
|
10735
|
+
nodeParts.join("")
|
|
10736
|
+
].join("");
|
|
10737
|
+
const vb = layout.viewBox;
|
|
10738
|
+
const svgEl = [
|
|
10739
|
+
`<svg id="graph" class="canvas" viewBox="${fmt(vb.x)} ${fmt(vb.y)} ${fmt(vb.w)} ${fmt(vb.h)}" role="img" aria-label="pi-dispatch trigger and flow graph" preserveAspectRatio="xMidYMid meet">`,
|
|
10740
|
+
`<g id="root">${svgBody}</g>`,
|
|
10741
|
+
`</svg>`
|
|
10742
|
+
].join("");
|
|
10743
|
+
return [
|
|
10744
|
+
"<!doctype html>",
|
|
10745
|
+
'<meta charset="utf-8">',
|
|
10746
|
+
"<title>pi-dispatch graph</title>",
|
|
10747
|
+
`<style>${PAGE_CSS}</style>`,
|
|
10748
|
+
"<body>",
|
|
10749
|
+
'<div id="hdr">',
|
|
10750
|
+
"<h1>pi-dispatch graph</h1>",
|
|
10751
|
+
'<span id="stamp"></span>',
|
|
10752
|
+
'<button id="reload" type="button">Reload</button>',
|
|
10753
|
+
'<select id="auto" aria-label="auto reload"><option value="0">off</option><option value="5">5s</option><option value="30">30s</option></select>',
|
|
10754
|
+
"</div>",
|
|
10755
|
+
bannersHtml(norm),
|
|
10756
|
+
`<div id="wrap">${svgEl}<div id="tip"></div></div>`,
|
|
10757
|
+
legendHtml(norm),
|
|
10758
|
+
`<script>
|
|
10759
|
+
"use strict";
|
|
10760
|
+
var GENERATED_AT = ${fmt(nowMs)};
|
|
10761
|
+
var GRAPH = ${embedJson(graphData)};
|
|
10762
|
+
${PAGE_JS}</script>`,
|
|
10763
|
+
"</body>"
|
|
10764
|
+
].join("\n");
|
|
10765
|
+
}
|
|
10766
|
+
function findFlags(flagsByOrig, _nodes, placed) {
|
|
10767
|
+
return flagsByOrig.get(placed.node.id) ?? [];
|
|
10768
|
+
}
|
|
10769
|
+
var GRAPH_HTML_KINDS, NODE_H, NODE_MIN_W, GRID, NODE_MAX_W, CHAR_W, LABEL_X, CHIP_MAX_CHARS, RANK_PITCH, ROW_GAP, GROUP_PAD_L, GROUP_PAD_R, GROUP_PAD_T, GROUP_PAD_B, GROUP_GAP, SELF_LOOP_DROP, UNDER_ROUTE_EXTRA, PARALLEL_BOW, PARALLEL_LABEL_STEP, LABEL_MIN_SPAN, SG_CHIP_X, SG_CHIP_Y, SG_RING, SG_MARKER, SG_HINT_CHARS, SG_SMALL_CHAR_W, SUB_CHIP_H, VIEW_MARGIN, PAGE_CANVAS, PAGE_PANEL, PAGE_BORDER, PAGE_FG, PAGE_DIM, PAGE_ACCENT, PAGE_AMBER, CHIP_STROKE, CHIP_LABEL, PORT_FILL, CHIP_FILL, STATUS_COMPLETED, STATUS_FAILED, STATUS_OTHER, BADGE_ORANGE, BADGE_GREEN, DANGER, GROUP_FILL, WIRE_OBSERVED, WIRE_CONFIG, WIRE_POTENTIAL, GLYPH, ORANGE_FLAGS, DANGLING_FLAGS, PAGE_CSS, PAGE_JS;
|
|
10770
|
+
var init_graph_html = __esm({
|
|
10771
|
+
"src/graph-html.mjs"() {
|
|
10772
|
+
GRAPH_HTML_KINDS = Object.freeze(["config", "observed", "potential", "cron-rearm"]);
|
|
10773
|
+
NODE_H = 30;
|
|
10774
|
+
NODE_MIN_W = 100;
|
|
10775
|
+
GRID = 20;
|
|
10776
|
+
NODE_MAX_W = 160;
|
|
10777
|
+
CHAR_W = 8;
|
|
10778
|
+
LABEL_X = 38;
|
|
10779
|
+
CHIP_MAX_CHARS = 14;
|
|
10780
|
+
RANK_PITCH = 180;
|
|
10781
|
+
ROW_GAP = 26;
|
|
10782
|
+
GROUP_PAD_L = 20;
|
|
10783
|
+
GROUP_PAD_R = 20;
|
|
10784
|
+
GROUP_PAD_T = 40;
|
|
10785
|
+
GROUP_PAD_B = 44;
|
|
10786
|
+
GROUP_GAP = 40;
|
|
10787
|
+
SELF_LOOP_DROP = 25;
|
|
10788
|
+
UNDER_ROUTE_EXTRA = 40;
|
|
10789
|
+
PARALLEL_BOW = 14;
|
|
10790
|
+
PARALLEL_LABEL_STEP = 10.5;
|
|
10791
|
+
LABEL_MIN_SPAN = 40;
|
|
10792
|
+
SG_CHIP_X = 22;
|
|
10793
|
+
SG_CHIP_Y = 26;
|
|
10794
|
+
SG_RING = 10;
|
|
10795
|
+
SG_MARKER = 40;
|
|
10796
|
+
SG_HINT_CHARS = 24;
|
|
10797
|
+
SG_SMALL_CHAR_W = 6;
|
|
10798
|
+
SUB_CHIP_H = 24;
|
|
10799
|
+
VIEW_MARGIN = 80;
|
|
10800
|
+
PAGE_CANVAS = "#0d1117";
|
|
10801
|
+
PAGE_PANEL = "#161b22";
|
|
10802
|
+
PAGE_BORDER = "#30363d";
|
|
10803
|
+
PAGE_FG = "#c9d1d9";
|
|
10804
|
+
PAGE_DIM = "#8b949e";
|
|
10805
|
+
PAGE_ACCENT = "#58a6ff";
|
|
10806
|
+
PAGE_AMBER = "#d29922";
|
|
10807
|
+
CHIP_STROKE = "#999";
|
|
10808
|
+
CHIP_LABEL = "#333";
|
|
10809
|
+
PORT_FILL = "#d9d9d9";
|
|
10810
|
+
CHIP_FILL = Object.freeze({
|
|
10811
|
+
cron: "#a6bbcf",
|
|
10812
|
+
label: "#d8bfd8",
|
|
10813
|
+
comment: "#e7e7ae",
|
|
10814
|
+
pull_request: "#c0deed",
|
|
10815
|
+
skill: "#fdd0a2"
|
|
10816
|
+
});
|
|
10817
|
+
STATUS_COMPLETED = "#3fb950";
|
|
10818
|
+
STATUS_FAILED = "#ff5f57";
|
|
10819
|
+
STATUS_OTHER = "#6e7681";
|
|
10820
|
+
BADGE_ORANGE = "#db6d28";
|
|
10821
|
+
BADGE_GREEN = "#3fb950";
|
|
10822
|
+
DANGER = "#f85149";
|
|
10823
|
+
GROUP_FILL = "#E6E0F8";
|
|
10824
|
+
WIRE_OBSERVED = "#999";
|
|
10825
|
+
WIRE_CONFIG = "#6e7681";
|
|
10826
|
+
WIRE_POTENTIAL = "#8b949e";
|
|
10827
|
+
GLYPH = Object.freeze({
|
|
10828
|
+
cron: "\u25F7",
|
|
10829
|
+
label: "\u25C8",
|
|
10830
|
+
comment: "\u275D",
|
|
10831
|
+
pull_request: "\u21C4",
|
|
10832
|
+
skill: "\u0192",
|
|
10833
|
+
"skill-missing": "!",
|
|
10834
|
+
"skill-unverified": "?",
|
|
10835
|
+
injected: "+"
|
|
10836
|
+
});
|
|
10837
|
+
ORANGE_FLAGS = /* @__PURE__ */ new Set(["unread", "injected-ai-trigger", "pr-spend-loop-risk"]);
|
|
10838
|
+
DANGLING_FLAGS = /* @__PURE__ */ new Set(["no-skill", "charset-invalid"]);
|
|
10839
|
+
PAGE_CSS = `
|
|
10840
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
10841
|
+
body{background:${PAGE_CANVAS};color:${PAGE_FG};font:14px/1.4 -apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}
|
|
10842
|
+
#hdr{position:fixed;top:0;left:0;right:0;height:46px;display:flex;align-items:center;gap:12px;padding:0 14px;background:${PAGE_PANEL};border-bottom:1px solid ${PAGE_BORDER};z-index:3}
|
|
10843
|
+
#hdr h1{font-size:14px;font-weight:600}
|
|
10844
|
+
#stamp{color:${PAGE_DIM};font-size:12px}
|
|
10845
|
+
#stamp.stale{color:${PAGE_AMBER}}
|
|
10846
|
+
#hdr button,#hdr select{background:#21262d;color:${PAGE_FG};border:1px solid ${PAGE_BORDER};border-radius:6px;padding:3px 10px;font-size:12px}
|
|
10847
|
+
#banners{position:fixed;top:52px;left:10px;z-index:3;max-width:46%}
|
|
10848
|
+
.banner{background:#2d1517;border:1px solid ${DANGER};color:${DANGER};border-radius:6px;padding:6px 10px;margin-bottom:6px;font-size:12px}
|
|
10849
|
+
#wrap{position:relative;height:100vh;padding-top:46px}
|
|
10850
|
+
svg.canvas{display:block;width:100%;height:100%;cursor:grab;touch-action:none}
|
|
10851
|
+
#tip{position:absolute;display:none;max-width:340px;background:${PAGE_PANEL};border:1px solid ${PAGE_BORDER};border-radius:6px;padding:8px 10px;font-size:12px;color:${PAGE_FG};white-space:pre-line;pointer-events:none;z-index:4}
|
|
10852
|
+
#legend{position:fixed;top:56px;right:10px;width:256px;background:${PAGE_PANEL};border:1px solid ${PAGE_BORDER};border-radius:6px;padding:10px 12px;font-size:11px;color:${PAGE_DIM};z-index:2}
|
|
10853
|
+
#legend h2{font-size:11px;color:${PAGE_FG};margin:6px 0 4px}
|
|
10854
|
+
#legend .row{display:flex;align-items:center;gap:6px;margin:2px 0}
|
|
10855
|
+
#legend .caps{margin-top:8px;color:${PAGE_FG}}
|
|
10856
|
+
#legend .honesty{margin-top:4px;color:${PAGE_AMBER}}
|
|
10857
|
+
.gnode{cursor:pointer}
|
|
10858
|
+
.dim .gnode,.dim .gwire{opacity:.15}
|
|
10859
|
+
.dim .hi{opacity:1}
|
|
10860
|
+
`;
|
|
10861
|
+
PAGE_JS = `
|
|
10862
|
+
(function () {
|
|
10863
|
+
"use strict";
|
|
10864
|
+
var svg = document.getElementById("graph");
|
|
10865
|
+
var root = document.getElementById("root");
|
|
10866
|
+
var tip = document.getElementById("tip");
|
|
10867
|
+
var wrap = document.getElementById("wrap");
|
|
10868
|
+
var stamp = document.getElementById("stamp");
|
|
10869
|
+
var auto = document.getElementById("auto");
|
|
10870
|
+
var reloadBtn = document.getElementById("reload");
|
|
10871
|
+
var selected = null;
|
|
10872
|
+
var autoTimer = null;
|
|
10873
|
+
var hashTimer = null;
|
|
10874
|
+
|
|
10875
|
+
function nowClock() { return new Date().getTime(); }
|
|
10876
|
+
|
|
10877
|
+
function fmtAge(ms) {
|
|
10878
|
+
var s = Math.max(0, Math.floor(ms / 1000));
|
|
10879
|
+
if (s < 60) return s + "s";
|
|
10880
|
+
var m = Math.floor(s / 60);
|
|
10881
|
+
if (m < 60) return m + "m";
|
|
10882
|
+
return Math.floor(m / 60) + "h";
|
|
10883
|
+
}
|
|
10884
|
+
function tickStamp() {
|
|
10885
|
+
var age = nowClock() - GENERATED_AT;
|
|
10886
|
+
stamp.textContent = "generated " + fmtAge(age) + " ago";
|
|
10887
|
+
stamp.className = age > 600000 ? "stale" : "";
|
|
10888
|
+
}
|
|
10889
|
+
tickStamp();
|
|
10890
|
+
setInterval(tickStamp, 1000);
|
|
10891
|
+
|
|
10892
|
+
if (reloadBtn) reloadBtn.addEventListener("click", function () { location.reload(); });
|
|
10893
|
+
function applyAuto() {
|
|
10894
|
+
if (autoTimer) { clearInterval(autoTimer); autoTimer = null; }
|
|
10895
|
+
var s = auto ? parseInt(auto.value, 10) : 0;
|
|
10896
|
+
if (s > 0) autoTimer = setInterval(function () { location.reload(); }, s * 1000);
|
|
10897
|
+
}
|
|
10898
|
+
if (auto) auto.addEventListener("change", function () { applyAuto(); writeHash(); });
|
|
10899
|
+
|
|
10900
|
+
if (!svg || !root) return;
|
|
10901
|
+
var vb = svg.viewBox.baseVal;
|
|
10902
|
+
var baseW = vb.width;
|
|
10903
|
+
|
|
10904
|
+
function writeHash() {
|
|
10905
|
+
hashTimer = null;
|
|
10906
|
+
var parts = ["vb=" + [vb.x, vb.y, vb.width, vb.height].map(function (v) { return Math.round(v * 10) / 10; }).join("_")];
|
|
10907
|
+
if (selected) parts.push("sel=" + selected);
|
|
10908
|
+
if (auto && auto.value !== "0") parts.push("ar=" + auto.value);
|
|
10909
|
+
history.replaceState(null, "", "#" + parts.join("&"));
|
|
10910
|
+
}
|
|
10911
|
+
function scheduleHash() {
|
|
10912
|
+
if (hashTimer) return;
|
|
10913
|
+
hashTimer = setTimeout(writeHash, 300);
|
|
10914
|
+
}
|
|
10915
|
+
function readHash() {
|
|
10916
|
+
var h = location.hash.replace(/^#/, "");
|
|
10917
|
+
if (!h) return;
|
|
10918
|
+
var kvs = h.split("&");
|
|
10919
|
+
for (var i = 0; i < kvs.length; i++) {
|
|
10920
|
+
var at = kvs[i].indexOf("=");
|
|
10921
|
+
if (at < 0) continue;
|
|
10922
|
+
var k = kvs[i].slice(0, at);
|
|
10923
|
+
var v = kvs[i].slice(at + 1);
|
|
10924
|
+
if (k === "vb") {
|
|
10925
|
+
var n = v.split("_").map(parseFloat);
|
|
10926
|
+
if (n.length === 4 && n.every(isFinite) && n[2] > 0 && n[3] > 0) {
|
|
10927
|
+
vb.x = n[0]; vb.y = n[1]; vb.width = n[2]; vb.height = n[3];
|
|
10928
|
+
}
|
|
10929
|
+
} else if (k === "sel" && ownNode(v)) {
|
|
10930
|
+
select(v);
|
|
10931
|
+
} else if (k === "ar" && auto) {
|
|
10932
|
+
auto.value = v === "5" || v === "30" ? v : "0";
|
|
10933
|
+
applyAuto();
|
|
10934
|
+
}
|
|
10935
|
+
}
|
|
10936
|
+
}
|
|
10937
|
+
|
|
10938
|
+
// With preserveAspectRatio meet, the browser scales UNIFORMLY (the smaller of the two ratios)
|
|
10939
|
+
// and centres the letterboxed remainder; mapping each axis by its own ratio pans at the wrong
|
|
10940
|
+
// speed and zooms beside the cursor whenever the element and viewBox aspects differ.
|
|
10941
|
+
function view() {
|
|
10942
|
+
var r = svg.getBoundingClientRect();
|
|
10943
|
+
var s = Math.min(r.width / vb.width, r.height / vb.height);
|
|
10944
|
+
return { r: r, s: s, ox: (r.width - vb.width * s) / 2, oy: (r.height - vb.height * s) / 2 };
|
|
10945
|
+
}
|
|
10946
|
+
|
|
10947
|
+
var panning = null;
|
|
10948
|
+
var suppressClick = false;
|
|
10949
|
+
svg.addEventListener("pointerdown", function (e) {
|
|
10950
|
+
if (e.button !== 0) return;
|
|
10951
|
+
panning = { x: e.clientX, y: e.clientY, moved: 0 };
|
|
10952
|
+
svg.setPointerCapture(e.pointerId);
|
|
10953
|
+
});
|
|
10954
|
+
svg.addEventListener("pointermove", function (e) {
|
|
10955
|
+
if (panning) {
|
|
10956
|
+
var v = view();
|
|
10957
|
+
var dx = e.clientX - panning.x;
|
|
10958
|
+
var dy = e.clientY - panning.y;
|
|
10959
|
+
panning.moved += Math.abs(dx) + Math.abs(dy);
|
|
10960
|
+
vb.x -= dx / v.s;
|
|
10961
|
+
vb.y -= dy / v.s;
|
|
10962
|
+
panning.x = e.clientX;
|
|
10963
|
+
panning.y = e.clientY;
|
|
10964
|
+
scheduleHash();
|
|
10965
|
+
}
|
|
10966
|
+
moveTip(e);
|
|
10967
|
+
});
|
|
10968
|
+
svg.addEventListener("pointerup", function (e) {
|
|
10969
|
+
if (panning && svg.hasPointerCapture && svg.hasPointerCapture(e.pointerId)) svg.releasePointerCapture(e.pointerId);
|
|
10970
|
+
// Pointer capture makes the click after a pan land on the svg, which would read as a
|
|
10971
|
+
// background click and wipe the selection; a real drag therefore swallows that click.
|
|
10972
|
+
suppressClick = panning !== null && panning.moved > 3;
|
|
10973
|
+
panning = null;
|
|
10974
|
+
});
|
|
10975
|
+
svg.addEventListener("wheel", function (e) {
|
|
10976
|
+
e.preventDefault();
|
|
10977
|
+
var k = Math.exp(-e.deltaY * 0.0015);
|
|
10978
|
+
var scale = baseW / (vb.width / k);
|
|
10979
|
+
if (scale < 0.15 || scale > 4) return;
|
|
10980
|
+
var v = view();
|
|
10981
|
+
var px = vb.x + (e.clientX - v.r.left - v.ox) / v.s;
|
|
10982
|
+
var py = vb.y + (e.clientY - v.r.top - v.oy) / v.s;
|
|
10983
|
+
vb.x = px - (px - vb.x) / k;
|
|
10984
|
+
vb.y = py - (py - vb.y) / k;
|
|
10985
|
+
vb.width = vb.width / k;
|
|
10986
|
+
vb.height = vb.height / k;
|
|
10987
|
+
scheduleHash();
|
|
10988
|
+
}, { passive: false });
|
|
10989
|
+
|
|
10990
|
+
// The ONLY indexed read of GRAPH.nodes: an id arriving from the hash (or a DOM id) could be a
|
|
10991
|
+
// prototype-chain key like "constructor", which a bare truthy lookup would happily return and a
|
|
10992
|
+
// later .nb dereference would throw on. A test pins this as the single raw read.
|
|
10993
|
+
function ownNode(id) {
|
|
10994
|
+
return Object.prototype.hasOwnProperty.call(GRAPH.nodes, id) ? GRAPH.nodes[id] : null;
|
|
10995
|
+
}
|
|
10996
|
+
function nodeAt(e) {
|
|
10997
|
+
var el = e.target && e.target.closest ? e.target.closest(".gnode") : null;
|
|
10998
|
+
return el && ownNode(el.id) ? el : null;
|
|
10999
|
+
}
|
|
11000
|
+
function moveTip(e) {
|
|
11001
|
+
var el = nodeAt(e);
|
|
11002
|
+
if (!el) { tip.style.display = "none"; return; }
|
|
11003
|
+
tip.textContent = ownNode(el.id).tip;
|
|
11004
|
+
tip.style.display = "block";
|
|
11005
|
+
var wr = wrap.getBoundingClientRect();
|
|
11006
|
+
tip.style.left = e.clientX - wr.left + 14 + "px";
|
|
11007
|
+
tip.style.top = e.clientY - wr.top + 14 + "px";
|
|
11008
|
+
}
|
|
11009
|
+
|
|
11010
|
+
function mark(id) {
|
|
11011
|
+
var el = document.getElementById(id);
|
|
11012
|
+
if (el) el.classList.add("hi");
|
|
11013
|
+
}
|
|
11014
|
+
function clearSel() {
|
|
11015
|
+
selected = null;
|
|
11016
|
+
root.classList.remove("dim");
|
|
11017
|
+
var hi = root.querySelectorAll(".hi");
|
|
11018
|
+
for (var i = 0; i < hi.length; i++) hi[i].classList.remove("hi");
|
|
11019
|
+
scheduleHash();
|
|
11020
|
+
}
|
|
11021
|
+
function select(id) {
|
|
11022
|
+
clearSel();
|
|
11023
|
+
var g = ownNode(id);
|
|
11024
|
+
if (!g) return;
|
|
11025
|
+
selected = id;
|
|
11026
|
+
root.classList.add("dim");
|
|
11027
|
+
mark(id);
|
|
11028
|
+
for (var i = 0; i < g.nb.length; i++) mark(g.nb[i]);
|
|
11029
|
+
for (var j = 0; j < g.w.length; j++) mark(g.w[j]);
|
|
11030
|
+
scheduleHash();
|
|
11031
|
+
}
|
|
11032
|
+
svg.addEventListener("click", function (e) {
|
|
11033
|
+
if (suppressClick) { suppressClick = false; return; }
|
|
11034
|
+
var el = nodeAt(e);
|
|
11035
|
+
if (el) { if (selected === el.id) clearSel(); else select(el.id); }
|
|
11036
|
+
else clearSel();
|
|
11037
|
+
});
|
|
11038
|
+
document.addEventListener("keydown", function (e) { if (e.key === "Escape") clearSel(); });
|
|
11039
|
+
|
|
11040
|
+
readHash();
|
|
11041
|
+
})();
|
|
11042
|
+
`;
|
|
11043
|
+
}
|
|
11044
|
+
});
|
|
11045
|
+
|
|
11046
|
+
// ../worker/src/open-browser.mjs
|
|
11047
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
11048
|
+
function openBrowser(url, { spawn: spawn2 = nodeSpawn, platform = process.platform } = {}) {
|
|
11049
|
+
const [cmd, args] = platform === "darwin" ? ["open", [url]] : platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
11050
|
+
try {
|
|
11051
|
+
const child = spawn2(cmd, args, { stdio: "ignore", detached: true });
|
|
11052
|
+
child.on("error", () => {
|
|
11053
|
+
});
|
|
11054
|
+
child.unref();
|
|
11055
|
+
} catch {
|
|
11056
|
+
}
|
|
11057
|
+
}
|
|
11058
|
+
var init_open_browser = __esm({
|
|
11059
|
+
"../worker/src/open-browser.mjs"() {
|
|
9287
11060
|
}
|
|
9288
11061
|
});
|
|
9289
11062
|
|
|
@@ -35315,7 +37088,7 @@ function setGlyphs(ascii) {
|
|
|
35315
37088
|
function stripControls(s) {
|
|
35316
37089
|
return String(s ?? "").replace(CONTROL_CHARS, "");
|
|
35317
37090
|
}
|
|
35318
|
-
function
|
|
37091
|
+
function clip2(line, w) {
|
|
35319
37092
|
const width = Math.max(0, Math.trunc(w) || 0);
|
|
35320
37093
|
const clean = stripControls(line);
|
|
35321
37094
|
if (clean.length <= width) return clean;
|
|
@@ -35325,13 +37098,13 @@ function clip(line, w) {
|
|
|
35325
37098
|
}
|
|
35326
37099
|
function pad(line, w) {
|
|
35327
37100
|
const width = Math.max(0, Math.trunc(w) || 0);
|
|
35328
|
-
return
|
|
37101
|
+
return clip2(line, width).padEnd(width);
|
|
35329
37102
|
}
|
|
35330
37103
|
function box({ title = "", sections = [], footer, width = 40 } = {}) {
|
|
35331
37104
|
const w = Math.max(MIN_WIDTH, Math.trunc(width) || MIN_WIDTH);
|
|
35332
37105
|
const inner = w - 4;
|
|
35333
37106
|
const lines = [];
|
|
35334
|
-
const titleText = title ? ` ${
|
|
37107
|
+
const titleText = title ? ` ${clip2(title, Math.max(0, inner - 2))} ` : "";
|
|
35335
37108
|
const topFill = Math.max(0, w - 2 - 1 - titleText.length);
|
|
35336
37109
|
lines.push(active.tl + active.h + titleText + active.h.repeat(topFill) + active.tr);
|
|
35337
37110
|
const framed = (text) => `${active.v} ${pad(text, inner)} ${active.v}`;
|
|
@@ -35351,14 +37124,14 @@ function box({ title = "", sections = [], footer, width = 40 } = {}) {
|
|
|
35351
37124
|
function meter(reserved, cap, width = 24, state = "ok") {
|
|
35352
37125
|
const r = Number.isFinite(reserved) ? Math.max(0, Math.trunc(reserved)) : 0;
|
|
35353
37126
|
if (!Number.isInteger(cap) || cap <= 0) {
|
|
35354
|
-
return
|
|
37127
|
+
return clip2(`${r} / ? (cap unknown)`, width);
|
|
35355
37128
|
}
|
|
35356
37129
|
const tag = state === "soft-hold" ? " soft-hold" : state === "over" ? " over" : "";
|
|
35357
37130
|
const label = ` ${r}/${cap}${tag}`;
|
|
35358
37131
|
const barCells = Math.max(0, Math.trunc(width) - label.length - 2);
|
|
35359
37132
|
const filled = Math.min(barCells, Math.round(Math.min(r, cap) / cap * barCells));
|
|
35360
37133
|
const bar = `[${active.full.repeat(filled)}${active.empty.repeat(barCells - filled)}]`;
|
|
35361
|
-
return
|
|
37134
|
+
return clip2(bar + label, width);
|
|
35362
37135
|
}
|
|
35363
37136
|
function sparkline(values, width = 24, opts = {}) {
|
|
35364
37137
|
const w = Math.max(0, Math.trunc(width) || 0);
|
|
@@ -35366,7 +37139,7 @@ function sparkline(values, width = 24, opts = {}) {
|
|
|
35366
37139
|
const norm = (Array.isArray(values) ? values : []).map((v) => Number.isFinite(v) ? Math.max(0, v) : null);
|
|
35367
37140
|
const finite = norm.filter((v) => v !== null);
|
|
35368
37141
|
const ceiling = Number.isFinite(opts?.max) && opts.max > 0 ? opts.max : Math.max(0, ...finite);
|
|
35369
|
-
if (finite.length === 0 || ceiling <= 0) return
|
|
37142
|
+
if (finite.length === 0 || ceiling <= 0) return clip2("no data", w);
|
|
35370
37143
|
let cells = norm;
|
|
35371
37144
|
if (norm.length > w) {
|
|
35372
37145
|
cells = [];
|
|
@@ -35544,9 +37317,9 @@ var init_sandbox_store = __esm({
|
|
|
35544
37317
|
});
|
|
35545
37318
|
|
|
35546
37319
|
// ../worker/src/sandbox.mjs
|
|
35547
|
-
import { execFile as
|
|
37320
|
+
import { execFile as execFile4, spawn } from "node:child_process";
|
|
35548
37321
|
import { existsSync as existsSync2 } from "node:fs";
|
|
35549
|
-
import { promisify as
|
|
37322
|
+
import { promisify as promisify4 } from "node:util";
|
|
35550
37323
|
function sandboxContainerName(jobId) {
|
|
35551
37324
|
return `${SANDBOX_NAME_PREFIX}${sanitizeJobId(jobId)}`;
|
|
35552
37325
|
}
|
|
@@ -35591,14 +37364,14 @@ function launchSandbox({ args, spawnFn = spawn }) {
|
|
|
35591
37364
|
child.on("close", (code) => resolve({ code }));
|
|
35592
37365
|
});
|
|
35593
37366
|
}
|
|
35594
|
-
var
|
|
37367
|
+
var exec3, SANDBOX_NAME_PREFIX;
|
|
35595
37368
|
var init_sandbox = __esm({
|
|
35596
37369
|
"../worker/src/sandbox.mjs"() {
|
|
35597
37370
|
init_config2();
|
|
35598
37371
|
init_docker_run();
|
|
35599
37372
|
init_run_history();
|
|
35600
37373
|
init_sandbox_store();
|
|
35601
|
-
|
|
37374
|
+
exec3 = promisify4(execFile4);
|
|
35602
37375
|
SANDBOX_NAME_PREFIX = "pi-sandbox-";
|
|
35603
37376
|
}
|
|
35604
37377
|
});
|
|
@@ -35624,8 +37397,8 @@ function renderRuns(runs) {
|
|
|
35624
37397
|
const headers = RUN_COLUMNS.map((c) => c.header);
|
|
35625
37398
|
const rows = list.map((r) => RUN_COLUMNS.map((c) => c.derive ? c.derive(r) : cell(r?.[c.key])));
|
|
35626
37399
|
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((row) => row[i].length)));
|
|
35627
|
-
const
|
|
35628
|
-
return [
|
|
37400
|
+
const fmt2 = (cells) => cells.map((v, i) => v.padEnd(widths[i])).join(" ").trimEnd();
|
|
37401
|
+
return [fmt2(headers), ...rows.map(fmt2)].join("\n");
|
|
35629
37402
|
}
|
|
35630
37403
|
function renderBudget({ budget, settings: settings2 } = {}) {
|
|
35631
37404
|
if (!budget || budget.unreachable) {
|
|
@@ -35802,8 +37575,8 @@ function renderWhatIf(result, { flow, target } = {}) {
|
|
|
35802
37575
|
}
|
|
35803
37576
|
function tableLines(headers, rows) {
|
|
35804
37577
|
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((row) => row[i].length)));
|
|
35805
|
-
const
|
|
35806
|
-
return [
|
|
37578
|
+
const fmt2 = (cells) => cells.map((v, i) => v.padEnd(widths[i])).join(" ").trimEnd();
|
|
37579
|
+
return [fmt2(headers), ...rows.map(fmt2)];
|
|
35807
37580
|
}
|
|
35808
37581
|
function renderSettingsView(settings2) {
|
|
35809
37582
|
const path = settings2?.path ?? "(unknown path)";
|
|
@@ -35816,6 +37589,105 @@ function renderSettingsView(settings2) {
|
|
|
35816
37589
|
}
|
|
35817
37590
|
return out.join("\n");
|
|
35818
37591
|
}
|
|
37592
|
+
function renderGraph(model) {
|
|
37593
|
+
const m = model ?? {};
|
|
37594
|
+
if (m.meta?.triggersMissing) return "Graph: no triggers file found";
|
|
37595
|
+
if (typeof m.meta?.triggersInvalid === "string" && m.meta.triggersInvalid !== "") return `Graph: triggers file invalid: ${m.meta.triggersInvalid}`;
|
|
37596
|
+
const nodes = Array.isArray(m.nodes) ? m.nodes : [];
|
|
37597
|
+
const nodeById = new Map(nodes.map((n) => [n.id, n]));
|
|
37598
|
+
const edges = Array.isArray(m.edges) ? m.edges : [];
|
|
37599
|
+
const flagsByNode = /* @__PURE__ */ new Map();
|
|
37600
|
+
for (const f of Array.isArray(m.flags) ? m.flags : []) {
|
|
37601
|
+
if (!flagsByNode.has(f.nodeId)) flagsByNode.set(f.nodeId, []);
|
|
37602
|
+
flagsByNode.get(f.nodeId).push(f.flag);
|
|
37603
|
+
}
|
|
37604
|
+
const out = ["Graph: triggers and flows"];
|
|
37605
|
+
const triggerCount = nodes.filter((n) => n.kind === "trigger").length;
|
|
37606
|
+
if (triggerCount === 0) out.push("", "no triggers configured");
|
|
37607
|
+
for (const group of Array.isArray(m.folders) ? m.folders : []) {
|
|
37608
|
+
if ((group.triggerIds?.length ?? 0) === 0 && (group.skillIds?.length ?? 0) === 0) continue;
|
|
37609
|
+
out.push("", groupHeading(group));
|
|
37610
|
+
for (const id of group.triggerIds ?? []) {
|
|
37611
|
+
const t = nodeById.get(id);
|
|
37612
|
+
if (t) out.push(` ${graphTriggerLine(t, flagsByNode.get(id) ?? [])}`);
|
|
37613
|
+
}
|
|
37614
|
+
for (const id of group.skillIds ?? []) {
|
|
37615
|
+
const s = nodeById.get(id);
|
|
37616
|
+
if (!s || s.kind === "skill-unverified") continue;
|
|
37617
|
+
out.push(` ${graphSkillLine(s, flagsByNode.get(id) ?? [])}`);
|
|
37618
|
+
for (const e of edges) {
|
|
37619
|
+
if (e.from !== id || e.kind === "cron-rearm") continue;
|
|
37620
|
+
const target = nodeById.get(e.to);
|
|
37621
|
+
out.push(` ${graphEdgeLine(e, target)}`);
|
|
37622
|
+
}
|
|
37623
|
+
}
|
|
37624
|
+
}
|
|
37625
|
+
const injected = nodes.filter((n) => n.kind === "injected");
|
|
37626
|
+
if (injected.length > 0) {
|
|
37627
|
+
out.push("", "injected skills (trigger-reachable, never AI-reachable):");
|
|
37628
|
+
for (const n of injected) {
|
|
37629
|
+
const marks = (flagsByNode.get(n.id) ?? []).includes("injected-ai-trigger") ? " [ai-trigger: allow is a silent no-op here]" : "";
|
|
37630
|
+
out.push(` ${n.name} (${n.dir})${marks}`);
|
|
37631
|
+
}
|
|
37632
|
+
}
|
|
37633
|
+
out.push("");
|
|
37634
|
+
if ((m.meta?.unattributedRuns ?? 0) > 0) out.push(`${m.meta.unattributedRuns} runs unattributed (triggers file changed since they ran, or they predate attribution)`);
|
|
37635
|
+
const refusals = Object.entries(m.meta?.chainRefusals ?? {});
|
|
37636
|
+
if (refusals.length > 0) out.push(`chain refusals: ${refusals.map(([flow, n]) => `${flow} ${n}`).join(", ")}`);
|
|
37637
|
+
const trunc = m.meta?.truncated ?? {};
|
|
37638
|
+
if (trunc.folders) out.push("folder scan truncated (cap reached): unlisted folders are not empty, they are unscanned");
|
|
37639
|
+
if (trunc.skills) out.push("skill enumeration truncated or partly unread");
|
|
37640
|
+
if (trunc.edges) out.push("observed edges truncated (cap reached)");
|
|
37641
|
+
if ((m.meta?.droppedObservedEdges ?? 0) > 0) out.push(`${m.meta.droppedObservedEdges} observed edges dropped (no unique readable folder to attach them to)`);
|
|
37642
|
+
if ((m.meta?.injectedUnreachable ?? []).length > 0) out.push(`injected skills dir unreadable: ${m.meta.injectedUnreachable.join(", ")}`);
|
|
37643
|
+
out.push("edges: -> configured, observed xN (from records), mention (potential; a mention is not a promise)");
|
|
37644
|
+
out.push(capsLine(m.caps));
|
|
37645
|
+
return out.join("\n");
|
|
37646
|
+
}
|
|
37647
|
+
function groupHeading(group) {
|
|
37648
|
+
if (group.kind === "forge") {
|
|
37649
|
+
const seen = Array.isArray(group.repos) && group.repos.length > 0 ? ` -- ran against: ${group.repos.join(", ")}` : "";
|
|
37650
|
+
return `forge ${group.label}${seen} (skills unverifiable from the admin host)`;
|
|
37651
|
+
}
|
|
37652
|
+
const head = group.head ? `, HEAD ${String(group.head).slice(0, 7)}` : "";
|
|
37653
|
+
const state = group.unreachable ? `, ${group.unreachable}` : "";
|
|
37654
|
+
return `folder ${group.path ?? group.label}${head}${state}`;
|
|
37655
|
+
}
|
|
37656
|
+
function graphTriggerLine(t, flags) {
|
|
37657
|
+
const stats = t.runs > 0 ? `runs ${t.runs}${t.lastOutcome ? `, last ${t.lastOutcome}` : ""}` : "no runs in window";
|
|
37658
|
+
const badges = [];
|
|
37659
|
+
if (flags.includes("no-skill")) badges.push("[no-skill: flow absent at HEAD]");
|
|
37660
|
+
if (flags.includes("charset-invalid")) badges.push("[invalid flow name: can never materialise]");
|
|
37661
|
+
if (flags.includes("pr-spend-loop-risk")) badges.push("[spend-loop risk: fires on opened/synchronize]");
|
|
37662
|
+
const replicas = t.replicas ? ` x${t.replicas}` : "";
|
|
37663
|
+
return `${t.onType} ${t.label} -> ${t.flow ?? "(no flow)"}${replicas} (${stats})${badges.length ? ` ${badges.join(" ")}` : ""}`;
|
|
37664
|
+
}
|
|
37665
|
+
function graphSkillLine(s, flags) {
|
|
37666
|
+
const badges = [];
|
|
37667
|
+
if (s.kind === "skill-missing") badges.push("[missing at HEAD]");
|
|
37668
|
+
if (s.isSub) badges.push(`[sub-skill of ${s.group}: loadable, never a flow]`);
|
|
37669
|
+
if (s.aiTrigger) badges.push("[chainable]");
|
|
37670
|
+
for (const loop of Array.isArray(s.loops) ? s.loops : []) badges.push(`[loop: "${loop.hint}"]`);
|
|
37671
|
+
if (flags.includes("orphan")) badges.push("[orphan: no trigger, no ai-trigger, no mention]");
|
|
37672
|
+
if (flags.includes("ai-reachable-no-trigger")) badges.push("[AI-reachable, no trigger]");
|
|
37673
|
+
if (flags.includes("unread")) badges.push("[SKILL.md unread: facts unknown]");
|
|
37674
|
+
return `skill ${s.name}${badges.length ? ` ${badges.join(" ")}` : ""}`;
|
|
37675
|
+
}
|
|
37676
|
+
function graphEdgeLine(e, target) {
|
|
37677
|
+
const name = target?.name ?? "(unknown)";
|
|
37678
|
+
if (e.kind === "observed") return `-> ${name} observed x${e.count}`;
|
|
37679
|
+
if (e.kind === "potential") {
|
|
37680
|
+
const fire = e.eligible ? "could fire" : "can never fire: target has no ai-trigger allow";
|
|
37681
|
+
return `-> ${name} mention (potential${e.strong ? ", strong" : ""}; ${fire})`;
|
|
37682
|
+
}
|
|
37683
|
+
return `-> ${name} ${e.kind}`;
|
|
37684
|
+
}
|
|
37685
|
+
function capsLine(caps) {
|
|
37686
|
+
const depth = caps?.chainDepthMax ?? "?";
|
|
37687
|
+
const width = caps?.chainMaxPerJob ?? "?";
|
|
37688
|
+
const window = caps?.windowDays ?? "?";
|
|
37689
|
+
return `caps: chain depth <= ${depth}, <= ${width} per job, same folder only, window ${window}d`;
|
|
37690
|
+
}
|
|
35819
37691
|
var SETTINGS_KEYS, BUDGET_WINDOWS, RUN_COLUMNS, COSTS_WINDOW_LABELS;
|
|
35820
37692
|
var init_render = __esm({
|
|
35821
37693
|
"src/render.mjs"() {
|
|
@@ -36035,8 +37907,8 @@ var init_style = __esm({
|
|
|
36035
37907
|
inverse: (t) => t,
|
|
36036
37908
|
strikethrough: (t) => t
|
|
36037
37909
|
};
|
|
36038
|
-
OVERLAY_GLYPHS = { tl: "\u250C", tr: "\u2510", bl: "\u2514", br: "\u2518", ml: "\u251C", mr: "\u2524", h: "\u2500", v: "\u2502", full: "\u2588", empty: "\u2591", ellipsis: "\u2026" };
|
|
36039
|
-
OVERLAY_ASCII = { tl: "+", tr: "+", bl: "+", br: "+", ml: "+", mr: "+", h: "-", v: "|", full: "#", empty: ".", ellipsis: "..." };
|
|
37910
|
+
OVERLAY_GLYPHS = { tl: "\u250C", tr: "\u2510", bl: "\u2514", br: "\u2518", ml: "\u251C", mr: "\u2524", h: "\u2500", v: "\u2502", full: "\u2588", empty: "\u2591", ellipsis: "\u2026", arrowRight: "\u2500\u25B6", foldOpen: "\u25BE", foldClosed: "\u25B8", rearm: "\u21BB" };
|
|
37911
|
+
OVERLAY_ASCII = { tl: "+", tr: "+", bl: "+", br: "+", ml: "+", mr: "+", h: "-", v: "|", full: "#", empty: ".", ellipsis: "...", arrowRight: "->", foldOpen: "v", foldClosed: ">", rearm: "~" };
|
|
36040
37912
|
COST_COLORS = {
|
|
36041
37913
|
metered: "text",
|
|
36042
37914
|
plan: "accent",
|
|
@@ -36112,6 +37984,30 @@ function createDashboardDeps(paths) {
|
|
|
36112
37984
|
const fold = foldCosts({ records, subscriptions, pricing: pricing_exports, nowMs, piAiPin: piAiVersion() });
|
|
36113
37985
|
return { fold, records, subscriptions };
|
|
36114
37986
|
},
|
|
37987
|
+
/**
|
|
37988
|
+
* One GRAPH read (issue #54): triggers FRESH (OQ-008 -- a cached topology is a stale topology),
|
|
37989
|
+
* one bounded record scan, the folder/injected enumerations, and the pure fold. Schedulers ride
|
|
37990
|
+
* in from the caller's snapshot so this seam opens no second queue read. All fs/git access lives
|
|
37991
|
+
* in the read-model functions this calls; this module still touches nothing itself.
|
|
37992
|
+
*/
|
|
37993
|
+
fetchGraph({ schedulers } = {}) {
|
|
37994
|
+
const nowMs = Date.now();
|
|
37995
|
+
const triggersView = readTriggers({ triggersPath: paths.triggersPath });
|
|
37996
|
+
const triggerList2 = Array.isArray(triggersView?.triggers) ? triggersView.triggers : [];
|
|
37997
|
+
const records = scanRunRecords({ logsDir: paths.logsDir, sinceMs: nowMs - GRAPH_LIMITS.windowDays * 24 * 60 * 60 * 1e3, nowMs });
|
|
37998
|
+
const recs = Array.isArray(records) ? records : [];
|
|
37999
|
+
return buildGraphModel({
|
|
38000
|
+
triggers: triggersView,
|
|
38001
|
+
schedulers: Array.isArray(schedulers) ? schedulers : [],
|
|
38002
|
+
...collectGraphInputs({ triggers: triggerList2 }),
|
|
38003
|
+
cronStats: cronRunStats({ records: recs, schedulerIds: triggerList2.filter((t) => t.type === "cron" && typeof t.id === "string").map((t) => t.id) }),
|
|
38004
|
+
runJoin: joinRunsToTriggers({ records: recs, triggerCount: triggersView?.count, triggerTypes: Object.fromEntries(triggerList2.map((t) => [t.index, t.type])) }),
|
|
38005
|
+
chainEdges: observedChainEdges({ records: recs }),
|
|
38006
|
+
forgeRepos: forgeRepoTargets({ records: recs }),
|
|
38007
|
+
caps: { chainDepthMax: paths.chainDepthMax, chainMaxPerJob: paths.chainMaxPerJob, windowDays: GRAPH_LIMITS.windowDays },
|
|
38008
|
+
nowMs
|
|
38009
|
+
});
|
|
38010
|
+
},
|
|
36115
38011
|
/** The priced-model catalog for the what-if `/` filter -- the façade stays behind this seam. */
|
|
36116
38012
|
listPricedModels() {
|
|
36117
38013
|
return listPricedModels();
|
|
@@ -36146,7 +38042,7 @@ function makeDashboard({
|
|
|
36146
38042
|
intervalMs = REFRESH_MS,
|
|
36147
38043
|
deps = createDashboardDeps(paths)
|
|
36148
38044
|
} = {}) {
|
|
36149
|
-
const styler = makeStyler(theme);
|
|
38045
|
+
const styler = makeStyler(theme, { ascii: paths?.asciiGlyphs === true });
|
|
36150
38046
|
let snapshot = null;
|
|
36151
38047
|
let fetching = false;
|
|
36152
38048
|
let disposed = false;
|
|
@@ -36156,6 +38052,7 @@ function makeDashboard({
|
|
|
36156
38052
|
let detailRun = null;
|
|
36157
38053
|
let detailSandbox = null;
|
|
36158
38054
|
let detailTrigger = null;
|
|
38055
|
+
let detailReturnTo = "LIST";
|
|
36159
38056
|
let tailJobId = null;
|
|
36160
38057
|
let tail = null;
|
|
36161
38058
|
let tailTop = 0;
|
|
@@ -36169,6 +38066,9 @@ function makeDashboard({
|
|
|
36169
38066
|
let costs = { data: null, windowKey: "mtd", fetchedAt: 0, table: "flow", whatIf: null };
|
|
36170
38067
|
let costsSel = 0;
|
|
36171
38068
|
let costsFetching = false;
|
|
38069
|
+
let graph = { model: null, error: null, fetchedAt: 0, folded: /* @__PURE__ */ new Set() };
|
|
38070
|
+
let graphSel = 0;
|
|
38071
|
+
let graphFetching = false;
|
|
36172
38072
|
const refresh = async () => {
|
|
36173
38073
|
if (fetching || disposed) return;
|
|
36174
38074
|
fetching = true;
|
|
@@ -36209,6 +38109,20 @@ function makeDashboard({
|
|
|
36209
38109
|
tui?.requestRender?.();
|
|
36210
38110
|
}
|
|
36211
38111
|
};
|
|
38112
|
+
const fetchGraphNow = async () => {
|
|
38113
|
+
if (typeof deps?.fetchGraph !== "function" || graphFetching || disposed) return;
|
|
38114
|
+
graphFetching = true;
|
|
38115
|
+
try {
|
|
38116
|
+
graph.model = await deps.fetchGraph({ schedulers: snapshot?.schedulers ?? [] });
|
|
38117
|
+
graph.error = null;
|
|
38118
|
+
} catch (err) {
|
|
38119
|
+
graph.error = err?.message ?? String(err);
|
|
38120
|
+
} finally {
|
|
38121
|
+
graph.fetchedAt = Date.now();
|
|
38122
|
+
graphFetching = false;
|
|
38123
|
+
tui?.requestRender?.();
|
|
38124
|
+
}
|
|
38125
|
+
};
|
|
36212
38126
|
const computeWhatIf = () => {
|
|
36213
38127
|
const wi = costs.whatIf;
|
|
36214
38128
|
if (!wi) return;
|
|
@@ -36249,6 +38163,9 @@ function makeDashboard({
|
|
|
36249
38163
|
if (view === "COSTS" && costsSel > costsTableRows(costs).length - 1) {
|
|
36250
38164
|
costsSel = Math.max(0, costsTableRows(costs).length - 1);
|
|
36251
38165
|
}
|
|
38166
|
+
if (view === "GRAPH" && graphSel > graphRows(graph.model, graph.folded).length - 1) {
|
|
38167
|
+
graphSel = Math.max(0, graphRows(graph.model, graph.folded).length - 1);
|
|
38168
|
+
}
|
|
36252
38169
|
return renderPanel(snapshot, width, {
|
|
36253
38170
|
view,
|
|
36254
38171
|
selected,
|
|
@@ -36269,6 +38186,9 @@ function makeDashboard({
|
|
|
36269
38186
|
costs,
|
|
36270
38187
|
costsSel,
|
|
36271
38188
|
costsAvailable: typeof deps?.fetchCosts === "function",
|
|
38189
|
+
graph,
|
|
38190
|
+
graphSel,
|
|
38191
|
+
graphAvailable: typeof deps?.fetchGraph === "function",
|
|
36272
38192
|
copiedNote,
|
|
36273
38193
|
copyAvailable: typeof deps?.copyText === "function",
|
|
36274
38194
|
// Height through the injected seam, read per frame (a resize changes it): null (seam absent, or
|
|
@@ -36338,7 +38258,7 @@ function makeDashboard({
|
|
|
36338
38258
|
return;
|
|
36339
38259
|
}
|
|
36340
38260
|
if (matchesKey(data, "escape")) {
|
|
36341
|
-
view =
|
|
38261
|
+
view = detailReturnTo;
|
|
36342
38262
|
detailTrigger = null;
|
|
36343
38263
|
tui?.requestRender?.();
|
|
36344
38264
|
return;
|
|
@@ -36519,6 +38439,45 @@ function makeDashboard({
|
|
|
36519
38439
|
}
|
|
36520
38440
|
return;
|
|
36521
38441
|
}
|
|
38442
|
+
if (view === "GRAPH") {
|
|
38443
|
+
if (matchesKey(data, "escape")) {
|
|
38444
|
+
view = "LIST";
|
|
38445
|
+
tui?.requestRender?.();
|
|
38446
|
+
return;
|
|
38447
|
+
}
|
|
38448
|
+
if (matchesKey(data, "up") || matchesKey(data, "down")) {
|
|
38449
|
+
const step = matchesKey(data, "up") ? -1 : 1;
|
|
38450
|
+
graphSel = Math.min(Math.max(0, graphRows(graph.model, graph.folded).length - 1), Math.max(0, graphSel + step));
|
|
38451
|
+
tui?.requestRender?.();
|
|
38452
|
+
return;
|
|
38453
|
+
}
|
|
38454
|
+
if (data === "\r" || data === "\n") {
|
|
38455
|
+
const row = graphRows(graph.model, graph.folded)[graphSel];
|
|
38456
|
+
if (!row) return;
|
|
38457
|
+
if (row.kind === "folder") {
|
|
38458
|
+
if (graph.folded.has(row.key)) graph.folded.delete(row.key);
|
|
38459
|
+
else graph.folded.add(row.key);
|
|
38460
|
+
tui?.requestRender?.();
|
|
38461
|
+
return;
|
|
38462
|
+
}
|
|
38463
|
+
if (row.kind === "gtrigger") {
|
|
38464
|
+
const record = (snapshot?.triggers?.triggers ?? []).find((t) => t?.index === row.node.index);
|
|
38465
|
+
if (!record) return;
|
|
38466
|
+
detailTrigger = { record, index: row.node.index };
|
|
38467
|
+
pendingDelete = false;
|
|
38468
|
+
detailReturnTo = "GRAPH";
|
|
38469
|
+
view = "TRIGGER_DETAIL";
|
|
38470
|
+
tui?.requestRender?.();
|
|
38471
|
+
return;
|
|
38472
|
+
}
|
|
38473
|
+
return;
|
|
38474
|
+
}
|
|
38475
|
+
if (data === "r" || data === "R") {
|
|
38476
|
+
void fetchGraphNow();
|
|
38477
|
+
return;
|
|
38478
|
+
}
|
|
38479
|
+
return;
|
|
38480
|
+
}
|
|
36522
38481
|
if (matchesKey(data, "escape") || data === "q" || data === "Q") {
|
|
36523
38482
|
void dispose().finally(() => done(void 0));
|
|
36524
38483
|
return;
|
|
@@ -36530,6 +38489,7 @@ function makeDashboard({
|
|
|
36530
38489
|
if (row.kind === "trigger") {
|
|
36531
38490
|
detailTrigger = { record: row.trigger, index: row.index };
|
|
36532
38491
|
pendingDelete = false;
|
|
38492
|
+
detailReturnTo = "LIST";
|
|
36533
38493
|
view = "TRIGGER_DETAIL";
|
|
36534
38494
|
tui?.requestRender?.();
|
|
36535
38495
|
} else if (row.kind === "active") {
|
|
@@ -36563,6 +38523,13 @@ function makeDashboard({
|
|
|
36563
38523
|
tui?.requestRender?.();
|
|
36564
38524
|
return;
|
|
36565
38525
|
}
|
|
38526
|
+
if (data === "g" || data === "G") {
|
|
38527
|
+
view = "GRAPH";
|
|
38528
|
+
graphSel = 0;
|
|
38529
|
+
void fetchGraphNow();
|
|
38530
|
+
tui?.requestRender?.();
|
|
38531
|
+
return;
|
|
38532
|
+
}
|
|
36566
38533
|
if (data === " ") {
|
|
36567
38534
|
const rows = buildRows(snapshot, runSort);
|
|
36568
38535
|
if (rows.length === 0) return;
|
|
@@ -36631,7 +38598,7 @@ function budgetMeters(budget, settings2, width) {
|
|
|
36631
38598
|
return out;
|
|
36632
38599
|
}
|
|
36633
38600
|
function renderPanel(snapshot, width, state, styler) {
|
|
36634
|
-
const { view, selected, detailRun, detailTrigger, tailJobId, tail, tailTop, tailFollow, tailAvailable, tailSearchInput, tailQuery, tailMatchLine, detailSandbox, sandboxAvailable, pendingDelete, runSort, costs, costsSel, costsAvailable, copiedNote, copyAvailable, terminalRows } = state;
|
|
38601
|
+
const { view, selected, detailRun, detailTrigger, tailJobId, tail, tailTop, tailFollow, tailAvailable, tailSearchInput, tailQuery, tailMatchLine, detailSandbox, sandboxAvailable, pendingDelete, runSort, costs, costsSel, costsAvailable, graph, graphSel, graphAvailable, copiedNote, copyAvailable, terminalRows } = state;
|
|
36635
38602
|
const framed = Number.isFinite(width) && Math.trunc(width) >= MIN_WIDTH2;
|
|
36636
38603
|
const inner = Math.trunc(width) - 4;
|
|
36637
38604
|
const title = "pi-dispatch";
|
|
@@ -36668,6 +38635,9 @@ function renderPanel(snapshot, width, state, styler) {
|
|
|
36668
38635
|
if (view === "COSTS") {
|
|
36669
38636
|
return renderCosts2({ costs, costsSel, costsAvailable, framed, width: Math.trunc(width), styler, availableRows: terminalRows });
|
|
36670
38637
|
}
|
|
38638
|
+
if (view === "GRAPH") {
|
|
38639
|
+
return renderGraphView({ graph, graphSel, graphAvailable, framed, width: Math.trunc(width), styler });
|
|
38640
|
+
}
|
|
36671
38641
|
if (snapshot === null) {
|
|
36672
38642
|
if (!framed) return [`${title} -- loading`, "", KEY_HINTS];
|
|
36673
38643
|
return box({ title, sections: [{ lines: ["loading"] }], footer: KEY_HINTS, width });
|
|
@@ -37094,7 +39064,162 @@ function triggerDetailHints(inner, styler, pendingDelete = false) {
|
|
|
37094
39064
|
}
|
|
37095
39065
|
function keyHints(inner, styler) {
|
|
37096
39066
|
const k = (key, label) => styler.fg("accent", key) + " " + styler.fg("dim", label);
|
|
37097
|
-
const hints = [k("\u2191\u2193
|
|
39067
|
+
const hints = [k("\u2191\u2193", "open"), k("a", "add"), k("w", "pauses"), k("l", "logs"), k("c", "costs"), k("g", "graph"), k("p/r", "pause"), k("q", "quit")].join(styler.fg("dim", " \xB7 "));
|
|
39068
|
+
return fitLine(hints, inner, styler);
|
|
39069
|
+
}
|
|
39070
|
+
function graphRows(model, folded) {
|
|
39071
|
+
if (!model) return [];
|
|
39072
|
+
const nodeById = new Map((model.nodes ?? []).map((n) => [n.id, n]));
|
|
39073
|
+
const flagsByNode = /* @__PURE__ */ new Map();
|
|
39074
|
+
for (const f of model.flags ?? []) {
|
|
39075
|
+
if (!flagsByNode.has(f.nodeId)) flagsByNode.set(f.nodeId, []);
|
|
39076
|
+
flagsByNode.get(f.nodeId).push(f.flag);
|
|
39077
|
+
}
|
|
39078
|
+
const rows = [];
|
|
39079
|
+
for (const group of model.folders ?? []) {
|
|
39080
|
+
if ((group.triggerIds?.length ?? 0) === 0 && (group.skillIds?.length ?? 0) === 0) continue;
|
|
39081
|
+
const isFolded = folded?.has?.(group.key) === true;
|
|
39082
|
+
rows.push({ kind: "folder", key: group.key, group, folded: isFolded });
|
|
39083
|
+
if (isFolded) continue;
|
|
39084
|
+
for (const id of group.triggerIds ?? []) {
|
|
39085
|
+
const node = nodeById.get(id);
|
|
39086
|
+
if (node) rows.push({ kind: "gtrigger", node, flags: flagsByNode.get(id) ?? [] });
|
|
39087
|
+
}
|
|
39088
|
+
for (const id of group.skillIds ?? []) {
|
|
39089
|
+
const node = nodeById.get(id);
|
|
39090
|
+
if (!node || node.kind === "skill-unverified") continue;
|
|
39091
|
+
rows.push({ kind: "gskill", node, flags: flagsByNode.get(id) ?? [] });
|
|
39092
|
+
for (const e of model.edges ?? []) {
|
|
39093
|
+
if (e.from !== id || e.kind === "cron-rearm") continue;
|
|
39094
|
+
rows.push({ kind: "gedge", edge: e, target: nodeById.get(e.to) });
|
|
39095
|
+
}
|
|
39096
|
+
}
|
|
39097
|
+
}
|
|
39098
|
+
for (const n of model.nodes ?? []) {
|
|
39099
|
+
if (n.kind === "injected") rows.push({ kind: "ginjected", node: n, flags: flagsByNode.get(n.id) ?? [] });
|
|
39100
|
+
}
|
|
39101
|
+
return rows;
|
|
39102
|
+
}
|
|
39103
|
+
function graphRowLine(row, cursor, inner, styler) {
|
|
39104
|
+
const G = styler.glyphs;
|
|
39105
|
+
const pre = cursor ? styler.fg("accent", "\u203A ") : " ";
|
|
39106
|
+
if (row.kind === "folder") {
|
|
39107
|
+
const g = row.group;
|
|
39108
|
+
const fold = styler.fg("accent", row.folded ? G.foldClosed : G.foldOpen);
|
|
39109
|
+
const label = g.kind === "forge" ? `forge ${g.label}` : `folder ${g.path ?? g.label}`;
|
|
39110
|
+
const seen = g.kind === "forge" && Array.isArray(g.repos) && g.repos.length > 0 ? styler.fg("dim", ` \xB7 ran against ${g.repos.join(", ")}`) : "";
|
|
39111
|
+
const state = g.unreachable ? styler.fg("warning", ` \xB7 ${g.kind === "forge" ? "skills unverifiable from this host" : g.unreachable}`) : g.head ? styler.fg("dim", ` \xB7 HEAD ${String(g.head).slice(0, 7)}`) : "";
|
|
39112
|
+
return fitLine(pre + fold + " " + styler.fg("accent", label) + seen + state, inner, styler);
|
|
39113
|
+
}
|
|
39114
|
+
if (row.kind === "gtrigger") {
|
|
39115
|
+
const t = row.node;
|
|
39116
|
+
const kind = styler.cell(t.onType, KIND_WIDTH, { color: KIND_COLOR[t.onType] ?? "text" });
|
|
39117
|
+
const stats = t.runs > 0 ? `runs ${t.runs}${t.lastOutcome ? ` \xB7 last ${t.lastOutcome}` : ""}` : "no runs in window";
|
|
39118
|
+
const statColor = t.lastOutcome === "completed" ? "success" : t.lastOutcome ? "warning" : "dim";
|
|
39119
|
+
const badges = graphBadges(row.flags, styler);
|
|
39120
|
+
const left = pre + kind + styler.stripAnsi(t.label ?? "") + " " + styler.fg("dim", G.arrowRight) + " " + (t.flow ?? "(no flow)") + (t.replicas ? styler.fg("warning", ` x${t.replicas}`) : "") + badges;
|
|
39121
|
+
const right = styler.fg(statColor, stats);
|
|
39122
|
+
return joinEnds(left, right, inner, styler);
|
|
39123
|
+
}
|
|
39124
|
+
if (row.kind === "gskill") {
|
|
39125
|
+
const s = row.node;
|
|
39126
|
+
const missing = s.kind === "skill-missing";
|
|
39127
|
+
const name = missing ? styler.fg("error", s.name) : s.name;
|
|
39128
|
+
const marks = [];
|
|
39129
|
+
if (missing) marks.push(styler.fg("error", "[missing at HEAD]"));
|
|
39130
|
+
if (s.isSub) marks.push(styler.fg("dim", `[sub of ${s.group}]`));
|
|
39131
|
+
if (s.aiTrigger) marks.push(styler.fg("success", "[chainable]"));
|
|
39132
|
+
for (const loop of Array.isArray(s.loops) ? s.loops : []) marks.push(styler.fg("accent", `${G.rearm} "${loop.hint}"`));
|
|
39133
|
+
const badges = graphBadges(row.flags, styler);
|
|
39134
|
+
return fitLine(pre + styler.fg("dim", "skill ") + name + (marks.length ? " " + marks.join(" ") : "") + badges, inner, styler);
|
|
39135
|
+
}
|
|
39136
|
+
if (row.kind === "gedge") {
|
|
39137
|
+
const e = row.edge;
|
|
39138
|
+
const name = row.target?.name ?? "(unknown)";
|
|
39139
|
+
const label = e.kind === "observed" ? styler.fg("accent", `observed x${e.count}`) : e.kind === "potential" ? styler.fg("dim", `mention (potential${e.strong ? ", strong" : ""}; ${e.eligible ? "could fire" : "can never fire"})`) : styler.fg("dim", e.kind);
|
|
39140
|
+
return fitLine(pre + " " + styler.fg("dim", `${G.bl} ${G.arrowRight} `) + name + " " + label, inner, styler);
|
|
39141
|
+
}
|
|
39142
|
+
if (row.kind === "ginjected") {
|
|
39143
|
+
const warn = row.flags.includes("injected-ai-trigger") ? " " + styler.fg("error", "[ai-trigger is a silent no-op here]") : "";
|
|
39144
|
+
return fitLine(pre + styler.fg("dim", "injected ") + row.node.name + styler.fg("dim", ` (${row.node.dir})`) + warn, inner, styler);
|
|
39145
|
+
}
|
|
39146
|
+
return fitLine(pre, inner, styler);
|
|
39147
|
+
}
|
|
39148
|
+
function graphBadges(flags, styler) {
|
|
39149
|
+
const parts = [];
|
|
39150
|
+
for (const flag of flags ?? []) {
|
|
39151
|
+
if (flag === "no-skill") parts.push(styler.fg("error", "[no-skill]"));
|
|
39152
|
+
else if (flag === "charset-invalid") parts.push(styler.fg("error", "[invalid name]"));
|
|
39153
|
+
else if (flag === "orphan") parts.push(styler.fg("warning", "[orphan]"));
|
|
39154
|
+
else if (flag === "ai-reachable-no-trigger") parts.push(styler.fg("warning", "[AI-reachable, no trigger]"));
|
|
39155
|
+
else if (flag === "unread") parts.push(styler.fg("warning", "[unread]"));
|
|
39156
|
+
else if (flag === "pr-spend-loop-risk") parts.push(styler.fg("error", "[spend-loop risk]"));
|
|
39157
|
+
}
|
|
39158
|
+
return parts.length ? " " + parts.join(" ") : "";
|
|
39159
|
+
}
|
|
39160
|
+
function joinEnds(left, right, inner, styler) {
|
|
39161
|
+
const rightLen = styler.visibleLen(right);
|
|
39162
|
+
const leftMax = Math.max(0, inner - rightLen - 2);
|
|
39163
|
+
const leftFit = styler.visibleLen(left) > leftMax ? styler.cell(styler.stripAnsi(left), leftMax) : left + " ".repeat(leftMax - styler.visibleLen(left));
|
|
39164
|
+
return leftFit + " " + right;
|
|
39165
|
+
}
|
|
39166
|
+
function renderGraphView({ graph, graphSel, graphAvailable, framed, width, styler }) {
|
|
39167
|
+
const title = `pi-dispatch ${styler.glyphs.h}${styler.glyphs.h} GRAPH \xB7 triggers and flows`;
|
|
39168
|
+
const inner = width - 4;
|
|
39169
|
+
const model = graph?.model ?? null;
|
|
39170
|
+
if (!framed) {
|
|
39171
|
+
const plain = graph?.error ? [`graph unreachable (${graph.error})`] : model ? renderGraph(model).split("\n") : [graphAvailable ? "loading graph\u2026" : "graph unavailable in this build"];
|
|
39172
|
+
return [styler.stripAnsi(title), "", ...plain, "", "\u2191\u2193 move \xB7 \u21B5 open/fold \xB7 r refresh \xB7 esc back"];
|
|
39173
|
+
}
|
|
39174
|
+
const lines = [];
|
|
39175
|
+
if (graph?.error) {
|
|
39176
|
+
lines.push(fitLine(styler.fg("error", `graph unreachable (${graph.error})`), inner, styler));
|
|
39177
|
+
} else if (model === null) {
|
|
39178
|
+
lines.push(fitLine(styler.fg("dim", graphAvailable ? "loading graph\u2026" : "graph unavailable in this build"), inner, styler));
|
|
39179
|
+
} else if (model.meta?.triggersMissing) {
|
|
39180
|
+
lines.push(fitLine(styler.fg("warning", "no triggers file found"), inner, styler));
|
|
39181
|
+
} else if (typeof model.meta?.triggersInvalid === "string" && model.meta.triggersInvalid !== "") {
|
|
39182
|
+
lines.push(fitLine(styler.fg("error", `triggers file invalid: ${model.meta.triggersInvalid}`), inner, styler));
|
|
39183
|
+
} else {
|
|
39184
|
+
const rows = graphRows(model, graph.folded);
|
|
39185
|
+
if (rows.length === 0) {
|
|
39186
|
+
lines.push(fitLine(styler.fg("dim", "no triggers configured"), inner, styler));
|
|
39187
|
+
} else {
|
|
39188
|
+
const top = Math.max(0, Math.min(graphSel - GRAPH_VIEWPORT + 1, rows.length - GRAPH_VIEWPORT));
|
|
39189
|
+
if (top > 0) lines.push(fitLine(styler.fg("dim", `\u2026 ${top} above`), inner, styler));
|
|
39190
|
+
rows.slice(top, top + GRAPH_VIEWPORT).forEach((row, i) => {
|
|
39191
|
+
lines.push(graphRowLine(row, top + i === graphSel, inner, styler));
|
|
39192
|
+
});
|
|
39193
|
+
const below = rows.length - (top + GRAPH_VIEWPORT);
|
|
39194
|
+
if (below > 0) lines.push(fitLine(styler.fg("dim", `\u2026 ${below} below`), inner, styler));
|
|
39195
|
+
}
|
|
39196
|
+
const counters = graphCounterLine(model, styler);
|
|
39197
|
+
lines.push(RULE);
|
|
39198
|
+
if (counters) lines.push(fitLine(counters, inner, styler));
|
|
39199
|
+
lines.push(fitLine(styler.fg("dim", capsLineText2(model.caps)), inner, styler));
|
|
39200
|
+
}
|
|
39201
|
+
return frame(styler, { title, width, lines, footer: graphHints(inner, styler) });
|
|
39202
|
+
}
|
|
39203
|
+
function graphCounterLine(model, styler) {
|
|
39204
|
+
const bits = [];
|
|
39205
|
+
const meta = model?.meta ?? {};
|
|
39206
|
+
if ((meta.unattributedRuns ?? 0) > 0) bits.push(`${meta.unattributedRuns} runs unattributed`);
|
|
39207
|
+
const refused = Object.values(meta.chainRefusals ?? {}).reduce((a, n) => a + (Number(n) || 0), 0);
|
|
39208
|
+
if (refused > 0) bits.push(`${refused} chain requests refused`);
|
|
39209
|
+
const t = meta.truncated ?? {};
|
|
39210
|
+
if (t.folders) bits.push("folders truncated");
|
|
39211
|
+
if (t.skills) bits.push("skills truncated/unread");
|
|
39212
|
+
if (t.edges) bits.push("edges truncated");
|
|
39213
|
+
if ((meta.droppedObservedEdges ?? 0) > 0) bits.push(`${meta.droppedObservedEdges} observed edges dropped`);
|
|
39214
|
+
if ((meta.injectedUnreachable ?? []).length > 0) bits.push(`${meta.injectedUnreachable.length} injected dirs unreadable`);
|
|
39215
|
+
return bits.length ? styler.fg("warning", bits.join(" \xB7 ")) : null;
|
|
39216
|
+
}
|
|
39217
|
+
function capsLineText2(caps) {
|
|
39218
|
+
return `caps: chain depth <= ${caps?.chainDepthMax ?? "?"} \xB7 <= ${caps?.chainMaxPerJob ?? "?"} per job \xB7 same folder only \xB7 window ${caps?.windowDays ?? "?"}d`;
|
|
39219
|
+
}
|
|
39220
|
+
function graphHints(inner, styler) {
|
|
39221
|
+
const k = (key, label) => styler.fg("accent", key) + " " + styler.fg("dim", label);
|
|
39222
|
+
const hints = [k("\u2191\u2193", "move"), k("\u21B5", "open/fold"), k("r", "refresh"), k("esc", "back")].join(styler.fg("dim", " \xB7 "));
|
|
37098
39223
|
return fitLine(hints, inner, styler);
|
|
37099
39224
|
}
|
|
37100
39225
|
function costsWindowSinceMs(windowKey, nowMs) {
|
|
@@ -37359,7 +39484,7 @@ function countdownText(ms) {
|
|
|
37359
39484
|
return h ? `resets ${h}` : "";
|
|
37360
39485
|
}
|
|
37361
39486
|
function buildRows(snapshot, runSort = "time") {
|
|
37362
|
-
const triggers = (snapshot?.triggers?.triggers ?? []).map((t, i) => ({ kind: "trigger", trigger: t, index: i }));
|
|
39487
|
+
const triggers = (snapshot?.triggers?.triggers ?? []).map((t, i) => ({ kind: "trigger", trigger: t, index: Number.isInteger(t?.index) ? t.index : i }));
|
|
37363
39488
|
const active2 = snapshot?.activeJobId ? [{ kind: "active", jobId: snapshot.activeJobId }] : [];
|
|
37364
39489
|
const runs = (Array.isArray(snapshot?.runs) ? snapshot.runs : []).map((record) => ({ kind: "run", record }));
|
|
37365
39490
|
return [...triggers, ...active2, ...sortRuns(runs, runSort)];
|
|
@@ -37375,24 +39500,24 @@ function sortRuns(rows, runSort) {
|
|
|
37375
39500
|
return sorted;
|
|
37376
39501
|
}
|
|
37377
39502
|
function renderRunList(rows, selected, w) {
|
|
37378
|
-
if (!Array.isArray(rows) || rows.length === 0) return [
|
|
39503
|
+
if (!Array.isArray(rows) || rows.length === 0) return [clip2("(no runs)", w)];
|
|
37379
39504
|
const { top, count } = runsWindow(rows.length, selected);
|
|
37380
39505
|
const out = [];
|
|
37381
|
-
if (top > 0) out.push(
|
|
39506
|
+
if (top > 0) out.push(clip2(`^ ${top} more`, w));
|
|
37382
39507
|
for (let i = top; i < top + count; i++) {
|
|
37383
39508
|
const row = rows[i];
|
|
37384
39509
|
const cursor = i === selected ? "\u203A" : " ";
|
|
37385
39510
|
if (row.kind === "active") {
|
|
37386
|
-
out.push(
|
|
39511
|
+
out.push(clip2(`${cursor} * ACTIVE ${row.jobId} running`, w));
|
|
37387
39512
|
continue;
|
|
37388
39513
|
}
|
|
37389
39514
|
const run = row.record;
|
|
37390
39515
|
const rep = run?.replica > 0 ? `r${run.replica}/${run.replicas ?? "?"} ` : "";
|
|
37391
39516
|
const cells = [run?.jobId, run?.target, run?.flow, run?.outcome, run?.turns, run?.tokens?.total].map((f) => f === null || f === void 0 ? "-" : String(f)).join(" \xB7 ");
|
|
37392
|
-
out.push(
|
|
39517
|
+
out.push(clip2(`${cursor} ${rep}${cells}`, w));
|
|
37393
39518
|
}
|
|
37394
39519
|
const below = rows.length - top - count;
|
|
37395
|
-
if (below > 0) out.push(
|
|
39520
|
+
if (below > 0) out.push(clip2(`v ${below} more`, w));
|
|
37396
39521
|
return out;
|
|
37397
39522
|
}
|
|
37398
39523
|
function tailMatches(lines, query) {
|
|
@@ -37433,14 +39558,14 @@ function renderLiveTail({ snapshot, framed, width, tailJobId, tail, tailTop, tai
|
|
|
37433
39558
|
return [boxTitle, "", ...plain, "", footer];
|
|
37434
39559
|
}
|
|
37435
39560
|
const inner = Math.trunc(width) - 4;
|
|
37436
|
-
const lines = [
|
|
39561
|
+
const lines = [clip2(`live ${tailJobId} -- ${len} line(s)`, inner)];
|
|
37437
39562
|
for (let i = top; i < Math.min(top + TAIL_VIEWPORT, len); i++) {
|
|
37438
|
-
const safe =
|
|
39563
|
+
const safe = clip2(all[i], inner);
|
|
37439
39564
|
lines.push(tailQuery !== null && i === tailMatchLine ? styler.fg("warning", safe) : safe);
|
|
37440
39565
|
}
|
|
37441
|
-
if (ended) lines.push(
|
|
39566
|
+
if (ended) lines.push(clip2("(run ended -- Esc to go back)", inner));
|
|
37442
39567
|
if (tailSearchInput) lines.push(styler.fg("accent", "/ ") + styler.lineInput(tailSearchInput, Math.max(1, inner - 2)));
|
|
37443
|
-
return frame(styler, { title: boxTitle, width: Math.trunc(width), lines, footer:
|
|
39568
|
+
return frame(styler, { title: boxTitle, width: Math.trunc(width), lines, footer: clip2(footer, inner) });
|
|
37444
39569
|
}
|
|
37445
39570
|
function renderRunDetail(record, inner, styler, allRuns = [], sandbox = null) {
|
|
37446
39571
|
const r = record ?? {};
|
|
@@ -37536,7 +39661,7 @@ function runDetailHints(inner, styler, canOpen = false, canCopy = false, copiedN
|
|
|
37536
39661
|
if (copiedNote) return fitLine(styler.fg("success", copiedNote) + styler.fg("dim", " \xB7 ") + line, inner, styler);
|
|
37537
39662
|
return fitLine(line, inner, styler);
|
|
37538
39663
|
}
|
|
37539
|
-
var KEY_HINTS, RUNS_ON_DASHBOARD, RUNS_VIEWPORT, RUN_SORTS, REFRESH_MS, TAIL_LINES, TAIL_VIEWPORT, MIN_WIDTH2, DRILL_WIDTH, COSTS_WINDOWS, COSTS_STALE_MS, KIND_COLOR, KIND_WIDTH, FRAME_CHROME_ROWS, MONTHS;
|
|
39664
|
+
var KEY_HINTS, RUNS_ON_DASHBOARD, RUNS_VIEWPORT, RUN_SORTS, REFRESH_MS, TAIL_LINES, TAIL_VIEWPORT, MIN_WIDTH2, DRILL_WIDTH, COSTS_WINDOWS, COSTS_STALE_MS, GRAPH_VIEWPORT, KIND_COLOR, KIND_WIDTH, FRAME_CHROME_ROWS, MONTHS;
|
|
37540
39665
|
var init_dashboard = __esm({
|
|
37541
39666
|
"src/dashboard.ts"() {
|
|
37542
39667
|
init_budget();
|
|
@@ -37547,6 +39672,7 @@ var init_dashboard = __esm({
|
|
|
37547
39672
|
init_pricing();
|
|
37548
39673
|
init_read_model();
|
|
37549
39674
|
init_render();
|
|
39675
|
+
init_graph_model();
|
|
37550
39676
|
init_keys2();
|
|
37551
39677
|
init_panel();
|
|
37552
39678
|
init_style();
|
|
@@ -37562,6 +39688,7 @@ var init_dashboard = __esm({
|
|
|
37562
39688
|
DRILL_WIDTH = 70;
|
|
37563
39689
|
COSTS_WINDOWS = ["7d", "30d", "mtd"];
|
|
37564
39690
|
COSTS_STALE_MS = 1e4;
|
|
39691
|
+
GRAPH_VIEWPORT = 16;
|
|
37565
39692
|
KIND_COLOR = { cron: "accent", label: "syntaxType", comment: "syntaxKeyword", pull_request: "syntaxFunction" };
|
|
37566
39693
|
KIND_WIDTH = 13;
|
|
37567
39694
|
FRAME_CHROME_ROWS = 4;
|
|
@@ -37589,7 +39716,7 @@ __export(setup_wizard_exports, {
|
|
|
37589
39716
|
runtimeDirFor: () => runtimeDirFor
|
|
37590
39717
|
});
|
|
37591
39718
|
import * as nodeFs4 from "node:fs";
|
|
37592
|
-
import { execFileSync as execFileSync3, spawn as
|
|
39719
|
+
import { execFileSync as execFileSync3, spawn as nodeSpawn2 } from "node:child_process";
|
|
37593
39720
|
import { homedir as homedir2 } from "node:os";
|
|
37594
39721
|
import { join as join6 } from "node:path";
|
|
37595
39722
|
function envIsSet(env, key) {
|
|
@@ -37624,7 +39751,7 @@ async function detectDeployment({
|
|
|
37624
39751
|
}
|
|
37625
39752
|
return { state: "none", detail: "no pointer, no env, no deployment files here, queue unreachable" };
|
|
37626
39753
|
}
|
|
37627
|
-
async function runAttached(ctx, { title, argv0, args, cwd, env, shell, spawnFn =
|
|
39754
|
+
async function runAttached(ctx, { title, argv0, args, cwd, env, shell, spawnFn = nodeSpawn2 }) {
|
|
37628
39755
|
if (ctx?.mode !== "tui" || typeof ctx?.ui?.custom !== "function") {
|
|
37629
39756
|
return { code: null, error: new Error("needs the terminal UI") };
|
|
37630
39757
|
}
|
|
@@ -38098,8 +40225,8 @@ var init_setup_wizard = __esm({
|
|
|
38098
40225
|
init_deployment_pointer();
|
|
38099
40226
|
init_read_model();
|
|
38100
40227
|
init_index();
|
|
38101
|
-
RUNTIME_VERSION = "0.
|
|
38102
|
-
RECEIVER_VERSION = "0.2.
|
|
40228
|
+
RUNTIME_VERSION = "0.3.0";
|
|
40229
|
+
RECEIVER_VERSION = "0.2.1";
|
|
38103
40230
|
RUNTIME_PKG = "@edgehero/pi-dispatch";
|
|
38104
40231
|
RECEIVER_PKG = "@edgehero/pi-dispatch-receiver";
|
|
38105
40232
|
NUDGE_MARKER_BASENAME = "pi-dispatch-setup.nudged";
|
|
@@ -38116,7 +40243,7 @@ var init_setup_wizard = __esm({
|
|
|
38116
40243
|
|
|
38117
40244
|
// src/index.ts
|
|
38118
40245
|
import { VERSION } from "@earendil-works/pi-coding-agent";
|
|
38119
|
-
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
40246
|
+
import { fileURLToPath as fileURLToPath2, pathToFileURL } from "node:url";
|
|
38120
40247
|
import * as nodeFs5 from "node:fs";
|
|
38121
40248
|
function computePiVersionAdvisory(version, supported) {
|
|
38122
40249
|
if (version === supported || version === "0.0.0") return void 0;
|
|
@@ -38633,6 +40760,18 @@ ${renderBudget({ budget, settings: settings2 })}`);
|
|
|
38633
40760
|
costsCommand(pi, paths, tokens, notify);
|
|
38634
40761
|
return;
|
|
38635
40762
|
}
|
|
40763
|
+
case "graph": {
|
|
40764
|
+
if (tokens[1] === "html") {
|
|
40765
|
+
await graphHtmlCommand(paths, tokens, notify);
|
|
40766
|
+
return;
|
|
40767
|
+
}
|
|
40768
|
+
if (tokens.length > 1) {
|
|
40769
|
+
notify?.(GRAPH_USAGE, "warning");
|
|
40770
|
+
return;
|
|
40771
|
+
}
|
|
40772
|
+
send(pi, renderGraph(await assembleGraph(paths)));
|
|
40773
|
+
return;
|
|
40774
|
+
}
|
|
38636
40775
|
case "run": {
|
|
38637
40776
|
const folder = tokens[1];
|
|
38638
40777
|
const flow = tokens[2];
|
|
@@ -38709,6 +40848,63 @@ function assembleCosts(paths, window, flow) {
|
|
|
38709
40848
|
});
|
|
38710
40849
|
return { fold };
|
|
38711
40850
|
}
|
|
40851
|
+
async function assembleGraph(paths) {
|
|
40852
|
+
const nowMs = Date.now();
|
|
40853
|
+
const triggers = readTriggers({ triggersPath: paths.triggersPath });
|
|
40854
|
+
const triggerList2 = Array.isArray(triggers?.triggers) ? triggers.triggers : [];
|
|
40855
|
+
const schedulers = await readSchedulers({ url: paths.valkeyUrl });
|
|
40856
|
+
const records = scanRunRecords({ logsDir: paths.logsDir, sinceMs: nowMs - GRAPH_LIMITS.windowDays * 24 * 60 * 60 * 1e3, nowMs });
|
|
40857
|
+
const recs = Array.isArray(records) ? records : [];
|
|
40858
|
+
return buildGraphModel({
|
|
40859
|
+
triggers,
|
|
40860
|
+
schedulers: Array.isArray(schedulers) ? schedulers : [],
|
|
40861
|
+
...collectGraphInputs({ triggers: triggerList2 }),
|
|
40862
|
+
cronStats: cronRunStats({ records: recs, schedulerIds: triggerList2.filter((t) => t.type === "cron" && typeof t.id === "string").map((t) => t.id) }),
|
|
40863
|
+
runJoin: joinRunsToTriggers({ records: recs, triggerCount: triggers?.count, triggerTypes: Object.fromEntries(triggerList2.map((t) => [t.index, t.type])) }),
|
|
40864
|
+
chainEdges: observedChainEdges({ records: recs }),
|
|
40865
|
+
forgeRepos: forgeRepoTargets({ records: recs }),
|
|
40866
|
+
caps: { chainDepthMax: paths.chainDepthMax, chainMaxPerJob: paths.chainMaxPerJob, windowDays: GRAPH_LIMITS.windowDays },
|
|
40867
|
+
nowMs
|
|
40868
|
+
});
|
|
40869
|
+
}
|
|
40870
|
+
function realGraphHtmlDeps() {
|
|
40871
|
+
return { fs: nodeFs5, openBrowser, env: process.env, now: () => Date.now(), platform: process.platform };
|
|
40872
|
+
}
|
|
40873
|
+
function isHeadlessEnv(env, platform) {
|
|
40874
|
+
if (env?.SSH_CONNECTION || env?.SSH_TTY) return "SSH session";
|
|
40875
|
+
if (platform === "linux" && !env?.DISPLAY && !env?.WAYLAND_DISPLAY) return "no display";
|
|
40876
|
+
return null;
|
|
40877
|
+
}
|
|
40878
|
+
async function graphHtmlCommand(paths, tokens, notify, deps = realGraphHtmlDeps()) {
|
|
40879
|
+
const rest = tokens.slice(2);
|
|
40880
|
+
const noOpen = rest.includes("--no-open");
|
|
40881
|
+
const fullPaths = rest.includes("--full-paths");
|
|
40882
|
+
if (rest.some((t) => t !== "--no-open" && t !== "--full-paths")) {
|
|
40883
|
+
notify?.(GRAPH_USAGE, "warning");
|
|
40884
|
+
return;
|
|
40885
|
+
}
|
|
40886
|
+
const model = await assembleGraph(paths);
|
|
40887
|
+
const html = buildGraphHtml(model, { now: deps.now(), fullPaths });
|
|
40888
|
+
const file = `${paths.graphDir}/graph.html`;
|
|
40889
|
+
try {
|
|
40890
|
+
deps.fs.mkdirSync(paths.graphDir, { recursive: true });
|
|
40891
|
+
const tmp = `${file}.tmp`;
|
|
40892
|
+
deps.fs.writeFileSync(tmp, html, { mode: 420 });
|
|
40893
|
+
deps.fs.renameSync(tmp, file);
|
|
40894
|
+
} catch (err) {
|
|
40895
|
+
notify?.(`graph html: could not write ${file} (${err?.message ?? err})`, "error");
|
|
40896
|
+
return;
|
|
40897
|
+
}
|
|
40898
|
+
const url = pathToFileURL(file).href;
|
|
40899
|
+
notify?.(`graph written: ${url}`, "info");
|
|
40900
|
+
if (noOpen) return;
|
|
40901
|
+
const headless = isHeadlessEnv(deps.env, deps.platform);
|
|
40902
|
+
if (headless) {
|
|
40903
|
+
notify?.(`not opening a browser (${headless}): open the URL on this machine's desktop, or scp the file there`, "info");
|
|
40904
|
+
return;
|
|
40905
|
+
}
|
|
40906
|
+
deps.openBrowser(url);
|
|
40907
|
+
}
|
|
38712
40908
|
function costsCommand(pi, paths, tokens, notify) {
|
|
38713
40909
|
if (tokens[1] === "whatif") {
|
|
38714
40910
|
costsWhatIf(pi, paths, tokens, notify);
|
|
@@ -39148,6 +41344,11 @@ function completeArguments(prefix) {
|
|
|
39148
41344
|
const items = ["7d", "30d", "mtd", "whatif"].filter((w) => w.startsWith(partial)).map((w) => ({ value: `costs ${w}`, label: w }));
|
|
39149
41345
|
return items.length > 0 ? items : null;
|
|
39150
41346
|
}
|
|
41347
|
+
if (parts[0] === "graph" && parts.length === 2) {
|
|
41348
|
+
const partial = parts[1];
|
|
41349
|
+
const items = ["html"].filter((w) => w.startsWith(partial)).map((w) => ({ value: `graph ${w}`, label: w }));
|
|
41350
|
+
return items.length > 0 ? items : null;
|
|
41351
|
+
}
|
|
39151
41352
|
if ((parts[0] === "set" || parts[0] === "unset") && parts.length === 2) {
|
|
39152
41353
|
const partial = parts[1];
|
|
39153
41354
|
const items = KNOWN_KEYS.filter((k) => k.startsWith(partial)).map((k) => ({
|
|
@@ -39158,11 +41359,14 @@ function completeArguments(prefix) {
|
|
|
39158
41359
|
}
|
|
39159
41360
|
return null;
|
|
39160
41361
|
}
|
|
39161
|
-
var USED_API, SUPPORTED_PI_VERSION, piVersionAdvisory, piVersionCheckDone, runtimeSkewNoticed, CHANNEL, REBUILT_NOTICE, USAGE, KNOWN_SUBCOMMANDS, FORGE_PROMPT, PR_ACTION_VOCAB, COSTS_WINDOWS2, COSTS_USAGE, DAY_MS2, PRICING, VIEWPORT_LINES;
|
|
41362
|
+
var USED_API, SUPPORTED_PI_VERSION, piVersionAdvisory, piVersionCheckDone, runtimeSkewNoticed, CHANNEL, REBUILT_NOTICE, USAGE, KNOWN_SUBCOMMANDS, FORGE_PROMPT, PR_ACTION_VOCAB, COSTS_WINDOWS2, COSTS_USAGE, DAY_MS2, PRICING, GRAPH_USAGE, VIEWPORT_LINES;
|
|
39162
41363
|
var init_index = __esm({
|
|
39163
41364
|
"src/index.ts"() {
|
|
39164
41365
|
init_build();
|
|
39165
41366
|
init_read_model();
|
|
41367
|
+
init_graph_model();
|
|
41368
|
+
init_graph_html();
|
|
41369
|
+
init_open_browser();
|
|
39166
41370
|
init_deployment_pointer();
|
|
39167
41371
|
init_costs();
|
|
39168
41372
|
init_pricing();
|
|
@@ -39179,7 +41383,7 @@ var init_index = __esm({
|
|
|
39179
41383
|
runtimeSkewNoticed = false;
|
|
39180
41384
|
CHANNEL = "pi-dispatch-admin";
|
|
39181
41385
|
REBUILT_NOTICE = (reason) => `replaced invalid settings file (${reason}) \u2014 other keys were lost`;
|
|
39182
|
-
USAGE = "usage: /dispatch <status|pause|resume|run|runs|logs|budget|costs|triggers|settings|set|unset|setup>";
|
|
41386
|
+
USAGE = "usage: /dispatch <status|pause|resume|run|runs|logs|budget|costs|graph|triggers|settings|set|unset|setup>";
|
|
39183
41387
|
KNOWN_SUBCOMMANDS = [
|
|
39184
41388
|
"status",
|
|
39185
41389
|
"pause",
|
|
@@ -39189,6 +41393,7 @@ var init_index = __esm({
|
|
|
39189
41393
|
"logs",
|
|
39190
41394
|
"budget",
|
|
39191
41395
|
"costs",
|
|
41396
|
+
"graph",
|
|
39192
41397
|
"triggers",
|
|
39193
41398
|
"settings",
|
|
39194
41399
|
"set",
|
|
@@ -39206,6 +41411,7 @@ var init_index = __esm({
|
|
|
39206
41411
|
COSTS_USAGE = "usage: /dispatch costs [7d|30d|mtd] | /dispatch costs whatif <provider/model> --flow <flow>";
|
|
39207
41412
|
DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
39208
41413
|
PRICING = { listPricedModels, getPricedModel, isZeroRated, reprice, piAiVersion };
|
|
41414
|
+
GRAPH_USAGE = "usage: /dispatch graph [html [--no-open] [--full-paths]]";
|
|
39209
41415
|
VIEWPORT_LINES = 20;
|
|
39210
41416
|
}
|
|
39211
41417
|
});
|
|
@@ -39217,5 +41423,7 @@ export {
|
|
|
39217
41423
|
buildTriggerEntry,
|
|
39218
41424
|
computePiVersionAdvisory,
|
|
39219
41425
|
admin as default,
|
|
39220
|
-
|
|
41426
|
+
graphHtmlCommand,
|
|
41427
|
+
handleDashboardAction,
|
|
41428
|
+
isHeadlessEnv
|
|
39221
41429
|
};
|