@kairyou/agent-tools 0.17.1 → 0.19.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: fix, verify, stage; asks before committing and before writing status back.
59
+ Work ZenTao (禅道) bugs/tasks end to end, manage task status and hours, or read linked story context; 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>` — work a specific task
70
+ - `/at-zentao task <id> [request]` — work a task, start/pause/resume it, 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>`直接处理指定 task
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 workflow and secure API script.
17
+ │ └── at-zentao/ # Self-contained ZenTao development and task-lifecycle 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/ # 自包含的禅道工作流和安全 API 脚本.
17
+ │ └── at-zentao/ # 自包含的禅道研发事项与任务生命周期工作流.
18
18
  ├── docs/ # 高级指南和贡献者参考.
19
19
  ├── tools/ # 仅供维护者使用的上游同步和仓库工具.
20
20
  └── scripts/ # 安装, 同步, 校验和仓库维护脚本.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kairyou/agent-tools",
3
- "version": "0.17.1",
3
+ "version": "0.19.0",
4
4
  "description": "Reusable Agent Skills, plus runtime capabilities (statusline, provider usage, vision) for Codex, Claude Code, and opencode.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: at-zentao
3
- description: "Work ZenTao bugs/tasks end to end: fetch details, confirm understanding, fix, verify, stage with git add, then ask before committing and before writing status back to ZenTao. Supports single items and sequential batches. Use when the user references ZenTao (禅道) bugs or tasks."
4
- argument-hint: "bug <id> | task <id> | bugs | tasks | export bug|task <id>"
3
+ description: "Work ZenTao bugs/tasks end to end, manage task lifecycle and 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, task status, 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,10 @@ 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 start task <id>
89
+ node <skill-root>/scripts/zentao-cli.mjs pause task <id>
90
+ node <skill-root>/scripts/zentao-cli.mjs resume task <id>
91
+ node <skill-root>/scripts/zentao-cli.mjs log-hours task <id>
87
92
  node <skill-root>/scripts/zentao-cli.mjs finish task <id>
88
93
  ```
89
94
 
@@ -92,28 +97,56 @@ Input shapes:
92
97
  ```json
93
98
  {"comment":"Root cause and result."}
94
99
  {"resolution":"fixed","resolvedBuild":"trunk","comment":"Root cause and result, commit abc1234."}
100
+ {"realStarted":"2026-08-11 09:00:00","comment":"Started implementation."}
101
+ {"date":"2026-08-11","consumed":2,"left":14,"work":"Implemented the first part of the task."}
95
102
  {"currentConsumed":1.5,"realStarted":"2026-08-11 09:00:00","finishedDate":"2026-08-11 10:30:00"}
96
103
  ```
97
104
 
98
105
  For `duplicate`, also pass `"duplicateBug": <id>`. Send JSON through stdin,
99
106
  not as a command-line argument. The CLI handles UTF-8 form encoding and
100
107
  computes a task's total consumed hours from its current ZenTao value.
108
+ `start`, `pause`, and `resume` accept an optional `comment`; `start` also
109
+ accepts `realStarted` and otherwise uses the current time. The CLI preserves
110
+ the task's current hours when starting or resuming it.
111
+ `log-hours` defaults `date` to today, requires positive remaining hours,
112
+ and keeps the task open. Use `finish` when the task is complete.
101
113
 
102
114
  ## Usage
103
115
 
104
116
  - `/at-zentao bug <id>` — handle a single bug.
105
117
  - `/at-zentao task <id>` — handle a single task.
118
+ - `/at-zentao story <id>` — read requirement scope and acceptance context.
119
+ This mode is read-only; do not implement a Story status or comment workflow.
106
120
  - `/at-zentao bugs` — list bugs assigned to the configured account; let the
107
121
  user select one or more.
108
122
  - `/at-zentao tasks` — list assigned tasks and let the user select.
109
123
  - `/at-zentao export bug <id>` or `export task <id>` — create a read-only,
110
124
  self-contained handoff bundle.
111
125
 
126
+ Treat text after an item id as a natural-language request. Recognize task
127
+ lifecycle requests such as `开始`, `暂停`, `继续`, `start`, `pause`, and
128
+ `resume`, and time-entry requests such as `填工时`, `记录工时`, `log hours`,
129
+ and `worklog`. Users do not need to know the internal CLI commands. Reuse any
130
+ date, hours, or work description already supplied instead of asking twice.
131
+
112
132
  If a list response includes pager data showing more items than returned, tell
113
133
  the user the shown and total counts. Do not silently imply the list is complete.
114
134
  Do not browse through products/projects; start from assigned lists or an
115
135
  explicit item id.
116
136
 
137
+ When a fetched Bug or Task has a positive `story` id, fetch that Story before
138
+ planning the implementation. Use its `spec` and `verify` fields to identify
139
+ scope, acceptance criteria, constraints, and non-goals. Keep the Bug or Task as
140
+ the unit of work: never change, close, activate, or comment on the Story.
141
+
142
+ For a Task in `wait`, offer `start` when the user is about to work on it. For a
143
+ Task in `pause`, offer `resume`; the CLI maps this to ZenTao's `restart`
144
+ operation. Invoke `pause` only when the user explicitly asks or confirms that
145
+ the work itself is paused; the end of a session or workday is not enough. These
146
+ are status writes, so show the transition and optional comment and obtain
147
+ explicit confirmation before invoking the CLI. Starting or resuming preserves
148
+ the current hours and never implies new consumed time.
149
+
117
150
  ## Per-item workflow
118
151
 
119
152
  Follow these steps in order:
@@ -147,10 +180,20 @@ Bug resolutions are `fixed`, `notrepro`, `duplicate`, `bydesign`, `external`,
147
180
  write-back comment is one sentence containing root cause, change summary, and
148
181
  the commit hash when committed.
149
182
 
150
- For tasks, default to comment only. Offer `finish` only for a simple task
151
- completed in one sitting, and ask the user for `currentConsumed`; never invent
152
- hours. For multi-day work or per-day time records, comment and direct the user
153
- to ZenTao's web UI.
183
+ For tasks, ask whether to record the current work after the verified result.
184
+ For an incomplete task, collect the actual `consumed` hours and work date. When
185
+ the task has a numeric current `left`, suggest the new `left` by subtracting the
186
+ current entry and make that estimate editable in the confirmation; ask only
187
+ when no reliable suggestion is possible. Draft `work` from the verified result
188
+ when context is available. It is optional, so mention the omission without
189
+ blocking the write when there is nothing useful to add. For a completed task,
190
+ collect `currentConsumed` and draft a `finish` write. Never infer consumed
191
+ hours. Show all submitted values and require the same explicit ZenTao
192
+ confirmation before either write.
193
+
194
+ When the user wants to record hours and pause, show both exact writes in one
195
+ confirmation, then run `log-hours` before `pause`. Stop if the hour write fails.
196
+ Do not add lifecycle support for `activate`, `cancel`, or `close`.
154
197
 
155
198
  ## Export mode
156
199
 
@@ -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
- return { raw: detail, safe: pick(detail, [
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].filter((entry) => typeof entry === "string").join("\n");
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("&amp;", "&"));
394
410
  }
@@ -461,18 +477,67 @@ 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 start task <id> # JSON on stdin
538
+ zentao-cli.mjs pause task <id> # JSON on stdin
539
+ zentao-cli.mjs resume task <id> # JSON on stdin
540
+ zentao-cli.mjs log-hours task <id> # JSON on stdin
476
541
  zentao-cli.mjs finish task <id> # JSON on stdin`;
477
542
  }
478
543
 
@@ -497,20 +562,21 @@ export async function run(argv, { env = process.env } = {}) {
497
562
  const singular = plural.slice(0, -1);
498
563
  const data = decodeLegacy(await client.json(`my-work-${singular}.json`));
499
564
  const fields = singular === "bug"
500
- ? ["id", "title", "severity", "pri", "status", "project", "product"]
501
- : ["id", "name", "title", "pri", "status", "project", "execution", "module"];
565
+ ? ["id", "title", "severity", "pri", "status", "project", "product", "story"]
566
+ : ["id", "name", "title", "pri", "status", "project", "execution", "module", "story", "estimate", "consumed", "left", "realStarted", "finishedDate"];
502
567
  const items = Array.isArray(data?.[plural]) ? data[plural].map((item) => pick(item, fields)) : [];
503
568
  return { items, ...(data?.pager ? { pager: pick(data.pager, ["recTotal", "recPerPage", "pageID", "pageTotal"]) } : {}) };
504
569
  }
505
570
 
506
571
  if (command === "get") {
507
- const kind = itemKind(args[0]);
572
+ const kind = detailKind(args[0]);
508
573
  const id = positiveId(args[1]);
509
574
  let directory;
510
575
  if (args[2] === "--download-dir" && args[3]) directory = path.resolve(args[3]);
511
576
  else if (args.length > 2) throw new CliError("usage_error", "get accepts only --download-dir <path>");
512
577
  else directory = fs.mkdtempSync(path.join(os.tmpdir(), `agent-tools-zentao-${kind}-${id}-`));
513
- const detail = normalizeDetail(kind, await client.json(`api.php/v1/${kind}s/${id}`));
578
+ const resource = kind === "story" ? "stories" : `${kind}s`;
579
+ const detail = normalizeDetail(kind, await client.json(`api.php/v1/${resource}/${id}`));
514
580
  const attachments = await downloadAttachments(client, detail.raw, directory);
515
581
  return { item: detail.safe, attachments };
516
582
  }
@@ -551,6 +617,91 @@ export async function run(argv, { env = process.env } = {}) {
551
617
  return legacyResult(response);
552
618
  }
553
619
 
620
+ if (["start", "pause", "resume"].includes(command)) {
621
+ if (args[0] !== "task") throw new CliError("usage_error", `${command} supports tasks only`);
622
+ const id = positiveId(args[1]);
623
+ const input = await readInput();
624
+ if (input.comment !== undefined && typeof input.comment !== "string") {
625
+ throw new CliError("usage_error", "comment must be a string when provided");
626
+ }
627
+ if (input.realStarted !== undefined && (
628
+ command !== "start" ||
629
+ typeof input.realStarted !== "string" ||
630
+ !input.realStarted.trim()
631
+ )) {
632
+ throw new CliError("usage_error", "realStarted is accepted only for start and must be a non-empty string");
633
+ }
634
+
635
+ const method = command === "resume" ? "restart" : command;
636
+ const route = `task-${method}-${id}.json`;
637
+ const form = decodeLegacy(await client.json(route));
638
+ const task = form?.task || form;
639
+ const fields = { comment: input.comment?.trim() };
640
+
641
+ if (command !== "pause") {
642
+ const consumed = Number(task?.consumed);
643
+ const left = Number(task?.left);
644
+ if (!Number.isFinite(consumed) || consumed < 0 || !Number.isFinite(left) || left <= 0) {
645
+ throw new CliError("response_error", "ZenTao task has no safe current consumed/left values for this transition");
646
+ }
647
+ const assignedTo = typeof task.assignedTo === "string" && task.assignedTo
648
+ ? task.assignedTo
649
+ : config.account;
650
+ const existingStarted = typeof task.realStarted === "string" &&
651
+ task.realStarted.trim() &&
652
+ !task.realStarted.startsWith("0000-00-00")
653
+ ? task.realStarted
654
+ : null;
655
+ Object.assign(fields, {
656
+ assignedTo,
657
+ consumed,
658
+ left,
659
+ realStarted: command === "start"
660
+ ? input.realStarted?.trim() || localDateTime()
661
+ : existingStarted || localDateTime(),
662
+ });
663
+ }
664
+
665
+ const response = await client.json(route, {
666
+ method: "POST",
667
+ headers: { "content-type": "application/x-www-form-urlencoded" },
668
+ body: formBody(fields),
669
+ });
670
+ return legacyResult(response);
671
+ }
672
+
673
+ if (command === "log-hours") {
674
+ if (args[0] !== "task") throw new CliError("usage_error", "log-hours supports tasks only");
675
+ const id = positiveId(args[1]);
676
+ const input = await readInput();
677
+ const consumed = Number(input.consumed);
678
+ const left = Number(input.left);
679
+ if (!Number.isFinite(consumed) || consumed <= 0) {
680
+ throw new CliError("usage_error", "consumed must be positive");
681
+ }
682
+ if (!Number.isFinite(left) || left <= 0) {
683
+ throw new CliError("usage_error", "left must be positive; use finish to complete a task");
684
+ }
685
+ if (input.work !== undefined && typeof input.work !== "string") {
686
+ throw new CliError("usage_error", "work must be a string when provided");
687
+ }
688
+ const date = effortDate(input.date);
689
+ const variant = await workhourVariant(client, id);
690
+ const fields = {
691
+ [variant.dateField]: date,
692
+ "work[1]": input.work?.trim(),
693
+ "consumed[1]": consumed,
694
+ "left[1]": left,
695
+ };
696
+ if (variant.legacy) fields["id[1]"] = 1;
697
+ const response = await client.json(variant.route, {
698
+ method: "POST",
699
+ headers: { "content-type": "application/x-www-form-urlencoded" },
700
+ body: formBody(fields),
701
+ });
702
+ return legacyResult(response);
703
+ }
704
+
554
705
  if (command === "finish") {
555
706
  if (args[0] !== "task") throw new CliError("usage_error", "finish supports tasks only");
556
707
  const id = positiveId(args[1]);