@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.
- package/dst/ase-agent-definition-api.d.ts +2 -0
- package/dst/ase-agent-definition-core.js +24 -3
- package/dst/ase-agent-fleet.js +2 -1
- package/dst/ase-agent-runtime-engine.js +22 -4
- package/dst/ase-agent-runtime-tool.js +10 -8
- package/dst/ase-agent-runtime.js +8 -4
- package/dst/ase-config-schema.js +1 -1
- package/dst/ase-man-catalog.js +1 -1
- package/dst/ase-setup-settings.js +111 -1
- package/dst/ase-setup.js +19 -7
- package/dst/ase-spec.js +184 -49
- package/dst/ase-task-board-tui-model.js +8 -1
- package/dst/ase-task-board-tui.js +26 -2
- package/dst/ase-task-board-web-client.js +10 -10
- package/dst/ase-task-store-plugin-gitea.js +3 -3
- package/dst/man/ase-agent-adhoc.md +25 -6
- package/dst/man/ase-agent-condenser.md +7 -0
- package/dst/man/ase-agent-editor.md +7 -0
- package/dst/man/ase-agent-estimator.md +7 -0
- package/dst/man/ase-agent-fleet.md +7 -4
- package/dst/man/ase-agent-implementer.md +7 -0
- package/dst/man/ase-agent-integrator.md +7 -0
- package/dst/man/ase-agent-qualifier.md +7 -0
- package/dst/man/ase-agent-rebooter.md +7 -0
- package/dst/man/ase-agent-researcher.md +7 -0
- package/dst/man/ase-artifact.md +1 -1
- package/dst/man/ase-config.md +5 -3
- package/dst/man/ase-mcp.md +1 -1
- package/dst/man/ase-setup-install.md +10 -1
- package/dst/man/ase-setup-status.md +6 -0
- package/dst/man/ase-setup-uninstall.md +3 -1
- package/dst/man/ase-setup-update.md +3 -0
- package/dst/man/ase-setup.md +4 -4
- package/dst/man/ase-spec-export.md +14 -3
- package/dst/man/ase-spec-init.md +10 -1
- package/dst/man/ase-spec-lint.md +2 -1
- package/dst/man/ase-spec-preview.md +10 -1
- package/dst/man/ase-spec-query.md +96 -0
- package/dst/man/ase-spec.md +32 -11
- package/dst/man/ase.md +3 -3
- package/package.json +20 -17
- package/plugin/.claude-plugin/icon.png +0 -0
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.github/plugin/plugin.json +1 -1
- package/plugin/meta/ase-common-task.md +9 -1
- package/plugin/meta/ase-control.md +14 -0
- package/plugin/meta/ase-format-spec.md +29 -5
- package/plugin/meta/ase-format-specbook.md +427 -47
- package/plugin/meta/ase-format-specbook.yaml +189 -87
- package/plugin/meta/ase-skill.md +7 -0
- package/plugin/mods/ase-mods-common.ts +36 -6
- package/plugin/mods/ase-mods-evaluate.ts +227 -0
- package/plugin/mods/ase-mods-grill.ts +1 -30
- package/plugin/mods/ase-mods-help-browse.ts +18 -13
- package/plugin/mods/ase-mods-help-find.ts +17 -9
- package/plugin/mods/ase-mods-help-hint.ts +1 -1
- package/plugin/mods/ase-mods.ts +23 -17
- package/plugin/skills/ase-code-edit/SKILL.md +53 -16
- package/plugin/skills/ase-code-edit/help.md +26 -13
- package/plugin/skills/ase-code-lint/SKILL.md +7 -3
- package/plugin/skills/ase-help-intent/data.md +170 -28
- package/plugin/skills/ase-help-skill/catalog.md +1 -0
- package/plugin/skills/ase-meta-workflow/workflow.txt +2 -0
- package/plugin/skills/ase-repo-changelog/SKILL.md +29 -10
- package/plugin/skills/ase-repo-changelog/help.md +5 -1
- package/plugin/skills/ase-repo-commit/SKILL.md +4 -1
- package/plugin/skills/ase-repo-dissect/help.md +2 -1
- package/plugin/skills/ase-repo-stage/SKILL.md +459 -0
- package/plugin/skills/ase-repo-stage/help.md +120 -0
- package/plugin/skills/ase-spec-activate/SKILL.md +4 -1
- package/plugin/skills/ase-spec-edit/SKILL.md +20 -12
- package/plugin/skills/ase-spec-edit/help.md +22 -14
- 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,
|
|
16
|
-
|
|
17
|
-
|
|
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,
|
|
232
|
-
|
|
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,
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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"
|
|
14
|
-
|
|
15
|
-
any other key restores the prompt as typed
|
|
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"
|
|
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,
|
|
204
|
-
|
|
205
|
-
|
|
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"
|
|
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
|
|
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 =
|
|
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,
|
package/plugin/mods/ase-mods.ts
CHANGED
|
@@ -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
|
|
10
|
-
("ase-mods-
|
|
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
|
|
33
|
-
the manpage browser, and the hint band */
|
|
34
|
-
const grill
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
const
|
|
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
|
|
202
|
-
the chain beneath runs, although its drawing is
|
|
203
|
-
at least silences a warning in the debug output
|
|
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
|
|
206
|
-
|
|
207
|
-
|
|
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
|
|
241
|
+
<if condition="<getopt-option-verify/> is equal `none`">
|
|
242
242
|
Raise *no* questions at all of focus area `REGRESSION`
|
|
243
|
-
or `CONFIRMATION`, as
|
|
244
|
-
takes place which could honor their
|
|
245
|
-
|
|
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 `
|
|
392
|
-
Otherwise you *MUST* *strictly skip* the entire
|
|
393
|
-
verification: do *NOT* run any build, tests, linter,
|
|
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/>
|
|
398
|
-
(build, tests, linter, type-checker)
|
|
404
|
+
<todo-how/>
|
|
399
405
|
<if condition="<worktree-dir/> is not empty">
|
|
400
|
-
,
|
|
401
|
-
|
|
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*
|
|
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
|
-
|
|
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`">
|