@mestreyoda/fabrica 0.2.7 → 0.2.11
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/CHANGELOG.md +10 -0
- package/README.md +52 -15
- package/defaults/fabrica/prompts/architect.md +6 -6
- package/defaults/fabrica/prompts/reviewer.md +19 -37
- package/defaults/fabrica/prompts/tester.md +8 -7
- package/dist/index.js +963 -449
- package/openclaw.plugin.json +73 -0
- package/package.json +11 -5
- package/dist/index.js.map +0 -7
package/dist/index.js
CHANGED
|
@@ -87701,7 +87701,7 @@ var require_gaxios = __commonJS({
|
|
|
87701
87701
|
var retry_js_1 = require_retry();
|
|
87702
87702
|
var stream_1 = __require("stream");
|
|
87703
87703
|
var interceptor_js_1 = require_interceptor();
|
|
87704
|
-
var
|
|
87704
|
+
var randomUUID6 = async () => globalThis.crypto?.randomUUID() || (await import("crypto")).randomUUID();
|
|
87705
87705
|
var HTTP_STATUS_NO_CONTENT = 204;
|
|
87706
87706
|
var Gaxios = class {
|
|
87707
87707
|
agentCache = /* @__PURE__ */ new Map();
|
|
@@ -87974,7 +87974,7 @@ var require_gaxios = __commonJS({
|
|
|
87974
87974
|
*/
|
|
87975
87975
|
["Blob", "File", "FormData"].includes(opts.data?.constructor?.name || "");
|
|
87976
87976
|
if (opts.multipart?.length) {
|
|
87977
|
-
const boundary = await
|
|
87977
|
+
const boundary = await randomUUID6();
|
|
87978
87978
|
preparedHeaders.set("content-type", `multipart/related; boundary=${boundary}`);
|
|
87979
87979
|
opts.body = stream_1.Readable.from(this.getMultipartRequest(opts.multipart, boundary));
|
|
87980
87980
|
} else if (shouldDirectlyPassData) {
|
|
@@ -110860,18 +110860,6 @@ function mergeConfig(base, overlay, traceOpts) {
|
|
|
110860
110860
|
if (base.instance || overlay.instance) {
|
|
110861
110861
|
merged.instance = { ...base.instance, ...overlay.instance };
|
|
110862
110862
|
}
|
|
110863
|
-
if (base.providers || overlay.providers) {
|
|
110864
|
-
merged.providers = {
|
|
110865
|
-
github: base.providers?.github || overlay.providers?.github ? {
|
|
110866
|
-
...base.providers?.github,
|
|
110867
|
-
...overlay.providers?.github,
|
|
110868
|
-
authProfiles: base.providers?.github?.authProfiles || overlay.providers?.github?.authProfiles ? {
|
|
110869
|
-
...base.providers?.github?.authProfiles,
|
|
110870
|
-
...overlay.providers?.github?.authProfiles
|
|
110871
|
-
} : void 0
|
|
110872
|
-
} : void 0
|
|
110873
|
-
};
|
|
110874
|
-
}
|
|
110875
110863
|
if (traceOpts) {
|
|
110876
110864
|
const { baseLabel, overlayLabel } = traceOpts;
|
|
110877
110865
|
const trace2 = {};
|
|
@@ -111116,8 +111104,11 @@ function parseLegacyFlatState(worker, role) {
|
|
|
111116
111104
|
issueId: worker.issueId,
|
|
111117
111105
|
sessionKey,
|
|
111118
111106
|
startTime: worker.startTime,
|
|
111107
|
+
dispatchCycleId: worker.dispatchCycleId ?? null,
|
|
111108
|
+
dispatchRunId: worker.dispatchRunId ?? null,
|
|
111119
111109
|
previousLabel: worker.previousLabel ?? null,
|
|
111120
|
-
name: worker.name ?? worker.slotName
|
|
111110
|
+
name: worker.name ?? worker.slotName,
|
|
111111
|
+
lastIssueId: worker.lastIssueId ?? null
|
|
111121
111112
|
};
|
|
111122
111113
|
return { levels: { [migratedLevel]: [slot] } };
|
|
111123
111114
|
}
|
|
@@ -111133,8 +111124,11 @@ function parseOldSlotState(worker, role) {
|
|
|
111133
111124
|
issueId: s2.issueId,
|
|
111134
111125
|
sessionKey: s2.sessionKey,
|
|
111135
111126
|
startTime: s2.startTime,
|
|
111127
|
+
dispatchCycleId: s2.dispatchCycleId ?? null,
|
|
111128
|
+
dispatchRunId: s2.dispatchRunId ?? null,
|
|
111136
111129
|
previousLabel: s2.previousLabel ?? null,
|
|
111137
|
-
name: s2.name ?? s2.slotName
|
|
111130
|
+
name: s2.name ?? s2.slotName,
|
|
111131
|
+
lastIssueId: s2.lastIssueId ?? null
|
|
111138
111132
|
});
|
|
111139
111133
|
}
|
|
111140
111134
|
return { levels };
|
|
@@ -111152,8 +111146,11 @@ function parseLevelState(worker, role) {
|
|
|
111152
111146
|
issueId: s2.issueId,
|
|
111153
111147
|
sessionKey: s2.sessionKey,
|
|
111154
111148
|
startTime: s2.startTime,
|
|
111149
|
+
dispatchCycleId: s2.dispatchCycleId ?? null,
|
|
111150
|
+
dispatchRunId: s2.dispatchRunId ?? null,
|
|
111155
111151
|
previousLabel: s2.previousLabel ?? null,
|
|
111156
|
-
name: s2.name ?? s2.slotName
|
|
111152
|
+
name: s2.name ?? s2.slotName,
|
|
111153
|
+
lastIssueId: s2.lastIssueId ?? null
|
|
111157
111154
|
});
|
|
111158
111155
|
}
|
|
111159
111156
|
}
|
|
@@ -111330,8 +111327,8 @@ import fsSync from "node:fs";
|
|
|
111330
111327
|
import path5 from "node:path";
|
|
111331
111328
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
111332
111329
|
function getCurrentVersion() {
|
|
111333
|
-
if ("0.2.
|
|
111334
|
-
return "0.2.
|
|
111330
|
+
if ("0.2.11") {
|
|
111331
|
+
return "0.2.11";
|
|
111335
111332
|
}
|
|
111336
111333
|
try {
|
|
111337
111334
|
const pkgPath = path5.join(THIS_DIR, "..", "..", "package.json");
|
|
@@ -112333,6 +112330,7 @@ async function readWorkflowFile(dir) {
|
|
|
112333
112330
|
try {
|
|
112334
112331
|
const content = await fs9.readFile(filePath, "utf-8");
|
|
112335
112332
|
const parsed = import_yaml2.default.parse(content);
|
|
112333
|
+
rejectPluginOnlyKeys(parsed, filePath);
|
|
112336
112334
|
if (parsed) validateConfig(parsed);
|
|
112337
112335
|
return parsed;
|
|
112338
112336
|
} catch (err) {
|
|
@@ -112345,6 +112343,7 @@ async function readLegacyConfigFile(dir) {
|
|
|
112345
112343
|
try {
|
|
112346
112344
|
const content = await fs9.readFile(filePath, "utf-8");
|
|
112347
112345
|
const parsed = import_yaml2.default.parse(content);
|
|
112346
|
+
rejectPluginOnlyKeys(parsed, filePath);
|
|
112348
112347
|
if (parsed) validateConfig(parsed);
|
|
112349
112348
|
return parsed;
|
|
112350
112349
|
} catch (err) {
|
|
@@ -112352,6 +112351,14 @@ async function readLegacyConfigFile(dir) {
|
|
|
112352
112351
|
throw new Error(formatConfigReadError(filePath, err, "legacy config.yaml"));
|
|
112353
112352
|
}
|
|
112354
112353
|
}
|
|
112354
|
+
function rejectPluginOnlyKeys(raw, filePath) {
|
|
112355
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
|
|
112356
|
+
const forbidden = PLUGIN_ONLY_KEYS.filter((key) => key in raw);
|
|
112357
|
+
if (forbidden.length === 0) return;
|
|
112358
|
+
throw new Error(
|
|
112359
|
+
`Invalid workflow config at ${filePath}: plugin-only keys are not allowed in workflow.yaml/config.yaml (${forbidden.join(", ")}). Configure them in pluginConfig/openclaw.json instead.`
|
|
112360
|
+
);
|
|
112361
|
+
}
|
|
112355
112362
|
async function readLegacyWorkflowJson(dir) {
|
|
112356
112363
|
const filePath = path9.join(dir, "workflow.json");
|
|
112357
112364
|
try {
|
|
@@ -112374,7 +112381,7 @@ function formatConfigReadError(filePath, err, kind) {
|
|
|
112374
112381
|
const prefix = invalidKinds.has(err?.name ?? "") ? `Invalid ${kind}` : `Failed to read ${kind}`;
|
|
112375
112382
|
return `${prefix} at ${filePath}: ${message}`;
|
|
112376
112383
|
}
|
|
112377
|
-
var import_yaml2, DEFAULT_MAX_WORKERS_PER_LEVEL;
|
|
112384
|
+
var import_yaml2, DEFAULT_MAX_WORKERS_PER_LEVEL, PLUGIN_ONLY_KEYS;
|
|
112378
112385
|
var init_loader = __esm({
|
|
112379
112386
|
"lib/config/loader.ts"() {
|
|
112380
112387
|
"use strict";
|
|
@@ -112387,6 +112394,13 @@ var init_loader = __esm({
|
|
|
112387
112394
|
init_migrate_layout();
|
|
112388
112395
|
init_workflow_policy();
|
|
112389
112396
|
DEFAULT_MAX_WORKERS_PER_LEVEL = 2;
|
|
112397
|
+
PLUGIN_ONLY_KEYS = [
|
|
112398
|
+
"providers",
|
|
112399
|
+
"telegram",
|
|
112400
|
+
"work_heartbeat",
|
|
112401
|
+
"notifications",
|
|
112402
|
+
"projectExecution"
|
|
112403
|
+
];
|
|
112390
112404
|
}
|
|
112391
112405
|
});
|
|
112392
112406
|
|
|
@@ -112519,9 +112533,9 @@ function resolveNotifyChannel(issueLabels, channels) {
|
|
|
112519
112533
|
const channelName = value.slice(colonIdx + 1);
|
|
112520
112534
|
return channels.find(
|
|
112521
112535
|
(ch) => ch.channel === channelType && (ch.name === channelName || String(channels.indexOf(ch)) === channelName)
|
|
112522
|
-
)
|
|
112536
|
+
);
|
|
112523
112537
|
}
|
|
112524
|
-
return channels.find((ch) => ch.channelId === value)
|
|
112538
|
+
return channels.find((ch) => ch.channelId === value);
|
|
112525
112539
|
}
|
|
112526
112540
|
return channels[0];
|
|
112527
112541
|
}
|
|
@@ -112742,7 +112756,7 @@ function validateWorkflowIntegrity(workflow) {
|
|
|
112742
112756
|
}
|
|
112743
112757
|
return errors;
|
|
112744
112758
|
}
|
|
112745
|
-
var STATE_TYPES, TransitionTargetSchema, StateConfigSchema, WorkflowConfigSchema, ModelEntrySchema, EffortLevelSchema, RoleOverrideSchema, TimeoutConfigSchema, InstanceConfigSchema, GitHubAppProfileSchema, ProvidersConfigSchema, FabricaConfigSchema;
|
|
112759
|
+
var STATE_TYPES, TransitionTargetSchema, StateConfigSchema, WorkflowConfigSchema, ModelEntrySchema, EffortLevelSchema, RoleOverrideSchema, TimeoutConfigSchema, InstanceConfigSchema, GitHubAppProfileSchema, ProvidersConfigSchema, FabricaConfigSchema, HeartbeatPluginConfigSchema, TelegramPluginConfigSchema, NotificationPluginConfigSchema, FabricaPluginConfigSchema;
|
|
112746
112760
|
var init_schema = __esm({
|
|
112747
112761
|
"lib/config/schema.ts"() {
|
|
112748
112762
|
"use strict";
|
|
@@ -112850,9 +112864,30 @@ var init_schema = __esm({
|
|
|
112850
112864
|
roles: external_exports.record(external_exports.string(), RoleOverrideSchema).optional(),
|
|
112851
112865
|
workflow: WorkflowConfigSchema.partial().optional(),
|
|
112852
112866
|
timeouts: TimeoutConfigSchema,
|
|
112853
|
-
instance: InstanceConfigSchema
|
|
112854
|
-
providers: ProvidersConfigSchema
|
|
112867
|
+
instance: InstanceConfigSchema
|
|
112855
112868
|
});
|
|
112869
|
+
HeartbeatPluginConfigSchema = external_exports.object({
|
|
112870
|
+
enabled: external_exports.boolean().optional(),
|
|
112871
|
+
intervalSeconds: external_exports.number().int().positive().optional(),
|
|
112872
|
+
maxPickupsPerTick: external_exports.number().int().positive().optional()
|
|
112873
|
+
}).optional();
|
|
112874
|
+
TelegramPluginConfigSchema = external_exports.object({
|
|
112875
|
+
bootstrapDmEnabled: external_exports.boolean().optional(),
|
|
112876
|
+
projectsForumChatId: external_exports.string().min(1).optional(),
|
|
112877
|
+
projectsForumAccountId: external_exports.string().min(1).optional(),
|
|
112878
|
+
opsChatId: external_exports.string().min(1).optional()
|
|
112879
|
+
}).optional();
|
|
112880
|
+
NotificationPluginConfigSchema = external_exports.object({
|
|
112881
|
+
workerStart: external_exports.boolean().optional(),
|
|
112882
|
+
workerComplete: external_exports.boolean().optional()
|
|
112883
|
+
}).optional();
|
|
112884
|
+
FabricaPluginConfigSchema = external_exports.object({
|
|
112885
|
+
work_heartbeat: HeartbeatPluginConfigSchema,
|
|
112886
|
+
projectExecution: external_exports.enum(["parallel", "sequential"]).optional(),
|
|
112887
|
+
notifications: NotificationPluginConfigSchema,
|
|
112888
|
+
telegram: TelegramPluginConfigSchema,
|
|
112889
|
+
providers: ProvidersConfigSchema
|
|
112890
|
+
}).passthrough();
|
|
112856
112891
|
}
|
|
112857
112892
|
});
|
|
112858
112893
|
|
|
@@ -115455,7 +115490,7 @@ var receive_exports = {};
|
|
|
115455
115490
|
__export(receive_exports, {
|
|
115456
115491
|
receiveStep: () => receiveStep
|
|
115457
115492
|
});
|
|
115458
|
-
import { randomUUID as
|
|
115493
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
115459
115494
|
var receiveStep;
|
|
115460
115495
|
var init_receive = __esm({
|
|
115461
115496
|
"lib/intake/steps/receive.ts"() {
|
|
@@ -115466,7 +115501,7 @@ var init_receive = __esm({
|
|
|
115466
115501
|
async execute(payload) {
|
|
115467
115502
|
return {
|
|
115468
115503
|
...payload,
|
|
115469
|
-
session_id: payload.session_id ||
|
|
115504
|
+
session_id: payload.session_id || randomUUID3(),
|
|
115470
115505
|
timestamp: payload.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
|
|
115471
115506
|
step: "receive"
|
|
115472
115507
|
};
|
|
@@ -116305,12 +116340,10 @@ function createPluginContext(api) {
|
|
|
116305
116340
|
const logger6 = getLogger({ plugin: "fabrica" });
|
|
116306
116341
|
const rawConfig = api.pluginConfig;
|
|
116307
116342
|
if (rawConfig && Object.keys(rawConfig).length > 0) {
|
|
116308
|
-
const result =
|
|
116343
|
+
const result = FabricaPluginConfigSchema.safeParse(rawConfig);
|
|
116309
116344
|
if (!result.success) {
|
|
116310
|
-
|
|
116311
|
-
|
|
116312
|
-
"pluginConfig validation failed \u2014 using config as-is, some fields may be ignored"
|
|
116313
|
-
);
|
|
116345
|
+
const issues = result.error.issues.map((i2) => `${i2.path.join(".")}: ${i2.message}`);
|
|
116346
|
+
throw new Error(`pluginConfig validation failed: ${issues.join("; ")}`);
|
|
116314
116347
|
}
|
|
116315
116348
|
}
|
|
116316
116349
|
return {
|
|
@@ -116352,7 +116385,9 @@ function emptySlot() {
|
|
|
116352
116385
|
active: false,
|
|
116353
116386
|
issueId: null,
|
|
116354
116387
|
sessionKey: null,
|
|
116355
|
-
startTime: null
|
|
116388
|
+
startTime: null,
|
|
116389
|
+
dispatchCycleId: null,
|
|
116390
|
+
dispatchRunId: null
|
|
116356
116391
|
};
|
|
116357
116392
|
}
|
|
116358
116393
|
function emptyRoleWorkerState(levelMaxWorkers) {
|
|
@@ -116850,6 +116885,8 @@ async function activateWorker(workspaceDir, slugOrChannelId, role, params) {
|
|
|
116850
116885
|
issueId: params.issueId,
|
|
116851
116886
|
sessionKey: params.sessionKey ?? slots[idx].sessionKey,
|
|
116852
116887
|
startTime: params.startTime ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
116888
|
+
dispatchCycleId: params.dispatchCycleId ?? null,
|
|
116889
|
+
dispatchRunId: params.dispatchRunId ?? null,
|
|
116853
116890
|
previousLabel: params.previousLabel ?? null,
|
|
116854
116891
|
name: params.name ?? slots[idx].name,
|
|
116855
116892
|
lastIssueId: null
|
|
@@ -116887,6 +116924,8 @@ async function deactivateWorker(workspaceDir, slugOrChannelId, role, opts) {
|
|
|
116887
116924
|
issueId: null,
|
|
116888
116925
|
sessionKey: slot.sessionKey,
|
|
116889
116926
|
startTime: null,
|
|
116927
|
+
dispatchCycleId: slot.dispatchCycleId ?? null,
|
|
116928
|
+
dispatchRunId: slot.dispatchRunId ?? null,
|
|
116890
116929
|
previousLabel: null,
|
|
116891
116930
|
name: slot.name,
|
|
116892
116931
|
lastIssueId: slot.issueId
|
|
@@ -116913,6 +116952,37 @@ async function updateIssueRuntime(workspaceDir, slugOrChannelId, issueId, update
|
|
|
116913
116952
|
});
|
|
116914
116953
|
return data;
|
|
116915
116954
|
}
|
|
116955
|
+
async function bindDispatchRunIdBySessionKey(workspaceDir, sessionKey, runId) {
|
|
116956
|
+
const { result } = await withProjectsMutation(workspaceDir, (data) => {
|
|
116957
|
+
for (const [slug, project] of Object.entries(data.projects)) {
|
|
116958
|
+
for (const [role, roleWorker] of Object.entries(project.workers ?? {})) {
|
|
116959
|
+
for (const [level, slots] of Object.entries(roleWorker.levels ?? {})) {
|
|
116960
|
+
for (let slotIndex = 0; slotIndex < slots.length; slotIndex++) {
|
|
116961
|
+
const slot = slots[slotIndex];
|
|
116962
|
+
if (!slot.active || !slot.issueId || slot.sessionKey !== sessionKey) continue;
|
|
116963
|
+
slot.dispatchRunId = runId;
|
|
116964
|
+
project.issueRuntime ??= {};
|
|
116965
|
+
const issueKey = String(slot.issueId);
|
|
116966
|
+
project.issueRuntime[issueKey] = {
|
|
116967
|
+
...project.issueRuntime[issueKey] ?? {},
|
|
116968
|
+
dispatchRunId: runId,
|
|
116969
|
+
lastSessionKey: sessionKey
|
|
116970
|
+
};
|
|
116971
|
+
return {
|
|
116972
|
+
slug,
|
|
116973
|
+
issueId: Number(slot.issueId),
|
|
116974
|
+
role,
|
|
116975
|
+
level,
|
|
116976
|
+
slotIndex
|
|
116977
|
+
};
|
|
116978
|
+
}
|
|
116979
|
+
}
|
|
116980
|
+
}
|
|
116981
|
+
}
|
|
116982
|
+
return null;
|
|
116983
|
+
});
|
|
116984
|
+
return result;
|
|
116985
|
+
}
|
|
116916
116986
|
async function clearIssueRuntime(workspaceDir, slugOrChannelId, issueId) {
|
|
116917
116987
|
const { data } = await withProjectsMutation(workspaceDir, (data2) => {
|
|
116918
116988
|
const slug = resolveProjectSlug(data2, slugOrChannelId);
|
|
@@ -116944,23 +117014,18 @@ function findIssueBySessionKey(data, sessionKey) {
|
|
|
116944
117014
|
if (slot.sessionKey !== sessionKey) continue;
|
|
116945
117015
|
const issueId = slot.issueId ?? slot.lastIssueId;
|
|
116946
117016
|
if (!issueId) continue;
|
|
117017
|
+
const runtime = getIssueRuntime(project, issueId);
|
|
117018
|
+
if (!slot.dispatchCycleId || !runtime?.lastDispatchCycleId || slot.dispatchCycleId !== runtime.lastDispatchCycleId) {
|
|
117019
|
+
continue;
|
|
117020
|
+
}
|
|
116947
117021
|
return {
|
|
116948
117022
|
slug,
|
|
116949
117023
|
issueId: Number(issueId),
|
|
116950
|
-
runtime
|
|
117024
|
+
runtime
|
|
116951
117025
|
};
|
|
116952
117026
|
}
|
|
116953
117027
|
}
|
|
116954
117028
|
}
|
|
116955
|
-
for (const [issueId, runtime] of Object.entries(project.issueRuntime ?? {})) {
|
|
116956
|
-
if (runtime.lastSessionKey === sessionKey) {
|
|
116957
|
-
return {
|
|
116958
|
-
slug,
|
|
116959
|
-
issueId: Number(issueId),
|
|
116960
|
-
runtime
|
|
116961
|
-
};
|
|
116962
|
-
}
|
|
116963
|
-
}
|
|
116964
117029
|
}
|
|
116965
117030
|
return null;
|
|
116966
117031
|
}
|
|
@@ -117839,7 +117904,7 @@ var GitLabProvider = class {
|
|
|
117839
117904
|
return null;
|
|
117840
117905
|
}
|
|
117841
117906
|
}
|
|
117842
|
-
async getPrDetails(_issueId) {
|
|
117907
|
+
async getPrDetails(_issueId, _selector) {
|
|
117843
117908
|
return null;
|
|
117844
117909
|
}
|
|
117845
117910
|
async healthCheck() {
|
|
@@ -118396,7 +118461,7 @@ Bootstrapped by Fabrica.
|
|
|
118396
118461
|
if (!pr?.number || !pr?.url) return null;
|
|
118397
118462
|
let state;
|
|
118398
118463
|
if (pr.state === "closed" && pr.mergedAt) {
|
|
118399
|
-
state =
|
|
118464
|
+
state = PrState.MERGED;
|
|
118400
118465
|
} else if (pr.state === "closed") {
|
|
118401
118466
|
state = PrState.CLOSED;
|
|
118402
118467
|
} else if (pr.reviewDecision === "APPROVED") {
|
|
@@ -118712,7 +118777,7 @@ Bootstrapped by Fabrica.
|
|
|
118712
118777
|
if (merged.length > 0) {
|
|
118713
118778
|
merged.sort((a, b) => new Date(b.mergedAt ?? 0).getTime() - new Date(a.mergedAt ?? 0).getTime());
|
|
118714
118779
|
const pr = merged[0];
|
|
118715
|
-
const state =
|
|
118780
|
+
const state = PrState.MERGED;
|
|
118716
118781
|
return {
|
|
118717
118782
|
number: pr.number,
|
|
118718
118783
|
nodeId: pr.id,
|
|
@@ -118747,34 +118812,49 @@ Bootstrapped by Fabrica.
|
|
|
118747
118812
|
}
|
|
118748
118813
|
return { state: PrState.CLOSED, url: null };
|
|
118749
118814
|
}
|
|
118750
|
-
async getPrDetails(issueId) {
|
|
118815
|
+
async getPrDetails(issueId, selector) {
|
|
118751
118816
|
try {
|
|
118752
|
-
let prs = await this.findPrsForIssue(
|
|
118753
|
-
issueId,
|
|
118754
|
-
"open",
|
|
118755
|
-
"number,headRefName,url,state,mergedAt"
|
|
118756
|
-
);
|
|
118757
118817
|
let prState = "open";
|
|
118758
|
-
|
|
118759
|
-
|
|
118760
|
-
|
|
118818
|
+
let prNumber;
|
|
118819
|
+
let prUrl;
|
|
118820
|
+
let sourceBranch;
|
|
118821
|
+
if (selector?.prNumber) {
|
|
118822
|
+
const status = await this.getPrStatusForNumber(selector.prNumber);
|
|
118823
|
+
if (!status?.number || !status.url || !status.sourceBranch) return null;
|
|
118824
|
+
prNumber = status.number;
|
|
118825
|
+
prUrl = status.url;
|
|
118826
|
+
sourceBranch = status.sourceBranch;
|
|
118827
|
+
prState = status.state === PrState.MERGED ? "merged" : status.state === PrState.CLOSED ? "closed" : "open";
|
|
118828
|
+
} else {
|
|
118829
|
+
let prs = await this.findPrsForIssue(
|
|
118830
|
+
issueId,
|
|
118831
|
+
"open",
|
|
118832
|
+
"number,headRefName,url,state,mergedAt"
|
|
118833
|
+
);
|
|
118834
|
+
if (!prs.length) {
|
|
118835
|
+
prs = await this.findPrsForIssue(issueId, "merged", "number,headRefName,url,state,mergedAt");
|
|
118836
|
+
if (prs.length) prState = "merged";
|
|
118837
|
+
}
|
|
118838
|
+
if (!prs.length) return null;
|
|
118839
|
+
const pr = prs[0];
|
|
118840
|
+
prNumber = pr.number;
|
|
118841
|
+
prUrl = pr.url ?? null;
|
|
118842
|
+
sourceBranch = pr.headRefName;
|
|
118761
118843
|
}
|
|
118762
|
-
if (!prs.length) return null;
|
|
118763
|
-
const pr = prs[0];
|
|
118764
118844
|
const raw = await this.gh([
|
|
118765
118845
|
"api",
|
|
118766
|
-
`repos/:owner/:repo/pulls/${
|
|
118846
|
+
`repos/:owner/:repo/pulls/${prNumber}`,
|
|
118767
118847
|
"--jq",
|
|
118768
118848
|
"{headSha: .head.sha, repositoryId: .head.repo.id, owner: .head.repo.owner.login, repo: .head.repo.name}"
|
|
118769
118849
|
]);
|
|
118770
118850
|
const extra = JSON.parse(raw);
|
|
118771
118851
|
if (!extra.headSha || !extra.repositoryId || !extra.owner || !extra.repo) return null;
|
|
118772
118852
|
return {
|
|
118773
|
-
prNumber
|
|
118853
|
+
prNumber,
|
|
118774
118854
|
headSha: extra.headSha,
|
|
118775
118855
|
prState,
|
|
118776
|
-
prUrl
|
|
118777
|
-
sourceBranch
|
|
118856
|
+
prUrl,
|
|
118857
|
+
sourceBranch,
|
|
118778
118858
|
repositoryId: extra.repositoryId,
|
|
118779
118859
|
owner: extra.owner,
|
|
118780
118860
|
repo: extra.repo
|
|
@@ -119408,26 +119488,20 @@ function resolveRoute(ctx, explicitChannelId, explicitMessageThreadId) {
|
|
|
119408
119488
|
accountId: ctx.agentAccountId
|
|
119409
119489
|
};
|
|
119410
119490
|
}
|
|
119411
|
-
async function
|
|
119491
|
+
async function resolveProjectFromContext(workspaceDir, ctx, explicitChannelId, explicitMessageThreadId) {
|
|
119492
|
+
const route = resolveRoute(ctx, explicitChannelId, explicitMessageThreadId);
|
|
119412
119493
|
const data = await readProjects(workspaceDir);
|
|
119413
|
-
const project = getProject(data, channelId,
|
|
119494
|
+
const project = getProjectByRoute(data, route) ?? getProject(data, route.channelId, route.messageThreadId ?? void 0);
|
|
119414
119495
|
if (!project) {
|
|
119415
119496
|
const topicScopedCount = Object.values(data.projects).filter(
|
|
119416
|
-
(candidate) => candidate.channels.some((channel) => String(channel.channelId) === String(channelId) && channel.messageThreadId !== void 0 && channel.messageThreadId !== null)
|
|
119497
|
+
(candidate) => candidate.channels.some((channel) => String(channel.channelId) === String(route.channelId) && channel.messageThreadId !== void 0 && channel.messageThreadId !== null)
|
|
119417
119498
|
).length;
|
|
119418
|
-
const threadHint = topicScopedCount > 0 &&
|
|
119499
|
+
const threadHint = topicScopedCount > 0 && route.messageThreadId == null ? " This Telegram forum group has topic-scoped projects; pass the messageThreadId from the current topic." : "";
|
|
119419
119500
|
throw new Error(
|
|
119420
|
-
`No project found for "${
|
|
119501
|
+
`No project found for "${routeKey(route)}". Register a new project with project_register, or link this channel to an existing project.${threadHint}`
|
|
119421
119502
|
);
|
|
119422
119503
|
}
|
|
119423
|
-
return { data, project };
|
|
119424
|
-
}
|
|
119425
|
-
async function resolveProjectFromContext(workspaceDir, ctx, explicitChannelId, explicitMessageThreadId) {
|
|
119426
|
-
const route = resolveRoute(ctx, explicitChannelId, explicitMessageThreadId);
|
|
119427
|
-
const resolved = await resolveProject(workspaceDir, route.channelId, {
|
|
119428
|
-
messageThreadId: route.messageThreadId ?? void 0
|
|
119429
|
-
});
|
|
119430
|
-
return { ...resolved, route };
|
|
119504
|
+
return { data, project, route };
|
|
119431
119505
|
}
|
|
119432
119506
|
async function resolveProvider(project, runCommand, pluginConfig) {
|
|
119433
119507
|
return createProvider({
|
|
@@ -119979,27 +120053,30 @@ async function notify(event, opts) {
|
|
|
119979
120053
|
});
|
|
119980
120054
|
return true;
|
|
119981
120055
|
}
|
|
119982
|
-
|
|
119983
|
-
|
|
119984
|
-
|
|
119985
|
-
|
|
119986
|
-
|
|
119987
|
-
|
|
119988
|
-
|
|
119989
|
-
|
|
119990
|
-
|
|
119991
|
-
|
|
119992
|
-
|
|
119993
|
-
|
|
119994
|
-
|
|
119995
|
-
|
|
119996
|
-
|
|
119997
|
-
|
|
119998
|
-
|
|
119999
|
-
|
|
120000
|
-
|
|
120001
|
-
|
|
120002
|
-
|
|
120056
|
+
let notifyKey = null;
|
|
120057
|
+
if (!opts.skipOutboxWrite) {
|
|
120058
|
+
notifyKey = computeNotifyKey(
|
|
120059
|
+
event.project ?? "global",
|
|
120060
|
+
event.issueId ?? 0,
|
|
120061
|
+
event.type
|
|
120062
|
+
);
|
|
120063
|
+
const isNew = await writeIntent(opts.workspaceDir, notifyKey, event, {
|
|
120064
|
+
channelId: target.channelId,
|
|
120065
|
+
channel: target.channel,
|
|
120066
|
+
accountId: target.accountId,
|
|
120067
|
+
messageThreadId: target.messageThreadId
|
|
120068
|
+
}).catch(() => false);
|
|
120069
|
+
if (!isNew) {
|
|
120070
|
+
await log(opts.workspaceDir, "notify_skip", {
|
|
120071
|
+
eventId,
|
|
120072
|
+
correlationId,
|
|
120073
|
+
eventType: event.type,
|
|
120074
|
+
reason: "duplicate outbox key",
|
|
120075
|
+
key: notifyKey
|
|
120076
|
+
}).catch(() => {
|
|
120077
|
+
});
|
|
120078
|
+
return true;
|
|
120079
|
+
}
|
|
120003
120080
|
}
|
|
120004
120081
|
await log(opts.workspaceDir, "notify", {
|
|
120005
120082
|
eventId,
|
|
@@ -120030,7 +120107,7 @@ async function notify(event, opts) {
|
|
|
120030
120107
|
target
|
|
120031
120108
|
}
|
|
120032
120109
|
);
|
|
120033
|
-
if (sent) {
|
|
120110
|
+
if (sent && notifyKey) {
|
|
120034
120111
|
await markDelivered(opts.workspaceDir, notifyKey).catch(() => {
|
|
120035
120112
|
});
|
|
120036
120113
|
}
|
|
@@ -120864,8 +120941,12 @@ Once the PR shows as mergeable on GitHub, call work_finish again.`
|
|
|
120864
120941
|
if (err instanceof Error && (err.message.startsWith("Cannot mark work_finish(done)") || err.message.startsWith("Cannot complete work_finish(done)"))) {
|
|
120865
120942
|
throw err;
|
|
120866
120943
|
}
|
|
120867
|
-
logger6.warn({ err }, "PR validation warning; failing
|
|
120868
|
-
|
|
120944
|
+
logger6.warn({ err }, "PR validation warning; failing closed");
|
|
120945
|
+
throw new Error(
|
|
120946
|
+
`Cannot mark work_finish(done) because Fabrica could not verify PR state right now.
|
|
120947
|
+
|
|
120948
|
+
Resolve the provider/API error and call work_finish again.`
|
|
120949
|
+
);
|
|
120869
120950
|
}
|
|
120870
120951
|
}
|
|
120871
120952
|
function shouldAutoRecoverToFeedback(summary) {
|
|
@@ -120873,58 +120954,19 @@ function shouldAutoRecoverToFeedback(summary) {
|
|
|
120873
120954
|
const text = summary.toLowerCase();
|
|
120874
120955
|
return /retarget/.test(text) || /mismatch de escopo/.test(text) || /mismatch de escopo\/rastreabilidade/.test(text) || /new pr/.test(text) || /novo pr/.test(text) || /não pode satisfazer a issue/.test(text) || /cannot satisfy issue/.test(text);
|
|
120875
120956
|
}
|
|
120876
|
-
async function validateReviewerArtifact(provider, issueId, expectedResult, artifactId, artifactType, selector) {
|
|
120877
|
-
if (!artifactId || !artifactType) {
|
|
120878
|
-
throw new Error(
|
|
120879
|
-
"Reviewer must publish feedback to the PR with review_submit before calling work_finish."
|
|
120880
|
-
);
|
|
120881
|
-
}
|
|
120882
|
-
const comments = await provider.getPrReviewComments(issueId, selector);
|
|
120883
|
-
const match = comments.find(
|
|
120884
|
-
(comment) => matchesReviewArtifact(comment, artifactId, artifactType)
|
|
120885
|
-
);
|
|
120886
|
-
if (!match) {
|
|
120887
|
-
throw new Error(
|
|
120888
|
-
`Review artifact #${artifactId} (${artifactType}) was not found on the PR for issue #${issueId}.`
|
|
120889
|
-
);
|
|
120890
|
-
}
|
|
120891
|
-
if (artifactType === "formal_review") {
|
|
120892
|
-
if (expectedResult === "approve" && match.state !== "APPROVED") {
|
|
120893
|
-
throw new Error(
|
|
120894
|
-
`Review artifact #${artifactId} must be an APPROVED review before reviewer approve can complete issue #${issueId}.`
|
|
120895
|
-
);
|
|
120896
|
-
}
|
|
120897
|
-
if (expectedResult === "reject" && match.state !== "CHANGES_REQUESTED") {
|
|
120898
|
-
throw new Error(
|
|
120899
|
-
`Review artifact #${artifactId} must be a CHANGES_REQUESTED review before reviewer reject can complete issue #${issueId}.`
|
|
120900
|
-
);
|
|
120901
|
-
}
|
|
120902
|
-
}
|
|
120903
|
-
}
|
|
120904
|
-
async function getCanonicalQaEvidenceValidationForPr(provider, issueId, selector) {
|
|
120905
|
-
const prStatus = await provider.getPrStatus(issueId, selector);
|
|
120906
|
-
return validateCanonicalQaEvidence(prStatus.body);
|
|
120907
|
-
}
|
|
120908
|
-
function matchesReviewArtifact(comment, artifactId, artifactType) {
|
|
120909
|
-
if (comment.id !== artifactId) return false;
|
|
120910
|
-
if (artifactType === "formal_review") {
|
|
120911
|
-
return comment.state === "APPROVED" || comment.state === "CHANGES_REQUESTED";
|
|
120912
|
-
}
|
|
120913
|
-
return comment.state === "COMMENTED" && !comment.path;
|
|
120914
|
-
}
|
|
120915
120957
|
var INFRA_FAIL_CIRCUIT_BREAKER_THRESHOLD = 2;
|
|
120916
120958
|
function createWorkFinishTool(ctx) {
|
|
120917
120959
|
return (toolCtx) => ({
|
|
120918
120960
|
name: "work_finish",
|
|
120919
120961
|
label: "Work Finish",
|
|
120920
|
-
description: `Complete a task: Developer done (PR created, goes to review) or blocked. Tester pass/fail/refine/blocked.
|
|
120962
|
+
description: `Complete a task: Developer done (PR created, goes to review) or blocked. Tester pass/fail/fail_infra/refine/blocked. Architect done/blocked. Handles label transition, state update, issue close/reopen, notifications, and audit logging.`,
|
|
120921
120963
|
parameters: {
|
|
120922
120964
|
type: "object",
|
|
120923
120965
|
required: ["channelId", "role", "result"],
|
|
120924
120966
|
properties: {
|
|
120925
|
-
channelId: { type: "string", description: "Project slug (e.g. 'my-project'). Use the value from the 'Channel:'
|
|
120967
|
+
channelId: { type: "string", description: "Project slug (e.g. 'my-project'). Use the value from the 'Channel:' line in your task message. Do NOT use a numeric Telegram chat ID." },
|
|
120926
120968
|
role: { type: "string", enum: getAllRoleIds(), description: "Worker role" },
|
|
120927
|
-
result: { type: "string", enum: ["done", "pass", "fail", "fail_infra", "refine", "blocked"
|
|
120969
|
+
result: { type: "string", enum: ["done", "pass", "fail", "fail_infra", "refine", "blocked"], description: "Completion result. Use fail_infra (tester only) when the test toolchain is missing or broken \u2014 this keeps the issue in the test queue instead of routing it to the developer." },
|
|
120928
120970
|
summary: { type: "string", description: "Brief summary" },
|
|
120929
120971
|
prUrl: { type: "string", description: "PR/MR URL (auto-detected if omitted)" },
|
|
120930
120972
|
createdTasks: {
|
|
@@ -120939,15 +120981,6 @@ function createWorkFinishTool(ctx) {
|
|
|
120939
120981
|
}
|
|
120940
120982
|
},
|
|
120941
120983
|
description: "Tasks created during this work session (architect creates implementation tasks)."
|
|
120942
|
-
},
|
|
120943
|
-
reviewArtifactId: {
|
|
120944
|
-
type: "number",
|
|
120945
|
-
description: "Artifact ID returned by review_submit. Required for reviewer approve/reject."
|
|
120946
|
-
},
|
|
120947
|
-
reviewArtifactType: {
|
|
120948
|
-
type: "string",
|
|
120949
|
-
enum: ["formal_review", "pr_conversation_comment"],
|
|
120950
|
-
description: "Artifact type returned by review_submit. Required for reviewer approve/reject."
|
|
120951
120984
|
}
|
|
120952
120985
|
}
|
|
120953
120986
|
},
|
|
@@ -120957,16 +120990,13 @@ function createWorkFinishTool(ctx) {
|
|
|
120957
120990
|
const summary = params.summary;
|
|
120958
120991
|
const prUrl = params.prUrl;
|
|
120959
120992
|
const createdTasks = params.createdTasks;
|
|
120960
|
-
const
|
|
120961
|
-
const reviewArtifactType = params.reviewArtifactType;
|
|
120993
|
+
const dispatchRunId = typeof params._dispatchRunId === "string" ? params._dispatchRunId : void 0;
|
|
120962
120994
|
const workspaceDir = requireWorkspaceDir(toolCtx);
|
|
120963
|
-
|
|
120964
|
-
|
|
120965
|
-
|
|
120966
|
-
|
|
120967
|
-
|
|
120968
|
-
}).catch(() => {
|
|
120969
|
-
});
|
|
120995
|
+
if (role === "reviewer") {
|
|
120996
|
+
throw new Error(
|
|
120997
|
+
"Reviewer completion is no longer handled by work_finish. End your response with exactly one plain-text decision line: 'Review result: APPROVE' or 'Review result: REJECT'. Use the project slug from the 'Channel:' line in your task message for any follow-up task_create call."
|
|
120998
|
+
);
|
|
120999
|
+
}
|
|
120970
121000
|
if (!isValidResult(role, result)) {
|
|
120971
121001
|
const valid = getCompletionResults(role);
|
|
120972
121002
|
throw new Error(`${role.toUpperCase()} cannot complete with "${result}". Valid results: ${valid.join(", ")}`);
|
|
@@ -120979,6 +121009,41 @@ function createWorkFinishTool(ctx) {
|
|
|
120979
121009
|
}
|
|
120980
121010
|
const { slotIndex, slotLevel, issueId, recovered } = workerSlot;
|
|
120981
121011
|
const issueRuntime = project.issueRuntime?.[String(issueId)];
|
|
121012
|
+
const currentDispatchRunId = workerSlot.dispatchRunId ?? issueRuntime?.dispatchRunId ?? null;
|
|
121013
|
+
const hasCycleMismatch = Boolean(
|
|
121014
|
+
workerSlot.dispatchCycleId && issueRuntime?.lastDispatchCycleId && workerSlot.dispatchCycleId !== issueRuntime.lastDispatchCycleId
|
|
121015
|
+
);
|
|
121016
|
+
if (!dispatchRunId || !currentDispatchRunId || dispatchRunId !== currentDispatchRunId || hasCycleMismatch) {
|
|
121017
|
+
await log(workspaceDir, "work_finish_rejected", {
|
|
121018
|
+
project: project.name,
|
|
121019
|
+
projectSlug: project.slug,
|
|
121020
|
+
issue: issueId,
|
|
121021
|
+
role,
|
|
121022
|
+
result,
|
|
121023
|
+
reason: "stale_dispatch_cycle",
|
|
121024
|
+
sessionKey: toolCtx.sessionKey ?? null,
|
|
121025
|
+
providedDispatchRunId: dispatchRunId ?? null,
|
|
121026
|
+
currentDispatchRunId,
|
|
121027
|
+
slotDispatchCycleId: workerSlot.dispatchCycleId ?? null,
|
|
121028
|
+
runtimeDispatchCycleId: issueRuntime?.lastDispatchCycleId ?? null
|
|
121029
|
+
});
|
|
121030
|
+
return jsonResult2({
|
|
121031
|
+
success: false,
|
|
121032
|
+
project: project.name,
|
|
121033
|
+
projectSlug: project.slug,
|
|
121034
|
+
issueId,
|
|
121035
|
+
role,
|
|
121036
|
+
result,
|
|
121037
|
+
reason: "stale_dispatch_cycle"
|
|
121038
|
+
});
|
|
121039
|
+
}
|
|
121040
|
+
await recordIssueLifecycleBySessionKey({
|
|
121041
|
+
workspaceDir,
|
|
121042
|
+
sessionKey: toolCtx.sessionKey,
|
|
121043
|
+
stage: "first_worker_activity",
|
|
121044
|
+
details: { source: "work_finish" }
|
|
121045
|
+
}).catch(() => {
|
|
121046
|
+
});
|
|
120982
121047
|
const prSelector = role === "reviewer" || role === "tester" ? requireCanonicalPrSelector(project, issueId, `${role} completion`) : issueRuntime?.currentPrNumber ? { prNumber: issueRuntime.currentPrNumber } : void 0;
|
|
120983
121048
|
if (recovered) {
|
|
120984
121049
|
await log(workspaceDir, "work_finish_recovered_slot", {
|
|
@@ -121118,24 +121183,6 @@ function createWorkFinishTool(ctx) {
|
|
|
121118
121183
|
boundAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
121119
121184
|
});
|
|
121120
121185
|
}
|
|
121121
|
-
if (role === "reviewer" && (result === "approve" || result === "reject")) {
|
|
121122
|
-
await validateReviewerArtifact(provider, issueId, result, reviewArtifactId, reviewArtifactType, prSelector);
|
|
121123
|
-
}
|
|
121124
|
-
if (role === "reviewer" && result === "approve") {
|
|
121125
|
-
const qaEvidence = await getCanonicalQaEvidenceValidationForPr(provider, issueId, prSelector);
|
|
121126
|
-
if (!qaEvidence.valid) {
|
|
121127
|
-
await log(workspaceDir, "work_finish_rejected", {
|
|
121128
|
-
project: project.slug,
|
|
121129
|
-
issue: issueId,
|
|
121130
|
-
reason: "invalid_qa_evidence",
|
|
121131
|
-
role,
|
|
121132
|
-
result,
|
|
121133
|
-
prNumber: issueRuntime?.currentPrNumber ?? null,
|
|
121134
|
-
qaProblems: qaEvidence.problems
|
|
121135
|
-
});
|
|
121136
|
-
throwInvalidQaEvidence(qaEvidence, "reviewer");
|
|
121137
|
-
}
|
|
121138
|
-
}
|
|
121139
121186
|
const completion = await ctx.observability.withContext({
|
|
121140
121187
|
sessionKey: toolCtx.sessionKey ?? void 0,
|
|
121141
121188
|
issueId,
|
|
@@ -121230,7 +121277,9 @@ function resolveWorkerSlot(roleWorker, sessionKey) {
|
|
|
121230
121277
|
slotIndex: i2,
|
|
121231
121278
|
slotLevel: level,
|
|
121232
121279
|
issueId: Number(slot.issueId),
|
|
121233
|
-
recovered: false
|
|
121280
|
+
recovered: false,
|
|
121281
|
+
dispatchCycleId: slot.dispatchCycleId ?? null,
|
|
121282
|
+
dispatchRunId: slot.dispatchRunId ?? null
|
|
121234
121283
|
};
|
|
121235
121284
|
}
|
|
121236
121285
|
}
|
|
@@ -121244,7 +121293,9 @@ function resolveWorkerSlot(roleWorker, sessionKey) {
|
|
|
121244
121293
|
slotIndex: i2,
|
|
121245
121294
|
slotLevel: level,
|
|
121246
121295
|
issueId: Number(slot.lastIssueId),
|
|
121247
|
-
recovered: true
|
|
121296
|
+
recovered: true,
|
|
121297
|
+
dispatchCycleId: slot.dispatchCycleId ?? null,
|
|
121298
|
+
dispatchRunId: slot.dispatchRunId ?? null
|
|
121248
121299
|
};
|
|
121249
121300
|
}
|
|
121250
121301
|
}
|
|
@@ -121505,7 +121556,7 @@ Use cases:
|
|
|
121505
121556
|
- Orchestrator adds summary comments
|
|
121506
121557
|
- Cross-referencing related issues or PRs
|
|
121507
121558
|
|
|
121508
|
-
|
|
121559
|
+
Reviewer worker decisions belong in the review response, not in \`task_comment\`.
|
|
121509
121560
|
|
|
121510
121561
|
Examples:
|
|
121511
121562
|
- Simple: { issueId: 42, body: "Found an edge case with null inputs" }
|
|
@@ -121564,7 +121615,7 @@ Examples:
|
|
|
121564
121615
|
sessionKey: toolCtx.sessionKey
|
|
121565
121616
|
});
|
|
121566
121617
|
throw new Error(
|
|
121567
|
-
"Reviewer
|
|
121618
|
+
"Reviewer workers must keep findings in the review response and finish with `Review result: APPROVE` or `Review result: REJECT`. task_comment is reserved for issue-side operational notes from non-reviewer roles."
|
|
121568
121619
|
);
|
|
121569
121620
|
}
|
|
121570
121621
|
const { provider, type: providerType } = await resolveProvider(project, ctx.runCommand);
|
|
@@ -121576,7 +121627,7 @@ Examples:
|
|
|
121576
121627
|
sessionKey: toolCtx.sessionKey ?? null
|
|
121577
121628
|
});
|
|
121578
121629
|
throw new Error(
|
|
121579
|
-
"Reviewer
|
|
121630
|
+
"Reviewer worker findings must stay in the review response. task_comment is not allowed for reviewer findings."
|
|
121580
121631
|
);
|
|
121581
121632
|
}
|
|
121582
121633
|
const issue2 = await provider.getIssue(issueId);
|
|
@@ -122193,6 +122244,7 @@ import { jsonResult as jsonResult9 } from "openclaw/plugin-sdk";
|
|
|
122193
122244
|
|
|
122194
122245
|
// lib/dispatch/index.ts
|
|
122195
122246
|
init_audit();
|
|
122247
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
122196
122248
|
init_roles();
|
|
122197
122249
|
init_workflow();
|
|
122198
122250
|
|
|
@@ -122626,6 +122678,7 @@ function buildTaskMessage(opts) {
|
|
|
122626
122678
|
const results = opts.resolvedRole?.completionResults ?? [];
|
|
122627
122679
|
const availableResults = results.map((r2) => `"${r2}"`).join(", ");
|
|
122628
122680
|
const isFeedbackCycle = !!opts.prFeedback;
|
|
122681
|
+
const requiresWorkFinish = role !== "reviewer";
|
|
122629
122682
|
const parts = [
|
|
122630
122683
|
`${role.toUpperCase()} task for project "${projectName}" \u2014 Issue #${issueId}`,
|
|
122631
122684
|
``,
|
|
@@ -122690,23 +122743,25 @@ ${issueDescription}` : ""
|
|
|
122690
122743
|
`Repo: ${repoDisplay} | Branch: ${baseBranch} | ${issueUrl}`,
|
|
122691
122744
|
`Project: ${projectName} | Channel: ${channelId}`
|
|
122692
122745
|
);
|
|
122693
|
-
|
|
122694
|
-
|
|
122695
|
-
|
|
122696
|
-
|
|
122697
|
-
|
|
122698
|
-
|
|
122699
|
-
|
|
122700
|
-
|
|
122701
|
-
|
|
122702
|
-
|
|
122703
|
-
|
|
122704
|
-
|
|
122705
|
-
|
|
122706
|
-
|
|
122707
|
-
|
|
122708
|
-
|
|
122709
|
-
|
|
122746
|
+
if (requiresWorkFinish) {
|
|
122747
|
+
parts.push(
|
|
122748
|
+
``,
|
|
122749
|
+
`---`,
|
|
122750
|
+
``,
|
|
122751
|
+
`## MANDATORY: Task Completion`,
|
|
122752
|
+
``,
|
|
122753
|
+
`When you finish this task, you MUST invoke the \`work_finish\` **tool** (API tool_use call \u2014 NOT a shell command):`,
|
|
122754
|
+
`- \`role\`: "${role}"`,
|
|
122755
|
+
`- \`channelId\`: "${channelId}" (project slug from the \`Channel:\` line above)`,
|
|
122756
|
+
`- \`result\`: ${availableResults}`,
|
|
122757
|
+
`- \`summary\`: brief description of what you did`,
|
|
122758
|
+
``,
|
|
122759
|
+
`\u26A0\uFE0F \`work_finish\` is a Fabrica tool, not a CLI command. Call it as a tool (the same way you use task_create or other tools), not via bash.`,
|
|
122760
|
+
`\u26A0\uFE0F You MUST call work_finish even if you encounter errors or cannot finish.`,
|
|
122761
|
+
`Use "blocked" with a summary explaining why you're stuck.`,
|
|
122762
|
+
`Never end your session without calling work_finish.`
|
|
122763
|
+
);
|
|
122764
|
+
}
|
|
122710
122765
|
return parts.join("\n");
|
|
122711
122766
|
}
|
|
122712
122767
|
function buildConflictFixMessage(opts) {
|
|
@@ -122725,6 +122780,7 @@ function buildConflictFixMessage(opts) {
|
|
|
122725
122780
|
const repoDisplay = sanitizeRepoContext(repo);
|
|
122726
122781
|
const results = opts.resolvedRole?.completionResults ?? [];
|
|
122727
122782
|
const availableResults = results.map((r2) => `"${r2}"`).join(", ");
|
|
122783
|
+
const requiresWorkFinish = role !== "reviewer";
|
|
122728
122784
|
const parts = [
|
|
122729
122785
|
`${role.toUpperCase()} task for project "${projectName}" \u2014 Issue #${issueId}`,
|
|
122730
122786
|
``,
|
|
@@ -122738,23 +122794,25 @@ function buildConflictFixMessage(opts) {
|
|
|
122738
122794
|
`Repo: ${repoDisplay} | Branch: ${baseBranch} | ${issueUrl}`,
|
|
122739
122795
|
`Project: ${projectName} | Channel: ${channelId}`
|
|
122740
122796
|
);
|
|
122741
|
-
|
|
122742
|
-
|
|
122743
|
-
|
|
122744
|
-
|
|
122745
|
-
|
|
122746
|
-
|
|
122747
|
-
|
|
122748
|
-
|
|
122749
|
-
|
|
122750
|
-
|
|
122751
|
-
|
|
122752
|
-
|
|
122753
|
-
|
|
122754
|
-
|
|
122755
|
-
|
|
122756
|
-
|
|
122757
|
-
|
|
122797
|
+
if (requiresWorkFinish) {
|
|
122798
|
+
parts.push(
|
|
122799
|
+
``,
|
|
122800
|
+
`---`,
|
|
122801
|
+
``,
|
|
122802
|
+
`## MANDATORY: Task Completion`,
|
|
122803
|
+
``,
|
|
122804
|
+
`When you finish this task, you MUST invoke the \`work_finish\` **tool** (API tool_use call \u2014 NOT a shell command):`,
|
|
122805
|
+
`- \`role\`: "${role}"`,
|
|
122806
|
+
`- \`channelId\`: "${channelId}" (project slug from the \`Channel:\` line above)`,
|
|
122807
|
+
`- \`result\`: ${availableResults}`,
|
|
122808
|
+
`- \`summary\`: brief description of what you did`,
|
|
122809
|
+
``,
|
|
122810
|
+
`\u26A0\uFE0F \`work_finish\` is a Fabrica tool, not a CLI command. Call it as a tool (the same way you use task_create or other tools), not via bash.`,
|
|
122811
|
+
`\u26A0\uFE0F You MUST call work_finish even if you encounter errors or cannot finish.`,
|
|
122812
|
+
`Use "blocked" with a summary explaining why you're stuck.`,
|
|
122813
|
+
`Never end your session without calling work_finish.`
|
|
122814
|
+
);
|
|
122815
|
+
}
|
|
122758
122816
|
return parts.join("\n");
|
|
122759
122817
|
}
|
|
122760
122818
|
function buildAnnouncement(level, role, sessionAction, issueId, issueTitle, issueUrl, resolvedRole, botName) {
|
|
@@ -123006,6 +123064,10 @@ function sendToAgent(sessionKey, taskMessage, opts) {
|
|
|
123006
123064
|
deliver: false,
|
|
123007
123065
|
...opts.extraSystemPrompt ? { extraSystemPrompt: opts.extraSystemPrompt } : {}
|
|
123008
123066
|
}).then((result) => {
|
|
123067
|
+
if (result?.runId) {
|
|
123068
|
+
bindDispatchRunIdBySessionKey(opts.workspaceDir, sessionKey, result.runId).catch(() => {
|
|
123069
|
+
});
|
|
123070
|
+
}
|
|
123009
123071
|
log(opts.workspaceDir, "dispatch_agent_sent", {
|
|
123010
123072
|
step: "sendToAgent",
|
|
123011
123073
|
sessionKey,
|
|
@@ -123229,6 +123291,7 @@ async function dispatchTask(opts) {
|
|
|
123229
123291
|
}
|
|
123230
123292
|
}
|
|
123231
123293
|
const sessionAction = existingSessionKey ? "send" : "spawn";
|
|
123294
|
+
const dispatchCycleId = randomUUID2();
|
|
123232
123295
|
const allComments = await provider.listComments(issueId);
|
|
123233
123296
|
const { workflow } = resolvedConfig;
|
|
123234
123297
|
const prFeedback = isFeedbackState(workflow, fromLabel) ? await fetchPrFeedback(provider, issueId, prSelector) : void 0;
|
|
@@ -123294,12 +123357,16 @@ async function dispatchTask(opts) {
|
|
|
123294
123357
|
sessionKey,
|
|
123295
123358
|
sessionAction,
|
|
123296
123359
|
fromLabel,
|
|
123297
|
-
name: botName
|
|
123360
|
+
name: botName,
|
|
123361
|
+
dispatchCycleId
|
|
123298
123362
|
});
|
|
123299
123363
|
await updateIssueRuntime(workspaceDir, project.slug, String(issueId), {
|
|
123300
123364
|
dispatchRequestedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
123365
|
+
lastDispatchCycleId: dispatchCycleId,
|
|
123366
|
+
dispatchRunId: null,
|
|
123301
123367
|
agentAcceptedAt: null,
|
|
123302
|
-
firstWorkerActivityAt: null
|
|
123368
|
+
firstWorkerActivityAt: null,
|
|
123369
|
+
lastSessionKey: sessionKey
|
|
123303
123370
|
}).catch((err) => {
|
|
123304
123371
|
log(workspaceDir, "dispatch_warning", { step: "record_dispatch_requested", issue: issueId, err: String(err) }).catch(() => {
|
|
123305
123372
|
});
|
|
@@ -123485,6 +123552,8 @@ async function recordWorkerState(workspaceDir, slug, role, slotIndex, opts) {
|
|
|
123485
123552
|
level: opts.level,
|
|
123486
123553
|
sessionKey: opts.sessionKey,
|
|
123487
123554
|
startTime: (/* @__PURE__ */ new Date()).toISOString(),
|
|
123555
|
+
dispatchCycleId: opts.dispatchCycleId,
|
|
123556
|
+
dispatchRunId: null,
|
|
123488
123557
|
previousLabel: opts.fromLabel,
|
|
123489
123558
|
slotIndex,
|
|
123490
123559
|
name: opts.name
|
|
@@ -123810,7 +123879,6 @@ function createReviewSubmitTool(ctx) {
|
|
|
123810
123879
|
label: "Review Submit",
|
|
123811
123880
|
description: `Submit canonical review feedback to the PR linked to an issue.
|
|
123812
123881
|
|
|
123813
|
-
Use this instead of task_comment for reviewer approvals/rejections.
|
|
123814
123882
|
It writes the review to the PR itself, preferring a formal PR review and
|
|
123815
123883
|
falling back to a top-level PR conversation comment when necessary.`,
|
|
123816
123884
|
parameters: {
|
|
@@ -123841,6 +123909,20 @@ falling back to a top-level PR conversation comment when necessary.`,
|
|
|
123841
123909
|
const result = params.result;
|
|
123842
123910
|
const body = params.body;
|
|
123843
123911
|
const workspaceDir = requireWorkspaceDir(toolCtx);
|
|
123912
|
+
const workerSession = toolCtx.sessionKey ? parseFabricaSessionKey(toolCtx.sessionKey) : null;
|
|
123913
|
+
if (workerSession) {
|
|
123914
|
+
await log(workspaceDir, "review_submit_blocked", {
|
|
123915
|
+
project: workerSession.projectName,
|
|
123916
|
+
role: workerSession.role,
|
|
123917
|
+
issue: issueId,
|
|
123918
|
+
sessionKey: toolCtx.sessionKey,
|
|
123919
|
+
reason: "fabrica_worker_session"
|
|
123920
|
+
}).catch(() => {
|
|
123921
|
+
});
|
|
123922
|
+
throw new Error(
|
|
123923
|
+
workerSession.role === "reviewer" ? "Reviewer workers must finish by ending their response with `Review result: APPROVE` or `Review result: REJECT`. Do not call review_submit." : "Fabrica worker sessions must not call review_submit. Use the role's normal completion contract instead."
|
|
123924
|
+
);
|
|
123925
|
+
}
|
|
123844
123926
|
await recordIssueLifecycleBySessionKey({
|
|
123845
123927
|
workspaceDir,
|
|
123846
123928
|
sessionKey: toolCtx.sessionKey,
|
|
@@ -124206,12 +124288,7 @@ async function createProjectForumTopic(ctx, opts) {
|
|
|
124206
124288
|
}
|
|
124207
124289
|
}
|
|
124208
124290
|
}
|
|
124209
|
-
|
|
124210
|
-
chatId: opts.chatId,
|
|
124211
|
-
topicId: 1,
|
|
124212
|
-
name: opts.name,
|
|
124213
|
-
isFallback: true
|
|
124214
|
-
};
|
|
124291
|
+
throw lastError ?? new Error("Telegram topic creation failed after retry exhaustion");
|
|
124215
124292
|
}
|
|
124216
124293
|
|
|
124217
124294
|
// lib/telegram/config.ts
|
|
@@ -124233,6 +124310,11 @@ function readFabricaTelegramConfig(pluginConfig) {
|
|
|
124233
124310
|
};
|
|
124234
124311
|
}
|
|
124235
124312
|
|
|
124313
|
+
// lib/intake/lib/artifact-ids.ts
|
|
124314
|
+
function buildForumTopicArtifactId(channelId, messageThreadId) {
|
|
124315
|
+
return `telegram:${channelId}:${messageThreadId}`;
|
|
124316
|
+
}
|
|
124317
|
+
|
|
124236
124318
|
// lib/tools/admin/project-register.ts
|
|
124237
124319
|
async function scaffoldPromptFiles(workspaceDir, projectName) {
|
|
124238
124320
|
const projectDir = path22.join(workspaceDir, DATA_DIR, "projects", projectName);
|
|
@@ -124311,6 +124393,11 @@ async function hasProjectWorkflowOverride(workspaceDir, projectName) {
|
|
|
124311
124393
|
return false;
|
|
124312
124394
|
}
|
|
124313
124395
|
}
|
|
124396
|
+
function normalizeRepoIdentity(value) {
|
|
124397
|
+
const trimmed = value?.trim();
|
|
124398
|
+
if (!trimmed) return null;
|
|
124399
|
+
return trimmed.replace(/\.git$/i, "").toLowerCase();
|
|
124400
|
+
}
|
|
124314
124401
|
function adaptStepRunCommand(runCommand) {
|
|
124315
124402
|
return async (argv, optionsOrTimeout) => {
|
|
124316
124403
|
const [cmd, ...args] = argv;
|
|
@@ -124352,6 +124439,7 @@ async function registerProject(params) {
|
|
|
124352
124439
|
messageThreadId: route.messageThreadId ?? void 0,
|
|
124353
124440
|
accountId: route.accountId
|
|
124354
124441
|
});
|
|
124442
|
+
let createdArtifacts = [];
|
|
124355
124443
|
await acquireLock(workspaceDir);
|
|
124356
124444
|
try {
|
|
124357
124445
|
const data = await readProjects(workspaceDir);
|
|
@@ -124387,6 +124475,21 @@ async function registerProject(params) {
|
|
|
124387
124475
|
`${providerType.toUpperCase()} health check failed for ${repoPath}. Detected provider: ${providerType}. Ensure '${cliName}' CLI is installed, authenticated (${cliName} auth status), and the repo has a ${providerType.toUpperCase()} remote. Install ${cliName} from: ${cliInstallUrl}`
|
|
124388
124476
|
);
|
|
124389
124477
|
}
|
|
124478
|
+
let repoRemote;
|
|
124479
|
+
try {
|
|
124480
|
+
repoRemote = await provider.resolveRepositoryRemote() ?? void 0;
|
|
124481
|
+
} catch {
|
|
124482
|
+
repoRemote = void 0;
|
|
124483
|
+
}
|
|
124484
|
+
if (existing) {
|
|
124485
|
+
const sameRepoPath = resolveRepoPath(existing.repo) === repoPath;
|
|
124486
|
+
const sameRepoRemote = normalizeRepoIdentity(existing.repoRemote) != null && normalizeRepoIdentity(existing.repoRemote) === normalizeRepoIdentity(repoRemote);
|
|
124487
|
+
if (!sameRepoPath && !sameRepoRemote) {
|
|
124488
|
+
throw new Error(
|
|
124489
|
+
`Project slug "${slug}" already points to a different repository. Existing repo="${existing.repo}" remote="${existing.repoRemote ?? "unknown"}"; incoming repo="${repoPath}" remote="${repoRemote ?? "unknown"}".`
|
|
124490
|
+
);
|
|
124491
|
+
}
|
|
124492
|
+
}
|
|
124390
124493
|
await provider.ensureAllStateLabels();
|
|
124391
124494
|
const workflowOverrideCreated = autonomousProject ? await ensureAutonomousWorkflowOverride(
|
|
124392
124495
|
workspaceDir,
|
|
@@ -124410,12 +124513,6 @@ async function registerProject(params) {
|
|
|
124410
124513
|
for (const { name: labelName, color } of OPERATIONAL_LABELS) {
|
|
124411
124514
|
await provider.ensureLabel(labelName, color);
|
|
124412
124515
|
}
|
|
124413
|
-
let repoRemote;
|
|
124414
|
-
try {
|
|
124415
|
-
repoRemote = await provider.resolveRepositoryRemote() ?? void 0;
|
|
124416
|
-
} catch {
|
|
124417
|
-
repoRemote = void 0;
|
|
124418
|
-
}
|
|
124419
124516
|
if (createProjectTopic) {
|
|
124420
124517
|
if (!runtime || !config2) {
|
|
124421
124518
|
throw new Error("Runtime and config are required to create a Telegram project topic");
|
|
@@ -124436,6 +124533,13 @@ async function registerProject(params) {
|
|
|
124436
124533
|
name,
|
|
124437
124534
|
accountId: telegramConfig.projectsForumAccountId ?? initialRoute.accountId ?? void 0
|
|
124438
124535
|
});
|
|
124536
|
+
if (createdTopic.isFallback || createdTopic.topicId === 1) {
|
|
124537
|
+
throw new Error("DM bootstrap requires a dedicated Telegram topic; refusing to register against the General topic");
|
|
124538
|
+
}
|
|
124539
|
+
createdArtifacts = [{
|
|
124540
|
+
type: "forum_topic",
|
|
124541
|
+
id: buildForumTopicArtifactId(createdTopic.chatId, createdTopic.topicId)
|
|
124542
|
+
}];
|
|
124439
124543
|
targetRoute = buildRouteRef({
|
|
124440
124544
|
channel: "telegram",
|
|
124441
124545
|
channelId: createdTopic.chatId,
|
|
@@ -124524,10 +124628,33 @@ async function registerProject(params) {
|
|
|
124524
124628
|
},
|
|
124525
124629
|
announcement: `${action}. Labels ensured.${promptsNote} Ready for tasks.`
|
|
124526
124630
|
};
|
|
124631
|
+
} catch (err) {
|
|
124632
|
+
if (createdArtifacts.length > 0) {
|
|
124633
|
+
throw attachArtifactsToError(err, createdArtifacts);
|
|
124634
|
+
}
|
|
124635
|
+
throw err;
|
|
124527
124636
|
} finally {
|
|
124528
124637
|
await releaseLock(workspaceDir);
|
|
124529
124638
|
}
|
|
124530
124639
|
}
|
|
124640
|
+
function attachArtifactsToError(error48, artifacts) {
|
|
124641
|
+
const err = error48 instanceof Error ? error48 : new Error(String(error48));
|
|
124642
|
+
const existing = Array.isArray(err.artifacts) ? err.artifacts : [];
|
|
124643
|
+
const merged = [...existing, ...artifacts].filter((artifact, index, array2) => {
|
|
124644
|
+
if (!artifact || typeof artifact !== "object" || typeof artifact.type !== "string" || typeof artifact.id !== "string") {
|
|
124645
|
+
return false;
|
|
124646
|
+
}
|
|
124647
|
+
const current = `${artifact.type}:${artifact.id}`;
|
|
124648
|
+
return index === array2.findIndex((candidate) => {
|
|
124649
|
+
if (!candidate || typeof candidate !== "object" || typeof candidate.type !== "string" || typeof candidate.id !== "string") {
|
|
124650
|
+
return false;
|
|
124651
|
+
}
|
|
124652
|
+
return current === `${candidate.type}:${candidate.id}`;
|
|
124653
|
+
});
|
|
124654
|
+
});
|
|
124655
|
+
err.artifacts = merged;
|
|
124656
|
+
return err;
|
|
124657
|
+
}
|
|
124531
124658
|
function createProjectRegisterTool(ctx) {
|
|
124532
124659
|
return (toolCtx) => ({
|
|
124533
124660
|
name: "project_register",
|
|
@@ -124652,6 +124779,11 @@ async function auditHealthFixApplied(workspaceDir, fix, details) {
|
|
|
124652
124779
|
});
|
|
124653
124780
|
}));
|
|
124654
124781
|
}
|
|
124782
|
+
function hasDispatchCycleMismatch(slot, issueRuntime) {
|
|
124783
|
+
return Boolean(
|
|
124784
|
+
slot.dispatchCycleId && issueRuntime?.lastDispatchCycleId && slot.dispatchCycleId !== issueRuntime.lastDispatchCycleId
|
|
124785
|
+
);
|
|
124786
|
+
}
|
|
124655
124787
|
async function fetchIssue(provider, issueId) {
|
|
124656
124788
|
try {
|
|
124657
124789
|
return await provider.getIssue(issueId);
|
|
@@ -124662,17 +124794,16 @@ async function fetchIssue(provider, issueId) {
|
|
|
124662
124794
|
function isIssueClosed(issue2) {
|
|
124663
124795
|
return issue2.state.toLowerCase() === "closed";
|
|
124664
124796
|
}
|
|
124665
|
-
async function resolveOrphanRevertLabel(provider, issueId, role, defaultQueueLabel, workflow) {
|
|
124797
|
+
async function resolveOrphanRevertLabel(provider, project, issueId, role, defaultQueueLabel, workflow) {
|
|
124666
124798
|
try {
|
|
124667
|
-
const
|
|
124799
|
+
const prSelector = getCanonicalPrSelector(project, issueId);
|
|
124800
|
+
const prStatus = await provider.getPrStatus(issueId, prSelector);
|
|
124668
124801
|
if (prStatus.url && prStatus.state !== PrState.MERGED && prStatus.state !== PrState.CLOSED && prStatus.currentIssueMatch !== false) {
|
|
124669
124802
|
if (prStatus.state === PrState.CHANGES_REQUESTED || prStatus.state === PrState.HAS_COMMENTS) {
|
|
124670
124803
|
const queueLabels = getQueueLabels(workflow, role);
|
|
124671
124804
|
const feedbackLabel = queueLabels.find((l) => isFeedbackState(workflow, l));
|
|
124672
124805
|
if (feedbackLabel) return feedbackLabel;
|
|
124673
124806
|
}
|
|
124674
|
-
const rule = getCompletionRule(workflow, role, "done");
|
|
124675
|
-
if (rule) return rule.to;
|
|
124676
124807
|
}
|
|
124677
124808
|
} catch {
|
|
124678
124809
|
}
|
|
@@ -124692,7 +124823,8 @@ async function checkWorkerHealth(opts) {
|
|
|
124692
124823
|
sessions,
|
|
124693
124824
|
workflow = DEFAULT_WORKFLOW,
|
|
124694
124825
|
staleWorkerHours = 2,
|
|
124695
|
-
dispatchConfirmTimeoutMs = DISPATCH_CONFIRMATION_TIMEOUT_MS
|
|
124826
|
+
dispatchConfirmTimeoutMs = DISPATCH_CONFIRMATION_TIMEOUT_MS,
|
|
124827
|
+
healthGracePeriodMs = GRACE_PERIOD_MS
|
|
124696
124828
|
} = opts;
|
|
124697
124829
|
const fixes = [];
|
|
124698
124830
|
if (!hasWorkflowStates(workflow, role)) return fixes;
|
|
@@ -124705,7 +124837,7 @@ async function checkWorkerHealth(opts) {
|
|
|
124705
124837
|
const sessionKey = slot.sessionKey;
|
|
124706
124838
|
const slotQueueLabel = slot.previousLabel ?? queueLabel;
|
|
124707
124839
|
const workerStartTime = slot.startTime ? new Date(slot.startTime).getTime() : null;
|
|
124708
|
-
const withinGracePeriod = workerStartTime !== null && Date.now() - workerStartTime <
|
|
124840
|
+
const withinGracePeriod = workerStartTime !== null && Date.now() - workerStartTime < healthGracePeriodMs;
|
|
124709
124841
|
const issueIdNum = slot.issueId ? Number(slot.issueId) : null;
|
|
124710
124842
|
let issue2 = null;
|
|
124711
124843
|
let currentLabel = null;
|
|
@@ -124733,6 +124865,23 @@ async function checkWorkerHealth(opts) {
|
|
|
124733
124865
|
issueId: slot.issueId ?? void 0
|
|
124734
124866
|
});
|
|
124735
124867
|
}
|
|
124868
|
+
if (slot.active && hasDispatchCycleMismatch(slot, issueRuntime)) {
|
|
124869
|
+
await log(workspaceDir, "health_fix_rejected", {
|
|
124870
|
+
type: "dispatch_cycle_mismatch",
|
|
124871
|
+
reason: "stale_dispatch_cycle",
|
|
124872
|
+
project: project.name,
|
|
124873
|
+
projectSlug,
|
|
124874
|
+
role,
|
|
124875
|
+
level,
|
|
124876
|
+
slotIndex,
|
|
124877
|
+
issueId: slot.issueId ?? null,
|
|
124878
|
+
sessionKey,
|
|
124879
|
+
slotDispatchCycleId: slot.dispatchCycleId ?? null,
|
|
124880
|
+
runtimeDispatchCycleId: issueRuntime?.lastDispatchCycleId ?? null
|
|
124881
|
+
}).catch(() => {
|
|
124882
|
+
});
|
|
124883
|
+
continue;
|
|
124884
|
+
}
|
|
124736
124885
|
if (slot.active && issueIdNum && !issue2) {
|
|
124737
124886
|
const fix = {
|
|
124738
124887
|
issue: {
|
|
@@ -125013,32 +125162,6 @@ async function checkWorkerHealth(opts) {
|
|
|
125013
125162
|
fixes.push(fix);
|
|
125014
125163
|
}
|
|
125015
125164
|
}
|
|
125016
|
-
if (slot.active && issueIdNum && issue2 && currentLabel === expectedLabel && autoFix) {
|
|
125017
|
-
try {
|
|
125018
|
-
const prStatus = await provider.getPrStatus(issueIdNum);
|
|
125019
|
-
if (prStatus.url && prStatus.state !== PrState.MERGED && prStatus.state !== PrState.CLOSED && prStatus.state !== PrState.CHANGES_REQUESTED && prStatus.state !== PrState.HAS_COMMENTS && prStatus.currentIssueMatch !== false) {
|
|
125020
|
-
const rule = getCompletionRule(workflow, role, "done");
|
|
125021
|
-
if (rule && rule.to !== expectedLabel) {
|
|
125022
|
-
await resilientLabelTransition(provider, issueIdNum, expectedLabel, rule.to);
|
|
125023
|
-
await deactivateSlot();
|
|
125024
|
-
await log(workspaceDir, "health_transition_to_review", {
|
|
125025
|
-
project: project.name,
|
|
125026
|
-
projectSlug,
|
|
125027
|
-
role,
|
|
125028
|
-
level,
|
|
125029
|
-
issueId: slot.issueId,
|
|
125030
|
-
sessionKey,
|
|
125031
|
-
slotIndex,
|
|
125032
|
-
fromLabel: expectedLabel,
|
|
125033
|
-
toLabel: rule.to,
|
|
125034
|
-
prUrl: prStatus.url
|
|
125035
|
-
}).catch(() => {
|
|
125036
|
-
});
|
|
125037
|
-
}
|
|
125038
|
-
}
|
|
125039
|
-
} catch {
|
|
125040
|
-
}
|
|
125041
|
-
}
|
|
125042
125165
|
if (!slot.active && issue2 && currentLabel === expectedLabel) {
|
|
125043
125166
|
const fix = {
|
|
125044
125167
|
issue: {
|
|
@@ -125188,6 +125311,7 @@ async function scanOrphanedLabels(opts) {
|
|
|
125188
125311
|
try {
|
|
125189
125312
|
const revertTarget = await resolveOrphanRevertLabel(
|
|
125190
125313
|
provider,
|
|
125314
|
+
freshProject,
|
|
125191
125315
|
issue2.iid,
|
|
125192
125316
|
role,
|
|
125193
125317
|
queueLabel,
|
|
@@ -125309,6 +125433,7 @@ function createHealthTool(ctx) {
|
|
|
125309
125433
|
for (const slug of slugs) {
|
|
125310
125434
|
const project = data.projects[slug];
|
|
125311
125435
|
if (!project) continue;
|
|
125436
|
+
const resolvedConfig = await loadConfig(workspaceDir, slug);
|
|
125312
125437
|
const { provider } = await resolveProvider(project, ctx.runCommand);
|
|
125313
125438
|
for (const role of Object.keys(project.workers)) {
|
|
125314
125439
|
const healthFixes = await checkWorkerHealth({
|
|
@@ -125318,7 +125443,10 @@ function createHealthTool(ctx) {
|
|
|
125318
125443
|
role,
|
|
125319
125444
|
sessions,
|
|
125320
125445
|
autoFix: fix,
|
|
125321
|
-
provider
|
|
125446
|
+
provider,
|
|
125447
|
+
workflow: resolvedConfig.workflow,
|
|
125448
|
+
dispatchConfirmTimeoutMs: resolvedConfig.timeouts.dispatchConfirmTimeoutMs,
|
|
125449
|
+
healthGracePeriodMs: resolvedConfig.timeouts.healthGracePeriodMs
|
|
125322
125450
|
});
|
|
125323
125451
|
issues.push(...healthFixes.map((f3) => ({ ...f3, project: project.name, role })));
|
|
125324
125452
|
const orphanFixes = await scanOrphanedLabels({
|
|
@@ -125573,6 +125701,11 @@ function createChannelUnlinkTool(_ctx) {
|
|
|
125573
125701
|
type: "string",
|
|
125574
125702
|
description: "Channel ID to remove (e.g., Telegram group ID)"
|
|
125575
125703
|
},
|
|
125704
|
+
channel: {
|
|
125705
|
+
type: "string",
|
|
125706
|
+
enum: ["telegram", "whatsapp", "discord", "slack"],
|
|
125707
|
+
description: "Channel type. Defaults to 'telegram'."
|
|
125708
|
+
},
|
|
125576
125709
|
messageThreadId: {
|
|
125577
125710
|
type: "number",
|
|
125578
125711
|
description: "Optional Telegram topic ID. When provided, removes only that specific topic route."
|
|
@@ -125589,12 +125722,13 @@ function createChannelUnlinkTool(_ctx) {
|
|
|
125589
125722
|
},
|
|
125590
125723
|
async execute(_id, params) {
|
|
125591
125724
|
const channelId = params.channelId;
|
|
125725
|
+
const channelType = params.channel ?? "telegram";
|
|
125592
125726
|
const messageThreadId = typeof params.messageThreadId === "number" ? params.messageThreadId : void 0;
|
|
125593
125727
|
const projectRef = params.project;
|
|
125594
125728
|
const confirm = params.confirm;
|
|
125595
125729
|
const workspaceDir = requireWorkspaceDir(toolCtx);
|
|
125596
125730
|
const targetRoute = buildRouteRef({
|
|
125597
|
-
channel:
|
|
125731
|
+
channel: channelType,
|
|
125598
125732
|
channelId,
|
|
125599
125733
|
messageThreadId,
|
|
125600
125734
|
accountId: toolCtx.agentAccountId
|
|
@@ -125912,10 +126046,14 @@ var HEARTBEAT_DEFAULTS = {
|
|
|
125912
126046
|
intervalSeconds: 60,
|
|
125913
126047
|
maxPickupsPerTick: 4
|
|
125914
126048
|
};
|
|
126049
|
+
var DEFAULT_TICK_TIMEOUT_MS = 5e4;
|
|
125915
126050
|
function resolveHeartbeatConfig(pluginConfig) {
|
|
125916
126051
|
const raw = pluginConfig?.work_heartbeat;
|
|
125917
126052
|
return { ...HEARTBEAT_DEFAULTS, ...raw };
|
|
125918
126053
|
}
|
|
126054
|
+
function resolveTickTimeoutMs(config2) {
|
|
126055
|
+
return config2?.timeouts?.tickTimeoutMs ?? DEFAULT_TICK_TIMEOUT_MS;
|
|
126056
|
+
}
|
|
125919
126057
|
|
|
125920
126058
|
// lib/github/process-events.ts
|
|
125921
126059
|
init_zod();
|
|
@@ -130794,7 +130932,11 @@ async function reviewPass(opts) {
|
|
|
130794
130932
|
if (routing !== "human" && routing !== "agent") continue;
|
|
130795
130933
|
const isManaged = await provider.issueHasReaction(issue2.iid, "eyes");
|
|
130796
130934
|
if (!isManaged) continue;
|
|
130797
|
-
const
|
|
130935
|
+
const projectData = await readProjects(workspaceDir).catch(() => null);
|
|
130936
|
+
const project = projectData ? getProject(projectData, projectName) : null;
|
|
130937
|
+
const issueRuntime = project ? getIssueRuntime(project, issue2.iid) : void 0;
|
|
130938
|
+
const prSelector = project ? getCanonicalPrSelector(project, issue2.iid) : void 0;
|
|
130939
|
+
const status = await provider.getPrStatus(issue2.iid, prSelector);
|
|
130798
130940
|
if (status.currentIssueMatch === false) continue;
|
|
130799
130941
|
if (!status.url && status.state === PrState.CLOSED && baseBranch) {
|
|
130800
130942
|
try {
|
|
@@ -130873,14 +131015,12 @@ async function reviewPass(opts) {
|
|
|
130873
131015
|
const closedActions = typeof closedTransition === "object" ? closedTransition.actions : void 0;
|
|
130874
131016
|
const targetState2 = workflow.states[targetKey2];
|
|
130875
131017
|
if (targetState2) {
|
|
130876
|
-
|
|
131018
|
+
let aborted3 = false;
|
|
130877
131019
|
if (closedActions) {
|
|
130878
131020
|
for (const action of closedActions) {
|
|
130879
131021
|
switch (action) {
|
|
130880
131022
|
case Action.CLOSE_ISSUE:
|
|
130881
131023
|
try {
|
|
130882
|
-
const project = getProject(await readProjects(workspaceDir), projectName);
|
|
130883
|
-
const issueRuntime = project ? getIssueRuntime(project, issue2.iid) : void 0;
|
|
130884
131024
|
if (!project) throw new Error(`Project not found: ${projectName}`);
|
|
130885
131025
|
await guardedCloseIssue({
|
|
130886
131026
|
workspaceDir,
|
|
@@ -130889,10 +131029,12 @@ async function reviewPass(opts) {
|
|
|
130889
131029
|
issueId: issue2.iid,
|
|
130890
131030
|
role: "reviewer",
|
|
130891
131031
|
provider,
|
|
131032
|
+
selector: prSelector,
|
|
130892
131033
|
issueRuntime,
|
|
130893
131034
|
followUpPrRequired: issueRuntime?.followUpPrRequired === true
|
|
130894
131035
|
});
|
|
130895
131036
|
} catch {
|
|
131037
|
+
aborted3 = true;
|
|
130896
131038
|
}
|
|
130897
131039
|
break;
|
|
130898
131040
|
case Action.REOPEN_ISSUE:
|
|
@@ -130902,8 +131044,13 @@ async function reviewPass(opts) {
|
|
|
130902
131044
|
}
|
|
130903
131045
|
break;
|
|
130904
131046
|
}
|
|
131047
|
+
if (aborted3) break;
|
|
130905
131048
|
}
|
|
130906
131049
|
}
|
|
131050
|
+
if (aborted3) {
|
|
131051
|
+
continue;
|
|
131052
|
+
}
|
|
131053
|
+
await resilientLabelTransition(provider, issue2.iid, state.label, targetState2.label);
|
|
130907
131054
|
await log(workspaceDir, "review_transition", {
|
|
130908
131055
|
project: projectName,
|
|
130909
131056
|
issueId: issue2.iid,
|
|
@@ -130948,8 +131095,6 @@ async function reviewPass(opts) {
|
|
|
130948
131095
|
switch (action) {
|
|
130949
131096
|
case Action.MERGE_PR:
|
|
130950
131097
|
if (status.state === PrState.MERGED) {
|
|
130951
|
-
const project = getProject(await readProjects(workspaceDir), projectName);
|
|
130952
|
-
const issueRuntime = project ? getIssueRuntime(project, issue2.iid) : void 0;
|
|
130953
131098
|
if (project && issueRuntime?.currentPrNumber) {
|
|
130954
131099
|
await persistMergedArtifact({
|
|
130955
131100
|
workspaceDir,
|
|
@@ -130963,9 +131108,7 @@ async function reviewPass(opts) {
|
|
|
130963
131108
|
break;
|
|
130964
131109
|
}
|
|
130965
131110
|
try {
|
|
130966
|
-
await provider.mergePr(issue2.iid);
|
|
130967
|
-
const project = getProject(await readProjects(workspaceDir), projectName);
|
|
130968
|
-
const issueRuntime = project ? getIssueRuntime(project, issue2.iid) : void 0;
|
|
131111
|
+
await provider.mergePr(issue2.iid, prSelector);
|
|
130969
131112
|
if (project && issueRuntime?.currentPrNumber) {
|
|
130970
131113
|
await persistMergedArtifact({
|
|
130971
131114
|
workspaceDir,
|
|
@@ -131010,8 +131153,6 @@ async function reviewPass(opts) {
|
|
|
131010
131153
|
break;
|
|
131011
131154
|
case Action.CLOSE_ISSUE:
|
|
131012
131155
|
{
|
|
131013
|
-
const project = getProject(await readProjects(workspaceDir), projectName);
|
|
131014
|
-
const issueRuntime = project ? getIssueRuntime(project, issue2.iid) : void 0;
|
|
131015
131156
|
if (!project) throw new Error(`Project not found: ${projectName}`);
|
|
131016
131157
|
await guardedCloseIssue({
|
|
131017
131158
|
workspaceDir,
|
|
@@ -131020,6 +131161,7 @@ async function reviewPass(opts) {
|
|
|
131020
131161
|
issueId: issue2.iid,
|
|
131021
131162
|
role: "reviewer",
|
|
131022
131163
|
provider,
|
|
131164
|
+
selector: prSelector,
|
|
131023
131165
|
issueRuntime,
|
|
131024
131166
|
followUpPrRequired: issueRuntime?.followUpPrRequired === true
|
|
131025
131167
|
});
|
|
@@ -131085,6 +131227,10 @@ async function reviewSkipPass(opts) {
|
|
|
131085
131227
|
if (routing !== "skip") continue;
|
|
131086
131228
|
const isManaged = await provider.issueHasReaction(issue2.iid, "eyes");
|
|
131087
131229
|
if (!isManaged) continue;
|
|
131230
|
+
const projectData = await readProjects(workspaceDir).catch(() => null);
|
|
131231
|
+
const project = projectData ? getProject(projectData, projectName) : null;
|
|
131232
|
+
const issueRuntime = project ? getIssueRuntime(project, issue2.iid) : void 0;
|
|
131233
|
+
const prSelector = project ? getCanonicalPrSelector(project, issue2.iid) : void 0;
|
|
131088
131234
|
let aborted2 = false;
|
|
131089
131235
|
if (actions && effectiveActions && actions.length !== effectiveActions.length) {
|
|
131090
131236
|
await log(workspaceDir, "illegal_merge_before_test", {
|
|
@@ -131100,14 +131246,12 @@ async function reviewSkipPass(opts) {
|
|
|
131100
131246
|
for (const action of effectiveActions) {
|
|
131101
131247
|
switch (action) {
|
|
131102
131248
|
case Action.MERGE_PR: {
|
|
131103
|
-
const status = await provider.getPrStatus(issue2.iid);
|
|
131249
|
+
const status = await provider.getPrStatus(issue2.iid, prSelector);
|
|
131104
131250
|
if (status.currentIssueMatch === false) {
|
|
131105
131251
|
aborted2 = true;
|
|
131106
131252
|
break;
|
|
131107
131253
|
}
|
|
131108
131254
|
if (status.state === PrState.MERGED) {
|
|
131109
|
-
const project = getProject(await readProjects(workspaceDir), projectName);
|
|
131110
|
-
const issueRuntime = project ? getIssueRuntime(project, issue2.iid) : void 0;
|
|
131111
131255
|
if (project && issueRuntime?.currentPrNumber) {
|
|
131112
131256
|
await persistMergedArtifact({
|
|
131113
131257
|
workspaceDir,
|
|
@@ -131122,9 +131266,7 @@ async function reviewSkipPass(opts) {
|
|
|
131122
131266
|
}
|
|
131123
131267
|
if (!status.url) break;
|
|
131124
131268
|
try {
|
|
131125
|
-
await provider.mergePr(issue2.iid);
|
|
131126
|
-
const project = getProject(await readProjects(workspaceDir), projectName);
|
|
131127
|
-
const issueRuntime = project ? getIssueRuntime(project, issue2.iid) : void 0;
|
|
131269
|
+
await provider.mergePr(issue2.iid, prSelector);
|
|
131128
131270
|
if (project && issueRuntime?.currentPrNumber) {
|
|
131129
131271
|
await persistMergedArtifact({
|
|
131130
131272
|
workspaceDir,
|
|
@@ -131163,8 +131305,6 @@ async function reviewSkipPass(opts) {
|
|
|
131163
131305
|
break;
|
|
131164
131306
|
case Action.CLOSE_ISSUE:
|
|
131165
131307
|
try {
|
|
131166
|
-
const project = getProject(await readProjects(workspaceDir), projectName);
|
|
131167
|
-
const issueRuntime = project ? getIssueRuntime(project, issue2.iid) : void 0;
|
|
131168
131308
|
if (!project) throw new Error(`Project not found: ${projectName}`);
|
|
131169
131309
|
await guardedCloseIssue({
|
|
131170
131310
|
workspaceDir,
|
|
@@ -131173,10 +131313,12 @@ async function reviewSkipPass(opts) {
|
|
|
131173
131313
|
issueId: issue2.iid,
|
|
131174
131314
|
role: "reviewer",
|
|
131175
131315
|
provider,
|
|
131316
|
+
selector: prSelector,
|
|
131176
131317
|
issueRuntime,
|
|
131177
131318
|
followUpPrRequired: issueRuntime?.followUpPrRequired === true
|
|
131178
131319
|
});
|
|
131179
131320
|
} catch {
|
|
131321
|
+
aborted2 = true;
|
|
131180
131322
|
}
|
|
131181
131323
|
break;
|
|
131182
131324
|
case Action.REOPEN_ISSUE:
|
|
@@ -131223,19 +131365,21 @@ async function testSkipPass(opts) {
|
|
|
131223
131365
|
for (const issue2 of issues) {
|
|
131224
131366
|
const routing = detectStepRouting(issue2.labels, "test");
|
|
131225
131367
|
if (routing !== "skip") continue;
|
|
131368
|
+
const projectData = await readProjects(workspaceDir).catch(() => null);
|
|
131369
|
+
const project = projectData ? getProject(projectData, projectName) : null;
|
|
131370
|
+
const issueRuntime = project ? getIssueRuntime(project, issue2.iid) : void 0;
|
|
131371
|
+
const prSelector = project ? getCanonicalPrSelector(project, issue2.iid) : void 0;
|
|
131226
131372
|
let aborted2 = false;
|
|
131227
131373
|
if (actions) {
|
|
131228
131374
|
for (const action of actions) {
|
|
131229
131375
|
switch (action) {
|
|
131230
131376
|
case Action.MERGE_PR: {
|
|
131231
|
-
const status = await provider.getPrStatus(issue2.iid);
|
|
131377
|
+
const status = await provider.getPrStatus(issue2.iid, prSelector);
|
|
131232
131378
|
if (status.currentIssueMatch === false) {
|
|
131233
131379
|
aborted2 = true;
|
|
131234
131380
|
break;
|
|
131235
131381
|
}
|
|
131236
131382
|
if (status.state === PrState.MERGED) {
|
|
131237
|
-
const project = getProject(await readProjects(workspaceDir), projectName);
|
|
131238
|
-
const issueRuntime = project ? getIssueRuntime(project, issue2.iid) : void 0;
|
|
131239
131383
|
if (project && issueRuntime?.currentPrNumber) {
|
|
131240
131384
|
await persistMergedArtifact({
|
|
131241
131385
|
workspaceDir,
|
|
@@ -131249,9 +131393,7 @@ async function testSkipPass(opts) {
|
|
|
131249
131393
|
}
|
|
131250
131394
|
if (!status.url) break;
|
|
131251
131395
|
try {
|
|
131252
|
-
await provider.mergePr(issue2.iid);
|
|
131253
|
-
const project = getProject(await readProjects(workspaceDir), projectName);
|
|
131254
|
-
const issueRuntime = project ? getIssueRuntime(project, issue2.iid) : void 0;
|
|
131396
|
+
await provider.mergePr(issue2.iid, prSelector);
|
|
131255
131397
|
if (project && issueRuntime?.currentPrNumber) {
|
|
131256
131398
|
await persistMergedArtifact({
|
|
131257
131399
|
workspaceDir,
|
|
@@ -131291,8 +131433,6 @@ async function testSkipPass(opts) {
|
|
|
131291
131433
|
break;
|
|
131292
131434
|
case Action.CLOSE_ISSUE:
|
|
131293
131435
|
try {
|
|
131294
|
-
const project = getProject(await readProjects(workspaceDir), projectName);
|
|
131295
|
-
const issueRuntime = project ? getIssueRuntime(project, issue2.iid) : void 0;
|
|
131296
131436
|
if (!project) throw new Error(`Project not found: ${projectName}`);
|
|
131297
131437
|
await guardedCloseIssue({
|
|
131298
131438
|
workspaceDir,
|
|
@@ -131301,10 +131441,12 @@ async function testSkipPass(opts) {
|
|
|
131301
131441
|
issueId: issue2.iid,
|
|
131302
131442
|
role: "tester",
|
|
131303
131443
|
provider,
|
|
131444
|
+
selector: prSelector,
|
|
131304
131445
|
issueRuntime,
|
|
131305
131446
|
followUpPrRequired: issueRuntime?.followUpPrRequired === true
|
|
131306
131447
|
});
|
|
131307
131448
|
} catch {
|
|
131449
|
+
aborted2 = true;
|
|
131308
131450
|
}
|
|
131309
131451
|
break;
|
|
131310
131452
|
case Action.REOPEN_ISSUE:
|
|
@@ -131401,6 +131543,146 @@ async function holdEscapePass(opts) {
|
|
|
131401
131543
|
// lib/services/heartbeat/passes.ts
|
|
131402
131544
|
init_workflow();
|
|
131403
131545
|
init_audit();
|
|
131546
|
+
init_workflow();
|
|
131547
|
+
init_labels();
|
|
131548
|
+
|
|
131549
|
+
// lib/services/reviewer-completion.ts
|
|
131550
|
+
init_audit();
|
|
131551
|
+
init_labels();
|
|
131552
|
+
init_workflow();
|
|
131553
|
+
|
|
131554
|
+
// lib/services/reviewer-session.ts
|
|
131555
|
+
function extractTextContent(content) {
|
|
131556
|
+
if (typeof content === "string") return content;
|
|
131557
|
+
if (!Array.isArray(content)) return "";
|
|
131558
|
+
return content.filter((block) => typeof block === "object" && block != null).filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n");
|
|
131559
|
+
}
|
|
131560
|
+
function extractReviewerDecision(text) {
|
|
131561
|
+
const matches2 = Array.from(text.matchAll(/^\s*Review result:\s*(APPROVE|REJECT)\s*$/gim));
|
|
131562
|
+
const latestMatch = matches2.at(-1);
|
|
131563
|
+
if (!latestMatch) return null;
|
|
131564
|
+
return latestMatch[1]?.toUpperCase() === "APPROVE" ? "approve" : "reject";
|
|
131565
|
+
}
|
|
131566
|
+
function extractReviewerDecisionFromMessages(messages) {
|
|
131567
|
+
const assistantTexts = messages.filter((message) => typeof message === "object" && message != null).filter((message) => message.role === "assistant").map((message) => extractTextContent(message.content)).filter(Boolean).reverse();
|
|
131568
|
+
for (const text of assistantTexts) {
|
|
131569
|
+
const decision = extractReviewerDecision(text);
|
|
131570
|
+
if (decision) return decision;
|
|
131571
|
+
}
|
|
131572
|
+
return null;
|
|
131573
|
+
}
|
|
131574
|
+
async function parseReviewerSessionResult(runtime, sessionKey) {
|
|
131575
|
+
try {
|
|
131576
|
+
const messagesResult = await runtime.subagent?.getSessionMessages?.({ sessionKey });
|
|
131577
|
+
if (!messagesResult) return null;
|
|
131578
|
+
const messages = Array.isArray(messagesResult) ? messagesResult : Array.isArray(messagesResult?.messages) ? messagesResult.messages : [];
|
|
131579
|
+
return extractReviewerDecisionFromMessages(messages);
|
|
131580
|
+
} catch {
|
|
131581
|
+
return null;
|
|
131582
|
+
}
|
|
131583
|
+
}
|
|
131584
|
+
|
|
131585
|
+
// lib/services/reviewer-completion.ts
|
|
131586
|
+
function resolveReviewerDecisionTransition(workflow, decision) {
|
|
131587
|
+
const activeLabel = getActiveLabel(workflow, "reviewer");
|
|
131588
|
+
const reviewingState = findStateByLabel(workflow, activeLabel);
|
|
131589
|
+
if (!reviewingState?.on) return null;
|
|
131590
|
+
const eventKey = decision === "approve" ? WorkflowEvent.APPROVE : WorkflowEvent.REJECT;
|
|
131591
|
+
const transition2 = reviewingState.on[eventKey];
|
|
131592
|
+
const targetKey = typeof transition2 === "string" ? transition2 : transition2?.target;
|
|
131593
|
+
const targetState = targetKey ? workflow.states[targetKey] : void 0;
|
|
131594
|
+
if (!targetKey || !targetState) return null;
|
|
131595
|
+
return { eventKey, targetKey, targetLabel: targetState.label };
|
|
131596
|
+
}
|
|
131597
|
+
async function handleReviewerAgentEnd(opts) {
|
|
131598
|
+
const eventDecision = Array.isArray(opts.messages) && opts.messages.length > 0 ? extractReviewerDecisionFromMessages(opts.messages) : null;
|
|
131599
|
+
const decision = eventDecision ?? (opts.runtime ? await parseReviewerSessionResult(opts.runtime, opts.sessionKey) : null);
|
|
131600
|
+
if (!opts.workspaceDir || !opts.runCommand) {
|
|
131601
|
+
return decision;
|
|
131602
|
+
}
|
|
131603
|
+
const parsed = parseFabricaSessionKey(opts.sessionKey);
|
|
131604
|
+
if (!parsed || parsed.role !== "reviewer") {
|
|
131605
|
+
return decision;
|
|
131606
|
+
}
|
|
131607
|
+
const projects = await readProjects(opts.workspaceDir);
|
|
131608
|
+
const projectEntry = Object.entries(projects.projects).find(([, project2]) => project2.name === parsed.projectName);
|
|
131609
|
+
if (!projectEntry) {
|
|
131610
|
+
return decision;
|
|
131611
|
+
}
|
|
131612
|
+
const [projectSlug, project] = projectEntry;
|
|
131613
|
+
const reviewerWorker = project.workers.reviewer;
|
|
131614
|
+
if (!reviewerWorker) {
|
|
131615
|
+
return decision;
|
|
131616
|
+
}
|
|
131617
|
+
let issueId = null;
|
|
131618
|
+
let slotRef = null;
|
|
131619
|
+
for (const [level, slots] of Object.entries(reviewerWorker.levels)) {
|
|
131620
|
+
const slotIndex = slots.findIndex((candidate) => candidate.sessionKey === opts.sessionKey);
|
|
131621
|
+
if (slotIndex >= 0) {
|
|
131622
|
+
const slot = slots[slotIndex];
|
|
131623
|
+
issueId = Number(slot.issueId ?? slot.lastIssueId ?? 0) || null;
|
|
131624
|
+
slotRef = { level, slotIndex, active: slot.active };
|
|
131625
|
+
break;
|
|
131626
|
+
}
|
|
131627
|
+
}
|
|
131628
|
+
if (!issueId) {
|
|
131629
|
+
return decision;
|
|
131630
|
+
}
|
|
131631
|
+
const { workflow } = await loadConfig(opts.workspaceDir, projectSlug);
|
|
131632
|
+
const activeLabel = getActiveLabel(workflow, "reviewer");
|
|
131633
|
+
const revertLabel = getRevertLabel(workflow, "reviewer");
|
|
131634
|
+
const { provider } = await createProvider({
|
|
131635
|
+
repo: project.repo,
|
|
131636
|
+
provider: project.provider,
|
|
131637
|
+
runCommand: opts.runCommand
|
|
131638
|
+
});
|
|
131639
|
+
const issue2 = await provider.getIssue(issueId);
|
|
131640
|
+
const currentLabel = issue2.labels.find((label) => label === activeLabel || label === revertLabel);
|
|
131641
|
+
if (!currentLabel) {
|
|
131642
|
+
return decision;
|
|
131643
|
+
}
|
|
131644
|
+
if (decision) {
|
|
131645
|
+
const transition2 = resolveReviewerDecisionTransition(workflow, decision);
|
|
131646
|
+
if (transition2 && transition2.targetLabel !== currentLabel) {
|
|
131647
|
+
await resilientLabelTransition(provider, issueId, currentLabel, transition2.targetLabel);
|
|
131648
|
+
if (slotRef?.active) {
|
|
131649
|
+
await deactivateWorker(opts.workspaceDir, projectSlug, "reviewer", {
|
|
131650
|
+
level: slotRef.level,
|
|
131651
|
+
slotIndex: slotRef.slotIndex
|
|
131652
|
+
});
|
|
131653
|
+
}
|
|
131654
|
+
await log(opts.workspaceDir, "reviewer_session_transition", {
|
|
131655
|
+
sessionKey: opts.sessionKey,
|
|
131656
|
+
project: parsed.projectName,
|
|
131657
|
+
issueId,
|
|
131658
|
+
result: decision,
|
|
131659
|
+
eventKey: transition2.eventKey,
|
|
131660
|
+
from: currentLabel,
|
|
131661
|
+
to: transition2.targetLabel
|
|
131662
|
+
}).catch(() => {
|
|
131663
|
+
});
|
|
131664
|
+
}
|
|
131665
|
+
return decision;
|
|
131666
|
+
}
|
|
131667
|
+
if (opts.fallbackToQueueOnUndetermined && currentLabel === activeLabel) {
|
|
131668
|
+
await provider.transitionLabel(issueId, activeLabel, revertLabel);
|
|
131669
|
+
if (slotRef?.active) {
|
|
131670
|
+
await deactivateWorker(opts.workspaceDir, projectSlug, "reviewer", {
|
|
131671
|
+
level: slotRef.level,
|
|
131672
|
+
slotIndex: slotRef.slotIndex
|
|
131673
|
+
});
|
|
131674
|
+
}
|
|
131675
|
+
await log(opts.workspaceDir, "reviewer_session_no_result", {
|
|
131676
|
+
sessionKey: opts.sessionKey,
|
|
131677
|
+
project: parsed.projectName,
|
|
131678
|
+
issueId
|
|
131679
|
+
}).catch(() => {
|
|
131680
|
+
});
|
|
131681
|
+
}
|
|
131682
|
+
return null;
|
|
131683
|
+
}
|
|
131684
|
+
|
|
131685
|
+
// lib/services/heartbeat/passes.ts
|
|
131404
131686
|
async function fixDualStateLabels(workspaceDir, projectSlug, project, provider, resolvedConfig) {
|
|
131405
131687
|
const stateLabels = getStateLabels(resolvedConfig.workflow);
|
|
131406
131688
|
if (stateLabels.length === 0) return 0;
|
|
@@ -131448,24 +131730,27 @@ async function performHealthPass(workspaceDir, projectSlug, project, sessions, p
|
|
|
131448
131730
|
for (const intent of pendingIntents) {
|
|
131449
131731
|
if (intent.ts < staleThreshold) {
|
|
131450
131732
|
try {
|
|
131451
|
-
|
|
131452
|
-
|
|
131453
|
-
|
|
131454
|
-
|
|
131455
|
-
|
|
131456
|
-
|
|
131457
|
-
|
|
131458
|
-
|
|
131459
|
-
|
|
131460
|
-
|
|
131461
|
-
|
|
131462
|
-
|
|
131463
|
-
|
|
131464
|
-
|
|
131733
|
+
const eventType = typeof intent.data?.type === "string" ? intent.data.type : void 0;
|
|
131734
|
+
const notificationsEnabled = eventType ? notifyConfig[eventType] !== false : true;
|
|
131735
|
+
const hasDeliveryTarget = Boolean(intent.deliveryTarget?.channelId);
|
|
131736
|
+
const sent = intent.deliveryTarget?.channelId ? await notify(intent.data, {
|
|
131737
|
+
workspaceDir,
|
|
131738
|
+
config: notifyConfig,
|
|
131739
|
+
runtime,
|
|
131740
|
+
runCommand,
|
|
131741
|
+
deliveryTargetOverride: intent.deliveryTarget,
|
|
131742
|
+
skipOutboxWrite: true
|
|
131743
|
+
}) : await notify(intent.data, {
|
|
131744
|
+
workspaceDir,
|
|
131745
|
+
config: notifyConfig,
|
|
131746
|
+
runtime,
|
|
131747
|
+
runCommand,
|
|
131748
|
+
skipOutboxWrite: true
|
|
131749
|
+
});
|
|
131750
|
+
if (sent && hasDeliveryTarget && notificationsEnabled) {
|
|
131751
|
+
await markDelivered(workspaceDir, intent.key).catch(() => {
|
|
131465
131752
|
});
|
|
131466
131753
|
}
|
|
131467
|
-
await markDelivered(workspaceDir, intent.key).catch(() => {
|
|
131468
|
-
});
|
|
131469
131754
|
} catch {
|
|
131470
131755
|
}
|
|
131471
131756
|
}
|
|
@@ -131483,7 +131768,8 @@ async function performHealthPass(workspaceDir, projectSlug, project, sessions, p
|
|
|
131483
131768
|
provider,
|
|
131484
131769
|
staleWorkerHours,
|
|
131485
131770
|
workflow: resolvedConfig?.workflow,
|
|
131486
|
-
dispatchConfirmTimeoutMs: resolvedConfig?.timeouts?.dispatchConfirmTimeoutMs
|
|
131771
|
+
dispatchConfirmTimeoutMs: resolvedConfig?.timeouts?.dispatchConfirmTimeoutMs,
|
|
131772
|
+
healthGracePeriodMs: resolvedConfig?.timeouts?.healthGracePeriodMs
|
|
131487
131773
|
});
|
|
131488
131774
|
fixedCount += healthFixes.filter((f3) => f3.fixed).length;
|
|
131489
131775
|
const orphanFixes = await scanOrphanedLabels({
|
|
@@ -131722,6 +132008,75 @@ async function performHoldEscapePass(workspaceDir, projectSlug, project, provide
|
|
|
131722
132008
|
}
|
|
131723
132009
|
});
|
|
131724
132010
|
}
|
|
132011
|
+
async function performReviewerPollPass(workspaceDir, projectSlug, project, provider, resolvedConfig, runtime) {
|
|
132012
|
+
if (!runtime) return 0;
|
|
132013
|
+
const workflow = resolvedConfig.workflow;
|
|
132014
|
+
let activeLabel;
|
|
132015
|
+
try {
|
|
132016
|
+
activeLabel = getActiveLabel(workflow, "reviewer");
|
|
132017
|
+
} catch {
|
|
132018
|
+
return 0;
|
|
132019
|
+
}
|
|
132020
|
+
const reviewerWorker = project.workers["reviewer"];
|
|
132021
|
+
if (!reviewerWorker) return 0;
|
|
132022
|
+
let transitions = 0;
|
|
132023
|
+
for (const [level, slots] of Object.entries(reviewerWorker.levels)) {
|
|
132024
|
+
for (let slotIndex = 0; slotIndex < slots.length; slotIndex++) {
|
|
132025
|
+
const slot = slots[slotIndex];
|
|
132026
|
+
if (!slot.active || !slot.sessionKey || !slot.issueId) continue;
|
|
132027
|
+
const startTime = slot.startTime ? new Date(slot.startTime).getTime() : 0;
|
|
132028
|
+
if (startTime && Date.now() - startTime < 2 * 6e4) continue;
|
|
132029
|
+
const reviewResult = await handleReviewerAgentEnd({
|
|
132030
|
+
sessionKey: slot.sessionKey,
|
|
132031
|
+
runtime
|
|
132032
|
+
});
|
|
132033
|
+
if (!reviewResult) continue;
|
|
132034
|
+
const transition2 = resolveReviewerDecisionTransition(workflow, reviewResult);
|
|
132035
|
+
if (!transition2) continue;
|
|
132036
|
+
let issue2;
|
|
132037
|
+
try {
|
|
132038
|
+
issue2 = await provider.getIssue(Number(slot.issueId));
|
|
132039
|
+
} catch {
|
|
132040
|
+
continue;
|
|
132041
|
+
}
|
|
132042
|
+
if (!issue2.labels.includes(activeLabel)) {
|
|
132043
|
+
await deactivateWorker(workspaceDir, projectSlug, "reviewer", {
|
|
132044
|
+
level,
|
|
132045
|
+
slotIndex
|
|
132046
|
+
});
|
|
132047
|
+
await log(workspaceDir, "reviewer_poll_slot_released", {
|
|
132048
|
+
sessionKey: slot.sessionKey,
|
|
132049
|
+
project: project.name,
|
|
132050
|
+
projectSlug,
|
|
132051
|
+
issueId: slot.issueId,
|
|
132052
|
+
from: activeLabel,
|
|
132053
|
+
currentLabels: issue2.labels,
|
|
132054
|
+
reason: "issue_already_moved"
|
|
132055
|
+
}).catch(() => {
|
|
132056
|
+
});
|
|
132057
|
+
continue;
|
|
132058
|
+
}
|
|
132059
|
+
await resilientLabelTransition(provider, Number(slot.issueId), activeLabel, transition2.targetLabel);
|
|
132060
|
+
await deactivateWorker(workspaceDir, projectSlug, "reviewer", {
|
|
132061
|
+
level,
|
|
132062
|
+
slotIndex
|
|
132063
|
+
});
|
|
132064
|
+
await log(workspaceDir, "reviewer_poll_transition", {
|
|
132065
|
+
sessionKey: slot.sessionKey,
|
|
132066
|
+
project: project.name,
|
|
132067
|
+
projectSlug,
|
|
132068
|
+
issueId: slot.issueId,
|
|
132069
|
+
result: reviewResult,
|
|
132070
|
+
eventKey: transition2.eventKey,
|
|
132071
|
+
from: activeLabel,
|
|
132072
|
+
to: transition2.targetLabel
|
|
132073
|
+
}).catch(() => {
|
|
132074
|
+
});
|
|
132075
|
+
transitions++;
|
|
132076
|
+
}
|
|
132077
|
+
}
|
|
132078
|
+
return transitions;
|
|
132079
|
+
}
|
|
131725
132080
|
|
|
131726
132081
|
// lib/github/pr-event-source.ts
|
|
131727
132082
|
function buildRunId2(installationId, repositoryId, prNumber, headSha) {
|
|
@@ -131828,7 +132183,8 @@ async function runPrDiscoveryPass(params) {
|
|
|
131828
132183
|
}
|
|
131829
132184
|
for (const { issueId } of activeSlots) {
|
|
131830
132185
|
try {
|
|
131831
|
-
const
|
|
132186
|
+
const prSelector = getCanonicalPrSelector(params.project, issueId);
|
|
132187
|
+
const prDetails = await params.provider.getPrDetails(issueId, prSelector);
|
|
131832
132188
|
if (!prDetails) {
|
|
131833
132189
|
result.skipped++;
|
|
131834
132190
|
continue;
|
|
@@ -131955,52 +132311,6 @@ async function checkGenesisHealth(workspaceDir) {
|
|
|
131955
132311
|
}
|
|
131956
132312
|
}
|
|
131957
132313
|
|
|
131958
|
-
// lib/observability/health-score.ts
|
|
131959
|
-
function computeHealthScore(input) {
|
|
131960
|
-
const signals = [];
|
|
131961
|
-
let totalWeight = 0;
|
|
131962
|
-
let weightedSum = 0;
|
|
131963
|
-
let hasMeaningfulData = false;
|
|
131964
|
-
function addSignal(name, raw, weight, meaningful = true) {
|
|
131965
|
-
if (raw === null) {
|
|
131966
|
-
signals.push({ name, raw, weighted: 0 });
|
|
131967
|
-
return;
|
|
131968
|
-
}
|
|
131969
|
-
if (meaningful) hasMeaningfulData = true;
|
|
131970
|
-
const clamped = Math.max(0, Math.min(1, raw));
|
|
131971
|
-
const weighted = clamped * weight * 100;
|
|
131972
|
-
signals.push({ name, raw, weighted });
|
|
131973
|
-
totalWeight += weight;
|
|
131974
|
-
weightedSum += weighted;
|
|
131975
|
-
}
|
|
131976
|
-
addSignal("completion_rate", input.completionRate, 0.25);
|
|
131977
|
-
const speedRatio = input.avgDispatchToCompletionMinutes !== null && input.avgDispatchToCompletionMinutes > 0 ? Math.min(1, input.baselineMinutes / input.avgDispatchToCompletionMinutes) : null;
|
|
131978
|
-
addSignal("dispatch_speed", speedRatio, 0.2);
|
|
131979
|
-
const errorScore = input.errorRate !== null ? 1 - input.errorRate : null;
|
|
131980
|
-
addSignal("error_rate", errorScore, 0.2);
|
|
131981
|
-
const queueScore = input.maxQueueDepth > 0 ? 1 - Math.min(1, input.queueDepth / input.maxQueueDepth) : 1;
|
|
131982
|
-
addSignal("queue_depth", queueScore, 0.15, false);
|
|
131983
|
-
addSignal("heartbeat_regularity", input.heartbeatRegularity, 0.2);
|
|
131984
|
-
if (!hasMeaningfulData) {
|
|
131985
|
-
return { score: 50, status: "degraded", signals };
|
|
131986
|
-
}
|
|
131987
|
-
const score = totalWeight > 0 ? Math.round(weightedSum / totalWeight) : 50;
|
|
131988
|
-
const status = score > 80 ? "healthy" : score >= 50 ? "degraded" : "unhealthy";
|
|
131989
|
-
return { score, status, signals };
|
|
131990
|
-
}
|
|
131991
|
-
|
|
131992
|
-
// lib/observability/alerting.ts
|
|
131993
|
-
function shouldAlert(score, threshold, state, now2) {
|
|
131994
|
-
if (score < threshold) {
|
|
131995
|
-
if (now2 - state.lastAlertTs < state.cooldownMs) return "skip";
|
|
131996
|
-
return "alert";
|
|
131997
|
-
}
|
|
131998
|
-
if (state.lastAlertScore < threshold && score >= 80) {
|
|
131999
|
-
return "recovered";
|
|
132000
|
-
}
|
|
132001
|
-
return "skip";
|
|
132002
|
-
}
|
|
132003
|
-
|
|
132004
132314
|
// lib/observability/tracer.ts
|
|
132005
132315
|
init_esm();
|
|
132006
132316
|
var TRACER_NAME = "fabrica.heartbeat";
|
|
@@ -132023,8 +132333,6 @@ async function withTelemetrySpan2(name, fn) {
|
|
|
132023
132333
|
|
|
132024
132334
|
// lib/services/heartbeat/tick-runner.ts
|
|
132025
132335
|
var discoveredProjects = /* @__PURE__ */ new Set();
|
|
132026
|
-
var _tickCount = 0;
|
|
132027
|
-
var _alertState = { lastAlertTs: 0, lastAlertScore: 100, cooldownMs: 18e5 };
|
|
132028
132336
|
function validateWorkflowIntegrity2(workflow) {
|
|
132029
132337
|
const errors = [];
|
|
132030
132338
|
const stateKeys = new Set(Object.keys(workflow.states));
|
|
@@ -132119,6 +132427,14 @@ async function tick(opts) {
|
|
|
132119
132427
|
}).catch((err) => {
|
|
132120
132428
|
opts.logger.warn?.(`PR discovery pass failed for ${slug}: ${err.message}`);
|
|
132121
132429
|
});
|
|
132430
|
+
result.totalReviewTransitions += await performReviewerPollPass(
|
|
132431
|
+
workspaceDir,
|
|
132432
|
+
slug,
|
|
132433
|
+
project,
|
|
132434
|
+
provider,
|
|
132435
|
+
resolvedConfig,
|
|
132436
|
+
runtime
|
|
132437
|
+
);
|
|
132122
132438
|
result.totalReviewTransitions += await performReviewPass(
|
|
132123
132439
|
workspaceDir,
|
|
132124
132440
|
slug,
|
|
@@ -132200,35 +132516,6 @@ async function tick(opts) {
|
|
|
132200
132516
|
pickups: result.totalPickups,
|
|
132201
132517
|
skipped: result.totalSkipped
|
|
132202
132518
|
});
|
|
132203
|
-
_tickCount++;
|
|
132204
|
-
if (_tickCount % 10 === 0) {
|
|
132205
|
-
const healthScore = computeHealthScore({
|
|
132206
|
-
completionRate: slugs.length > 0 ? result.totalPickups / Math.max(1, slugs.length) : null,
|
|
132207
|
-
avgDispatchToCompletionMinutes: null,
|
|
132208
|
-
baselineMinutes: 30,
|
|
132209
|
-
errorRate: slugs.length > 0 ? result.totalHealthFixes / Math.max(1, slugs.length) : null,
|
|
132210
|
-
queueDepth: result.totalSkipped,
|
|
132211
|
-
maxQueueDepth: Math.max(20, slugs.length * 3),
|
|
132212
|
-
heartbeatRegularity: null
|
|
132213
|
-
});
|
|
132214
|
-
await log(workspaceDir, "health_score", {
|
|
132215
|
-
score: healthScore.score,
|
|
132216
|
-
status: healthScore.status,
|
|
132217
|
-
tickCount: _tickCount
|
|
132218
|
-
}).catch(() => {
|
|
132219
|
-
});
|
|
132220
|
-
const decision = shouldAlert(healthScore.score, 60, _alertState, Date.now());
|
|
132221
|
-
if (decision === "alert" || decision === "recovered") {
|
|
132222
|
-
_alertState.lastAlertTs = Date.now();
|
|
132223
|
-
_alertState.lastAlertScore = healthScore.score;
|
|
132224
|
-
await log(workspaceDir, "health_alert", {
|
|
132225
|
-
decision,
|
|
132226
|
-
score: healthScore.score,
|
|
132227
|
-
status: healthScore.status
|
|
132228
|
-
}).catch(() => {
|
|
132229
|
-
});
|
|
132230
|
-
}
|
|
132231
|
-
}
|
|
132232
132519
|
return result;
|
|
132233
132520
|
}
|
|
132234
132521
|
async function checkProjectActive(workspaceDir, slug) {
|
|
@@ -132288,7 +132575,6 @@ function registerHeartbeatService(api, pluginCtx) {
|
|
|
132288
132575
|
}
|
|
132289
132576
|
});
|
|
132290
132577
|
}
|
|
132291
|
-
var DEFAULT_TICK_TIMEOUT_MS = 5e4;
|
|
132292
132578
|
var _ticksTimedOut = 0;
|
|
132293
132579
|
async function withTickMutex(fn) {
|
|
132294
132580
|
if (_anyTickRunning) return "busy";
|
|
@@ -132312,6 +132598,8 @@ async function runHeartbeatTick(ctx, logger6, mode) {
|
|
|
132312
132598
|
let timedOut = false;
|
|
132313
132599
|
try {
|
|
132314
132600
|
const workspace = discoverAgents(ctx.config)[0]?.workspace;
|
|
132601
|
+
const resolvedWorkspaceConfig = workspace ? await loadConfig(workspace).catch(() => null) : null;
|
|
132602
|
+
const tickTimeoutMs = resolveTickTimeoutMs(resolvedWorkspaceConfig);
|
|
132315
132603
|
const lifecycle = workspace ? await getLifecycleService(workspace, logger6) : null;
|
|
132316
132604
|
const run = () => ctx.observability.withContext(
|
|
132317
132605
|
{ phase: `heartbeat:${mode}` },
|
|
@@ -132344,10 +132632,10 @@ async function runHeartbeatTick(ctx, logger6, mode) {
|
|
|
132344
132632
|
}
|
|
132345
132633
|
};
|
|
132346
132634
|
const HARD_TICK_TIMEOUT_MS = 5 * 6e4;
|
|
132347
|
-
const raceResult = await raceWithTimeout(wrappedTickFn,
|
|
132635
|
+
const raceResult = await raceWithTimeout(wrappedTickFn, tickTimeoutMs, () => {
|
|
132348
132636
|
_ticksTimedOut++;
|
|
132349
132637
|
timedOut = true;
|
|
132350
|
-
logger6.warn(`work_heartbeat ${mode} tick timed out after ${
|
|
132638
|
+
logger6.warn(`work_heartbeat ${mode} tick timed out after ${tickTimeoutMs}ms (total timeouts: ${_ticksTimedOut})`);
|
|
132351
132639
|
const hardTimeout = setTimeout(() => {
|
|
132352
132640
|
logger6.error("tick_mutex: hard timeout \u2014 forcing mutex release");
|
|
132353
132641
|
_tickRunning[mode] = false;
|
|
@@ -135758,6 +136046,7 @@ var registerStep = {
|
|
|
135758
136046
|
const resolvedName = name ?? fail("Missing project name or repository target for registration");
|
|
135759
136047
|
const resolvedRepo = repo ?? fail("Missing project name or repository target for registration");
|
|
135760
136048
|
const resolvedChannelId = channelId ?? fail("Missing channel binding for project registration");
|
|
136049
|
+
const createProjectTopic = payload.metadata.source === "telegram-dm-bootstrap";
|
|
135761
136050
|
try {
|
|
135762
136051
|
const programmaticSources = ["telegram-dm-bootstrap", "genesis-trigger-script"];
|
|
135763
136052
|
const projectWorkflowConfig = programmaticSources.includes(payload.metadata.source ?? "") ? { workflow: { reviewPolicy: "agent" } } : void 0;
|
|
@@ -135776,7 +136065,7 @@ var registerStep = {
|
|
|
135776
136065
|
pluginConfig: ctx.pluginConfig,
|
|
135777
136066
|
baseBranch,
|
|
135778
136067
|
deployBranch: baseBranch,
|
|
135779
|
-
createProjectTopic
|
|
136068
|
+
createProjectTopic,
|
|
135780
136069
|
projectWorkflowConfig
|
|
135781
136070
|
});
|
|
135782
136071
|
if (programmaticSources.includes(payload.metadata.source ?? "") && output.activeWorkflow.reviewPolicy !== "agent") {
|
|
@@ -135800,6 +136089,7 @@ var registerStep = {
|
|
|
135800
136089
|
metadata: {
|
|
135801
136090
|
...payload.metadata,
|
|
135802
136091
|
project_registered: output.success,
|
|
136092
|
+
project_topic_created: createProjectTopic && output.success && output.messageThreadId != null,
|
|
135803
136093
|
project_slug: output.projectSlug ?? payload.metadata.project_slug,
|
|
135804
136094
|
repo_path: resolvedRepo,
|
|
135805
136095
|
channel_id: output.channelId,
|
|
@@ -136341,7 +136631,7 @@ async function runPipeline(initialPayload, ctx) {
|
|
|
136341
136631
|
ctx.log(`Step ${step.name} completed in ${stepDuration}ms`);
|
|
136342
136632
|
} catch (err) {
|
|
136343
136633
|
ctx.log(`Step ${step.name} FAILED: ${String(err)}`);
|
|
136344
|
-
const failureArtifacts = deriveArtifacts(payload);
|
|
136634
|
+
const failureArtifacts = mergeArtifacts(deriveArtifacts(payload), extractErrorArtifacts(err));
|
|
136345
136635
|
if (failureArtifacts.length > 0) {
|
|
136346
136636
|
const cleanupResults = await cleanupArtifacts(failureArtifacts, {
|
|
136347
136637
|
log: ctx.log,
|
|
@@ -136383,6 +136673,12 @@ function deriveArtifacts(payload) {
|
|
|
136383
136673
|
const provider = payload.provisioning.provider === "gitlab" ? "gitlab_repo" : "github_repo";
|
|
136384
136674
|
artifacts.push({ type: provider, id: payload.provisioning.repo_url });
|
|
136385
136675
|
}
|
|
136676
|
+
if (payload.metadata.project_topic_created === true && payload.metadata.channel_id && payload.metadata.message_thread_id != null) {
|
|
136677
|
+
artifacts.push({
|
|
136678
|
+
type: "forum_topic",
|
|
136679
|
+
id: buildForumTopicArtifactId(payload.metadata.channel_id, payload.metadata.message_thread_id)
|
|
136680
|
+
});
|
|
136681
|
+
}
|
|
136386
136682
|
if (payload.issues?.length) {
|
|
136387
136683
|
for (const issue2 of payload.issues) {
|
|
136388
136684
|
artifacts.push({ type: "github_issue", id: String(issue2.number) });
|
|
@@ -136390,6 +136686,31 @@ function deriveArtifacts(payload) {
|
|
|
136390
136686
|
}
|
|
136391
136687
|
return artifacts;
|
|
136392
136688
|
}
|
|
136689
|
+
function extractErrorArtifacts(err) {
|
|
136690
|
+
const artifacts = err?.artifacts;
|
|
136691
|
+
if (!Array.isArray(artifacts)) {
|
|
136692
|
+
return [];
|
|
136693
|
+
}
|
|
136694
|
+
return artifacts.filter(isPipelineArtifact);
|
|
136695
|
+
}
|
|
136696
|
+
function isPipelineArtifact(value) {
|
|
136697
|
+
if (!value || typeof value !== "object") {
|
|
136698
|
+
return false;
|
|
136699
|
+
}
|
|
136700
|
+
const artifact = value;
|
|
136701
|
+
return typeof artifact.id === "string" && (artifact.type === "github_repo" || artifact.type === "gitlab_repo" || artifact.type === "forum_topic" || artifact.type === "github_issue");
|
|
136702
|
+
}
|
|
136703
|
+
function mergeArtifacts(primary, secondary) {
|
|
136704
|
+
const merged = [];
|
|
136705
|
+
const seen = /* @__PURE__ */ new Set();
|
|
136706
|
+
for (const artifact of [...primary, ...secondary]) {
|
|
136707
|
+
const key = `${artifact.type}:${artifact.id}`;
|
|
136708
|
+
if (seen.has(key)) continue;
|
|
136709
|
+
seen.add(key);
|
|
136710
|
+
merged.push(artifact);
|
|
136711
|
+
}
|
|
136712
|
+
return merged;
|
|
136713
|
+
}
|
|
136393
136714
|
|
|
136394
136715
|
// lib/tools/admin/genesis.ts
|
|
136395
136716
|
import { homedir as homedir2 } from "node:os";
|
|
@@ -136398,7 +136719,7 @@ import { homedir as homedir2 } from "node:os";
|
|
|
136398
136719
|
import { createHash as createHash5, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
136399
136720
|
import { existsSync, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "node:fs";
|
|
136400
136721
|
import { join as join3 } from "node:path";
|
|
136401
|
-
import { randomUUID as
|
|
136722
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
136402
136723
|
var GENESIS_TOKEN_VERSION = 1;
|
|
136403
136724
|
var GENESIS_TOKEN_PREFIX = "g1";
|
|
136404
136725
|
var GENESIS_TOKEN_SECRET_FILE = ".commit-token-secret";
|
|
@@ -136580,7 +136901,7 @@ function normalizeGenesisRequest(params, existingPayload) {
|
|
|
136580
136901
|
const dryRun = typeof params.dry_run === "boolean" ? params.dry_run : existingPayload?.dry_run ?? false;
|
|
136581
136902
|
return {
|
|
136582
136903
|
phase: phaseCandidate,
|
|
136583
|
-
sessionId: normalizeOptionalString(params.session_id) ?? existingPayload?.session_id ??
|
|
136904
|
+
sessionId: normalizeOptionalString(params.session_id) ?? existingPayload?.session_id ?? randomUUID4(),
|
|
136584
136905
|
rawIdea: normalizeOptionalString(params.idea) ?? command ?? (existingPayload?.raw_idea ? existingPayload.raw_idea : void 0),
|
|
136585
136906
|
answers,
|
|
136586
136907
|
answersJson,
|
|
@@ -136941,13 +137262,18 @@ async function handleCommit(params, normalized, ctx) {
|
|
|
136941
137262
|
}
|
|
136942
137263
|
};
|
|
136943
137264
|
const result = await runPipeline(payload, ctx);
|
|
137265
|
+
const projectRegistered = result.payload.metadata.project_registered === true;
|
|
137266
|
+
const hasRunnableWork = (result.payload.issues?.length ?? 0) > 0 || result.payload.triage?.ready_for_dispatch === true;
|
|
137267
|
+
const programmaticCommitFailedClosed = !dryRun && result.success && !projectRegistered && !hasRunnableWork;
|
|
137268
|
+
const success2 = programmaticCommitFailedClosed ? false : result.success;
|
|
137269
|
+
const error48 = programmaticCommitFailedClosed ? "Programmatic commit produced no registered project and no runnable work" : result.error;
|
|
136944
137270
|
return jsonResult25({
|
|
136945
|
-
success:
|
|
137271
|
+
success: success2,
|
|
136946
137272
|
session_id: result.payload.session_id,
|
|
136947
137273
|
steps_executed: result.steps_executed,
|
|
136948
137274
|
steps_skipped: result.steps_skipped,
|
|
136949
137275
|
duration_ms: result.duration_ms,
|
|
136950
|
-
error:
|
|
137276
|
+
error: error48,
|
|
136951
137277
|
spec: result.payload.spec,
|
|
136952
137278
|
scaffold: result.payload.scaffold,
|
|
136953
137279
|
qa_contract: result.payload.qa_contract,
|
|
@@ -138349,7 +138675,7 @@ init_zod();
|
|
|
138349
138675
|
|
|
138350
138676
|
// lib/dispatch/telegram-bootstrap-session.ts
|
|
138351
138677
|
init_constants();
|
|
138352
|
-
import { createHash as createHash6 } from "node:crypto";
|
|
138678
|
+
import { createHash as createHash6, randomUUID as randomUUID5 } from "node:crypto";
|
|
138353
138679
|
import fs39 from "node:fs/promises";
|
|
138354
138680
|
import path41 from "node:path";
|
|
138355
138681
|
var SESSION_TTL_MS = 10 * 6e4;
|
|
@@ -138405,7 +138731,7 @@ async function writeTelegramBootstrapSession(workspaceDir, session) {
|
|
|
138405
138731
|
const dir = sessionsDir(workspaceDir);
|
|
138406
138732
|
await fs39.mkdir(dir, { recursive: true });
|
|
138407
138733
|
const file2 = sessionPath(workspaceDir, session.conversationId);
|
|
138408
|
-
const tmp = `${file2}.tmp`;
|
|
138734
|
+
const tmp = `${file2}.${randomUUID5()}.tmp`;
|
|
138409
138735
|
await fs39.writeFile(tmp, JSON.stringify(session, null, 2) + "\n", "utf-8");
|
|
138410
138736
|
await fs39.rename(tmp, file2);
|
|
138411
138737
|
}
|
|
@@ -138718,6 +139044,54 @@ function logBootstrapWarning(ctx, message) {
|
|
|
138718
139044
|
ctx.logger.info(message);
|
|
138719
139045
|
}
|
|
138720
139046
|
}
|
|
139047
|
+
async function runBootstrapPreflightOrFail(ctx, conversationId, workspaceDir, request, sourceRoute, options) {
|
|
139048
|
+
const telegramConfig = readFabricaTelegramConfig(ctx.pluginConfig);
|
|
139049
|
+
const existingSession = await readTelegramBootstrapSession(workspaceDir, conversationId);
|
|
139050
|
+
const language = options?.language ?? existingSession?.language ?? "pt";
|
|
139051
|
+
if (!telegramConfig.projectsForumChatId) {
|
|
139052
|
+
await upsertTelegramBootstrapSession(workspaceDir, {
|
|
139053
|
+
conversationId,
|
|
139054
|
+
rawIdea: request.rawIdea,
|
|
139055
|
+
projectName: request.projectName ?? void 0,
|
|
139056
|
+
stackHint: request.stackHint ?? void 0,
|
|
139057
|
+
repoUrl: request.repoUrl ?? void 0,
|
|
139058
|
+
repoPath: request.repoPath ?? void 0,
|
|
139059
|
+
sourceRoute,
|
|
139060
|
+
status: "failed",
|
|
139061
|
+
language,
|
|
139062
|
+
error: "missing_projects_forum_chat"
|
|
139063
|
+
});
|
|
139064
|
+
await sendTelegramText(
|
|
139065
|
+
ctx,
|
|
139066
|
+
conversationId,
|
|
139067
|
+
"A Fabrica precisa de um grupo de projetos configurado para criar projetos automaticamente. Configure 'telegram.projectsForumChatId' no openclaw.json do plugin."
|
|
139068
|
+
);
|
|
139069
|
+
return true;
|
|
139070
|
+
}
|
|
139071
|
+
const candidateSlug = inferProjectSlug(request.projectName ?? request.rawIdea);
|
|
139072
|
+
if (!candidateSlug) return false;
|
|
139073
|
+
const projects = await readProjects(workspaceDir).catch(() => null);
|
|
139074
|
+
if (!projects?.projects?.[candidateSlug]) return false;
|
|
139075
|
+
await upsertTelegramBootstrapSession(workspaceDir, {
|
|
139076
|
+
conversationId,
|
|
139077
|
+
rawIdea: request.rawIdea,
|
|
139078
|
+
projectName: request.projectName ?? void 0,
|
|
139079
|
+
stackHint: request.stackHint ?? void 0,
|
|
139080
|
+
repoUrl: request.repoUrl ?? void 0,
|
|
139081
|
+
repoPath: request.repoPath ?? void 0,
|
|
139082
|
+
sourceRoute,
|
|
139083
|
+
status: "failed",
|
|
139084
|
+
projectSlug: candidateSlug,
|
|
139085
|
+
language,
|
|
139086
|
+
error: "duplicate_project_slug"
|
|
139087
|
+
});
|
|
139088
|
+
await sendTelegramText(
|
|
139089
|
+
ctx,
|
|
139090
|
+
conversationId,
|
|
139091
|
+
`Ja existe um projeto registrado com o slug "${candidateSlug}". Use o fluxo administrativo para vincular canais ou ajustar o projeto existente.`
|
|
139092
|
+
);
|
|
139093
|
+
return true;
|
|
139094
|
+
}
|
|
138721
139095
|
async function classifyAndBootstrap(ctx, workspaceDir, conversationId, content) {
|
|
138722
139096
|
await upsertTelegramBootstrapSession(workspaceDir, {
|
|
138723
139097
|
conversationId,
|
|
@@ -138734,7 +139108,6 @@ async function classifyAndBootstrap(ctx, workspaceDir, conversationId, content)
|
|
|
138734
139108
|
return;
|
|
138735
139109
|
}
|
|
138736
139110
|
const language = classification.language ?? "pt";
|
|
138737
|
-
await sendTelegramText(ctx, conversationId, BOOTSTRAP_MESSAGES.ack[language]);
|
|
138738
139111
|
const parsed = parseBootstrapRequest(content);
|
|
138739
139112
|
if (classification.stackHint && !parsed.stackHint) {
|
|
138740
139113
|
parsed.stackHint = classification.stackHint;
|
|
@@ -138763,6 +139136,18 @@ async function classifyAndBootstrap(ctx, workspaceDir, conversationId, content)
|
|
|
138763
139136
|
}
|
|
138764
139137
|
}
|
|
138765
139138
|
const sourceRoute = { channel: "telegram", channelId: conversationId };
|
|
139139
|
+
if (parsed.stackHint) {
|
|
139140
|
+
const handled = await runBootstrapPreflightOrFail(
|
|
139141
|
+
ctx,
|
|
139142
|
+
conversationId,
|
|
139143
|
+
workspaceDir,
|
|
139144
|
+
incomingRequest,
|
|
139145
|
+
sourceRoute,
|
|
139146
|
+
{ language }
|
|
139147
|
+
);
|
|
139148
|
+
if (handled) return;
|
|
139149
|
+
}
|
|
139150
|
+
await sendTelegramText(ctx, conversationId, BOOTSTRAP_MESSAGES.ack[language]);
|
|
138766
139151
|
const session = await upsertTelegramBootstrapSession(workspaceDir, {
|
|
138767
139152
|
conversationId,
|
|
138768
139153
|
...incomingRequest,
|
|
@@ -138805,15 +139190,10 @@ function bootstrapWithTimeout(ctx, conversationId, workspaceDir, request, source
|
|
|
138805
139190
|
});
|
|
138806
139191
|
}
|
|
138807
139192
|
async function continueBootstrap(ctx, conversationId, workspaceDir, request, sourceRoute) {
|
|
138808
|
-
|
|
138809
|
-
if (!telegramConfig.projectsForumChatId) {
|
|
138810
|
-
await sendTelegramText(
|
|
138811
|
-
ctx,
|
|
138812
|
-
conversationId,
|
|
138813
|
-
"A Fabrica precisa de um grupo de projetos configurado para criar projetos automaticamente. Configure 'telegram.projectsForumChatId' no openclaw.json do plugin."
|
|
138814
|
-
);
|
|
139193
|
+
if (await runBootstrapPreflightOrFail(ctx, conversationId, workspaceDir, request, sourceRoute)) {
|
|
138815
139194
|
return;
|
|
138816
139195
|
}
|
|
139196
|
+
const telegramConfig = readFabricaTelegramConfig(ctx.pluginConfig);
|
|
138817
139197
|
const stackHint = request.stackHint;
|
|
138818
139198
|
if (!stackHint) {
|
|
138819
139199
|
const existingSession = await readTelegramBootstrapSession(workspaceDir, conversationId);
|
|
@@ -138864,18 +139244,6 @@ async function continueBootstrap(ctx, conversationId, workspaceDir, request, sou
|
|
|
138864
139244
|
}
|
|
138865
139245
|
}
|
|
138866
139246
|
}
|
|
138867
|
-
const candidateSlug = inferProjectSlug(request.projectName ?? request.rawIdea);
|
|
138868
|
-
if (candidateSlug) {
|
|
138869
|
-
const projects = await readProjects(workspaceDir).catch(() => null);
|
|
138870
|
-
if (projects?.projects?.[candidateSlug]) {
|
|
138871
|
-
await sendTelegramText(
|
|
138872
|
-
ctx,
|
|
138873
|
-
conversationId,
|
|
138874
|
-
`Ja existe um projeto registrado com o slug "${candidateSlug}". Use o fluxo administrativo para vincular canais ou ajustar o projeto existente.`
|
|
138875
|
-
);
|
|
138876
|
-
return;
|
|
138877
|
-
}
|
|
138878
|
-
}
|
|
138879
139247
|
const stepCtx = {
|
|
138880
139248
|
runCommand: async (cmd, args, opts) => {
|
|
138881
139249
|
const result2 = await ctx.runCommand([cmd, ...args], {
|
|
@@ -138931,7 +139299,7 @@ async function continueBootstrap(ctx, conversationId, workspaceDir, request, sou
|
|
|
138931
139299
|
const result = await runPipeline(payload, stepCtx);
|
|
138932
139300
|
if (!result.success) {
|
|
138933
139301
|
const orphanedRepoArtifact = result.artifacts?.find((a) => a.type === "github_repo" || a.type === "gitlab_repo");
|
|
138934
|
-
const projectRegistered = result.payload?.project_registered === true;
|
|
139302
|
+
const projectRegistered = result.payload?.metadata?.project_registered === true;
|
|
138935
139303
|
if (orphanedRepoArtifact && !projectRegistered) {
|
|
138936
139304
|
await upsertTelegramBootstrapSession(workspaceDir, {
|
|
138937
139305
|
conversationId,
|
|
@@ -139121,6 +139489,20 @@ function registerTelegramBootstrapHook(api, ctx) {
|
|
|
139121
139489
|
repoPath: existingSession.repoPath ?? null
|
|
139122
139490
|
};
|
|
139123
139491
|
ctx.logger.info(`[telegram-bootstrap] clarification resolved: stack=${mergedRequest.stackHint}, idea="${mergedRequest.rawIdea}" (conversation: ${conversationId})`);
|
|
139492
|
+
if (mergedRequest.stackHint) {
|
|
139493
|
+
const handled2 = await runBootstrapPreflightOrFail(
|
|
139494
|
+
ctx,
|
|
139495
|
+
conversationId,
|
|
139496
|
+
workspaceDir,
|
|
139497
|
+
mergedRequest,
|
|
139498
|
+
existingSession.sourceRoute ?? {
|
|
139499
|
+
channel: "telegram",
|
|
139500
|
+
channelId: conversationId
|
|
139501
|
+
},
|
|
139502
|
+
{ language: existingSession.language ?? "pt" }
|
|
139503
|
+
);
|
|
139504
|
+
if (handled2) return;
|
|
139505
|
+
}
|
|
139124
139506
|
bootstrapWithTimeout(ctx, conversationId, workspaceDir, mergedRequest, existingSession.sourceRoute ?? {
|
|
139125
139507
|
channel: "telegram",
|
|
139126
139508
|
channelId: conversationId
|
|
@@ -139185,19 +139567,19 @@ function registerTelegramBootstrapHook(api, ctx) {
|
|
|
139185
139567
|
repoPath: parsed.repoPath ?? null
|
|
139186
139568
|
};
|
|
139187
139569
|
const language = /\b(cria|crie|criar|construa|desenvolva|registre|novo projeto)\b/i.test(content) ? "pt" : "en";
|
|
139188
|
-
await sendTelegramText(ctx, conversationId, BOOTSTRAP_MESSAGES.ack[language]);
|
|
139189
|
-
const session = await upsertTelegramBootstrapSession(workspaceDir, {
|
|
139190
|
-
conversationId,
|
|
139191
|
-
...incomingRequest,
|
|
139192
|
-
sourceRoute: {
|
|
139193
|
-
channel: "telegram",
|
|
139194
|
-
channelId: conversationId
|
|
139195
|
-
},
|
|
139196
|
-
sourceChannel: "telegram",
|
|
139197
|
-
status: "received",
|
|
139198
|
-
language
|
|
139199
|
-
});
|
|
139200
139570
|
if (!parsed.stackHint) {
|
|
139571
|
+
await sendTelegramText(ctx, conversationId, BOOTSTRAP_MESSAGES.ack[language]);
|
|
139572
|
+
const session = await upsertTelegramBootstrapSession(workspaceDir, {
|
|
139573
|
+
conversationId,
|
|
139574
|
+
...incomingRequest,
|
|
139575
|
+
sourceRoute: {
|
|
139576
|
+
channel: "telegram",
|
|
139577
|
+
channelId: conversationId
|
|
139578
|
+
},
|
|
139579
|
+
sourceChannel: "telegram",
|
|
139580
|
+
status: "received",
|
|
139581
|
+
language
|
|
139582
|
+
});
|
|
139201
139583
|
const pendingClarification = !parsed.projectName ? "stack_and_name" : "stack";
|
|
139202
139584
|
await upsertTelegramBootstrapSession(workspaceDir, {
|
|
139203
139585
|
conversationId,
|
|
@@ -139210,6 +139592,30 @@ function registerTelegramBootstrapHook(api, ctx) {
|
|
|
139210
139592
|
await sendTelegramText(ctx, conversationId, buildClarificationMessage(parsed, pendingClarification, language));
|
|
139211
139593
|
return;
|
|
139212
139594
|
}
|
|
139595
|
+
const handled = await runBootstrapPreflightOrFail(
|
|
139596
|
+
ctx,
|
|
139597
|
+
conversationId,
|
|
139598
|
+
workspaceDir,
|
|
139599
|
+
incomingRequest,
|
|
139600
|
+
{
|
|
139601
|
+
channel: "telegram",
|
|
139602
|
+
channelId: conversationId
|
|
139603
|
+
},
|
|
139604
|
+
{ language }
|
|
139605
|
+
);
|
|
139606
|
+
if (handled) return;
|
|
139607
|
+
await sendTelegramText(ctx, conversationId, BOOTSTRAP_MESSAGES.ack[language]);
|
|
139608
|
+
await upsertTelegramBootstrapSession(workspaceDir, {
|
|
139609
|
+
conversationId,
|
|
139610
|
+
...incomingRequest,
|
|
139611
|
+
sourceRoute: {
|
|
139612
|
+
channel: "telegram",
|
|
139613
|
+
channelId: conversationId
|
|
139614
|
+
},
|
|
139615
|
+
sourceChannel: "telegram",
|
|
139616
|
+
status: "received",
|
|
139617
|
+
language
|
|
139618
|
+
});
|
|
139213
139619
|
bootstrapWithTimeout(ctx, conversationId, workspaceDir, incomingRequest, {
|
|
139214
139620
|
channel: "telegram",
|
|
139215
139621
|
channelId: conversationId
|
|
@@ -139529,7 +139935,7 @@ function registerGatewayLifecycleHook(api, ctx) {
|
|
|
139529
139935
|
init_audit();
|
|
139530
139936
|
|
|
139531
139937
|
// lib/dispatch/reactive-dispatch-hook.ts
|
|
139532
|
-
var COMPLETION_TOOLS = /* @__PURE__ */ new Set(["work_finish"
|
|
139938
|
+
var COMPLETION_TOOLS = /* @__PURE__ */ new Set(["work_finish"]);
|
|
139533
139939
|
var spawnTimes = /* @__PURE__ */ new Map();
|
|
139534
139940
|
function getSpawnTime(sessionKey) {
|
|
139535
139941
|
return spawnTimes.get(sessionKey);
|
|
@@ -139538,6 +139944,18 @@ function clearSpawnTime(sessionKey) {
|
|
|
139538
139944
|
spawnTimes.delete(sessionKey);
|
|
139539
139945
|
}
|
|
139540
139946
|
function registerReactiveDispatchHooks(api, ctx) {
|
|
139947
|
+
const workspaceDir = resolveWorkspaceDir(ctx.config);
|
|
139948
|
+
api.on("before_tool_call", async (event, eventCtx) => {
|
|
139949
|
+
if (event.toolName !== "work_finish") return;
|
|
139950
|
+
const runId = eventCtx.runId ?? event.runId;
|
|
139951
|
+
if (!runId) return;
|
|
139952
|
+
return {
|
|
139953
|
+
params: {
|
|
139954
|
+
...event.params,
|
|
139955
|
+
_dispatchRunId: runId
|
|
139956
|
+
}
|
|
139957
|
+
};
|
|
139958
|
+
});
|
|
139541
139959
|
api.on("after_tool_call", async (event, _eventCtx) => {
|
|
139542
139960
|
if (!COMPLETION_TOOLS.has(event.toolName)) return;
|
|
139543
139961
|
ctx.runtime?.system.requestHeartbeatNow({ reason: "work_finish", coalesceMs: 2e3 });
|
|
@@ -139557,6 +139975,10 @@ function registerReactiveDispatchHooks(api, ctx) {
|
|
|
139557
139975
|
const sessionKey = event.childSessionKey;
|
|
139558
139976
|
if (!sessionKey) return;
|
|
139559
139977
|
spawnTimes.set(sessionKey, Date.now());
|
|
139978
|
+
if (workspaceDir && event.runId) {
|
|
139979
|
+
await bindDispatchRunIdBySessionKey(workspaceDir, sessionKey, event.runId).catch(() => {
|
|
139980
|
+
});
|
|
139981
|
+
}
|
|
139560
139982
|
});
|
|
139561
139983
|
}
|
|
139562
139984
|
|
|
@@ -139599,7 +140021,7 @@ function registerSubagentLifecycleHook(api, ctx) {
|
|
|
139599
140021
|
let foundSlot;
|
|
139600
140022
|
for (const [level, slots] of Object.entries(roleWorker.levels)) {
|
|
139601
140023
|
for (let i2 = 0; i2 < slots.length; i2++) {
|
|
139602
|
-
if (slots[i2].sessionKey === sessionKey
|
|
140024
|
+
if (slots[i2].sessionKey === sessionKey) {
|
|
139603
140025
|
foundLevel = level;
|
|
139604
140026
|
foundSlotIndex = i2;
|
|
139605
140027
|
foundSlot = slots[i2];
|
|
@@ -139609,24 +140031,88 @@ function registerSubagentLifecycleHook(api, ctx) {
|
|
|
139609
140031
|
if (foundSlot) break;
|
|
139610
140032
|
}
|
|
139611
140033
|
if (!foundSlot || foundLevel == null || foundSlotIndex == null) return;
|
|
139612
|
-
const issueId = foundSlot.issueId;
|
|
139613
|
-
|
|
139614
|
-
|
|
139615
|
-
|
|
139616
|
-
|
|
140034
|
+
const issueId = foundSlot.issueId ?? foundSlot.lastIssueId;
|
|
140035
|
+
const issueRuntime = issueId ? project.issueRuntime?.[String(issueId)] : void 0;
|
|
140036
|
+
const currentDispatchRunId = foundSlot.dispatchRunId ?? issueRuntime?.dispatchRunId ?? null;
|
|
140037
|
+
const currentDispatchCycleId = foundSlot.dispatchCycleId ?? issueRuntime?.lastDispatchCycleId ?? null;
|
|
140038
|
+
if (event.runId && currentDispatchRunId && event.runId !== currentDispatchRunId) {
|
|
140039
|
+
await log(workspaceDir, "subagent_ended_slot_cleanup_rejected", {
|
|
140040
|
+
sessionKey,
|
|
140041
|
+
project: projectName,
|
|
140042
|
+
projectSlug,
|
|
140043
|
+
role,
|
|
140044
|
+
level: foundLevel,
|
|
140045
|
+
slotIndex: foundSlotIndex,
|
|
140046
|
+
issueId,
|
|
140047
|
+
reason: "stale_dispatch_cycle",
|
|
140048
|
+
eventRunId: event.runId,
|
|
140049
|
+
currentDispatchRunId,
|
|
140050
|
+
currentDispatchCycleId
|
|
140051
|
+
}).catch(() => {
|
|
140052
|
+
});
|
|
140053
|
+
return;
|
|
140054
|
+
}
|
|
140055
|
+
if (foundSlot.dispatchCycleId && issueRuntime?.lastDispatchCycleId && foundSlot.dispatchCycleId !== issueRuntime.lastDispatchCycleId) {
|
|
140056
|
+
await log(workspaceDir, "subagent_ended_slot_cleanup_rejected", {
|
|
140057
|
+
sessionKey,
|
|
140058
|
+
project: projectName,
|
|
140059
|
+
projectSlug,
|
|
140060
|
+
role,
|
|
140061
|
+
level: foundLevel,
|
|
140062
|
+
slotIndex: foundSlotIndex,
|
|
140063
|
+
issueId,
|
|
140064
|
+
reason: "stale_dispatch_cycle",
|
|
140065
|
+
eventRunId: event.runId ?? null,
|
|
140066
|
+
currentDispatchRunId,
|
|
140067
|
+
slotDispatchCycleId: foundSlot.dispatchCycleId,
|
|
140068
|
+
runtimeDispatchCycleId: issueRuntime.lastDispatchCycleId
|
|
140069
|
+
}).catch(() => {
|
|
140070
|
+
});
|
|
140071
|
+
return;
|
|
140072
|
+
}
|
|
140073
|
+
if (foundSlot.active) {
|
|
140074
|
+
await deactivateWorker(workspaceDir, projectSlug, role, {
|
|
140075
|
+
level: foundLevel,
|
|
140076
|
+
slotIndex: foundSlotIndex
|
|
140077
|
+
});
|
|
140078
|
+
}
|
|
139617
140079
|
if (issueId) {
|
|
139618
140080
|
try {
|
|
139619
|
-
|
|
139620
|
-
|
|
139621
|
-
|
|
139622
|
-
|
|
139623
|
-
|
|
139624
|
-
|
|
139625
|
-
|
|
139626
|
-
|
|
139627
|
-
|
|
139628
|
-
|
|
139629
|
-
|
|
140081
|
+
if (role === "reviewer") {
|
|
140082
|
+
const reviewResult = await handleReviewerAgentEnd({
|
|
140083
|
+
sessionKey,
|
|
140084
|
+
runtime: ctx.runtime,
|
|
140085
|
+
workspaceDir,
|
|
140086
|
+
runCommand: ctx.runCommand,
|
|
140087
|
+
fallbackToQueueOnUndetermined: true
|
|
140088
|
+
});
|
|
140089
|
+
if (!reviewResult) {
|
|
140090
|
+
const { workflow } = await loadConfig(workspaceDir, projectSlug);
|
|
140091
|
+
const activeLabel = getActiveLabel(workflow, role);
|
|
140092
|
+
const revertLabel = getRevertLabel(workflow, role);
|
|
140093
|
+
const { provider } = await createProvider({
|
|
140094
|
+
repo: project.repo,
|
|
140095
|
+
provider: project.provider,
|
|
140096
|
+
runCommand: ctx.runCommand
|
|
140097
|
+
});
|
|
140098
|
+
const issue2 = await provider.getIssue(Number(issueId));
|
|
140099
|
+
if (issue2.labels.includes(activeLabel)) {
|
|
140100
|
+
await provider.transitionLabel(Number(issueId), activeLabel, revertLabel);
|
|
140101
|
+
}
|
|
140102
|
+
}
|
|
140103
|
+
} else {
|
|
140104
|
+
const { workflow } = await loadConfig(workspaceDir, projectSlug);
|
|
140105
|
+
const activeLabel = getActiveLabel(workflow, role);
|
|
140106
|
+
const revertLabel = getRevertLabel(workflow, role);
|
|
140107
|
+
const { provider } = await createProvider({
|
|
140108
|
+
repo: project.repo,
|
|
140109
|
+
provider: project.provider,
|
|
140110
|
+
runCommand: ctx.runCommand
|
|
140111
|
+
});
|
|
140112
|
+
const issue2 = await provider.getIssue(Number(issueId));
|
|
140113
|
+
if (issue2.labels.includes(activeLabel)) {
|
|
140114
|
+
await provider.transitionLabel(Number(issueId), activeLabel, revertLabel);
|
|
140115
|
+
}
|
|
139630
140116
|
}
|
|
139631
140117
|
} catch {
|
|
139632
140118
|
}
|
|
@@ -139711,13 +140197,25 @@ When you have finished your task, you MUST call the \`work_finish\` tool to sign
|
|
|
139711
140197
|
Do NOT rely on your session ending automatically \u2014 you must explicitly call \`work_finish\`.
|
|
139712
140198
|
This is required for the pipeline to advance to the next stage.
|
|
139713
140199
|
`;
|
|
140200
|
+
var REVIEWER_COMPLETION_CONTEXT = `## Task Completion
|
|
140201
|
+
|
|
140202
|
+
When you finish the review, signal completion by ending your response with the decision line below.
|
|
140203
|
+
End your response with exactly one decision line in plain text:
|
|
140204
|
+
- \`Review result: APPROVE\`
|
|
140205
|
+
- \`Review result: REJECT\`
|
|
140206
|
+
|
|
140207
|
+
The orchestrator reads that line directly from your response and advances the review stage automatically.
|
|
140208
|
+
If you need the project slug for follow-up tools such as \`task_create\`, use the value from the \`Channel:\` line in the task message.
|
|
140209
|
+
`;
|
|
139714
140210
|
function registerWorkerContextHook(api, _ctx) {
|
|
139715
140211
|
api.on("before_agent_start", async (_event, eventCtx) => {
|
|
139716
140212
|
const sessionKey = eventCtx.sessionKey;
|
|
139717
140213
|
if (!sessionKey) return;
|
|
139718
140214
|
const parsed = parseFabricaSessionKey(sessionKey);
|
|
139719
140215
|
if (!parsed) return;
|
|
139720
|
-
return {
|
|
140216
|
+
return {
|
|
140217
|
+
prependSystemContext: parsed.role === "reviewer" ? REVIEWER_COMPLETION_CONTEXT : WORK_FINISH_CONTEXT
|
|
140218
|
+
};
|
|
139721
140219
|
});
|
|
139722
140220
|
}
|
|
139723
140221
|
|
|
@@ -139758,14 +140256,30 @@ var plugin = {
|
|
|
139758
140256
|
type: "string",
|
|
139759
140257
|
description: "Environment variable containing the GitHub App webhook secret."
|
|
139760
140258
|
},
|
|
140259
|
+
webhookSecret: {
|
|
140260
|
+
type: "string",
|
|
140261
|
+
description: "Inline GitHub App webhook secret."
|
|
140262
|
+
},
|
|
140263
|
+
webhookSecretPath: {
|
|
140264
|
+
type: "string",
|
|
140265
|
+
description: "Filesystem path containing the GitHub App webhook secret."
|
|
140266
|
+
},
|
|
140267
|
+
webhookMode: {
|
|
140268
|
+
type: "string",
|
|
140269
|
+
enum: ["required", "optional", "disabled"],
|
|
140270
|
+
description: "Controls GitHub webhook route registration behavior."
|
|
140271
|
+
},
|
|
139761
140272
|
authProfiles: {
|
|
139762
140273
|
type: "object",
|
|
139763
140274
|
additionalProperties: {
|
|
139764
140275
|
type: "object",
|
|
139765
140276
|
properties: {
|
|
139766
140277
|
mode: { type: "string", enum: ["github-app"] },
|
|
140278
|
+
appId: { type: "string" },
|
|
139767
140279
|
appIdEnv: { type: "string" },
|
|
140280
|
+
privateKey: { type: "string" },
|
|
139768
140281
|
privateKeyEnv: { type: "string" },
|
|
140282
|
+
privateKeyPath: { type: "string" },
|
|
139769
140283
|
privateKeyPathEnv: { type: "string" },
|
|
139770
140284
|
baseUrl: { type: "string" },
|
|
139771
140285
|
fallbackMode: { type: "string", enum: ["pr-conversation-comment"] },
|