@rse/ase 2.0.2 → 2.0.4

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.
Files changed (236) hide show
  1. package/dst/ase-agent-definition-core.js +111 -87
  2. package/dst/ase-agent-fleet.js +1 -1
  3. package/dst/ase-agent-runtime-claim.js +5 -3
  4. package/dst/ase-agent-runtime-engine.js +1 -1
  5. package/dst/ase-agent-runtime-phase.js +1 -1
  6. package/dst/ase-agent-runtime-request.js +1 -1
  7. package/dst/ase-agent-runtime.js +3 -5
  8. package/dst/ase-artifact.js +3 -3
  9. package/dst/ase-config-core.js +2 -16
  10. package/dst/ase-config-schema.js +1 -3
  11. package/dst/ase-config.js +2 -0
  12. package/dst/ase-hook.js +13 -13
  13. package/dst/ase-lib-daemon-cli.js +117 -4
  14. package/dst/ase-lib-server-cli.js +4 -5
  15. package/dst/ase-lib-version.js +14 -5
  16. package/dst/ase-man-catalog.js +229 -0
  17. package/dst/ase-man-render.js +278 -0
  18. package/dst/ase-man-tui.js +636 -0
  19. package/dst/ase-man.js +33 -27
  20. package/dst/ase-mcp.js +4 -4
  21. package/dst/ase-service-getopt.js +2 -2
  22. package/dst/ase-service-otel.js +4 -4
  23. package/dst/ase-service-skills.js +5 -15
  24. package/dst/ase-service.js +87 -210
  25. package/dst/ase-setup-common.js +1 -2
  26. package/dst/ase-setup-settings.js +6 -9
  27. package/dst/ase-setup.js +4 -5
  28. package/dst/ase-spec.js +103 -20
  29. package/dst/ase-statusline.js +170 -170
  30. package/dst/ase-task-board-filter.js +5 -4
  31. package/dst/ase-task-board-flow.js +7 -6
  32. package/dst/ase-task-board-graph.js +5 -4
  33. package/dst/ase-task-board-tui-control.js +41 -35
  34. package/dst/ase-task-board-tui-model.js +185 -167
  35. package/dst/ase-task-board-tui-popup.js +2 -3
  36. package/dst/ase-task-board-tui-view.js +74 -70
  37. package/dst/ase-task-board-web-client.js +27 -27
  38. package/dst/ase-task-board-web-server.js +2 -2
  39. package/dst/ase-task-board.js +1 -1
  40. package/dst/ase-task-format.js +1 -1
  41. package/dst/ase-task-store-client.js +17 -5
  42. package/dst/ase-task-store-core.js +5 -7
  43. package/dst/ase-task-store-issues.js +16 -1
  44. package/dst/ase-task-store-plugin-ase.js +7 -3
  45. package/dst/ase-task-store-plugin-gitea.js +9 -32
  46. package/dst/ase-task-store-plugin-github.js +3 -12
  47. package/dst/ase-task-store-plugin-gitlab.js +2 -18
  48. package/dst/ase-task.js +51 -23
  49. package/dst/ase-util-compat.js +1 -1
  50. package/dst/ase-util-diagram.js +13 -37
  51. package/dst/ase.js +5 -5
  52. package/dst/man/ase-agent-adhoc.md +1165 -0
  53. package/dst/man/ase-agent-condenser.md +383 -0
  54. package/dst/man/ase-agent-editor.md +441 -0
  55. package/dst/man/ase-agent-estimator.md +477 -0
  56. package/dst/man/ase-agent-fleet.md +358 -0
  57. package/dst/man/ase-agent-implementer.md +521 -0
  58. package/dst/man/ase-agent-integrator.md +449 -0
  59. package/dst/man/ase-agent-qualifier.md +426 -0
  60. package/dst/man/ase-agent-rebooter.md +384 -0
  61. package/dst/man/ase-agent-relay.md +262 -0
  62. package/dst/man/ase-agent-researcher.md +478 -0
  63. package/dst/man/ase-agent.md +628 -0
  64. package/dst/man/ase-artifact-list.md +57 -0
  65. package/dst/man/ase-artifact-name.md +56 -0
  66. package/dst/man/ase-artifact.md +92 -0
  67. package/dst/man/ase-config-delete.md +74 -0
  68. package/dst/man/ase-config-edit.md +83 -0
  69. package/dst/man/ase-config-get.md +70 -0
  70. package/dst/man/ase-config-init.md +110 -0
  71. package/dst/man/ase-config-list.md +68 -0
  72. package/dst/man/ase-config-set.md +92 -0
  73. package/dst/man/ase-config.md +426 -0
  74. package/dst/man/ase-hook-permission-request.md +61 -0
  75. package/dst/man/ase-hook-pre-tool-use.md +96 -0
  76. package/dst/man/ase-hook-session-end.md +71 -0
  77. package/dst/man/ase-hook-session-persona.md +65 -0
  78. package/dst/man/ase-hook-session-start.md +184 -0
  79. package/dst/man/ase-hook-stop.md +52 -0
  80. package/dst/man/ase-hook-user-prompt-submit.md +54 -0
  81. package/dst/man/ase-hook.md +93 -0
  82. package/dst/man/ase-man.md +195 -0
  83. package/dst/man/ase-mcp.md +102 -0
  84. package/dst/man/ase-service-log.md +59 -0
  85. package/dst/man/ase-service-send.md +60 -0
  86. package/dst/man/ase-service-start.md +93 -0
  87. package/dst/man/ase-service-status.md +58 -0
  88. package/dst/man/ase-service-stop.md +52 -0
  89. package/dst/man/ase-service.md +124 -0
  90. package/dst/man/ase-setup-disable.md +67 -0
  91. package/dst/man/ase-setup-enable.md +66 -0
  92. package/dst/man/ase-setup-install.md +116 -0
  93. package/dst/man/ase-setup-mcp.md +213 -0
  94. package/dst/man/ase-setup-status.md +101 -0
  95. package/dst/man/ase-setup-statusline.md +162 -0
  96. package/dst/man/ase-setup-telemetry.md +166 -0
  97. package/dst/man/ase-setup-uninstall.md +85 -0
  98. package/dst/man/ase-setup-update.md +117 -0
  99. package/dst/man/ase-setup.md +156 -0
  100. package/dst/man/ase-spec-export.md +99 -0
  101. package/dst/man/ase-spec-init.md +57 -0
  102. package/dst/man/ase-spec-lint.md +74 -0
  103. package/dst/man/ase-spec-preview.md +57 -0
  104. package/dst/man/ase-spec.md +85 -0
  105. package/dst/man/ase-statusline.md +330 -0
  106. package/dst/man/ase-task-assign.md +138 -0
  107. package/dst/man/ase-task-board.md +651 -0
  108. package/dst/man/ase-task-delete.md +46 -0
  109. package/dst/man/ase-task-edit.md +73 -0
  110. package/dst/man/ase-task-idscheme.md +92 -0
  111. package/dst/man/ase-task-lifecycle.md +92 -0
  112. package/dst/man/ase-task-list.md +86 -0
  113. package/dst/man/ase-task-load.md +48 -0
  114. package/dst/man/ase-task-newid.md +100 -0
  115. package/dst/man/ase-task-purge.md +49 -0
  116. package/dst/man/ase-task-rename.md +69 -0
  117. package/dst/man/ase-task-request.md +108 -0
  118. package/dst/man/ase-task-save.md +78 -0
  119. package/dst/man/ase-task-status.md +79 -0
  120. package/dst/man/ase-task-store.md +377 -0
  121. package/dst/man/ase-task-view.md +50 -0
  122. package/dst/man/ase-task.md +335 -0
  123. package/dst/man/ase-util-compat.md +38 -0
  124. package/dst/man/ase-util-diagram.md +140 -0
  125. package/dst/man/ase-util-meta.md +68 -0
  126. package/dst/man/ase-util-metric.md +74 -0
  127. package/dst/man/ase-util-mint.md +85 -0
  128. package/dst/man/ase-util-worktree.md +109 -0
  129. package/dst/man/ase-util.md +81 -0
  130. package/dst/man/ase.md +228 -0
  131. package/package.json +12 -17
  132. package/plugin/.claude-plugin/plugin.json +1 -1
  133. package/plugin/.codex-plugin/plugin.json +1 -1
  134. package/plugin/.github/plugin/plugin.json +1 -1
  135. package/plugin/agents/ase-code-lint.md +3 -3
  136. package/plugin/agents/ase-docs-proofread.md +3 -3
  137. package/plugin/agents/ase-docs-refine.md +3 -3
  138. package/plugin/agents/ase-spec-lint.md +338 -0
  139. package/plugin/meta/ase-skill.md +7 -0
  140. package/plugin/mods/ase-mods.ts +10 -7
  141. package/plugin/mods/claude-code.d.ts +26 -3
  142. package/plugin/skills/ase-code-analyze/SKILL.md +18 -10
  143. package/plugin/skills/ase-code-analyze/help.md +11 -6
  144. package/plugin/skills/ase-code-lint/SKILL.md +37 -23
  145. package/plugin/skills/ase-code-lint/help.md +10 -5
  146. package/plugin/skills/ase-help-intent/data.md +204 -13
  147. package/plugin/skills/ase-help-skill/catalog.md +1 -0
  148. package/plugin/skills/ase-meta-workflow/workflow.txt +2 -0
  149. package/plugin/skills/ase-repo-commit/SKILL.md +1 -0
  150. package/plugin/skills/ase-repo-merge/SKILL.md +1 -0
  151. package/plugin/skills/ase-repo-review/SKILL.md +5 -3
  152. package/plugin/skills/ase-repo-review/help.md +3 -1
  153. package/plugin/skills/ase-spec-edit/SKILL.md +3 -3
  154. package/plugin/skills/ase-spec-edit/help.md +1 -1
  155. package/plugin/skills/ase-spec-lint/SKILL.md +695 -0
  156. package/plugin/skills/ase-spec-lint/help.md +171 -0
  157. package/plugin/skills/ase-task-implement/SKILL.md +24 -6
  158. package/plugin/skills/ase-task-implement/help.md +11 -1
  159. package/dst/man/ase-agent-adhoc.1 +0 -515
  160. package/dst/man/ase-agent-condenser.1 +0 -193
  161. package/dst/man/ase-agent-editor.1 +0 -212
  162. package/dst/man/ase-agent-estimator.1 +0 -214
  163. package/dst/man/ase-agent-fleet.1 +0 -185
  164. package/dst/man/ase-agent-implementer.1 +0 -233
  165. package/dst/man/ase-agent-integrator.1 +0 -213
  166. package/dst/man/ase-agent-qualifier.1 +0 -214
  167. package/dst/man/ase-agent-rebooter.1 +0 -193
  168. package/dst/man/ase-agent-relay.1 +0 -128
  169. package/dst/man/ase-agent-researcher.1 +0 -217
  170. package/dst/man/ase-agent.1 +0 -264
  171. package/dst/man/ase-artifact-list.1 +0 -38
  172. package/dst/man/ase-artifact-name.1 +0 -39
  173. package/dst/man/ase-artifact.1 +0 -56
  174. package/dst/man/ase-config-delete.1 +0 -45
  175. package/dst/man/ase-config-edit.1 +0 -48
  176. package/dst/man/ase-config-get.1 +0 -46
  177. package/dst/man/ase-config-init.1 +0 -60
  178. package/dst/man/ase-config-list.1 +0 -42
  179. package/dst/man/ase-config-set.1 +0 -52
  180. package/dst/man/ase-config.1 +0 -231
  181. package/dst/man/ase-hook-permission-request.1 +0 -36
  182. package/dst/man/ase-hook-pre-tool-use.1 +0 -59
  183. package/dst/man/ase-hook-session-end.1 +0 -47
  184. package/dst/man/ase-hook-session-persona.1 +0 -37
  185. package/dst/man/ase-hook-session-start.1 +0 -81
  186. package/dst/man/ase-hook-stop.1 +0 -34
  187. package/dst/man/ase-hook-user-prompt-submit.1 +0 -34
  188. package/dst/man/ase-hook.1 +0 -51
  189. package/dst/man/ase-man.1 +0 -67
  190. package/dst/man/ase-mcp.1 +0 -61
  191. package/dst/man/ase-service-log.1 +0 -43
  192. package/dst/man/ase-service-send.1 +0 -47
  193. package/dst/man/ase-service-start.1 +0 -54
  194. package/dst/man/ase-service-status.1 +0 -46
  195. package/dst/man/ase-service-stop.1 +0 -37
  196. package/dst/man/ase-service.1 +0 -85
  197. package/dst/man/ase-setup-disable.1 +0 -44
  198. package/dst/man/ase-setup-enable.1 +0 -45
  199. package/dst/man/ase-setup-install.1 +0 -73
  200. package/dst/man/ase-setup-mcp.1 +0 -124
  201. package/dst/man/ase-setup-status.1 +0 -60
  202. package/dst/man/ase-setup-statusline.1 +0 -92
  203. package/dst/man/ase-setup-telemetry.1 +0 -92
  204. package/dst/man/ase-setup-uninstall.1 +0 -59
  205. package/dst/man/ase-setup-update.1 +0 -64
  206. package/dst/man/ase-setup.1 +0 -93
  207. package/dst/man/ase-spec-export.1 +0 -62
  208. package/dst/man/ase-spec-lint.1 +0 -40
  209. package/dst/man/ase-spec-preview.1 +0 -40
  210. package/dst/man/ase-spec.1 +0 -46
  211. package/dst/man/ase-statusline.1 +0 -189
  212. package/dst/man/ase-task-assign.1 +0 -71
  213. package/dst/man/ase-task-board.1 +0 -384
  214. package/dst/man/ase-task-delete.1 +0 -35
  215. package/dst/man/ase-task-edit.1 +0 -46
  216. package/dst/man/ase-task-idscheme.1 +0 -65
  217. package/dst/man/ase-task-lifecycle.1 +0 -57
  218. package/dst/man/ase-task-list.1 +0 -47
  219. package/dst/man/ase-task-load.1 +0 -36
  220. package/dst/man/ase-task-newid.1 +0 -61
  221. package/dst/man/ase-task-purge.1 +0 -37
  222. package/dst/man/ase-task-rename.1 +0 -44
  223. package/dst/man/ase-task-request.1 +0 -61
  224. package/dst/man/ase-task-save.1 +0 -48
  225. package/dst/man/ase-task-status.1 +0 -55
  226. package/dst/man/ase-task-store.1 +0 -194
  227. package/dst/man/ase-task-view.1 +0 -40
  228. package/dst/man/ase-task.1 +0 -160
  229. package/dst/man/ase-util-compat.1 +0 -25
  230. package/dst/man/ase-util-diagram.1 +0 -76
  231. package/dst/man/ase-util-meta.1 +0 -45
  232. package/dst/man/ase-util-metric.1 +0 -54
  233. package/dst/man/ase-util-mint.1 +0 -59
  234. package/dst/man/ase-util-worktree.1 +0 -72
  235. package/dst/man/ase-util.1 +0 -50
  236. package/dst/man/ase.1 +0 -124
@@ -192,7 +192,7 @@ const attachmentDocument = async (log, id, n) => {
192
192
  try {
193
193
  text = new TextDecoder("utf-8", { fatal: true }).decode(a.content);
194
194
  }
195
- catch (_err) {
195
+ catch {
196
196
  text = "\0";
197
197
  }
198
198
  if (text.includes("\0"))
@@ -364,7 +364,7 @@ const registerPageRoutes = (server, log) => {
364
364
  };
365
365
  /* the filter query of a request (its optional "filter" query parameter) */
366
366
  const filterQuery = (request) => typeof request.query.filter === "string" ? request.query.filter : "";
367
- /* register the view routes of the web board: the board, the graph, the statistics, and the tasks with their attachments */
367
+ /* register the view routes of the web board: the board, the task and agent graphs, the statistics, the new task text, and the tasks with their attachments */
368
368
  const registerViewRoutes = (server, log) => {
369
369
  /* the board model, reduced onto the tasks matching the filter query */
370
370
  server.route({
@@ -78,7 +78,7 @@ export default class TaskBoardCommand {
78
78
  "(restart it with \"ase service stop\" once no agent session uses it)");
79
79
  return port;
80
80
  }
81
- /* open an URL in the default browser of the platform, waiting only for
81
+ /* open a URL in the default browser of the platform, waiting only for
82
82
  the opener to spawn, as e.g. "xdg-open" can stay attached to the browser */
83
83
  async openBrowser(url) {
84
84
  const [cmd, ...args] = process.platform === "darwin" ? ["open", url] :
@@ -745,7 +745,7 @@ export const bindEffort = (plan) => {
745
745
  if (effort !== null && effort.hash === "")
746
746
  plan.header.Effort = `${effort.size} ${hash}`;
747
747
  else if (effort === null || effort.hash !== hash)
748
- Reflect.deleteProperty(plan.header, "Effort");
748
+ delete plan.header.Effort;
749
749
  return plan;
750
750
  };
751
751
  /* derive the title of a task plan from its body as defined by the
@@ -58,6 +58,7 @@ export class LocalTaskStoreClient {
58
58
  this.configure("project.task.idscheme", spec);
59
59
  this.idscheme = (await this.core.projectSet(this.prjId, { idscheme: spec })).project.idscheme;
60
60
  }
61
+ /* run an operation, yielding a fallback for a missing (404) task plan */
61
62
  async missing(op, fallback) {
62
63
  try {
63
64
  return await op();
@@ -68,6 +69,7 @@ export class LocalTaskStoreClient {
68
69
  throw err;
69
70
  }
70
71
  }
72
+ /* open the storage delegate shared by all clients of the store and register the project */
71
73
  async open() {
72
74
  let entry = LocalTaskStoreClient.shared.get(this.storage.key);
73
75
  if (entry === undefined) {
@@ -99,6 +101,7 @@ export class LocalTaskStoreClient {
99
101
  throw err;
100
102
  }
101
103
  }
104
+ /* release the shared storage delegate, closing it with its last client */
102
105
  async close() {
103
106
  const entry = this.entry;
104
107
  if (entry === undefined)
@@ -107,7 +110,9 @@ export class LocalTaskStoreClient {
107
110
  if (--entry.refs === 0) {
108
111
  if (LocalTaskStoreClient.shared.get(this.storage.key) === entry)
109
112
  LocalTaskStoreClient.shared.delete(this.storage.key);
110
- await entry.opened.then((opened) => opened.store.close(), () => { });
113
+ await entry.opened.then((opened) => opened.store.close(), () => {
114
+ /* a failed opening was already raised to its opener */
115
+ });
111
116
  }
112
117
  }
113
118
  list(fields) {
@@ -153,11 +158,13 @@ export class LocalTaskStoreClient {
153
158
  onChange();
154
159
  };
155
160
  let stopped = false;
161
+ let started = false;
156
162
  const start = async () => {
157
163
  await this.open();
158
164
  if (stopped)
159
165
  await this.close();
160
166
  else {
167
+ started = true;
161
168
  this.entry?.listeners.add(listener);
162
169
  onState?.(true);
163
170
  }
@@ -167,6 +174,8 @@ export class LocalTaskStoreClient {
167
174
  });
168
175
  return () => {
169
176
  stopped = true;
177
+ if (!started)
178
+ return;
170
179
  this.entry?.listeners.delete(listener);
171
180
  this.close().catch(() => { });
172
181
  };
@@ -174,9 +183,10 @@ export class LocalTaskStoreClient {
174
183
  }
175
184
  /* the interval of the keepalive pings of the event subscription (where a connection without
176
185
  any traffic for this interval is pinged and without any traffic for twice this interval
177
- is terminated) and the timeout of its WebSocket handshake */
186
+ is terminated), the timeout of its WebSocket handshake, and the timeout of the REST API requests */
178
187
  const KEEPALIVE_MS = 15 * 1000;
179
188
  const HANDSHAKE_MS = 10 * 1000;
189
+ const REQUEST_MS = 10 * 1000;
180
190
  /* the remote client: the REST API of a task store server, with the
181
191
  project registered under its configured lifecycle model and task id
182
192
  scheme on first use only (afterwards adopting the lifecycle model and
@@ -231,7 +241,7 @@ export class RemoteTaskStoreClient {
231
241
  },
232
242
  body: body !== undefined ? JSON.stringify(body) : undefined,
233
243
  dispatcher: this.dispatcher,
234
- signal: AbortSignal.timeout(10000),
244
+ signal: AbortSignal.timeout(REQUEST_MS),
235
245
  ignoreResponseError: true
236
246
  }).catch((err) => this.unreachable(err));
237
247
  if (r.status === 404 || tolerated.includes(r.status))
@@ -402,7 +412,7 @@ export class RemoteTaskStoreClient {
402
412
  headers: { Authorization: `Bearer ${this.token}` },
403
413
  responseType: "arrayBuffer",
404
414
  dispatcher: this.dispatcher,
405
- signal: AbortSignal.timeout(10000),
415
+ signal: AbortSignal.timeout(REQUEST_MS),
406
416
  ignoreResponseError: true
407
417
  }).catch((err) => this.unreachable(err));
408
418
  if (r.status === 404)
@@ -489,7 +499,9 @@ export class RemoteTaskStoreClient {
489
499
  if (timer !== null)
490
500
  clearTimeout(timer);
491
501
  ws?.close();
492
- this.close().catch(() => { });
502
+ this.close().catch(() => {
503
+ /* best-effort release of the dispatcher on unsubscribe */
504
+ });
493
505
  };
494
506
  }
495
507
  }
@@ -143,7 +143,7 @@ export class TaskStoreCore {
143
143
  if (effort !== undefined)
144
144
  header.Effort = effort;
145
145
  else
146
- Reflect.deleteProperty(header, "Effort");
146
+ delete header.Effort;
147
147
  return header;
148
148
  }
149
149
  /* load an existing task plan of a registered project */
@@ -408,7 +408,7 @@ export class TaskStoreCore {
408
408
  if (lifecycle.states.includes(status) && !states.includes(status))
409
409
  continue;
410
410
  const att = fields === "usage" ? (await this.store.taskLoad(prjId, entry.id))?.attachment
411
- .find((att) => TaskFormat.isUsage(att) && att.Data !== undefined) : undefined;
411
+ .find((a) => TaskFormat.isUsage(a) && a.Data !== undefined) : undefined;
412
412
  const header = fields === "header" ? await this.boundHeader(prjId, entry) : entry.header;
413
413
  result.push({
414
414
  id: entry.id,
@@ -605,12 +605,10 @@ export class TaskStoreCore {
605
605
  references: referrers.length
606
606
  };
607
607
  }
608
- else {
609
- await this.store.taskSave(prjId, taskId, plan);
610
- this.emitUpdated(prjId, taskId, plan, lifecycle, ["header"]);
611
- }
608
+ await this.store.taskSave(prjId, taskId, plan);
609
+ this.emitUpdated(prjId, taskId, plan, lifecycle, ["header"]);
612
610
  return {
613
- id: newId ?? taskId,
611
+ id: taskId,
614
612
  status: TaskFormat.taskStatus(plan.header, lifecycle),
615
613
  ...(status !== null ? { from } : {})
616
614
  };
@@ -235,6 +235,19 @@ export const attachmentSync = (current, wanted) => {
235
235
  }
236
236
  return ops;
237
237
  };
238
+ /* reuse the cached promise of a load, else start the load and cache its promise
239
+ (dropped again on failure), so repeated and concurrent loads share one request */
240
+ export const memoize = (cache, key, load) => {
241
+ let promise = cache.get(key);
242
+ if (promise === undefined) {
243
+ promise = load();
244
+ cache.set(key, promise);
245
+ promise.catch(() => {
246
+ cache.delete(key);
247
+ });
248
+ }
249
+ return promise;
250
+ };
238
251
  /* the common base of the issue tracker storage plugins: the projects mapped onto
239
252
  their locations, the storage lifecycle polling for changes while opened and
240
253
  observed (without keeping the process alive), and the project listing */
@@ -261,7 +274,9 @@ export class IssueTaskStoragePlugin {
261
274
  this.opened = this.now();
262
275
  if (this.poll > 0 && this.listener !== null) {
263
276
  this.timer = setInterval(() => {
264
- this.pollAll().catch(() => { });
277
+ this.pollAll().catch(() => {
278
+ /* failures are already logged per project */
279
+ });
265
280
  }, this.poll * 1000);
266
281
  this.timer.unref();
267
282
  }
@@ -143,7 +143,7 @@ class FileTaskStoragePlugin {
143
143
  doc = parseYAML(text);
144
144
  }
145
145
  catch (err) {
146
- this.ctx.log("warning", `malformed project file "${file}" ignored: ${err.message}`);
146
+ this.ctx.log("warning", `malformed project file "${file}" ignored: ${err instanceof Error ? err.message : String(err)}`);
147
147
  return { lifecycle: this.lifecycle, idscheme: this.idscheme, seqmark: 0, broken: true };
148
148
  }
149
149
  return {
@@ -339,7 +339,9 @@ class FileTaskStoragePlugin {
339
339
  if (this.watcher !== w)
340
340
  return;
341
341
  this.watcher = null;
342
- w.close().catch(() => { }).finally(() => {
342
+ w.close().catch(() => {
343
+ /* re-arm even if the outdated watcher fails to close */
344
+ }).finally(() => {
343
345
  this.arm();
344
346
  this.schedule();
345
347
  });
@@ -418,7 +420,9 @@ class FileTaskStoragePlugin {
418
420
  clearTimeout(this.timer);
419
421
  this.timer = setTimeout(() => {
420
422
  this.timer = null;
421
- this.rescan().catch(() => { });
423
+ this.rescan().catch(() => {
424
+ /* failures are already logged per directory */
425
+ });
422
426
  }, 100);
423
427
  this.timer.unref();
424
428
  }
@@ -248,7 +248,7 @@ class GiteaTaskStoragePlugin extends TaskIssues.IssueTaskStoragePlugin {
248
248
  await found(this.call(loc, this.gt.repos.issueDeleteLabel(loc.owner, loc.repo, label.id)).then(() => true)) : null;
249
249
  this.labels.delete(`${loc.owner}/${loc.repo}`);
250
250
  this.registry.delete(prjId);
251
- return label !== undefined && result !== null;
251
+ return result !== null;
252
252
  }
253
253
  /* ==== issue mapping ==== */
254
254
  /* fetch the live issue of an issue number (null if none) */
@@ -264,15 +264,8 @@ class GiteaTaskStoragePlugin extends TaskIssues.IssueTaskStoragePlugin {
264
264
  const key = `${loc.owner}/${loc.repo}#${issue.number}@${issue.updated_at}`;
265
265
  if (fresh)
266
266
  this.depends.delete(key);
267
- let depends = this.depends.get(key);
268
- if (depends === undefined) {
269
- depends = found(this.all(loc, (query) => this.gt.repos.issueListIssueDependencies(loc.owner, loc.repo, String(issue.number), query)))
270
- .then((issues) => (issues ?? []));
271
- this.depends.set(key, depends);
272
- depends.catch(() => {
273
- this.depends.delete(key);
274
- });
275
- }
267
+ const depends = TaskIssues.memoize(this.depends, key, () => found(this.all(loc, (query) => this.gt.repos.issueListIssueDependencies(loc.owner, loc.repo, String(issue.number), query)))
268
+ .then((issues) => (issues ?? [])));
276
269
  return (await depends).filter((blocker) => inRepo(loc, blocker));
277
270
  }
278
271
  /* the comments of an issue */
@@ -323,10 +316,10 @@ class GiteaTaskStoragePlugin extends TaskIssues.IssueTaskStoragePlugin {
323
316
  if (label === undefined) {
324
317
  label = await this.call(loc, this.gt.repos.issueCreateLabel(loc.owner, loc.repo, { name, color: name.startsWith("ase:") ? "#c5def5" : "#ededed" })).catch(async (err) => {
325
318
  /* tolerate a label created concurrently */
326
- const label = statusOf(err) === 422 ? (await this.labelMap(loc, true)).get(name.toLowerCase()) : undefined;
327
- if (label === undefined)
319
+ const existing = statusOf(err) === 422 ? (await this.labelMap(loc, true)).get(name.toLowerCase()) : undefined;
320
+ if (existing === undefined)
328
321
  throw err;
329
- return label;
322
+ return existing;
330
323
  });
331
324
  const map = await this.labelMap(loc);
332
325
  map.set(name.toLowerCase(), label);
@@ -383,15 +376,8 @@ class GiteaTaskStoragePlugin extends TaskIssues.IssueTaskStoragePlugin {
383
376
  if (typeof assignee === "string" && assignee !== "") {
384
377
  const logins = issue?.assignees?.map((user) => user.login) ?? [];
385
378
  const key = `${loc.owner}/${loc.repo}`;
386
- let users = this.assignable.get(key);
387
- if (users === undefined) {
388
- users = this.call(loc, this.gt.repos.repoGetAssignees(loc.owner, loc.repo))
389
- .then((list) => new Set(list.map((user) => (user.login ?? "").toLowerCase())));
390
- this.assignable.set(key, users);
391
- users.catch(() => {
392
- this.assignable.delete(key);
393
- });
394
- }
379
+ const users = TaskIssues.memoize(this.assignable, key, () => this.call(loc, this.gt.repos.repoGetAssignees(loc.owner, loc.repo))
380
+ .then((list) => new Set(list.map((user) => (user.login ?? "").toLowerCase()))));
395
381
  if (logins.includes(assignee))
396
382
  assignees = logins;
397
383
  else if ((await users).has(assignee.toLowerCase()))
@@ -447,11 +433,7 @@ class GiteaTaskStoragePlugin extends TaskIssues.IssueTaskStoragePlugin {
447
433
  own write or detected change of the issue), as e.g. the boards load the plan
448
434
  and its attachments in separate operations */
449
435
  load(prjId, loc, n) {
450
- const key = `${prjId}#${n}`;
451
- const cached = this.loads.get(key);
452
- if (cached !== undefined)
453
- return cached;
454
- const load = (async () => {
436
+ return TaskIssues.memoize(this.loads, `${prjId}#${n}`, async () => {
455
437
  const [issue, comments] = await Promise.all([
456
438
  this.fetch(loc, n),
457
439
  n === 0 ? [] : found(this.comments(loc, n))
@@ -459,12 +441,7 @@ class GiteaTaskStoragePlugin extends TaskIssues.IssueTaskStoragePlugin {
459
441
  if (issue === null)
460
442
  return null;
461
443
  return { issue, after: await this.blockers(loc, issue), comments: comments ?? [] };
462
- })();
463
- this.loads.set(key, load);
464
- load.catch(() => {
465
- this.loads.delete(key);
466
444
  });
467
- return load;
468
445
  }
469
446
  /* ==== task plans ==== */
470
447
  async taskList(prjId) {
@@ -73,7 +73,7 @@ class GitHubTaskStoragePlugin extends TaskIssues.IssueTaskStoragePlugin {
73
73
  constructor(ctx) {
74
74
  super(ctx, "repositories", now);
75
75
  const options = ctx.options;
76
- const token = options.token ?? process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN ?? "";
76
+ const token = options.token ?? (process.env.GITHUB_TOKEN || process.env.GH_TOKEN) ?? "";
77
77
  if (typeof token !== "string" || token === "")
78
78
  throw new Error("task store: plugin \"github\" requires the \"token\" option (or $GITHUB_TOKEN resp. $GH_TOKEN)");
79
79
  const url = options.url ?? "https://github.com";
@@ -122,7 +122,7 @@ class GitHubTaskStoragePlugin extends TaskIssues.IssueTaskStoragePlugin {
122
122
  /* explain a denied access (but not an exceeded rate limit), as the
123
123
  GitHub message names neither the token source nor the missing permission */
124
124
  const source = options.token !== undefined ? "the \"token\" option (resp. \"project.task.store.token\")" :
125
- process.env.GITHUB_TOKEN !== undefined ? "$GITHUB_TOKEN" : "$GH_TOKEN";
125
+ process.env.GITHUB_TOKEN !== undefined && process.env.GITHUB_TOKEN !== "" ? "$GITHUB_TOKEN" : "$GH_TOKEN";
126
126
  this.gh.hook.error("request", (err, request) => {
127
127
  const status = statusOf(err);
128
128
  if ((status === 401 || status === 403) && !/rate limit/i.test(err.message)) {
@@ -462,11 +462,7 @@ class GitHubTaskStoragePlugin extends TaskIssues.IssueTaskStoragePlugin {
462
462
  own write or detected change of the issue), as e.g. the boards load the plan
463
463
  and its attachments in separate operations */
464
464
  load(prjId, loc, n) {
465
- const key = `${prjId}#${n}`;
466
- const cached = this.loads.get(key);
467
- if (cached !== undefined)
468
- return cached;
469
- const load = (async () => {
465
+ return TaskIssues.memoize(this.loads, `${prjId}#${n}`, async () => {
470
466
  const [issue, comments] = await Promise.all([
471
467
  this.fetch(loc, n),
472
468
  n === 0 ? [] : found(this.gh.paginate(this.gh.rest.issues.listComments, { ...loc, issue_number: n, per_page: 100 }))
@@ -474,12 +470,7 @@ class GitHubTaskStoragePlugin extends TaskIssues.IssueTaskStoragePlugin {
474
470
  if (issue === null)
475
471
  return null;
476
472
  return { issue, after: await this.blockers(loc, issue), comments: comments ?? [] };
477
- })();
478
- this.loads.set(key, load);
479
- load.catch(() => {
480
- this.loads.delete(key);
481
473
  });
482
- return load;
483
474
  }
484
475
  /* ==== task plans ==== */
485
476
  async taskList(prjId) {
@@ -240,14 +240,7 @@ class GitLabTaskStoragePlugin extends TaskIssues.IssueTaskStoragePlugin {
240
240
  const key = `${path}#${issue.iid}@${issue.updated_at}`;
241
241
  if (fresh)
242
242
  this.linked.delete(key);
243
- let links = this.linked.get(key);
244
- if (links === undefined) {
245
- links = this.links.all(path, issue.iid).then((data) => data);
246
- this.linked.set(key, links);
247
- links.catch(() => {
248
- this.linked.delete(key);
249
- });
250
- }
243
+ const links = TaskIssues.memoize(this.linked, key, () => this.links.all(path, issue.iid).then((data) => data));
251
244
  return (await links).filter((link) => link.link_type === "is_blocked_by" && link.project_id === issue.project_id);
252
245
  }
253
246
  /* the (non-system) notes of an issue */
@@ -451,11 +444,7 @@ class GitLabTaskStoragePlugin extends TaskIssues.IssueTaskStoragePlugin {
451
444
  own write or detected change of the issue), as e.g. the boards load the plan
452
445
  and its attachments in separate operations */
453
446
  load(prjId, path, n) {
454
- const key = `${prjId}#${n}`;
455
- const cached = this.loads.get(key);
456
- if (cached !== undefined)
457
- return cached;
458
- const load = (async () => {
447
+ return TaskIssues.memoize(this.loads, `${prjId}#${n}`, async () => {
459
448
  const [issue, notes] = await Promise.all([
460
449
  this.fetch(path, n),
461
450
  n === 0 ? [] : found(this.comments(path, n))
@@ -463,12 +452,7 @@ class GitLabTaskStoragePlugin extends TaskIssues.IssueTaskStoragePlugin {
463
452
  if (issue === null)
464
453
  return null;
465
454
  return { issue, after: await this.blockers(path, issue), notes: notes ?? [] };
466
- })();
467
- this.loads.set(key, load);
468
- load.catch(() => {
469
- this.loads.delete(key);
470
455
  });
471
- return load;
472
456
  }
473
457
  /* ==== task plans ==== */
474
458
  async taskList(prjId) {
package/dst/ase-task.js CHANGED
@@ -341,7 +341,7 @@ export class Task {
341
341
  try {
342
342
  url = new URL(store.value);
343
343
  }
344
- catch (_err) {
344
+ catch {
345
345
  throw unsupported();
346
346
  }
347
347
  if (url.hostname === "" || url.port === "" || url.username !== "" || url.password !== ""
@@ -444,21 +444,12 @@ export class Task {
444
444
  tag: Core.taskTag(plan)
445
445
  };
446
446
  }
447
- /* save a task as text under the given id; throws if its "Status:"
448
- frontmatter key is unknown to the task lifecycle model or not
449
- reachable from the previous status, on a conditional save (with
450
- an entity tag) a problem 412 if the task was changed in the meantime,
451
- and on a create-only save an error if the task already exists;
452
- returns a warning (else empty) if the id does not conform to the task id scheme,
453
- the entity tag of the just-saved plan, and the text of the just-saved plan
454
- (as sent, re-serialized from its parsed form, i.e., before any
455
- normalization by the task store, and with its elided attachment
456
- payloads still elided, while they are restored from the stored
457
- plan for the save itself); the entity tag
458
- is the one the task store reports for the plan as stored, and only on a
459
- task store server predating this report the one of the plan as sent (which
460
- never is the one of a plan saved by somebody else in the meantime, so a
461
- conditional save based on it at worst is refused spuriously) */
447
+ /* save a task as text under the given id, throwing on a "Status:" unknown to or not
448
+ reachable in the task lifecycle model, on a conditional save (with an entity tag) a
449
+ problem 412 on a concurrent change, and on a create-only save an error if the task
450
+ exists; elided attachment payloads are restored from the stored plan; returns the
451
+ id scheme warning (else empty), the entity tag of the saved plan (as reported by the
452
+ task store, else of the plan as sent), and the text of the plan as sent */
462
453
  static async save(log, id, text, tag, create = false) {
463
454
  if (typeof text !== "string")
464
455
  throw new Error("task: text must be a string");
@@ -993,6 +984,17 @@ export default class TaskCommand {
993
984
  process.exit(0);
994
985
  }
995
986
  });
987
+ /* register the further CLI sub-commands */
988
+ this.registerRequestCommands(task);
989
+ this.registerPlanCommands(task);
990
+ this.registerProjectCommands(task);
991
+ /* register CLI sub-command group "ase task store" */
992
+ new TaskStoreCommand(this.log).register(task);
993
+ /* provide "ase task" command for further sub-commands */
994
+ return task;
995
+ }
996
+ /* register the CLI sub-commands requesting and assigning a task */
997
+ registerRequestCommands(task) {
996
998
  /* register CLI sub-command "ase task request" */
997
999
  task
998
1000
  .command("request")
@@ -1049,6 +1051,9 @@ export default class TaskCommand {
1049
1051
  }
1050
1052
  process.exit(0);
1051
1053
  });
1054
+ }
1055
+ /* register the CLI sub-commands loading, viewing, editing, saving, deleting, and renaming a task */
1056
+ registerPlanCommands(task) {
1052
1057
  /* register CLI sub-command "ase task load" */
1053
1058
  task
1054
1059
  .command("load")
@@ -1130,6 +1135,9 @@ export default class TaskCommand {
1130
1135
  this.log.write("info", `task: no task "${oldId}" to rename`);
1131
1136
  process.exit(result !== null ? 0 : 1);
1132
1137
  });
1138
+ }
1139
+ /* register the CLI sub-commands of the task ids, the purging, and the project settings */
1140
+ registerProjectCommands(task) {
1133
1141
  /* register CLI sub-command "ase task newid" */
1134
1142
  task
1135
1143
  .command("newid")
@@ -1197,10 +1205,6 @@ export default class TaskCommand {
1197
1205
  process.exit(0);
1198
1206
  }
1199
1207
  });
1200
- /* register CLI sub-command group "ase task store" */
1201
- new TaskStoreCommand(this.log).register(task);
1202
- /* provide "ase task" command for further sub-commands */
1203
- return task;
1204
1208
  }
1205
1209
  }
1206
1210
  /* render a caught error as an MCP tool error result */
@@ -1264,9 +1268,8 @@ export class TaskMCP {
1264
1268
  try {
1265
1269
  const verbose = args.verbose ?? false;
1266
1270
  const items = await Task.list(this.log);
1267
- const tasks = verbose ?
1268
- items.map((item) => ({ id: item.id, status: item.status, title: item.title, mtime: item.mtime, effort: item.effort })) :
1269
- items.map((item) => ({ id: item.id, status: item.status, title: item.title }));
1271
+ const tasks = verbose ? items :
1272
+ items.map(({ id, status, title }) => ({ id, status, title }));
1270
1273
  const result = { tasks };
1271
1274
  return {
1272
1275
  structuredContent: result,
@@ -1277,6 +1280,16 @@ export class TaskMCP {
1277
1280
  return mcpError(err);
1278
1281
  }
1279
1282
  });
1283
+ /* register the further MCP tools */
1284
+ this.registerLoadTool(mcp);
1285
+ this.registerSaveTool(mcp);
1286
+ this.registerPlanTools(mcp);
1287
+ this.registerStatusTools(mcp);
1288
+ this.registerAssignTool(mcp);
1289
+ this.registerIdTools(mcp);
1290
+ }
1291
+ /* register the MCP tool loading a task plan */
1292
+ registerLoadTool(mcp) {
1280
1293
  /* task load */
1281
1294
  mcp.registerTool("ase_task_load", {
1282
1295
  title: "ASE task load",
@@ -1332,6 +1345,9 @@ export class TaskMCP {
1332
1345
  return mcpError(err);
1333
1346
  }
1334
1347
  });
1348
+ }
1349
+ /* register the MCP tool saving a task plan */
1350
+ registerSaveTool(mcp) {
1335
1351
  /* task save */
1336
1352
  mcp.registerTool("ase_task_save", {
1337
1353
  title: "ASE task save",
@@ -1408,6 +1424,9 @@ export class TaskMCP {
1408
1424
  return mcpError(err);
1409
1425
  }
1410
1426
  });
1427
+ }
1428
+ /* register the MCP tools reading an attachment of, deleting, and renaming a task plan */
1429
+ registerPlanTools(mcp) {
1411
1430
  /* task attachment */
1412
1431
  mcp.registerTool("ase_task_attachment", {
1413
1432
  title: "ASE task attachment",
@@ -1482,6 +1501,9 @@ export class TaskMCP {
1482
1501
  return mcpError(err);
1483
1502
  }
1484
1503
  });
1504
+ }
1505
+ /* register the MCP tools getting/setting the status of and requesting the processing of a task plan */
1506
+ registerStatusTools(mcp) {
1485
1507
  /* task status get/set */
1486
1508
  mcp.registerTool("ase_task_status", {
1487
1509
  title: "ASE task status get/set",
@@ -1539,6 +1561,9 @@ export class TaskMCP {
1539
1561
  return mcpError(err);
1540
1562
  }
1541
1563
  });
1564
+ }
1565
+ /* register the MCP tool assigning a task plan (with AI usage accounting) */
1566
+ registerAssignTool(mcp) {
1542
1567
  /* task assignment (with AI usage accounting) */
1543
1568
  mcp.registerTool("ase_task_assign", {
1544
1569
  title: "ASE task assignment",
@@ -1586,6 +1611,9 @@ export class TaskMCP {
1586
1611
  return mcpError(err);
1587
1612
  }
1588
1613
  });
1614
+ }
1615
+ /* register the MCP tools generating task ids and getting/setting the active task id */
1616
+ registerIdTools(mcp) {
1589
1617
  /* task id generation */
1590
1618
  mcp.registerTool("ase_task_newid", {
1591
1619
  title: "ASE task id generation",
@@ -50,7 +50,7 @@ export default class CompatCommand {
50
50
  });
51
51
  }
52
52
  }
53
- /* MCP tool "ase_compat" — lets the skill call this without a Bash tool */
53
+ /* MCP tool "ase_compat" -- lets the skill call this without a Bash tool */
54
54
  export class CompatMCP {
55
55
  register(mcp) {
56
56
  mcp.registerTool("ase_compat", {