@edgehero/pi-dispatch-admin 0.5.0 → 0.6.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 +1 -1
- package/dist/index.mjs +134 -23
- package/package.json +1 -1
- package/skills/operate-pi-dispatch/SKILL.md +6 -1
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@ Every trigger is one `{ on, run }` entry in a single `triggers.json`, read live
|
|
|
46
46
|
|
|
47
47
|
Four forges: GitHub, GitLab, Forgejo (and Gitea), Azure DevOps. **Who may fire a trigger is your forge's decision, not this service's**: on GitHub the label *is* the approval, because only collaborators can apply one, while GitLab, Forgejo and Azure resolve the actor's permission through their APIs. Each forge's action vocabulary is validated when the file loads, so a word from the wrong forge is refused instead of silently never matching.
|
|
48
48
|
|
|
49
|
-
**Flows, and workflows.** `run.flow` names a skill committed to the target repo at `.pi/skills/<flow>/SKILL.md`, read from the **default branch**, so the repo owns the prompt and merging it is the repo's consent. A skill may call other skills, which is already a workflow. For typed multi-stage ones, a pi extension such as `@juicesharp/rpiv-workflow` can be staged into the deployment: pinned to an exact version, installed on your host (never at job time, since jobs run offline), loaded in every container, and declinable per trigger.
|
|
49
|
+
**Flows, and workflows.** `run.flow` names a skill committed to the target repo at `.pi/skills/<flow>/SKILL.md`, read from the **default branch**, so the repo owns the prompt and merging it is the repo's consent. A skill may call other skills, which is already a workflow. For typed multi-stage ones, a pi extension such as `@juicesharp/rpiv-workflow` can be staged into the deployment: pinned to an exact version, installed on your host (never at job time, since jobs run offline), loaded in every container, and declinable per trigger. Anything you already installed with `pi install` is staged automatically, at the version your host has, so there is nothing to declare twice.
|
|
50
50
|
|
|
51
51
|
**How a workflow gets triggered, in one line:** `label / comment / PR / cron` fires **one job in one container**, that job runs `run.flow`, and the flow drives whatever stages follow. Four consequences worth knowing before you build on it. A trigger names a **flow, never a workflow**, so which stages run is decided inside the job by that skill. A job is **not an interactive session** (the container hands pi one assembled prompt and reads the exit line), so a workflow extension's slash command has nobody to type it: in a job the flow's instructions drive the workflow, or a small extension you also staged calls its API from a lifecycle hook. One trigger is **one job, one budget slot and one turn budget**, so ten stages share the same `PI_MAX_TURNS` and per-job token budget, and exhausting either ends the job as a policy refusal that is never retried. And whether the workflow's own state survives depends on the trigger kind: a cron or CLI job has your folder mounted read-write so state persists between runs, while a forge job gets a fresh clone that is discarded with the container. Full reference: [`docs/workflows.md`](https://github.com/edgehero/pi-dispatch/blob/main/docs/workflows.md).
|
|
52
52
|
|
package/dist/index.mjs
CHANGED
|
@@ -7935,11 +7935,13 @@ function normalizeCron(on, run, index, path, state) {
|
|
|
7935
7935
|
}
|
|
7936
7936
|
const packages = validatePackagesFlag(run, `cron trigger "${id}"`, path);
|
|
7937
7937
|
const image = validateImageRef(run, `cron trigger "${id}"`, path);
|
|
7938
|
+
const skillsDir = validateSkillsDir(run, `cron trigger "${id}"`, path);
|
|
7939
|
+
validateInstructions(run, `cron trigger "${id}"`, path, { cron: true });
|
|
7938
7940
|
const resume = validateResumeFlag(run, `cron trigger "${id}"`, path);
|
|
7939
7941
|
validateReplicas(run, `cron trigger "${id}"`, path);
|
|
7940
7942
|
return {
|
|
7941
7943
|
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 }
|
|
7944
|
+
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
7945
|
};
|
|
7944
7946
|
}
|
|
7945
7947
|
function validatePackagesFlag(run, at, path) {
|
|
@@ -7973,6 +7975,31 @@ function validateImageRef(run, at, path) {
|
|
|
7973
7975
|
}
|
|
7974
7976
|
return image;
|
|
7975
7977
|
}
|
|
7978
|
+
function validateSkillsDir(run, at, path) {
|
|
7979
|
+
const dir = run.skillsDir;
|
|
7980
|
+
if (dir === void 0) return void 0;
|
|
7981
|
+
if (typeof dir !== "string" || dir.trim() === "") {
|
|
7982
|
+
throw configError(`${at}: run.skillsDir must be a non-empty string when present: ${path}`);
|
|
7983
|
+
}
|
|
7984
|
+
if (dir !== dir.trim()) {
|
|
7985
|
+
throw configError(`${at}: run.skillsDir must not have leading or trailing whitespace (got ${JSON.stringify(dir)}): ${path}`);
|
|
7986
|
+
}
|
|
7987
|
+
return dir;
|
|
7988
|
+
}
|
|
7989
|
+
function validateInstructions(run, at, path, { cron = false } = {}) {
|
|
7990
|
+
const text = run.instructions;
|
|
7991
|
+
if (text === void 0) return void 0;
|
|
7992
|
+
if (cron) {
|
|
7993
|
+
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}`);
|
|
7994
|
+
}
|
|
7995
|
+
if (typeof text !== "string" || text.trim() === "") {
|
|
7996
|
+
throw configError(`${at}: run.instructions must be a non-empty string when present: ${path}`);
|
|
7997
|
+
}
|
|
7998
|
+
if (text.length > INSTRUCTIONS_MAX) {
|
|
7999
|
+
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}`);
|
|
8000
|
+
}
|
|
8001
|
+
return text;
|
|
8002
|
+
}
|
|
7976
8003
|
function validatePredicate(on, index, path, requirePositive) {
|
|
7977
8004
|
const at = `trigger at index ${index}`;
|
|
7978
8005
|
for (const key of ["any", "all", "none"]) {
|
|
@@ -8027,12 +8054,14 @@ function normalizeLabel(on, run, index, path) {
|
|
|
8027
8054
|
}
|
|
8028
8055
|
const packages = validatePackagesFlag(run, at, path);
|
|
8029
8056
|
const image = validateImageRef(run, at, path);
|
|
8057
|
+
const skillsDir = validateSkillsDir(run, at, path);
|
|
8058
|
+
const instructions = validateInstructions(run, at, path);
|
|
8030
8059
|
const resume = validateResumeFlag(run, at, path);
|
|
8031
8060
|
const repository = validateRepository(run, "label", at, path);
|
|
8032
8061
|
const replicas = validateReplicas(run, at, path);
|
|
8033
8062
|
return {
|
|
8034
8063
|
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 } }
|
|
8064
|
+
run: { kind: run.kind, flow: run.flow, packages, image, resume, replicas, ...skillsDir !== void 0 && { skillsDir }, ...instructions !== void 0 && { instructions }, ...repository !== void 0 && { repository } }
|
|
8036
8065
|
};
|
|
8037
8066
|
}
|
|
8038
8067
|
function normalizeComment(on, run, index, path, state) {
|
|
@@ -8049,12 +8078,14 @@ function normalizeComment(on, run, index, path, state) {
|
|
|
8049
8078
|
}
|
|
8050
8079
|
const packages = validatePackagesFlag(run, at, path);
|
|
8051
8080
|
const image = validateImageRef(run, at, path);
|
|
8081
|
+
const skillsDir = validateSkillsDir(run, at, path);
|
|
8082
|
+
const instructions = validateInstructions(run, at, path);
|
|
8052
8083
|
const resume = validateResumeFlag(run, at, path);
|
|
8053
8084
|
const repository = validateRepository(run, "comment", at, path);
|
|
8054
8085
|
const replicas = validateReplicas(run, at, path);
|
|
8055
8086
|
return {
|
|
8056
8087
|
on: { type: "comment", phrase: on.phrase },
|
|
8057
|
-
run: { kind: run.kind, flow: run.flow, packages, image, resume, replicas, ...repository !== void 0 && { repository } }
|
|
8088
|
+
run: { kind: run.kind, flow: run.flow, packages, image, resume, replicas, ...skillsDir !== void 0 && { skillsDir }, ...instructions !== void 0 && { instructions }, ...repository !== void 0 && { repository } }
|
|
8058
8089
|
};
|
|
8059
8090
|
}
|
|
8060
8091
|
function normalizePullRequest(on, run, index, path) {
|
|
@@ -8070,6 +8101,7 @@ function normalizePullRequest(on, run, index, path) {
|
|
|
8070
8101
|
throw configError(`${at}: pull_request on.action has an unsupported ${run.kind} action ${JSON.stringify(a)} (expected ${expected}): ${path}`);
|
|
8071
8102
|
}
|
|
8072
8103
|
}
|
|
8104
|
+
const reviewState = validateReviewState(on, actions, run, at, path);
|
|
8073
8105
|
const labelAction = forgeSpec(run.kind)?.prLabelAction;
|
|
8074
8106
|
const requirePositive = typeof labelAction === "string" && actions.includes(labelAction);
|
|
8075
8107
|
if (run.kind === "azure" && (on.any !== void 0 || on.all !== void 0 || on.none !== void 0)) {
|
|
@@ -8081,22 +8113,55 @@ function normalizePullRequest(on, run, index, path) {
|
|
|
8081
8113
|
}
|
|
8082
8114
|
const packages = validatePackagesFlag(run, at, path);
|
|
8083
8115
|
const image = validateImageRef(run, at, path);
|
|
8116
|
+
const skillsDir = validateSkillsDir(run, at, path);
|
|
8117
|
+
const instructions = validateInstructions(run, at, path);
|
|
8084
8118
|
const resume = validateResumeFlag(run, at, path);
|
|
8085
8119
|
validateRepository(run, "pull_request", at, path);
|
|
8086
8120
|
const replicas = validateReplicas(run, at, path);
|
|
8087
8121
|
return {
|
|
8088
|
-
on: {
|
|
8089
|
-
|
|
8122
|
+
on: {
|
|
8123
|
+
type: "pull_request",
|
|
8124
|
+
action: [...actions],
|
|
8125
|
+
// Absent rather than present-and-undefined: an unnarrowed rule's normalized shape must stay
|
|
8126
|
+
// byte-identical to the one every pre-#66 trigger file produces.
|
|
8127
|
+
...reviewState !== void 0 && { reviewState },
|
|
8128
|
+
any: predicate.any,
|
|
8129
|
+
all: predicate.all,
|
|
8130
|
+
none: predicate.none
|
|
8131
|
+
},
|
|
8132
|
+
run: { kind: run.kind, flow: run.flow, packages, image, resume, replicas, ...skillsDir !== void 0 && { skillsDir }, ...instructions !== void 0 && { instructions } }
|
|
8090
8133
|
};
|
|
8091
8134
|
}
|
|
8092
|
-
|
|
8135
|
+
function validateReviewState(on, actions, run, at, path) {
|
|
8136
|
+
if (on.reviewState === void 0) return void 0;
|
|
8137
|
+
if (run.kind !== "github") {
|
|
8138
|
+
throw configError(`${at}: on.reviewState is github-only (got ${run.kind}), no other forge reports a review verdict: ${path}`);
|
|
8139
|
+
}
|
|
8140
|
+
if (!actions.includes(REVIEW_ACTION)) {
|
|
8141
|
+
throw configError(`${at}: on.reviewState requires on.action to include ${JSON.stringify(REVIEW_ACTION)}, otherwise it narrows nothing: ${path}`);
|
|
8142
|
+
}
|
|
8143
|
+
if (!Array.isArray(on.reviewState) || on.reviewState.length === 0) {
|
|
8144
|
+
throw configError(`${at}: on.reviewState must be a non-empty array: ${path}`);
|
|
8145
|
+
}
|
|
8146
|
+
const expected = [...REVIEW_STATES].join("|");
|
|
8147
|
+
for (const s of on.reviewState) {
|
|
8148
|
+
if (!REVIEW_STATES.has(s)) {
|
|
8149
|
+
throw configError(`${at}: on.reviewState has an unsupported review state ${JSON.stringify(s)} (expected ${expected}): ${path}`);
|
|
8150
|
+
}
|
|
8151
|
+
}
|
|
8152
|
+
return [...on.reviewState];
|
|
8153
|
+
}
|
|
8154
|
+
var ON_TYPES, PR_ACTIONS, REVIEW_STATES, REVIEW_ACTION, ID_CHARSET, REPLICAS_MAX, INSTRUCTIONS_MAX;
|
|
8093
8155
|
var init_triggers = __esm({
|
|
8094
8156
|
"../worker/src/triggers.mjs"() {
|
|
8095
8157
|
init_config2();
|
|
8096
8158
|
init_forges();
|
|
8097
8159
|
ON_TYPES = /* @__PURE__ */ new Set(["cron", "label", "comment", "pull_request"]);
|
|
8098
8160
|
PR_ACTIONS = {
|
|
8099
|
-
github: /* @__PURE__ */ new Set(["labeled", "opened", "synchronize", "reopened"]),
|
|
8161
|
+
github: /* @__PURE__ */ new Set(["labeled", "opened", "synchronize", "reopened", "review_submitted"]),
|
|
8162
|
+
// GitLab's `approved` is its review gate (a member approved the MR). It is NOT github's
|
|
8163
|
+
// `review_submitted` renamed: `approved` is one verdict, `review_submitted` is every verdict, which is
|
|
8164
|
+
// what `on.reviewState` below exists to narrow.
|
|
8100
8165
|
gitlab: /* @__PURE__ */ new Set(["open", "update", "reopen", "approved"]),
|
|
8101
8166
|
// Forgejo's own spellings. `label_updated` is its `labeled` and `synchronized` its `synchronize` -- a
|
|
8102
8167
|
// one-letter difference that an operator would otherwise discover as a trigger that loads clean and
|
|
@@ -8109,8 +8174,11 @@ var init_triggers = __esm({
|
|
|
8109
8174
|
// requests -- which is why azure's `prLabelAction` is null and a predicated PR rule is refused below.
|
|
8110
8175
|
azure: /* @__PURE__ */ new Set(["created", "updated"])
|
|
8111
8176
|
};
|
|
8177
|
+
REVIEW_STATES = /* @__PURE__ */ new Set(["approved", "changes_requested", "commented"]);
|
|
8178
|
+
REVIEW_ACTION = "review_submitted";
|
|
8112
8179
|
ID_CHARSET = /^[A-Za-z0-9._-]+$/;
|
|
8113
8180
|
REPLICAS_MAX = 3;
|
|
8181
|
+
INSTRUCTIONS_MAX = 2e3;
|
|
8114
8182
|
}
|
|
8115
8183
|
});
|
|
8116
8184
|
|
|
@@ -8442,7 +8510,7 @@ import { Queue } from "bullmq";
|
|
|
8442
8510
|
function makeQueue(connection) {
|
|
8443
8511
|
return new Queue(QUEUE, { connection });
|
|
8444
8512
|
}
|
|
8445
|
-
async function enqueueLocalJob(queue, { folder, flow, task, provider, model, maxTurns, image, chainDepth, parentJobId, jobId, now = /* @__PURE__ */ new Date() }) {
|
|
8513
|
+
async function enqueueLocalJob(queue, { folder, flow, task, provider, model, maxTurns, image, skillsDir, chainDepth, parentJobId, jobId, now = /* @__PURE__ */ new Date() }) {
|
|
8446
8514
|
const minute = now.toISOString().slice(0, 16);
|
|
8447
8515
|
const id = jobId ?? localJobId({ folder, flow, task, minute });
|
|
8448
8516
|
const data = {
|
|
@@ -8454,6 +8522,11 @@ async function enqueueLocalJob(queue, { folder, flow, task, provider, model, max
|
|
|
8454
8522
|
model,
|
|
8455
8523
|
maxTurns,
|
|
8456
8524
|
...image !== void 0 && { image },
|
|
8525
|
+
// The host directory of operator-authored skills this trigger injects (REQ-PER-TRIGGER-SKILLS).
|
|
8526
|
+
// Conditional like `image`, so an unflagged job's data stays byte-identical, and at JOB level rather
|
|
8527
|
+
// than inside `trigger` because a worker-host path is an execution knob, not a fact about the
|
|
8528
|
+
// delivery -- and `trigger` is the object copied into /job/event.json.
|
|
8529
|
+
...skillsDir !== void 0 && { skillsDir },
|
|
8457
8530
|
...chainDepth !== void 0 && { chainDepth },
|
|
8458
8531
|
...parentJobId !== void 0 && { parentJobId }
|
|
8459
8532
|
};
|
|
@@ -8534,14 +8607,40 @@ var init_git_dirty = __esm({
|
|
|
8534
8607
|
}
|
|
8535
8608
|
});
|
|
8536
8609
|
|
|
8610
|
+
// ../worker/src/host-pi.mjs
|
|
8611
|
+
var init_host_pi = __esm({
|
|
8612
|
+
"../worker/src/host-pi.mjs"() {
|
|
8613
|
+
init_packages();
|
|
8614
|
+
}
|
|
8615
|
+
});
|
|
8616
|
+
|
|
8617
|
+
// ../worker/src/copy-tree.mjs
|
|
8618
|
+
var ENTRY_NAME_RE, INJECT_LIMITS;
|
|
8619
|
+
var init_copy_tree = __esm({
|
|
8620
|
+
"../worker/src/copy-tree.mjs"() {
|
|
8621
|
+
ENTRY_NAME_RE = /^[a-z0-9](?:[a-z0-9_.-]{0,62}[a-z0-9])?$/i;
|
|
8622
|
+
INJECT_LIMITS = Object.freeze({
|
|
8623
|
+
// The binding one, and its reason is NOT disk. Every loaded skill contributes a <name> and a
|
|
8624
|
+
// description (spec-capped at 1024 chars) to the SYSTEM PROMPT of every job of that trigger, so
|
|
8625
|
+
// this bounds the cached prefix rather than the filesystem. UNVERIFIED figure, in the sense
|
|
8626
|
+
// ISOLATION_FLAGS' --pids-limit=512 is: a ceiling on absurdity, not a measured budget.
|
|
8627
|
+
maxDirs: 64,
|
|
8628
|
+
maxFiles: 512,
|
|
8629
|
+
maxBytes: 4 << 20,
|
|
8630
|
+
maxDepth: 8
|
|
8631
|
+
});
|
|
8632
|
+
}
|
|
8633
|
+
});
|
|
8634
|
+
|
|
8537
8635
|
// ../worker/src/import-pi.mjs
|
|
8538
8636
|
import { execFile as execFile2 } from "node:child_process";
|
|
8539
8637
|
import { promisify as promisify2 } from "node:util";
|
|
8540
|
-
var
|
|
8638
|
+
var execFileAsync;
|
|
8541
8639
|
var init_import_pi = __esm({
|
|
8542
8640
|
"../worker/src/import-pi.mjs"() {
|
|
8543
8641
|
init_packages();
|
|
8544
|
-
|
|
8642
|
+
init_host_pi();
|
|
8643
|
+
init_copy_tree();
|
|
8545
8644
|
execFileAsync = promisify2(execFile2);
|
|
8546
8645
|
}
|
|
8547
8646
|
});
|
|
@@ -8627,14 +8726,14 @@ function readStageManifest({ globalPiDir, readFile = readFileSync, fileExists =
|
|
|
8627
8726
|
const { name, version, dir } = entry;
|
|
8628
8727
|
if (!isNonEmptyString5(name) || !isNonEmptyString5(version) || !isNonEmptyString5(dir)) return null;
|
|
8629
8728
|
if (!ENTRY_NAME_RE.test(dir) || dir.length > MAX_DIR_LENGTH) return null;
|
|
8630
|
-
packages.push({ name, version, dir });
|
|
8729
|
+
packages.push({ name, version, dir, from: entry.from === FROM_HOST ? FROM_HOST : FROM_DECLARED });
|
|
8631
8730
|
}
|
|
8632
8731
|
return { stagedAt: typeof parsed.stagedAt === "string" ? parsed.stagedAt : null, packages };
|
|
8633
8732
|
} catch {
|
|
8634
8733
|
return null;
|
|
8635
8734
|
}
|
|
8636
8735
|
}
|
|
8637
|
-
var PACKAGES_SUBDIR, STAGE_MANIFEST, CONTAINER_PACKAGES_ROOT, MAX_DIR_LENGTH;
|
|
8736
|
+
var PACKAGES_SUBDIR, STAGE_MANIFEST, CONTAINER_PACKAGES_ROOT, FROM_DECLARED, FROM_HOST, MAX_DIR_LENGTH;
|
|
8638
8737
|
var init_packages = __esm({
|
|
8639
8738
|
"../worker/src/packages.mjs"() {
|
|
8640
8739
|
init_config2();
|
|
@@ -8643,6 +8742,8 @@ var init_packages = __esm({
|
|
|
8643
8742
|
PACKAGES_SUBDIR = "packages";
|
|
8644
8743
|
STAGE_MANIFEST = "packages.json";
|
|
8645
8744
|
CONTAINER_PACKAGES_ROOT = `${CONTAINER_GLOBAL_PI_DIR}/${PACKAGES_SUBDIR}`;
|
|
8745
|
+
FROM_DECLARED = "pi-packages";
|
|
8746
|
+
FROM_HOST = "host";
|
|
8646
8747
|
MAX_DIR_LENGTH = 64;
|
|
8647
8748
|
}
|
|
8648
8749
|
});
|
|
@@ -9056,6 +9157,8 @@ function normalizeTriggerForDisplay(entry) {
|
|
|
9056
9157
|
const flow = typeof run.flow === "string" ? run.flow : null;
|
|
9057
9158
|
const packages = run.packages !== false;
|
|
9058
9159
|
const image = typeof run.image === "string" && run.image.trim() !== "" ? run.image : null;
|
|
9160
|
+
const skillsDir = typeof run.skillsDir === "string" && run.skillsDir.trim() !== "" ? run.skillsDir : null;
|
|
9161
|
+
const instructions = typeof run.instructions === "string" && run.instructions.trim() !== "";
|
|
9059
9162
|
const resume = run.resume === true;
|
|
9060
9163
|
const forge = typeof run.kind === "string" && run.kind.trim() !== "" ? run.kind : null;
|
|
9061
9164
|
const replicas = Number.isInteger(run.replicas) && run.replicas > 1 ? run.replicas : null;
|
|
@@ -9072,12 +9175,14 @@ function normalizeTriggerForDisplay(entry) {
|
|
|
9072
9175
|
model: typeof run.model === "string" ? run.model : null,
|
|
9073
9176
|
packages,
|
|
9074
9177
|
image,
|
|
9178
|
+
skillsDir,
|
|
9179
|
+
instructions,
|
|
9075
9180
|
resume
|
|
9076
9181
|
};
|
|
9077
9182
|
case "label":
|
|
9078
|
-
return { type: "label", any: normalizeSelector(on.any), all: normalizeSelector(on.all), none: normalizeSelector(on.none), flow, packages, image, resume, replicas, forge };
|
|
9183
|
+
return { type: "label", any: normalizeSelector(on.any), all: normalizeSelector(on.all), none: normalizeSelector(on.none), flow, packages, image, skillsDir, instructions, resume, replicas, forge };
|
|
9079
9184
|
case "comment":
|
|
9080
|
-
return { type: "comment", phrase: typeof on.phrase === "string" ? on.phrase : null, flow, packages, image, resume, replicas, forge };
|
|
9185
|
+
return { type: "comment", phrase: typeof on.phrase === "string" ? on.phrase : null, flow, packages, image, skillsDir, instructions, resume, replicas, forge };
|
|
9081
9186
|
case "pull_request":
|
|
9082
9187
|
return {
|
|
9083
9188
|
type: "pull_request",
|
|
@@ -9088,6 +9193,8 @@ function normalizeTriggerForDisplay(entry) {
|
|
|
9088
9193
|
flow,
|
|
9089
9194
|
packages,
|
|
9090
9195
|
image,
|
|
9196
|
+
skillsDir,
|
|
9197
|
+
instructions,
|
|
9091
9198
|
resume,
|
|
9092
9199
|
replicas,
|
|
9093
9200
|
forge
|
|
@@ -35570,19 +35677,21 @@ function triggerLine(t) {
|
|
|
35570
35677
|
const forge = t?.forge && t.forge !== "github" ? ` [${t.forge}]` : "";
|
|
35571
35678
|
const pkgs = t?.packages === true ? " [packages]" : "";
|
|
35572
35679
|
const img = t?.image ? ` [image ${t.image}]` : "";
|
|
35680
|
+
const skl = t?.skillsDir ? ` [skills ${String(t.skillsDir).split(/[\\/]/).filter(Boolean).pop()}]` : "";
|
|
35681
|
+
const ins = t?.instructions === true ? " [instructions]" : "";
|
|
35573
35682
|
const res = t?.resume === true ? " [resume]" : "";
|
|
35574
35683
|
const rep = t?.replicas > 1 ? ` [x${t.replicas}]` : "";
|
|
35575
35684
|
switch (t?.type) {
|
|
35576
35685
|
case "cron":
|
|
35577
|
-
return `cron ${t.id ?? "-"} ${t.pattern ?? "-"} \u2192 ${t.folder ?? "-"}/${flow}${forge}${pkgs}${img}${res}${rep}`;
|
|
35686
|
+
return `cron ${t.id ?? "-"} ${t.pattern ?? "-"} \u2192 ${t.folder ?? "-"}/${flow}${forge}${pkgs}${img}${skl}${ins}${res}${rep}`;
|
|
35578
35687
|
case "label":
|
|
35579
|
-
return `label ${ruleClauses(t) || "(no selector)"} \u2192 ${flow}${forge}${pkgs}${img}${res}${rep}`;
|
|
35688
|
+
return `label ${ruleClauses(t) || "(no selector)"} \u2192 ${flow}${forge}${pkgs}${img}${skl}${ins}${res}${rep}`;
|
|
35580
35689
|
case "comment":
|
|
35581
|
-
return `comment "${t.phrase ?? "-"}" \u2192 ${flow}${forge}${pkgs}${img}${res}${rep}`;
|
|
35690
|
+
return `comment "${t.phrase ?? "-"}" \u2192 ${flow}${forge}${pkgs}${img}${skl}${ins}${res}${rep}`;
|
|
35582
35691
|
case "pull_request": {
|
|
35583
35692
|
const clauses = ruleClauses(t);
|
|
35584
35693
|
const action = `action[${(t.action ?? []).join(",")}]`;
|
|
35585
|
-
return `pull_request ${action}${clauses ? ` ${clauses}` : ""} \u2192 ${flow}${forge}${pkgs}${img}${res}${rep}`;
|
|
35694
|
+
return `pull_request ${action}${clauses ? ` ${clauses}` : ""} \u2192 ${flow}${forge}${pkgs}${img}${skl}${ins}${res}${rep}`;
|
|
35586
35695
|
}
|
|
35587
35696
|
default:
|
|
35588
35697
|
return "(unknown trigger)";
|
|
@@ -36968,7 +37077,9 @@ function trustModel(t) {
|
|
|
36968
37077
|
];
|
|
36969
37078
|
default:
|
|
36970
37079
|
return [
|
|
36971
|
-
|
|
37080
|
+
// A review trigger reads the REVIEWER's author_association, not the PR author's (issue #66), so it
|
|
37081
|
+
// cannot share the generic line: an operator reading "author gate" would picture the wrong person.
|
|
37082
|
+
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
37083
|
"dedup by X-GitHub-Delivery GUID (redelivery-safe)",
|
|
36973
37084
|
where
|
|
36974
37085
|
];
|
|
@@ -37987,8 +38098,8 @@ var init_setup_wizard = __esm({
|
|
|
37987
38098
|
init_deployment_pointer();
|
|
37988
38099
|
init_read_model();
|
|
37989
38100
|
init_index();
|
|
37990
|
-
RUNTIME_VERSION = "0.
|
|
37991
|
-
RECEIVER_VERSION = "0.
|
|
38101
|
+
RUNTIME_VERSION = "0.2.0";
|
|
38102
|
+
RECEIVER_VERSION = "0.2.0";
|
|
37992
38103
|
RUNTIME_PKG = "@edgehero/pi-dispatch";
|
|
37993
38104
|
RECEIVER_PKG = "@edgehero/pi-dispatch-receiver";
|
|
37994
38105
|
NUDGE_MARKER_BASENAME = "pi-dispatch-setup.nudged";
|
|
@@ -38190,7 +38301,7 @@ function registerTools(pi) {
|
|
|
38190
38301
|
pi.registerTool({
|
|
38191
38302
|
name: "dispatch_trigger_add",
|
|
38192
38303
|
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.",
|
|
38304
|
+
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
38305
|
executionMode: "sequential",
|
|
38195
38306
|
parameters: typebox_exports.Object({
|
|
38196
38307
|
kind: typebox_exports.String(),
|
|
@@ -39086,7 +39197,7 @@ var init_index = __esm({
|
|
|
39086
39197
|
];
|
|
39087
39198
|
FORGE_PROMPT = "forge \u2014 github, gitlab, forgejo or azure";
|
|
39088
39199
|
PR_ACTION_VOCAB = {
|
|
39089
|
-
github: { hint: "labeled opened synchronize reopened", dflt: "labeled" },
|
|
39200
|
+
github: { hint: "labeled opened synchronize reopened review_submitted", dflt: "labeled" },
|
|
39090
39201
|
gitlab: { hint: "open update reopen approved", dflt: "update" },
|
|
39091
39202
|
forgejo: { hint: "label_updated opened synchronized reopened", dflt: "label_updated" },
|
|
39092
39203
|
azure: { hint: "created updated", dflt: "updated" }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@edgehero/pi-dispatch-admin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Operator console (a pi extension) + skill for pi-dispatch: run the pi coding agent as a self-hosted service. A /dispatch TUI for the queue, spend caps, run history, editable GitHub, GitLab, Forgejo and Azure DevOps triggers (cron/label/comment/PR/MR/work item), and scheduled pause windows — plus AI-operable, human-confirmed controls. Runs against a live pi-dispatch deployment.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -128,9 +128,14 @@ trigger is the same — the `on.type`, the `{any, all, none}` label predicate, `
|
|
|
128
128
|
Two things are NOT the same, and both refuse at load rather than misbehaving quietly:
|
|
129
129
|
|
|
130
130
|
- **`pull_request` actions are the forge's own words.** GitHub takes
|
|
131
|
-
`labeled | opened | synchronize | reopened`; GitLab takes
|
|
131
|
+
`labeled | opened | synchronize | reopened | review_submitted`; GitLab takes
|
|
132
|
+
`open | update | reopen | approved`. A word
|
|
132
133
|
from the wrong forge is refused when the file is written. It would not break anything otherwise — it
|
|
133
134
|
would simply never match an event, and the trigger would look configured while doing nothing.
|
|
135
|
+
`review_submitted` is GitHub's `pull_request_review` event: it fires on **every** submitted review, so
|
|
136
|
+
add `reviewState` (`approved | changes_requested | commented`) to narrow which verdicts are worth paying
|
|
137
|
+
for. That field is github-only and legal only beside `review_submitted`; anywhere else it refuses at
|
|
138
|
+
load, because a narrowing that cannot apply reads as one that does.
|
|
134
139
|
- **One `comment` trigger per forge.** Two GitHub comment triggers are refused; one GitHub and one GitLab
|
|
135
140
|
are fine.
|
|
136
141
|
|