@m8i-51/shoal 0.1.11 → 0.1.12

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.
@@ -2,6 +2,7 @@ import type { IssueTracker, OpenIssue, ClosedIssue } from "./types";
2
2
 
3
3
  export class AsanaTracker implements IssueTracker {
4
4
  readonly name = "asana";
5
+ readonly isEmpty = false;
5
6
  private token: string;
6
7
  private projectId: string;
7
8
 
@@ -30,15 +31,19 @@ export class AsanaTracker implements IssueTracker {
30
31
  },
31
32
  }),
32
33
  });
34
+ if (!res.ok) {
35
+ const msg = await res.text().catch(() => "");
36
+ console.error(`[asana] failed to create task (${res.status}): ${msg.slice(0, 200)}`);
37
+ return null;
38
+ }
33
39
  const data = await res.json() as {
34
40
  data?: { gid: string; permalink_url?: string };
35
- errors?: { message: string }[];
36
41
  };
37
- if (!res.ok) {
38
- console.error(`[asana] failed to create task (${res.status}): ${JSON.stringify(data.errors)}`);
42
+ if (!data.data?.gid) {
43
+ console.error("[asana] create task response missing gid");
39
44
  return null;
40
45
  }
41
- const url = data.data?.permalink_url ?? `https://app.asana.com/0/${this.projectId}/${data.data?.gid}`;
46
+ const url = data.data.permalink_url ?? `https://app.asana.com/0/${this.projectId}/${data.data.gid}`;
42
47
  console.log(`[asana] task created: ${url}`);
43
48
  return url;
44
49
  }
@@ -2,6 +2,7 @@ import type { IssueTracker, OpenIssue, ClosedIssue } from "./types";
2
2
 
3
3
  export class BacklogTracker implements IssueTracker {
4
4
  readonly name = "backlog";
5
+ readonly isEmpty = false;
5
6
  private baseUrl: string;
6
7
  private apiKey: string;
7
8
  private projectId: number;
@@ -30,9 +31,14 @@ export class BacklogTracker implements IssueTracker {
30
31
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
31
32
  body: form,
32
33
  });
33
- const data = await res.json() as { issueKey?: string; message?: string };
34
34
  if (!res.ok) {
35
- console.error(`[backlog] failed to create issue (${res.status}): ${data.message}`);
35
+ const msg = await res.text().catch(() => "");
36
+ console.error(`[backlog] failed to create issue (${res.status}): ${msg.slice(0, 200)}`);
37
+ return null;
38
+ }
39
+ const data = await res.json() as { issueKey?: string };
40
+ if (!data.issueKey) {
41
+ console.error("[backlog] create issue response missing issueKey");
36
42
  return null;
37
43
  }
38
44
  const url = `${this.baseUrl}/view/${data.issueKey}`;
@@ -3,6 +3,7 @@ import { postGitHubIssue, fetchOpenIssues as ghFetchOpen, fetchClosedIssues as g
3
3
 
4
4
  export class GitHubTracker implements IssueTracker {
5
5
  readonly name = "github";
6
+ readonly isEmpty = false;
6
7
  private opts: { token: string; repo: string };
7
8
 
8
9
  constructor(token: string, repo: string) {
@@ -21,18 +21,28 @@ export class AggregatedTracker implements IssueTracker {
21
21
 
22
22
  async createIssue(title: string, body: string, labels: string[]): Promise<string | null> {
23
23
  if (this.trackers.length === 0) return null;
24
- const urls = await Promise.all(this.trackers.map((t) => t.createIssue(title, body, labels)));
25
- return urls.find((u) => u !== null) ?? null;
24
+ const results = await Promise.allSettled(this.trackers.map((t) => t.createIssue(title, body, labels)));
25
+ for (const r of results) {
26
+ if (r.status === "rejected") console.error("[trackers] createIssue error:", r.reason);
27
+ }
28
+ const urls = results.filter((r): r is PromiseFulfilledResult<string | null> => r.status === "fulfilled");
29
+ return urls.map((r) => r.value).find((u) => u !== null) ?? null;
26
30
  }
27
31
 
28
32
  async fetchOpenIssues(): Promise<OpenIssue[]> {
29
- const results = await Promise.all(this.trackers.map((t) => t.fetchOpenIssues()));
30
- return results.flat();
33
+ const results = await Promise.allSettled(this.trackers.map((t) => t.fetchOpenIssues()));
34
+ return results.flatMap((r) => {
35
+ if (r.status === "rejected") { console.error("[trackers] fetchOpenIssues error:", r.reason); return []; }
36
+ return r.value;
37
+ });
31
38
  }
32
39
 
33
40
  async fetchClosedIssues(): Promise<ClosedIssue[]> {
34
- const results = await Promise.all(this.trackers.map((t) => t.fetchClosedIssues()));
35
- return results.flat();
41
+ const results = await Promise.allSettled(this.trackers.map((t) => t.fetchClosedIssues()));
42
+ return results.flatMap((r) => {
43
+ if (r.status === "rejected") { console.error("[trackers] fetchClosedIssues error:", r.reason); return []; }
44
+ return r.value;
45
+ });
36
46
  }
37
47
  }
38
48
 
@@ -87,8 +97,8 @@ export function buildTrackers(): AggregatedTracker {
87
97
  case "backlog": {
88
98
  const space = process.env.BACKLOG_SPACE ?? "";
89
99
  const apiKey = process.env.BACKLOG_API_KEY ?? "";
90
- const projectId = parseInt(process.env.BACKLOG_PROJECT_ID ?? "0", 10);
91
- if (space && apiKey && projectId) {
100
+ const projectId = parseInt(process.env.BACKLOG_PROJECT_ID ?? "", 10);
101
+ if (space && apiKey && !isNaN(projectId)) {
92
102
  trackers.push(new BacklogTracker(space, apiKey, projectId));
93
103
  } else {
94
104
  console.warn("[trackers] backlog: BACKLOG_SPACE / BACKLOG_API_KEY / BACKLOG_PROJECT_ID required, skipping");
@@ -2,6 +2,7 @@ import type { IssueTracker, OpenIssue, ClosedIssue } from "./types";
2
2
 
3
3
  export class JiraTracker implements IssueTracker {
4
4
  readonly name = "jira";
5
+ readonly isEmpty = false;
5
6
  private baseUrl: string;
6
7
  private authHeader: string;
7
8
  private projectKey: string;
@@ -38,9 +39,14 @@ export class JiraTracker implements IssueTracker {
38
39
  },
39
40
  }),
40
41
  });
41
- const data = await res.json() as { key?: string; errors?: Record<string, string> };
42
42
  if (!res.ok) {
43
- console.error(`[jira] failed to create issue (${res.status}): ${JSON.stringify(data.errors ?? data)}`);
43
+ const msg = await res.text().catch(() => "");
44
+ console.error(`[jira] failed to create issue (${res.status}): ${msg.slice(0, 200)}`);
45
+ return null;
46
+ }
47
+ const data = await res.json() as { key?: string };
48
+ if (!data.key) {
49
+ console.error("[jira] create issue response missing key");
44
50
  return null;
45
51
  }
46
52
  const url = `${this.baseUrl}/browse/${data.key}`;
@@ -2,6 +2,7 @@ import type { IssueTracker, OpenIssue, ClosedIssue } from "./types";
2
2
 
3
3
  export class NotionTracker implements IssueTracker {
4
4
  readonly name = "notion";
5
+ readonly isEmpty = false;
5
6
  private token: string;
6
7
  private databaseId: string;
7
8
 
@@ -38,13 +39,15 @@ export class NotionTracker implements IssueTracker {
38
39
  ],
39
40
  }),
40
41
  });
41
- const data = await res.json() as { id?: string; url?: string; message?: string };
42
42
  if (!res.ok) {
43
- console.error(`[notion] failed to create page (${res.status}): ${data.message}`);
43
+ const msg = await res.text().catch(() => "");
44
+ console.error(`[notion] failed to create page (${res.status}): ${msg.slice(0, 200)}`);
44
45
  return null;
45
46
  }
46
- console.log(`[notion] page created: ${data.url}`);
47
- return data.url ?? null;
47
+ const data = await res.json() as { url?: string };
48
+ const url = data.url ?? null;
49
+ console.log(`[notion] page created: ${url}`);
50
+ return url;
48
51
  }
49
52
 
50
53
  async fetchOpenIssues(): Promise<OpenIssue[]> {
@@ -13,6 +13,7 @@ export interface ClosedIssue {
13
13
 
14
14
  export interface IssueTracker {
15
15
  readonly name: string;
16
+ readonly isEmpty: boolean;
16
17
  createIssue(title: string, body: string, labels: string[]): Promise<string | null>;
17
18
  fetchOpenIssues(): Promise<OpenIssue[]>;
18
19
  fetchClosedIssues(): Promise<ClosedIssue[]>;
@@ -165,10 +165,14 @@ Organize feedback collected by multiple agents and post it as issue tickets.
165
165
  const fullBody = `**Category:** ${category}\n\n${body}${screenshotSection}\n\n---\n**Reported by:** ${mergedAgents.join(", ")}\n*This Issue was auto-generated by an AI triage agent*`;
166
166
  const cleanTitle = title.replace(/^\[[^\]]+\]\s*/i, "");
167
167
  const url = await tracker.createIssue(`[${category}] ${cleanTitle}`, fullBody, [category, "feedback-agent"]);
168
- mergedIds.forEach((id) => { pendingIds.delete(id); issuedIds.push(id); });
169
- issuesCreated++;
170
- result = { created: true, url, mergedCount: mergedIds.length };
171
- console.log(` [triage] issue created: "[${category}] ${cleanTitle}" (merged ${mergedIds.length})`);
168
+ if (url === null && !tracker.isEmpty) {
169
+ result = { created: false, error: "tracker returned null — check logs" };
170
+ } else {
171
+ mergedIds.forEach((id) => { pendingIds.delete(id); issuedIds.push(id); });
172
+ issuesCreated++;
173
+ result = { created: true, url, mergedCount: mergedIds.length };
174
+ console.log(` [triage] issue created: "[${category}] ${cleanTitle}" (merged ${mergedIds.length})`);
175
+ }
172
176
 
173
177
  } else if (toolUse.name === "skip_finding") {
174
178
  const { finding_id, reason } = toolUse.input as { finding_id: string; reason: string };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m8i-51/shoal",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "type": "module",
5
5
  "description": "Multi-agent web exploration framework — finds bugs, UX issues, and missing features by running AI agents against your app",
6
6
  "repository": {
@@ -51,7 +51,7 @@
51
51
  "@types/react-dom": "^19.2.3",
52
52
  "@vitejs/plugin-react": "^4.7.0",
53
53
  "@vitest/coverage-v8": "^4.1.6",
54
- "concurrently": "^9.2.1",
54
+ "concurrently": "^10.0.3",
55
55
  "i18next": "^26.0.6",
56
56
  "react": "^19.2.5",
57
57
  "react-dom": "^19.2.5",