ework-daemon 0.4.22 β†’ 0.4.23

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.22",
3
+ "version": "0.4.23",
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/gitea.ts CHANGED
@@ -63,6 +63,23 @@ export class GiteaClient {
63
63
  }, true);
64
64
  }
65
65
 
66
+ async updateIssueStatus(
67
+ owner: string,
68
+ repo: string,
69
+ issueNumber: number,
70
+ status: string,
71
+ detail?: string
72
+ ): Promise<void> {
73
+ try {
74
+ await this.request("POST", `/repos/${owner}/${repo}/issues/${issueNumber}/status`, {
75
+ status,
76
+ detail,
77
+ }, true);
78
+ } catch {
79
+ // Status callback is best-effort β€” don't block processing on API failure.
80
+ }
81
+ }
82
+
66
83
  async editComment(
67
84
  owner: string,
68
85
  repo: string,
package/src/opencode.ts CHANGED
@@ -579,6 +579,11 @@ export class Engine {
579
579
  return this.handleCommented(ref, scopeKey, issueData, event.comment!, tracker, event.model);
580
580
  case "issue_closed":
581
581
  return this.handleClosed(ref, scopeKey, tracker);
582
+ case "status_changed": {
583
+ const to = event.status?.to;
584
+ if (to === "halted") return this.handleHalted(ref, scopeKey, tracker);
585
+ return;
586
+ }
582
587
  }
583
588
  }
584
589
 
@@ -629,6 +634,7 @@ export class Engine {
629
634
  log.info(`engine: session "${session.name}" created for ${k}, workdir=${workdir}`);
630
635
 
631
636
  await tracker.createComment(ref, `[system] πŸ”„ **${session.name}** picked up this issue.\n> session: ${this.sessionRef(session)} | workdir: ${this.workdirLink(workdir)}`);
637
+ void tracker.updateStatus(ref, "processing");
632
638
 
633
639
  const instructions = tracker.getTrackerInstructions(ref);
634
640
  const prompt = this.buildInitialPrompt(
@@ -813,6 +819,36 @@ export class Engine {
813
819
  this.senders.delete(gcKey);
814
820
 
815
821
  log.info(`engine: issue closed, ${sessions.length} sessions paused for ${scopeKey}#${ref.issueId}`);
822
+ void tracker.updateStatus(ref, "completed");
823
+ }
824
+
825
+ private async handleHalted(
826
+ ref: TrackerRef,
827
+ scopeKey: string,
828
+ tracker: IssueTracker
829
+ ) {
830
+ const issue = await this.store.findIssue(ref.trackerType, scopeKey, ref.issueId);
831
+ if (!issue) return;
832
+ this.stopObserver(issue.id);
833
+ const sessions = await this.store.getSessionsForIssue(issue.id);
834
+ for (const session of sessions) {
835
+ const k = this.sessionKey(session, issue);
836
+ const proc = this.processes.get(k);
837
+ if (proc) {
838
+ this.stopping.add(k);
839
+ try { this.killProcessTree(proc.pid, "SIGTERM"); } catch { /* already dead */ }
840
+ }
841
+ this.clearRuntimeState(k);
842
+ const msgs = await this.store.getMessagesForSession(session.id);
843
+ for (const msg of msgs) {
844
+ if (msg.status === "pending" || msg.status === "running") {
845
+ await this.store.updateMessageStatus(msg.id, "interrupted", "halted by user");
846
+ }
847
+ }
848
+ await this.store.updateSession(session.id, { state: "idle", opencodePid: undefined });
849
+ }
850
+ log.info(`engine: issue halted, ${sessions.length} sessions killed for ${scopeKey}#${ref.issueId}`);
851
+ try { await tracker.createComment(ref, "[system] ⏸️ AI processing halted by user."); } catch { /* tracker unavailable */ }
816
852
  }
817
853
 
818
854
  private clearRuntimeState(k: string) {
@@ -65,6 +65,12 @@ export class GiteaTracker implements IssueTracker {
65
65
  );
66
66
  }
67
67
 
68
+ async updateStatus(ref: TrackerRef, status: string, detail?: string) {
69
+ await this.client.updateIssueStatus(
70
+ this.owner(ref), this.repo(ref), Number(ref.issueId), status, detail
71
+ );
72
+ }
73
+
68
74
  async setReaction(ref: TrackerRef, commentId: string, content: string, remove = false) {
69
75
  if (remove) {
70
76
  await this.client.removeCommentReaction(
@@ -189,6 +195,26 @@ export class GiteaTracker implements IssueTracker {
189
195
  };
190
196
  }
191
197
 
198
+ if (action === "status_changed") {
199
+ const status = payload.status as { from?: string; to?: string; detail?: string } | undefined;
200
+ return {
201
+ type: "status_changed" as const,
202
+ ref,
203
+ issue: {
204
+ title: issue.title as string,
205
+ body: (issue.body as string) ?? "",
206
+ state: (issue.state as string) ?? "open",
207
+ author: issueUser?.login ?? "",
208
+ },
209
+ status: {
210
+ from: status?.from ?? "",
211
+ to: status?.to ?? "",
212
+ detail: status?.detail,
213
+ },
214
+ sender,
215
+ };
216
+ }
217
+
192
218
  return null;
193
219
  }
194
220
 
@@ -13,7 +13,7 @@ export interface TrackerRef {
13
13
  }
14
14
 
15
15
  /** Parsed, tracker-agnostic webhook event */
16
- export type TrackerEventType = "issue_opened" | "comment_created" | "issue_closed";
16
+ export type TrackerEventType = "issue_opened" | "comment_created" | "issue_closed" | "status_changed";
17
17
 
18
18
  export interface TrackerEvent {
19
19
  type: TrackerEventType;
@@ -39,6 +39,7 @@ export interface TrackerEvent {
39
39
  // Webhook sender (who triggered the event). Forwarded to the daemon's
40
40
  // task context so the AI knows who it's responding to.
41
41
  sender?: string;
42
+ status?: { from: string; to: string; detail?: string };
42
43
  }
43
44
 
44
45
  /** Tracker-agnostic comment */
@@ -130,6 +131,7 @@ export interface IssueTracker {
130
131
  deleteComment(ref: TrackerRef, commentId: string): Promise<void>;
131
132
  listComments(ref: TrackerRef): Promise<TrackerComment[]>;
132
133
  closeIssue(ref: TrackerRef): Promise<void>;
134
+ updateStatus(ref: TrackerRef, status: string, detail?: string): Promise<void>;
133
135
 
134
136
  setReaction(ref: TrackerRef, commentId: string, content: string, remove?: boolean): Promise<void>;
135
137