@edgehero/pi-dispatch-admin 0.5.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 +7 -1
- package/dist/index.mjs +2391 -72
- package/package.json +1 -1
- package/skills/operate-pi-dispatch/SKILL.md +6 -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
|
|
|
@@ -7935,11 +7941,13 @@ function normalizeCron(on, run, index, path, state) {
|
|
|
7935
7941
|
}
|
|
7936
7942
|
const packages = validatePackagesFlag(run, `cron trigger "${id}"`, path);
|
|
7937
7943
|
const image = validateImageRef(run, `cron trigger "${id}"`, path);
|
|
7944
|
+
const skillsDir = validateSkillsDir(run, `cron trigger "${id}"`, path);
|
|
7945
|
+
validateInstructions(run, `cron trigger "${id}"`, path, { cron: true });
|
|
7938
7946
|
const resume = validateResumeFlag(run, `cron trigger "${id}"`, path);
|
|
7939
7947
|
validateReplicas(run, `cron trigger "${id}"`, path);
|
|
7940
7948
|
return {
|
|
7941
7949
|
on: { type: "cron", id, pattern },
|
|
7942
|
-
run: { kind: "local", folder: run.folder, flow: run.flow, task: run.task, provider: run.provider, model: run.model, maxTurns: run.maxTurns, github: run.github, packages, image, resume }
|
|
7950
|
+
run: { kind: "local", folder: run.folder, flow: run.flow, task: run.task, provider: run.provider, model: run.model, maxTurns: run.maxTurns, github: run.github, packages, image, resume, ...skillsDir !== void 0 && { skillsDir } }
|
|
7943
7951
|
};
|
|
7944
7952
|
}
|
|
7945
7953
|
function validatePackagesFlag(run, at, path) {
|
|
@@ -7973,6 +7981,31 @@ function validateImageRef(run, at, path) {
|
|
|
7973
7981
|
}
|
|
7974
7982
|
return image;
|
|
7975
7983
|
}
|
|
7984
|
+
function validateSkillsDir(run, at, path) {
|
|
7985
|
+
const dir = run.skillsDir;
|
|
7986
|
+
if (dir === void 0) return void 0;
|
|
7987
|
+
if (typeof dir !== "string" || dir.trim() === "") {
|
|
7988
|
+
throw configError(`${at}: run.skillsDir must be a non-empty string when present: ${path}`);
|
|
7989
|
+
}
|
|
7990
|
+
if (dir !== dir.trim()) {
|
|
7991
|
+
throw configError(`${at}: run.skillsDir must not have leading or trailing whitespace (got ${JSON.stringify(dir)}): ${path}`);
|
|
7992
|
+
}
|
|
7993
|
+
return dir;
|
|
7994
|
+
}
|
|
7995
|
+
function validateInstructions(run, at, path, { cron = false } = {}) {
|
|
7996
|
+
const text = run.instructions;
|
|
7997
|
+
if (text === void 0) return void 0;
|
|
7998
|
+
if (cron) {
|
|
7999
|
+
throw configError(`${at}: run.instructions is not accepted on a cron trigger -- a local job's prompt IS run.task, the same operator-authored text in the same place. Put the standing instruction at the top of run.task: ${path}`);
|
|
8000
|
+
}
|
|
8001
|
+
if (typeof text !== "string" || text.trim() === "") {
|
|
8002
|
+
throw configError(`${at}: run.instructions must be a non-empty string when present: ${path}`);
|
|
8003
|
+
}
|
|
8004
|
+
if (text.length > INSTRUCTIONS_MAX) {
|
|
8005
|
+
throw configError(`${at}: run.instructions is ${text.length} characters, over the ${INSTRUCTIONS_MAX} cap -- a standing instruction is a sentence or two. Anything longer belongs in the flow's own SKILL.md, or in the global overlay's APPEND_SYSTEM.md if it applies to every job: ${path}`);
|
|
8006
|
+
}
|
|
8007
|
+
return text;
|
|
8008
|
+
}
|
|
7976
8009
|
function validatePredicate(on, index, path, requirePositive) {
|
|
7977
8010
|
const at = `trigger at index ${index}`;
|
|
7978
8011
|
for (const key of ["any", "all", "none"]) {
|
|
@@ -8027,12 +8060,14 @@ function normalizeLabel(on, run, index, path) {
|
|
|
8027
8060
|
}
|
|
8028
8061
|
const packages = validatePackagesFlag(run, at, path);
|
|
8029
8062
|
const image = validateImageRef(run, at, path);
|
|
8063
|
+
const skillsDir = validateSkillsDir(run, at, path);
|
|
8064
|
+
const instructions = validateInstructions(run, at, path);
|
|
8030
8065
|
const resume = validateResumeFlag(run, at, path);
|
|
8031
8066
|
const repository = validateRepository(run, "label", at, path);
|
|
8032
8067
|
const replicas = validateReplicas(run, at, path);
|
|
8033
8068
|
return {
|
|
8034
8069
|
on: { type: "label", any: predicate.any, all: predicate.all, none: predicate.none },
|
|
8035
|
-
run: { kind: run.kind, flow: run.flow, packages, image, resume, replicas, ...repository !== void 0 && { repository } }
|
|
8070
|
+
run: { kind: run.kind, flow: run.flow, packages, image, resume, replicas, ...skillsDir !== void 0 && { skillsDir }, ...instructions !== void 0 && { instructions }, ...repository !== void 0 && { repository } }
|
|
8036
8071
|
};
|
|
8037
8072
|
}
|
|
8038
8073
|
function normalizeComment(on, run, index, path, state) {
|
|
@@ -8049,12 +8084,14 @@ function normalizeComment(on, run, index, path, state) {
|
|
|
8049
8084
|
}
|
|
8050
8085
|
const packages = validatePackagesFlag(run, at, path);
|
|
8051
8086
|
const image = validateImageRef(run, at, path);
|
|
8087
|
+
const skillsDir = validateSkillsDir(run, at, path);
|
|
8088
|
+
const instructions = validateInstructions(run, at, path);
|
|
8052
8089
|
const resume = validateResumeFlag(run, at, path);
|
|
8053
8090
|
const repository = validateRepository(run, "comment", at, path);
|
|
8054
8091
|
const replicas = validateReplicas(run, at, path);
|
|
8055
8092
|
return {
|
|
8056
8093
|
on: { type: "comment", phrase: on.phrase },
|
|
8057
|
-
run: { kind: run.kind, flow: run.flow, packages, image, resume, replicas, ...repository !== void 0 && { repository } }
|
|
8094
|
+
run: { kind: run.kind, flow: run.flow, packages, image, resume, replicas, ...skillsDir !== void 0 && { skillsDir }, ...instructions !== void 0 && { instructions }, ...repository !== void 0 && { repository } }
|
|
8058
8095
|
};
|
|
8059
8096
|
}
|
|
8060
8097
|
function normalizePullRequest(on, run, index, path) {
|
|
@@ -8070,6 +8107,7 @@ function normalizePullRequest(on, run, index, path) {
|
|
|
8070
8107
|
throw configError(`${at}: pull_request on.action has an unsupported ${run.kind} action ${JSON.stringify(a)} (expected ${expected}): ${path}`);
|
|
8071
8108
|
}
|
|
8072
8109
|
}
|
|
8110
|
+
const reviewState = validateReviewState(on, actions, run, at, path);
|
|
8073
8111
|
const labelAction = forgeSpec(run.kind)?.prLabelAction;
|
|
8074
8112
|
const requirePositive = typeof labelAction === "string" && actions.includes(labelAction);
|
|
8075
8113
|
if (run.kind === "azure" && (on.any !== void 0 || on.all !== void 0 || on.none !== void 0)) {
|
|
@@ -8081,22 +8119,55 @@ function normalizePullRequest(on, run, index, path) {
|
|
|
8081
8119
|
}
|
|
8082
8120
|
const packages = validatePackagesFlag(run, at, path);
|
|
8083
8121
|
const image = validateImageRef(run, at, path);
|
|
8122
|
+
const skillsDir = validateSkillsDir(run, at, path);
|
|
8123
|
+
const instructions = validateInstructions(run, at, path);
|
|
8084
8124
|
const resume = validateResumeFlag(run, at, path);
|
|
8085
8125
|
validateRepository(run, "pull_request", at, path);
|
|
8086
8126
|
const replicas = validateReplicas(run, at, path);
|
|
8087
8127
|
return {
|
|
8088
|
-
on: {
|
|
8089
|
-
|
|
8128
|
+
on: {
|
|
8129
|
+
type: "pull_request",
|
|
8130
|
+
action: [...actions],
|
|
8131
|
+
// Absent rather than present-and-undefined: an unnarrowed rule's normalized shape must stay
|
|
8132
|
+
// byte-identical to the one every pre-#66 trigger file produces.
|
|
8133
|
+
...reviewState !== void 0 && { reviewState },
|
|
8134
|
+
any: predicate.any,
|
|
8135
|
+
all: predicate.all,
|
|
8136
|
+
none: predicate.none
|
|
8137
|
+
},
|
|
8138
|
+
run: { kind: run.kind, flow: run.flow, packages, image, resume, replicas, ...skillsDir !== void 0 && { skillsDir }, ...instructions !== void 0 && { instructions } }
|
|
8090
8139
|
};
|
|
8091
8140
|
}
|
|
8092
|
-
|
|
8141
|
+
function validateReviewState(on, actions, run, at, path) {
|
|
8142
|
+
if (on.reviewState === void 0) return void 0;
|
|
8143
|
+
if (run.kind !== "github") {
|
|
8144
|
+
throw configError(`${at}: on.reviewState is github-only (got ${run.kind}), no other forge reports a review verdict: ${path}`);
|
|
8145
|
+
}
|
|
8146
|
+
if (!actions.includes(REVIEW_ACTION)) {
|
|
8147
|
+
throw configError(`${at}: on.reviewState requires on.action to include ${JSON.stringify(REVIEW_ACTION)}, otherwise it narrows nothing: ${path}`);
|
|
8148
|
+
}
|
|
8149
|
+
if (!Array.isArray(on.reviewState) || on.reviewState.length === 0) {
|
|
8150
|
+
throw configError(`${at}: on.reviewState must be a non-empty array: ${path}`);
|
|
8151
|
+
}
|
|
8152
|
+
const expected = [...REVIEW_STATES].join("|");
|
|
8153
|
+
for (const s of on.reviewState) {
|
|
8154
|
+
if (!REVIEW_STATES.has(s)) {
|
|
8155
|
+
throw configError(`${at}: on.reviewState has an unsupported review state ${JSON.stringify(s)} (expected ${expected}): ${path}`);
|
|
8156
|
+
}
|
|
8157
|
+
}
|
|
8158
|
+
return [...on.reviewState];
|
|
8159
|
+
}
|
|
8160
|
+
var ON_TYPES, PR_ACTIONS, REVIEW_STATES, REVIEW_ACTION, ID_CHARSET, REPLICAS_MAX, INSTRUCTIONS_MAX;
|
|
8093
8161
|
var init_triggers = __esm({
|
|
8094
8162
|
"../worker/src/triggers.mjs"() {
|
|
8095
8163
|
init_config2();
|
|
8096
8164
|
init_forges();
|
|
8097
8165
|
ON_TYPES = /* @__PURE__ */ new Set(["cron", "label", "comment", "pull_request"]);
|
|
8098
8166
|
PR_ACTIONS = {
|
|
8099
|
-
github: /* @__PURE__ */ new Set(["labeled", "opened", "synchronize", "reopened"]),
|
|
8167
|
+
github: /* @__PURE__ */ new Set(["labeled", "opened", "synchronize", "reopened", "review_submitted"]),
|
|
8168
|
+
// GitLab's `approved` is its review gate (a member approved the MR). It is NOT github's
|
|
8169
|
+
// `review_submitted` renamed: `approved` is one verdict, `review_submitted` is every verdict, which is
|
|
8170
|
+
// what `on.reviewState` below exists to narrow.
|
|
8100
8171
|
gitlab: /* @__PURE__ */ new Set(["open", "update", "reopen", "approved"]),
|
|
8101
8172
|
// Forgejo's own spellings. `label_updated` is its `labeled` and `synchronized` its `synchronize` -- a
|
|
8102
8173
|
// one-letter difference that an operator would otherwise discover as a trigger that loads clean and
|
|
@@ -8109,8 +8180,11 @@ var init_triggers = __esm({
|
|
|
8109
8180
|
// requests -- which is why azure's `prLabelAction` is null and a predicated PR rule is refused below.
|
|
8110
8181
|
azure: /* @__PURE__ */ new Set(["created", "updated"])
|
|
8111
8182
|
};
|
|
8183
|
+
REVIEW_STATES = /* @__PURE__ */ new Set(["approved", "changes_requested", "commented"]);
|
|
8184
|
+
REVIEW_ACTION = "review_submitted";
|
|
8112
8185
|
ID_CHARSET = /^[A-Za-z0-9._-]+$/;
|
|
8113
8186
|
REPLICAS_MAX = 3;
|
|
8187
|
+
INSTRUCTIONS_MAX = 2e3;
|
|
8114
8188
|
}
|
|
8115
8189
|
});
|
|
8116
8190
|
|
|
@@ -8194,7 +8268,7 @@ function normalizeWindow(w, index, path) {
|
|
|
8194
8268
|
return norm;
|
|
8195
8269
|
}
|
|
8196
8270
|
function zonedParts(tz, ms) {
|
|
8197
|
-
const
|
|
8271
|
+
const fmt2 = new Intl.DateTimeFormat("en-US", {
|
|
8198
8272
|
timeZone: tz,
|
|
8199
8273
|
hour12: false,
|
|
8200
8274
|
year: "numeric",
|
|
@@ -8205,7 +8279,7 @@ function zonedParts(tz, ms) {
|
|
|
8205
8279
|
weekday: "short"
|
|
8206
8280
|
});
|
|
8207
8281
|
const p = {};
|
|
8208
|
-
for (const part of
|
|
8282
|
+
for (const part of fmt2.formatToParts(new Date(ms))) p[part.type] = part.value;
|
|
8209
8283
|
let hh = Number(p.hour);
|
|
8210
8284
|
if (hh === 24) hh = 0;
|
|
8211
8285
|
return { y: Number(p.year), mo: Number(p.month), d: Number(p.day), hh, mi: Number(p.minute), dow: WEEKDAY_INDEX[p.weekday] };
|
|
@@ -8442,7 +8516,7 @@ import { Queue } from "bullmq";
|
|
|
8442
8516
|
function makeQueue(connection) {
|
|
8443
8517
|
return new Queue(QUEUE, { connection });
|
|
8444
8518
|
}
|
|
8445
|
-
async function enqueueLocalJob(queue, { folder, flow, task, provider, model, maxTurns, image, chainDepth, parentJobId, jobId, now = /* @__PURE__ */ new Date() }) {
|
|
8519
|
+
async function enqueueLocalJob(queue, { folder, flow, task, provider, model, maxTurns, image, skillsDir, chainDepth, parentJobId, jobId, now = /* @__PURE__ */ new Date() }) {
|
|
8446
8520
|
const minute = now.toISOString().slice(0, 16);
|
|
8447
8521
|
const id = jobId ?? localJobId({ folder, flow, task, minute });
|
|
8448
8522
|
const data = {
|
|
@@ -8454,6 +8528,11 @@ async function enqueueLocalJob(queue, { folder, flow, task, provider, model, max
|
|
|
8454
8528
|
model,
|
|
8455
8529
|
maxTurns,
|
|
8456
8530
|
...image !== void 0 && { image },
|
|
8531
|
+
// The host directory of operator-authored skills this trigger injects (REQ-PER-TRIGGER-SKILLS).
|
|
8532
|
+
// Conditional like `image`, so an unflagged job's data stays byte-identical, and at JOB level rather
|
|
8533
|
+
// than inside `trigger` because a worker-host path is an execution knob, not a fact about the
|
|
8534
|
+
// delivery -- and `trigger` is the object copied into /job/event.json.
|
|
8535
|
+
...skillsDir !== void 0 && { skillsDir },
|
|
8457
8536
|
...chainDepth !== void 0 && { chainDepth },
|
|
8458
8537
|
...parentJobId !== void 0 && { parentJobId }
|
|
8459
8538
|
};
|
|
@@ -8521,9 +8600,9 @@ var init_flow_gate = __esm({
|
|
|
8521
8600
|
|
|
8522
8601
|
// ../worker/src/git-dirty.mjs
|
|
8523
8602
|
import { execFileSync } from "node:child_process";
|
|
8524
|
-
function gitDirty(folder, { exec:
|
|
8603
|
+
function gitDirty(folder, { exec: exec4 = execFileSync } = {}) {
|
|
8525
8604
|
try {
|
|
8526
|
-
const out =
|
|
8605
|
+
const out = exec4("git", ["-C", folder, "status", "--porcelain"], { encoding: "utf8" });
|
|
8527
8606
|
return out.trim().length > 0;
|
|
8528
8607
|
} catch {
|
|
8529
8608
|
return null;
|
|
@@ -8534,14 +8613,40 @@ var init_git_dirty = __esm({
|
|
|
8534
8613
|
}
|
|
8535
8614
|
});
|
|
8536
8615
|
|
|
8616
|
+
// ../worker/src/host-pi.mjs
|
|
8617
|
+
var init_host_pi = __esm({
|
|
8618
|
+
"../worker/src/host-pi.mjs"() {
|
|
8619
|
+
init_packages();
|
|
8620
|
+
}
|
|
8621
|
+
});
|
|
8622
|
+
|
|
8623
|
+
// ../worker/src/copy-tree.mjs
|
|
8624
|
+
var ENTRY_NAME_RE, INJECT_LIMITS;
|
|
8625
|
+
var init_copy_tree = __esm({
|
|
8626
|
+
"../worker/src/copy-tree.mjs"() {
|
|
8627
|
+
ENTRY_NAME_RE = /^[a-z0-9](?:[a-z0-9_.-]{0,62}[a-z0-9])?$/i;
|
|
8628
|
+
INJECT_LIMITS = Object.freeze({
|
|
8629
|
+
// The binding one, and its reason is NOT disk. Every loaded skill contributes a <name> and a
|
|
8630
|
+
// description (spec-capped at 1024 chars) to the SYSTEM PROMPT of every job of that trigger, so
|
|
8631
|
+
// this bounds the cached prefix rather than the filesystem. UNVERIFIED figure, in the sense
|
|
8632
|
+
// ISOLATION_FLAGS' --pids-limit=512 is: a ceiling on absurdity, not a measured budget.
|
|
8633
|
+
maxDirs: 64,
|
|
8634
|
+
maxFiles: 512,
|
|
8635
|
+
maxBytes: 4 << 20,
|
|
8636
|
+
maxDepth: 8
|
|
8637
|
+
});
|
|
8638
|
+
}
|
|
8639
|
+
});
|
|
8640
|
+
|
|
8537
8641
|
// ../worker/src/import-pi.mjs
|
|
8538
8642
|
import { execFile as execFile2 } from "node:child_process";
|
|
8539
8643
|
import { promisify as promisify2 } from "node:util";
|
|
8540
|
-
var
|
|
8644
|
+
var execFileAsync;
|
|
8541
8645
|
var init_import_pi = __esm({
|
|
8542
8646
|
"../worker/src/import-pi.mjs"() {
|
|
8543
8647
|
init_packages();
|
|
8544
|
-
|
|
8648
|
+
init_host_pi();
|
|
8649
|
+
init_copy_tree();
|
|
8545
8650
|
execFileAsync = promisify2(execFile2);
|
|
8546
8651
|
}
|
|
8547
8652
|
});
|
|
@@ -8627,14 +8732,14 @@ function readStageManifest({ globalPiDir, readFile = readFileSync, fileExists =
|
|
|
8627
8732
|
const { name, version, dir } = entry;
|
|
8628
8733
|
if (!isNonEmptyString5(name) || !isNonEmptyString5(version) || !isNonEmptyString5(dir)) return null;
|
|
8629
8734
|
if (!ENTRY_NAME_RE.test(dir) || dir.length > MAX_DIR_LENGTH) return null;
|
|
8630
|
-
packages.push({ name, version, dir });
|
|
8735
|
+
packages.push({ name, version, dir, from: entry.from === FROM_HOST ? FROM_HOST : FROM_DECLARED });
|
|
8631
8736
|
}
|
|
8632
8737
|
return { stagedAt: typeof parsed.stagedAt === "string" ? parsed.stagedAt : null, packages };
|
|
8633
8738
|
} catch {
|
|
8634
8739
|
return null;
|
|
8635
8740
|
}
|
|
8636
8741
|
}
|
|
8637
|
-
var PACKAGES_SUBDIR, STAGE_MANIFEST, CONTAINER_PACKAGES_ROOT, MAX_DIR_LENGTH;
|
|
8742
|
+
var PACKAGES_SUBDIR, STAGE_MANIFEST, CONTAINER_PACKAGES_ROOT, FROM_DECLARED, FROM_HOST, MAX_DIR_LENGTH;
|
|
8638
8743
|
var init_packages = __esm({
|
|
8639
8744
|
"../worker/src/packages.mjs"() {
|
|
8640
8745
|
init_config2();
|
|
@@ -8643,10 +8748,499 @@ var init_packages = __esm({
|
|
|
8643
8748
|
PACKAGES_SUBDIR = "packages";
|
|
8644
8749
|
STAGE_MANIFEST = "packages.json";
|
|
8645
8750
|
CONTAINER_PACKAGES_ROOT = `${CONTAINER_GLOBAL_PI_DIR}/${PACKAGES_SUBDIR}`;
|
|
8751
|
+
FROM_DECLARED = "pi-packages";
|
|
8752
|
+
FROM_HOST = "host";
|
|
8646
8753
|
MAX_DIR_LENGTH = 64;
|
|
8647
8754
|
}
|
|
8648
8755
|
});
|
|
8649
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
|
+
|
|
8650
9244
|
// src/read-model.mjs
|
|
8651
9245
|
import * as nodeFs2 from "node:fs";
|
|
8652
9246
|
import { join as join2, delimiter, sep } from "node:path";
|
|
@@ -8688,7 +9282,18 @@ function resolvePaths(env = process.env) {
|
|
|
8688
9282
|
dispatchRunPerHour: parseNonNegInt(env.PI_DISPATCH_RUN_PER_HOUR, 3),
|
|
8689
9283
|
// The per-scheduler stall threshold, mirrored from the worker's PI_SCHEDULER_STALL_MAX (default 2) so the
|
|
8690
9284
|
// cron drill-in can show `stalls n/threshold`. Read directly from env for the same reason as above.
|
|
8691
|
-
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)
|
|
8692
9297
|
};
|
|
8693
9298
|
}
|
|
8694
9299
|
function parseNonNegInt(raw, fallback) {
|
|
@@ -8725,9 +9330,9 @@ async function setQueuePaused({ url, paused, makeQueueFn = makeQueue, parseConne
|
|
|
8725
9330
|
});
|
|
8726
9331
|
}
|
|
8727
9332
|
}
|
|
8728
|
-
function revParseHead(folder, { exec:
|
|
9333
|
+
function revParseHead(folder, { exec: exec4 = execFileSync2 } = {}) {
|
|
8729
9334
|
try {
|
|
8730
|
-
const out =
|
|
9335
|
+
const out = exec4("git", ["-C", folder, "rev-parse", "HEAD"], { encoding: "utf8" });
|
|
8731
9336
|
const sha = out.trim();
|
|
8732
9337
|
return sha.length > 0 ? sha : null;
|
|
8733
9338
|
} catch {
|
|
@@ -9042,11 +9647,11 @@ function readTriggers({ triggersPath, fs = nodeFs2 }) {
|
|
|
9042
9647
|
return { invalid: 'triggers file must have a "triggers" array' };
|
|
9043
9648
|
}
|
|
9044
9649
|
const triggers = [];
|
|
9045
|
-
|
|
9650
|
+
entries.forEach((entry, index) => {
|
|
9046
9651
|
const display = normalizeTriggerForDisplay(entry);
|
|
9047
|
-
if (display) triggers.push(display);
|
|
9048
|
-
}
|
|
9049
|
-
return { triggers };
|
|
9652
|
+
if (display) triggers.push({ ...display, index });
|
|
9653
|
+
});
|
|
9654
|
+
return { triggers, count: entries.length };
|
|
9050
9655
|
}
|
|
9051
9656
|
function normalizeTriggerForDisplay(entry) {
|
|
9052
9657
|
if (entry === null || typeof entry !== "object") return null;
|
|
@@ -9056,6 +9661,8 @@ function normalizeTriggerForDisplay(entry) {
|
|
|
9056
9661
|
const flow = typeof run.flow === "string" ? run.flow : null;
|
|
9057
9662
|
const packages = run.packages !== false;
|
|
9058
9663
|
const image = typeof run.image === "string" && run.image.trim() !== "" ? run.image : null;
|
|
9664
|
+
const skillsDir = typeof run.skillsDir === "string" && run.skillsDir.trim() !== "" ? run.skillsDir : null;
|
|
9665
|
+
const instructions = typeof run.instructions === "string" && run.instructions.trim() !== "";
|
|
9059
9666
|
const resume = run.resume === true;
|
|
9060
9667
|
const forge = typeof run.kind === "string" && run.kind.trim() !== "" ? run.kind : null;
|
|
9061
9668
|
const replicas = Number.isInteger(run.replicas) && run.replicas > 1 ? run.replicas : null;
|
|
@@ -9072,12 +9679,14 @@ function normalizeTriggerForDisplay(entry) {
|
|
|
9072
9679
|
model: typeof run.model === "string" ? run.model : null,
|
|
9073
9680
|
packages,
|
|
9074
9681
|
image,
|
|
9682
|
+
skillsDir,
|
|
9683
|
+
instructions,
|
|
9075
9684
|
resume
|
|
9076
9685
|
};
|
|
9077
9686
|
case "label":
|
|
9078
|
-
return { type: "label", any: normalizeSelector(on.any), all: normalizeSelector(on.all), none: normalizeSelector(on.none), flow, packages, image, resume, replicas, forge };
|
|
9687
|
+
return { type: "label", any: normalizeSelector(on.any), all: normalizeSelector(on.all), none: normalizeSelector(on.none), flow, packages, image, skillsDir, instructions, resume, replicas, forge };
|
|
9079
9688
|
case "comment":
|
|
9080
|
-
return { type: "comment", phrase: typeof on.phrase === "string" ? on.phrase : null, flow, packages, image, resume, replicas, forge };
|
|
9689
|
+
return { type: "comment", phrase: typeof on.phrase === "string" ? on.phrase : null, flow, packages, image, skillsDir, instructions, resume, replicas, forge };
|
|
9081
9690
|
case "pull_request":
|
|
9082
9691
|
return {
|
|
9083
9692
|
type: "pull_request",
|
|
@@ -9088,6 +9697,8 @@ function normalizeTriggerForDisplay(entry) {
|
|
|
9088
9697
|
flow,
|
|
9089
9698
|
packages,
|
|
9090
9699
|
image,
|
|
9700
|
+
skillsDir,
|
|
9701
|
+
instructions,
|
|
9091
9702
|
resume,
|
|
9092
9703
|
replicas,
|
|
9093
9704
|
forge
|
|
@@ -9122,6 +9733,225 @@ function readStagedPackages({ globalPiDir, fs = nodeFs2, readManifest: readManif
|
|
|
9122
9733
|
function nameAtVersion(pkg) {
|
|
9123
9734
|
return typeof pkg.version === "string" && pkg.version !== "" ? `${pkg.name}@${pkg.version}` : pkg.name;
|
|
9124
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
|
+
}
|
|
9125
9955
|
function listRunIds({ logsDir, fs = nodeFs2 }) {
|
|
9126
9956
|
let names;
|
|
9127
9957
|
try {
|
|
@@ -9160,7 +9990,7 @@ function clampLines(lines) {
|
|
|
9160
9990
|
const n = Number.isFinite(lines) ? Math.floor(lines) : 200;
|
|
9161
9991
|
return Math.min(2e3, Math.max(1, n));
|
|
9162
9992
|
}
|
|
9163
|
-
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;
|
|
9164
9994
|
var init_read_model = __esm({
|
|
9165
9995
|
"src/read-model.mjs"() {
|
|
9166
9996
|
init_config2();
|
|
@@ -9175,8 +10005,1058 @@ var init_read_model = __esm({
|
|
|
9175
10005
|
init_flow_gate();
|
|
9176
10006
|
init_git_dirty();
|
|
9177
10007
|
init_packages();
|
|
10008
|
+
init_materialize();
|
|
10009
|
+
init_forges();
|
|
10010
|
+
init_graph_model();
|
|
9178
10011
|
DISPATCH_RUN_TTL_SECONDS = 2 * 60 * 60;
|
|
9179
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"() {
|
|
9180
11060
|
}
|
|
9181
11061
|
});
|
|
9182
11062
|
|
|
@@ -35208,7 +37088,7 @@ function setGlyphs(ascii) {
|
|
|
35208
37088
|
function stripControls(s) {
|
|
35209
37089
|
return String(s ?? "").replace(CONTROL_CHARS, "");
|
|
35210
37090
|
}
|
|
35211
|
-
function
|
|
37091
|
+
function clip2(line, w) {
|
|
35212
37092
|
const width = Math.max(0, Math.trunc(w) || 0);
|
|
35213
37093
|
const clean = stripControls(line);
|
|
35214
37094
|
if (clean.length <= width) return clean;
|
|
@@ -35218,13 +37098,13 @@ function clip(line, w) {
|
|
|
35218
37098
|
}
|
|
35219
37099
|
function pad(line, w) {
|
|
35220
37100
|
const width = Math.max(0, Math.trunc(w) || 0);
|
|
35221
|
-
return
|
|
37101
|
+
return clip2(line, width).padEnd(width);
|
|
35222
37102
|
}
|
|
35223
37103
|
function box({ title = "", sections = [], footer, width = 40 } = {}) {
|
|
35224
37104
|
const w = Math.max(MIN_WIDTH, Math.trunc(width) || MIN_WIDTH);
|
|
35225
37105
|
const inner = w - 4;
|
|
35226
37106
|
const lines = [];
|
|
35227
|
-
const titleText = title ? ` ${
|
|
37107
|
+
const titleText = title ? ` ${clip2(title, Math.max(0, inner - 2))} ` : "";
|
|
35228
37108
|
const topFill = Math.max(0, w - 2 - 1 - titleText.length);
|
|
35229
37109
|
lines.push(active.tl + active.h + titleText + active.h.repeat(topFill) + active.tr);
|
|
35230
37110
|
const framed = (text) => `${active.v} ${pad(text, inner)} ${active.v}`;
|
|
@@ -35244,14 +37124,14 @@ function box({ title = "", sections = [], footer, width = 40 } = {}) {
|
|
|
35244
37124
|
function meter(reserved, cap, width = 24, state = "ok") {
|
|
35245
37125
|
const r = Number.isFinite(reserved) ? Math.max(0, Math.trunc(reserved)) : 0;
|
|
35246
37126
|
if (!Number.isInteger(cap) || cap <= 0) {
|
|
35247
|
-
return
|
|
37127
|
+
return clip2(`${r} / ? (cap unknown)`, width);
|
|
35248
37128
|
}
|
|
35249
37129
|
const tag = state === "soft-hold" ? " soft-hold" : state === "over" ? " over" : "";
|
|
35250
37130
|
const label = ` ${r}/${cap}${tag}`;
|
|
35251
37131
|
const barCells = Math.max(0, Math.trunc(width) - label.length - 2);
|
|
35252
37132
|
const filled = Math.min(barCells, Math.round(Math.min(r, cap) / cap * barCells));
|
|
35253
37133
|
const bar = `[${active.full.repeat(filled)}${active.empty.repeat(barCells - filled)}]`;
|
|
35254
|
-
return
|
|
37134
|
+
return clip2(bar + label, width);
|
|
35255
37135
|
}
|
|
35256
37136
|
function sparkline(values, width = 24, opts = {}) {
|
|
35257
37137
|
const w = Math.max(0, Math.trunc(width) || 0);
|
|
@@ -35259,7 +37139,7 @@ function sparkline(values, width = 24, opts = {}) {
|
|
|
35259
37139
|
const norm = (Array.isArray(values) ? values : []).map((v) => Number.isFinite(v) ? Math.max(0, v) : null);
|
|
35260
37140
|
const finite = norm.filter((v) => v !== null);
|
|
35261
37141
|
const ceiling = Number.isFinite(opts?.max) && opts.max > 0 ? opts.max : Math.max(0, ...finite);
|
|
35262
|
-
if (finite.length === 0 || ceiling <= 0) return
|
|
37142
|
+
if (finite.length === 0 || ceiling <= 0) return clip2("no data", w);
|
|
35263
37143
|
let cells = norm;
|
|
35264
37144
|
if (norm.length > w) {
|
|
35265
37145
|
cells = [];
|
|
@@ -35437,9 +37317,9 @@ var init_sandbox_store = __esm({
|
|
|
35437
37317
|
});
|
|
35438
37318
|
|
|
35439
37319
|
// ../worker/src/sandbox.mjs
|
|
35440
|
-
import { execFile as
|
|
37320
|
+
import { execFile as execFile4, spawn } from "node:child_process";
|
|
35441
37321
|
import { existsSync as existsSync2 } from "node:fs";
|
|
35442
|
-
import { promisify as
|
|
37322
|
+
import { promisify as promisify4 } from "node:util";
|
|
35443
37323
|
function sandboxContainerName(jobId) {
|
|
35444
37324
|
return `${SANDBOX_NAME_PREFIX}${sanitizeJobId(jobId)}`;
|
|
35445
37325
|
}
|
|
@@ -35484,14 +37364,14 @@ function launchSandbox({ args, spawnFn = spawn }) {
|
|
|
35484
37364
|
child.on("close", (code) => resolve({ code }));
|
|
35485
37365
|
});
|
|
35486
37366
|
}
|
|
35487
|
-
var
|
|
37367
|
+
var exec3, SANDBOX_NAME_PREFIX;
|
|
35488
37368
|
var init_sandbox = __esm({
|
|
35489
37369
|
"../worker/src/sandbox.mjs"() {
|
|
35490
37370
|
init_config2();
|
|
35491
37371
|
init_docker_run();
|
|
35492
37372
|
init_run_history();
|
|
35493
37373
|
init_sandbox_store();
|
|
35494
|
-
|
|
37374
|
+
exec3 = promisify4(execFile4);
|
|
35495
37375
|
SANDBOX_NAME_PREFIX = "pi-sandbox-";
|
|
35496
37376
|
}
|
|
35497
37377
|
});
|
|
@@ -35517,8 +37397,8 @@ function renderRuns(runs) {
|
|
|
35517
37397
|
const headers = RUN_COLUMNS.map((c) => c.header);
|
|
35518
37398
|
const rows = list.map((r) => RUN_COLUMNS.map((c) => c.derive ? c.derive(r) : cell(r?.[c.key])));
|
|
35519
37399
|
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((row) => row[i].length)));
|
|
35520
|
-
const
|
|
35521
|
-
return [
|
|
37400
|
+
const fmt2 = (cells) => cells.map((v, i) => v.padEnd(widths[i])).join(" ").trimEnd();
|
|
37401
|
+
return [fmt2(headers), ...rows.map(fmt2)].join("\n");
|
|
35522
37402
|
}
|
|
35523
37403
|
function renderBudget({ budget, settings: settings2 } = {}) {
|
|
35524
37404
|
if (!budget || budget.unreachable) {
|
|
@@ -35570,19 +37450,21 @@ function triggerLine(t) {
|
|
|
35570
37450
|
const forge = t?.forge && t.forge !== "github" ? ` [${t.forge}]` : "";
|
|
35571
37451
|
const pkgs = t?.packages === true ? " [packages]" : "";
|
|
35572
37452
|
const img = t?.image ? ` [image ${t.image}]` : "";
|
|
37453
|
+
const skl = t?.skillsDir ? ` [skills ${String(t.skillsDir).split(/[\\/]/).filter(Boolean).pop()}]` : "";
|
|
37454
|
+
const ins = t?.instructions === true ? " [instructions]" : "";
|
|
35573
37455
|
const res = t?.resume === true ? " [resume]" : "";
|
|
35574
37456
|
const rep = t?.replicas > 1 ? ` [x${t.replicas}]` : "";
|
|
35575
37457
|
switch (t?.type) {
|
|
35576
37458
|
case "cron":
|
|
35577
|
-
return `cron ${t.id ?? "-"} ${t.pattern ?? "-"} \u2192 ${t.folder ?? "-"}/${flow}${forge}${pkgs}${img}${res}${rep}`;
|
|
37459
|
+
return `cron ${t.id ?? "-"} ${t.pattern ?? "-"} \u2192 ${t.folder ?? "-"}/${flow}${forge}${pkgs}${img}${skl}${ins}${res}${rep}`;
|
|
35578
37460
|
case "label":
|
|
35579
|
-
return `label ${ruleClauses(t) || "(no selector)"} \u2192 ${flow}${forge}${pkgs}${img}${res}${rep}`;
|
|
37461
|
+
return `label ${ruleClauses(t) || "(no selector)"} \u2192 ${flow}${forge}${pkgs}${img}${skl}${ins}${res}${rep}`;
|
|
35580
37462
|
case "comment":
|
|
35581
|
-
return `comment "${t.phrase ?? "-"}" \u2192 ${flow}${forge}${pkgs}${img}${res}${rep}`;
|
|
37463
|
+
return `comment "${t.phrase ?? "-"}" \u2192 ${flow}${forge}${pkgs}${img}${skl}${ins}${res}${rep}`;
|
|
35582
37464
|
case "pull_request": {
|
|
35583
37465
|
const clauses = ruleClauses(t);
|
|
35584
37466
|
const action = `action[${(t.action ?? []).join(",")}]`;
|
|
35585
|
-
return `pull_request ${action}${clauses ? ` ${clauses}` : ""} \u2192 ${flow}${forge}${pkgs}${img}${res}${rep}`;
|
|
37467
|
+
return `pull_request ${action}${clauses ? ` ${clauses}` : ""} \u2192 ${flow}${forge}${pkgs}${img}${skl}${ins}${res}${rep}`;
|
|
35586
37468
|
}
|
|
35587
37469
|
default:
|
|
35588
37470
|
return "(unknown trigger)";
|
|
@@ -35693,8 +37575,8 @@ function renderWhatIf(result, { flow, target } = {}) {
|
|
|
35693
37575
|
}
|
|
35694
37576
|
function tableLines(headers, rows) {
|
|
35695
37577
|
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((row) => row[i].length)));
|
|
35696
|
-
const
|
|
35697
|
-
return [
|
|
37578
|
+
const fmt2 = (cells) => cells.map((v, i) => v.padEnd(widths[i])).join(" ").trimEnd();
|
|
37579
|
+
return [fmt2(headers), ...rows.map(fmt2)];
|
|
35698
37580
|
}
|
|
35699
37581
|
function renderSettingsView(settings2) {
|
|
35700
37582
|
const path = settings2?.path ?? "(unknown path)";
|
|
@@ -35707,6 +37589,105 @@ function renderSettingsView(settings2) {
|
|
|
35707
37589
|
}
|
|
35708
37590
|
return out.join("\n");
|
|
35709
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
|
+
}
|
|
35710
37691
|
var SETTINGS_KEYS, BUDGET_WINDOWS, RUN_COLUMNS, COSTS_WINDOW_LABELS;
|
|
35711
37692
|
var init_render = __esm({
|
|
35712
37693
|
"src/render.mjs"() {
|
|
@@ -35926,8 +37907,8 @@ var init_style = __esm({
|
|
|
35926
37907
|
inverse: (t) => t,
|
|
35927
37908
|
strikethrough: (t) => t
|
|
35928
37909
|
};
|
|
35929
|
-
OVERLAY_GLYPHS = { tl: "\u250C", tr: "\u2510", bl: "\u2514", br: "\u2518", ml: "\u251C", mr: "\u2524", h: "\u2500", v: "\u2502", full: "\u2588", empty: "\u2591", ellipsis: "\u2026" };
|
|
35930
|
-
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: "~" };
|
|
35931
37912
|
COST_COLORS = {
|
|
35932
37913
|
metered: "text",
|
|
35933
37914
|
plan: "accent",
|
|
@@ -36003,6 +37984,30 @@ function createDashboardDeps(paths) {
|
|
|
36003
37984
|
const fold = foldCosts({ records, subscriptions, pricing: pricing_exports, nowMs, piAiPin: piAiVersion() });
|
|
36004
37985
|
return { fold, records, subscriptions };
|
|
36005
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
|
+
},
|
|
36006
38011
|
/** The priced-model catalog for the what-if `/` filter -- the façade stays behind this seam. */
|
|
36007
38012
|
listPricedModels() {
|
|
36008
38013
|
return listPricedModels();
|
|
@@ -36037,7 +38042,7 @@ function makeDashboard({
|
|
|
36037
38042
|
intervalMs = REFRESH_MS,
|
|
36038
38043
|
deps = createDashboardDeps(paths)
|
|
36039
38044
|
} = {}) {
|
|
36040
|
-
const styler = makeStyler(theme);
|
|
38045
|
+
const styler = makeStyler(theme, { ascii: paths?.asciiGlyphs === true });
|
|
36041
38046
|
let snapshot = null;
|
|
36042
38047
|
let fetching = false;
|
|
36043
38048
|
let disposed = false;
|
|
@@ -36047,6 +38052,7 @@ function makeDashboard({
|
|
|
36047
38052
|
let detailRun = null;
|
|
36048
38053
|
let detailSandbox = null;
|
|
36049
38054
|
let detailTrigger = null;
|
|
38055
|
+
let detailReturnTo = "LIST";
|
|
36050
38056
|
let tailJobId = null;
|
|
36051
38057
|
let tail = null;
|
|
36052
38058
|
let tailTop = 0;
|
|
@@ -36060,6 +38066,9 @@ function makeDashboard({
|
|
|
36060
38066
|
let costs = { data: null, windowKey: "mtd", fetchedAt: 0, table: "flow", whatIf: null };
|
|
36061
38067
|
let costsSel = 0;
|
|
36062
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;
|
|
36063
38072
|
const refresh = async () => {
|
|
36064
38073
|
if (fetching || disposed) return;
|
|
36065
38074
|
fetching = true;
|
|
@@ -36100,6 +38109,20 @@ function makeDashboard({
|
|
|
36100
38109
|
tui?.requestRender?.();
|
|
36101
38110
|
}
|
|
36102
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
|
+
};
|
|
36103
38126
|
const computeWhatIf = () => {
|
|
36104
38127
|
const wi = costs.whatIf;
|
|
36105
38128
|
if (!wi) return;
|
|
@@ -36140,6 +38163,9 @@ function makeDashboard({
|
|
|
36140
38163
|
if (view === "COSTS" && costsSel > costsTableRows(costs).length - 1) {
|
|
36141
38164
|
costsSel = Math.max(0, costsTableRows(costs).length - 1);
|
|
36142
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
|
+
}
|
|
36143
38169
|
return renderPanel(snapshot, width, {
|
|
36144
38170
|
view,
|
|
36145
38171
|
selected,
|
|
@@ -36160,6 +38186,9 @@ function makeDashboard({
|
|
|
36160
38186
|
costs,
|
|
36161
38187
|
costsSel,
|
|
36162
38188
|
costsAvailable: typeof deps?.fetchCosts === "function",
|
|
38189
|
+
graph,
|
|
38190
|
+
graphSel,
|
|
38191
|
+
graphAvailable: typeof deps?.fetchGraph === "function",
|
|
36163
38192
|
copiedNote,
|
|
36164
38193
|
copyAvailable: typeof deps?.copyText === "function",
|
|
36165
38194
|
// Height through the injected seam, read per frame (a resize changes it): null (seam absent, or
|
|
@@ -36229,7 +38258,7 @@ function makeDashboard({
|
|
|
36229
38258
|
return;
|
|
36230
38259
|
}
|
|
36231
38260
|
if (matchesKey(data, "escape")) {
|
|
36232
|
-
view =
|
|
38261
|
+
view = detailReturnTo;
|
|
36233
38262
|
detailTrigger = null;
|
|
36234
38263
|
tui?.requestRender?.();
|
|
36235
38264
|
return;
|
|
@@ -36410,6 +38439,45 @@ function makeDashboard({
|
|
|
36410
38439
|
}
|
|
36411
38440
|
return;
|
|
36412
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
|
+
}
|
|
36413
38481
|
if (matchesKey(data, "escape") || data === "q" || data === "Q") {
|
|
36414
38482
|
void dispose().finally(() => done(void 0));
|
|
36415
38483
|
return;
|
|
@@ -36421,6 +38489,7 @@ function makeDashboard({
|
|
|
36421
38489
|
if (row.kind === "trigger") {
|
|
36422
38490
|
detailTrigger = { record: row.trigger, index: row.index };
|
|
36423
38491
|
pendingDelete = false;
|
|
38492
|
+
detailReturnTo = "LIST";
|
|
36424
38493
|
view = "TRIGGER_DETAIL";
|
|
36425
38494
|
tui?.requestRender?.();
|
|
36426
38495
|
} else if (row.kind === "active") {
|
|
@@ -36454,6 +38523,13 @@ function makeDashboard({
|
|
|
36454
38523
|
tui?.requestRender?.();
|
|
36455
38524
|
return;
|
|
36456
38525
|
}
|
|
38526
|
+
if (data === "g" || data === "G") {
|
|
38527
|
+
view = "GRAPH";
|
|
38528
|
+
graphSel = 0;
|
|
38529
|
+
void fetchGraphNow();
|
|
38530
|
+
tui?.requestRender?.();
|
|
38531
|
+
return;
|
|
38532
|
+
}
|
|
36457
38533
|
if (data === " ") {
|
|
36458
38534
|
const rows = buildRows(snapshot, runSort);
|
|
36459
38535
|
if (rows.length === 0) return;
|
|
@@ -36522,7 +38598,7 @@ function budgetMeters(budget, settings2, width) {
|
|
|
36522
38598
|
return out;
|
|
36523
38599
|
}
|
|
36524
38600
|
function renderPanel(snapshot, width, state, styler) {
|
|
36525
|
-
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;
|
|
36526
38602
|
const framed = Number.isFinite(width) && Math.trunc(width) >= MIN_WIDTH2;
|
|
36527
38603
|
const inner = Math.trunc(width) - 4;
|
|
36528
38604
|
const title = "pi-dispatch";
|
|
@@ -36559,6 +38635,9 @@ function renderPanel(snapshot, width, state, styler) {
|
|
|
36559
38635
|
if (view === "COSTS") {
|
|
36560
38636
|
return renderCosts2({ costs, costsSel, costsAvailable, framed, width: Math.trunc(width), styler, availableRows: terminalRows });
|
|
36561
38637
|
}
|
|
38638
|
+
if (view === "GRAPH") {
|
|
38639
|
+
return renderGraphView({ graph, graphSel, graphAvailable, framed, width: Math.trunc(width), styler });
|
|
38640
|
+
}
|
|
36562
38641
|
if (snapshot === null) {
|
|
36563
38642
|
if (!framed) return [`${title} -- loading`, "", KEY_HINTS];
|
|
36564
38643
|
return box({ title, sections: [{ lines: ["loading"] }], footer: KEY_HINTS, width });
|
|
@@ -36968,7 +39047,9 @@ function trustModel(t) {
|
|
|
36968
39047
|
];
|
|
36969
39048
|
default:
|
|
36970
39049
|
return [
|
|
36971
|
-
|
|
39050
|
+
// A review trigger reads the REVIEWER's author_association, not the PR author's (issue #66), so it
|
|
39051
|
+
// cannot share the generic line: an operator reading "author gate" would picture the wrong person.
|
|
39052
|
+
t?.type === "comment" ? "authorized by a collaborator comment (author_association) + HMAC" : t?.type === "pull_request" && (t?.action ?? []).includes("review_submitted") ? "authorized by the REVIEWER's author_association (not the PR author's) + HMAC" : "authorized by a collaborator's label + HMAC webhook + author gate",
|
|
36972
39053
|
"dedup by X-GitHub-Delivery GUID (redelivery-safe)",
|
|
36973
39054
|
where
|
|
36974
39055
|
];
|
|
@@ -36983,7 +39064,162 @@ function triggerDetailHints(inner, styler, pendingDelete = false) {
|
|
|
36983
39064
|
}
|
|
36984
39065
|
function keyHints(inner, styler) {
|
|
36985
39066
|
const k = (key, label) => styler.fg("accent", key) + " " + styler.fg("dim", label);
|
|
36986
|
-
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 "));
|
|
36987
39223
|
return fitLine(hints, inner, styler);
|
|
36988
39224
|
}
|
|
36989
39225
|
function costsWindowSinceMs(windowKey, nowMs) {
|
|
@@ -37248,7 +39484,7 @@ function countdownText(ms) {
|
|
|
37248
39484
|
return h ? `resets ${h}` : "";
|
|
37249
39485
|
}
|
|
37250
39486
|
function buildRows(snapshot, runSort = "time") {
|
|
37251
|
-
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 }));
|
|
37252
39488
|
const active2 = snapshot?.activeJobId ? [{ kind: "active", jobId: snapshot.activeJobId }] : [];
|
|
37253
39489
|
const runs = (Array.isArray(snapshot?.runs) ? snapshot.runs : []).map((record) => ({ kind: "run", record }));
|
|
37254
39490
|
return [...triggers, ...active2, ...sortRuns(runs, runSort)];
|
|
@@ -37264,24 +39500,24 @@ function sortRuns(rows, runSort) {
|
|
|
37264
39500
|
return sorted;
|
|
37265
39501
|
}
|
|
37266
39502
|
function renderRunList(rows, selected, w) {
|
|
37267
|
-
if (!Array.isArray(rows) || rows.length === 0) return [
|
|
39503
|
+
if (!Array.isArray(rows) || rows.length === 0) return [clip2("(no runs)", w)];
|
|
37268
39504
|
const { top, count } = runsWindow(rows.length, selected);
|
|
37269
39505
|
const out = [];
|
|
37270
|
-
if (top > 0) out.push(
|
|
39506
|
+
if (top > 0) out.push(clip2(`^ ${top} more`, w));
|
|
37271
39507
|
for (let i = top; i < top + count; i++) {
|
|
37272
39508
|
const row = rows[i];
|
|
37273
39509
|
const cursor = i === selected ? "\u203A" : " ";
|
|
37274
39510
|
if (row.kind === "active") {
|
|
37275
|
-
out.push(
|
|
39511
|
+
out.push(clip2(`${cursor} * ACTIVE ${row.jobId} running`, w));
|
|
37276
39512
|
continue;
|
|
37277
39513
|
}
|
|
37278
39514
|
const run = row.record;
|
|
37279
39515
|
const rep = run?.replica > 0 ? `r${run.replica}/${run.replicas ?? "?"} ` : "";
|
|
37280
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 ");
|
|
37281
|
-
out.push(
|
|
39517
|
+
out.push(clip2(`${cursor} ${rep}${cells}`, w));
|
|
37282
39518
|
}
|
|
37283
39519
|
const below = rows.length - top - count;
|
|
37284
|
-
if (below > 0) out.push(
|
|
39520
|
+
if (below > 0) out.push(clip2(`v ${below} more`, w));
|
|
37285
39521
|
return out;
|
|
37286
39522
|
}
|
|
37287
39523
|
function tailMatches(lines, query) {
|
|
@@ -37322,14 +39558,14 @@ function renderLiveTail({ snapshot, framed, width, tailJobId, tail, tailTop, tai
|
|
|
37322
39558
|
return [boxTitle, "", ...plain, "", footer];
|
|
37323
39559
|
}
|
|
37324
39560
|
const inner = Math.trunc(width) - 4;
|
|
37325
|
-
const lines = [
|
|
39561
|
+
const lines = [clip2(`live ${tailJobId} -- ${len} line(s)`, inner)];
|
|
37326
39562
|
for (let i = top; i < Math.min(top + TAIL_VIEWPORT, len); i++) {
|
|
37327
|
-
const safe =
|
|
39563
|
+
const safe = clip2(all[i], inner);
|
|
37328
39564
|
lines.push(tailQuery !== null && i === tailMatchLine ? styler.fg("warning", safe) : safe);
|
|
37329
39565
|
}
|
|
37330
|
-
if (ended) lines.push(
|
|
39566
|
+
if (ended) lines.push(clip2("(run ended -- Esc to go back)", inner));
|
|
37331
39567
|
if (tailSearchInput) lines.push(styler.fg("accent", "/ ") + styler.lineInput(tailSearchInput, Math.max(1, inner - 2)));
|
|
37332
|
-
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) });
|
|
37333
39569
|
}
|
|
37334
39570
|
function renderRunDetail(record, inner, styler, allRuns = [], sandbox = null) {
|
|
37335
39571
|
const r = record ?? {};
|
|
@@ -37425,7 +39661,7 @@ function runDetailHints(inner, styler, canOpen = false, canCopy = false, copiedN
|
|
|
37425
39661
|
if (copiedNote) return fitLine(styler.fg("success", copiedNote) + styler.fg("dim", " \xB7 ") + line, inner, styler);
|
|
37426
39662
|
return fitLine(line, inner, styler);
|
|
37427
39663
|
}
|
|
37428
|
-
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;
|
|
37429
39665
|
var init_dashboard = __esm({
|
|
37430
39666
|
"src/dashboard.ts"() {
|
|
37431
39667
|
init_budget();
|
|
@@ -37436,6 +39672,7 @@ var init_dashboard = __esm({
|
|
|
37436
39672
|
init_pricing();
|
|
37437
39673
|
init_read_model();
|
|
37438
39674
|
init_render();
|
|
39675
|
+
init_graph_model();
|
|
37439
39676
|
init_keys2();
|
|
37440
39677
|
init_panel();
|
|
37441
39678
|
init_style();
|
|
@@ -37451,6 +39688,7 @@ var init_dashboard = __esm({
|
|
|
37451
39688
|
DRILL_WIDTH = 70;
|
|
37452
39689
|
COSTS_WINDOWS = ["7d", "30d", "mtd"];
|
|
37453
39690
|
COSTS_STALE_MS = 1e4;
|
|
39691
|
+
GRAPH_VIEWPORT = 16;
|
|
37454
39692
|
KIND_COLOR = { cron: "accent", label: "syntaxType", comment: "syntaxKeyword", pull_request: "syntaxFunction" };
|
|
37455
39693
|
KIND_WIDTH = 13;
|
|
37456
39694
|
FRAME_CHROME_ROWS = 4;
|
|
@@ -37478,7 +39716,7 @@ __export(setup_wizard_exports, {
|
|
|
37478
39716
|
runtimeDirFor: () => runtimeDirFor
|
|
37479
39717
|
});
|
|
37480
39718
|
import * as nodeFs4 from "node:fs";
|
|
37481
|
-
import { execFileSync as execFileSync3, spawn as
|
|
39719
|
+
import { execFileSync as execFileSync3, spawn as nodeSpawn2 } from "node:child_process";
|
|
37482
39720
|
import { homedir as homedir2 } from "node:os";
|
|
37483
39721
|
import { join as join6 } from "node:path";
|
|
37484
39722
|
function envIsSet(env, key) {
|
|
@@ -37513,7 +39751,7 @@ async function detectDeployment({
|
|
|
37513
39751
|
}
|
|
37514
39752
|
return { state: "none", detail: "no pointer, no env, no deployment files here, queue unreachable" };
|
|
37515
39753
|
}
|
|
37516
|
-
async function runAttached(ctx, { title, argv0, args, cwd, env, shell, spawnFn =
|
|
39754
|
+
async function runAttached(ctx, { title, argv0, args, cwd, env, shell, spawnFn = nodeSpawn2 }) {
|
|
37517
39755
|
if (ctx?.mode !== "tui" || typeof ctx?.ui?.custom !== "function") {
|
|
37518
39756
|
return { code: null, error: new Error("needs the terminal UI") };
|
|
37519
39757
|
}
|
|
@@ -37987,8 +40225,8 @@ var init_setup_wizard = __esm({
|
|
|
37987
40225
|
init_deployment_pointer();
|
|
37988
40226
|
init_read_model();
|
|
37989
40227
|
init_index();
|
|
37990
|
-
RUNTIME_VERSION = "0.
|
|
37991
|
-
RECEIVER_VERSION = "0.
|
|
40228
|
+
RUNTIME_VERSION = "0.3.0";
|
|
40229
|
+
RECEIVER_VERSION = "0.2.1";
|
|
37992
40230
|
RUNTIME_PKG = "@edgehero/pi-dispatch";
|
|
37993
40231
|
RECEIVER_PKG = "@edgehero/pi-dispatch-receiver";
|
|
37994
40232
|
NUDGE_MARKER_BASENAME = "pi-dispatch-setup.nudged";
|
|
@@ -38005,7 +40243,7 @@ var init_setup_wizard = __esm({
|
|
|
38005
40243
|
|
|
38006
40244
|
// src/index.ts
|
|
38007
40245
|
import { VERSION } from "@earendil-works/pi-coding-agent";
|
|
38008
|
-
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
40246
|
+
import { fileURLToPath as fileURLToPath2, pathToFileURL } from "node:url";
|
|
38009
40247
|
import * as nodeFs5 from "node:fs";
|
|
38010
40248
|
function computePiVersionAdvisory(version, supported) {
|
|
38011
40249
|
if (version === supported || version === "0.0.0") return void 0;
|
|
@@ -38190,7 +40428,7 @@ function registerTools(pi) {
|
|
|
38190
40428
|
pi.registerTool({
|
|
38191
40429
|
name: "dispatch_trigger_add",
|
|
38192
40430
|
label: "pi-dispatch add trigger",
|
|
38193
|
-
description: "Adds a trigger to triggers.json and applies it live. The operator MUST approve a confirm dialog showing the entry; with no interactive operator it is refused. `flow` is the .pi/skills/<name> skill the job runs (its SKILL.md is the agent's instructions). `kind` = cron|label|comment|pull_request. cron (local) needs id, pattern, `folder` (absolute host path the job runs in), `flow`, and `task` (the prompt text handed to the agent), and may set optional model/provider/maxTurns for that schedule (omit = deployment default). label needs labels[]+flow; comment needs phrase+flow; pull_request needs action[] (+ optional labels[]) + flow. Webhook triggers take an optional `forge` = github (default) | gitlab, which also decides which action words pull_request accepts: github is labeled|opened|synchronize|reopened, gitlab is open|update|reopen|approved. For webhook triggers the repo and the task come from the triggering issue/PR event \u2014 set only the match + flow \u2014 and they run under the deployment default model.",
|
|
40431
|
+
description: "Adds a trigger to triggers.json and applies it live. The operator MUST approve a confirm dialog showing the entry; with no interactive operator it is refused. `flow` is the .pi/skills/<name> skill the job runs (its SKILL.md is the agent's instructions). `kind` = cron|label|comment|pull_request. cron (local) needs id, pattern, `folder` (absolute host path the job runs in), `flow`, and `task` (the prompt text handed to the agent), and may set optional model/provider/maxTurns for that schedule (omit = deployment default). label needs labels[]+flow; comment needs phrase+flow; pull_request needs action[] (+ optional labels[]) + flow. Webhook triggers take an optional `forge` = github (default) | gitlab, which also decides which action words pull_request accepts: github is labeled|opened|synchronize|reopened|review_submitted, gitlab is open|update|reopen|approved. A github review_submitted trigger may also set reviewState[] (approved|changes_requested|commented) to narrow which verdicts fire; omitted, all three do. For webhook triggers the repo and the task come from the triggering issue/PR event \u2014 set only the match + flow \u2014 and they run under the deployment default model.",
|
|
38194
40432
|
executionMode: "sequential",
|
|
38195
40433
|
parameters: typebox_exports.Object({
|
|
38196
40434
|
kind: typebox_exports.String(),
|
|
@@ -38522,6 +40760,18 @@ ${renderBudget({ budget, settings: settings2 })}`);
|
|
|
38522
40760
|
costsCommand(pi, paths, tokens, notify);
|
|
38523
40761
|
return;
|
|
38524
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
|
+
}
|
|
38525
40775
|
case "run": {
|
|
38526
40776
|
const folder = tokens[1];
|
|
38527
40777
|
const flow = tokens[2];
|
|
@@ -38598,6 +40848,63 @@ function assembleCosts(paths, window, flow) {
|
|
|
38598
40848
|
});
|
|
38599
40849
|
return { fold };
|
|
38600
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
|
+
}
|
|
38601
40908
|
function costsCommand(pi, paths, tokens, notify) {
|
|
38602
40909
|
if (tokens[1] === "whatif") {
|
|
38603
40910
|
costsWhatIf(pi, paths, tokens, notify);
|
|
@@ -39037,6 +41344,11 @@ function completeArguments(prefix) {
|
|
|
39037
41344
|
const items = ["7d", "30d", "mtd", "whatif"].filter((w) => w.startsWith(partial)).map((w) => ({ value: `costs ${w}`, label: w }));
|
|
39038
41345
|
return items.length > 0 ? items : null;
|
|
39039
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
|
+
}
|
|
39040
41352
|
if ((parts[0] === "set" || parts[0] === "unset") && parts.length === 2) {
|
|
39041
41353
|
const partial = parts[1];
|
|
39042
41354
|
const items = KNOWN_KEYS.filter((k) => k.startsWith(partial)).map((k) => ({
|
|
@@ -39047,11 +41359,14 @@ function completeArguments(prefix) {
|
|
|
39047
41359
|
}
|
|
39048
41360
|
return null;
|
|
39049
41361
|
}
|
|
39050
|
-
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;
|
|
39051
41363
|
var init_index = __esm({
|
|
39052
41364
|
"src/index.ts"() {
|
|
39053
41365
|
init_build();
|
|
39054
41366
|
init_read_model();
|
|
41367
|
+
init_graph_model();
|
|
41368
|
+
init_graph_html();
|
|
41369
|
+
init_open_browser();
|
|
39055
41370
|
init_deployment_pointer();
|
|
39056
41371
|
init_costs();
|
|
39057
41372
|
init_pricing();
|
|
@@ -39068,7 +41383,7 @@ var init_index = __esm({
|
|
|
39068
41383
|
runtimeSkewNoticed = false;
|
|
39069
41384
|
CHANNEL = "pi-dispatch-admin";
|
|
39070
41385
|
REBUILT_NOTICE = (reason) => `replaced invalid settings file (${reason}) \u2014 other keys were lost`;
|
|
39071
|
-
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>";
|
|
39072
41387
|
KNOWN_SUBCOMMANDS = [
|
|
39073
41388
|
"status",
|
|
39074
41389
|
"pause",
|
|
@@ -39078,6 +41393,7 @@ var init_index = __esm({
|
|
|
39078
41393
|
"logs",
|
|
39079
41394
|
"budget",
|
|
39080
41395
|
"costs",
|
|
41396
|
+
"graph",
|
|
39081
41397
|
"triggers",
|
|
39082
41398
|
"settings",
|
|
39083
41399
|
"set",
|
|
@@ -39086,7 +41402,7 @@ var init_index = __esm({
|
|
|
39086
41402
|
];
|
|
39087
41403
|
FORGE_PROMPT = "forge \u2014 github, gitlab, forgejo or azure";
|
|
39088
41404
|
PR_ACTION_VOCAB = {
|
|
39089
|
-
github: { hint: "labeled opened synchronize reopened", dflt: "labeled" },
|
|
41405
|
+
github: { hint: "labeled opened synchronize reopened review_submitted", dflt: "labeled" },
|
|
39090
41406
|
gitlab: { hint: "open update reopen approved", dflt: "update" },
|
|
39091
41407
|
forgejo: { hint: "label_updated opened synchronized reopened", dflt: "label_updated" },
|
|
39092
41408
|
azure: { hint: "created updated", dflt: "updated" }
|
|
@@ -39095,6 +41411,7 @@ var init_index = __esm({
|
|
|
39095
41411
|
COSTS_USAGE = "usage: /dispatch costs [7d|30d|mtd] | /dispatch costs whatif <provider/model> --flow <flow>";
|
|
39096
41412
|
DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
39097
41413
|
PRICING = { listPricedModels, getPricedModel, isZeroRated, reprice, piAiVersion };
|
|
41414
|
+
GRAPH_USAGE = "usage: /dispatch graph [html [--no-open] [--full-paths]]";
|
|
39098
41415
|
VIEWPORT_LINES = 20;
|
|
39099
41416
|
}
|
|
39100
41417
|
});
|
|
@@ -39106,5 +41423,7 @@ export {
|
|
|
39106
41423
|
buildTriggerEntry,
|
|
39107
41424
|
computePiVersionAdvisory,
|
|
39108
41425
|
admin as default,
|
|
39109
|
-
|
|
41426
|
+
graphHtmlCommand,
|
|
41427
|
+
handleDashboardAction,
|
|
41428
|
+
isHeadlessEnv
|
|
39110
41429
|
};
|