@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.
- 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-relay-client.js +3 -4
- 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 +4 -1
- package/dst/ase-lib-fetch.js +12 -0
- package/dst/ase-lib-server-cli.js +8 -5
- package/dst/ase-man-catalog.js +1 -1
- package/dst/ase-task-board-diff.js +120 -0
- package/dst/ase-task-board-tui-control.js +8 -5
- package/dst/ase-task-board-tui-model.js +54 -14
- package/dst/ase-task-board-tui-popup.js +41 -8
- package/dst/ase-task-board-tui.js +42 -27
- package/dst/ase-task-board-web-client.css +1 -1
- package/dst/ase-task-board-web-client.html +16 -0
- package/dst/ase-task-board-web-client.js +33 -33
- package/dst/ase-task-board-web-server.js +44 -9
- package/dst/ase-task-board.js +12 -4
- package/dst/ase-task-store-client.js +4 -5
- 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-config-init.md +2 -1
- package/dst/man/ase-config.md +5 -0
- package/dst/man/ase-task-board.md +44 -2
- package/package.json +11 -10
- 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 +33 -1
- package/plugin/meta/ase-control.md +14 -0
- package/plugin/meta/ase-format-specbook.yaml +9 -9
- package/plugin/meta/ase-skill.md +7 -0
- package/plugin/mods/ase-mods-common.ts +73 -42
- package/plugin/mods/ase-mods-help-hint.ts +1 -1
- package/plugin/mods/ase-mods-task-edit.ts +427 -0
- package/plugin/mods/ase-mods.ts +45 -14
- 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 +248 -36
- 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-edit/SKILL.md +20 -12
- package/plugin/skills/ase-spec-edit/help.md +22 -14
- package/plugin/skills/ase-task-edit/SKILL.md +114 -9
- package/plugin/skills/ase-task-edit/help.md +60 -0
- package/plugin/skills/ase-task-grill/SKILL.md +35 -20
- package/plugin/skills/ase-task-grill/help.md +18 -8
- /package/plugin/mods/{ase-mods-evaluate.ts → ase-mods-meta-evaluate.ts} +0 -0
- /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
|
+
}
|
package/plugin/mods/ase-mods.ts
CHANGED
|
@@ -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"),
|
|
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
|
|
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 "//"
|
|
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
|
|
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
|
|
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`">
|