@kairyou/agent-tools 0.17.1 → 0.18.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/README.md
CHANGED
|
@@ -56,7 +56,7 @@ Usage:
|
|
|
56
56
|
|
|
57
57
|
### at-zentao
|
|
58
58
|
|
|
59
|
-
Work ZenTao (禅道) bugs/tasks end to end
|
|
59
|
+
Work ZenTao (禅道) bugs/tasks end to end, read linked story context, or log task hours; asks before committing and before writing back.
|
|
60
60
|
|
|
61
61
|
```bash
|
|
62
62
|
npx -y skills@latest add kairyou/agent-tools --skill at-zentao -g -y
|
|
@@ -67,7 +67,8 @@ Usage:
|
|
|
67
67
|
- `/at-zentao bugs` — list bugs assigned to you (the configured account); pick one or several (several = batch mode)
|
|
68
68
|
- `/at-zentao tasks` — same, for tasks
|
|
69
69
|
- `/at-zentao bug <id>` — work a specific bug
|
|
70
|
-
- `/at-zentao task <id
|
|
70
|
+
- `/at-zentao task <id> [request]` — work a specific task or log hours with a natural-language request
|
|
71
|
+
- `/at-zentao story <id>` — read requirement scope and acceptance context without writing back
|
|
71
72
|
|
|
72
73
|
Config: `~/.agent-tools/config.jsonc` → `"zentao": { "url", "account", "password" }`. The password can be entered directly or referenced with `"password": { "env": "ZENTAO_PASSWORD" }`; never paste credentials or tokens into chat.
|
|
73
74
|
|
package/README.zh-CN.md
CHANGED
|
@@ -56,7 +56,7 @@ npx -y skills@latest add kairyou/agent-tools --skill at-simplify -g -y
|
|
|
56
56
|
|
|
57
57
|
### at-zentao
|
|
58
58
|
|
|
59
|
-
读取禅道 bug/task
|
|
59
|
+
读取禅道 bug/task 并端到端处理, 补充关联 story 上下文, 或记录任务工时; 提交和回写前均需确认.
|
|
60
60
|
|
|
61
61
|
```bash
|
|
62
62
|
npx -y skills@latest add kairyou/agent-tools --skill at-zentao -g -y
|
|
@@ -67,7 +67,8 @@ npx -y skills@latest add kairyou/agent-tools --skill at-zentao -g -y
|
|
|
67
67
|
- `/at-zentao bugs` — 列出指派给你(配置的账号)的 bug, 挑一个或多个(多个 = 批量模式)
|
|
68
68
|
- `/at-zentao tasks` — 同上, 任务清单
|
|
69
69
|
- `/at-zentao bug <id>` — 直接处理指定 bug
|
|
70
|
-
- `/at-zentao task <id
|
|
70
|
+
- `/at-zentao task <id> [请求]` — 处理指定 task, 或用自然语言记录工时
|
|
71
|
+
- `/at-zentao story <id>` — 只读查看研发需求范围和验收信息
|
|
71
72
|
|
|
72
73
|
配置: `~/.agent-tools/config.jsonc` → `"zentao": { "url", "account", "password" }`. 密码可以直接填写, 也可以使用 `"password": { "env": "ZENTAO_PASSWORD" }` 引用环境变量; 不要在对话中粘贴凭据或 token.
|
|
73
74
|
|
|
@@ -14,7 +14,7 @@ agent-tools/
|
|
|
14
14
|
│ │ ├── at-review/ # Review local or hosted changes for bugs and regressions.
|
|
15
15
|
│ │ └── at-simplify/ # Reduce complexity and duplication in changes.
|
|
16
16
|
│ └── systems/ # Self-contained Skills for external systems.
|
|
17
|
-
│ └── at-zentao/ # Self-contained ZenTao
|
|
17
|
+
│ └── at-zentao/ # Self-contained ZenTao development and time-entry workflow.
|
|
18
18
|
├── docs/ # Advanced guides and contributor reference.
|
|
19
19
|
├── tools/ # Maintainer-only upstream sync and repository tooling.
|
|
20
20
|
└── scripts/ # Install, sync, validation, and maintenance scripts.
|
|
@@ -14,7 +14,7 @@ agent-tools/
|
|
|
14
14
|
│ │ ├── at-review/ # 审查本地或托管改动中的 bug 与回归风险.
|
|
15
15
|
│ │ └── at-simplify/ # 减少改动中的冗余和复杂度.
|
|
16
16
|
│ └── systems/ # 面向外部系统的自包含 Skills.
|
|
17
|
-
│ └── at-zentao/ #
|
|
17
|
+
│ └── at-zentao/ # 自包含的禅道研发事项与工时工作流.
|
|
18
18
|
├── docs/ # 高级指南和贡献者参考.
|
|
19
19
|
├── tools/ # 仅供维护者使用的上游同步和仓库工具.
|
|
20
20
|
└── scripts/ # 安装, 同步, 校验和仓库维护脚本.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: at-zentao
|
|
3
|
-
description: "Work ZenTao bugs/tasks end to end: fetch details,
|
|
4
|
-
argument-hint: "bug <id> | task <id> | bugs | tasks | export bug|task <id>"
|
|
3
|
+
description: "Work ZenTao bugs/tasks end to end, log task hours, or read a linked story as development context: fetch details, handle the user's request, verify changes, and ask before committing or writing back to ZenTao. Supports single items and sequential batches. Use when the user references ZenTao (禅道) bugs, tasks, stories, requirements, or task hours."
|
|
4
|
+
argument-hint: "bug <id> [request] | task <id> [request] | story <id> | bugs | tasks | export bug|task <id>"
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# ZenTao Bug/Task Workflow
|
|
@@ -70,6 +70,7 @@ node <skill-root>/scripts/zentao-cli.mjs list bugs
|
|
|
70
70
|
node <skill-root>/scripts/zentao-cli.mjs list tasks
|
|
71
71
|
node <skill-root>/scripts/zentao-cli.mjs get bug <id>
|
|
72
72
|
node <skill-root>/scripts/zentao-cli.mjs get task <id>
|
|
73
|
+
node <skill-root>/scripts/zentao-cli.mjs get story <id>
|
|
73
74
|
node <skill-root>/scripts/zentao-cli.mjs get bug <id> --download-dir <path>
|
|
74
75
|
```
|
|
75
76
|
|
|
@@ -84,6 +85,7 @@ confirmation steps below:
|
|
|
84
85
|
node <skill-root>/scripts/zentao-cli.mjs comment bug <id>
|
|
85
86
|
node <skill-root>/scripts/zentao-cli.mjs comment task <id>
|
|
86
87
|
node <skill-root>/scripts/zentao-cli.mjs resolve bug <id>
|
|
88
|
+
node <skill-root>/scripts/zentao-cli.mjs log-hours task <id>
|
|
87
89
|
node <skill-root>/scripts/zentao-cli.mjs finish task <id>
|
|
88
90
|
```
|
|
89
91
|
|
|
@@ -92,28 +94,43 @@ Input shapes:
|
|
|
92
94
|
```json
|
|
93
95
|
{"comment":"Root cause and result."}
|
|
94
96
|
{"resolution":"fixed","resolvedBuild":"trunk","comment":"Root cause and result, commit abc1234."}
|
|
97
|
+
{"date":"2026-08-11","consumed":2,"left":14,"work":"Implemented the first part of the task."}
|
|
95
98
|
{"currentConsumed":1.5,"realStarted":"2026-08-11 09:00:00","finishedDate":"2026-08-11 10:30:00"}
|
|
96
99
|
```
|
|
97
100
|
|
|
98
101
|
For `duplicate`, also pass `"duplicateBug": <id>`. Send JSON through stdin,
|
|
99
102
|
not as a command-line argument. The CLI handles UTF-8 form encoding and
|
|
100
103
|
computes a task's total consumed hours from its current ZenTao value.
|
|
104
|
+
`log-hours` defaults `date` to today, requires positive remaining hours,
|
|
105
|
+
and keeps the task open. Use `finish` when the task is complete.
|
|
101
106
|
|
|
102
107
|
## Usage
|
|
103
108
|
|
|
104
109
|
- `/at-zentao bug <id>` — handle a single bug.
|
|
105
110
|
- `/at-zentao task <id>` — handle a single task.
|
|
111
|
+
- `/at-zentao story <id>` — read requirement scope and acceptance context.
|
|
112
|
+
This mode is read-only; do not implement a Story status or comment workflow.
|
|
106
113
|
- `/at-zentao bugs` — list bugs assigned to the configured account; let the
|
|
107
114
|
user select one or more.
|
|
108
115
|
- `/at-zentao tasks` — list assigned tasks and let the user select.
|
|
109
116
|
- `/at-zentao export bug <id>` or `export task <id>` — create a read-only,
|
|
110
117
|
self-contained handoff bundle.
|
|
111
118
|
|
|
119
|
+
Treat text after an item id as a natural-language request. Phrases such as
|
|
120
|
+
`填工时`, `记录工时`, `log hours`, and `worklog` enter task time-entry mode;
|
|
121
|
+
users do not need to know the internal `log-hours` CLI command. Reuse any date,
|
|
122
|
+
hours, or work description already supplied instead of asking twice.
|
|
123
|
+
|
|
112
124
|
If a list response includes pager data showing more items than returned, tell
|
|
113
125
|
the user the shown and total counts. Do not silently imply the list is complete.
|
|
114
126
|
Do not browse through products/projects; start from assigned lists or an
|
|
115
127
|
explicit item id.
|
|
116
128
|
|
|
129
|
+
When a fetched Bug or Task has a positive `story` id, fetch that Story before
|
|
130
|
+
planning the implementation. Use its `spec` and `verify` fields to identify
|
|
131
|
+
scope, acceptance criteria, constraints, and non-goals. Keep the Bug or Task as
|
|
132
|
+
the unit of work: never change, close, activate, or comment on the Story.
|
|
133
|
+
|
|
117
134
|
## Per-item workflow
|
|
118
135
|
|
|
119
136
|
Follow these steps in order:
|
|
@@ -147,10 +164,16 @@ Bug resolutions are `fixed`, `notrepro`, `duplicate`, `bydesign`, `external`,
|
|
|
147
164
|
write-back comment is one sentence containing root cause, change summary, and
|
|
148
165
|
the commit hash when committed.
|
|
149
166
|
|
|
150
|
-
For tasks,
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
167
|
+
For tasks, ask whether to record the current work after the verified result.
|
|
168
|
+
For an incomplete task, collect the actual `consumed` hours and work date. When
|
|
169
|
+
the task has a numeric current `left`, suggest the new `left` by subtracting the
|
|
170
|
+
current entry and make that estimate editable in the confirmation; ask only
|
|
171
|
+
when no reliable suggestion is possible. Draft `work` from the verified result
|
|
172
|
+
when context is available. It is optional, so mention the omission without
|
|
173
|
+
blocking the write when there is nothing useful to add. For a completed task,
|
|
174
|
+
collect `currentConsumed` and draft a `finish` write. Never infer consumed
|
|
175
|
+
hours. Show all submitted values and require the same explicit ZenTao
|
|
176
|
+
confirmation before either write.
|
|
154
177
|
|
|
155
178
|
## Export mode
|
|
156
179
|
|
|
@@ -355,6 +355,13 @@ function itemKind(value) {
|
|
|
355
355
|
return value;
|
|
356
356
|
}
|
|
357
357
|
|
|
358
|
+
function detailKind(value) {
|
|
359
|
+
if (value !== "bug" && value !== "task" && value !== "story") {
|
|
360
|
+
throw new CliError("usage_error", "detail type must be bug, task, or story");
|
|
361
|
+
}
|
|
362
|
+
return value;
|
|
363
|
+
}
|
|
364
|
+
|
|
358
365
|
function pick(source, keys) {
|
|
359
366
|
const output = {};
|
|
360
367
|
for (const key of keys) if (source?.[key] !== undefined) output[key] = source[key];
|
|
@@ -367,7 +374,7 @@ function normalizeDetail(kind, response) {
|
|
|
367
374
|
if (!detail || typeof detail !== "object") {
|
|
368
375
|
throw new CliError("response_error", `ZenTao response has no ${kind} detail`);
|
|
369
376
|
}
|
|
370
|
-
|
|
377
|
+
const fields = [
|
|
371
378
|
"id",
|
|
372
379
|
"title",
|
|
373
380
|
"name",
|
|
@@ -380,15 +387,24 @@ function normalizeDetail(kind, response) {
|
|
|
380
387
|
"product",
|
|
381
388
|
"project",
|
|
382
389
|
"execution",
|
|
390
|
+
"story",
|
|
383
391
|
"type",
|
|
384
392
|
"openedDate",
|
|
385
393
|
"deadline",
|
|
386
|
-
]
|
|
394
|
+
];
|
|
395
|
+
if (kind === "task") {
|
|
396
|
+
fields.push("estimate", "consumed", "left", "realStarted", "finishedDate");
|
|
397
|
+
} else if (kind === "story") {
|
|
398
|
+
fields.push("stage", "category", "plan", "estimate", "spec", "verify", "source", "sourceNote");
|
|
399
|
+
}
|
|
400
|
+
return { raw: detail, safe: pick(detail, fields) };
|
|
387
401
|
}
|
|
388
402
|
|
|
389
403
|
function attachmentUrls(detail) {
|
|
390
404
|
const found = new Set();
|
|
391
|
-
const html = [detail.steps, detail.desc
|
|
405
|
+
const html = [detail.steps, detail.desc, detail.spec, detail.verify]
|
|
406
|
+
.filter((entry) => typeof entry === "string")
|
|
407
|
+
.join("\n");
|
|
392
408
|
for (const match of html.matchAll(/(?:src|href)=["']([^"']*\/file-(?:read|download)-\d+[^"']*)["']/gi)) {
|
|
393
409
|
found.add(match[1].replaceAll("&", "&"));
|
|
394
410
|
}
|
|
@@ -461,18 +477,64 @@ function legacyResult(response) {
|
|
|
461
477
|
return { ok: true, result, ...(typeof message === "string" ? { message: message.slice(0, 500) } : {}) };
|
|
462
478
|
}
|
|
463
479
|
|
|
480
|
+
async function workhourVariant(client, id) {
|
|
481
|
+
const variants = [
|
|
482
|
+
{ route: `task-recordworkhour-${id}.json`, dateField: "date[1]", legacy: false },
|
|
483
|
+
{ route: `task-recordestimate-${id}.json`, dateField: "dates[1]", legacy: true },
|
|
484
|
+
];
|
|
485
|
+
for (const variant of variants) {
|
|
486
|
+
try {
|
|
487
|
+
decodeLegacy(await client.json(variant.route));
|
|
488
|
+
return variant;
|
|
489
|
+
} catch (error) {
|
|
490
|
+
const unavailable = error instanceof CliError && (
|
|
491
|
+
(error.code === "http_error" && error.status === 404) ||
|
|
492
|
+
error.code === "response_error"
|
|
493
|
+
);
|
|
494
|
+
if (!unavailable) throw error;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
throw new CliError(
|
|
498
|
+
"unsupported_version",
|
|
499
|
+
"ZenTao exposes neither recordWorkhour nor recordEstimate for this task"
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
464
503
|
function localDateTime(date = new Date()) {
|
|
465
504
|
const part = (value) => String(value).padStart(2, "0");
|
|
466
505
|
return `${date.getFullYear()}-${part(date.getMonth() + 1)}-${part(date.getDate())} ${part(date.getHours())}:${part(date.getMinutes())}:${part(date.getSeconds())}`;
|
|
467
506
|
}
|
|
468
507
|
|
|
508
|
+
function localDate(date = new Date()) {
|
|
509
|
+
return localDateTime(date).slice(0, 10);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function effortDate(value) {
|
|
513
|
+
const date = value === undefined ? localDate() : value;
|
|
514
|
+
if (typeof date !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
|
515
|
+
throw new CliError("usage_error", "date must use YYYY-MM-DD");
|
|
516
|
+
}
|
|
517
|
+
const [year, month, day] = date.split("-").map(Number);
|
|
518
|
+
const parsed = new Date(year, month - 1, day);
|
|
519
|
+
if (
|
|
520
|
+
parsed.getFullYear() !== year ||
|
|
521
|
+
parsed.getMonth() !== month - 1 ||
|
|
522
|
+
parsed.getDate() !== day
|
|
523
|
+
) {
|
|
524
|
+
throw new CliError("usage_error", "date must be a valid calendar date");
|
|
525
|
+
}
|
|
526
|
+
if (date > localDate()) throw new CliError("usage_error", "date cannot be in the future");
|
|
527
|
+
return date;
|
|
528
|
+
}
|
|
529
|
+
|
|
469
530
|
function help() {
|
|
470
531
|
return `Usage:
|
|
471
532
|
zentao-cli.mjs doctor
|
|
472
533
|
zentao-cli.mjs list <bugs|tasks>
|
|
473
|
-
zentao-cli.mjs get <bug|task> <id> [--download-dir <path>]
|
|
534
|
+
zentao-cli.mjs get <bug|task|story> <id> [--download-dir <path>]
|
|
474
535
|
zentao-cli.mjs resolve bug <id> # JSON on stdin
|
|
475
536
|
zentao-cli.mjs comment <bug|task> <id> # {"comment":"..."} on stdin
|
|
537
|
+
zentao-cli.mjs log-hours task <id> # JSON on stdin
|
|
476
538
|
zentao-cli.mjs finish task <id> # JSON on stdin`;
|
|
477
539
|
}
|
|
478
540
|
|
|
@@ -497,20 +559,21 @@ export async function run(argv, { env = process.env } = {}) {
|
|
|
497
559
|
const singular = plural.slice(0, -1);
|
|
498
560
|
const data = decodeLegacy(await client.json(`my-work-${singular}.json`));
|
|
499
561
|
const fields = singular === "bug"
|
|
500
|
-
? ["id", "title", "severity", "pri", "status", "project", "product"]
|
|
501
|
-
: ["id", "name", "title", "pri", "status", "project", "execution", "module"];
|
|
562
|
+
? ["id", "title", "severity", "pri", "status", "project", "product", "story"]
|
|
563
|
+
: ["id", "name", "title", "pri", "status", "project", "execution", "module", "story", "estimate", "consumed", "left", "realStarted", "finishedDate"];
|
|
502
564
|
const items = Array.isArray(data?.[plural]) ? data[plural].map((item) => pick(item, fields)) : [];
|
|
503
565
|
return { items, ...(data?.pager ? { pager: pick(data.pager, ["recTotal", "recPerPage", "pageID", "pageTotal"]) } : {}) };
|
|
504
566
|
}
|
|
505
567
|
|
|
506
568
|
if (command === "get") {
|
|
507
|
-
const kind =
|
|
569
|
+
const kind = detailKind(args[0]);
|
|
508
570
|
const id = positiveId(args[1]);
|
|
509
571
|
let directory;
|
|
510
572
|
if (args[2] === "--download-dir" && args[3]) directory = path.resolve(args[3]);
|
|
511
573
|
else if (args.length > 2) throw new CliError("usage_error", "get accepts only --download-dir <path>");
|
|
512
574
|
else directory = fs.mkdtempSync(path.join(os.tmpdir(), `agent-tools-zentao-${kind}-${id}-`));
|
|
513
|
-
const
|
|
575
|
+
const resource = kind === "story" ? "stories" : `${kind}s`;
|
|
576
|
+
const detail = normalizeDetail(kind, await client.json(`api.php/v1/${resource}/${id}`));
|
|
514
577
|
const attachments = await downloadAttachments(client, detail.raw, directory);
|
|
515
578
|
return { item: detail.safe, attachments };
|
|
516
579
|
}
|
|
@@ -551,6 +614,38 @@ export async function run(argv, { env = process.env } = {}) {
|
|
|
551
614
|
return legacyResult(response);
|
|
552
615
|
}
|
|
553
616
|
|
|
617
|
+
if (command === "log-hours") {
|
|
618
|
+
if (args[0] !== "task") throw new CliError("usage_error", "log-hours supports tasks only");
|
|
619
|
+
const id = positiveId(args[1]);
|
|
620
|
+
const input = await readInput();
|
|
621
|
+
const consumed = Number(input.consumed);
|
|
622
|
+
const left = Number(input.left);
|
|
623
|
+
if (!Number.isFinite(consumed) || consumed <= 0) {
|
|
624
|
+
throw new CliError("usage_error", "consumed must be positive");
|
|
625
|
+
}
|
|
626
|
+
if (!Number.isFinite(left) || left <= 0) {
|
|
627
|
+
throw new CliError("usage_error", "left must be positive; use finish to complete a task");
|
|
628
|
+
}
|
|
629
|
+
if (input.work !== undefined && typeof input.work !== "string") {
|
|
630
|
+
throw new CliError("usage_error", "work must be a string when provided");
|
|
631
|
+
}
|
|
632
|
+
const date = effortDate(input.date);
|
|
633
|
+
const variant = await workhourVariant(client, id);
|
|
634
|
+
const fields = {
|
|
635
|
+
[variant.dateField]: date,
|
|
636
|
+
"work[1]": input.work?.trim(),
|
|
637
|
+
"consumed[1]": consumed,
|
|
638
|
+
"left[1]": left,
|
|
639
|
+
};
|
|
640
|
+
if (variant.legacy) fields["id[1]"] = 1;
|
|
641
|
+
const response = await client.json(variant.route, {
|
|
642
|
+
method: "POST",
|
|
643
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
644
|
+
body: formBody(fields),
|
|
645
|
+
});
|
|
646
|
+
return legacyResult(response);
|
|
647
|
+
}
|
|
648
|
+
|
|
554
649
|
if (command === "finish") {
|
|
555
650
|
if (args[0] !== "task") throw new CliError("usage_error", "finish supports tasks only");
|
|
556
651
|
const id = positiveId(args[1]);
|