@rse/ase 2.0.7 → 2.0.8

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 (33) hide show
  1. package/dst/ase-agent-relay-client.js +3 -4
  2. package/dst/ase-config-schema.js +4 -1
  3. package/dst/ase-lib-fetch.js +12 -0
  4. package/dst/ase-lib-server-cli.js +8 -5
  5. package/dst/ase-task-board-diff.js +120 -0
  6. package/dst/ase-task-board-tui-control.js +8 -5
  7. package/dst/ase-task-board-tui-model.js +54 -14
  8. package/dst/ase-task-board-tui-popup.js +41 -8
  9. package/dst/ase-task-board-tui.js +42 -27
  10. package/dst/ase-task-board-web-client.css +1 -1
  11. package/dst/ase-task-board-web-client.html +16 -0
  12. package/dst/ase-task-board-web-client.js +33 -33
  13. package/dst/ase-task-board-web-server.js +44 -9
  14. package/dst/ase-task-board.js +12 -4
  15. package/dst/ase-task-store-client.js +4 -5
  16. package/dst/man/ase-config-init.md +2 -1
  17. package/dst/man/ase-config.md +5 -0
  18. package/dst/man/ase-task-board.md +44 -2
  19. package/package.json +7 -6
  20. package/plugin/.claude-plugin/plugin.json +1 -1
  21. package/plugin/.codex-plugin/plugin.json +1 -1
  22. package/plugin/.github/plugin/plugin.json +1 -1
  23. package/plugin/meta/ase-common-task.md +24 -0
  24. package/plugin/mods/ase-mods-common.ts +73 -42
  25. package/plugin/mods/ase-mods-task-edit.ts +427 -0
  26. package/plugin/mods/ase-mods.ts +45 -14
  27. package/plugin/skills/ase-help-intent/data.md +78 -8
  28. package/plugin/skills/ase-task-edit/SKILL.md +114 -9
  29. package/plugin/skills/ase-task-edit/help.md +60 -0
  30. package/plugin/skills/ase-task-grill/SKILL.md +35 -20
  31. package/plugin/skills/ase-task-grill/help.md +18 -8
  32. /package/plugin/mods/{ase-mods-evaluate.ts → ase-mods-meta-evaluate.ts} +0 -0
  33. /package/plugin/mods/{ase-mods-grill.ts → ase-mods-task-grill.ts} +0 -0
@@ -13,6 +13,7 @@ import { userId } from "./ase-service-otel.js";
13
13
  import { Config } from "./ase-config-core.js";
14
14
  import { configSchema, webColorDefaults, webColorNames } from "./ase-config-schema.js";
15
15
  import { buildBoard, watchTasks, toneOf, laneMoves, cardMoves, BoardState, attachmentTabs, isDiff, diffTones, dataLang, dataLoad, dataSpans, newTaskText, createTask, saveTask, TaskConflict, watchAgents } from "./ase-task-board-core.js";
16
+ import { diffHunks, diffMarkdown, diffMarksToHTML } from "./ase-task-board-diff.js";
16
17
  import { layoutGraph, drawGraphSVG } from "./ase-task-board-graph.js";
17
18
  import { flowOf, layoutFlow, drawFlowSVG } from "./ase-task-board-flow.js";
18
19
  import { filterBoard, dropStandalone } from "./ase-task-board-filter.js";
@@ -94,9 +95,9 @@ const markedLoad = async (remote) => {
94
95
  (title ? ` title="${escapeHTML(title)}"` : "") + ">" : escapeHTML(text);
95
96
  },
96
97
  code({ text, lang }) {
97
- if (lang === "mermaid") {
98
+ if (lang === "mermaid" && !text.includes("⟦chg:")) {
98
99
  try {
99
- return `<div class="diagram">${renderMermaidSVG(text)}</div>`;
100
+ return `<div class="diagram">${renderMermaidSVG(text, { bg: "var(--c-paper)", fg: "var(--c-text)" })}</div>`;
100
101
  }
101
102
  catch {
102
103
  /* fall through to the plain rendering of the source */
@@ -119,12 +120,12 @@ const renderPlan = async (body, remote = false) => {
119
120
  const outside = fence === "";
120
121
  fence = TaskFormat.fenceTrack(fence, line);
121
122
  return !outside || fence !== "" ? line : line
122
- .replace(/^(\s*(?:[-*]|\d+[.)])\s+)\[([ x/?\->])\]/, (_m, lead, box) => `${lead}⟦box:${boxes[box]}⟧`);
123
+ .replace(/^(\s*(?:[-*]|\d+[.)])\s+(?:⟦chg:[a-z-]+⟧)?)\[([ x/?\->])\]/, (_m, lead, box) => `${lead}⟦box:${boxes[box]}⟧`);
123
124
  }).join("\n");
124
125
  if (!marked.has(remote))
125
126
  marked.set(remote, markedLoad(remote));
126
127
  const html = (await marked.get(remote)).parse(prepared, { async: false });
127
- return html.replace(/⟦box:([a-z]+)⟧/g, (_m, state) => `<span class="box ${state}" title="[${escapeHTML(boxChars[state] ?? " ")}]"></span>`);
128
+ return diffMarksToHTML(html.replace(/⟦box:([a-z]+)⟧/g, (_m, state) => `<span class="box ${state}" title="[${escapeHTML(boxChars[state] ?? " ")}]"></span>`));
128
129
  };
129
130
  const colorRoles = Object.keys(webColorDefaults);
130
131
  const colorBases = (log) => {
@@ -154,10 +155,18 @@ const editorKeymap = (log) => {
154
155
  const value = cfg.get("board.web.editor.keymap");
155
156
  return value === "vim" || value === "emacs" ? value : "default";
156
157
  };
157
- /* render a standalone document (styled by the client stylesheet) out of
158
- key/value fields, separated by a line from the content */
158
+ /* the color theme of the web board, resolved from the
159
+ "board.web.theme" configuration (on each page load) */
160
+ const boardTheme = (log) => {
161
+ const cfg = new Config("config", configSchema, log);
162
+ cfg.read();
163
+ const value = cfg.get("board.web.theme");
164
+ return value === "light" || value === "dark" ? value : "auto";
165
+ };
166
+ /* render a standalone document (styled by the client stylesheet) out of key/value
167
+ fields (optionally marked as added or removed), separated by a line from the content */
159
168
  const fieldsDocument = (fields, content) => {
160
- const rows = fields.map(([k, v]) => `<tr><td class="k">${escapeHTML(k)}</td><td>${escapeHTML(v)}</td></tr>`).join("");
169
+ const rows = fields.map(([k, v, mark]) => `<tr${mark !== undefined ? ` class="${mark}"` : ""}><td class="k">${escapeHTML(k)}</td><td>${escapeHTML(v)}</td></tr>`).join("");
161
170
  return "<!DOCTYPE html><html><head><meta charset=\"utf-8\">" +
162
171
  "<link rel=\"stylesheet\" href=\"/task-board/colors.css\">" +
163
172
  "<link rel=\"stylesheet\" href=\"/task-board/ase-task-board-web-client.css\"></head><body class=\"plan\"><article>" +
@@ -166,6 +175,16 @@ const fieldsDocument = (fields, content) => {
166
175
  };
167
176
  /* render a task plan as a standalone document: frontmatter fields and the numbered plan sections */
168
177
  const taskDocument = async (keys, body) => fieldsDocument([...keys].filter(([k]) => k !== "Type"), await renderPlan(body));
178
+ const isPlanText = (value) => {
179
+ const plan = value;
180
+ return typeof plan?.body === "string" && Array.isArray(plan.keys) && plan.keys.every((field) => Array.isArray(field) && field.length === 2 && field.every((text) => typeof text === "string"));
181
+ };
182
+ /* render the diff of two task plans as a standalone document, in the look of a task plan */
183
+ const diffDocument = async (before, after) => fieldsDocument(diffHunks(before.keys, after.keys, (field) => JSON.stringify(field)).flatMap(({ kept, removed, added }) => [
184
+ ...kept,
185
+ ...removed.map(([k, v]) => [k, v, "del"]),
186
+ ...added.map(([k, v]) => [k, v, "add"])
187
+ ]), await renderPlan(diffMarkdown(before.body, after.body)));
169
188
  /* render a task plan attachment as a standalone document: its fields and
170
189
  its content, with images embedded, Markdown rendered, YAML or JSON data syntax-highlighted, a diff
171
190
  colored per line, binary content as a placeholder, and else verbatim text */
@@ -335,11 +354,13 @@ const guarded = (handler) => async (request, h) => {
335
354
  };
336
355
  /* register the page routes of the web board: its client assets and its colors */
337
356
  const registerPageRoutes = (server, log) => {
338
- /* the single page of the web board, with its stylesheet and browser-side code */
357
+ /* the single page of the web board, with its stylesheet and browser-side code,
358
+ and with its color theme (resolved on each page load) on its root element */
339
359
  server.route({
340
360
  method: "GET",
341
361
  path: "/task-board",
342
- handler: (_request, h) => h.response(clientAsset("html")).type("text/html; charset=utf-8")
362
+ handler: (_request, h) => h.response(clientAsset("html")
363
+ .replace(/<html\b/, `<html data-theme="${boardTheme(log)}"`)).type("text/html; charset=utf-8")
343
364
  });
344
365
  server.route({
345
366
  method: "GET",
@@ -439,12 +460,26 @@ const registerViewRoutes = (server, log) => {
439
460
  group: board.groups.find((g) => g.lanes.some((l) => l.status === card.status))?.title ?? "",
440
461
  epic: card.group,
441
462
  doc: await taskDocument(parts.keys, parts.body),
463
+ plan: { keys: [...parts.keys].filter(([k]) => k !== "Type"), body: parts.body },
442
464
  tabs: attachmentTabs(await Task.attachments(log, id), parts.keys.get("Modified") ?? ""),
443
465
  pred: ref(board.pred.get(id) ?? []),
444
466
  succ: ref(board.succ.get(id) ?? [])
445
467
  });
446
468
  })
447
469
  });
470
+ /* the diff of two states of a task plan, rendered statelessly out of the states
471
+ kept by the client, as the changes are followed per task dialog */
472
+ server.route({
473
+ method: "POST",
474
+ path: "/task-board/api/diff",
475
+ options: { payload: { parse: true, allow: "application/json" } },
476
+ handler: guarded(async (request, h) => {
477
+ const p = request.payload;
478
+ if (!isPlanText(p?.before) || !isPlanText(p.after))
479
+ return h.response({ error: "invalid diff request" }).code(400);
480
+ return h.response({ doc: await diffDocument(p.before, p.after) });
481
+ })
482
+ });
448
483
  /* one task plan as text for editing, with the entity tag it is based on and the editor key bindings */
449
484
  server.route({
450
485
  method: "GET",
@@ -108,27 +108,35 @@ export default class TaskBoardCommand {
108
108
  .option("--agent-relay <url>", "agent relay to receive the live presence of the agents from, " +
109
109
  "as \"ase[s]://<addr>:<port>[/<token>]\" (default: the configured \"project.relay.url\", else none; " +
110
110
  "the web board always follows the configured one)")
111
+ .option("-T, --task <id>", "show only the task dialog of the given task as a task viewer, " +
112
+ "following all its changes live (with --text or without a terminal: print its plan and exit)")
111
113
  .action(async (opts) => {
114
+ /* refuse the task viewer on an unknown task */
115
+ const plan = opts.task !== undefined ? await Task.load(this.log, opts.task) : "";
116
+ if (opts.task !== undefined && plan === "")
117
+ throw new Error(`board: no task "${opts.task}"`);
112
118
  /* serve and open the web board */
113
119
  if (opts.web === true) {
114
120
  if (opts.agentRelay !== undefined)
115
121
  this.log.write("warning", "board: \"--agent-relay\" is ignored by the web board, " +
116
122
  "which follows the configured \"project.relay.url\"");
117
- const url = `http://${SERVICE_HOST}:${await this.servicePort()}/task-board`;
123
+ const url = `http://${SERVICE_HOST}:${await this.servicePort()}/task-board` +
124
+ (opts.task !== undefined ? `?task=${encodeURIComponent(opts.task)}` : "");
118
125
  await writeStdout(`${url}\n`);
119
126
  await this.openBrowser(url);
120
127
  return;
121
128
  }
122
- /* print the plain text overview if requested or without a terminal */
129
+ /* print the plain text overview (resp. the plan of the task
130
+ of the task viewer) if requested or without a terminal */
123
131
  if (opts.text === true || !process.stdout.isTTY || !process.stdin.isTTY) {
124
- await writeStdout(await textOverview(this.log, opts.agentRelay));
132
+ await writeStdout(opts.task !== undefined ? plan : await textOverview(this.log, opts.agentRelay));
125
133
  return;
126
134
  }
127
135
  /* run the interactive terminal board, with the production build of React
128
136
  (its development build records a never cleared performance measure per render) */
129
137
  process.env.NODE_ENV ??= "production";
130
138
  const { runTUI } = await import("./ase-task-board-tui.js");
131
- await runTUI(this.log, opts.agentRelay);
139
+ await runTUI(this.log, opts.agentRelay, opts.task);
132
140
  });
133
141
  }
134
142
  }
@@ -5,10 +5,9 @@
5
5
  */
6
6
  import path from "node:path";
7
7
  import fs from "node:fs";
8
- import { ofetch } from "ofetch";
9
- import { Agent } from "undici";
10
8
  import { WebSocket } from "ws";
11
9
  import { LRUCache } from "lru-cache";
10
+ import { dispatchFetch, insecureAgent } from "./ase-lib-fetch.js";
12
11
  import { Config } from "./ase-config-core.js";
13
12
  import { configSchema } from "./ase-config-schema.js";
14
13
  import { parseScope, userStateDir } from "./ase-config-scope.js";
@@ -217,7 +216,7 @@ export class RemoteTaskStoreClient {
217
216
  this.insecure = insecure;
218
217
  this.lifecycle = configured;
219
218
  this.idscheme = configuredIdScheme;
220
- this.dispatcher = insecure ? new Agent({ connect: { rejectUnauthorized: false } }) : undefined;
219
+ this.dispatcher = insecure ? insecureAgent() : undefined;
221
220
  }
222
221
  /* raise a failed connection as an unreachable store error,
223
222
  reporting the innermost cause (like "connect ECONNREFUSED") */
@@ -232,7 +231,7 @@ export class RemoteTaskStoreClient {
232
231
  a null result, any other error response is raised as a problem carrying
233
232
  its status, and a failed connection is raised as an unreachable store error */
234
233
  async request(method, url, body, headers = {}, tolerated = []) {
235
- const r = await ofetch.raw(`${this.base}${url}`, {
234
+ const r = await dispatchFetch.raw(`${this.base}${url}`, {
236
235
  method,
237
236
  headers: {
238
237
  Authorization: `Bearer ${this.token}`,
@@ -408,7 +407,7 @@ export class RemoteTaskStoreClient {
408
407
  return await this.task(await this.request("POST", `${this.tasks}/${encodeURIComponent(id)}/attachment${append ? "?append=true" : ""}`, attachment)) !== null;
409
408
  }
410
409
  async content(id, index) {
411
- const r = await ofetch.raw(`${this.base}${this.tasks}/${encodeURIComponent(id)}/attachment/${index}/content`, {
410
+ const r = await dispatchFetch.raw(`${this.base}${this.tasks}/${encodeURIComponent(id)}/attachment/${index}/content`, {
412
411
  headers: { Authorization: `Bearer ${this.token}` },
413
412
  responseType: "arrayBuffer",
414
413
  dispatcher: this.dispatcher,
@@ -63,7 +63,8 @@ file lock and atomically.
63
63
  `project.task.effort.line` `0.025`,
64
64
  `project.task.effort.iteration` `2.5`, the full set of
65
65
  `project.artifact.*` globs, the `board.tui.color.*`,
66
- `board.web.color.*`, and `board.web.editor.keymap` defaults, and
66
+ `board.web.color.*`, `board.web.editor.keymap`, and
67
+ `board.web.theme` defaults, and
67
68
  `telemetry.enabled` `false`, `telemetry.prices.ai`
68
69
  `*=4.00/20.00/0.20/5.00`, and `telemetry.prices.hr` `*=110`.
69
70
  - `vibe`:
@@ -361,6 +361,11 @@ Task board (all writable on the `user` and `project` scopes only, see
361
361
  The key bindings of the task plan editor of the web board (default:
362
362
  `default`).
363
363
 
364
+ - `board.web.theme` (`auto`, `light`, or `dark`):
365
+ The color theme of the web board, where `auto` follows the light or
366
+ dark preference of the operating system resp. browser (default:
367
+ `auto`).
368
+
364
369
  ## ENVIRONMENT
365
370
 
366
371
  - `EDITOR`, `VISUAL`:
@@ -7,6 +7,7 @@
7
7
  \[`-w`|`--web`\]
8
8
  \[`-t`|`--text`\]
9
9
  \[`--agent-relay` *url*\]
10
+ \[`-T`|`--task` *id*\]
10
11
 
11
12
  ## DESCRIPTION
12
13
 
@@ -89,6 +90,11 @@ set snapshot `kind="changeset"` of the `implementer` and `integrator`
89
90
  agents) is colored per line, YAML and JSON data are syntax-highlighted,
90
91
  and a change set snapshot older than the plan is marked `(stale)`.
91
92
 
93
+ While the task dialog is open, the last change of the shown plan is displayed
94
+ as a diff in the plan tab, with the removed and added lines colored (in the
95
+ terminal board also signed) and their changed words shown inverse, until `#` clears it or a next change
96
+ replaces it. Closing the task dialog or switching to another task drops it.
97
+
92
98
  A task is moved into another state (changing its `Status:`) either by
93
99
  picking it up with `SPACE` and dropping it with a second `SPACE` onto
94
100
  the selected lane (only lanes whose state is reachable from the current
@@ -214,6 +220,26 @@ waiting) with their status and their last processing log line, and
214
220
  finally all warnings of the board (behind `⚠`, e.g. on dependency
215
221
  cycles).
216
222
 
223
+ ### Task Viewer
224
+
225
+ With `--task`, the terminal and the web board act as a *task viewer*:
226
+ instead of the board, they show only the task dialog of the given task,
227
+ spanning the entire window and following all changes of the task live.
228
+ All actions of the task dialog stay available, and a click onto a
229
+ predecessor or successor shows that task instead. The shown view of the
230
+ board stays untouched. An unknown task is refused with an error.
231
+
232
+ The terminal board quits on `q` or `Ctrl`+`C` only, and with a warning
233
+ once the shown task no longer exists (e.g. as it was deleted or renamed
234
+ elsewhere).
235
+
236
+ The web board has no closing keys, as a page cannot close its browser
237
+ tab. Once the shown task no longer exists, it shows only a notice, and
238
+ shows the task again as soon as it exists again.
239
+
240
+ With `--text`, and also without an interactive terminal, the plan of the
241
+ task is printed as plain text and the command exits.
242
+
217
243
  ### Statistics View
218
244
 
219
245
  The third view *statistics* shows the statistics of the tasks matching the
@@ -410,6 +436,10 @@ relay, the agent graph view is skipped.
410
436
  board always follows the configured `project.relay.url` (and hence
411
437
  warns about the option).
412
438
 
439
+ - \[`-T`|`--task` *id*\]:
440
+ Show only the task dialog of the task *id* instead of the board, as a
441
+ viewer and watcher of the task (see **Task Viewer**).
442
+
413
443
  ## KEYS
414
444
 
415
445
  The following keys are supported by the terminal board (and, unless
@@ -426,6 +456,9 @@ letter under the kitty keyboard protocol.
426
456
  views).
427
457
  - `?`: Hide or show the key hint lines (persisted); while hidden, the
428
458
  information line ends with `?: show key hints`.
459
+ - `L`: Cycle the color theme through `auto`, `light`, and `dark`,
460
+ overriding `board.web.theme` in the current browser until cycled back
461
+ to its value (web board only).
429
462
  - `D`: Delete the selected task, after a confirmation (not in the
430
463
  agent logs and statistics views).
431
464
  - `T`: Transfer the selected task to another state via the transfer
@@ -515,7 +548,8 @@ letter under the kitty keyboard protocol.
515
548
  - `↑`/`↓`: Scroll the content.
516
549
  - `PgUp`/`PgDn`: Scroll the content by one page.
517
550
  - `e`: Edit the task plan.
518
- - `D`, `T`, `A`, `E`, `R`, `M`: As in every view.
551
+ - `#`: Clear the diff of the last change of the task plan.
552
+ - `D`, `T`, `A`, `E`, `R`, `M`, `L`: As in every view.
519
553
  - `⏎`, `ESC`: Close the task dialog.
520
554
 
521
555
  ### Filter Field
@@ -594,6 +628,11 @@ The following configuration keys (on scope `user` or `project`, see
594
628
  The key bindings of the task plan editor of the web board: `default`,
595
629
  `vim`, or `emacs` (default: `default`).
596
630
 
631
+ - `board.web.theme`:
632
+ The color theme of the web board: `light`, `dark`, or `auto`, which
633
+ follows the light or dark preference of the operating system resp.
634
+ browser (default: `auto`).
635
+
597
636
  - `project.relay.url`:
598
637
  The default agent relay of the boards (see `--agent-relay`).
599
638
 
@@ -625,7 +664,7 @@ The following configuration keys (on scope `user` or `project`, see
625
664
  The command exits with status `0` once the terminal board is quit, the
626
665
  text overview is printed, resp. the web board is opened, and with
627
666
  status `1` on any error (e.g. an *ASE* service without web board on
628
- `--web`).
667
+ `--web`, or an unknown task on `--task`).
629
668
 
630
669
  ## EXAMPLES
631
670
 
@@ -636,6 +675,9 @@ $ ase task board
636
675
  # show the web board in the browser
637
676
  $ ase task board --web
638
677
 
678
+ # view and watch a single task in the terminal
679
+ $ ase task board --task T1
680
+
639
681
  # print the lane overview, including the agents of a remote agent relay
640
682
  $ ase task board --text --agent-relay ase://relay.example.com:42043
641
683
  ```
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "homepage": "https://ase.tools",
7
7
  "repository": { "url": "git+https://github.com/rse/ase.git", "type": "git" },
8
8
  "bugs": { "url": "https://github.com/rse/ase/issues" },
9
- "version": "2.0.7",
9
+ "version": "2.0.8",
10
10
  "license": "Apache-2.0",
11
11
  "author": {
12
12
  "name": "Dr. Ralf S. Engelschall",
@@ -31,7 +31,7 @@
31
31
  "globals": "17.14.0",
32
32
  "typescript": "6.0.3",
33
33
  "eslint-plugin-vue": "10.12.0",
34
- "vue-eslint-parser": "10.4.1",
34
+ "vue-eslint-parser": "10.4.2",
35
35
  "vue-tsc": "3.3.14",
36
36
 
37
37
  "@rse/stx": "1.1.7",
@@ -39,9 +39,9 @@
39
39
  "shx": "0.4.0",
40
40
  "sync-directory": "6.0.6",
41
41
  "stylus": "0.64.0",
42
- "vite": "8.3.5",
42
+ "vite": "8.3.6",
43
43
  "@vitejs/plugin-vue": "6.0.10",
44
- "vue": "3.5.44",
44
+ "vue": "3.5.45",
45
45
  "mousetrap": "1.6.5",
46
46
  "@codemirror/view": "6.43.15",
47
47
  "@codemirror/state": "6.7.6",
@@ -78,7 +78,7 @@
78
78
  "valibot": "1.5.0",
79
79
  "execa": "10.2.1",
80
80
  "mkdirp": "3.0.1",
81
- "@hapi/hapi": "21.4.12",
81
+ "@hapi/hapi": "21.4.13",
82
82
  "beautiful-mermaid": "1.1.3",
83
83
  "@d2lang/d2": "0.1.34",
84
84
  "elkjs": "0.12.1",
@@ -87,6 +87,7 @@
87
87
  "react": "19.3.1",
88
88
  "chokidar": "6.0.0",
89
89
  "marked": "18.1.3",
90
+ "diff": "9.0.0",
90
91
  "shiki": "4.6.0",
91
92
  "cli-table3": "0.6.5",
92
93
  "chalk": "6.0.1",
@@ -105,7 +106,7 @@
105
106
  "p-queue": "10.0.0",
106
107
  "pacote": "22.0.1",
107
108
  "ofetch": "1.5.1",
108
- "undici": "8.12.3",
109
+ "undici": "8.12.4",
109
110
  "picomatch": "4.0.7",
110
111
  "lru-cache": "11.5.3",
111
112
  "@rse/specbook": "1.6.2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ase",
3
- "version": "2.0.7",
3
+ "version": "2.0.8",
4
4
  "description": "Agentic Software Engineering (ASE)",
5
5
  "keywords": [ "agentic", "software", "engineering" ],
6
6
  "homepage": "https://ase.tools",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ase",
3
- "version": "2.0.7",
3
+ "version": "2.0.8",
4
4
  "description": "Agentic Software Engineering (ASE)",
5
5
  "keywords": [ "agentic", "software", "engineering" ],
6
6
  "homepage": "https://ase.tools",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ase",
3
- "version": "2.0.7",
3
+ "version": "2.0.8",
4
4
  "description": "Agentic Software Engineering (ASE)",
5
5
  "keywords": [ "agentic", "software", "engineering" ],
6
6
  "homepage": "https://ase.tools",
@@ -58,6 +58,30 @@ Task Skill Common Steps
58
58
 
59
59
  </define>
60
60
 
61
+ <define name="task-focus">
62
+
63
+ Resolve the plan sections under focus: treat <getopt-option-focus/> as a
64
+ comma-separated list of *section tokens*. Trim every token, then expand
65
+ the synonyms `SPEC` and `WHAT` to `SPECIFICATION`, the synonyms `DES` and
66
+ `HOW` to `DESIGN`, and the synonyms `VERIFY`, `VER`, and `WHEN` to
67
+ `VERIFICATION`, and expand the sentinel `all` (the default) to
68
+ `SPECIFICATION,DESIGN,VERIFICATION`. Set <sections/> to the resulting
69
+ list, *keeping the given order*. The getopt parser validates only the
70
+ *first* token, so you *MUST* validate each token yourself: if any token
71
+ is *empty*, *not* one of the recognized sections or synonyms (matched
72
+ *case-sensitively*), or results in a section already present in
73
+ <sections/>, bind <token/> to that offending token, then only output the
74
+ following <template/> and immediately *STOP* processing the entire
75
+ current skill:
76
+
77
+ <template>
78
+ ⧉ **ASE**: ☻ skill: **<arg1/>**, ▶ ERROR: invalid `--focus` token: **<token/>**
79
+ </template>
80
+
81
+ Do not output anything else.
82
+
83
+ </define>
84
+
61
85
  <define name="task-load-content">
62
86
 
63
87
  <if condition="