ai-dev-requirements 0.1.9 → 0.1.10
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/README.md +68 -14
- package/README.zh-CN.md +68 -14
- package/dist/index.cjs +234 -15
- package/dist/index.mjs +234 -15
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/skills/dev-workflow/SKILL.md +186 -123
- package/skills/dev-workflow/references/task-types.md +148 -97
- package/skills/dev-workflow/references/templates/code-dev-task.md +34 -21
- package/skills/dev-workflow/references/templates/code-fix-task.md +34 -20
- package/skills/dev-workflow/references/templates/code-refactor-task.md +34 -22
- package/skills/dev-workflow/references/templates/doc-write-task.md +33 -21
- package/skills/dev-workflow/references/templates/research-task.md +34 -22
- package/skills/dev-workflow/references/templates/test-task.md +33 -22
- package/skills/dev-workflow/references/workflow.md +290 -118
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[中文](./README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
An agent harness workflow for AI coding tools, enabling controlled requirement intake, planning, gated execution, verification, review, and handoff.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -11,13 +11,13 @@ A parallel-task development framework for AI coding tools, enabling end-to-end d
|
|
|
11
11
|
| Deliverable | Description |
|
|
12
12
|
|-------------|-------------|
|
|
13
13
|
| **Requirements MCP Server** (`src/`) | MCP server for fetching requirements, with built-in ONES adapter. Installable via npm. |
|
|
14
|
-
| **
|
|
14
|
+
| **Agent Harness Workflow Skill** (`skills/dev-workflow/`) | Self-contained agent harness skill. Install it to run requirement intake, planning, gated execution, verification, review, and handoff. |
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
18
18
|
## Quick Start
|
|
19
19
|
|
|
20
|
-
### 1. Install
|
|
20
|
+
### 1. Install Agent Harness Workflow Skill
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
23
|
npx skills add daguanren21/ai-dev-workflow
|
|
@@ -29,9 +29,61 @@ Install to a specific agent with `-a`:
|
|
|
29
29
|
npx skills add daguanren21/ai-dev-workflow -a claude-code
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
Once installed, AI coding tools will automatically use the dev-workflow
|
|
32
|
+
Once installed, AI coding tools will automatically use the dev-workflow harness to govern the full development process.
|
|
33
33
|
|
|
34
|
-
### 2. Install
|
|
34
|
+
### 2. Install For Codex
|
|
35
|
+
|
|
36
|
+
Codex loads skills from `$CODEX_HOME/skills`. If `CODEX_HOME` is not set, the default is `~/.codex`.
|
|
37
|
+
|
|
38
|
+
From this repository:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
mkdir -p "${CODEX_HOME:-$HOME/.codex}/skills/dev-workflow"
|
|
42
|
+
cp -R skills/dev-workflow/* "${CODEX_HOME:-$HOME/.codex}/skills/dev-workflow/"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
For local development, use a symlink instead so Codex picks up edits from this checkout after restart:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
mkdir -p "${CODEX_HOME:-$HOME/.codex}/skills"
|
|
49
|
+
ln -s "$(pwd)/skills/dev-workflow" "${CODEX_HOME:-$HOME/.codex}/skills/dev-workflow"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Restart Codex after installing or updating the skill.
|
|
53
|
+
|
|
54
|
+
### 3. Trigger The Harness
|
|
55
|
+
|
|
56
|
+
The skill can be triggered automatically when the task looks like AI-assisted development work: requirement intake, issue implementation, task planning, gated execution, verification, review, or handoff.
|
|
57
|
+
|
|
58
|
+
You can also trigger it explicitly:
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
Use the dev-workflow harness to implement this requirement: <requirement text or ticket id>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```text
|
|
65
|
+
Use the dev-workflow harness. Read ONES-123, write the plan first, then wait for confirmation before implementation.
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```text
|
|
69
|
+
Use the dev-workflow harness for this GitHub issue: <issue url>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
When the harness is active, the agent should announce:
|
|
73
|
+
|
|
74
|
+
```text
|
|
75
|
+
I'm using the dev-workflow harness to drive this development task.
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
By default, the harness generates user stories and an implementation plan before writing code, then pauses for confirmation. You do not need to repeat "write the plan first" in every prompt. Say so only when you want to bypass that gate.
|
|
79
|
+
|
|
80
|
+
Expected flow:
|
|
81
|
+
|
|
82
|
+
```text
|
|
83
|
+
Intake -> Context Load -> Normalize -> Harness Plan -> Coverage Validation -> Gated Execution -> Verification -> Review -> Handoff
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 4. Install MCP Server (Optional)
|
|
35
87
|
|
|
36
88
|
If you use ONES for requirement management:
|
|
37
89
|
|
|
@@ -75,7 +127,7 @@ Add to your `.mcp.json`:
|
|
|
75
127
|
}
|
|
76
128
|
```
|
|
77
129
|
|
|
78
|
-
###
|
|
130
|
+
### 5. Add Companion MCP Servers (Optional)
|
|
79
131
|
|
|
80
132
|
Requirements are not limited to ONES. Pair with official MCP servers for GitHub / Jira / Figma:
|
|
81
133
|
|
|
@@ -108,22 +160,24 @@ Requirements are not limited to ONES. Pair with official MCP servers for GitHub
|
|
|
108
160
|
|
|
109
161
|
---
|
|
110
162
|
|
|
111
|
-
##
|
|
163
|
+
## Agent Harness Workflow Skill
|
|
112
164
|
|
|
113
|
-
A self-contained AI-assisted
|
|
165
|
+
A self-contained AI-assisted agent harness skill that governs the full development lifecycle:
|
|
114
166
|
|
|
115
167
|
```
|
|
116
|
-
|
|
168
|
+
Intake -> Context Load -> Normalize -> Harness Plan -> Coverage Validation -> Gated Execution -> Verification -> Review -> Handoff
|
|
117
169
|
```
|
|
118
170
|
|
|
171
|
+
The harness follows a feedforward + feedback model: it guides the agent with plans, artifacts, and task boundaries, then uses deterministic gates such as lint, typecheck, build, tests, and review as backpressure before handoff.
|
|
172
|
+
|
|
119
173
|
Skill directory structure:
|
|
120
174
|
|
|
121
175
|
```
|
|
122
176
|
skills/dev-workflow/
|
|
123
|
-
├── SKILL.md # Skill entry (YAML frontmatter +
|
|
177
|
+
├── SKILL.md # Skill entry (YAML frontmatter + harness definition)
|
|
124
178
|
└── references/
|
|
125
|
-
├── workflow.md #
|
|
126
|
-
├── task-types.md #
|
|
179
|
+
├── workflow.md # Agent harness lifecycle
|
|
180
|
+
├── task-types.md # Harness task types, scheduler modes, declaration syntax
|
|
127
181
|
├── service-transform.md # Service-layer transform pattern for Mock/API adaptation
|
|
128
182
|
└── templates/ # Task declaration templates
|
|
129
183
|
├── code-dev-task.md
|
|
@@ -140,10 +194,10 @@ skills/dev-workflow/
|
|
|
140
194
|
|
|
141
195
|
```
|
|
142
196
|
ai-dev-workflow/
|
|
143
|
-
├── skills/dev-workflow/ #
|
|
197
|
+
├── skills/dev-workflow/ # Agent Harness Workflow Skill (self-contained)
|
|
144
198
|
│ ├── SKILL.md
|
|
145
199
|
│ └── references/
|
|
146
|
-
│ ├── workflow.md
|
|
200
|
+
│ ├── workflow.md # Agent harness lifecycle
|
|
147
201
|
│ ├── task-types.md
|
|
148
202
|
│ ├── service-transform.md
|
|
149
203
|
│ └── templates/
|
package/README.zh-CN.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[English](./README.md)
|
|
4
4
|
|
|
5
|
-
一套面向 AI
|
|
5
|
+
一套面向 AI 编码工具的 agent harness 工作流,用于管控需求接入、计划、门禁执行、验证、审查和交付。
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
| 交付物 | 说明 |
|
|
12
12
|
|-------|------|
|
|
13
13
|
| **Requirements MCP Server** (`src/`) | 需求获取 MCP 服务,内置 ONES 适配器,可通过 npm 安装 |
|
|
14
|
-
| **
|
|
14
|
+
| **Agent Harness Workflow Skill** (`skills/dev-workflow/`) | 自包含的 AI agent harness 工作流 Skill,安装后即可跑通需求接入、计划、门禁执行、验证、审查和交付。 |
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
18
18
|
## 快速开始
|
|
19
19
|
|
|
20
|
-
### 1. 安装
|
|
20
|
+
### 1. 安装 Agent Harness Workflow Skill
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
23
|
npx skills add daguanren21/ai-dev-workflow
|
|
@@ -29,9 +29,61 @@ npx skills add daguanren21/ai-dev-workflow
|
|
|
29
29
|
npx skills add daguanren21/ai-dev-workflow -a claude-code
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
安装后,AI 编码工具会自动识别并使用 dev-workflow
|
|
32
|
+
安装后,AI 编码工具会自动识别并使用 dev-workflow harness 管控完整开发流程。
|
|
33
33
|
|
|
34
|
-
### 2.
|
|
34
|
+
### 2. 安装到 Codex
|
|
35
|
+
|
|
36
|
+
Codex 从 `$CODEX_HOME/skills` 加载 skills。未设置 `CODEX_HOME` 时,默认目录是 `~/.codex`。
|
|
37
|
+
|
|
38
|
+
从当前仓库安装:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
mkdir -p "${CODEX_HOME:-$HOME/.codex}/skills/dev-workflow"
|
|
42
|
+
cp -R skills/dev-workflow/* "${CODEX_HOME:-$HOME/.codex}/skills/dev-workflow/"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
如果是在本地开发这个 skill,建议使用软链接,这样更新当前仓库后重启 Codex 即可生效:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
mkdir -p "${CODEX_HOME:-$HOME/.codex}/skills"
|
|
49
|
+
ln -s "$(pwd)/skills/dev-workflow" "${CODEX_HOME:-$HOME/.codex}/skills/dev-workflow"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
安装或更新后需要重启 Codex。
|
|
53
|
+
|
|
54
|
+
### 3. 触发 Harness
|
|
55
|
+
|
|
56
|
+
当任务看起来是 AI 辅助开发工作时,skill 可以自动触发,例如:需求接入、issue 实现、任务规划、门禁执行、验证、审查或交付。
|
|
57
|
+
|
|
58
|
+
也可以显式触发:
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
使用 dev-workflow harness 实现这个需求:<需求文本或工单号>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```text
|
|
65
|
+
使用 dev-workflow harness。读取 ONES-123,先写计划,确认后再实现。
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```text
|
|
69
|
+
使用 dev-workflow harness 处理这个 GitHub issue:<issue url>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Harness 生效时,agent 应该先声明:
|
|
73
|
+
|
|
74
|
+
```text
|
|
75
|
+
I'm using the dev-workflow harness to drive this development task.
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
默认情况下,harness 会先生成 user stories 和 implementation plan,然后暂停等待确认,再开始写代码。你不需要每次重复“先写计划再实现”。只有想跳过这个门禁时,才需要明确说明。
|
|
79
|
+
|
|
80
|
+
预期流程:
|
|
81
|
+
|
|
82
|
+
```text
|
|
83
|
+
需求接入 → 上下文加载 → 需求规范化 → Harness 计划 → 覆盖校验 → 门禁执行 → 验证 → 审查 → 交付
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 4. 安装 MCP Server(可选)
|
|
35
87
|
|
|
36
88
|
如果使用 ONES 进行需求管理:
|
|
37
89
|
|
|
@@ -75,7 +127,7 @@ npm install -g ai-dev-requirements
|
|
|
75
127
|
}
|
|
76
128
|
```
|
|
77
129
|
|
|
78
|
-
###
|
|
130
|
+
### 5. 搭配其他 MCP Server(可选)
|
|
79
131
|
|
|
80
132
|
需求不限于 ONES,可搭配官方 MCP Server 获取 GitHub / Jira / Figma 资源:
|
|
81
133
|
|
|
@@ -108,22 +160,24 @@ npm install -g ai-dev-requirements
|
|
|
108
160
|
|
|
109
161
|
---
|
|
110
162
|
|
|
111
|
-
##
|
|
163
|
+
## Agent Harness Workflow Skill
|
|
112
164
|
|
|
113
|
-
自包含的 AI
|
|
165
|
+
自包含的 AI 辅助 agent harness 工作流 Skill,安装后自动管控完整开发生命周期:
|
|
114
166
|
|
|
115
167
|
```
|
|
116
|
-
|
|
168
|
+
需求接入 → 上下文加载 → 需求规范化 → Harness 计划 → 覆盖校验 → 门禁执行 → 验证 → 审查 → 交付
|
|
117
169
|
```
|
|
118
170
|
|
|
171
|
+
这个 harness 遵循“前馈 + 反馈”模型:先用计划、产物和任务边界引导 agent,再用 lint、typecheck、build、tests、review 等确定性门禁形成反压,合格后再交付。
|
|
172
|
+
|
|
119
173
|
Skill 目录结构:
|
|
120
174
|
|
|
121
175
|
```
|
|
122
176
|
skills/dev-workflow/
|
|
123
|
-
├── SKILL.md # Skill 入口(YAML frontmatter +
|
|
177
|
+
├── SKILL.md # Skill 入口(YAML frontmatter + harness 定义)
|
|
124
178
|
└── references/
|
|
125
|
-
├── workflow.md #
|
|
126
|
-
├── task-types.md #
|
|
179
|
+
├── workflow.md # Agent harness 生命周期
|
|
180
|
+
├── task-types.md # Harness 任务类型、调度模式、声明语法
|
|
127
181
|
├── service-transform.md # Service 层 Transform 适配模式
|
|
128
182
|
└── templates/ # 任务声明模板
|
|
129
183
|
├── code-dev-task.md
|
|
@@ -140,10 +194,10 @@ skills/dev-workflow/
|
|
|
140
194
|
|
|
141
195
|
```
|
|
142
196
|
ai-dev-workflow/
|
|
143
|
-
├── skills/dev-workflow/ #
|
|
197
|
+
├── skills/dev-workflow/ # Agent Harness Workflow Skill(自包含工作流)
|
|
144
198
|
│ ├── SKILL.md
|
|
145
199
|
│ └── references/
|
|
146
|
-
│ ├── workflow.md
|
|
200
|
+
│ ├── workflow.md # Agent harness 生命周期
|
|
147
201
|
│ ├── task-types.md
|
|
148
202
|
│ ├── service-transform.md
|
|
149
203
|
│ └── templates/
|
package/dist/index.cjs
CHANGED
|
@@ -93,6 +93,9 @@ const TASK_DETAIL_QUERY = `
|
|
|
93
93
|
query Task($key: Key) {
|
|
94
94
|
task(key: $key) {
|
|
95
95
|
key uuid number name
|
|
96
|
+
description
|
|
97
|
+
descriptionText
|
|
98
|
+
desc_rich: description
|
|
96
99
|
issueType { uuid name }
|
|
97
100
|
status { uuid name category }
|
|
98
101
|
priority { value }
|
|
@@ -322,7 +325,120 @@ function getSetCookies(response) {
|
|
|
322
325
|
const raw = response.headers.get("set-cookie");
|
|
323
326
|
return raw ? [raw] : [];
|
|
324
327
|
}
|
|
325
|
-
function
|
|
328
|
+
function extractWikiPageUuidsFromText(text) {
|
|
329
|
+
if (!text) return [];
|
|
330
|
+
const uuids = /* @__PURE__ */ new Set();
|
|
331
|
+
for (const pattern of [/\/page\/([\w-]+)/g, /page=([\w-]+)/g]) for (const match of text.matchAll(pattern)) if (match[1]) uuids.add(match[1]);
|
|
332
|
+
return [...uuids];
|
|
333
|
+
}
|
|
334
|
+
function htmlToPlainText(html) {
|
|
335
|
+
return html.replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<[^>]+>/g, "").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\n{3,}/g, "\n\n").trim();
|
|
336
|
+
}
|
|
337
|
+
function getTaskDetailText(task) {
|
|
338
|
+
return task.descriptionText?.trim() || htmlToPlainText(task.desc_rich ?? task.description ?? "");
|
|
339
|
+
}
|
|
340
|
+
function isRecord(value) {
|
|
341
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
342
|
+
}
|
|
343
|
+
function parseJsonRecord(value) {
|
|
344
|
+
try {
|
|
345
|
+
const parsed = JSON.parse(value);
|
|
346
|
+
return isRecord(parsed) ? parsed : null;
|
|
347
|
+
} catch {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
function asWikiBlocks(value) {
|
|
352
|
+
if (!Array.isArray(value)) return [];
|
|
353
|
+
return value.filter(isRecord);
|
|
354
|
+
}
|
|
355
|
+
function renderWikiTextRuns(value) {
|
|
356
|
+
if (!Array.isArray(value)) return "";
|
|
357
|
+
return value.map((run) => {
|
|
358
|
+
if (!isRecord(run)) return "";
|
|
359
|
+
const attributes = isRecord(run.attributes) ? run.attributes : {};
|
|
360
|
+
const insert = typeof run.insert === "string" ? run.insert.replace(/\u00A0/g, " ") : "";
|
|
361
|
+
const link = typeof attributes.link === "string" ? attributes.link : "";
|
|
362
|
+
if (link && insert.trim()) return `[${insert}](${link})`;
|
|
363
|
+
const taskName = typeof attributes.taskName === "string" ? attributes.taskName : "";
|
|
364
|
+
if (link && taskName) return `[${taskName}](${link})`;
|
|
365
|
+
return insert;
|
|
366
|
+
}).join("").replace(/\n{3,}/g, "\n\n").trim();
|
|
367
|
+
}
|
|
368
|
+
function renderWikiHeading(text, heading) {
|
|
369
|
+
if (!heading) return text;
|
|
370
|
+
const level = Math.min(Math.max(Math.trunc(heading), 1), 6);
|
|
371
|
+
return `${"#".repeat(level)} ${text}`;
|
|
372
|
+
}
|
|
373
|
+
function getWikiImageSource(block) {
|
|
374
|
+
const embedData = isRecord(block.embedData) ? block.embedData : {};
|
|
375
|
+
return typeof embedData.src === "string" ? embedData.src.trim() : "";
|
|
376
|
+
}
|
|
377
|
+
function renderWikiEmbed(block, context) {
|
|
378
|
+
if (block.embedType === "image") {
|
|
379
|
+
const src = getWikiImageSource(block);
|
|
380
|
+
if (src && !context.imageSources.includes(src)) context.imageSources.push(src);
|
|
381
|
+
return src ? `[Image: ${src}]` : "[Image]";
|
|
382
|
+
}
|
|
383
|
+
return block.embedType ? `[Embed: ${block.embedType}]` : "";
|
|
384
|
+
}
|
|
385
|
+
function escapeWikiTableCell(value) {
|
|
386
|
+
return value.replace(/\|/g, "\\|").replace(/[ \t]*\n+[ \t]*/g, " ").trim();
|
|
387
|
+
}
|
|
388
|
+
function renderWikiCell(value, document, context) {
|
|
389
|
+
const blocks = asWikiBlocks(value);
|
|
390
|
+
if (!blocks.length) return "";
|
|
391
|
+
return blocks.map((block) => renderWikiBlock(block, document, context)).filter(Boolean).join(" ").replace(/[ \t]*\n+[ \t]*/g, " ").trim();
|
|
392
|
+
}
|
|
393
|
+
function renderWikiTable(block, document, context) {
|
|
394
|
+
const cols = typeof block.cols === "number" && block.cols > 0 ? Math.trunc(block.cols) : 0;
|
|
395
|
+
const children = Array.isArray(block.children) ? block.children.filter((child) => typeof child === "string") : [];
|
|
396
|
+
if (!cols || !children.length) return "";
|
|
397
|
+
const rows = [];
|
|
398
|
+
for (let index = 0; index < children.length; index += cols) {
|
|
399
|
+
const cells = children.slice(index, index + cols).map((childId) => escapeWikiTableCell(renderWikiCell(document[childId], document, context)));
|
|
400
|
+
while (cells.length < cols) cells.push("");
|
|
401
|
+
rows.push(`| ${cells.join(" | ")} |`);
|
|
402
|
+
}
|
|
403
|
+
if (rows.length > 1) rows.splice(1, 0, `| ${Array.from({ length: cols }, () => "---").join(" | ")} |`);
|
|
404
|
+
return rows.join("\n");
|
|
405
|
+
}
|
|
406
|
+
function renderWikiBlock(block, document, context) {
|
|
407
|
+
if (block.type === "table") return renderWikiTable(block, document, context);
|
|
408
|
+
if (block.type === "embed") return renderWikiEmbed(block, context);
|
|
409
|
+
const text = renderWikiTextRuns(block.text);
|
|
410
|
+
if (!text) return "";
|
|
411
|
+
if (block.type === "list") {
|
|
412
|
+
const level = typeof block.level === "number" ? Math.max(Math.trunc(block.level), 1) : 1;
|
|
413
|
+
return `${" ".repeat(level - 1)}${block.ordered ? `${block.start ?? 1}.` : "-"} ${text}`;
|
|
414
|
+
}
|
|
415
|
+
return renderWikiHeading(text, block.heading);
|
|
416
|
+
}
|
|
417
|
+
function renderWikiContent(content, context = { imageSources: [] }) {
|
|
418
|
+
const trimmed = content.trim();
|
|
419
|
+
if (!trimmed) return "";
|
|
420
|
+
const document = parseJsonRecord(trimmed);
|
|
421
|
+
if (!document) return trimmed;
|
|
422
|
+
if (!("blocks" in document)) return trimmed;
|
|
423
|
+
return asWikiBlocks(document.blocks).map((block) => renderWikiBlock(block, document, context)).filter(Boolean).join("\n\n").replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
424
|
+
}
|
|
425
|
+
function mimeTypeFromFileName(fileName) {
|
|
426
|
+
const normalized = fileName.toLowerCase();
|
|
427
|
+
if (normalized.endsWith(".jpg") || normalized.endsWith(".jpeg")) return "image/jpeg";
|
|
428
|
+
if (normalized.endsWith(".gif")) return "image/gif";
|
|
429
|
+
if (normalized.endsWith(".webp")) return "image/webp";
|
|
430
|
+
if (normalized.endsWith(".svg")) return "image/svg+xml";
|
|
431
|
+
return "image/png";
|
|
432
|
+
}
|
|
433
|
+
function attachmentNameFromPath(path) {
|
|
434
|
+
const name = path.split("/").pop() || path;
|
|
435
|
+
try {
|
|
436
|
+
return decodeURIComponent(name);
|
|
437
|
+
} catch {
|
|
438
|
+
return name;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
function toRequirement(task, description = "", attachments = []) {
|
|
326
442
|
return {
|
|
327
443
|
id: task.uuid,
|
|
328
444
|
source: "ones",
|
|
@@ -337,7 +453,7 @@ function toRequirement(task, description = "") {
|
|
|
337
453
|
createdAt: "",
|
|
338
454
|
updatedAt: "",
|
|
339
455
|
dueDate: null,
|
|
340
|
-
attachments
|
|
456
|
+
attachments,
|
|
341
457
|
raw: task
|
|
342
458
|
};
|
|
343
459
|
}
|
|
@@ -617,12 +733,51 @@ var OnesAdapter = class extends BaseAdapter {
|
|
|
617
733
|
* Fetch wiki page content via REST API.
|
|
618
734
|
* Endpoint: /wiki/api/wiki/team/{teamUuid}/online_page/{wikiUuid}/content
|
|
619
735
|
*/
|
|
736
|
+
async fetchWikiPageDetail(wikiUuid) {
|
|
737
|
+
const session = await this.login();
|
|
738
|
+
const url = `${this.config.apiBase}/wiki/api/wiki/team/${session.teamUuid}/page/${wikiUuid}/detail`;
|
|
739
|
+
const response = await fetch(url, { headers: { Authorization: `Bearer ${session.accessToken}` } });
|
|
740
|
+
if (!response.ok) return {};
|
|
741
|
+
return response.json();
|
|
742
|
+
}
|
|
743
|
+
buildWikiImageUrl(session, refUuid, source, token) {
|
|
744
|
+
const encodedRefUuid = encodeURIComponent(refUuid);
|
|
745
|
+
const encodedSource = source.split("/").map((part) => encodeURIComponent(part)).join("/");
|
|
746
|
+
const encodedToken = encodeURIComponent(token);
|
|
747
|
+
return `${this.config.apiBase}/wiki/api/wiki/editor/${session.teamUuid}/${encodedRefUuid}/resources/${encodedSource}?token=${encodedToken}`;
|
|
748
|
+
}
|
|
620
749
|
async fetchWikiContent(wikiUuid) {
|
|
621
750
|
const session = await this.login();
|
|
622
751
|
const url = `${this.config.apiBase}/wiki/api/wiki/team/${session.teamUuid}/online_page/${wikiUuid}/content`;
|
|
623
752
|
const response = await fetch(url, { headers: { Authorization: `Bearer ${session.accessToken}` } });
|
|
624
|
-
if (!response.ok) return
|
|
625
|
-
|
|
753
|
+
if (!response.ok) return {
|
|
754
|
+
content: "",
|
|
755
|
+
attachments: []
|
|
756
|
+
};
|
|
757
|
+
const data = await response.json();
|
|
758
|
+
const renderContext = { imageSources: [] };
|
|
759
|
+
const content = renderWikiContent(typeof data.content === "string" ? data.content : "", renderContext);
|
|
760
|
+
const token = typeof data.token === "string" ? data.token : "";
|
|
761
|
+
if (!renderContext.imageSources.length || !token) return {
|
|
762
|
+
content,
|
|
763
|
+
attachments: []
|
|
764
|
+
};
|
|
765
|
+
const detail = await this.fetchWikiPageDetail(wikiUuid);
|
|
766
|
+
const refUuid = typeof detail.ref_uuid === "string" ? detail.ref_uuid : "";
|
|
767
|
+
if (!refUuid) return {
|
|
768
|
+
content,
|
|
769
|
+
attachments: []
|
|
770
|
+
};
|
|
771
|
+
return {
|
|
772
|
+
content,
|
|
773
|
+
attachments: renderContext.imageSources.map((source, index) => ({
|
|
774
|
+
id: `${wikiUuid}-image-${index + 1}`,
|
|
775
|
+
name: attachmentNameFromPath(source),
|
|
776
|
+
url: this.buildWikiImageUrl(session, refUuid, source, token),
|
|
777
|
+
mimeType: mimeTypeFromFileName(source),
|
|
778
|
+
size: 0
|
|
779
|
+
}))
|
|
780
|
+
};
|
|
626
781
|
}
|
|
627
782
|
/**
|
|
628
783
|
* Fetch a single task by UUID or number (e.g. "#95945" or "95945").
|
|
@@ -650,13 +805,27 @@ var OnesAdapter = class extends BaseAdapter {
|
|
|
650
805
|
}
|
|
651
806
|
const task = (await this.graphql(TASK_DETAIL_QUERY, { key: `task-${taskUuid}` }, "Task")).data?.task;
|
|
652
807
|
if (!task) throw new Error(`ONES: Task "${taskUuid}" not found`);
|
|
653
|
-
const
|
|
654
|
-
const
|
|
655
|
-
|
|
808
|
+
const wikiRefs = /* @__PURE__ */ new Map();
|
|
809
|
+
for (const wiki of task.relatedWikiPages ?? []) if (!wiki.errorMessage) wikiRefs.set(wiki.uuid, {
|
|
810
|
+
title: wiki.title,
|
|
811
|
+
uuid: wiki.uuid
|
|
812
|
+
});
|
|
813
|
+
const detailForLinkExtraction = [
|
|
814
|
+
task.description,
|
|
815
|
+
task.descriptionText,
|
|
816
|
+
task.desc_rich
|
|
817
|
+
].filter(Boolean).join("\n");
|
|
818
|
+
for (const wikiUuid of extractWikiPageUuidsFromText(detailForLinkExtraction)) if (!wikiRefs.has(wikiUuid)) wikiRefs.set(wikiUuid, {
|
|
819
|
+
title: `Wiki ${wikiUuid}`,
|
|
820
|
+
uuid: wikiUuid
|
|
821
|
+
});
|
|
822
|
+
const wikiContents = await Promise.all([...wikiRefs.values()].map(async (wiki) => {
|
|
823
|
+
const rendered = await this.fetchWikiContent(wiki.uuid);
|
|
656
824
|
return {
|
|
657
825
|
title: wiki.title,
|
|
658
826
|
uuid: wiki.uuid,
|
|
659
|
-
content
|
|
827
|
+
content: rendered.content,
|
|
828
|
+
attachments: rendered.attachments
|
|
660
829
|
};
|
|
661
830
|
}));
|
|
662
831
|
const parts = [];
|
|
@@ -695,7 +864,18 @@ var OnesAdapter = class extends BaseAdapter {
|
|
|
695
864
|
else parts.push("(No content available)");
|
|
696
865
|
}
|
|
697
866
|
}
|
|
698
|
-
|
|
867
|
+
const detailText = getTaskDetailText(task);
|
|
868
|
+
const hasWikiContent = wikiContents.some((wiki) => wiki.content.trim());
|
|
869
|
+
if (detailText && !hasWikiContent) {
|
|
870
|
+
parts.push("");
|
|
871
|
+
parts.push("---");
|
|
872
|
+
parts.push("");
|
|
873
|
+
parts.push("## Requirement Detail");
|
|
874
|
+
parts.push("");
|
|
875
|
+
parts.push(detailText);
|
|
876
|
+
}
|
|
877
|
+
const wikiAttachments = wikiContents.flatMap((wiki) => wiki.attachments);
|
|
878
|
+
return toRequirement(task, parts.join("\n"), wikiAttachments);
|
|
699
879
|
}
|
|
700
880
|
/**
|
|
701
881
|
* Search tasks assigned to current user via GraphQL.
|
|
@@ -1116,7 +1296,7 @@ const GetIssueDetailSchema = zod_v4.z.object({
|
|
|
1116
1296
|
* Download an image from URL and return as base64 data URI.
|
|
1117
1297
|
* Returns null if download fails.
|
|
1118
1298
|
*/
|
|
1119
|
-
async function downloadImageAsBase64(url) {
|
|
1299
|
+
async function downloadImageAsBase64$1(url) {
|
|
1120
1300
|
try {
|
|
1121
1301
|
const res = await fetch(url, { redirect: "follow" });
|
|
1122
1302
|
if (!res.ok) return null;
|
|
@@ -1142,7 +1322,7 @@ async function handleGetIssueDetail(input, adapters, defaultSource) {
|
|
|
1142
1322
|
if (!adapter) throw new Error(`Source "${sourceType}" is not configured. Available: ${[...adapters.keys()].join(", ")}`);
|
|
1143
1323
|
const detail = await adapter.getIssueDetail({ issueId: input.issueId });
|
|
1144
1324
|
const imageUrls = detail.descriptionRich ? extractImageUrls(detail.descriptionRich) : [];
|
|
1145
|
-
const imageResults = await Promise.all(imageUrls.map((url) => downloadImageAsBase64(url)));
|
|
1325
|
+
const imageResults = await Promise.all(imageUrls.map((url) => downloadImageAsBase64$1(url)));
|
|
1146
1326
|
const content = [{
|
|
1147
1327
|
type: "text",
|
|
1148
1328
|
text: formatIssueDetail(detail)
|
|
@@ -1228,15 +1408,54 @@ const GetRequirementSchema = zod_v4.z.object({
|
|
|
1228
1408
|
id: zod_v4.z.string().describe("The requirement/issue ID"),
|
|
1229
1409
|
source: zod_v4.z.string().optional().describe("Source to fetch from. If omitted, uses the default source.")
|
|
1230
1410
|
});
|
|
1411
|
+
async function downloadImageAsBase64(url, fallbackMimeType = "image/png") {
|
|
1412
|
+
try {
|
|
1413
|
+
const res = await fetch(url, { redirect: "follow" });
|
|
1414
|
+
if (!res.ok) return null;
|
|
1415
|
+
const mimeType = (res.headers.get("content-type") ?? fallbackMimeType).split(";")[0].trim() || fallbackMimeType;
|
|
1416
|
+
return {
|
|
1417
|
+
base64: Buffer.from(await res.arrayBuffer()).toString("base64"),
|
|
1418
|
+
mimeType
|
|
1419
|
+
};
|
|
1420
|
+
} catch {
|
|
1421
|
+
return null;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
function isImageAttachment(attachment) {
|
|
1425
|
+
if (attachment.mimeType.startsWith("image/")) return true;
|
|
1426
|
+
return /\.(?:png|jpe?g|gif|webp|svg)$/i.test(attachment.url);
|
|
1427
|
+
}
|
|
1428
|
+
function displayAttachmentUrl(url) {
|
|
1429
|
+
try {
|
|
1430
|
+
const parsed = new URL(url);
|
|
1431
|
+
parsed.search = "";
|
|
1432
|
+
parsed.hash = "";
|
|
1433
|
+
return parsed.toString();
|
|
1434
|
+
} catch {
|
|
1435
|
+
return url.replace(/[?#].*$/, "");
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1231
1438
|
async function handleGetRequirement(input, adapters, defaultSource) {
|
|
1232
1439
|
const sourceType = input.source ?? defaultSource;
|
|
1233
1440
|
if (!sourceType) throw new Error("No source specified and no default source configured");
|
|
1234
1441
|
const adapter = adapters.get(sourceType);
|
|
1235
1442
|
if (!adapter) throw new Error(`Source "${sourceType}" is not configured. Available: ${[...adapters.keys()].join(", ")}`);
|
|
1236
|
-
|
|
1443
|
+
const requirement = await adapter.getRequirement({ id: input.id });
|
|
1444
|
+
const imageAttachments = requirement.attachments.filter(isImageAttachment);
|
|
1445
|
+
const imageResults = await Promise.all(imageAttachments.map((attachment) => downloadImageAsBase64(attachment.url, attachment.mimeType)));
|
|
1446
|
+
const content = [{
|
|
1237
1447
|
type: "text",
|
|
1238
|
-
text: formatRequirement(
|
|
1239
|
-
}]
|
|
1448
|
+
text: formatRequirement(requirement)
|
|
1449
|
+
}];
|
|
1450
|
+
for (const image of imageResults) {
|
|
1451
|
+
if (!image) continue;
|
|
1452
|
+
content.push({
|
|
1453
|
+
type: "image",
|
|
1454
|
+
data: image.base64,
|
|
1455
|
+
mimeType: image.mimeType
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
return { content };
|
|
1240
1459
|
}
|
|
1241
1460
|
function formatRequirement(req) {
|
|
1242
1461
|
const lines = [
|
|
@@ -1257,7 +1476,7 @@ function formatRequirement(req) {
|
|
|
1257
1476
|
lines.push("", "## Description", "", req.description || "_No description_");
|
|
1258
1477
|
if (req.attachments.length > 0) {
|
|
1259
1478
|
lines.push("", "## Attachments");
|
|
1260
|
-
for (const att of req.attachments) lines.push(`- [${att.name}](${att.url}) (${att.mimeType}, ${att.size} bytes)`);
|
|
1479
|
+
for (const att of req.attachments) lines.push(`- [${att.name}](${displayAttachmentUrl(att.url)}) (${att.mimeType}, ${att.size} bytes)`);
|
|
1261
1480
|
}
|
|
1262
1481
|
return lines.join("\n");
|
|
1263
1482
|
}
|