@mestreyoda/fabrica 0.2.1 → 0.2.2
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/dist/index.js +97 -334
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -111324,8 +111324,8 @@ import fsSync from "node:fs";
|
|
|
111324
111324
|
import path5 from "node:path";
|
|
111325
111325
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
111326
111326
|
function getCurrentVersion() {
|
|
111327
|
-
if ("0.2.
|
|
111328
|
-
return "0.2.
|
|
111327
|
+
if ("0.2.2") {
|
|
111328
|
+
return "0.2.2";
|
|
111329
111329
|
}
|
|
111330
111330
|
try {
|
|
111331
111331
|
const pkgPath = path5.join(THIS_DIR, "..", "..", "package.json");
|
|
@@ -124578,103 +124578,6 @@ function createProjectRegisterTool(ctx) {
|
|
|
124578
124578
|
import { jsonResult as jsonResult15 } from "openclaw/plugin-sdk";
|
|
124579
124579
|
init_audit();
|
|
124580
124580
|
|
|
124581
|
-
// lib/services/heartbeat/diagnostic.ts
|
|
124582
|
-
import { execFileSync } from "child_process";
|
|
124583
|
-
async function diagnoseStall(input) {
|
|
124584
|
-
const { owner, repo, issueId, dispatchAttemptCount } = input;
|
|
124585
|
-
if ((dispatchAttemptCount ?? 0) >= 2) {
|
|
124586
|
-
const hasArtifacts = await checkForArtifacts(owner, repo, issueId);
|
|
124587
|
-
if (!hasArtifacts) {
|
|
124588
|
-
return { action: "needs_human_review", reason: "stall", evidence: `${dispatchAttemptCount} attempts, zero artifacts` };
|
|
124589
|
-
}
|
|
124590
|
-
}
|
|
124591
|
-
const pr = await findPrForIssue(owner, repo, issueId);
|
|
124592
|
-
if (pr) {
|
|
124593
|
-
const qaStatus = await checkPrQaStatus(owner, repo, pr.number);
|
|
124594
|
-
if (qaStatus === "pass") {
|
|
124595
|
-
return { action: "transition_to_review", reason: "stall", evidence: `PR #${pr.number} exists, QA passing`, prNumber: pr.number };
|
|
124596
|
-
}
|
|
124597
|
-
return { action: "redispatch_same_level", reason: "stall", evidence: `PR #${pr.number} exists, QA ${qaStatus}`, prNumber: pr.number };
|
|
124598
|
-
}
|
|
124599
|
-
const hasCommits = await checkForBranchCommits(owner, repo, issueId);
|
|
124600
|
-
if (hasCommits) {
|
|
124601
|
-
return { action: "nudge_open_pr", reason: "stall", evidence: "Branch has commits but no PR" };
|
|
124602
|
-
}
|
|
124603
|
-
const sessionAge = Date.now() - input.sessionUpdatedAt;
|
|
124604
|
-
const isSessionDead = sessionAge > 30 * 6e4;
|
|
124605
|
-
if (isSessionDead) {
|
|
124606
|
-
return { action: "log_infra", reason: "infra", evidence: "Session dead, zero artifacts" };
|
|
124607
|
-
}
|
|
124608
|
-
return { action: "escalate_level", reason: "complexity", evidence: "Active session without commits" };
|
|
124609
|
-
}
|
|
124610
|
-
function ghSync(args) {
|
|
124611
|
-
return execFileSync("gh", args, { encoding: "utf-8", timeout: 15e3 });
|
|
124612
|
-
}
|
|
124613
|
-
async function findPrForIssue(owner, repo, issueId) {
|
|
124614
|
-
if (!owner || !repo) return null;
|
|
124615
|
-
try {
|
|
124616
|
-
const stdout = ghSync([
|
|
124617
|
-
"pr",
|
|
124618
|
-
"list",
|
|
124619
|
-
"--repo",
|
|
124620
|
-
`${owner}/${repo}`,
|
|
124621
|
-
"--search",
|
|
124622
|
-
`${issueId}`,
|
|
124623
|
-
"--json",
|
|
124624
|
-
"number",
|
|
124625
|
-
"--limit",
|
|
124626
|
-
"1"
|
|
124627
|
-
]);
|
|
124628
|
-
const prs = JSON.parse(stdout);
|
|
124629
|
-
return prs.length > 0 ? prs[0] : null;
|
|
124630
|
-
} catch {
|
|
124631
|
-
return null;
|
|
124632
|
-
}
|
|
124633
|
-
}
|
|
124634
|
-
async function checkPrQaStatus(owner, repo, prNumber) {
|
|
124635
|
-
if (!owner || !repo) return "pending";
|
|
124636
|
-
try {
|
|
124637
|
-
const stdout = ghSync([
|
|
124638
|
-
"pr",
|
|
124639
|
-
"view",
|
|
124640
|
-
String(prNumber),
|
|
124641
|
-
"--repo",
|
|
124642
|
-
`${owner}/${repo}`,
|
|
124643
|
-
"--json",
|
|
124644
|
-
"statusCheckRollup"
|
|
124645
|
-
]);
|
|
124646
|
-
const data = JSON.parse(stdout);
|
|
124647
|
-
const checks = data.statusCheckRollup ?? [];
|
|
124648
|
-
if (checks.length === 0) return "pass";
|
|
124649
|
-
if (checks.every((c) => c.conclusion === "SUCCESS")) return "pass";
|
|
124650
|
-
if (checks.some((c) => c.conclusion === "FAILURE")) return "fail";
|
|
124651
|
-
return "pending";
|
|
124652
|
-
} catch {
|
|
124653
|
-
return "pass";
|
|
124654
|
-
}
|
|
124655
|
-
}
|
|
124656
|
-
async function checkForBranchCommits(owner, repo, issueId) {
|
|
124657
|
-
if (!owner || !repo) return false;
|
|
124658
|
-
try {
|
|
124659
|
-
const stdout = ghSync([
|
|
124660
|
-
"api",
|
|
124661
|
-
`repos/${owner}/${repo}/commits`,
|
|
124662
|
-
"--jq",
|
|
124663
|
-
".[0].sha",
|
|
124664
|
-
"-f",
|
|
124665
|
-
`sha=issue-${issueId}`
|
|
124666
|
-
]);
|
|
124667
|
-
return stdout.trim().length > 0;
|
|
124668
|
-
} catch {
|
|
124669
|
-
return false;
|
|
124670
|
-
}
|
|
124671
|
-
}
|
|
124672
|
-
async function checkForArtifacts(owner, repo, issueId) {
|
|
124673
|
-
const pr = await findPrForIssue(owner, repo, issueId);
|
|
124674
|
-
if (pr) return true;
|
|
124675
|
-
return checkForBranchCommits(owner, repo, issueId);
|
|
124676
|
-
}
|
|
124677
|
-
|
|
124678
124581
|
// lib/services/heartbeat/health.ts
|
|
124679
124582
|
init_audit();
|
|
124680
124583
|
init_workflow();
|
|
@@ -124682,9 +124585,6 @@ init_context3();
|
|
|
124682
124585
|
init_labels();
|
|
124683
124586
|
var GRACE_PERIOD_MS = 5 * 60 * 1e3;
|
|
124684
124587
|
var DISPATCH_CONFIRMATION_TIMEOUT_MS = 2 * 60 * 1e3;
|
|
124685
|
-
var NUDGE_MESSAGE = `You appear to have stalled. Continue working on your current task. If you are blocked or unable to proceed, call work_finish with result "blocked".`;
|
|
124686
|
-
var MAX_STALL_NUDGES = 3;
|
|
124687
|
-
var MAX_DISPATCH_ATTEMPTS = 5;
|
|
124688
124588
|
async function auditHealthFixApplied(workspaceDir, fix, details) {
|
|
124689
124589
|
if (!fix.fixed) return;
|
|
124690
124590
|
await withCorrelationContext({
|
|
@@ -124716,8 +124616,7 @@ async function auditHealthFixApplied(workspaceDir, fix, details) {
|
|
|
124716
124616
|
toLabel: details.toLabel ?? fix.issue.expectedLabel ?? null,
|
|
124717
124617
|
idleMinutes: details.idleMinutes ?? null,
|
|
124718
124618
|
deliveryState: details.deliveryState ?? null,
|
|
124719
|
-
labelReverted: fix.labelReverted ?? null
|
|
124720
|
-
nudgeSent: fix.nudgeSent ?? false
|
|
124619
|
+
labelReverted: fix.labelReverted ?? null
|
|
124721
124620
|
}).catch(() => {
|
|
124722
124621
|
});
|
|
124723
124622
|
}));
|
|
@@ -125050,218 +124949,6 @@ async function checkWorkerHealth(opts) {
|
|
|
125050
124949
|
fixes.push(fix);
|
|
125051
124950
|
continue;
|
|
125052
124951
|
}
|
|
125053
|
-
if (slot.active && sessionKey && sessions && !withinGracePeriod && isSessionAlive(sessionKey, sessions)) {
|
|
125054
|
-
const session = sessions.get(sessionKey);
|
|
125055
|
-
const stallThresholdMs = (opts.stallTimeoutMinutes ?? 5) * 6e4;
|
|
125056
|
-
if (session.updatedAt == null) continue;
|
|
125057
|
-
const sessionIdleMs = Date.now() - session.updatedAt;
|
|
125058
|
-
if (sessionIdleMs > stallThresholdMs) {
|
|
125059
|
-
const idleMinutes = Math.round(sessionIdleMs / 6e4);
|
|
125060
|
-
const repoRemote = project.repoRemote ?? "";
|
|
125061
|
-
const remoteMatch = repoRemote.match(/github\.com\/([^/]+)\/([^/.]+)/);
|
|
125062
|
-
const diagnostic = await diagnoseStall({
|
|
125063
|
-
projectSlug,
|
|
125064
|
-
owner: remoteMatch?.[1] ?? "",
|
|
125065
|
-
repo: remoteMatch?.[2] ?? "",
|
|
125066
|
-
issueId: issueIdNum ?? 0,
|
|
125067
|
-
sessionKey,
|
|
125068
|
-
slotStartTime: slot.startTime ? new Date(slot.startTime).getTime() : Date.now() - sessionIdleMs,
|
|
125069
|
-
sessionUpdatedAt: session.updatedAt,
|
|
125070
|
-
dispatchAttemptCount: issueRuntime?.dispatchAttemptCount ?? 0
|
|
125071
|
-
});
|
|
125072
|
-
const evidenceActions = ["transition_to_review", "nudge_open_pr"];
|
|
125073
|
-
const hasActionableEvidence = evidenceActions.includes(diagnostic.action);
|
|
125074
|
-
const modelUnresponsiveMs = stallThresholdMs * MAX_STALL_NUDGES;
|
|
125075
|
-
const slotAgeMs = slot.startTime ? Date.now() - new Date(slot.startTime).getTime() : sessionIdleMs;
|
|
125076
|
-
const isModelUnresponsive = slotAgeMs > modelUnresponsiveMs;
|
|
125077
|
-
const currentAttempts = issueRuntime?.dispatchAttemptCount ?? 0;
|
|
125078
|
-
if (currentAttempts >= MAX_DISPATCH_ATTEMPTS && !hasActionableEvidence) {
|
|
125079
|
-
const fix2 = {
|
|
125080
|
-
issue: {
|
|
125081
|
-
type: "diagnostic_needs_human_review",
|
|
125082
|
-
severity: "critical",
|
|
125083
|
-
project: project.name,
|
|
125084
|
-
projectSlug,
|
|
125085
|
-
role,
|
|
125086
|
-
level,
|
|
125087
|
-
sessionKey,
|
|
125088
|
-
issueId: slot.issueId,
|
|
125089
|
-
slotIndex,
|
|
125090
|
-
message: `${role.toUpperCase()} ${level}[${slotIndex}] circuit breaker: ${currentAttempts} dispatch attempts without completion \u2014 moving to Refining`
|
|
125091
|
-
},
|
|
125092
|
-
fixed: false
|
|
125093
|
-
};
|
|
125094
|
-
if (autoFix) {
|
|
125095
|
-
await revertLabel(fix2, expectedLabel, "Refining");
|
|
125096
|
-
await deactivateSlot();
|
|
125097
|
-
fix2.fixed = true;
|
|
125098
|
-
}
|
|
125099
|
-
await log(workspaceDir, "dispatch_circuit_breaker", {
|
|
125100
|
-
project: project.name,
|
|
125101
|
-
projectSlug,
|
|
125102
|
-
role,
|
|
125103
|
-
level,
|
|
125104
|
-
sessionKey,
|
|
125105
|
-
issueId: slot.issueId,
|
|
125106
|
-
slotIndex,
|
|
125107
|
-
dispatchAttemptCount: currentAttempts,
|
|
125108
|
-
diagnostic: diagnostic.action,
|
|
125109
|
-
evidence: diagnostic.evidence
|
|
125110
|
-
}).catch(() => {
|
|
125111
|
-
});
|
|
125112
|
-
fixes.push(fix2);
|
|
125113
|
-
continue;
|
|
125114
|
-
}
|
|
125115
|
-
if (hasActionableEvidence || isModelUnresponsive) {
|
|
125116
|
-
if (issueIdNum) {
|
|
125117
|
-
const runtimeUpdate = {
|
|
125118
|
-
lastDiagnosticResult: diagnostic.evidence,
|
|
125119
|
-
lastFailureReason: diagnostic.reason,
|
|
125120
|
-
dispatchAttemptCount: (issueRuntime?.dispatchAttemptCount ?? 0) + 1
|
|
125121
|
-
};
|
|
125122
|
-
if (diagnostic.prNumber) {
|
|
125123
|
-
const prOwner = remoteMatch?.[1] ?? "";
|
|
125124
|
-
const prRepo = remoteMatch?.[2] ?? "";
|
|
125125
|
-
runtimeUpdate.currentPrNumber = diagnostic.prNumber;
|
|
125126
|
-
runtimeUpdate.currentPrUrl = `https://github.com/${prOwner}/${prRepo}/pull/${diagnostic.prNumber}`;
|
|
125127
|
-
runtimeUpdate.currentPrState = "open";
|
|
125128
|
-
runtimeUpdate.bindingSource = "diagnostic";
|
|
125129
|
-
runtimeUpdate.bindingConfidence = "high";
|
|
125130
|
-
runtimeUpdate.boundAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
125131
|
-
}
|
|
125132
|
-
await updateIssueRuntime(workspaceDir, projectSlug, String(issueIdNum), runtimeUpdate).catch(() => {
|
|
125133
|
-
});
|
|
125134
|
-
}
|
|
125135
|
-
const diagnosticType = `diagnostic_${diagnostic.action}`;
|
|
125136
|
-
const fix2 = {
|
|
125137
|
-
issue: {
|
|
125138
|
-
type: diagnosticType,
|
|
125139
|
-
severity: diagnostic.action === "needs_human_review" ? "critical" : "warning",
|
|
125140
|
-
project: project.name,
|
|
125141
|
-
projectSlug,
|
|
125142
|
-
role,
|
|
125143
|
-
level,
|
|
125144
|
-
sessionKey,
|
|
125145
|
-
issueId: slot.issueId,
|
|
125146
|
-
slotIndex,
|
|
125147
|
-
message: `${role.toUpperCase()} ${level}[${slotIndex}] stall diagnosed: ${diagnostic.action} \u2014 ${diagnostic.evidence}`
|
|
125148
|
-
},
|
|
125149
|
-
fixed: false
|
|
125150
|
-
};
|
|
125151
|
-
if (autoFix) {
|
|
125152
|
-
switch (diagnostic.action) {
|
|
125153
|
-
case "transition_to_review": {
|
|
125154
|
-
const targetLabel = role === "tester" ? "Done" : role === "reviewer" ? "To Test" : "To Review";
|
|
125155
|
-
await revertLabel(fix2, expectedLabel, targetLabel);
|
|
125156
|
-
if (!fix2.labelRevertFailed) {
|
|
125157
|
-
await deactivateSlot();
|
|
125158
|
-
if (role === "tester" && issueIdNum) {
|
|
125159
|
-
await provider.closeIssue(issueIdNum).catch(() => {
|
|
125160
|
-
});
|
|
125161
|
-
}
|
|
125162
|
-
fix2.fixed = true;
|
|
125163
|
-
}
|
|
125164
|
-
break;
|
|
125165
|
-
}
|
|
125166
|
-
case "redispatch_same_level":
|
|
125167
|
-
case "nudge_open_pr":
|
|
125168
|
-
case "retry_infra":
|
|
125169
|
-
await revertLabel(fix2, expectedLabel, slotQueueLabel);
|
|
125170
|
-
if (!fix2.labelRevertFailed) {
|
|
125171
|
-
await deactivateSlot();
|
|
125172
|
-
fix2.fixed = true;
|
|
125173
|
-
}
|
|
125174
|
-
break;
|
|
125175
|
-
case "escalate_level":
|
|
125176
|
-
await revertLabel(fix2, expectedLabel, slotQueueLabel);
|
|
125177
|
-
if (!fix2.labelRevertFailed) {
|
|
125178
|
-
await deactivateSlot();
|
|
125179
|
-
fix2.fixed = true;
|
|
125180
|
-
}
|
|
125181
|
-
break;
|
|
125182
|
-
case "needs_human_review":
|
|
125183
|
-
await revertLabel(fix2, expectedLabel, "Refining");
|
|
125184
|
-
await deactivateSlot();
|
|
125185
|
-
fix2.fixed = true;
|
|
125186
|
-
break;
|
|
125187
|
-
case "log_infra":
|
|
125188
|
-
await revertLabel(fix2, expectedLabel, "Refining");
|
|
125189
|
-
await deactivateSlot();
|
|
125190
|
-
fix2.fixed = true;
|
|
125191
|
-
break;
|
|
125192
|
-
}
|
|
125193
|
-
}
|
|
125194
|
-
await log(workspaceDir, "stall_diagnostic", {
|
|
125195
|
-
project: project.name,
|
|
125196
|
-
projectSlug,
|
|
125197
|
-
role,
|
|
125198
|
-
level,
|
|
125199
|
-
sessionKey,
|
|
125200
|
-
issueId: slot.issueId,
|
|
125201
|
-
slotIndex,
|
|
125202
|
-
idleMinutes,
|
|
125203
|
-
diagnostic: diagnostic.action,
|
|
125204
|
-
reason: diagnostic.reason,
|
|
125205
|
-
evidence: diagnostic.evidence,
|
|
125206
|
-
fastPath: hasActionableEvidence,
|
|
125207
|
-
dispatchAttemptCount: (issueRuntime?.dispatchAttemptCount ?? 0) + 1
|
|
125208
|
-
}).catch(() => {
|
|
125209
|
-
});
|
|
125210
|
-
fixes.push(fix2);
|
|
125211
|
-
continue;
|
|
125212
|
-
}
|
|
125213
|
-
const fix = {
|
|
125214
|
-
issue: {
|
|
125215
|
-
type: "session_stalled",
|
|
125216
|
-
severity: "critical",
|
|
125217
|
-
project: project.name,
|
|
125218
|
-
projectSlug,
|
|
125219
|
-
role,
|
|
125220
|
-
level,
|
|
125221
|
-
sessionKey,
|
|
125222
|
-
issueId: slot.issueId,
|
|
125223
|
-
slotIndex,
|
|
125224
|
-
message: `${role.toUpperCase()} ${level}[${slotIndex}] session idle ${idleMinutes}m \u2014 no deliverables yet, sending nudge`
|
|
125225
|
-
},
|
|
125226
|
-
fixed: false
|
|
125227
|
-
};
|
|
125228
|
-
if (autoFix) {
|
|
125229
|
-
sendToAgent(sessionKey, NUDGE_MESSAGE, {
|
|
125230
|
-
agentId: opts.agentId,
|
|
125231
|
-
projectName: project.name,
|
|
125232
|
-
issueId: issueIdNum,
|
|
125233
|
-
role,
|
|
125234
|
-
level,
|
|
125235
|
-
slotIndex,
|
|
125236
|
-
workspaceDir,
|
|
125237
|
-
runCommand: opts.runCommand,
|
|
125238
|
-
runtime: opts.runtime
|
|
125239
|
-
});
|
|
125240
|
-
fix.nudgeSent = true;
|
|
125241
|
-
fix.fixed = true;
|
|
125242
|
-
await auditHealthFixApplied(workspaceDir, fix, {
|
|
125243
|
-
action: "nudge_session",
|
|
125244
|
-
idleMinutes,
|
|
125245
|
-
deliveryState
|
|
125246
|
-
});
|
|
125247
|
-
}
|
|
125248
|
-
await log(workspaceDir, "session_stalled", {
|
|
125249
|
-
project: project.name,
|
|
125250
|
-
projectSlug,
|
|
125251
|
-
role,
|
|
125252
|
-
level,
|
|
125253
|
-
sessionKey,
|
|
125254
|
-
issueId: slot.issueId,
|
|
125255
|
-
slotIndex,
|
|
125256
|
-
idleMinutes,
|
|
125257
|
-
deliveryState,
|
|
125258
|
-
action: "nudge"
|
|
125259
|
-
}).catch(() => {
|
|
125260
|
-
});
|
|
125261
|
-
fixes.push(fix);
|
|
125262
|
-
continue;
|
|
125263
|
-
}
|
|
125264
|
-
}
|
|
125265
124952
|
if (slot.active && slot.startTime && sessionKey && sessions && isSessionAlive(sessionKey, sessions)) {
|
|
125266
124953
|
const hours = (Date.now() - new Date(slot.startTime).getTime()) / 36e5;
|
|
125267
124954
|
if (hours > staleWorkerHours) {
|
|
@@ -125295,6 +124982,32 @@ async function checkWorkerHealth(opts) {
|
|
|
125295
124982
|
fixes.push(fix);
|
|
125296
124983
|
}
|
|
125297
124984
|
}
|
|
124985
|
+
if (slot.active && issueIdNum && issue2 && currentLabel === expectedLabel && autoFix) {
|
|
124986
|
+
try {
|
|
124987
|
+
const prStatus = await provider.getPrStatus(issueIdNum);
|
|
124988
|
+
if (prStatus.url && prStatus.state !== PrState.MERGED && prStatus.state !== PrState.CLOSED && prStatus.state !== PrState.CHANGES_REQUESTED && prStatus.state !== PrState.HAS_COMMENTS && prStatus.currentIssueMatch !== false) {
|
|
124989
|
+
const rule = getCompletionRule(workflow, role, "done");
|
|
124990
|
+
if (rule && rule.to !== expectedLabel) {
|
|
124991
|
+
await resilientLabelTransition(provider, issueIdNum, expectedLabel, rule.to);
|
|
124992
|
+
await deactivateSlot();
|
|
124993
|
+
await log(workspaceDir, "health_transition_to_review", {
|
|
124994
|
+
project: project.name,
|
|
124995
|
+
projectSlug,
|
|
124996
|
+
role,
|
|
124997
|
+
level,
|
|
124998
|
+
issueId: slot.issueId,
|
|
124999
|
+
sessionKey,
|
|
125000
|
+
slotIndex,
|
|
125001
|
+
fromLabel: expectedLabel,
|
|
125002
|
+
toLabel: rule.to,
|
|
125003
|
+
prUrl: prStatus.url
|
|
125004
|
+
}).catch(() => {
|
|
125005
|
+
});
|
|
125006
|
+
}
|
|
125007
|
+
}
|
|
125008
|
+
} catch {
|
|
125009
|
+
}
|
|
125010
|
+
}
|
|
125298
125011
|
if (!slot.active && issue2 && currentLabel === expectedLabel) {
|
|
125299
125012
|
const fix = {
|
|
125300
125013
|
issue: {
|
|
@@ -125574,8 +125287,7 @@ function createHealthTool(ctx) {
|
|
|
125574
125287
|
role,
|
|
125575
125288
|
sessions,
|
|
125576
125289
|
autoFix: fix,
|
|
125577
|
-
provider
|
|
125578
|
-
runCommand: ctx.runCommand
|
|
125290
|
+
provider
|
|
125579
125291
|
});
|
|
125580
125292
|
issues.push(...healthFixes.map((f3) => ({ ...f3, project: project.name, role })));
|
|
125581
125293
|
const orphanFixes = await scanOrphanedLabels({
|
|
@@ -131714,10 +131426,6 @@ async function performHealthPass(workspaceDir, projectSlug, project, sessions, p
|
|
|
131714
131426
|
autoFix: true,
|
|
131715
131427
|
provider,
|
|
131716
131428
|
staleWorkerHours,
|
|
131717
|
-
stallTimeoutMinutes,
|
|
131718
|
-
runCommand,
|
|
131719
|
-
runtime,
|
|
131720
|
-
agentId,
|
|
131721
131429
|
workflow: resolvedConfig?.workflow,
|
|
131722
131430
|
dispatchConfirmTimeoutMs: resolvedConfig?.timeouts?.dispatchConfirmTimeoutMs
|
|
131723
131431
|
});
|
|
@@ -138902,14 +138610,16 @@ function buildTopicDeepLink(chatId, topicId) {
|
|
|
138902
138610
|
function buildDmAck(projectName, topicLink, language = "pt") {
|
|
138903
138611
|
return BOOTSTRAP_MESSAGES.registered[language](projectName, topicLink);
|
|
138904
138612
|
}
|
|
138905
|
-
function buildTopicKickoff(projectName, idea) {
|
|
138906
|
-
|
|
138907
|
-
|
|
138908
|
-
|
|
138909
|
-
|
|
138910
|
-
|
|
138911
|
-
|
|
138912
|
-
|
|
138613
|
+
function buildTopicKickoff(projectName, idea, language = "pt") {
|
|
138614
|
+
const header = language === "en" ? `\u{1F9F1} Project automatically registered by Fabrica.
|
|
138615
|
+
Project: ${projectName}
|
|
138616
|
+
|
|
138617
|
+
Original request summary:` : `\u{1F9F1} Projeto registrado automaticamente pela Fabrica.
|
|
138618
|
+
Projeto: ${projectName}
|
|
138619
|
+
|
|
138620
|
+
Resumo do pedido inicial:`;
|
|
138621
|
+
return `${header}
|
|
138622
|
+
${idea}`;
|
|
138913
138623
|
}
|
|
138914
138624
|
async function sendTelegramText(ctx, target, message, opts) {
|
|
138915
138625
|
const sendOpts = {
|
|
@@ -139199,7 +138909,8 @@ Erro: ${result.error ?? "erro desconhecido"}`
|
|
|
139199
138909
|
accountId: telegramConfig.projectsForumAccountId ?? void 0
|
|
139200
138910
|
}
|
|
139201
138911
|
});
|
|
139202
|
-
|
|
138912
|
+
const sessionLang = currentSession?.language ?? "pt";
|
|
138913
|
+
await sendTelegramText(ctx, projectChannelId, buildTopicKickoff(resolvedProjectName, request.rawIdea, sessionLang), {
|
|
139203
138914
|
accountId: telegramConfig.projectsForumAccountId,
|
|
139204
138915
|
messageThreadId
|
|
139205
138916
|
});
|
|
@@ -139218,7 +138929,6 @@ Erro: ${result.error ?? "erro desconhecido"}`
|
|
|
139218
138929
|
logBootstrapWarning(ctx, `[telegram-bootstrap] immediate projectTick failed: ${error48 instanceof Error ? error48.message : String(error48)}`);
|
|
139219
138930
|
});
|
|
139220
138931
|
}
|
|
139221
|
-
const sessionLang = currentSession?.language ?? "pt";
|
|
139222
138932
|
await sendTelegramText(ctx, conversationId, buildDmAck(resolvedProjectName, buildTopicDeepLink(String(projectChannelId), messageThreadId), sessionLang));
|
|
139223
138933
|
await upsertTelegramBootstrapSession(workspaceDir, {
|
|
139224
138934
|
conversationId,
|
|
@@ -139738,6 +139448,36 @@ function registerGatewayLifecycleHook(api, ctx) {
|
|
|
139738
139448
|
|
|
139739
139449
|
// lib/dispatch/subagent-lifecycle-hook.ts
|
|
139740
139450
|
init_audit();
|
|
139451
|
+
|
|
139452
|
+
// lib/dispatch/reactive-dispatch-hook.ts
|
|
139453
|
+
var COMPLETION_TOOLS = /* @__PURE__ */ new Set(["work_finish", "review_submit"]);
|
|
139454
|
+
var spawnTimes = /* @__PURE__ */ new Map();
|
|
139455
|
+
function getSpawnTime(sessionKey) {
|
|
139456
|
+
return spawnTimes.get(sessionKey);
|
|
139457
|
+
}
|
|
139458
|
+
function clearSpawnTime(sessionKey) {
|
|
139459
|
+
spawnTimes.delete(sessionKey);
|
|
139460
|
+
}
|
|
139461
|
+
function registerReactiveDispatchHooks(api, ctx) {
|
|
139462
|
+
api.on("after_tool_call", async (event, _eventCtx) => {
|
|
139463
|
+
if (!COMPLETION_TOOLS.has(event.toolName)) return;
|
|
139464
|
+
ctx.runtime?.system.requestHeartbeatNow({ reason: "work_finish", coalesceMs: 2e3 });
|
|
139465
|
+
});
|
|
139466
|
+
api.on("agent_end", async (_event, eventCtx) => {
|
|
139467
|
+
const sessionKey = eventCtx.sessionKey;
|
|
139468
|
+
if (!sessionKey) return;
|
|
139469
|
+
const parsed = parseFabricaSessionKey(sessionKey);
|
|
139470
|
+
if (!parsed) return;
|
|
139471
|
+
ctx.runtime?.system.requestHeartbeatNow({ reason: "agent_end", coalesceMs: 2e3 });
|
|
139472
|
+
});
|
|
139473
|
+
api.on("subagent_spawned", async (event, _eventCtx) => {
|
|
139474
|
+
const sessionKey = event.childSessionKey;
|
|
139475
|
+
if (!sessionKey) return;
|
|
139476
|
+
spawnTimes.set(sessionKey, Date.now());
|
|
139477
|
+
});
|
|
139478
|
+
}
|
|
139479
|
+
|
|
139480
|
+
// lib/dispatch/subagent-lifecycle-hook.ts
|
|
139741
139481
|
function registerSubagentLifecycleHook(api, ctx) {
|
|
139742
139482
|
const workspaceDir = resolveWorkspaceDir(ctx.config);
|
|
139743
139483
|
if (!workspaceDir) return;
|
|
@@ -139747,11 +139487,15 @@ function registerSubagentLifecycleHook(api, ctx) {
|
|
|
139747
139487
|
const parsed = parseFabricaSessionKey(sessionKey);
|
|
139748
139488
|
if (!parsed) return;
|
|
139749
139489
|
const { projectName, role } = parsed;
|
|
139490
|
+
const spawnTime = getSpawnTime(sessionKey);
|
|
139491
|
+
clearSpawnTime(sessionKey);
|
|
139492
|
+
const durationMs = spawnTime != null ? Date.now() - spawnTime : void 0;
|
|
139750
139493
|
await log(workspaceDir, "subagent_ended", {
|
|
139751
139494
|
sessionKey,
|
|
139752
139495
|
project: projectName,
|
|
139753
139496
|
role,
|
|
139754
|
-
outcome: event.outcome ?? "unknown"
|
|
139497
|
+
outcome: event.outcome ?? "unknown",
|
|
139498
|
+
...durationMs != null ? { durationMs } : {}
|
|
139755
139499
|
}).catch(() => {
|
|
139756
139500
|
});
|
|
139757
139501
|
ctx.logger.info(
|
|
@@ -139815,6 +139559,23 @@ function registerModelResolveHook(api, ctx) {
|
|
|
139815
139559
|
});
|
|
139816
139560
|
}
|
|
139817
139561
|
|
|
139562
|
+
// lib/dispatch/worker-context-hook.ts
|
|
139563
|
+
var WORK_FINISH_CONTEXT = `## Task Completion
|
|
139564
|
+
|
|
139565
|
+
When you have finished your task, you MUST call the \`work_finish\` tool to signal completion.
|
|
139566
|
+
Do NOT rely on your session ending automatically \u2014 you must explicitly call \`work_finish\`.
|
|
139567
|
+
This is required for the pipeline to advance to the next stage.
|
|
139568
|
+
`;
|
|
139569
|
+
function registerWorkerContextHook(api, _ctx) {
|
|
139570
|
+
api.on("before_agent_start", async (_event, eventCtx) => {
|
|
139571
|
+
const sessionKey = eventCtx.sessionKey;
|
|
139572
|
+
if (!sessionKey) return;
|
|
139573
|
+
const parsed = parseFabricaSessionKey(sessionKey);
|
|
139574
|
+
if (!parsed) return;
|
|
139575
|
+
return { prependSystemContext: WORK_FINISH_CONTEXT };
|
|
139576
|
+
});
|
|
139577
|
+
}
|
|
139578
|
+
|
|
139818
139579
|
// index.ts
|
|
139819
139580
|
var plugin = {
|
|
139820
139581
|
id: "fabrica",
|
|
@@ -139955,8 +139716,10 @@ var plugin = {
|
|
|
139955
139716
|
registerGatewayLifecycleHook(api, ctx);
|
|
139956
139717
|
registerSubagentLifecycleHook(api, ctx);
|
|
139957
139718
|
registerModelResolveHook(api, ctx);
|
|
139719
|
+
registerWorkerContextHook(api, ctx);
|
|
139720
|
+
registerReactiveDispatchHooks(api, ctx);
|
|
139958
139721
|
ctx.logger.info(
|
|
139959
|
-
"Fabrica plugin registered (25 tools, 1 CLI command group, 1 service,
|
|
139722
|
+
"Fabrica plugin registered (25 tools, 1 CLI command group, 1 service, 9 hooks total: bootstrap, telegram-dm bootstrap, attachment, gateway lifecycle, subagent lifecycle, model-resolve, worker-context, reactive-dispatch, optional GitHub webhook route)"
|
|
139960
139723
|
);
|
|
139961
139724
|
}
|
|
139962
139725
|
};
|