@rse/ase 2.0.6 → 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 (68) hide show
  1. package/dst/ase-agent-definition-api.d.ts +2 -0
  2. package/dst/ase-agent-definition-core.js +24 -3
  3. package/dst/ase-agent-fleet.js +2 -1
  4. package/dst/ase-agent-relay-client.js +3 -4
  5. package/dst/ase-agent-runtime-engine.js +22 -4
  6. package/dst/ase-agent-runtime-tool.js +10 -8
  7. package/dst/ase-agent-runtime.js +8 -4
  8. package/dst/ase-config-schema.js +4 -1
  9. package/dst/ase-lib-fetch.js +12 -0
  10. package/dst/ase-lib-server-cli.js +8 -5
  11. package/dst/ase-man-catalog.js +1 -1
  12. package/dst/ase-task-board-diff.js +120 -0
  13. package/dst/ase-task-board-tui-control.js +8 -5
  14. package/dst/ase-task-board-tui-model.js +54 -14
  15. package/dst/ase-task-board-tui-popup.js +41 -8
  16. package/dst/ase-task-board-tui.js +42 -27
  17. package/dst/ase-task-board-web-client.css +1 -1
  18. package/dst/ase-task-board-web-client.html +16 -0
  19. package/dst/ase-task-board-web-client.js +33 -33
  20. package/dst/ase-task-board-web-server.js +44 -9
  21. package/dst/ase-task-board.js +12 -4
  22. package/dst/ase-task-store-client.js +4 -5
  23. package/dst/ase-task-store-plugin-gitea.js +3 -3
  24. package/dst/man/ase-agent-adhoc.md +25 -6
  25. package/dst/man/ase-agent-condenser.md +7 -0
  26. package/dst/man/ase-agent-editor.md +7 -0
  27. package/dst/man/ase-agent-estimator.md +7 -0
  28. package/dst/man/ase-agent-fleet.md +7 -4
  29. package/dst/man/ase-agent-implementer.md +7 -0
  30. package/dst/man/ase-agent-integrator.md +7 -0
  31. package/dst/man/ase-agent-qualifier.md +7 -0
  32. package/dst/man/ase-agent-rebooter.md +7 -0
  33. package/dst/man/ase-agent-researcher.md +7 -0
  34. package/dst/man/ase-config-init.md +2 -1
  35. package/dst/man/ase-config.md +5 -0
  36. package/dst/man/ase-task-board.md +44 -2
  37. package/package.json +11 -10
  38. package/plugin/.claude-plugin/plugin.json +1 -1
  39. package/plugin/.codex-plugin/plugin.json +1 -1
  40. package/plugin/.github/plugin/plugin.json +1 -1
  41. package/plugin/meta/ase-common-task.md +33 -1
  42. package/plugin/meta/ase-control.md +14 -0
  43. package/plugin/meta/ase-format-specbook.yaml +9 -9
  44. package/plugin/meta/ase-skill.md +7 -0
  45. package/plugin/mods/ase-mods-common.ts +73 -42
  46. package/plugin/mods/ase-mods-help-hint.ts +1 -1
  47. package/plugin/mods/ase-mods-task-edit.ts +427 -0
  48. package/plugin/mods/ase-mods.ts +45 -14
  49. package/plugin/skills/ase-code-edit/SKILL.md +53 -16
  50. package/plugin/skills/ase-code-edit/help.md +26 -13
  51. package/plugin/skills/ase-code-lint/SKILL.md +7 -3
  52. package/plugin/skills/ase-help-intent/data.md +248 -36
  53. package/plugin/skills/ase-help-skill/catalog.md +1 -0
  54. package/plugin/skills/ase-meta-workflow/workflow.txt +2 -0
  55. package/plugin/skills/ase-repo-changelog/SKILL.md +29 -10
  56. package/plugin/skills/ase-repo-changelog/help.md +5 -1
  57. package/plugin/skills/ase-repo-commit/SKILL.md +4 -1
  58. package/plugin/skills/ase-repo-dissect/help.md +2 -1
  59. package/plugin/skills/ase-repo-stage/SKILL.md +459 -0
  60. package/plugin/skills/ase-repo-stage/help.md +120 -0
  61. package/plugin/skills/ase-spec-edit/SKILL.md +20 -12
  62. package/plugin/skills/ase-spec-edit/help.md +22 -14
  63. package/plugin/skills/ase-task-edit/SKILL.md +114 -9
  64. package/plugin/skills/ase-task-edit/help.md +60 -0
  65. package/plugin/skills/ase-task-grill/SKILL.md +35 -20
  66. package/plugin/skills/ase-task-grill/help.md +18 -8
  67. /package/plugin/mods/{ase-mods-evaluate.ts → ase-mods-meta-evaluate.ts} +0 -0
  68. /package/plugin/mods/{ase-mods-grill.ts → ase-mods-task-grill.ts} +0 -0
@@ -0,0 +1,427 @@
1
+ /*
2
+ ** Agentic Software Engineering (ASE)
3
+ ** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
+ ** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
5
+ */
6
+
7
+ /* Claude Code function hooks module ("mod", EARLY ACCESS):
8
+ Task band, showing for "ase-task-edit --interactive" (signaled through a tool
9
+ registered by this mod, whose absence tells the skill that no band exists) the
10
+ task in the band above the prompt, rendered like the read dialog of the task
11
+ board TUI, following all changes of the task (keeping the scroll position and
12
+ showing the last change as a unified diff with line numbers) and scrolled by the
13
+ wheel, while the prompt stays free for typing. A "##" typed at the end of the
14
+ prompt drops the diff, showing just the latest state of the task. The band closes
15
+ once a next step of the skill (like "DONE") is submitted at the prompt. */
16
+
17
+ import type { Elements, PromptBox, RenderElement, Timer } from "claude-code"
18
+ import { type Engine, type Char, type BandRender, type BandScroll, type View, PICKED,
19
+ uniform, width, wrap, layout, wheel, panel } from "./ase-mods-common.ts"
20
+
21
+ /* the tool signaling the band, as listed for the model */
22
+ export const TOOL = {
23
+ name: "ase_task_band",
24
+ description: "Open the band above the prompt, which shows the task `id` and follows its changes, " +
25
+ "for \"ase-task-edit --interactive\". Returns `OK: ...` or `ERROR: ...` as text.",
26
+ inputSchema: {
27
+ type: "object",
28
+ properties: {
29
+ id: { type: "string", description: "task identifier" },
30
+ sections: { type: "string", description: "comma-separated names of the plan sections under focus, " +
31
+ "or empty to show the entire task" }
32
+ },
33
+ required: [ "id" ]
34
+ }
35
+ }
36
+
37
+ /* the next steps of the skill closing the band once submitted, the entity tag
38
+ at the end of a loaded task (present for an existing task only), the share of
39
+ the terminal height taken by the band outside fullscreen, the interval of
40
+ following the task changes, the least digits of the line numbers, the columns of
41
+ comparing the shown task, and the colors of the rendering (the "dim", "accent", and
42
+ "signal" roles of the task board TUI, the first also for the line numbers, and the
43
+ lines and words of the diff, with the codes the diffs of the session end up with, so
44
+ they look alike under any recoloring, where the accent and the removed lines use
45
+ the terminal palette, as the engine maps a color name to a fixed color and
46
+ refuses "ansi:*"), and the changed share of the words up to which they are shown */
47
+ const CLOSE = /^(?:DONE|GRILL|DRAFT|IMPLEMENT|CANCEL)$/i
48
+ const TAG = /\n<task-plan-tag>[^<]*<\/task-plan-tag>\s*$/
49
+ const HEIGHT = 0.75
50
+ const POLL = 2000
51
+ const DIGITS = 2
52
+ const WIDE = 1000
53
+ const DIM = "ansi256(244)"
54
+ const ACCENT = "ansi256(4)"
55
+ const SIGNAL = PICKED
56
+ const REMOVE = "ansi256(1)"
57
+ const ADDBG = "ansi256(194)"
58
+ const DELBG = "ansi256(224)"
59
+ const ADDWD = "ansi256(157)"
60
+ const DELWD = "ansi256(217)"
61
+ const DELFG = "ansi256(16)"
62
+ const ALIKE = 0.4
63
+
64
+ /* a task band: the scroll state of its frame, the task, the plan sections under
65
+ focus (empty for the entire task), the loaded task and its predecessor (if it
66
+ already changed), and the lines of the task with the digits of their line
67
+ numbers and the band columns they were wrapped for */
68
+ type Band = View & {
69
+ id: string,
70
+ sections: string[],
71
+ loaded: string,
72
+ before: string | undefined,
73
+ lines: Char[][],
74
+ digits: number,
75
+ columns: number
76
+ }
77
+
78
+ /* make a text safe for the terminal: expand tabs and strip control characters
79
+ (a copy of "sanitize" of "tool/src/ase-task-board-tui-popup.ts", so keep it in sync) */
80
+ const sanitize = (text: string): string =>
81
+ text.replace(/\t/g, " ").replace(/[\p{Cc}\u{061C}\u{200E}\u{200F}\u{202A}-\u{202E}\u{2066}-\u{2069}]/gu, "")
82
+
83
+ /* a shown row of a task: its styled characters and the line of the task text it
84
+ shows, as seen in the editor (absent for the continuation of a wrapped line) */
85
+ type Row = { chars: Char[], number: number | undefined }
86
+
87
+ /* split a task into the keys of its frontmatter (without "Type"), each with its
88
+ line, and its body with its first line (without the backmatter, which starts
89
+ at the first "---" line outside a fenced code block) */
90
+ const parts = (source: string): { keys: [ string, string, number ][], body: string, first: number } => {
91
+ const head = /^---\n([^]*?)\n---\n/.exec(source)
92
+ const lines = source.slice(head?.[0].length ?? 0).split("\n")
93
+ let fence = false
94
+ const end = lines.findIndex((line) => {
95
+ if (line.startsWith("```"))
96
+ fence = !fence
97
+ return !fence && line === "---"
98
+ })
99
+ return {
100
+ keys: (head?.[1] ?? "").split("\n").flatMap((line, k): [ string, string, number ][] => {
101
+ const m = /^([^:\s]+):\s*(.*)$/.exec(line)
102
+ return m !== null && m[1] !== "Type" ? [ [ m[1]!, m[2]!, k + 2 ] ] : []
103
+ }),
104
+ body: (end === -1 ? lines : lines.slice(0, end)).join("\n"),
105
+ first: head?.[0].split("\n").length ?? 1
106
+ }
107
+ }
108
+
109
+ /* convert inline Markdown (code spans, bold, and italic) into styled characters */
110
+ const inline = (text: string, bold: boolean, color: string | undefined): Char[] => {
111
+ const chars: Char[] = []
112
+ let last = 0
113
+ for (const m of text.matchAll(/`([^`]+)`|\*\*(.+?)\*\*|__(.+?)__|\*([^*\s](?:[^*]*?[^*\s])?)\*/g)) {
114
+ chars.push(...uniform(text.slice(last, m.index), bold, color))
115
+ const body = uniform(m[1] ?? m[2] ?? m[3] ?? m[4]!, bold || m[2] !== undefined || m[3] !== undefined,
116
+ m[1] !== undefined ? ACCENT : color)
117
+ chars.push(...(m[4] !== undefined ? body.map((char) => ({ ...char, italic: true })) : body))
118
+ last = m.index + m[0].length
119
+ }
120
+ chars.push(...uniform(text.slice(last), bold, color))
121
+ return chars
122
+ }
123
+
124
+ /* convert a list marker into styled characters, where a checkbox of an item which
125
+ is done, in progress, or open is accented, and any other checkbox is signaled */
126
+ const marker = (prefix: string): Char[] => {
127
+ const chars = uniform(prefix, false, undefined)
128
+ const m = /^(\s*(?:[-*]|\d+[.)])\s+)\[(.)\]/.exec(prefix)
129
+ if (m !== null)
130
+ for (let k = m[1]!.length; k < m[1]!.length + 3; k++)
131
+ chars[k] = { ...chars[k]!, color: /[x/ ]/.test(m[2]!) ? ACCENT : SIGNAL }
132
+ return chars
133
+ }
134
+
135
+ /* convert a Markdown body, starting at the given line of the task text, into rows,
136
+ hard-wrapped to the given columns (except for the lines of fenced code blocks) */
137
+ const markdown = (body: string, columns: number, first: number): Row[] => {
138
+ const rows: Row[] = []
139
+ const lines = body.split("\n")
140
+ const wrapped = (chars: Char[][], number: number): Row[] =>
141
+ chars.map((chars, k) => ({ chars, number: k === 0 ? number : undefined }))
142
+ let fence = false
143
+ if (lines.at(-1) === "")
144
+ lines.pop()
145
+ for (const [ k, line ] of lines.entries()) {
146
+ const number = first + k
147
+ const heading = /^(#{1,6})[ \t]+/.exec(line)
148
+ const prefix = /^\s*(?:(?:[-*]|\d+[.)])\s+(?:\[.\]\s+)?)?/.exec(line)![0]
149
+ if (line.trimStart().startsWith("```"))
150
+ fence = !fence
151
+ else if (fence)
152
+ rows.push({ chars: uniform(line, false, ACCENT), number })
153
+ else if (line.trim() === "")
154
+ rows.push({ chars: [], number })
155
+ else if (heading !== null)
156
+ rows.push(...wrapped(wrap(inline(line.slice(heading[0].length), true,
157
+ heading[1]!.length <= 4 ? ACCENT : undefined), columns, 0), number))
158
+ else {
159
+ const text = wrap(inline(line.slice(prefix.length), false, undefined), columns, prefix.length, prefix.length)
160
+ text[0]!.splice(0, prefix.length, ...marker(prefix))
161
+ rows.push(...wrapped(text, number))
162
+ }
163
+ }
164
+ return rows
165
+ }
166
+
167
+ /* convert a task into rows, hard-wrapped to the given columns: under a focus only
168
+ the focused sections (in plan order), else the keys of the frontmatter (each cut
169
+ to the columns), a rule for its end, and the body, all without trailing blank rows */
170
+ const page = (task: string, sections: string[], columns: number): Row[] => {
171
+ const { keys, body, first } = parts(task.replace(TAG, "").split("\n").map(sanitize).join("\n"))
172
+ const rows: Row[] = []
173
+ if (sections.length > 0) {
174
+ let line = first
175
+ for (const part of body.split(/^(?=##\s)/m)) {
176
+ if (sections.includes(/^##\s+(\S+)/.exec(part)?.[1] ?? ""))
177
+ rows.push(...markdown(part, columns, line))
178
+ line += part.split("\n").length - 1
179
+ }
180
+ }
181
+ else {
182
+ for (const [ key, val, number ] of keys) {
183
+ const text = `${`${key}:`.padEnd(11)}${val}`
184
+ rows.push({ chars: uniform(text.length > columns ? `${text.slice(0, Math.max(0, columns - 1))}…` : text, false, DIM), number })
185
+ }
186
+ if (keys.length > 0)
187
+ rows.push({ chars: uniform("─".repeat(columns), false, DIM), number: first - 1 })
188
+ rows.push(...markdown(body, columns, first))
189
+ }
190
+ while (rows.at(-1)?.chars.length === 0)
191
+ rows.pop()
192
+ return rows
193
+ }
194
+
195
+ /* prefix a line with its line number (if any): grey for an unchanged line, resp.
196
+ colored for a changed line, whose number and text get a background over all
197
+ columns (except for its already emphasized words) */
198
+ const numbered = (line: Char[], number: number | undefined, digits: number, columns: number, sign?: "-" | "+"): Char[] => {
199
+ const cell = (number !== undefined ? String(number) : "").padStart(digits)
200
+ if (sign === undefined)
201
+ return [ ...uniform(cell, false, DIM), ...line ]
202
+ const background = sign === "-" ? DELBG : ADDBG
203
+ return [
204
+ ...uniform(cell, false, sign === "-" ? REMOVE : ACCENT),
205
+ ...line,
206
+ ...uniform(" ".repeat(Math.max(0, columns - width(line))), false, undefined)
207
+ ].map((char) => ({ ...char, background: char.background ?? background }))
208
+ }
209
+
210
+ /* align two lists of strings along the longest common subsequence between their
211
+ common ends, into steps of a kept, removed, or added string (with the removed
212
+ before the added strings of a change) */
213
+ type Step = { sign: " " | "-" | "+", i: number, j: number }
214
+ const align = (a: string[], b: string[]): Step[] => {
215
+ let head = 0
216
+ while (head < a.length && head < b.length && a[head] === b[head])
217
+ head++
218
+ let tail = 0
219
+ while (tail < a.length - head && tail < b.length - head && a[a.length - 1 - tail] === b[b.length - 1 - tail])
220
+ tail++
221
+ const n = a.length - head - tail
222
+ const m = b.length - head - tail
223
+ const span = m + 1
224
+ const lcs = new Uint32Array((n + 1) * span)
225
+ for (let i = n - 1; i >= 0; i--)
226
+ for (let j = m - 1; j >= 0; j--)
227
+ lcs[i * span + j] = a[head + i] === b[head + j] ? lcs[(i + 1) * span + j + 1]! + 1 :
228
+ Math.max(lcs[(i + 1) * span + j]!, lcs[i * span + j + 1]!)
229
+ const steps: Step[] = Array.from({ length: head }, (_, k) => ({ sign: " ", i: k, j: k }))
230
+ let i = 0
231
+ let j = 0
232
+ while (i < n || j < m) {
233
+ if (i < n && j < m && a[head + i] === b[head + j])
234
+ steps.push({ sign: " ", i: head + i++, j: head + j++ })
235
+ else if (i < n && (j === m || lcs[(i + 1) * span + j]! >= lcs[i * span + j + 1]!))
236
+ steps.push({ sign: "-", i: head + i++, j: head + j })
237
+ else
238
+ steps.push({ sign: "+", i: head + i, j: head + j++ })
239
+ }
240
+ for (let k = tail; k > 0; k--)
241
+ steps.push({ sign: " ", i: a.length - k, j: b.length - k })
242
+ return steps
243
+ }
244
+
245
+ /* split lines into their words and other non-blank characters, each with its position */
246
+ type Token = { text: string, row: number, from: number, to: number }
247
+ const tokens = (lines: Char[][]): Token[] => lines.flatMap((line, row) => {
248
+ const list: Token[] = []
249
+ for (let from = 0, to = 0; from < line.length; from = to) {
250
+ to = from + 1
251
+ if (/[\p{L}\p{N}_]/u.test(line[from]!.ch))
252
+ while (to < line.length && /[\p{L}\p{N}_]/u.test(line[to]!.ch))
253
+ to++
254
+ if (line[from]!.ch !== " ")
255
+ list.push({ text: line.slice(from, to).map((char) => char.ch).join(""), row, from, to })
256
+ }
257
+ return list
258
+ })
259
+
260
+ /* emphasize the changed words of the removed and added lines of a change (and the
261
+ blanks between adjacent ones), unless too many of the words changed, where the
262
+ words alone decide, so a change of the wrapping only emphasizes nothing */
263
+ const emphasize = (removed: Char[][], added: Char[][]): [ Char[][], Char[][] ] => {
264
+ const a = tokens(removed)
265
+ const b = tokens(added)
266
+ const steps = align(a.map((token) => token.text), b.map((token) => token.text))
267
+ if (steps.filter(({ sign }) => sign !== " ").length > (a.length + b.length) * ALIKE)
268
+ return [ removed, added ]
269
+ const del = removed.map((line) => [ ...line ])
270
+ const add = added.map((line) => [ ...line ])
271
+ let last: Token | undefined
272
+ let kind = " "
273
+ for (const { sign, i, j } of steps) {
274
+ const token = sign === "-" ? a[i]! : sign === "+" ? b[j]! : undefined
275
+ if (token !== undefined) {
276
+ const line = (sign === "-" ? del : add)[token.row]!
277
+ for (let k = kind === sign && last?.row === token.row ? last.to : token.from; k < token.to; k++)
278
+ line[k] = sign === "-" ? { ...line[k]!, color: DELFG, background: DELWD } : { ...line[k]!, background: ADDWD }
279
+ }
280
+ last = token
281
+ kind = sign
282
+ }
283
+ return [ del, add ]
284
+ }
285
+
286
+ /* merge the old and the new rows of a task into the lines of their unified diff,
287
+ each with its line number (the old one for a removed row, else the new one),
288
+ where only the shown characters of the rows decide about a change */
289
+ const diff = (before: Row[], after: Row[], digits: number, columns: number): Char[][] => {
290
+ const lines: Char[][] = []
291
+ let removed: Row[] = []
292
+ let added: Row[] = []
293
+ const flush = () => {
294
+ const [ del, add ] = emphasize(removed.map((row) => row.chars), added.map((row) => row.chars))
295
+ lines.push(
296
+ ...del.map((line, k) => numbered(line, removed[k]!.number, digits, columns, "-")),
297
+ ...add.map((line, k) => numbered(line, added[k]!.number, digits, columns, "+")))
298
+ removed = []
299
+ added = []
300
+ }
301
+ for (const { sign, i, j } of align(before.map((row) => JSON.stringify(row.chars)), after.map((row) => JSON.stringify(row.chars)))) {
302
+ if (sign === "-")
303
+ removed.push(before[i]!)
304
+ else if (sign === "+")
305
+ added.push(after[j]!)
306
+ else {
307
+ flush()
308
+ lines.push(numbered(after[j]!.chars, after[j]!.number, digits, columns))
309
+ }
310
+ }
311
+ flush()
312
+ return lines
313
+ }
314
+
315
+ /* the task band: its state and its operations */
316
+ export const modTaskEdit = () => {
317
+ /* the currently shown band and the timer following the changes of its task */
318
+ let shown: Band | undefined
319
+ let timer: Timer | undefined
320
+
321
+ /* determine whether the task band is shown */
322
+ const active = () => shown !== undefined
323
+
324
+ /* close the task band */
325
+ const reset = () => {
326
+ timer?.cancel()
327
+ timer = undefined
328
+ shown = undefined
329
+ }
330
+
331
+ /* show the task band of a task and follow the changes of the task, where an
332
+ update keeps the scroll position, and determine the answer of the tool */
333
+ const open = async (id: string, sections: string,
334
+ engine: Pick<Engine, "load" | "every" | "invalidate">): Promise<string> => {
335
+ const loaded = await engine.load(id)
336
+ if (!TAG.test(loaded))
337
+ return `ERROR: task "${id}" cannot be loaded`
338
+ reset()
339
+ const band: Band = { id, sections: sections.split(",").map((name) => name.trim()).filter((name) => name !== ""),
340
+ loaded, before: undefined, lines: [], digits: DIGITS, columns: 0, top: 0, scroll: 0, page: 1, split: 0 }
341
+ shown = band
342
+ let busy = false
343
+ timer = engine.every(POLL, () => {
344
+ if (busy)
345
+ return
346
+ busy = true
347
+ engine.load(id).then((text) => {
348
+ if (shown === band && text !== band.loaded && TAG.test(text)) {
349
+ /* a change without any shown effect (like a usage booking in
350
+ an attachment) keeps the last change and its diff */
351
+ if (JSON.stringify(page(text, band.sections, WIDE)) !==
352
+ JSON.stringify(page(band.loaded, band.sections, WIDE))) {
353
+ band.before = band.loaded
354
+ band.columns = 0
355
+ engine.invalidate()
356
+ }
357
+ band.loaded = text
358
+ }
359
+ }).catch(() => {
360
+ /* ignore a task store which is temporarily not accessible */
361
+ }).finally(() => {
362
+ busy = false
363
+ })
364
+ })
365
+ engine.invalidate()
366
+ return `OK: showing task "${id}" in the band above the prompt`
367
+ }
368
+
369
+ /* close the task band once a next step of the skill is submitted at the prompt */
370
+ const submit = (text: string): void => {
371
+ if (CLOSE.test(text.trim()))
372
+ reset()
373
+ }
374
+
375
+ /* drop the diff of the last change from the task band, so just the latest state
376
+ remains, once "##" is typed (not pasted with other text) at the end of the
377
+ prompt, and remove it from there again */
378
+ const edit = (box: PromptBox, input: string): PromptBox => {
379
+ if (shown === undefined || !/^#{1,2}$/.test(input) || box.cursor !== box.text.length || !box.text.endsWith("##"))
380
+ return box
381
+ shown.before = undefined
382
+ shown.columns = 0
383
+ const text = box.text.slice(0, -2)
384
+ return { ...box, text, cursor: text.length }
385
+ }
386
+
387
+ /* scroll the task in the band by the wheel
388
+ (and tell whether the band took the scrolling) */
389
+ const scroll = (e: BandScroll): boolean => {
390
+ if (shown === undefined)
391
+ return false
392
+ wheel(e, shown)
393
+ return true
394
+ }
395
+
396
+ /* draw the task into the band above the prompt (terminal only), yielding to a survey,
397
+ with a blank row (taken from the rows of the view) separating it from the transcript */
398
+ const render = (e: BandRender, ui: Elements["terminal"]): RenderElement | undefined => {
399
+ if (shown === undefined || e.props.hasSurvey)
400
+ return undefined
401
+ const geometry = layout(e, 0, HEIGHT)
402
+ geometry.body = Math.max(1, geometry.body - 1)
403
+ if (shown.columns !== geometry.span) {
404
+ const { before, loaded, sections } = shown
405
+ const largest = (task: string) => Math.max(0, ...page(task, sections, WIDE).map((row) => row.number ?? 0))
406
+ const digits = Math.max(DIGITS, String(Math.max(largest(loaded), largest(before ?? loaded))).length)
407
+ const columns = geometry.span - 4 - digits
408
+ const rows = page(loaded, sections, columns)
409
+ shown.lines = diff(before !== undefined ? page(before, sections, columns) : rows, rows, digits, columns)
410
+ shown.digits = digits
411
+ shown.columns = geometry.span
412
+ }
413
+ return ui.Box({ flexDirection: "column", width: "100%", children: [
414
+ ui.Text({ children: " " }),
415
+ panel(ui, geometry, shown, {
416
+ title: `[ ⧉ ASE: Edit Task: ${shown.id}${shown.sections.length > 0 ? ` (${shown.sections.join(", ")})` : ""} ]`,
417
+ hints: (geometry.overflow ? "wheel or CTRL+x + TAB + ↑/↓/PgUp/PgDn: scroll · " : "") +
418
+ (shown.before !== undefined ? "##: clear diff · " : "") +
419
+ "DONE · GRILL · DRAFT · IMPLEMENT · CANCEL · (type for refining)",
420
+ lines: shown.lines,
421
+ gutter: shown.digits
422
+ })
423
+ ] })
424
+ }
425
+
426
+ return { active, reset, open, submit, edit, scroll, render }
427
+ }
@@ -6,24 +6,26 @@
6
6
 
7
7
  /* Claude Code function hooks module ("mod", EARLY ACCESS):
8
8
  Hooking infrastructure, registering the function hooks and dispatching
9
- them into the grill table ("ase-mods-grill.ts"), the decision matrix
10
- ("ase-mods-evaluate.ts"), the intent help ("ase-mods-help-find.ts"),
9
+ them into the grill table ("ase-mods-task-grill.ts"), the decision matrix
10
+ ("ase-mods-meta-evaluate.ts"), the intent help ("ase-mods-help-find.ts"),
11
11
  the synopsis form ("ase-mods-help-compose.ts"),
12
12
  the manual band ("ase-mods-help-show.ts"), the manpage browser
13
- ("ase-mods-help-browse.ts"), and the hint band ("ase-mods-help-hint.ts").
13
+ ("ase-mods-help-browse.ts"), the hint band ("ase-mods-help-hint.ts"),
14
+ and the task band ("ase-mods-task-edit.ts").
14
15
  As the engine loads only a module spelling "on", "$", and "next" literally,
15
16
  this is the only module touching the engine, and it binds the engine
16
17
  services needed by the other modules as plain functions. */
17
18
 
18
19
  import type { Register, PromptBox, Timer } from "claude-code"
19
20
  import { type Band, TRIGGER } from "./ase-mods-common.ts"
20
- import { modGrill } from "./ase-mods-grill.ts"
21
- import { modEvaluate } from "./ase-mods-evaluate.ts"
21
+ import { modGrill } from "./ase-mods-task-grill.ts"
22
+ import { modEvaluate } from "./ase-mods-meta-evaluate.ts"
22
23
  import { modHelpFind } from "./ase-mods-help-find.ts"
23
24
  import { modHelpCompose } from "./ase-mods-help-compose.ts"
24
25
  import { modHelpShow } from "./ase-mods-help-show.ts"
25
26
  import { modHelpBrowse } from "./ase-mods-help-browse.ts"
26
27
  import { modHelpHint } from "./ase-mods-help-hint.ts"
28
+ import { modTaskEdit, TOOL } from "./ase-mods-task-edit.ts"
27
29
 
28
30
  /* function hook registration entry point */
29
31
  export const register: Register = (on, options) => {
@@ -32,7 +34,7 @@ export const register: Register = (on, options) => {
32
34
  return
33
35
 
34
36
  /* the grill table, the decision matrix, the intent help, the synopsis form,
35
- the manual band, the manpage browser, and the hint band */
37
+ the manual band, the manpage browser, the hint band, and the task band */
36
38
  const grill = modGrill()
37
39
  const evaluate = modEvaluate()
38
40
  const intent = modHelpFind()
@@ -40,6 +42,27 @@ export const register: Register = (on, options) => {
40
42
  const manual = modHelpShow()
41
43
  const browse = modHelpBrowse()
42
44
  const hint = modHelpHint()
45
+ const task = modTaskEdit()
46
+
47
+ /* provide the tool of the task band where a person sees the band only, so
48
+ its absence tells "ase-task-edit --interactive" that no band exists */
49
+ on("session.start", async ($, e, next) => {
50
+ if (e.isInteractive && e.surface === "terminal")
51
+ await $.tool.register(TOOL)
52
+ return next(e)
53
+ })
54
+
55
+ /* serve the tool of the task band, which needs no permission, as it just
56
+ shows a task, and whose loading of the task is as harmless */
57
+ on("tool.check", { tool: "mcp__ase__ase_task_band" }, () => ({ decision: "allow" }))
58
+ on("tool.call", { tool: "mcp__ase__ase_task_band" }, async ($, e) => ({
59
+ result: await task.open(String(e.id), String(e.sections ?? ""), {
60
+ invalidate: () => $.ui.invalidate("ui.render"),
61
+ every: (ms, fn) => $.clock.every(ms, fn),
62
+ load: async (id) => (await $.tool.call({ tool: "mcp__plugin_ase_ase__ase_task_load",
63
+ id, variant: "source", tag: true })).text ?? ""
64
+ })
65
+ }))
43
66
 
44
67
  /* the bands above the prompt (the synopsis form, the intent band, and the
45
68
  manpage browser, opened in this order), the shown one (as at most one is
@@ -65,7 +88,8 @@ export const register: Register = (on, options) => {
65
88
  (toggling the manual band for a skill command with "//", resp. showing the
66
89
  synopsis form for a skill command with a manual page with "!!", else arming
67
90
  the matching of the prompt as an intent with "!!", resp. showing the manpage
68
- browser for any other prompt with "//"), close the manual band once the
91
+ browser for any other prompt with "//", resp. dropping the diff of the task
92
+ band with "##"), close the manual band once the
69
93
  prompt left its skill command or another band opened, track the skill
70
94
  command of the hint band, and track the picks of the grill table inside
71
95
  the edited prompt while no band is shown */
@@ -95,6 +119,11 @@ export const register: Register = (on, options) => {
95
119
  }
96
120
  else {
97
121
  box = await next(e)
122
+ const stripped = task.edit(box, e.inputText)
123
+ if (stripped !== box) {
124
+ box = stripped
125
+ $.ui.invalidate("ui.render")
126
+ }
98
127
  if (/[!/]$/.test(e.inputText) && box.cursor === box.text.length && TRIGGER.test(box.text))
99
128
  box = await open(box)
100
129
  }
@@ -130,10 +159,10 @@ export const register: Register = (on, options) => {
130
159
  })
131
160
 
132
161
  /* operate the synopsis form resp. the intent band, else scroll the manual
133
- page in the band, by the wheel, and never let the engine move the window
134
- over our (overflowing) band, even while it takes no scrolling */
162
+ page, else the task, in the band, by the wheel, and never let the engine
163
+ move the window over our (overflowing) band, even while it takes no scrolling */
135
164
  on("ui.scroll", { component: "AbovePrompt" }, ($, e, next) => {
136
- if (!bands.some((band) => band.scroll(e)) && !manual.scroll(e))
165
+ if (!bands.some((band) => band.scroll(e)) && !manual.scroll(e) && !task.scroll(e))
137
166
  return shown() === undefined && !manual.active() ? next(e) : {}
138
167
  $.ui.invalidate("ui.render")
139
168
  return {}
@@ -194,6 +223,7 @@ export const register: Register = (on, options) => {
194
223
  reset()
195
224
  manual.reset()
196
225
  hint.reset()
226
+ task.submit(e.text)
197
227
  $.ui.invalidate("ui.render")
198
228
  return next(e)
199
229
  }).catch(($, e, next) =>
@@ -216,19 +246,20 @@ export const register: Register = (on, options) => {
216
246
  }) ?? evaluate.render(e, ui, { invalidate }) ?? drawn
217
247
  })
218
248
 
219
- /* draw the intent band resp. the synopsis form, else the manual page,
220
- else the argument hint, into the band above the prompt (terminal only) */
249
+ /* draw the intent band resp. the synopsis form, else the manual page, else
250
+ the task, else the argument hint, into the band above the prompt (terminal only) */
221
251
  on("ui.render", { component: "AbovePrompt", surface: "terminal" }, async ($, e, next) => {
222
252
  const drawn = await next(e)
223
253
  const ui = $.ui.resolve(e)
224
- return shown()?.render(e, ui) ?? manual.render(e, ui) ?? hint.render(e, ui) ?? drawn
254
+ return shown()?.render(e, ui) ?? manual.render(e, ui) ?? task.render(e, ui) ?? hint.render(e, ui) ?? drawn
225
255
  }).catch(($, e, next) => {
226
256
  /* close a failed intent band resp. synopsis form, restore the prompt
227
257
  resp. the command as typed (instead of the held prompt), resp. close
228
- a failed manual band and hint band, and keep the drawing beneath */
258
+ a failed manual band, task band, and hint band, and keep the drawing beneath */
229
259
  const text = cancel()
230
260
  if (text === undefined) {
231
261
  manual.reset()
262
+ task.reset()
232
263
  hint.reset()
233
264
  }
234
265
  if (text !== undefined)
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: ase-code-edit
3
- argument-hint: "[--help|-h] [--mode|-m auto|craft|refactor|resolve] [--approach|-a] [--grill|-g] [--grill-rounds|-r <n>] [--grill-until|-u MUST|SHOULD|MAY] [--grill-perspective|-p <perspective>] [--grill-ground|-G] [--verify|-v] [--worktree|-w <name>[:<branch>]] [--loop|-l] [<query>|<issue-id>]"
3
+ argument-hint: "[--help|-h] [--mode|-m auto|craft|refactor|resolve] [--approach|-a] [--grill|-g] [--grill-rounds|-r <n>] [--grill-until|-u MUST|SHOULD|MAY] [--grill-perspective|-p <perspective>] [--grill-ground|-G] [--verify|-v light|none|full] [--worktree|-w <name>[:<branch>]] [--loop|-l] [<query>|<issue-id>]"
4
4
  description: >
5
5
  Edit Source Code: Use when the user wants to "edit" the code base in
6
6
  one shot from a query or a bare analyzer issue id like "P1", fusing
@@ -22,7 +22,7 @@ Edit Source Code
22
22
 
23
23
  <expand name="getopt"
24
24
  arg1="ase-code-edit"
25
- arg2="--mode|-m=(auto|craft|refactor|resolve) --approach|-a --grill|-g --grill-rounds|-r=1 --grill-until|-u=(MUST|SHOULD|MAY) --grill-perspective|-p= --grill-ground|-G --verify|-v --worktree|-w= --loop|-l">
25
+ arg2="--mode|-m=(auto|craft|refactor|resolve) --approach|-a --grill|-g --grill-rounds|-r=1 --grill-until|-u=(MUST|SHOULD|MAY) --grill-perspective|-p= --grill-ground|-G --verify|-v=(light|none|full) --worktree|-w= --loop|-l">
26
26
  $ARGUMENTS
27
27
  </expand>
28
28
 
@@ -238,11 +238,18 @@ empty <todo-what/> or <todo-how/> renders as `(none)`:
238
238
  arg3="file and directory paths, identifiers, symbols, types, commands, options, configuration keys, and literal values"
239
239
  arg4="the code base"
240
240
  arg5="">
241
- <if condition="<getopt-option-verify/> is not equal `true`">
241
+ <if condition="<getopt-option-verify/> is equal `none`">
242
242
  Raise *no* questions at all of focus area `REGRESSION`
243
- or `CONFIRMATION`, as without `--verify` no verification
244
- takes place which could honor their decisions.
245
- </if></expand>
243
+ or `CONFIRMATION`, as under `--verify none` no
244
+ verification takes place which could honor their
245
+ decisions.
246
+ </if>
247
+ <elseif condition="<getopt-option-verify/> is equal `light`">
248
+ Raise *no* questions at all of focus area
249
+ `CONFIRMATION`, as under `--verify light` no
250
+ change-specific tests take place which could honor
251
+ their decisions.
252
+ </elseif></expand>
246
253
 
247
254
  If <grill-stop/> is `true`, skip the remaining items of
248
255
  this round and all remaining rounds, and continue with
@@ -388,27 +395,57 @@ empty <todo-what/> or <todo-how/> renders as `(none)`:
388
395
 
389
396
  7. **State: verifying:**
390
397
 
391
- Enter this state only if <getopt-option-verify/> is equal `true`.
392
- Otherwise you *MUST* *strictly skip* the entire state and *any*
393
- verification: do *NOT* run any build, tests, linter, or
394
- type-checker, and do *NOT* execute the modified program.
398
+ Enter this state only if <getopt-option-verify/> is equal `light` or
399
+ `full`. Otherwise (`none`) you *MUST* *strictly skip* the entire
400
+ state and *any* verification: do *NOT* run any build, tests, linter,
401
+ or type-checker, and do *NOT* execute the modified program.
395
402
 
396
403
  1. Verify whether the implementation fulfills <todo-what/> and
397
- <todo-how/> by running available verification commands
398
- (build, tests, linter, type-checker)
404
+ <todo-how/>
399
405
  <if condition="<worktree-dir/> is not empty">
400
- , each with <worktree-dir/> as its working directory
401
- </if>.
406
+ , running every command with <worktree-dir/> as its working
407
+ directory
408
+ </if>:
409
+
410
+ - Under `light` and `full`: run the available lint and build
411
+ commands (including the type-checker) and the already
412
+ existing regression test suite, if any.
413
+
414
+ - Under `full` only: additionally perform *individual*,
415
+ *change-specific* manual tests, which exercise the changed
416
+ behavior through ad-hoc scratch tests and program runs.
417
+ Clean up all scratch test files afterwards, so they never
418
+ become part of the change set.
402
419
 
403
420
  If the verification fails, you *MUST* *adjust* the failing parts
404
- of the change set and *RE-VERIFY* until the verification passes!
421
+ of the change set and *RE-VERIFY* -- for at most *ten* rounds
422
+ in total.
423
+
424
+ 2. <if condition="the verification still fails after the last round">
425
+
426
+ Only output the following <template/>, listing one bullet line
427
+ per remaining failure:
428
+
429
+ <template>
430
+ ⧉ **ASE**: ✪ skill: **ase-code-edit**, ▶ status: **verification failed**
405
431
 
406
- 2. Only output the following <template/>:
432
+ <ase-tpl-bullet-signal/> **REMAINING FAILURES**:
433
+
434
+ - <failure/>
435
+ [...]
436
+ </template>
437
+
438
+ </if>
439
+ <else>
440
+
441
+ Only output the following <template/>:
407
442
 
408
443
  <template>
409
444
  ⧉ **ASE**: ✪ skill: **ase-code-edit**, ▶ status: **verification passed**
410
445
  </template>
411
446
 
447
+ </else>
448
+
412
449
  8. **Loop or Finish:**
413
450
 
414
451
  <if condition="<getopt-option-loop/> is equal `true`">