@rse/ase 2.0.5 → 2.0.7

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 (74) 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-runtime-engine.js +22 -4
  5. package/dst/ase-agent-runtime-tool.js +10 -8
  6. package/dst/ase-agent-runtime.js +8 -4
  7. package/dst/ase-config-schema.js +1 -1
  8. package/dst/ase-man-catalog.js +1 -1
  9. package/dst/ase-setup-settings.js +111 -1
  10. package/dst/ase-setup.js +19 -7
  11. package/dst/ase-spec.js +184 -49
  12. package/dst/ase-task-board-tui-model.js +8 -1
  13. package/dst/ase-task-board-tui.js +26 -2
  14. package/dst/ase-task-board-web-client.js +10 -10
  15. package/dst/ase-task-store-plugin-gitea.js +3 -3
  16. package/dst/man/ase-agent-adhoc.md +25 -6
  17. package/dst/man/ase-agent-condenser.md +7 -0
  18. package/dst/man/ase-agent-editor.md +7 -0
  19. package/dst/man/ase-agent-estimator.md +7 -0
  20. package/dst/man/ase-agent-fleet.md +7 -4
  21. package/dst/man/ase-agent-implementer.md +7 -0
  22. package/dst/man/ase-agent-integrator.md +7 -0
  23. package/dst/man/ase-agent-qualifier.md +7 -0
  24. package/dst/man/ase-agent-rebooter.md +7 -0
  25. package/dst/man/ase-agent-researcher.md +7 -0
  26. package/dst/man/ase-artifact.md +1 -1
  27. package/dst/man/ase-config.md +5 -3
  28. package/dst/man/ase-mcp.md +1 -1
  29. package/dst/man/ase-setup-install.md +10 -1
  30. package/dst/man/ase-setup-status.md +6 -0
  31. package/dst/man/ase-setup-uninstall.md +3 -1
  32. package/dst/man/ase-setup-update.md +3 -0
  33. package/dst/man/ase-setup.md +4 -4
  34. package/dst/man/ase-spec-export.md +14 -3
  35. package/dst/man/ase-spec-init.md +10 -1
  36. package/dst/man/ase-spec-lint.md +2 -1
  37. package/dst/man/ase-spec-preview.md +10 -1
  38. package/dst/man/ase-spec-query.md +96 -0
  39. package/dst/man/ase-spec.md +32 -11
  40. package/dst/man/ase.md +3 -3
  41. package/package.json +20 -17
  42. package/plugin/.claude-plugin/icon.png +0 -0
  43. package/plugin/.claude-plugin/plugin.json +1 -1
  44. package/plugin/.codex-plugin/plugin.json +1 -1
  45. package/plugin/.github/plugin/plugin.json +1 -1
  46. package/plugin/meta/ase-common-task.md +9 -1
  47. package/plugin/meta/ase-control.md +14 -0
  48. package/plugin/meta/ase-format-spec.md +29 -5
  49. package/plugin/meta/ase-format-specbook.md +427 -47
  50. package/plugin/meta/ase-format-specbook.yaml +189 -87
  51. package/plugin/meta/ase-skill.md +7 -0
  52. package/plugin/mods/ase-mods-common.ts +36 -6
  53. package/plugin/mods/ase-mods-evaluate.ts +227 -0
  54. package/plugin/mods/ase-mods-grill.ts +1 -30
  55. package/plugin/mods/ase-mods-help-browse.ts +18 -13
  56. package/plugin/mods/ase-mods-help-find.ts +17 -9
  57. package/plugin/mods/ase-mods-help-hint.ts +1 -1
  58. package/plugin/mods/ase-mods.ts +23 -17
  59. package/plugin/skills/ase-code-edit/SKILL.md +53 -16
  60. package/plugin/skills/ase-code-edit/help.md +26 -13
  61. package/plugin/skills/ase-code-lint/SKILL.md +7 -3
  62. package/plugin/skills/ase-help-intent/data.md +170 -28
  63. package/plugin/skills/ase-help-skill/catalog.md +1 -0
  64. package/plugin/skills/ase-meta-workflow/workflow.txt +2 -0
  65. package/plugin/skills/ase-repo-changelog/SKILL.md +29 -10
  66. package/plugin/skills/ase-repo-changelog/help.md +5 -1
  67. package/plugin/skills/ase-repo-commit/SKILL.md +4 -1
  68. package/plugin/skills/ase-repo-dissect/help.md +2 -1
  69. package/plugin/skills/ase-repo-stage/SKILL.md +459 -0
  70. package/plugin/skills/ase-repo-stage/help.md +120 -0
  71. package/plugin/skills/ase-spec-activate/SKILL.md +4 -1
  72. package/plugin/skills/ase-spec-edit/SKILL.md +20 -12
  73. package/plugin/skills/ase-spec-edit/help.md +22 -14
  74. package/plugin/mods/claude-code.d.ts +0 -13440
@@ -0,0 +1,227 @@
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
+ Decision matrix, redrawing the weighted decision matrices of the
9
+ "ase-meta-evaluate" skill as a grid and rendering in blue and bold the
10
+ column of the best alternative (resp. of all tied best ones). Its
11
+ header cells are clickable and sort the criteria rows by their column
12
+ (cycling the primary, the reverse, and the original order), while the
13
+ rating row stays the last one. */
14
+
15
+ import type { Elements, Frozen, MatchedEvent, RenderElement } from "claude-code"
16
+ import { type Engine, type Char, ROW, RULE, cells, strip, style, width, wrap, split, paint } from "./ase-mods-common.ts"
17
+
18
+ /* the marker line above a decision matrix */
19
+ const MARKER = /\*{0,2}EVALUATION\*{0,2}:\s+\*{0,2}Weighted Multi-Criteria Decision Matrix/
20
+
21
+ /* the color of the best alternatives, and the rating distance (relative to
22
+ the best rating) below which alternatives tie (as in "ase-meta-evaluate") */
23
+ const WINNER = "blue"
24
+ const TIE = 0.01
25
+
26
+ /* the sort marks of the orders (each taking the same columns) */
27
+ const MARK = { desc: " ▼", asc: " ▲", none: " " } as const
28
+
29
+ /* the indent of the wrapped lines of a criterion, the narrowest criteria
30
+ column, and the size limit of a single "Markdown" element */
31
+ const INDENT = 2
32
+ const NARROW = 12
33
+ const LIMIT = 10000
34
+
35
+ /* a decision matrix with the Markdown text before and after it: its header
36
+ cells, its criteria rows (with the values of their weight and evaluations),
37
+ its rating row, and the columns of the best alternatives */
38
+ type Matrix = {
39
+ before: string,
40
+ after: string,
41
+ header: string[],
42
+ rows: { cells: string[], values: number[] }[],
43
+ rating: string[],
44
+ winners: Set<number>
45
+ }
46
+
47
+ /* the sort of a matrix: the sorted column and its order */
48
+ type Order = "desc" | "asc"
49
+ type Sort = { column: number, order: Order }
50
+
51
+ /* read the number of a cell (without bold markup and a leading plus sign) */
52
+ const number = (cell: string): number => {
53
+ const text = cell.replace(/\*/g, "").trim().replace(/^\+/, "")
54
+ return text !== "" ? Number(text) : NaN
55
+ }
56
+
57
+ /* parse the decision matrix out of the text of a reply */
58
+ const parse = (text: string): Matrix | undefined => {
59
+ const lines = text.split("\n")
60
+ const m = lines.findIndex((line) => MARKER.test(line))
61
+ const h = lines.findIndex((line, i) => m !== -1 && i > m && ROW.test(line))
62
+ if (h === -1 || !RULE.test(lines[h + 1] ?? ""))
63
+ return undefined
64
+ const header = cells(lines[h]!).map((cell) => cell.replace(/\*/g, ""))
65
+ if (header.length < 3 || !/Criteria/.test(header[0]!) || !/Weight/.test(header[1]!))
66
+ return undefined
67
+ let end = h + 2
68
+ while (end < lines.length && ROW.test(lines[end]!))
69
+ end++
70
+ const rows = lines.slice(h + 2, end).map((line) => cells(line))
71
+ const rating = rows.pop() ?? []
72
+ if (rows.length === 0 || rows.some((row) => row.length !== header.length)
73
+ || rating.length !== header.length || rating[0]!.replace(/\*/g, "") !== "RATING")
74
+ return undefined
75
+ const values = rows.map((row) => row.slice(1).map(number))
76
+ const ratings = rating.slice(2).map(number)
77
+ if ([ ...values.flat(), ...ratings ].some((value) => !Number.isFinite(value)))
78
+ return undefined
79
+ const best = Math.max(...ratings)
80
+ return {
81
+ before: lines.slice(0, h).join("\n").trim(),
82
+ after: lines.slice(end).join("\n").replace(/^\n+/, "").trimEnd(),
83
+ header,
84
+ rows: rows.map((row, k) => ({ cells: row, values: values[k]! })),
85
+ rating,
86
+ winners: new Set(ratings.flatMap((value, j) =>
87
+ value === best || best - value < TIE * Math.abs(best) ? [ j + 2 ] : []))
88
+ }
89
+ }
90
+
91
+ /* advance the sort by a clicked column: its primary order (ascending for
92
+ the criteria names, descending for the values), then its reverse order,
93
+ and then the original order again */
94
+ const advance = (sort: Sort | undefined, column: number): Sort | undefined => {
95
+ const primary: Order = column === 0 ? "asc" : "desc"
96
+ const reverse: Order = column === 0 ? "desc" : "asc"
97
+ if (sort?.column !== column)
98
+ return { column, order: primary }
99
+ return sort.order === primary ? { column, order: reverse } : undefined
100
+ }
101
+
102
+ /* order the criteria rows by the sort (stable, so equal rows keep their original order) */
103
+ const order = (rows: Matrix["rows"], sort: Sort | undefined): Matrix["rows"] => {
104
+ if (sort === undefined)
105
+ return rows
106
+ const sign = sort.order === "asc" ? 1 : -1
107
+ return [ ...rows ].sort((a, b) => sign * (sort.column === 0 ?
108
+ strip(a.cells[0]!).localeCompare(strip(b.cells[0]!)) :
109
+ a.values[sort.column - 1]! - b.values[sort.column - 1]!))
110
+ }
111
+
112
+ /* the decision matrix: its state and its operations */
113
+ export const modEvaluate = () => {
114
+ /* the last known width of the terminal and the sorts of the matrices */
115
+ let columns = 80
116
+ const sorts = new Map<string, Sort>()
117
+
118
+ /* sort the criteria rows of a matrix by a clicked header cell and redraw it */
119
+ const press = (engine: Pick<Engine, "invalidate">, request: string, column: number) => () => {
120
+ const sort = advance(sorts.get(request), column)
121
+ if (sort !== undefined)
122
+ sorts.set(request, sort)
123
+ else
124
+ sorts.delete(request)
125
+ engine.invalidate()
126
+ }
127
+
128
+ /* redraw a reply which contains a decision matrix (terminal only),
129
+ resp. draw nothing for any other reply */
130
+ const render = (e: Frozen<MatchedEvent<"ui.render", { component: "AssistantMessage", surface: "terminal" }>>,
131
+ ui: Elements["terminal"], engine: Pick<Engine, "invalidate">): RenderElement | undefined => {
132
+ /* act on decision matrices only */
133
+ columns = e.viewport?.columns ?? columns
134
+ const matrix = parse(e.props.text)
135
+ if (matrix === undefined || matrix.before.length > LIMIT || matrix.after.length > LIMIT)
136
+ return undefined
137
+
138
+ /* style the cells: the header ones in bold, the criteria rows in their sort
139
+ order, and the columns of the best alternatives in blue and bold */
140
+ const sort = sorts.get(e.requestId)
141
+ const tint = (chars: Char[], column: number): Char[] =>
142
+ matrix.winners.has(column) ? chars.map((char) => ({ ...char, bold: true, color: WINNER })) : chars
143
+ const head = matrix.header.map((cell, c) => tint(style(`**${cell}**`, undefined), c))
144
+ const body = [ ...order(matrix.rows, sort).map((row) => row.cells), matrix.rating ]
145
+ .map((row) => row.map((cell, c) => tint(style(c === 0 ? strip(cell) : cell, undefined), c)))
146
+
147
+ /* determine the column widths: the value columns at their natural width
148
+ (the header cells with room for the sort mark), the criteria column
149
+ within the remaining columns (gutter, grid lines, cell paddings, and a
150
+ spare column subtracted), else leave the matrix to the engine */
151
+ const widths = head.map((cell, c) =>
152
+ Math.max(width(cell) + MARK.none.length, ...body.map((row) => width(row[c]!))))
153
+ const avail = columns - 2 - (3 * head.length + 1) - 1 - widths.slice(1).reduce((sum, w) => sum + w, 0)
154
+ if (avail < Math.max(NARROW, width(head[0]!) + MARK.none.length))
155
+ return undefined
156
+ widths[0] = Math.min(widths[0]!, avail)
157
+
158
+ /* resolve the UI elements */
159
+ const { Box, Text, Button, Markdown } = ui
160
+
161
+ /* pad the parts of a cell line of the given width to its column width:
162
+ left-aligned for the criteria, right-aligned for the values */
163
+ const align = <T>(c: number, used: number, parts: T[]): (T | string)[] => {
164
+ const pad = " ".repeat(Math.max(0, widths[c]! - used))
165
+ return c === 0 ? [ ...parts, pad ] : [ pad, ...parts ]
166
+ }
167
+
168
+ /* draw a header cell: its styled text inside a hover-scoping box, which while
169
+ hovered reveals a row of clickable plain buttons (one per styled run, as in
170
+ the grill table) on top of it, followed by its sort mark */
171
+ const heading = (c: number): RenderElement[] => {
172
+ const group = `h${c}`
173
+ const onPress = press(engine, e.requestId, c)
174
+ const buttons = split(head[c]!).map(({ text, bold }, r) =>
175
+ Button({ key: `${group}/${r}`, label: text, plain: true, onPress,
176
+ hover: { scope: group, inverse: true, bold } }))
177
+ return align(c, width(head[c]!) + MARK.none.length, [
178
+ Box({ key: group, hover: { scope: group }, children: [
179
+ Text({ children: paint(Text, head[c]!) }),
180
+ Box({ position: "absolute", top: 0, left: 0, display: "none",
181
+ hover: { scope: group, display: "flex" }, flexDirection: "row", children: buttons })
182
+ ] }),
183
+ Text({ children: sort?.column === c ? MARK[sort.order] : MARK.none })
184
+ ]).map((part) => typeof part === "string" ? Text({ children: part }) : part)
185
+ }
186
+
187
+ /* draw a line of the grid out of the parts of its cells, resp. a rule */
188
+ const draw = (parts: RenderElement[][]) => Box({ flexDirection: "row", children: [
189
+ Text({ children: "│ " }),
190
+ ...parts.flatMap((part, c) => c > 0 ? [ Text({ children: " │ " }), ...part ] : part),
191
+ Text({ children: " │" })
192
+ ] })
193
+ const rule = (l: string, m: string, r: string) =>
194
+ Text({ wrap: "truncate-end", children: l + widths.map((w) => "─".repeat(w + 2)).join(m) + r })
195
+
196
+ /* draw the matrix as a grid: a rule below the header and above the rating
197
+ row, and the criteria wrapped within their column */
198
+ const grid: RenderElement[] = [
199
+ rule("┌", "┬", "┐"),
200
+ draw(head.map((_, c) => heading(c))),
201
+ rule("├", "┼", "┤")
202
+ ]
203
+ body.forEach((row, k) => {
204
+ if (k === body.length - 1)
205
+ grid.push(rule("├", "┼", "┤"))
206
+ wrap(row[0]!, widths[0]!, INDENT).forEach((line, i) =>
207
+ grid.push(draw(row.map((cell, c) => {
208
+ const chars = c === 0 ? line : i === 0 ? cell : []
209
+ return [ Text({ children: align(c, width(chars), paint(Text, chars)) }) ]
210
+ }))))
211
+ })
212
+ grid.push(rule("└", "┴", "┘"))
213
+
214
+ /* mimic the reply row of the engine: blank row above and bullet gutter */
215
+ return Box({ flexDirection: "row", marginTop: 1, width: "100%", children: [
216
+ Box({ flexShrink: 0, minWidth: 2, children: Text({ children: e.props.isFirstOfReply ? "⏺" : " " }) }),
217
+ Box({ flexDirection: "column", flexGrow: 1, flexShrink: 1, children: [
218
+ ...(matrix.before !== "" ? [ Markdown({ text: matrix.before }) ] : []),
219
+ Box({ flexDirection: "column", marginTop: matrix.before !== "" ? 1 : 0, children: grid }),
220
+ ...(matrix.after !== "" ? [ Box({ marginTop: 1, children: Markdown({ text: matrix.after }) }) ] : [])
221
+ ] })
222
+ ] })
223
+ }
224
+
225
+ return { render }
226
+ }
227
+
@@ -13,16 +13,12 @@
13
13
  "n" or "nX" into the prompt. */
14
14
 
15
15
  import type { Elements, Frozen, MatchedEvent, RenderElement } from "claude-code"
16
- import { type Engine, type Char, PICKED, style, width, wrap, split, paint } from "./ase-mods-common.ts"
16
+ import { type Engine, type Char, PICKED, ROW, RULE, cells, style, width, wrap, split, paint } from "./ase-mods-common.ts"
17
17
 
18
18
  /* the marker line above a grilling table (single-round and multi-round,
19
19
  optionally labeled with the perspective and/or grounding) */
20
20
  const MARKER = /⧉ \*{0,2}ASE\*{0,2}: GRILLING(?: ROUND \d+\/\d+)?(?: \([^)]*\))?:/
21
21
 
22
- /* a row and the delimiter row of a Markdown table */
23
- const ROW = /^ *\|.*\| *$/
24
- const RULE = /^ *\|(?: *:?-+:? *\|)+ *$/
25
-
26
22
  /* the separator between two answers, the letter of
27
23
  an answer, and the number of a question */
28
24
  const ANSWER = /,\s+(?=\*{0,2}[A-Z]\*{0,2}\s+▶)/
@@ -48,31 +44,6 @@ type Table = {
48
44
  rows: { number: number, question: string, answers: { letter: string, text: string }[] }[]
49
45
  }
50
46
 
51
- /* split a Markdown table row into its cells */
52
- const cells = (line: string): string[] => {
53
- const result: string[] = []
54
- const body = line.trim().replace(/^\|/, "").replace(/\|$/, "")
55
- let cell = ""
56
- let code = false
57
- for (let i = 0; i < body.length; i++) {
58
- const ch = body[i]!
59
- if (ch === "`")
60
- code = !code
61
- if (!code && ch === "\\" && body[i + 1] === "|") {
62
- cell += "|"
63
- i++
64
- }
65
- else if (!code && ch === "|") {
66
- result.push(cell.trim())
67
- cell = ""
68
- }
69
- else
70
- cell += ch
71
- }
72
- result.push(cell.trim())
73
- return result
74
- }
75
-
76
47
  /* parse the grilling table out of the text of a reply */
77
48
  const parse = (text: string): Table | undefined => {
78
49
  const lines = text.split("\n")
@@ -12,9 +12,10 @@
12
12
  where typed text edits the filter, LEFT/RIGHT (or the wheel over the
13
13
  skills) selects a skill, UP/DOWN (or the wheel over the manual page)
14
14
  scroll its manual page (with the matches of the filter marked), RETURN
15
- takes over its command into the prompt, "!!" switches over to the
16
- intent help for the filter, and "//" leaves the filter in the prompt
17
- (which mirrors the filter while the browser is shown). */
15
+ takes over its command into the prompt, SPACE takes it over into the
16
+ synopsis form, "!!" switches over to the intent help for the filter,
17
+ and "//" leaves the filter in the prompt (which mirrors the filter while
18
+ the browser is shown). */
18
19
 
19
20
  import type { Elements, Frozen, PromptBox, PromptEditInput, RenderElement } from "claude-code"
20
21
  import { type Engine, type Char, type BandRender, type BandScroll, type View, PICKED,
@@ -228,24 +229,26 @@ export const modHelpBrowse = () => {
228
229
  const hold = (current: Browse): PromptBox => held(current.filter.trimEnd())
229
230
 
230
231
  /* take over the command of the selected manual page into the prompt (closing
231
- the browser), if any page matches, and determine the prompt to show */
232
- const take = (current: Browse): PromptBox => {
232
+ the browser), if any page matches, optionally suffixed with a trigger (to
233
+ switch over to its band), and determine the prompt to show */
234
+ const take = (current: Browse, trigger = ""): PromptBox => {
233
235
  const doc = matching(current)[current.select]?.doc
234
236
  if (doc === undefined)
235
237
  return hold(current)
236
238
  reset()
237
- const text = `/${doc.skill} `
239
+ const text = `/${doc.skill} ${trigger}`
238
240
  return { text, cursor: text.length }
239
241
  }
240
242
 
241
243
  /* while the manpage browser is shown, every edit is a key of the browser:
242
244
  LEFT/RIGHT select a manual page, UP/DOWN scroll it by a line, RETURN takes
243
- over the command of the selected page, "//" (two "/" in a row) closes the
244
- browser and leaves the filter in the prompt, "!!" (two "!"
245
- in a row, for a non-empty filter) closes the browser with the filter suffixed
246
- with the trigger of the intent help (to switch over to it), a deletion (like
247
- BACKSPACE) deletes the last character of the filter, and any other typed
248
- text is appended to the filter */
245
+ over the command of the selected page, SPACE takes it over suffixed with
246
+ the trigger of the synopsis form, "//" (two "/" in a row) closes the
247
+ browser and leaves the filter in the prompt, "!!" (two "!" in a row, for
248
+ a non-empty filter) closes the browser with the filter suffixed with the
249
+ trigger of the intent help (both to switch over to the other band), a
250
+ deletion (like BACKSPACE) deletes the last character of the filter, and
251
+ any other typed text is appended to the filter */
249
252
  const edit = (e: Frozen<PromptEditInput>): PromptBox => {
250
253
  const current = browse!
251
254
  const key = decode(e)
@@ -272,6 +275,8 @@ export const modHelpBrowse = () => {
272
275
  current.filter += pending
273
276
  if (key.name === "return")
274
277
  return take(current)
278
+ if (key.text === " ")
279
+ return take(current, "!!")
275
280
  if (key.name === "up" || key.name === "down")
276
281
  current.scroll = Math.max(0, current.scroll + (key.name === "up" ? -1 : 1))
277
282
  else if (key.step !== 0) {
@@ -393,7 +398,7 @@ export const modHelpBrowse = () => {
393
398
  status: `[ ${list.length}/${docs?.length ?? 0} manpages ]`,
394
399
  hints: `←/→${wheeled}: select · ` +
395
400
  `${geometry.overflow ? "wheel or CTRL+x + TAB + ↑/↓/PgUp/PgDn" : "↑/↓"}: scroll · ` +
396
- "//: cancel+close · ⏎: apply+close" +
401
+ "//: cancel+close · ⏎: apply+close · SPACE: compose" +
397
402
  (current.filter.trim() !== "" ? " · !!: Find Skill Command" : "") + " · (type for filtering)",
398
403
  rows: (i) => i === 0 ? field : entry(current.top + i - 2),
399
404
  rule: (i) => i === 1,
@@ -10,9 +10,10 @@
10
10
  the skill "ase-help-intent", but over the session context), and showing
11
11
  the proposed skills in a master/detail view in the band above the
12
12
  prompt, where LEFT/RIGHT (or the wheel over the skills) selects a skill,
13
- UP/DOWN and "b"/"f"/SPACE (or the wheel over the details) scroll its
14
- details, RETURN picks its command into the prompt, and "!!", BACKSPACE, or
15
- any other key restores the prompt as typed without the trigger. */
13
+ UP/DOWN and "b"/"f" (or the wheel over the details) scroll its details,
14
+ RETURN picks its command into the prompt, SPACE picks it into the synopsis
15
+ form, and "!!", BACKSPACE, or any other key restores the prompt as typed
16
+ without the trigger. */
16
17
 
17
18
  import type { Elements, Frozen, PromptBox, PromptEditInput, RenderElement } from "claude-code"
18
19
  import { type Engine, type Char, type BandRender, type BandScroll, type View, PICKED, CODE,
@@ -198,11 +199,13 @@ export const modHelpFind = () => {
198
199
 
199
200
  /* while the intent band is shown, every edit is a key of the band (with the
200
201
  prompt held): LEFT/RIGHT select a proposal, UP/DOWN scroll its details by
201
- a line and "b"/"f"/SPACE by a page (as the engine consumes PAGEUP/PAGEDOWN),
202
+ a line and "b"/"f" by a page (as the engine consumes PAGEUP/PAGEDOWN),
202
203
  RETURN (arriving as a typed newline in the held prompt) operates the band
203
- as at the prompt, "//" (two "/" in a row) closes the band with the prompt
204
- as typed suffixed with the trigger of the manpage browser (to switch over
205
- to it), and "!!" (two "!" in a row), a deletion (like BACKSPACE), or any
204
+ as at the prompt, SPACE closes the band with the command of the selected
205
+ proposal suffixed with the trigger of the synopsis form, "//" (two "/"
206
+ in a row) closes the band with the prompt as typed suffixed with the
207
+ trigger of the manpage browser (both to switch over to the other band),
208
+ and "!!" (two "!" in a row), a deletion (like BACKSPACE), or any
206
209
  other key closes the band and restores the prompt as typed without the
207
210
  trigger (onto whose end the edit of a typed text is still to be applied) */
208
211
  const edit = (e: Frozen<PromptEditInput>): PromptBox | string => {
@@ -222,8 +225,13 @@ export const modHelpFind = () => {
222
225
  const text = submit()
223
226
  return intent !== undefined ? held(draft) : { text, cursor: text.length }
224
227
  }
228
+ if (state === "done" && key.text === " ") {
229
+ reset()
230
+ const text = `${proposals[current.select]!.command} !!`
231
+ return { text, cursor: text.length }
232
+ }
225
233
  const roll = key.name === "up" ? -1 : key.name === "down" ? 1 :
226
- key.text === "b" ? -current.page : key.text === "f" || key.text === " " ? current.page : 0
234
+ key.text === "b" ? -current.page : key.text === "f" ? current.page : 0
227
235
  const step = roll !== 0 ? 0 : key.step
228
236
  if (state === "done" && (roll !== 0 || step !== 0)) {
229
237
  if (step !== 0) {
@@ -406,7 +414,7 @@ export const modHelpFind = () => {
406
414
  (the intent itself stays visible in the prompt) */
407
415
  return panel(ui, geometry, current, {
408
416
  title: TITLE,
409
- hints: current.state === "done" ? "⌫/!!: cancel+close · ⏎: apply+close · ←/→: select · b/f/SPACE: scroll" +
417
+ hints: current.state === "done" ? "⌫/!!: cancel+close · ⏎: apply+close · SPACE: compose · ←/→: select · b/f: scroll" +
410
418
  (geometry.overflow ? " · wheel: select/scroll" : "") + " · //: Browse Skill Manpages" :
411
419
  "⌫/!!/any key: cancel+close · //: Browse Skill Manpages",
412
420
  rows: (i) => {
@@ -29,7 +29,7 @@ const GROUP = /\[(?:[^[\]]|\[[^\]]*\])*\]|\((?:[^()]|\([^)]*\))*\)|\S+/g
29
29
  const NBSP = String.fromCodePoint(0xA0)
30
30
  const SYNTAX = "gray"
31
31
  const REFER = "red"
32
- const INDENT = 4
32
+ const INDENT = 2
33
33
  const TITLE = "[ ⧉ ASE: Show Skill Synopsis ]"
34
34
 
35
35
  /* a hint band: the scroll state of its frame, the skill command as typed,
@@ -6,8 +6,9 @@
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 intent help
10
- ("ase-mods-help-find.ts"), the synopsis form ("ase-mods-help-compose.ts"),
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"),
11
+ the synopsis form ("ase-mods-help-compose.ts"),
11
12
  the manual band ("ase-mods-help-show.ts"), the manpage browser
12
13
  ("ase-mods-help-browse.ts"), and the hint band ("ase-mods-help-hint.ts").
13
14
  As the engine loads only a module spelling "on", "$", and "next" literally,
@@ -17,6 +18,7 @@
17
18
  import type { Register, PromptBox, Timer } from "claude-code"
18
19
  import { type Band, TRIGGER } from "./ase-mods-common.ts"
19
20
  import { modGrill } from "./ase-mods-grill.ts"
21
+ import { modEvaluate } from "./ase-mods-evaluate.ts"
20
22
  import { modHelpFind } from "./ase-mods-help-find.ts"
21
23
  import { modHelpCompose } from "./ase-mods-help-compose.ts"
22
24
  import { modHelpShow } from "./ase-mods-help-show.ts"
@@ -29,14 +31,15 @@ export const register: Register = (on, options) => {
29
31
  if (options.enabled === false)
30
32
  return
31
33
 
32
- /* the grill table, the intent help, the synopsis form, the manual band,
33
- the manpage browser, and the hint band */
34
- const grill = modGrill()
35
- const intent = modHelpFind()
36
- const skill = modHelpCompose()
37
- const manual = modHelpShow()
38
- const browse = modHelpBrowse()
39
- const hint = modHelpHint()
34
+ /* the grill table, the decision matrix, the intent help, the synopsis form,
35
+ the manual band, the manpage browser, and the hint band */
36
+ const grill = modGrill()
37
+ const evaluate = modEvaluate()
38
+ const intent = modHelpFind()
39
+ const skill = modHelpCompose()
40
+ const manual = modHelpShow()
41
+ const browse = modHelpBrowse()
42
+ const hint = modHelpHint()
40
43
 
41
44
  /* the bands above the prompt (the synopsis form, the intent band, and the
42
45
  manpage browser, opened in this order), the shown one (as at most one is
@@ -198,16 +201,19 @@ export const register: Register = (on, options) => {
198
201
  next.called || shown() === undefined ? next(e) :
199
202
  { drop: "intent band or synopsis form failed" })
200
203
 
201
- /* redraw the replies which contain a grilling table (terminal only), where
202
- the chain beneath runs, although its drawing is replaced by us (but this
203
- at least silences a warning in the debug output of Claude Code) */
204
+ /* redraw the replies which contain a grilling table or a decision matrix
205
+ (terminal only), where the chain beneath runs, although its drawing is
206
+ replaced by us (but this at least silences a warning in the debug output
207
+ of Claude Code) */
204
208
  on("ui.render", { component: "AssistantMessage", surface: "terminal" }, async ($, e, next) => {
205
- const drawn = await next(e)
206
- return grill.render(e, $.ui.resolve(e), {
207
- invalidate: () => $.ui.invalidate("ui.render"),
209
+ const drawn = await next(e)
210
+ const ui = $.ui.resolve(e)
211
+ const invalidate = () => $.ui.invalidate("ui.render")
212
+ return grill.render(e, ui, {
213
+ invalidate,
208
214
  prompt: () => $.prompt.read(),
209
215
  fill: (text) => $.prompt.fill({ text })
210
- }) ?? drawn
216
+ }) ?? evaluate.render(e, ui, { invalidate }) ?? drawn
211
217
  })
212
218
 
213
219
  /* draw the intent band resp. the synopsis form, else the manual page,
@@ -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`">