@mestreyoda/fabrica 0.2.3 → 0.2.7
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/defaults/fabrica/prompts/reviewer.md +38 -11
- package/dist/index.js +175 -52
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
|
@@ -73,8 +73,7 @@ Do **not** treat the task envelope (`Repo:`, `Project:`, `Channel:`, branch hint
|
|
|
73
73
|
|
|
74
74
|
- Read the PR diff carefully
|
|
75
75
|
- Check the code against the review checklist
|
|
76
|
-
-
|
|
77
|
-
- Then call `work_finish`
|
|
76
|
+
- Submit your review using **one of the two methods below** (prefer `review_submit` if available)
|
|
78
77
|
|
|
79
78
|
## Conventions
|
|
80
79
|
|
|
@@ -92,22 +91,50 @@ If you discover unrelated bugs or needed improvements, call `task_create`:
|
|
|
92
91
|
|
|
93
92
|
## Completing Your Task
|
|
94
93
|
|
|
95
|
-
When you are done, submit
|
|
94
|
+
When you are done, submit your review using **Method A** if the tools are available, or **Method B** otherwise.
|
|
96
95
|
|
|
97
|
-
|
|
98
|
-
- **Reject review artifact:** `review_submit({ channelId: "<project slug from 'Project:' field in task message>", issueId: <issue number>, result: "reject", body: "<specific issues>" })`
|
|
99
|
-
- Capture the returned `artifactId` and `artifactType` from `review_submit`.
|
|
96
|
+
### Method A — Fabrica tools (preferred)
|
|
100
97
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
- **
|
|
104
|
-
-
|
|
105
|
-
|
|
98
|
+
1. Call `review_submit` to write the review artifact to the PR:
|
|
99
|
+
- **Approve:** `review_submit({ channelId: "<project slug>", issueId: <issue number>, result: "approve", body: "<what you checked>" })`
|
|
100
|
+
- **Reject:** `review_submit({ channelId: "<project slug>", issueId: <issue number>, result: "reject", body: "<specific issues>" })`
|
|
101
|
+
- Capture the returned `artifactId` and `artifactType`.
|
|
102
|
+
2. Then call `work_finish`:
|
|
103
|
+
- **Approve:** `work_finish({ role: "reviewer", result: "approve", channelId: "<project slug>", summary: "<what you checked>", reviewArtifactId: <artifactId>, reviewArtifactType: "<artifactType>" })`
|
|
104
|
+
- **Reject:** `work_finish({ role: "reviewer", result: "reject", channelId: "<project slug>", summary: "<specific issues>", reviewArtifactId: <artifactId>, reviewArtifactType: "<artifactType>" })`
|
|
105
|
+
- **Blocked:** `work_finish({ role: "reviewer", result: "blocked", channelId: "<project slug>", summary: "<what you need>" })`
|
|
106
106
|
|
|
107
107
|
> **IMPORTANT:** The `channelId` parameter accepts the project slug (e.g., "gestao-notas").
|
|
108
108
|
> Extract it from the "Project: <name>" line in your task message. Do NOT use the numeric
|
|
109
109
|
> channel ID — use the project slug to avoid resolution errors when channels are shared.
|
|
110
110
|
|
|
111
|
+
### Method B — GitHub CLI fallback (use only if `review_submit` / `work_finish` are unavailable)
|
|
112
|
+
|
|
113
|
+
Extract from your task message:
|
|
114
|
+
- `OWNER/REPO` from the `Repo:` line
|
|
115
|
+
- `PR_NUMBER` from the PR URL in the diff header or the `Branch:` line
|
|
116
|
+
- `ISSUE_NUMBER` from the `Issue:` field
|
|
117
|
+
|
|
118
|
+
**Approve:**
|
|
119
|
+
```bash
|
|
120
|
+
gh pr review PR_NUMBER --repo OWNER/REPO --approve -b "$(cat <<'EOF'
|
|
121
|
+
<your full review body here>
|
|
122
|
+
EOF
|
|
123
|
+
)"
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**Reject (request changes):**
|
|
127
|
+
```bash
|
|
128
|
+
gh pr review PR_NUMBER --repo OWNER/REPO --request-changes -b "$(cat <<'EOF'
|
|
129
|
+
<specific issues and how to fix them>
|
|
130
|
+
EOF
|
|
131
|
+
)"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
After submitting via `gh pr review`, the Fabrica heartbeat will detect the PR review state and advance the pipeline automatically. **Do NOT manually edit issue labels.**
|
|
135
|
+
|
|
136
|
+
**Never call `task_comment` for review findings.** Your review must be posted on the PR itself.
|
|
137
|
+
|
|
111
138
|
## Tools You Should NOT Use
|
|
112
139
|
|
|
113
140
|
These are orchestrator-only tools. Do not call them:
|
package/dist/index.js
CHANGED
|
@@ -110821,7 +110821,7 @@ var init_registry = __esm({
|
|
|
110821
110821
|
});
|
|
110822
110822
|
|
|
110823
110823
|
// lib/config/merge.ts
|
|
110824
|
-
function mergeConfig(base, overlay) {
|
|
110824
|
+
function mergeConfig(base, overlay, traceOpts) {
|
|
110825
110825
|
const merged = {};
|
|
110826
110826
|
if (base.roles || overlay.roles) {
|
|
110827
110827
|
merged.roles = { ...base.roles };
|
|
@@ -110872,6 +110872,44 @@ function mergeConfig(base, overlay) {
|
|
|
110872
110872
|
} : void 0
|
|
110873
110873
|
};
|
|
110874
110874
|
}
|
|
110875
|
+
if (traceOpts) {
|
|
110876
|
+
const { baseLabel, overlayLabel } = traceOpts;
|
|
110877
|
+
const trace2 = {};
|
|
110878
|
+
if (merged.workflow) {
|
|
110879
|
+
for (const key of ["initial", "reviewPolicy", "testPolicy", "roleExecution", "maxWorkersPerLevel"]) {
|
|
110880
|
+
if (merged.workflow[key] !== void 0) {
|
|
110881
|
+
const fromOverlay = overlay.workflow?.[key] !== void 0;
|
|
110882
|
+
trace2[`workflow.${key}`] = fromOverlay ? overlayLabel : baseLabel;
|
|
110883
|
+
}
|
|
110884
|
+
}
|
|
110885
|
+
}
|
|
110886
|
+
if (merged.timeouts) {
|
|
110887
|
+
for (const [key, value] of Object.entries(merged.timeouts)) {
|
|
110888
|
+
if (value !== void 0) {
|
|
110889
|
+
const fromOverlay = overlay.timeouts?.[key] !== void 0;
|
|
110890
|
+
trace2[`timeouts.${key}`] = fromOverlay ? overlayLabel : baseLabel;
|
|
110891
|
+
}
|
|
110892
|
+
}
|
|
110893
|
+
}
|
|
110894
|
+
if (merged.roles) {
|
|
110895
|
+
for (const [roleId, roleValue] of Object.entries(merged.roles)) {
|
|
110896
|
+
if (roleValue === false) {
|
|
110897
|
+
trace2[`roles.${roleId}`] = overlay.roles?.[roleId] === false ? overlayLabel : baseLabel;
|
|
110898
|
+
continue;
|
|
110899
|
+
}
|
|
110900
|
+
if (typeof roleValue === "object") {
|
|
110901
|
+
for (const key of ["defaultLevel", "levels", "completionResults"]) {
|
|
110902
|
+
if (roleValue[key] !== void 0) {
|
|
110903
|
+
const overlayRole = overlay.roles?.[roleId];
|
|
110904
|
+
const fromOverlay = typeof overlayRole === "object" && overlayRole?.[key] !== void 0;
|
|
110905
|
+
trace2[`roles.${roleId}.${key}`] = fromOverlay ? overlayLabel : baseLabel;
|
|
110906
|
+
}
|
|
110907
|
+
}
|
|
110908
|
+
}
|
|
110909
|
+
}
|
|
110910
|
+
}
|
|
110911
|
+
return Object.assign(merged, { _trace: trace2 });
|
|
110912
|
+
}
|
|
110875
110913
|
return merged;
|
|
110876
110914
|
}
|
|
110877
110915
|
function mergeWorkflowStates(base, overlay) {
|
|
@@ -110917,42 +110955,10 @@ function mergeRoleOverride(base, overlay) {
|
|
|
110917
110955
|
};
|
|
110918
110956
|
}
|
|
110919
110957
|
function mergeConfigWithTrace(base, overlay, baseLabel, overlayLabel) {
|
|
110920
|
-
const
|
|
110921
|
-
const trace2 = {};
|
|
110922
|
-
|
|
110923
|
-
|
|
110924
|
-
if (merged.workflow[key] !== void 0) {
|
|
110925
|
-
const fromOverlay = overlay.workflow?.[key] !== void 0;
|
|
110926
|
-
trace2[`workflow.${key}`] = fromOverlay ? overlayLabel : baseLabel;
|
|
110927
|
-
}
|
|
110928
|
-
}
|
|
110929
|
-
}
|
|
110930
|
-
if (merged.timeouts) {
|
|
110931
|
-
for (const [key, value] of Object.entries(merged.timeouts)) {
|
|
110932
|
-
if (value !== void 0) {
|
|
110933
|
-
const fromOverlay = overlay.timeouts?.[key] !== void 0;
|
|
110934
|
-
trace2[`timeouts.${key}`] = fromOverlay ? overlayLabel : baseLabel;
|
|
110935
|
-
}
|
|
110936
|
-
}
|
|
110937
|
-
}
|
|
110938
|
-
if (merged.roles) {
|
|
110939
|
-
for (const [roleId, roleValue] of Object.entries(merged.roles)) {
|
|
110940
|
-
if (roleValue === false) {
|
|
110941
|
-
trace2[`roles.${roleId}`] = overlay.roles?.[roleId] === false ? overlayLabel : baseLabel;
|
|
110942
|
-
continue;
|
|
110943
|
-
}
|
|
110944
|
-
if (typeof roleValue === "object") {
|
|
110945
|
-
for (const key of ["defaultLevel", "levels", "completionResults"]) {
|
|
110946
|
-
if (roleValue[key] !== void 0) {
|
|
110947
|
-
const overlayRole = overlay.roles?.[roleId];
|
|
110948
|
-
const fromOverlay = typeof overlayRole === "object" && overlayRole?.[key] !== void 0;
|
|
110949
|
-
trace2[`roles.${roleId}.${key}`] = fromOverlay ? overlayLabel : baseLabel;
|
|
110950
|
-
}
|
|
110951
|
-
}
|
|
110952
|
-
}
|
|
110953
|
-
}
|
|
110954
|
-
}
|
|
110955
|
-
return { merged, trace: trace2 };
|
|
110958
|
+
const result = mergeConfig(base, overlay, { baseLabel, overlayLabel });
|
|
110959
|
+
const trace2 = result._trace ?? {};
|
|
110960
|
+
delete result._trace;
|
|
110961
|
+
return { merged: result, trace: trace2 };
|
|
110956
110962
|
}
|
|
110957
110963
|
var init_merge = __esm({
|
|
110958
110964
|
"lib/config/merge.ts"() {
|
|
@@ -111324,8 +111330,8 @@ import fsSync from "node:fs";
|
|
|
111324
111330
|
import path5 from "node:path";
|
|
111325
111331
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
111326
111332
|
function getCurrentVersion() {
|
|
111327
|
-
if ("0.2.
|
|
111328
|
-
return "0.2.
|
|
111333
|
+
if ("0.2.7") {
|
|
111334
|
+
return "0.2.7";
|
|
111329
111335
|
}
|
|
111330
111336
|
try {
|
|
111331
111337
|
const pkgPath = path5.join(THIS_DIR, "..", "..", "package.json");
|
|
@@ -117132,6 +117138,8 @@ var GitHubRateLimitError = class extends Error {
|
|
|
117132
117138
|
this.name = "GitHubRateLimitError";
|
|
117133
117139
|
}
|
|
117134
117140
|
};
|
|
117141
|
+
var MAX_RATE_LIMIT_RETRIES = 2;
|
|
117142
|
+
var JITTER_MAX_MS = 5e3;
|
|
117135
117143
|
var MAX_ENTRIES = 50;
|
|
117136
117144
|
var policyCache = /* @__PURE__ */ new Map();
|
|
117137
117145
|
var accessOrder = [];
|
|
@@ -117167,9 +117175,27 @@ function getProviderPolicy(providerKey) {
|
|
|
117167
117175
|
accessOrder.push(providerKey);
|
|
117168
117176
|
return policy;
|
|
117169
117177
|
}
|
|
117170
|
-
function withResilience(fn, providerKey) {
|
|
117178
|
+
async function withResilience(fn, providerKey, opts) {
|
|
117171
117179
|
const policy = providerKey ? getProviderPolicy(providerKey) : getProviderPolicy("__global__");
|
|
117172
|
-
|
|
117180
|
+
const jitterMax = opts?.jitterMaxMs ?? JITTER_MAX_MS;
|
|
117181
|
+
const withRateLimitRetry = async () => {
|
|
117182
|
+
let lastError;
|
|
117183
|
+
for (let attempt = 0; attempt <= MAX_RATE_LIMIT_RETRIES; attempt++) {
|
|
117184
|
+
try {
|
|
117185
|
+
return await fn();
|
|
117186
|
+
} catch (err) {
|
|
117187
|
+
if (err instanceof GitHubRateLimitError && attempt < MAX_RATE_LIMIT_RETRIES) {
|
|
117188
|
+
lastError = err;
|
|
117189
|
+
const jitter = jitterMax > 0 ? Math.floor(Math.random() * jitterMax) : 0;
|
|
117190
|
+
await new Promise((resolve3) => setTimeout(resolve3, err.retryAfterMs + jitter));
|
|
117191
|
+
continue;
|
|
117192
|
+
}
|
|
117193
|
+
throw err;
|
|
117194
|
+
}
|
|
117195
|
+
}
|
|
117196
|
+
throw lastError;
|
|
117197
|
+
};
|
|
117198
|
+
return policy.execute(withRateLimitRetry);
|
|
117173
117199
|
}
|
|
117174
117200
|
var providerPolicy = getProviderPolicy("__legacy_global__");
|
|
117175
117201
|
|
|
@@ -118078,7 +118104,9 @@ var GitHubProvider = class {
|
|
|
118078
118104
|
if (result.code != null && result.code !== 0) {
|
|
118079
118105
|
const errText = result.stderr?.trim() ?? "";
|
|
118080
118106
|
if (errText.includes("rate limit") || errText.includes("429")) {
|
|
118081
|
-
|
|
118107
|
+
const retryMatch = errText.match(/retry after (\d+)/i);
|
|
118108
|
+
const retryMs = retryMatch ? parseInt(retryMatch[1], 10) * 1e3 : 6e4;
|
|
118109
|
+
throw new GitHubRateLimitError(retryMs);
|
|
118082
118110
|
}
|
|
118083
118111
|
throw new Error(errText || `gh api ${method} ${endpoint} failed with exit code ${result.code}`);
|
|
118084
118112
|
}
|
|
@@ -122967,7 +122995,8 @@ ${roleInstructions}`;
|
|
|
122967
122995
|
return effortPrefix ?? roleInstructions ?? "";
|
|
122968
122996
|
}
|
|
122969
122997
|
function sendToAgent(sessionKey, taskMessage, opts) {
|
|
122970
|
-
const
|
|
122998
|
+
const epoch = opts.dispatchEpoch ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
122999
|
+
const idempotencyKey = `fabrica-${opts.projectName}-${opts.issueId}-${opts.role}-${opts.level ?? "unknown"}-${opts.slotIndex ?? 0}-${opts.fromLabel ?? "unknown"}-${sessionKey}-${epoch}`;
|
|
122971
123000
|
if (opts.runtime?.subagent?.run) {
|
|
122972
123001
|
opts.runtime.subagent.run({
|
|
122973
123002
|
sessionKey,
|
|
@@ -123388,6 +123417,7 @@ async function dispatchTask(opts) {
|
|
|
123388
123417
|
error: err.message ?? String(err)
|
|
123389
123418
|
}).catch((auditErr) => console.error("[fabrica] silent-catch:", auditErr.message));
|
|
123390
123419
|
});
|
|
123420
|
+
const dispatchEpoch = (/* @__PURE__ */ new Date()).toISOString();
|
|
123391
123421
|
sendToAgent(sessionKey, taskMessage, {
|
|
123392
123422
|
agentId,
|
|
123393
123423
|
projectName: project.name,
|
|
@@ -123405,7 +123435,8 @@ async function dispatchTask(opts) {
|
|
|
123405
123435
|
roleInstructions.trim() || void 0
|
|
123406
123436
|
) || void 0,
|
|
123407
123437
|
runCommand: rc,
|
|
123408
|
-
runtime
|
|
123438
|
+
runtime,
|
|
123439
|
+
dispatchEpoch
|
|
123409
123440
|
});
|
|
123410
123441
|
await recordIssueLifecycle({
|
|
123411
123442
|
workspaceDir,
|
|
@@ -130457,7 +130488,10 @@ async function cleanupExpired(workspaceDir, ttlMs = DEFAULT_TTL_MS2) {
|
|
|
130457
130488
|
const cutoff = Date.now() - ttlMs;
|
|
130458
130489
|
const kept = entries.filter((e2) => e2.ts >= cutoff);
|
|
130459
130490
|
if (kept.length < entries.length) {
|
|
130460
|
-
|
|
130491
|
+
const content = kept.map((e2) => JSON.stringify(e2)).join("\n") + (kept.length > 0 ? "\n" : "");
|
|
130492
|
+
const tmpPath = filePath + ".tmp";
|
|
130493
|
+
await fs28.writeFile(tmpPath, content, "utf-8");
|
|
130494
|
+
await fs28.rename(tmpPath, filePath);
|
|
130461
130495
|
}
|
|
130462
130496
|
}
|
|
130463
130497
|
|
|
@@ -130757,7 +130791,7 @@ async function reviewPass(opts) {
|
|
|
130757
130791
|
const issues = await provider.listIssuesByLabel(state.label);
|
|
130758
130792
|
for (const issue2 of issues) {
|
|
130759
130793
|
const routing = detectStepRouting(issue2.labels, "review");
|
|
130760
|
-
if (routing !== "human") continue;
|
|
130794
|
+
if (routing !== "human" && routing !== "agent") continue;
|
|
130761
130795
|
const isManaged = await provider.issueHasReaction(issue2.iid, "eyes");
|
|
130762
130796
|
if (!isManaged) continue;
|
|
130763
130797
|
const status = await provider.getPrStatus(issue2.iid);
|
|
@@ -132206,23 +132240,46 @@ async function checkProjectActive(workspaceDir, slug) {
|
|
|
132206
132240
|
);
|
|
132207
132241
|
}
|
|
132208
132242
|
|
|
132243
|
+
// lib/services/heartbeat/wake-bridge.ts
|
|
132244
|
+
var _wakeCallback = null;
|
|
132245
|
+
function setPluginWakeHandler(cb) {
|
|
132246
|
+
_wakeCallback = cb;
|
|
132247
|
+
}
|
|
132248
|
+
async function wakeHeartbeat(reason) {
|
|
132249
|
+
await _wakeCallback?.(reason);
|
|
132250
|
+
}
|
|
132251
|
+
|
|
132209
132252
|
// lib/services/heartbeat/index.ts
|
|
132210
132253
|
function registerHeartbeatService(api, pluginCtx) {
|
|
132211
132254
|
let sharedIntervalId = null;
|
|
132212
|
-
let
|
|
132255
|
+
let _pendingWake = false;
|
|
132213
132256
|
api.registerService({
|
|
132214
132257
|
id: "fabrica-heartbeat",
|
|
132215
132258
|
start: async (svcCtx) => {
|
|
132216
132259
|
const { intervalSeconds } = resolveHeartbeatConfig(pluginCtx.pluginConfig);
|
|
132217
132260
|
const intervalMs = intervalSeconds * 1e3;
|
|
132218
|
-
setTimeout(() => runHeartbeatTick(pluginCtx, svcCtx.logger, "
|
|
132261
|
+
setTimeout(() => runHeartbeatTick(pluginCtx, svcCtx.logger, "full"), 2e3);
|
|
132219
132262
|
sharedIntervalId = setInterval(() => {
|
|
132220
|
-
|
|
132221
|
-
|
|
132222
|
-
|
|
132263
|
+
runHeartbeatTick(pluginCtx, svcCtx.logger, "full").finally(() => {
|
|
132264
|
+
if (_pendingWake) {
|
|
132265
|
+
_pendingWake = false;
|
|
132266
|
+
svcCtx.logger.info("heartbeat_wake: running deferred full tick");
|
|
132267
|
+
runHeartbeatTick(pluginCtx, svcCtx.logger, "full");
|
|
132268
|
+
}
|
|
132269
|
+
});
|
|
132223
132270
|
}, intervalMs);
|
|
132271
|
+
setPluginWakeHandler(async (reason) => {
|
|
132272
|
+
if (_anyTickRunning) {
|
|
132273
|
+
_pendingWake = true;
|
|
132274
|
+
svcCtx.logger.info(`heartbeat_wake: deferred (tick-in-progress), reason=${reason}`);
|
|
132275
|
+
return;
|
|
132276
|
+
}
|
|
132277
|
+
svcCtx.logger.info(`heartbeat_wake: running full tick, reason=${reason}`);
|
|
132278
|
+
await runHeartbeatTick(pluginCtx, svcCtx.logger, "full");
|
|
132279
|
+
});
|
|
132224
132280
|
},
|
|
132225
132281
|
stop: async (svcCtx) => {
|
|
132282
|
+
setPluginWakeHandler(null);
|
|
132226
132283
|
if (sharedIntervalId) {
|
|
132227
132284
|
clearInterval(sharedIntervalId);
|
|
132228
132285
|
sharedIntervalId = null;
|
|
@@ -139108,7 +139165,7 @@ function registerTelegramBootstrapHook(api, ctx) {
|
|
|
139108
139165
|
ctx.logger.info(`[telegram-bootstrap] stale received session (expired) \u2014 restarting pipeline for conversation ${conversationId}`);
|
|
139109
139166
|
}
|
|
139110
139167
|
}
|
|
139111
|
-
if (!parsed.projectName && ctx.runtime?.subagent?.run) {
|
|
139168
|
+
if (!parsed.projectName && ctx.runtime?.subagent?.run != null) {
|
|
139112
139169
|
await upsertTelegramBootstrapSession(workspaceDir, {
|
|
139113
139170
|
conversationId,
|
|
139114
139171
|
rawIdea: parsed.rawIdea,
|
|
@@ -139484,6 +139541,8 @@ function registerReactiveDispatchHooks(api, ctx) {
|
|
|
139484
139541
|
api.on("after_tool_call", async (event, _eventCtx) => {
|
|
139485
139542
|
if (!COMPLETION_TOOLS.has(event.toolName)) return;
|
|
139486
139543
|
ctx.runtime?.system.requestHeartbeatNow({ reason: "work_finish", coalesceMs: 2e3 });
|
|
139544
|
+
wakeHeartbeat("work_finish").catch(() => {
|
|
139545
|
+
});
|
|
139487
139546
|
});
|
|
139488
139547
|
api.on("agent_end", async (_event, eventCtx) => {
|
|
139489
139548
|
const sessionKey = eventCtx.sessionKey;
|
|
@@ -139491,6 +139550,8 @@ function registerReactiveDispatchHooks(api, ctx) {
|
|
|
139491
139550
|
const parsed = parseFabricaSessionKey(sessionKey);
|
|
139492
139551
|
if (!parsed) return;
|
|
139493
139552
|
ctx.runtime?.system.requestHeartbeatNow({ reason: "agent_end", coalesceMs: 2e3 });
|
|
139553
|
+
wakeHeartbeat("agent_end").catch(() => {
|
|
139554
|
+
});
|
|
139494
139555
|
});
|
|
139495
139556
|
api.on("subagent_spawned", async (event, _eventCtx) => {
|
|
139496
139557
|
const sessionKey = event.childSessionKey;
|
|
@@ -139500,6 +139561,7 @@ function registerReactiveDispatchHooks(api, ctx) {
|
|
|
139500
139561
|
}
|
|
139501
139562
|
|
|
139502
139563
|
// lib/dispatch/subagent-lifecycle-hook.ts
|
|
139564
|
+
init_workflow();
|
|
139503
139565
|
function registerSubagentLifecycleHook(api, ctx) {
|
|
139504
139566
|
const workspaceDir = resolveWorkspaceDir(ctx.config);
|
|
139505
139567
|
if (!workspaceDir) return;
|
|
@@ -139523,6 +139585,67 @@ function registerSubagentLifecycleHook(api, ctx) {
|
|
|
139523
139585
|
ctx.logger.info(
|
|
139524
139586
|
`subagent_ended: worker ${role} in "${projectName}" ended with outcome=${event.outcome ?? "unknown"} (session=${sessionKey})`
|
|
139525
139587
|
);
|
|
139588
|
+
try {
|
|
139589
|
+
const projects = await readProjects(workspaceDir);
|
|
139590
|
+
const projectEntry = Object.entries(projects.projects).find(
|
|
139591
|
+
([, p]) => p.name === projectName
|
|
139592
|
+
);
|
|
139593
|
+
if (!projectEntry) return;
|
|
139594
|
+
const [projectSlug, project] = projectEntry;
|
|
139595
|
+
const roleWorker = project.workers[role];
|
|
139596
|
+
if (!roleWorker) return;
|
|
139597
|
+
let foundLevel;
|
|
139598
|
+
let foundSlotIndex;
|
|
139599
|
+
let foundSlot;
|
|
139600
|
+
for (const [level, slots] of Object.entries(roleWorker.levels)) {
|
|
139601
|
+
for (let i2 = 0; i2 < slots.length; i2++) {
|
|
139602
|
+
if (slots[i2].sessionKey === sessionKey && slots[i2].active) {
|
|
139603
|
+
foundLevel = level;
|
|
139604
|
+
foundSlotIndex = i2;
|
|
139605
|
+
foundSlot = slots[i2];
|
|
139606
|
+
break;
|
|
139607
|
+
}
|
|
139608
|
+
}
|
|
139609
|
+
if (foundSlot) break;
|
|
139610
|
+
}
|
|
139611
|
+
if (!foundSlot || foundLevel == null || foundSlotIndex == null) return;
|
|
139612
|
+
const issueId = foundSlot.issueId;
|
|
139613
|
+
await deactivateWorker(workspaceDir, projectSlug, role, {
|
|
139614
|
+
level: foundLevel,
|
|
139615
|
+
slotIndex: foundSlotIndex
|
|
139616
|
+
});
|
|
139617
|
+
if (issueId) {
|
|
139618
|
+
try {
|
|
139619
|
+
const config2 = await loadConfig(workspaceDir, projectSlug);
|
|
139620
|
+
const activeLabel = getActiveLabel(config2.workflow, role);
|
|
139621
|
+
const revertLabel = getRevertLabel(config2.workflow, role);
|
|
139622
|
+
const { provider } = await createProvider({
|
|
139623
|
+
repo: project.repo,
|
|
139624
|
+
provider: project.provider,
|
|
139625
|
+
runCommand: ctx.runCommand
|
|
139626
|
+
});
|
|
139627
|
+
const issue2 = await provider.getIssue(Number(issueId));
|
|
139628
|
+
if (issue2.labels.includes(activeLabel)) {
|
|
139629
|
+
await provider.transitionLabel(Number(issueId), activeLabel, revertLabel);
|
|
139630
|
+
}
|
|
139631
|
+
} catch {
|
|
139632
|
+
}
|
|
139633
|
+
}
|
|
139634
|
+
await log(workspaceDir, "subagent_ended_slot_cleanup", {
|
|
139635
|
+
sessionKey,
|
|
139636
|
+
project: projectName,
|
|
139637
|
+
role,
|
|
139638
|
+
level: foundLevel,
|
|
139639
|
+
slotIndex: foundSlotIndex,
|
|
139640
|
+
issueId,
|
|
139641
|
+
outcome: event.outcome ?? "unknown"
|
|
139642
|
+
}).catch(() => {
|
|
139643
|
+
});
|
|
139644
|
+
wakeHeartbeat("subagent_ended").catch(() => {
|
|
139645
|
+
});
|
|
139646
|
+
} catch (err) {
|
|
139647
|
+
ctx.logger.warn(`subagent_ended_slot_cleanup failed: ${err.message}`);
|
|
139648
|
+
}
|
|
139526
139649
|
});
|
|
139527
139650
|
}
|
|
139528
139651
|
|