@avocadostudio-ai/orchestrator-core 0.1.0
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/LICENSE +201 -0
- package/dist/agent/agent-context.d.ts +19 -0
- package/dist/agent/agent-context.js +67 -0
- package/dist/agent/agent-logger.d.ts +5 -0
- package/dist/agent/agent-logger.js +22 -0
- package/dist/agent/agent-loop-openai.d.ts +8 -0
- package/dist/agent/agent-loop-openai.js +172 -0
- package/dist/agent/agent-loop.d.ts +56 -0
- package/dist/agent/agent-loop.js +167 -0
- package/dist/agent/agent-provider.d.ts +28 -0
- package/dist/agent/agent-provider.js +63 -0
- package/dist/agent/agent-tools.d.ts +28 -0
- package/dist/agent/agent-tools.js +899 -0
- package/dist/agent/context/editing-guidelines.md +46 -0
- package/dist/agent/context/role.md +39 -0
- package/dist/agent/integration-prompt.d.ts +9 -0
- package/dist/agent/integration-prompt.js +154 -0
- package/dist/agent/sites-agent-context.d.ts +12 -0
- package/dist/agent/sites-agent-context.js +316 -0
- package/dist/agent/sites-agent-shared.d.ts +161 -0
- package/dist/agent/sites-agent-shared.js +1101 -0
- package/dist/agent/sites-agent-tools.d.ts +18 -0
- package/dist/agent/sites-agent-tools.js +1227 -0
- package/dist/chat/anthropic-cache.d.ts +20 -0
- package/dist/chat/anthropic-cache.js +54 -0
- package/dist/chat/anthropic-planner.d.ts +98 -0
- package/dist/chat/anthropic-planner.js +1012 -0
- package/dist/chat/changelog-coverage-validator.d.ts +37 -0
- package/dist/chat/changelog-coverage-validator.js +215 -0
- package/dist/chat/chat-pipeline-context.d.ts +211 -0
- package/dist/chat/chat-pipeline-context.js +249 -0
- package/dist/chat/chat-pipeline-deterministic.d.ts +61 -0
- package/dist/chat/chat-pipeline-deterministic.js +407 -0
- package/dist/chat/chat-pipeline-image.d.ts +86 -0
- package/dist/chat/chat-pipeline-image.js +897 -0
- package/dist/chat/chat-pipeline-shared.d.ts +69 -0
- package/dist/chat/chat-pipeline-shared.js +212 -0
- package/dist/chat/chat-pipeline-translation.d.ts +27 -0
- package/dist/chat/chat-pipeline-translation.js +417 -0
- package/dist/chat/chat-pipeline-ui.d.ts +14 -0
- package/dist/chat/chat-pipeline-ui.js +244 -0
- package/dist/chat/chat-pipeline.d.ts +99 -0
- package/dist/chat/chat-pipeline.js +3999 -0
- package/dist/chat/decomposer.d.ts +21 -0
- package/dist/chat/decomposer.js +65 -0
- package/dist/chat/gemini-planner.d.ts +70 -0
- package/dist/chat/gemini-planner.js +541 -0
- package/dist/chat/hallucination-validator.d.ts +36 -0
- package/dist/chat/hallucination-validator.js +110 -0
- package/dist/chat/locale-strings.d.ts +47 -0
- package/dist/chat/locale-strings.js +100 -0
- package/dist/chat/plan-json-schema.d.ts +133 -0
- package/dist/chat/plan-json-schema.js +112 -0
- package/dist/chat/planner-types.d.ts +120 -0
- package/dist/chat/planner-types.js +66 -0
- package/dist/chat/planner.d.ts +148 -0
- package/dist/chat/planner.js +1361 -0
- package/dist/chat/prompts.d.ts +67 -0
- package/dist/chat/prompts.js +356 -0
- package/dist/chat/provider-routing.d.ts +14 -0
- package/dist/chat/provider-routing.js +27 -0
- package/dist/chat/variation-pipeline.d.ts +135 -0
- package/dist/chat/variation-pipeline.js +837 -0
- package/dist/chat/vision-alt-generator.d.ts +35 -0
- package/dist/chat/vision-alt-generator.js +152 -0
- package/dist/cms/adapter.d.ts +62 -0
- package/dist/cms/adapter.js +1 -0
- package/dist/cms/bootstrap.d.ts +17 -0
- package/dist/cms/bootstrap.js +85 -0
- package/dist/cms/editor-api-adapter.d.ts +23 -0
- package/dist/cms/editor-api-adapter.js +71 -0
- package/dist/cms/index.d.ts +4 -0
- package/dist/cms/index.js +3 -0
- package/dist/cms/json-file-adapter.d.ts +11 -0
- package/dist/cms/json-file-adapter.js +62 -0
- package/dist/demo-mode.d.ts +59 -0
- package/dist/demo-mode.js +201 -0
- package/dist/errors.d.ts +67 -0
- package/dist/errors.js +129 -0
- package/dist/http/chat-stream-resumable.d.ts +108 -0
- package/dist/http/chat-stream-resumable.js +290 -0
- package/dist/http/chat-stream.d.ts +99 -0
- package/dist/http/chat-stream.js +92 -0
- package/dist/image/gdrive-client.d.ts +22 -0
- package/dist/image/gdrive-client.js +215 -0
- package/dist/image/image-helpers.d.ts +95 -0
- package/dist/image/image-helpers.js +488 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/jira/jira-approval.d.ts +22 -0
- package/dist/jira/jira-approval.js +51 -0
- package/dist/jira/jira-client.d.ts +44 -0
- package/dist/jira/jira-client.js +313 -0
- package/dist/jira/jira-poller.d.ts +46 -0
- package/dist/jira/jira-poller.js +184 -0
- package/dist/jira/jira-processor.d.ts +103 -0
- package/dist/jira/jira-processor.js +1085 -0
- package/dist/jira/jira-types.d.ts +117 -0
- package/dist/jira/jira-types.js +38 -0
- package/dist/logger.d.ts +12 -0
- package/dist/logger.js +28 -0
- package/dist/migration/mcp-server-stdio.d.ts +8 -0
- package/dist/migration/mcp-server-stdio.js +672 -0
- package/dist/migration/migration-prompt.d.ts +7 -0
- package/dist/migration/migration-prompt.js +197 -0
- package/dist/migration/migration-tools.d.ts +17 -0
- package/dist/migration/migration-tools.js +159 -0
- package/dist/migration/scrape-cache.d.ts +9 -0
- package/dist/migration/scrape-cache.js +19 -0
- package/dist/nlp/deterministic-planner-context.d.ts +141 -0
- package/dist/nlp/deterministic-planner-context.js +362 -0
- package/dist/nlp/deterministic-planner-pages.d.ts +26 -0
- package/dist/nlp/deterministic-planner-pages.js +170 -0
- package/dist/nlp/deterministic-planner-patches.d.ts +80 -0
- package/dist/nlp/deterministic-planner-patches.js +508 -0
- package/dist/nlp/deterministic-planner-refs.d.ts +33 -0
- package/dist/nlp/deterministic-planner-refs.js +164 -0
- package/dist/nlp/deterministic-planner-suggestions.d.ts +49 -0
- package/dist/nlp/deterministic-planner-suggestions.js +579 -0
- package/dist/nlp/deterministic-planner.d.ts +85 -0
- package/dist/nlp/deterministic-planner.js +1631 -0
- package/dist/nlp/intent-detection.d.ts +309 -0
- package/dist/nlp/intent-detection.js +730 -0
- package/dist/nlp/intent-helpers.d.ts +15 -0
- package/dist/nlp/intent-helpers.js +243 -0
- package/dist/nlp/intent-patterns.d.ts +40 -0
- package/dist/nlp/intent-patterns.js +223 -0
- package/dist/nlp/plan-normalizer.d.ts +41 -0
- package/dist/nlp/plan-normalizer.js +1537 -0
- package/dist/ops/destructive-action-gate.d.ts +44 -0
- package/dist/ops/destructive-action-gate.js +90 -0
- package/dist/ops/ops-engine.d.ts +151 -0
- package/dist/ops/ops-engine.js +1394 -0
- package/dist/publish/diff-engine.d.ts +18 -0
- package/dist/publish/diff-engine.js +305 -0
- package/dist/publish/publish-helpers.d.ts +87 -0
- package/dist/publish/publish-helpers.js +521 -0
- package/dist/publish/publish-target-registry.d.ts +7 -0
- package/dist/publish/publish-target-registry.js +61 -0
- package/dist/publish/publish-target.d.ts +81 -0
- package/dist/publish/publish-target.js +1 -0
- package/dist/publish/targets/deploy-hook.d.ts +13 -0
- package/dist/publish/targets/deploy-hook.js +123 -0
- package/dist/publish/targets/git.d.ts +13 -0
- package/dist/publish/targets/git.js +55 -0
- package/dist/publish/targets/site-contract.d.ts +19 -0
- package/dist/publish/targets/site-contract.js +124 -0
- package/dist/state/content-source.d.ts +17 -0
- package/dist/state/content-source.js +1 -0
- package/dist/state/in-memory-content-source.d.ts +27 -0
- package/dist/state/in-memory-content-source.js +51 -0
- package/dist/state/session-lock.d.ts +13 -0
- package/dist/state/session-lock.js +29 -0
- package/dist/state/session-state.d.ts +310 -0
- package/dist/state/session-state.js +1083 -0
- package/dist/state/sqlite-store-singleton.d.ts +31 -0
- package/dist/state/sqlite-store-singleton.js +170 -0
- package/dist/state/sqlite-store.d.ts +135 -0
- package/dist/state/sqlite-store.js +421 -0
- package/dist/telemetry/chat-telemetry.d.ts +105 -0
- package/dist/telemetry/chat-telemetry.js +247 -0
- package/dist/telemetry/eval-candidate-store.d.ts +50 -0
- package/dist/telemetry/eval-candidate-store.js +120 -0
- package/dist/telemetry/feedback-store.d.ts +34 -0
- package/dist/telemetry/feedback-store.js +76 -0
- package/dist/telemetry/jira-telemetry.d.ts +57 -0
- package/dist/telemetry/jira-telemetry.js +68 -0
- package/dist/telemetry/migration-telemetry.d.ts +35 -0
- package/dist/telemetry/migration-telemetry.js +40 -0
- package/dist/telemetry/usage.d.ts +24 -0
- package/dist/telemetry/usage.js +80 -0
- package/dist/tools/builtin-registrations.d.ts +12 -0
- package/dist/tools/builtin-registrations.js +33 -0
- package/dist/tools/builtins/gdrive-browse.d.ts +3 -0
- package/dist/tools/builtins/gdrive-browse.js +68 -0
- package/dist/tools/builtins/image-generate.d.ts +3 -0
- package/dist/tools/builtins/image-generate.js +211 -0
- package/dist/tools/builtins/unsplash-get-by-id.d.ts +23 -0
- package/dist/tools/builtins/unsplash-get-by-id.js +119 -0
- package/dist/tools/builtins/unsplash-search.d.ts +3 -0
- package/dist/tools/builtins/unsplash-search.js +74 -0
- package/dist/tools/executor.d.ts +23 -0
- package/dist/tools/executor.js +169 -0
- package/dist/tools/index.d.ts +5 -0
- package/dist/tools/index.js +5 -0
- package/dist/tools/registry.d.ts +21 -0
- package/dist/tools/registry.js +75 -0
- package/dist/tools/runtime.d.ts +27 -0
- package/dist/tools/runtime.js +48 -0
- package/dist/tools/schema-validator.d.ts +24 -0
- package/dist/tools/schema-validator.js +88 -0
- package/dist/tools/types.d.ts +86 -0
- package/dist/tools/types.js +1 -0
- package/dist/variation-images.d.ts +19 -0
- package/dist/variation-images.js +12 -0
- package/package.json +78 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JIRA REST API client.
|
|
3
|
+
*
|
|
4
|
+
* Supports both JIRA Cloud (Basic auth: email + API token) and
|
|
5
|
+
* JIRA Server/Data Center (Bearer token / PAT).
|
|
6
|
+
*
|
|
7
|
+
* Uses only native fetch — no external HTTP library needed.
|
|
8
|
+
*/
|
|
9
|
+
export class JiraClient {
|
|
10
|
+
baseUrl;
|
|
11
|
+
authHeader;
|
|
12
|
+
isCloud;
|
|
13
|
+
constructor(config) {
|
|
14
|
+
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
15
|
+
this.isCloud = this.baseUrl.includes(".atlassian.net");
|
|
16
|
+
if (this.isCloud && config.email) {
|
|
17
|
+
// Cloud: Basic auth with email:apiToken
|
|
18
|
+
const creds = Buffer.from(`${config.email}:${config.apiToken}`).toString("base64");
|
|
19
|
+
this.authHeader = `Basic ${creds}`;
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
// Server/Data Center: Bearer PAT
|
|
23
|
+
this.authHeader = `Bearer ${config.apiToken}`;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Core HTTP
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
async request(path, options) {
|
|
30
|
+
const url = `${this.baseUrl}/rest/api/${this.isCloud ? "3" : "2"}${path}`;
|
|
31
|
+
const res = await fetch(url, {
|
|
32
|
+
...options,
|
|
33
|
+
headers: {
|
|
34
|
+
"Authorization": this.authHeader,
|
|
35
|
+
"Accept": "application/json",
|
|
36
|
+
"Content-Type": "application/json",
|
|
37
|
+
...options?.headers,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
if (!res.ok) {
|
|
41
|
+
const body = await res.text().catch(() => "");
|
|
42
|
+
throw new Error(`JIRA API ${res.status}: ${res.statusText} — ${body.slice(0, 500)}`);
|
|
43
|
+
}
|
|
44
|
+
if (res.status === 204)
|
|
45
|
+
return undefined;
|
|
46
|
+
return res.json();
|
|
47
|
+
}
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Issue operations
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
async getIssue(issueKey) {
|
|
52
|
+
return this.request(`/issue/${encodeURIComponent(issueKey)}?fields=summary,description,status,labels,attachment,reporter,assignee,creator,comment`);
|
|
53
|
+
}
|
|
54
|
+
async getIssueAttachments(issueKey) {
|
|
55
|
+
const issue = await this.getIssue(issueKey);
|
|
56
|
+
return issue.fields.attachment ?? [];
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Download an attachment binary. The `content` URL from JIRA requires the
|
|
60
|
+
* same auth header, so we can't just fetch it unauthenticated.
|
|
61
|
+
*/
|
|
62
|
+
async downloadAttachment(contentUrl) {
|
|
63
|
+
const res = await fetch(contentUrl, {
|
|
64
|
+
headers: {
|
|
65
|
+
"Authorization": this.authHeader,
|
|
66
|
+
"Accept": "*/*",
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
if (!res.ok) {
|
|
70
|
+
throw new Error(`JIRA attachment download failed: ${res.status} ${res.statusText}`);
|
|
71
|
+
}
|
|
72
|
+
return Buffer.from(await res.arrayBuffer());
|
|
73
|
+
}
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Feedback operations
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
async addComment(issueKey, markdown) {
|
|
78
|
+
if (this.isCloud) {
|
|
79
|
+
// Cloud API v3 uses Atlassian Document Format
|
|
80
|
+
await this.request(`/issue/${encodeURIComponent(issueKey)}/comment`, {
|
|
81
|
+
method: "POST",
|
|
82
|
+
body: JSON.stringify({
|
|
83
|
+
body: markdownToAdf(markdown),
|
|
84
|
+
}),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
// Server API v2 uses plain text/wiki markup
|
|
89
|
+
await this.request(`/issue/${encodeURIComponent(issueKey)}/comment`, {
|
|
90
|
+
method: "POST",
|
|
91
|
+
body: JSON.stringify({ body: markdown }),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Transition an issue to a target status by name.
|
|
97
|
+
* Looks up available transitions to find the matching one.
|
|
98
|
+
*/
|
|
99
|
+
async transitionIssue(issueKey, targetStatusName) {
|
|
100
|
+
const res = await this.request(`/issue/${encodeURIComponent(issueKey)}/transitions`);
|
|
101
|
+
const match = res.transitions.find((t) => t.name.toLowerCase() === targetStatusName.toLowerCase() ||
|
|
102
|
+
t.to.name.toLowerCase() === targetStatusName.toLowerCase());
|
|
103
|
+
if (!match)
|
|
104
|
+
return false;
|
|
105
|
+
await this.request(`/issue/${encodeURIComponent(issueKey)}/transitions`, {
|
|
106
|
+
method: "POST",
|
|
107
|
+
body: JSON.stringify({ transition: { id: match.id } }),
|
|
108
|
+
});
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Assign an issue to a user by account ID (Cloud) or username (Server).
|
|
113
|
+
*/
|
|
114
|
+
async assignIssue(issueKey, assignee) {
|
|
115
|
+
await this.request(`/issue/${encodeURIComponent(issueKey)}/assignee`, {
|
|
116
|
+
method: "PUT",
|
|
117
|
+
body: JSON.stringify(this.isCloud ? { accountId: assignee.accountId } : { name: assignee.name }),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// Search
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
async searchJql(jql, maxResults = 20) {
|
|
124
|
+
// Cloud deprecated `/search` in favor of `/search/jql` (changelog CHANGE-2046).
|
|
125
|
+
// The new endpoint keeps the same `issues[]` response shape; we don't need
|
|
126
|
+
// pagination (maxResults is capped at 100) so nextPageToken is ignored.
|
|
127
|
+
// Server/DC still uses the legacy `/search` on API v2.
|
|
128
|
+
const path = this.isCloud ? "/search/jql" : "/search";
|
|
129
|
+
const res = await this.request(`${path}?jql=${encodeURIComponent(jql)}&maxResults=${maxResults}&fields=summary,description,status,labels,attachment,reporter,assignee,creator,comment`);
|
|
130
|
+
return res.issues ?? [];
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// Helpers
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
/**
|
|
137
|
+
* Convert simple markdown text to Atlassian Document Format (ADF).
|
|
138
|
+
* Handles paragraphs, bulletLists, tables, bold (**text**), italic (_text_),
|
|
139
|
+
* inline code (`text`), and links ([label](url)). Strips HTML comments.
|
|
140
|
+
* Good enough for status/clarification comments.
|
|
141
|
+
*/
|
|
142
|
+
export function markdownToAdf(markdown) {
|
|
143
|
+
// HTML comments are not an ADF node type — they leak through as literal text.
|
|
144
|
+
// Strip them here so stray markers or annotations don't surface in Jira comments.
|
|
145
|
+
const normalized = markdown.replace(/<!--[\s\S]*?-->/g, "");
|
|
146
|
+
const lines = normalized.split("\n");
|
|
147
|
+
const content = [];
|
|
148
|
+
let i = 0;
|
|
149
|
+
while (i < lines.length) {
|
|
150
|
+
const line = lines[i];
|
|
151
|
+
if (line.trim() === "") {
|
|
152
|
+
i++;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
// Table: pipe-row header + separator row + pipe-row data rows.
|
|
156
|
+
// Tolerates blank lines between rows because Jira re-renders each line
|
|
157
|
+
// as its own paragraph when the table parser doesn't fire, and LLMs
|
|
158
|
+
// sometimes emit tables that way.
|
|
159
|
+
if (isTableRow(line)) {
|
|
160
|
+
let j = i + 1;
|
|
161
|
+
while (j < lines.length && lines[j].trim() === "")
|
|
162
|
+
j++;
|
|
163
|
+
if (j < lines.length && isTableSeparator(lines[j])) {
|
|
164
|
+
const headerCells = splitTableRow(line);
|
|
165
|
+
const rows = [buildTableRow(headerCells, "tableHeader")];
|
|
166
|
+
i = j + 1;
|
|
167
|
+
while (i < lines.length) {
|
|
168
|
+
if (lines[i].trim() === "") {
|
|
169
|
+
i++;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (!isTableRow(lines[i]))
|
|
173
|
+
break;
|
|
174
|
+
rows.push(buildTableRow(splitTableRow(lines[i]), "tableCell"));
|
|
175
|
+
i++;
|
|
176
|
+
}
|
|
177
|
+
content.push({
|
|
178
|
+
type: "table",
|
|
179
|
+
attrs: { isNumberColumnEnabled: false, layout: "default" },
|
|
180
|
+
content: rows,
|
|
181
|
+
});
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// Bullet list: consecutive lines starting with "- "
|
|
186
|
+
if (/^\s*-\s+/.test(line)) {
|
|
187
|
+
const items = [];
|
|
188
|
+
while (i < lines.length && /^\s*-\s+/.test(lines[i])) {
|
|
189
|
+
const itemText = lines[i].replace(/^\s*-\s+/, "");
|
|
190
|
+
items.push({
|
|
191
|
+
type: "listItem",
|
|
192
|
+
content: [{ type: "paragraph", content: parseInlineMarkdown(itemText) }],
|
|
193
|
+
});
|
|
194
|
+
i++;
|
|
195
|
+
}
|
|
196
|
+
content.push({ type: "bulletList", content: items });
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
content.push({ type: "paragraph", content: parseInlineMarkdown(line) });
|
|
200
|
+
i++;
|
|
201
|
+
}
|
|
202
|
+
return { version: 1, type: "doc", content };
|
|
203
|
+
}
|
|
204
|
+
function isTableRow(line) {
|
|
205
|
+
const t = line.trim();
|
|
206
|
+
return t.length >= 2 && t.startsWith("|") && t.endsWith("|");
|
|
207
|
+
}
|
|
208
|
+
function isTableSeparator(line) {
|
|
209
|
+
if (!isTableRow(line))
|
|
210
|
+
return false;
|
|
211
|
+
const inner = line.trim().slice(1, -1);
|
|
212
|
+
return /^\s*:?-+:?\s*(\|\s*:?-+:?\s*)*$/.test(inner);
|
|
213
|
+
}
|
|
214
|
+
function splitTableRow(line) {
|
|
215
|
+
const inner = line.trim().slice(1, -1);
|
|
216
|
+
return inner.split("|").map((s) => s.trim());
|
|
217
|
+
}
|
|
218
|
+
function buildTableRow(cells, cellType) {
|
|
219
|
+
return {
|
|
220
|
+
type: "tableRow",
|
|
221
|
+
content: cells.map((text) => ({
|
|
222
|
+
type: cellType,
|
|
223
|
+
attrs: {},
|
|
224
|
+
// ADF rejects empty text nodes — substitute a single space for blank cells.
|
|
225
|
+
content: [{ type: "paragraph", content: parseInlineMarkdown(text.length > 0 ? text : " ") }],
|
|
226
|
+
})),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Parse inline markdown (bold + links) into ADF inline nodes.
|
|
231
|
+
* Scans left-to-right, handling **bold** and [label](url) as first-class constructs
|
|
232
|
+
* and falling through to plain text between them.
|
|
233
|
+
*/
|
|
234
|
+
function parseInlineMarkdown(text) {
|
|
235
|
+
const nodes = [];
|
|
236
|
+
let i = 0;
|
|
237
|
+
function pushText(raw, marks) {
|
|
238
|
+
if (!raw)
|
|
239
|
+
return;
|
|
240
|
+
const node = { type: "text", text: raw };
|
|
241
|
+
if (marks && marks.length)
|
|
242
|
+
node.marks = marks;
|
|
243
|
+
nodes.push(node);
|
|
244
|
+
}
|
|
245
|
+
while (i < text.length) {
|
|
246
|
+
// Bold: **...**
|
|
247
|
+
if (text[i] === "*" && text[i + 1] === "*") {
|
|
248
|
+
const end = text.indexOf("**", i + 2);
|
|
249
|
+
if (end !== -1) {
|
|
250
|
+
pushText(text.slice(i + 2, end), [{ type: "strong" }]);
|
|
251
|
+
i = end + 2;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// Italic: _..._ (word-boundary aware — avoids matching snake_case identifiers)
|
|
256
|
+
if (text[i] === "_") {
|
|
257
|
+
const prev = i > 0 ? text[i - 1] : "";
|
|
258
|
+
if (!prev || !/\w/.test(prev)) {
|
|
259
|
+
const end = text.indexOf("_", i + 1);
|
|
260
|
+
if (end > i + 1) {
|
|
261
|
+
const next = text[end + 1] ?? "";
|
|
262
|
+
const inner = text.slice(i + 1, end);
|
|
263
|
+
if ((!next || !/\w/.test(next)) && inner.trim() === inner && inner.length > 0) {
|
|
264
|
+
pushText(inner, [{ type: "em" }]);
|
|
265
|
+
i = end + 1;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// Inline code: `...`
|
|
272
|
+
if (text[i] === "`") {
|
|
273
|
+
const end = text.indexOf("`", i + 1);
|
|
274
|
+
if (end !== -1) {
|
|
275
|
+
pushText(text.slice(i + 1, end), [{ type: "code" }]);
|
|
276
|
+
i = end + 1;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
// Link: [label](url)
|
|
281
|
+
if (text[i] === "[") {
|
|
282
|
+
const labelEnd = text.indexOf("]", i + 1);
|
|
283
|
+
if (labelEnd !== -1 && text[labelEnd + 1] === "(") {
|
|
284
|
+
const urlEnd = text.indexOf(")", labelEnd + 2);
|
|
285
|
+
if (urlEnd !== -1) {
|
|
286
|
+
const label = text.slice(i + 1, labelEnd);
|
|
287
|
+
const url = text.slice(labelEnd + 2, urlEnd);
|
|
288
|
+
nodes.push({
|
|
289
|
+
type: "text",
|
|
290
|
+
text: label,
|
|
291
|
+
marks: [{ type: "link", attrs: { href: url } }],
|
|
292
|
+
});
|
|
293
|
+
i = urlEnd + 1;
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// Plain run up to the next special char
|
|
299
|
+
let next = text.length;
|
|
300
|
+
for (const marker of ["**", "[", "`", "_"]) {
|
|
301
|
+
const idx = text.indexOf(marker, i);
|
|
302
|
+
if (idx !== -1 && idx < next)
|
|
303
|
+
next = idx;
|
|
304
|
+
}
|
|
305
|
+
if (next === i)
|
|
306
|
+
next = i + 1;
|
|
307
|
+
pushText(text.slice(i, next));
|
|
308
|
+
i = next;
|
|
309
|
+
}
|
|
310
|
+
if (nodes.length === 0)
|
|
311
|
+
nodes.push({ type: "text", text });
|
|
312
|
+
return nodes;
|
|
313
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JIRA polling mode.
|
|
3
|
+
*
|
|
4
|
+
* Periodically queries JIRA via JQL to find tickets that need action. Used
|
|
5
|
+
* when webhooks aren't available (firewall, local dev, etc.).
|
|
6
|
+
*
|
|
7
|
+
* The poller inspects each matching issue and decides which processing mode
|
|
8
|
+
* to run based on current status + comment history:
|
|
9
|
+
*
|
|
10
|
+
* - To Do, no prior agent comment → review (first look)
|
|
11
|
+
* - To Do, new reporter comment (approval) → execute
|
|
12
|
+
* - To Do, new reporter comment (other) → review (re-review with new info)
|
|
13
|
+
* - In Review, new reporter comment (approval) → publish
|
|
14
|
+
* - In Review, new reporter comment (other) → execute (apply follow-up edits)
|
|
15
|
+
*
|
|
16
|
+
* Deduplication key = `${issueKey}:${status}:${commentCount}`, so the poller
|
|
17
|
+
* re-processes only when state meaningfully changes.
|
|
18
|
+
*
|
|
19
|
+
* Enabled via JIRA_POLL_ENABLED=1.
|
|
20
|
+
*/
|
|
21
|
+
import type { Logger } from "../logger.ts";
|
|
22
|
+
import { type JiraProcessingMode } from "./jira-processor.ts";
|
|
23
|
+
import type { JiraConfig, JiraIssue } from "./jira-types.ts";
|
|
24
|
+
export type PollerRoute = {
|
|
25
|
+
mode: JiraProcessingMode;
|
|
26
|
+
reason: string;
|
|
27
|
+
} | null;
|
|
28
|
+
/**
|
|
29
|
+
* Decide whether an issue found by the poller needs processing right now, and
|
|
30
|
+
* in which mode. Returns null when the issue should be left alone (e.g. agent
|
|
31
|
+
* has already reviewed and is waiting for the reporter).
|
|
32
|
+
*/
|
|
33
|
+
export declare function routePollerIssue(issue: JiraIssue, config: JiraConfig): PollerRoute;
|
|
34
|
+
export declare function startJiraPoller(options: {
|
|
35
|
+
config: JiraConfig;
|
|
36
|
+
generatedImageDir: string;
|
|
37
|
+
orchestratorPublicOrigin: string;
|
|
38
|
+
sitePublicOrigin: string;
|
|
39
|
+
logger: Logger;
|
|
40
|
+
}): void;
|
|
41
|
+
export declare function stopJiraPoller(): void;
|
|
42
|
+
export declare function getPollerStatus(): {
|
|
43
|
+
running: boolean;
|
|
44
|
+
processedCount: number;
|
|
45
|
+
isPolling: boolean;
|
|
46
|
+
};
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JIRA polling mode.
|
|
3
|
+
*
|
|
4
|
+
* Periodically queries JIRA via JQL to find tickets that need action. Used
|
|
5
|
+
* when webhooks aren't available (firewall, local dev, etc.).
|
|
6
|
+
*
|
|
7
|
+
* The poller inspects each matching issue and decides which processing mode
|
|
8
|
+
* to run based on current status + comment history:
|
|
9
|
+
*
|
|
10
|
+
* - To Do, no prior agent comment → review (first look)
|
|
11
|
+
* - To Do, new reporter comment (approval) → execute
|
|
12
|
+
* - To Do, new reporter comment (other) → review (re-review with new info)
|
|
13
|
+
* - In Review, new reporter comment (approval) → publish
|
|
14
|
+
* - In Review, new reporter comment (other) → execute (apply follow-up edits)
|
|
15
|
+
*
|
|
16
|
+
* Deduplication key = `${issueKey}:${status}:${commentCount}`, so the poller
|
|
17
|
+
* re-processes only when state meaningfully changes.
|
|
18
|
+
*
|
|
19
|
+
* Enabled via JIRA_POLL_ENABLED=1.
|
|
20
|
+
*/
|
|
21
|
+
import { JiraClient } from "./jira-client.js";
|
|
22
|
+
import { processJiraTicket, adfToPlainText, isAgentAuthoredComment } from "./jira-processor.js";
|
|
23
|
+
import { isApprovalComment } from "./jira-approval.js";
|
|
24
|
+
// Dedup: per-issue state fingerprint we've already processed.
|
|
25
|
+
const processedStates = new Map();
|
|
26
|
+
let pollTimer = null;
|
|
27
|
+
let clearTimer = null;
|
|
28
|
+
let isPolling = false;
|
|
29
|
+
function statusEquals(a, b) {
|
|
30
|
+
if (!a || !b)
|
|
31
|
+
return false;
|
|
32
|
+
return a.trim().toLowerCase() === b.trim().toLowerCase();
|
|
33
|
+
}
|
|
34
|
+
function fingerprint(issue) {
|
|
35
|
+
const status = issue.fields.status?.name ?? "?";
|
|
36
|
+
const commentCount = issue.fields.comment?.comments?.length ?? 0;
|
|
37
|
+
return `${issue.key}:${status}:${commentCount}`;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Decide whether an issue found by the poller needs processing right now, and
|
|
41
|
+
* in which mode. Returns null when the issue should be left alone (e.g. agent
|
|
42
|
+
* has already reviewed and is waiting for the reporter).
|
|
43
|
+
*/
|
|
44
|
+
export function routePollerIssue(issue, config) {
|
|
45
|
+
const status = issue.fields.status?.name;
|
|
46
|
+
const comments = issue.fields.comment?.comments ?? [];
|
|
47
|
+
const agentId = config.agentAccountId;
|
|
48
|
+
const bodyOf = (c) => typeof c.body === "string" ? c.body : adfToPlainText(c.body);
|
|
49
|
+
// Classify by body shape too, not only accountId — solo Jira tenants have
|
|
50
|
+
// the same human acting as reporter + agent, and we must still see reporter
|
|
51
|
+
// replies to progress the workflow.
|
|
52
|
+
const agentComments = comments.filter((c) => isAgentAuthoredComment(bodyOf(c), c.author?.accountId, agentId));
|
|
53
|
+
const nonAgentComments = comments.filter((c) => !agentComments.includes(c));
|
|
54
|
+
const latestAgent = agentComments.at(-1);
|
|
55
|
+
const latestNonAgent = nonAgentComments.at(-1);
|
|
56
|
+
const hasNewerReporterComment = (latestNonAgent) => {
|
|
57
|
+
if (!latestNonAgent)
|
|
58
|
+
return false;
|
|
59
|
+
if (!latestAgent)
|
|
60
|
+
return true;
|
|
61
|
+
return latestNonAgent.created > latestAgent.created;
|
|
62
|
+
};
|
|
63
|
+
const latestBodyText = (c) => c ? bodyOf(c) : "";
|
|
64
|
+
// ---- Review stage ----
|
|
65
|
+
if (statusEquals(status, config.reviewStatus) || statusEquals(status, config.triggerStatus)) {
|
|
66
|
+
if (agentComments.length === 0) {
|
|
67
|
+
return { mode: "review", reason: "first review — To Do ticket with no agent comment" };
|
|
68
|
+
}
|
|
69
|
+
if (hasNewerReporterComment(latestNonAgent)) {
|
|
70
|
+
const body = latestBodyText(latestNonAgent);
|
|
71
|
+
if (isApprovalComment(body)) {
|
|
72
|
+
return { mode: "execute", reason: "approval comment on To Do" };
|
|
73
|
+
}
|
|
74
|
+
return { mode: "review", reason: "new reporter comment on To Do — re-review" };
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
// ---- Preview stage ----
|
|
79
|
+
if (statusEquals(status, config.previewStatus)) {
|
|
80
|
+
if (hasNewerReporterComment(latestNonAgent)) {
|
|
81
|
+
const body = latestBodyText(latestNonAgent);
|
|
82
|
+
if (isApprovalComment(body)) {
|
|
83
|
+
return { mode: "publish", reason: "approval comment on In Review" };
|
|
84
|
+
}
|
|
85
|
+
return { mode: "execute", reason: "follow-up comment on In Review — re-edit" };
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
export function startJiraPoller(options) {
|
|
92
|
+
const { config, generatedImageDir, orchestratorPublicOrigin, sitePublicOrigin, logger } = options;
|
|
93
|
+
if (pollTimer) {
|
|
94
|
+
logger.warn("JIRA poller already running, skipping duplicate start");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const client = new JiraClient(config);
|
|
98
|
+
logger.info({ jql: config.pollJql, intervalMs: config.pollIntervalMs }, "JIRA: starting poller");
|
|
99
|
+
async function poll() {
|
|
100
|
+
if (isPolling)
|
|
101
|
+
return;
|
|
102
|
+
isPolling = true;
|
|
103
|
+
try {
|
|
104
|
+
const issues = await client.searchJql(config.pollJql, 20);
|
|
105
|
+
const actionable = [];
|
|
106
|
+
for (const issue of issues) {
|
|
107
|
+
const route = routePollerIssue(issue, config);
|
|
108
|
+
if (!route)
|
|
109
|
+
continue;
|
|
110
|
+
const fp = fingerprint(issue);
|
|
111
|
+
if (processedStates.get(issue.key) === fp)
|
|
112
|
+
continue;
|
|
113
|
+
actionable.push({ issue, route, fp });
|
|
114
|
+
}
|
|
115
|
+
if (actionable.length > 0) {
|
|
116
|
+
logger.info({ count: actionable.length, keys: actionable.map((a) => `${a.issue.key}(${a.route.mode})`) }, "JIRA: poller found actionable issues");
|
|
117
|
+
}
|
|
118
|
+
for (const { issue, route, fp } of actionable) {
|
|
119
|
+
processedStates.set(issue.key, fp);
|
|
120
|
+
try {
|
|
121
|
+
await processJiraTicket({
|
|
122
|
+
issueKey: issue.key,
|
|
123
|
+
config,
|
|
124
|
+
mode: route.mode,
|
|
125
|
+
generatedImageDir,
|
|
126
|
+
orchestratorPublicOrigin,
|
|
127
|
+
sitePublicOrigin,
|
|
128
|
+
logger,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
logger.error({ issueKey: issue.key, mode: route.mode, error: err instanceof Error ? err.message : String(err) }, "JIRA: poller failed to process issue");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
logger.error({ error: err instanceof Error ? err.message : String(err) }, "JIRA: poller query failed");
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
isPolling = false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
void poll();
|
|
144
|
+
pollTimer = setInterval(() => void poll(), config.pollIntervalMs);
|
|
145
|
+
// Clear dedup cache every 24h so issues that sit in the same state for a
|
|
146
|
+
// long time can still be re-picked-up if needed (e.g. after server restart
|
|
147
|
+
// with stale cache).
|
|
148
|
+
const CLEAR_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
149
|
+
clearTimer = setInterval(() => {
|
|
150
|
+
const cleared = processedStates.size;
|
|
151
|
+
processedStates.clear();
|
|
152
|
+
logger.debug({ cleared }, "JIRA: cleared poller dedup cache");
|
|
153
|
+
}, CLEAR_INTERVAL_MS);
|
|
154
|
+
const shutdown = () => {
|
|
155
|
+
if (pollTimer) {
|
|
156
|
+
clearInterval(pollTimer);
|
|
157
|
+
pollTimer = null;
|
|
158
|
+
}
|
|
159
|
+
if (clearTimer) {
|
|
160
|
+
clearInterval(clearTimer);
|
|
161
|
+
clearTimer = null;
|
|
162
|
+
}
|
|
163
|
+
logger.info("JIRA: poller stopped");
|
|
164
|
+
};
|
|
165
|
+
process.on("SIGTERM", shutdown);
|
|
166
|
+
process.on("SIGINT", shutdown);
|
|
167
|
+
}
|
|
168
|
+
export function stopJiraPoller() {
|
|
169
|
+
if (pollTimer) {
|
|
170
|
+
clearInterval(pollTimer);
|
|
171
|
+
pollTimer = null;
|
|
172
|
+
}
|
|
173
|
+
if (clearTimer) {
|
|
174
|
+
clearInterval(clearTimer);
|
|
175
|
+
clearTimer = null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
export function getPollerStatus() {
|
|
179
|
+
return {
|
|
180
|
+
running: pollTimer !== null,
|
|
181
|
+
processedCount: processedStates.size,
|
|
182
|
+
isPolling,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JIRA ticket processor.
|
|
3
|
+
*
|
|
4
|
+
* Extracts instructions + attachments from a JIRA issue, runs the agent loop
|
|
5
|
+
* to edit the site, then posts results back to the ticket.
|
|
6
|
+
*/
|
|
7
|
+
import type { Logger } from "../logger.ts";
|
|
8
|
+
import type { JiraConfig, JiraIssue, JiraProcessingResult, JiraProcessingEntry, JiraUser } from "./jira-types.ts";
|
|
9
|
+
/**
|
|
10
|
+
* Controls what processJiraTicket does:
|
|
11
|
+
* - "review": agent reads the ticket without tools, posts a plan-or-questions
|
|
12
|
+
* comment, does not change the site, does not transition the issue.
|
|
13
|
+
* - "execute": agent runs with the full tool set, applies edits, transitions
|
|
14
|
+
* the issue to the preview status (or directly to done if
|
|
15
|
+
* autoPublish is enabled).
|
|
16
|
+
* - "publish": publish the session's draft, transition to done.
|
|
17
|
+
*/
|
|
18
|
+
export type JiraProcessingMode = "review" | "execute" | "publish";
|
|
19
|
+
export declare function getProcessingStatus(): {
|
|
20
|
+
queue: JiraProcessingEntry[];
|
|
21
|
+
recent: JiraProcessingResult[];
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Convert Atlassian Document Format to plain text.
|
|
25
|
+
* Handles the common node types: paragraph, heading, text, hardBreak, listItem, codeBlock.
|
|
26
|
+
*/
|
|
27
|
+
export declare function adfToPlainText(node: unknown): string;
|
|
28
|
+
/**
|
|
29
|
+
* Render the ticket's comment thread as plain text labelled by role
|
|
30
|
+
* (Agent / Reporter). Returns "" when there are no comments.
|
|
31
|
+
*
|
|
32
|
+
* Long bodies are truncated to keep token usage bounded — the reporter's
|
|
33
|
+
* latest reply is what matters most, and we don't want old agent formatting
|
|
34
|
+
* boilerplate to crowd out the real task context.
|
|
35
|
+
*/
|
|
36
|
+
export declare function buildCommentHistory(issue: JiraIssue, agentAccountId: string | undefined): string;
|
|
37
|
+
export declare function processJiraTicket(options: {
|
|
38
|
+
issueKey: string;
|
|
39
|
+
config: JiraConfig;
|
|
40
|
+
/** Which stage of the workflow to run. Defaults to "execute" for backward compat. */
|
|
41
|
+
mode?: JiraProcessingMode;
|
|
42
|
+
generatedImageDir: string;
|
|
43
|
+
orchestratorPublicOrigin: string;
|
|
44
|
+
sitePublicOrigin: string;
|
|
45
|
+
logger: Logger;
|
|
46
|
+
assignBackTo?: JiraUser;
|
|
47
|
+
}): Promise<JiraProcessingResult>;
|
|
48
|
+
export type ReviewDecision = {
|
|
49
|
+
decision: "proceed" | "questions";
|
|
50
|
+
plan: string[];
|
|
51
|
+
questions?: string[];
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Extract the review JSON from raw agent output. Tolerant of: leading/trailing
|
|
55
|
+
* prose, ```json fences, and stray <thinking> tags.
|
|
56
|
+
*/
|
|
57
|
+
export declare function parseReviewDecision(raw: string): ReviewDecision;
|
|
58
|
+
/**
|
|
59
|
+
* Count how many prior review-mode comments the agent has posted on this issue,
|
|
60
|
+
* so we can cap the number of review passes per ticket.
|
|
61
|
+
*/
|
|
62
|
+
export declare function countAgentReviewComments(issue: JiraIssue, agentAccountId: string | undefined): number;
|
|
63
|
+
/**
|
|
64
|
+
* Decide whether a comment was authored by our agent.
|
|
65
|
+
*
|
|
66
|
+
* Looks at both the author's accountId AND the body shape. This matters because
|
|
67
|
+
* in solo/dev Jira tenants the reporter and agent share a single human's
|
|
68
|
+
* accountId — filtering by accountId alone would discard every reporter reply
|
|
69
|
+
* as a self-loop and the workflow would stall.
|
|
70
|
+
*/
|
|
71
|
+
export declare function isAgentAuthoredComment(body: string, authorAccountId: string | undefined, agentAccountId: string | undefined): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Strip internal reasoning artifacts from an agent summary before posting to Jira.
|
|
74
|
+
* Claude occasionally emits <thinking>…</thinking> and similar XML-style reasoning
|
|
75
|
+
* tags as plain text, plus interstitial tool-calling narration accumulates across
|
|
76
|
+
* turns. Drop the noise, collapse whitespace.
|
|
77
|
+
*/
|
|
78
|
+
export declare function cleanAgentSummary(raw: string): string;
|
|
79
|
+
export type SiteResolution = {
|
|
80
|
+
siteId: string;
|
|
81
|
+
via: "text-match" | "single-site" | "env-fallback" | "default-fallback";
|
|
82
|
+
} | {
|
|
83
|
+
ambiguous: true;
|
|
84
|
+
candidates: Array<{
|
|
85
|
+
id: string;
|
|
86
|
+
name?: string;
|
|
87
|
+
}>;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Decide which site a ticket targets.
|
|
91
|
+
*
|
|
92
|
+
* Priority:
|
|
93
|
+
* 1. Ticket text (summary + description + reporter comments) uniquely names a
|
|
94
|
+
* registered site → use it. Reporter comments are included so that a reply
|
|
95
|
+
* to the "Which site?" clarification comment ("use avocado-stories") is
|
|
96
|
+
* honoured instead of looping the same question.
|
|
97
|
+
* 2. Multiple registered sites → ambiguous, ask the reporter.
|
|
98
|
+
* (JIRA_SITE_ID is intentionally NOT consulted here — it's a fallback, not a lock,
|
|
99
|
+
* so adding a second site doesn't silently keep editing the first one.)
|
|
100
|
+
* 3. Exactly one registered site → use it.
|
|
101
|
+
* 4. No registered sites → use `config.siteId` (JIRA_SITE_ID env or built-in default).
|
|
102
|
+
*/
|
|
103
|
+
export declare function resolveSiteForTicket(issue: JiraIssue, config: JiraConfig): SiteResolution;
|