ework-daemon 0.4.37 → 0.4.39

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ework-daemon",
3
- "version": "0.4.37",
3
+ "version": "0.4.39",
4
4
  "description": "Issue-driven AI development daemon. Spawns opencode subprocesses to resolve Gitea issues.",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
package/src/opencode.ts CHANGED
@@ -388,6 +388,25 @@ export class Engine {
388
388
  } catch { /* non-critical */ }
389
389
  }
390
390
 
391
+ private async webGateAllows(issue: Issue): Promise<{ allowed: boolean; reason: string }> {
392
+ const parts = issue.trackerScopeKey.split("/");
393
+ const owner = parts[0] ?? "";
394
+ const repo = parts.slice(1).join("/");
395
+ if (!owner || !repo) return { allowed: true, reason: "unparseable scope" };
396
+ const url = `${this.cfg.gitea.url}/api/v1/dispatch-state?owner=${encodeURIComponent(owner)}&repo=${encodeURIComponent(repo)}&number=${encodeURIComponent(issue.trackerIssueId)}`;
397
+ try {
398
+ const resp = await fetch(url, { signal: AbortSignal.timeout(5000), headers: { Authorization: `token ${this.cfg.gitea.token}` } });
399
+ if (!resp.ok) return { allowed: false, reason: `web returned ${resp.status}` };
400
+ const data = await resp.json() as { dispatchOff?: boolean; aiStatus?: string };
401
+ if (data.dispatchOff) return { allowed: false, reason: "dispatch off" };
402
+ if (data.aiStatus === "halted" || data.aiStatus === "dispatch_off") return { allowed: false, reason: `ai_status=${data.aiStatus}` };
403
+ return { allowed: true, reason: "ok" };
404
+ } catch (err) {
405
+ log.warn(`engine: web gate query failed for ${issue.trackerScopeKey}#${issue.trackerIssueId}: ${(err as Error).message} — fail-closed (skipping)`);
406
+ return { allowed: false, reason: `web unreachable: ${(err as Error).message}` };
407
+ }
408
+ }
409
+
391
410
  setMaxConcurrent(n: number): void {
392
411
  if (!Number.isFinite(n) || n < 1) return;
393
412
  this.maxConcurrent = Math.floor(n);
@@ -667,6 +686,11 @@ export class Engine {
667
686
  return;
668
687
  }
669
688
 
689
+ if (event.dispatch_off && event.type === "issue_opened") {
690
+ log.info(`engine: dispatch_off (global/project) — skipping issue_opened for ${ref.trackerType}:${scopeKey}#${ref.issueId}`);
691
+ return;
692
+ }
693
+
670
694
  if (event.type === "comment_created" && event.comment?.author) {
671
695
  const author = event.comment.author;
672
696
  const authorKind = event.comment.author_kind ?? "human";
@@ -1550,6 +1574,11 @@ export class Engine {
1550
1574
 
1551
1575
  for (const issue of ownedIssues) {
1552
1576
  try {
1577
+ const gate = await this.webGateAllows(issue);
1578
+ if (!gate.allowed) {
1579
+ log.info(`engine: observer — web gate blocked ${issue.trackerScopeKey}#${issue.trackerIssueId} (${gate.reason})`);
1580
+ continue;
1581
+ }
1553
1582
  await this.observeIssue(issue);
1554
1583
  } catch (err) {
1555
1584
  log.error(`engine: observer error for ${issue.trackerScopeKey}#${issue.trackerIssueId}:`, (err as Error).message);
@@ -1782,6 +1811,13 @@ export class Engine {
1782
1811
  const issue = await this.store.getIssue(session.issueId);
1783
1812
  if (!issue || issue.state === "closed") continue;
1784
1813
 
1814
+ const gate = await this.webGateAllows(issue);
1815
+ if (!gate.allowed) {
1816
+ log.info(`engine: recover — web gate blocked ${issue.trackerScopeKey}#${issue.trackerIssueId} (${gate.reason}), marking pending messages as skipped`);
1817
+ for (const m of msgs) await this.store.updateMessageStatus(m.id, "failed", `web gate: ${gate.reason}`);
1818
+ continue;
1819
+ }
1820
+
1785
1821
  const k = this.sessionKey(session, issue);
1786
1822
  if (this.running.has(k)) continue;
1787
1823
 
@@ -141,11 +141,13 @@ export class GiteaTracker implements IssueTracker {
141
141
 
142
142
  const issueUser = issue.user as Record<string, string>;
143
143
  const aiStatus = typeof issue.ai_status === "string" ? issue.ai_status : "";
144
+ const dispatchOff = payload.dispatch_off === true;
144
145
 
145
146
  if (action === "opened" || action === "reopened") {
146
147
  return {
147
148
  type: "issue_opened",
148
149
  ref,
150
+ dispatch_off: dispatchOff,
149
151
  issue: {
150
152
  title: issue.title as string,
151
153
  body: (issue.body as string) ?? "",
@@ -18,6 +18,7 @@ export type TrackerEventType = "issue_opened" | "comment_created" | "issue_close
18
18
  export interface TrackerEvent {
19
19
  type: TrackerEventType;
20
20
  ref: TrackerRef;
21
+ dispatch_off?: boolean;
21
22
  issue: {
22
23
  title: string;
23
24
  body: string;