@rse/ase 0.9.46 → 0.9.47
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-compat.js +3 -2
- package/dst/ase-config.js +6 -5
- package/dst/ase-hook.js +22 -17
- package/dst/ase-kv.js +38 -43
- package/dst/ase-log.js +13 -5
- package/dst/ase-markdown.js +12 -2
- package/dst/ase-mcp.js +20 -3
- package/dst/ase-persona.js +1 -3
- package/dst/ase-service.js +15 -25
- package/dst/ase-setup.js +12 -39
- package/dst/ase-statusline.js +14 -28
- package/dst/ase-task.js +14 -23
- package/dst/ase.js +6 -2
- package/package.json +1 -1
- 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/etc/stx.conf +2 -2
- package/plugin/meta/ase-skill.md +9 -0
- package/plugin/package.json +1 -1
- package/plugin/skills/{ase-meta-intent → ase-help-intent}/SKILL.md +4 -4
- package/plugin/skills/{ase-meta-intent → ase-help-intent}/help.md +8 -7
- package/plugin/skills/ase-help-skill/SKILL.md +203 -0
- package/plugin/skills/ase-help-skill/catalog.md +60 -0
- package/plugin/skills/ase-help-skill/help.md +83 -0
- package/plugin/skills/ase-task-condense/help.md +3 -3
- package/plugin/skills/ase-task-reboot/help.md +3 -3
- package/plugin/skills/ase-task-view/help.md +3 -3
package/dst/ase-compat.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
4
|
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
5
|
*/
|
|
6
|
+
import { writeStdout } from "./ase-stdout.js";
|
|
6
7
|
/* the canonical expected values for every ase-meta-compat probe,
|
|
7
8
|
keyed by "<category>/<probe-name>" as used in the skill */
|
|
8
9
|
const EXPECTED = {
|
|
@@ -44,8 +45,8 @@ export default class CompatCommand {
|
|
|
44
45
|
program
|
|
45
46
|
.command("compat")
|
|
46
47
|
.description("Output expected probe values for the ase-meta-compat self-test skill")
|
|
47
|
-
.action(() => {
|
|
48
|
-
|
|
48
|
+
.action(async () => {
|
|
49
|
+
await writeStdout(formatExpected());
|
|
49
50
|
});
|
|
50
51
|
}
|
|
51
52
|
}
|
package/dst/ase-config.js
CHANGED
|
@@ -14,6 +14,7 @@ import Table from "cli-table3";
|
|
|
14
14
|
import writeFileAtomic from "write-file-atomic";
|
|
15
15
|
import lockfile from "proper-lockfile";
|
|
16
16
|
import { z } from "zod";
|
|
17
|
+
import { writeStdout } from "./ase-stdout.js";
|
|
17
18
|
/* classification taxonomy */
|
|
18
19
|
export const projectClassification = {
|
|
19
20
|
boxing: ["white", "grey", "black"]
|
|
@@ -188,7 +189,7 @@ export class Config {
|
|
|
188
189
|
docs;
|
|
189
190
|
target;
|
|
190
191
|
/* creation */
|
|
191
|
-
constructor(name, schema, log, scope = [{ kind: "project" }]) {
|
|
192
|
+
constructor(name, schema, log, scope = [{ kind: "user" }, { kind: "project" }]) {
|
|
192
193
|
if (scope.length === 0)
|
|
193
194
|
throw new Error("invalid scope: chain must not be empty");
|
|
194
195
|
this.name = name;
|
|
@@ -560,7 +561,7 @@ export default class ConfigCommand {
|
|
|
560
561
|
configCmd
|
|
561
562
|
.command("list")
|
|
562
563
|
.description("list all configured values as flat dotted keys")
|
|
563
|
-
.action((_opts, cmd) => {
|
|
564
|
+
.action(async (_opts, cmd) => {
|
|
564
565
|
const scope = parseScope(cmd.optsWithGlobals().scope);
|
|
565
566
|
const cfg = new Config("config", configSchema, this.log, scope);
|
|
566
567
|
cfg.read();
|
|
@@ -573,7 +574,7 @@ export default class ConfigCommand {
|
|
|
573
574
|
const val = isScalar(e.value) ? e.value.value : e.value;
|
|
574
575
|
table.push([e.key, String(val), Config.scopeLabel(e.scope)]);
|
|
575
576
|
}
|
|
576
|
-
|
|
577
|
+
await writeStdout(`${table.toString()}\n`);
|
|
577
578
|
});
|
|
578
579
|
/* register CLI sub-command "ase config edit" */
|
|
579
580
|
configCmd
|
|
@@ -612,7 +613,7 @@ export default class ConfigCommand {
|
|
|
612
613
|
.command("get")
|
|
613
614
|
.description("print the value at a dotted configuration key")
|
|
614
615
|
.argument("<key>", "configuration key (dotted path)")
|
|
615
|
-
.action((key, _opts, cmd) => {
|
|
616
|
+
.action(async (key, _opts, cmd) => {
|
|
616
617
|
const scope = parseScope(cmd.optsWithGlobals().scope);
|
|
617
618
|
const cfg = new Config("config", configSchema, this.log, scope);
|
|
618
619
|
cfg.read();
|
|
@@ -621,7 +622,7 @@ export default class ConfigCommand {
|
|
|
621
622
|
throw new Error(`key "${key}" is not set`);
|
|
622
623
|
if (isMap(val))
|
|
623
624
|
throw new Error(`key "${key}" is not a leaf key`);
|
|
624
|
-
|
|
625
|
+
await writeStdout(`${isScalar(val) ? val.value : val}\n`);
|
|
625
626
|
});
|
|
626
627
|
/* register CLI sub-command "ase config set" */
|
|
627
628
|
configCmd
|
package/dst/ase-hook.js
CHANGED
|
@@ -69,6 +69,13 @@ const toolSpecs = {
|
|
|
69
69
|
approvalEvent: "PermissionRequest"
|
|
70
70
|
}
|
|
71
71
|
};
|
|
72
|
+
/* schema (and derived type) of the tool invocation input fields
|
|
73
|
+
inspected by the tool-approval decision logic */
|
|
74
|
+
const toolInputSchema = v.object({
|
|
75
|
+
command: v.optional(v.string()),
|
|
76
|
+
skill: v.optional(v.string()),
|
|
77
|
+
file_path: v.optional(v.string())
|
|
78
|
+
});
|
|
72
79
|
/* CLI command "ase hook" */
|
|
73
80
|
export default class HookCommand {
|
|
74
81
|
log;
|
|
@@ -242,16 +249,15 @@ export default class HookCommand {
|
|
|
242
249
|
const projectId = path.basename(projectDir);
|
|
243
250
|
/* determine user id */
|
|
244
251
|
const userId = process.env.USER ?? process.env.LOGNAME ?? "unknown";
|
|
245
|
-
/* determine
|
|
246
|
-
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
boxing = valBoxing;
|
|
252
|
+
/* helper function: determine a setting from the configuration,
|
|
253
|
+
falling back to an environment variable and a default */
|
|
254
|
+
const setting = (key, envVar, dflt) => {
|
|
255
|
+
const val = cfg.get(key);
|
|
256
|
+
return typeof val === "string" ? val : (process.env[envVar] ?? dflt);
|
|
257
|
+
};
|
|
258
|
+
/* determine agent persona style and project boxing transparency */
|
|
259
|
+
const persona = setting("agent.persona", "ASE_PERSONA_STYLE", "engineer");
|
|
260
|
+
const boxing = setting("project.boxing", "ASE_PROJECT_BOXING", "white");
|
|
255
261
|
/* determine headless mode */
|
|
256
262
|
const headless = (process.env.ASE_HEADLESS ?? "false") === "true" ? "true" : "false";
|
|
257
263
|
/* provide ASE information to Anthropic Claude Code CLI shell commands
|
|
@@ -422,13 +428,12 @@ export default class HookCommand {
|
|
|
422
428
|
let toolInput = {};
|
|
423
429
|
const rawInput = input[spec.toolInputField];
|
|
424
430
|
if (spec.toolInputIsString && typeof rawInput === "string")
|
|
425
|
-
toolInput = this.parseJSON(rawInput,
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
toolInput = rawInput;
|
|
431
|
+
toolInput = this.parseJSON(rawInput, toolInputSchema);
|
|
432
|
+
else if (!spec.toolInputIsString && typeof rawInput === "object" && rawInput !== null) {
|
|
433
|
+
const result = v.safeParse(toolInputSchema, rawInput);
|
|
434
|
+
if (result.success)
|
|
435
|
+
toolInput = result.output;
|
|
436
|
+
}
|
|
432
437
|
const command = toolInput.command ?? "";
|
|
433
438
|
if (toolName === spec.bashToolName && /^ase(\s|$)/.test(command)
|
|
434
439
|
&& !/[;&|<>`\n]|\$\(/.test(command))
|
package/dst/ase-kv.js
CHANGED
|
@@ -72,6 +72,36 @@ export class KV {
|
|
|
72
72
|
for (const [k, v] of snap)
|
|
73
73
|
KV.store.set(k, v);
|
|
74
74
|
}
|
|
75
|
+
/* execute a single key/value command and return its result text */
|
|
76
|
+
static execute(c) {
|
|
77
|
+
if (c.command === "clear")
|
|
78
|
+
return `OK: removed ${KV.clear(c.prefix)} key(s)`;
|
|
79
|
+
else if (c.command === "set") {
|
|
80
|
+
if (c.key === undefined)
|
|
81
|
+
throw new Error("missing `key`");
|
|
82
|
+
if (c.val === undefined)
|
|
83
|
+
throw new Error("missing `val`");
|
|
84
|
+
KV.set(c.key, c.val);
|
|
85
|
+
return `OK: stored key "${c.key}"`;
|
|
86
|
+
}
|
|
87
|
+
else if (c.command === "get") {
|
|
88
|
+
if (c.key === undefined)
|
|
89
|
+
throw new Error("missing `key`");
|
|
90
|
+
if (!KV.has(c.key))
|
|
91
|
+
return "";
|
|
92
|
+
const val = KV.get(c.key);
|
|
93
|
+
return val === undefined ? "" : JSON.stringify(val);
|
|
94
|
+
}
|
|
95
|
+
else if (c.command === "delete") {
|
|
96
|
+
if (c.key === undefined)
|
|
97
|
+
throw new Error("missing `key`");
|
|
98
|
+
return KV.delete(c.key) ?
|
|
99
|
+
`OK: removed key "${c.key}"` :
|
|
100
|
+
`WARNING: no key "${c.key}" to remove`;
|
|
101
|
+
}
|
|
102
|
+
else
|
|
103
|
+
throw new Error(`invalid command "${String(c.command)}"`);
|
|
104
|
+
}
|
|
75
105
|
}
|
|
76
106
|
/* MCP registration entry point for in-memory key/value tools */
|
|
77
107
|
export class KVMCP {
|
|
@@ -87,10 +117,7 @@ export class KVMCP {
|
|
|
87
117
|
}
|
|
88
118
|
}, async (args) => {
|
|
89
119
|
try {
|
|
90
|
-
|
|
91
|
-
return { content: [{ type: "text", text: "" }] };
|
|
92
|
-
const val = KV.get(args.key);
|
|
93
|
-
const text = val === undefined ? "" : JSON.stringify(val);
|
|
120
|
+
const text = KV.execute({ command: "get", key: args.key });
|
|
94
121
|
return { content: [{ type: "text", text }] };
|
|
95
122
|
}
|
|
96
123
|
catch (err) {
|
|
@@ -112,8 +139,8 @@ export class KVMCP {
|
|
|
112
139
|
}
|
|
113
140
|
}, async (args) => {
|
|
114
141
|
try {
|
|
115
|
-
KV.set
|
|
116
|
-
return { content: [{ type: "text", text
|
|
142
|
+
const text = KV.execute({ command: "set", key: args.key, val: args.val });
|
|
143
|
+
return { content: [{ type: "text", text }] };
|
|
117
144
|
}
|
|
118
145
|
catch (err) {
|
|
119
146
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -133,8 +160,8 @@ export class KVMCP {
|
|
|
133
160
|
}
|
|
134
161
|
}, async (args) => {
|
|
135
162
|
try {
|
|
136
|
-
const
|
|
137
|
-
return { content: [{ type: "text", text
|
|
163
|
+
const text = KV.execute({ command: "clear", prefix: args.prefix });
|
|
164
|
+
return { content: [{ type: "text", text }] };
|
|
138
165
|
}
|
|
139
166
|
catch (err) {
|
|
140
167
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -152,11 +179,8 @@ export class KVMCP {
|
|
|
152
179
|
}
|
|
153
180
|
}, async (args) => {
|
|
154
181
|
try {
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
`OK: removed key "${args.key}"` :
|
|
158
|
-
`WARNING: no key "${args.key}" to remove`;
|
|
159
|
-
return { content: [{ type: "text", text: msg }] };
|
|
182
|
+
const text = KV.execute({ command: "delete", key: args.key });
|
|
183
|
+
return { content: [{ type: "text", text }] };
|
|
160
184
|
}
|
|
161
185
|
catch (err) {
|
|
162
186
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -199,36 +223,7 @@ export class KVMCP {
|
|
|
199
223
|
const snapshot = tx ? KV.snapshot() : null;
|
|
200
224
|
for (const c of args.commands) {
|
|
201
225
|
try {
|
|
202
|
-
|
|
203
|
-
const n = KV.clear(c.prefix);
|
|
204
|
-
results.push(`OK: removed ${n} key(s)`);
|
|
205
|
-
}
|
|
206
|
-
else if (c.command === "set") {
|
|
207
|
-
if (c.key === undefined)
|
|
208
|
-
throw new Error("missing `key`");
|
|
209
|
-
if (c.val === undefined)
|
|
210
|
-
throw new Error("missing `val`");
|
|
211
|
-
KV.set(c.key, c.val);
|
|
212
|
-
results.push(`OK: stored key "${c.key}"`);
|
|
213
|
-
}
|
|
214
|
-
else if (c.command === "get") {
|
|
215
|
-
if (c.key === undefined)
|
|
216
|
-
throw new Error("missing `key`");
|
|
217
|
-
if (!KV.has(c.key))
|
|
218
|
-
results.push("");
|
|
219
|
-
else {
|
|
220
|
-
const val = KV.get(c.key);
|
|
221
|
-
results.push(val === undefined ? "" : JSON.stringify(val));
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
else if (c.command === "delete") {
|
|
225
|
-
if (c.key === undefined)
|
|
226
|
-
throw new Error("missing `key`");
|
|
227
|
-
const removed = KV.delete(c.key);
|
|
228
|
-
results.push(removed ?
|
|
229
|
-
`OK: removed key "${c.key}"` :
|
|
230
|
-
`WARNING: no key "${c.key}" to remove`);
|
|
231
|
-
}
|
|
226
|
+
results.push(KV.execute(c));
|
|
232
227
|
}
|
|
233
228
|
catch (err) {
|
|
234
229
|
const message = err instanceof Error ? err.message : String(err);
|
package/dst/ase-log.js
CHANGED
|
@@ -24,11 +24,7 @@ export default class Log {
|
|
|
24
24
|
this._logFile = _logFile;
|
|
25
25
|
}
|
|
26
26
|
async init() {
|
|
27
|
-
|
|
28
|
-
const idx = levels.findIndex((l) => l.name === this._logLevel);
|
|
29
|
-
if (idx === -1)
|
|
30
|
-
throw new RangeError(`invalid log level "${this._logLevel}" (expected one of: ${levels.map((l) => l.name).join(", ")})`);
|
|
31
|
-
this.logLevelIdx = idx;
|
|
27
|
+
this.logLevel(this._logLevel);
|
|
32
28
|
if (this._logFile !== "-")
|
|
33
29
|
this.stream = this.openStream(this._logFile);
|
|
34
30
|
}
|
|
@@ -73,4 +69,16 @@ export default class Log {
|
|
|
73
69
|
this.stream.write(line);
|
|
74
70
|
}
|
|
75
71
|
}
|
|
72
|
+
/* close the log file stream, flushing all pending writes */
|
|
73
|
+
async close() {
|
|
74
|
+
if (this.stream === null)
|
|
75
|
+
return;
|
|
76
|
+
const stream = this.stream;
|
|
77
|
+
this.stream = null;
|
|
78
|
+
await new Promise((resolve) => {
|
|
79
|
+
stream.end(() => {
|
|
80
|
+
resolve();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
}
|
|
76
84
|
}
|
package/dst/ase-markdown.js
CHANGED
|
@@ -69,6 +69,16 @@ export class Markdown {
|
|
|
69
69
|
n++;
|
|
70
70
|
return n;
|
|
71
71
|
}
|
|
72
|
+
/* test whether the newline at offset pos starts a paragraph break,
|
|
73
|
+
i.e. whether it is followed by a blank line or the end of the text */
|
|
74
|
+
static isParagraphBreak(text, pos) {
|
|
75
|
+
let n = pos + 1;
|
|
76
|
+
while (n < text.length && (text[n] === " " || text[n] === "\t"))
|
|
77
|
+
n++;
|
|
78
|
+
if (text[n] === "\r")
|
|
79
|
+
n++;
|
|
80
|
+
return n >= text.length || text[n] === "\n";
|
|
81
|
+
}
|
|
72
82
|
/* PASS 1 of rewrite(): rewrite inline code spans that carry
|
|
73
83
|
backslash-escaped backticks (`\``) into CommonMark-correct spans */
|
|
74
84
|
static rewriteEscapedSpans(text) {
|
|
@@ -118,7 +128,7 @@ export class Markdown {
|
|
|
118
128
|
k += runLen;
|
|
119
129
|
continue;
|
|
120
130
|
}
|
|
121
|
-
if (c === "\n" &&
|
|
131
|
+
if (c === "\n" && Markdown.isParagraphBreak(text, k))
|
|
122
132
|
/* a code span never crosses a paragraph break */
|
|
123
133
|
break;
|
|
124
134
|
inner += c;
|
|
@@ -185,7 +195,7 @@ export class Markdown {
|
|
|
185
195
|
closes = true;
|
|
186
196
|
p += r;
|
|
187
197
|
}
|
|
188
|
-
else if (text[p] === "\n" &&
|
|
198
|
+
else if (text[p] === "\n" && Markdown.isParagraphBreak(text, p))
|
|
189
199
|
/* a code span never crosses a paragraph break */
|
|
190
200
|
break;
|
|
191
201
|
else
|
package/dst/ase-mcp.js
CHANGED
|
@@ -94,8 +94,11 @@ export default class MCPCommand {
|
|
|
94
94
|
return;
|
|
95
95
|
triggerReconnect("http connection lost");
|
|
96
96
|
};
|
|
97
|
+
/* activate the connection and flush buffered messages */
|
|
97
98
|
await next.start();
|
|
98
99
|
client = next;
|
|
100
|
+
for (const msg of pending.splice(0, pending.length))
|
|
101
|
+
sendToClient(msg);
|
|
99
102
|
};
|
|
100
103
|
/* reconnect loop: restart service if needed, then reconnect client */
|
|
101
104
|
const reconnect = async (attempt = 0) => {
|
|
@@ -132,12 +135,26 @@ export default class MCPCommand {
|
|
|
132
135
|
this.log.write("warning", `mcp: ${reason} — reconnecting`);
|
|
133
136
|
reconnect(0).catch(() => { });
|
|
134
137
|
};
|
|
135
|
-
/*
|
|
136
|
-
|
|
137
|
-
|
|
138
|
+
/* bounded buffer for messages arriving while no HTTP client is connected */
|
|
139
|
+
const MAX_PENDING = 100;
|
|
140
|
+
const pending = [];
|
|
141
|
+
/* forward a message to the HTTP client, buffering it while the client
|
|
142
|
+
is disconnected so requests survive a reconnect window */
|
|
143
|
+
const sendToClient = (msg) => {
|
|
144
|
+
if (client === null) {
|
|
145
|
+
if (pending.length >= MAX_PENDING) {
|
|
146
|
+
this.log.write("warning", "mcp: pending queue overflow, dropping oldest message");
|
|
147
|
+
pending.shift();
|
|
148
|
+
}
|
|
149
|
+
pending.push(msg);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
client.send(msg).catch((err) => {
|
|
138
153
|
this.log.write("error", `mcp: http send: ${this.asError(err).message}`);
|
|
139
154
|
});
|
|
140
155
|
};
|
|
156
|
+
/* wire stdio server */
|
|
157
|
+
server.onmessage = sendToClient;
|
|
141
158
|
server.onerror = (err) => {
|
|
142
159
|
this.log.write("error", `mcp: stdio: ${err.message}`);
|
|
143
160
|
};
|
package/dst/ase-persona.js
CHANGED
|
@@ -20,9 +20,7 @@ export class Persona {
|
|
|
20
20
|
if (val === undefined)
|
|
21
21
|
return "engineer";
|
|
22
22
|
const style = String(isScalar(val) ? val.value : val);
|
|
23
|
-
|
|
24
|
-
return "engineer";
|
|
25
|
-
return style;
|
|
23
|
+
return Persona.styles.find((s) => s === style) ?? "engineer";
|
|
26
24
|
}
|
|
27
25
|
/* set the persona style on the strongest scope of an optional session */
|
|
28
26
|
static set(log, style, session) {
|
package/dst/ase-service.js
CHANGED
|
@@ -477,6 +477,16 @@ export default class ServiceCommand {
|
|
|
477
477
|
}
|
|
478
478
|
throw lastErr;
|
|
479
479
|
}
|
|
480
|
+
/* build the "POST /command" request options for a command token */
|
|
481
|
+
commandRequest(cmd, timeoutMs) {
|
|
482
|
+
return {
|
|
483
|
+
method: "POST",
|
|
484
|
+
headers: { "Content-Type": "application/json" },
|
|
485
|
+
body: { command: cmd },
|
|
486
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
487
|
+
ignoreResponseError: true
|
|
488
|
+
};
|
|
489
|
+
}
|
|
480
490
|
/* status flow: report whether the service is running */
|
|
481
491
|
async doStatus() {
|
|
482
492
|
const ctx = this.loadContext();
|
|
@@ -486,13 +496,7 @@ export default class ServiceCommand {
|
|
|
486
496
|
}
|
|
487
497
|
const match = await probe(ctx.port, ctx.projectId);
|
|
488
498
|
if (match === true) {
|
|
489
|
-
const r = await ofetch.raw(`http://${HOST}:${ctx.port}/command`,
|
|
490
|
-
method: "POST",
|
|
491
|
-
headers: { "Content-Type": "application/json" },
|
|
492
|
-
body: { command: "status" },
|
|
493
|
-
signal: AbortSignal.timeout(2000),
|
|
494
|
-
ignoreResponseError: true
|
|
495
|
-
});
|
|
499
|
+
const r = await ofetch.raw(`http://${HOST}:${ctx.port}/command`, this.commandRequest("status", 2000));
|
|
496
500
|
const d = r._data;
|
|
497
501
|
const uptimeMs = typeof d?.uptimeMs === "number" ? d.uptimeMs : 0;
|
|
498
502
|
const uptime = prettyMs(uptimeMs, { verbose: true });
|
|
@@ -509,30 +513,16 @@ export default class ServiceCommand {
|
|
|
509
513
|
/* send command: POST /command with the arbitrary cmd token */
|
|
510
514
|
async doSend(cmd) {
|
|
511
515
|
let ctx = this.loadContext();
|
|
512
|
-
if (ctx.port === null) {
|
|
513
|
-
|
|
514
|
-
ctx = this.loadContext();
|
|
515
|
-
if (ctx.port === null)
|
|
516
|
-
throw new Error("service not running (no port configured after auto-start)");
|
|
517
|
-
}
|
|
518
|
-
let match = await probe(ctx.port, ctx.projectId);
|
|
519
|
-
if (match !== true) {
|
|
516
|
+
if (ctx.port === null || await probe(ctx.port, ctx.projectId) !== true) {
|
|
517
|
+
/* auto-start the service once, then re-check */
|
|
520
518
|
await this.doStart();
|
|
521
519
|
ctx = this.loadContext();
|
|
522
520
|
if (ctx.port === null)
|
|
523
521
|
throw new Error("service not running (no port configured after auto-start)");
|
|
524
|
-
|
|
525
|
-
if (match !== true)
|
|
522
|
+
if (await probe(ctx.port, ctx.projectId) !== true)
|
|
526
523
|
throw new Error(`service not responding on port ${ctx.port} after auto-start`);
|
|
527
524
|
}
|
|
528
|
-
const r = await ofetch.raw(`http://${HOST}:${ctx.port}/command`, {
|
|
529
|
-
method: "POST",
|
|
530
|
-
headers: { "Content-Type": "application/json" },
|
|
531
|
-
body: { command: cmd },
|
|
532
|
-
signal: AbortSignal.timeout(5000),
|
|
533
|
-
ignoreResponseError: true,
|
|
534
|
-
responseType: "text"
|
|
535
|
-
});
|
|
525
|
+
const r = await ofetch.raw(`http://${HOST}:${ctx.port}/command`, { ...this.commandRequest(cmd, 5000), responseType: "text" });
|
|
536
526
|
const body = typeof r._data === "string" ? r._data : JSON.stringify(r._data);
|
|
537
527
|
process.stdout.write(body);
|
|
538
528
|
if (!body.endsWith("\n"))
|
package/dst/ase-setup.js
CHANGED
|
@@ -63,27 +63,18 @@ export default class SetupCommand {
|
|
|
63
63
|
path.join(prefix, "bin"),
|
|
64
64
|
path.join(prefix, "lib", "node_modules")
|
|
65
65
|
];
|
|
66
|
+
const accessible = (dir, mode) => fs.access(dir, mode).then(() => true, () => false);
|
|
66
67
|
for (const dir of candidates) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
catch {
|
|
78
|
-
/* directory does not exist: check parent writability */
|
|
79
|
-
try {
|
|
80
|
-
await fs.access(path.dirname(dir), fs.constants.W_OK);
|
|
81
|
-
}
|
|
82
|
-
catch {
|
|
83
|
-
return true;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
68
|
+
/* a writable directory needs no elevation at all */
|
|
69
|
+
if (await accessible(dir, fs.constants.W_OK))
|
|
70
|
+
continue;
|
|
71
|
+
/* the directory exists, but is not writable: require sudo */
|
|
72
|
+
if (await accessible(dir, fs.constants.F_OK))
|
|
73
|
+
return true;
|
|
74
|
+
/* the directory does not exist: require sudo unless it can
|
|
75
|
+
be created inside a writable parent directory */
|
|
76
|
+
if (!(await accessible(path.dirname(dir), fs.constants.W_OK)))
|
|
77
|
+
return true;
|
|
87
78
|
}
|
|
88
79
|
return false;
|
|
89
80
|
}
|
|
@@ -110,25 +101,7 @@ export default class SetupCommand {
|
|
|
110
101
|
for (let i = 0; i < retries; i++) {
|
|
111
102
|
const final = (i === retries - 1);
|
|
112
103
|
try {
|
|
113
|
-
|
|
114
|
-
const result = await execa(cmd, args, { stdio: "ignore", cwd, reject: false });
|
|
115
|
-
if (typeof result.exitCode === "number" && result.exitCode !== 0) {
|
|
116
|
-
if (!final) {
|
|
117
|
-
this.log.write("info", `setup: attempt ${i + 1}/${retries} failed for "${cmd} ${args.join(" ")}" ` +
|
|
118
|
-
`(exit code: ${result.exitCode}): retrying...`);
|
|
119
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
if (ignoreError !== undefined) {
|
|
123
|
-
this.log.write("info", `setup: ${ignoreError} (skipped)`);
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
this.log.write("error", `setup: command failed: exit code: ${result.exitCode}`);
|
|
127
|
-
throw new Error(`command "${cmd} ${args.join(" ")}" failed (exit code: ${result.exitCode})`);
|
|
128
|
-
}
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
await execa(cmd, args, { stdio: "pipe", cwd });
|
|
104
|
+
await execa(cmd, args, { stdio: quiet ? "ignore" : "pipe", cwd });
|
|
132
105
|
return;
|
|
133
106
|
}
|
|
134
107
|
catch (err) {
|
package/dst/ase-statusline.js
CHANGED
|
@@ -166,6 +166,11 @@ const probeMemory = () => {
|
|
|
166
166
|
return { used: 0, total: 0 };
|
|
167
167
|
}
|
|
168
168
|
};
|
|
169
|
+
/* memoize a zero-argument function, computing its value at most once on first use */
|
|
170
|
+
const memoize = (fn) => {
|
|
171
|
+
let cache = null;
|
|
172
|
+
return () => (cache ??= { value: fn() }).value;
|
|
173
|
+
};
|
|
169
174
|
/* command-line handling */
|
|
170
175
|
export default class StatuslineCommand {
|
|
171
176
|
log;
|
|
@@ -263,16 +268,8 @@ export default class StatuslineCommand {
|
|
|
263
268
|
/* lazy memoized probes for cross-renderer values: each is computed at most
|
|
264
269
|
once per run and only when first requested by a renderer (or by the
|
|
265
270
|
post-loop tmux publish for the Config cascade) */
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
if (sessCache === null)
|
|
269
|
-
sessCache = data.session_id ?? "unknown";
|
|
270
|
-
return sessCache;
|
|
271
|
-
};
|
|
272
|
-
let cfgCache = null;
|
|
273
|
-
const getCfg = () => {
|
|
274
|
-
if (cfgCache !== null)
|
|
275
|
-
return cfgCache;
|
|
271
|
+
const getSession = memoize(() => data.session_id ?? "unknown");
|
|
272
|
+
const getCfg = memoize(() => {
|
|
276
273
|
let taskId = process.env.ASE_TASK_ID ?? "";
|
|
277
274
|
let persona = process.env.ASE_PERSONA_STYLE ?? "";
|
|
278
275
|
try {
|
|
@@ -288,21 +285,10 @@ export default class StatuslineCommand {
|
|
|
288
285
|
catch (_e) {
|
|
289
286
|
/* cascade unavailable; keep env-var fallbacks */
|
|
290
287
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
const getGit = () => {
|
|
296
|
-
if (gitCache === null)
|
|
297
|
-
gitCache = probeGit(data.workspace?.current_dir ?? "");
|
|
298
|
-
return gitCache;
|
|
299
|
-
};
|
|
300
|
-
let memCache = null;
|
|
301
|
-
const getMem = () => {
|
|
302
|
-
if (memCache === null)
|
|
303
|
-
memCache = probeMemory();
|
|
304
|
-
return memCache;
|
|
305
|
-
};
|
|
288
|
+
return { taskId, persona };
|
|
289
|
+
});
|
|
290
|
+
const getGit = memoize(() => probeGit(data.workspace?.current_dir ?? ""));
|
|
291
|
+
const getMem = memoize(() => probeMemory());
|
|
306
292
|
/* identifier to renderer map: each callback fetches its own information
|
|
307
293
|
directly from data (or via the lazy helpers above for shared values) */
|
|
308
294
|
const renderers = {
|
|
@@ -432,11 +418,11 @@ export default class StatuslineCommand {
|
|
|
432
418
|
},
|
|
433
419
|
/* ==== VERSIONS ==== */
|
|
434
420
|
V: () => {
|
|
435
|
-
const
|
|
421
|
+
const toolVersion = data.version ?? "";
|
|
436
422
|
const aseVersion = pkg.version ?? "";
|
|
437
423
|
let version = "";
|
|
438
|
-
if (
|
|
439
|
-
version +=
|
|
424
|
+
if (toolVersion !== "")
|
|
425
|
+
version += `${tool}/${toolVersion}`;
|
|
440
426
|
if (aseVersion !== "")
|
|
441
427
|
version += `${version !== "" ? " " : ""}ase/${aseVersion}`;
|
|
442
428
|
emit(`${prefix("⎈", "version")}${c.bold(version)}`);
|
package/dst/ase-task.js
CHANGED
|
@@ -71,6 +71,8 @@ export class Task {
|
|
|
71
71
|
};
|
|
72
72
|
const basedir = (read("project.artifact.task.basedir") || ".ase/task")
|
|
73
73
|
.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
|
|
74
|
+
if (basedir.split("/").includes(".."))
|
|
75
|
+
throw new Error(`task: configured "basedir" "${basedir}" must not escape the project root`);
|
|
74
76
|
const files = read("project.artifact.task.files") || "*.md";
|
|
75
77
|
return { basedir, files };
|
|
76
78
|
}
|
|
@@ -89,33 +91,18 @@ export class Task {
|
|
|
89
91
|
}
|
|
90
92
|
/* resolve the on-disk path for a given task id; as a side effect,
|
|
91
93
|
eagerly migrate any legacy <basedir>/<id>/plan.md files to the
|
|
92
|
-
current <basedir>/TASK-<id>.md layout
|
|
93
|
-
|
|
94
|
+
current <basedir>/TASK-<id>.md layout ("migrateAll" is a cheap
|
|
95
|
+
no-op once the store is migrated) */
|
|
94
96
|
static path(log, id) {
|
|
95
97
|
Task.validateId(id);
|
|
96
98
|
Task.enforceFiles(log, id);
|
|
97
|
-
|
|
98
|
-
Task.migrateAll(log);
|
|
99
|
+
Task.migrateAll(log);
|
|
99
100
|
return path.join(Task.baseDir(log), `TASK-${id}.md`);
|
|
100
101
|
}
|
|
101
|
-
/* cheaply check whether any legacy <basedir>/<id>/plan.md file still
|
|
102
|
-
exists in the task base directory and thus needs migration */
|
|
103
|
-
static needsMigration(log) {
|
|
104
|
-
const dir = Task.baseDir(log);
|
|
105
|
-
if (!fs.existsSync(dir))
|
|
106
|
-
return false;
|
|
107
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
108
|
-
if (!entry.isDirectory() || !/^[A-Za-z0-9_-]+$/.test(entry.name))
|
|
109
|
-
continue;
|
|
110
|
-
if (fs.existsSync(path.join(dir, entry.name, "plan.md")))
|
|
111
|
-
return true;
|
|
112
|
-
}
|
|
113
|
-
return false;
|
|
114
|
-
}
|
|
115
102
|
/* migrate all legacy <basedir>/<id>/plan.md task files to the current
|
|
116
103
|
<basedir>/TASK-<id>.md layout; an existing TASK-<id>.md is never
|
|
117
|
-
overwritten; the
|
|
118
|
-
returns the
|
|
104
|
+
overwritten; the legacy <id>/ directory is removed only if it is
|
|
105
|
+
empty afterwards; returns the migrated task ids in lexicographic order */
|
|
119
106
|
static migrateAll(log) {
|
|
120
107
|
const dir = Task.baseDir(log);
|
|
121
108
|
if (!fs.existsSync(dir))
|
|
@@ -134,7 +121,12 @@ export class Task {
|
|
|
134
121
|
continue;
|
|
135
122
|
}
|
|
136
123
|
fs.renameSync(oldFile, newFile);
|
|
137
|
-
|
|
124
|
+
/* drop the legacy directory, but only if it is really empty */
|
|
125
|
+
const legacyDir = path.dirname(oldFile);
|
|
126
|
+
if (fs.readdirSync(legacyDir).length === 0)
|
|
127
|
+
fs.rmdirSync(legacyDir);
|
|
128
|
+
else
|
|
129
|
+
log.write("warning", `task: keeping non-empty legacy directory "${legacyDir}"`);
|
|
138
130
|
migrated.push(id);
|
|
139
131
|
}
|
|
140
132
|
migrated.sort((a, b) => a.localeCompare(b));
|
|
@@ -187,8 +179,7 @@ export class Task {
|
|
|
187
179
|
/* scan the task base directory (after eager migration) for
|
|
188
180
|
"TASK-<id>.md" files matching the configured "files" miniglob */
|
|
189
181
|
static scan(log) {
|
|
190
|
-
|
|
191
|
-
Task.migrateAll(log);
|
|
182
|
+
Task.migrateAll(log);
|
|
192
183
|
const { basedir, files } = Task.spec(log);
|
|
193
184
|
const dir = path.join(Task.projectRoot(), basedir);
|
|
194
185
|
if (!fs.existsSync(dir))
|
package/dst/ase.js
CHANGED
|
@@ -59,12 +59,16 @@ const main = async () => {
|
|
|
59
59
|
/* parse program arguments */
|
|
60
60
|
await program.parseAsync(process.argv);
|
|
61
61
|
/* gracefully terminate */
|
|
62
|
+
await log.close();
|
|
62
63
|
process.exit(0);
|
|
63
64
|
};
|
|
64
|
-
main().catch((err) => {
|
|
65
|
-
if (err instanceof CommanderError)
|
|
65
|
+
main().catch(async (err) => {
|
|
66
|
+
if (err instanceof CommanderError) {
|
|
67
|
+
await log.close();
|
|
66
68
|
process.exit(err.exitCode);
|
|
69
|
+
}
|
|
67
70
|
const message = err instanceof Error ? err.message : String(err);
|
|
68
71
|
log.write("error", message);
|
|
72
|
+
await log.close();
|
|
69
73
|
process.exit(1);
|
|
70
74
|
});
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"homepage": "https://ase.tools",
|
|
7
7
|
"repository": { "url": "git+https://github.com/rse/ase.git", "type": "git" },
|
|
8
8
|
"bugs": { "url": "https://github.com/rse/ase/issues" },
|
|
9
|
-
"version": "0.9.
|
|
9
|
+
"version": "0.9.47",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"author": {
|
|
12
12
|
"name": "Dr. Ralf S. Engelschall",
|
package/plugin/etc/stx.conf
CHANGED
|
@@ -11,12 +11,12 @@ lint
|
|
|
11
11
|
|
|
12
12
|
# [plugin] build project
|
|
13
13
|
build : lint
|
|
14
|
-
( for name in $(cd skills; ls -1 | grep -v ase-
|
|
14
|
+
( for name in $(cd skills; ls -1 | grep -v ase-help-intent | sort); do
|
|
15
15
|
echo "<skill name=\"$name\">"
|
|
16
16
|
cat skills/$name/help.md | sed -e '/^## SEE ALSO/,$d'
|
|
17
17
|
echo "</skill>"
|
|
18
18
|
done
|
|
19
|
-
) >skills/ase-
|
|
19
|
+
) >skills/ase-help-intent/data.md
|
|
20
20
|
|
|
21
21
|
# [plugin] remove all generated files
|
|
22
22
|
clean
|
package/plugin/meta/ase-skill.md
CHANGED
|
@@ -140,6 +140,15 @@ Skill Sequential Processing
|
|
|
140
140
|
- *IMPORTANT*: For each <step/> you *MUST* use the `TaskUpdate` tool
|
|
141
141
|
for updating its status *during* processing, once a <step/> finished.
|
|
142
142
|
|
|
143
|
+
- *IMPORTANT*: Whenever a skill *stops early* -- because a <step/>
|
|
144
|
+
instructed you to *STOP* processing (on an error, a cancelled
|
|
145
|
+
dialog, a short-circuit branch, or any other early exit) -- you
|
|
146
|
+
*MUST*, *before* emitting the final <template/>, use the `TaskUpdate`
|
|
147
|
+
tool to set the status of *every* still `pending` or `in_progress`
|
|
148
|
+
task of the current <flow/> to `deleted`, so that no task of a
|
|
149
|
+
finished skill remains open. Steps that were *skipped* because their
|
|
150
|
+
`condition` attribute was not met *MUST* be treated the same way.
|
|
151
|
+
|
|
143
152
|
- *IMPORTANT*: You *MUST* *strictly sequentially* execute every <step/> in
|
|
144
153
|
a <flow/>. You *MUST* not implicitly skip any <step/> during
|
|
145
154
|
processing, except you were explicitly requested to do this or the
|
package/plugin/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"homepage": "https://ase.tools",
|
|
7
7
|
"repository": { "url": "git+https://github.com/rse/ase.git", "type": "git" },
|
|
8
8
|
"bugs": { "url": "https://github.com/rse/ase/issues" },
|
|
9
|
-
"version": "0.9.
|
|
9
|
+
"version": "0.9.47",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"author": {
|
|
12
12
|
"name": "Dr. Ralf S. Engelschall",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: ase-
|
|
2
|
+
name: ase-help-intent
|
|
3
3
|
argument-hint: "[--help|-h] <intent>"
|
|
4
4
|
description: >
|
|
5
5
|
Match a free-text intent against the accumulated help of all ASE
|
|
@@ -20,12 +20,12 @@ allowed-tools:
|
|
|
20
20
|
@${CLAUDE_SKILL_DIR}/../../meta/ase-dialog.md
|
|
21
21
|
@${CLAUDE_SKILL_DIR}/../../meta/ase-getopt.md
|
|
22
22
|
|
|
23
|
-
<skill name="ase-
|
|
23
|
+
<skill name="ase-help-intent">
|
|
24
24
|
Match an Intent to an ASE Command
|
|
25
25
|
</skill>
|
|
26
26
|
|
|
27
27
|
<expand name="getopt"
|
|
28
|
-
arg1="ase-
|
|
28
|
+
arg1="ase-help-intent"
|
|
29
29
|
arg2="">
|
|
30
30
|
$ARGUMENTS
|
|
31
31
|
</expand>
|
|
@@ -54,7 +54,7 @@ catalog you match <intent/> against:
|
|
|
54
54
|
processing the entire current skill:
|
|
55
55
|
|
|
56
56
|
<template>
|
|
57
|
-
⧉ **ASE**: ✪ skill: **ase-
|
|
57
|
+
⧉ **ASE**: ✪ skill: **ase-help-intent**, ▶ ERROR: expected a `<intent>` argument
|
|
58
58
|
</template>
|
|
59
59
|
</if>
|
|
60
60
|
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
|
|
2
2
|
## NAME
|
|
3
3
|
|
|
4
|
-
`ase-
|
|
4
|
+
`ase-help-intent` - Match an Intent to an ASE Command
|
|
5
5
|
|
|
6
6
|
## SYNOPSIS
|
|
7
7
|
|
|
8
|
-
`ase-
|
|
8
|
+
`ase-help-intent`
|
|
9
9
|
[`--help`|`-h`]
|
|
10
10
|
*intent*
|
|
11
11
|
|
|
12
12
|
## DESCRIPTION
|
|
13
13
|
|
|
14
|
-
The `ase-
|
|
14
|
+
The `ase-help-intent` skill matches a free-text *intent* against the
|
|
15
15
|
*accumulated help* of all ASE skills -- the concatenation of every
|
|
16
|
-
skill's `help.md` file into `skills/ase-
|
|
16
|
+
skill's `help.md` file into `skills/ase-help-intent/data.md`, built by
|
|
17
17
|
`npm start build` in `plugin/` -- and generates the *single* best-fitting
|
|
18
18
|
`/ase:ase-xxx-xxx` command that realizes the intent, complete with
|
|
19
19
|
concrete option flags and positional arguments derived from the selected
|
|
@@ -42,15 +42,16 @@ entirely through the intent argument and the interactive dialog.
|
|
|
42
42
|
Route an intent to the matching command and pick from the dialog:
|
|
43
43
|
|
|
44
44
|
```text
|
|
45
|
-
❯ /ase-
|
|
45
|
+
❯ /ase-help-intent lint the TypeScript sources for high-severity issues only
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
Route a planning intent to the matching command:
|
|
49
49
|
|
|
50
50
|
```text
|
|
51
|
-
❯ /ase-
|
|
51
|
+
❯ /ase-help-intent explain how the authentication module works
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
## SEE ALSO
|
|
55
55
|
|
|
56
|
-
[`ase-
|
|
56
|
+
[`ase-help-skill`](../ase-help-skill/help.md), [`ase-meta-proximity`](../ase-meta-proximity/help.md), [`ase-meta-search`](../ase-meta-search/help.md),
|
|
57
|
+
[`ase-code-craft`](../ase-code-craft/help.md).
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ase-help-skill
|
|
3
|
+
argument-hint: "[--help|-h] [<skill-name>]"
|
|
4
|
+
description: >
|
|
5
|
+
Show the manual page of an ASE skill, addressed by its full name, by
|
|
6
|
+
any abbreviation of it, or by a description of its purpose, and list
|
|
7
|
+
the entire skill catalog when no name is given. Use when the user
|
|
8
|
+
wants the "manual", "man page", "manpage", or "help" of a particular
|
|
9
|
+
ASE skill, or asks what a certain `ase-xxx-xxx` skill does.
|
|
10
|
+
user-invocable: true
|
|
11
|
+
disable-model-invocation: false
|
|
12
|
+
effort: medium
|
|
13
|
+
allowed-tools:
|
|
14
|
+
- "Read"
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
@${CLAUDE_SKILL_DIR}/../../meta/ase-control.md
|
|
18
|
+
@${CLAUDE_SKILL_DIR}/../../meta/ase-skill.md
|
|
19
|
+
@${CLAUDE_SKILL_DIR}/../../meta/ase-dialog.md
|
|
20
|
+
@${CLAUDE_SKILL_DIR}/../../meta/ase-getopt.md
|
|
21
|
+
|
|
22
|
+
<skill name="ase-help-skill">
|
|
23
|
+
Show the Manual Page of an ASE Skill
|
|
24
|
+
</skill>
|
|
25
|
+
|
|
26
|
+
<expand name="getopt"
|
|
27
|
+
arg1="ase-help-skill"
|
|
28
|
+
arg2="">
|
|
29
|
+
$ARGUMENTS
|
|
30
|
+
</expand>
|
|
31
|
+
|
|
32
|
+
<objective>
|
|
33
|
+
*Show* the *manual page* of the ASE skill addressed by the following
|
|
34
|
+
skill name, abbreviation of it, or description of its purpose:
|
|
35
|
+
<skill-ref><getopt-arguments/></skill-ref>
|
|
36
|
+
</objective>
|
|
37
|
+
|
|
38
|
+
The following <catalog/> is index of all ASE skills -- one
|
|
39
|
+
`⎈ **<group/>**` entry per skill group and one
|
|
40
|
+
``○ `<name/>`: <purpose/>`` entry per skill -- and this is
|
|
41
|
+
the *sole* index <skill-ref/> is resolved against:
|
|
42
|
+
|
|
43
|
+
<catalog>
|
|
44
|
+
@${CLAUDE_SKILL_DIR}/catalog.md
|
|
45
|
+
</catalog>
|
|
46
|
+
|
|
47
|
+
<flow>
|
|
48
|
+
|
|
49
|
+
1. <step id="STEP 1: Resolve Skill Name">
|
|
50
|
+
|
|
51
|
+
1. <if condition="<skill-ref/> is empty">
|
|
52
|
+
No particular skill was addressed, so render the *entire*
|
|
53
|
+
<catalog/> as a browsable list with the following <template/>
|
|
54
|
+
-- one list entry per catalog entry, in catalog order, where
|
|
55
|
+
<name/> and <purpose/> are the two fields of the entry (and
|
|
56
|
+
<name-padded/> is <name/>, padded to 22 characters with
|
|
57
|
+
spaces on the right) -- and then immediately *STOP* processing
|
|
58
|
+
the entire current skill:
|
|
59
|
+
|
|
60
|
+
<template>
|
|
61
|
+
<ase-tpl-head title="SKILL CATALOG"/>
|
|
62
|
+
|
|
63
|
+
<catalog/>
|
|
64
|
+
|
|
65
|
+
<ase-tpl-foot title="SKILL CATALOG"/>
|
|
66
|
+
|
|
67
|
+
⧉ **ASE**: ✪ skill: **ase-help-skill**, ▶ hint: **run `/ase-help-skill ase-xxx-xxx` for manual page of individual skill**
|
|
68
|
+
</template>
|
|
69
|
+
</if>
|
|
70
|
+
|
|
71
|
+
2. Set <skill-ref-raw/> to <skill-ref/> with only its leading and
|
|
72
|
+
trailing whitespace stripped, as the *verbatim* wording of the
|
|
73
|
+
user is required later on.
|
|
74
|
+
|
|
75
|
+
*Normalize* <skill-ref/> by stripping all leading and trailing
|
|
76
|
+
whitespace and then, repeatedly, any leading `/` and `ase:`
|
|
77
|
+
prefix, so that `ase-code-lint`, `/ase-code-lint`,
|
|
78
|
+
`ase:ase-code-lint`, and `/ase:ase-code-lint` all normalize to
|
|
79
|
+
`ase-code-lint`. Do not output anything.
|
|
80
|
+
|
|
81
|
+
3. Resolve the normalized <skill-ref/> against <catalog/> in *three*
|
|
82
|
+
tiers and store the outcome in <candidates/>. Each tier is tried
|
|
83
|
+
only if all preceding tiers yielded *no* candidate at all:
|
|
84
|
+
|
|
85
|
+
1. *Exact Name Tier*:
|
|
86
|
+
|
|
87
|
+
If a catalog *name* is *equal* to <skill-ref/>, set
|
|
88
|
+
<candidates/> to exactly that *single* name.
|
|
89
|
+
|
|
90
|
+
2. *Substring Name Tier*:
|
|
91
|
+
|
|
92
|
+
Set <candidates/> to *all* catalog *names* *containing*
|
|
93
|
+
<skill-ref/> as a substring, in alphabetical order.
|
|
94
|
+
|
|
95
|
+
3. *Fuzzy Purpose Tier*:
|
|
96
|
+
|
|
97
|
+
Set <candidates/> to *all* catalog names whose *purpose* --
|
|
98
|
+
the part *after* the colon of the catalog entry -- *fuzzily*
|
|
99
|
+
matches <skill-ref-raw/>, in *descending* order of match
|
|
100
|
+
quality. Match against <skill-ref-raw/>, and *not* against
|
|
101
|
+
<skill-ref/>, as this tier matches free-text wording, which
|
|
102
|
+
the normalization of sub-step 2 would distort.
|
|
103
|
+
|
|
104
|
+
A purpose matches fuzzily if it shares the topic, the
|
|
105
|
+
wording, or evident synonyms with <skill-ref-raw/>, so
|
|
106
|
+
that e.g. `manpage` matches `Show the Manual Page of an
|
|
107
|
+
ASE Skill` and `root cause` matches `Five-Whys Root-Cause
|
|
108
|
+
Analysis`. Include *plausible* matches only -- if none is
|
|
109
|
+
plausible, leave <candidates/> empty.
|
|
110
|
+
|
|
111
|
+
Set <count/> to the number of entries in <candidates/>.
|
|
112
|
+
Do not output anything.
|
|
113
|
+
|
|
114
|
+
</step>
|
|
115
|
+
|
|
116
|
+
2. <step id="STEP 2: Dispatch Resolution">
|
|
117
|
+
|
|
118
|
+
1. <if condition="<count/> is equal 0">
|
|
119
|
+
Only output the following <template/> and then immediately
|
|
120
|
+
*STOP* processing the entire current skill:
|
|
121
|
+
|
|
122
|
+
<template>
|
|
123
|
+
⧉ **ASE**: ✪ skill: **ase-help-skill**, ▶ ERROR: unknown skill: **<skill-ref-raw/>**
|
|
124
|
+
</template>
|
|
125
|
+
</if>
|
|
126
|
+
|
|
127
|
+
2. <elseif condition="<count/> is equal 1">
|
|
128
|
+
Set <name/> to the single entry of <candidates/> and continue
|
|
129
|
+
processing. Do not output anything.
|
|
130
|
+
</elseif>
|
|
131
|
+
|
|
132
|
+
3. <else>
|
|
133
|
+
The abbreviation is *ambiguous*, so let the user pick the
|
|
134
|
+
intended skill.
|
|
135
|
+
|
|
136
|
+
Set <shown/> to the *first* 9 entries of <candidates/>, as the
|
|
137
|
+
dialog renders at most *nine* answer lines.
|
|
138
|
+
|
|
139
|
+
<if condition="<count/> is greater than 9">
|
|
140
|
+
Set <truncation> (showing 9 of <count/> candidates)</truncation>
|
|
141
|
+
</if>
|
|
142
|
+
<else>
|
|
143
|
+
Set <truncation></truncation> (set to empty)
|
|
144
|
+
</else>
|
|
145
|
+
|
|
146
|
+
In the following, you *MUST* *NOT* use your built-in
|
|
147
|
+
<user-dialog-tool/> tool! Instead, you *MUST* just show a custom
|
|
148
|
+
dialog according to the expanded `custom-dialog` definition. You
|
|
149
|
+
*MUST* closely follow this definition.
|
|
150
|
+
|
|
151
|
+
Let the user select the intended skill by raising a question
|
|
152
|
+
with the following custom dialog, where each answer line
|
|
153
|
+
corresponds to one entry of <shown/>, using the catalog *name*
|
|
154
|
+
as the label and its catalog *purpose* as the description:
|
|
155
|
+
|
|
156
|
+
<expand name="custom-dialog" arg1="--no-other">
|
|
157
|
+
Ambiguous Skill: Which skill's manual page should be shown?<truncation/>
|
|
158
|
+
<name/>: <purpose/>
|
|
159
|
+
[...]
|
|
160
|
+
</expand>
|
|
161
|
+
|
|
162
|
+
Check the <result/> and dispatch accordingly:
|
|
163
|
+
|
|
164
|
+
- If <result/> is `CANCEL`:
|
|
165
|
+
*STOP* processing without any further output.
|
|
166
|
+
|
|
167
|
+
- Otherwise: Set <name/> to the selected skill name and
|
|
168
|
+
continue processing.
|
|
169
|
+
</else>
|
|
170
|
+
|
|
171
|
+
</step>
|
|
172
|
+
|
|
173
|
+
3. <step id="STEP 3: Render Manual Page">
|
|
174
|
+
|
|
175
|
+
1. Use the `Read` tool to read the manual page of the resolved
|
|
176
|
+
skill <name/> and set <manual/> to its content. The file path is
|
|
177
|
+
formed by joining <ase-plugin-root/> and `skills/<name/>/help.md`
|
|
178
|
+
with exactly *one* `/` separator, as <ase-plugin-root/> may or
|
|
179
|
+
may not carry a trailing `/`. Do not output anything related to
|
|
180
|
+
this tool call.
|
|
181
|
+
|
|
182
|
+
2. <if condition="<manual/> is empty or could not be read">
|
|
183
|
+
Only output the following <template/> and then immediately
|
|
184
|
+
*STOP* processing the entire current skill:
|
|
185
|
+
|
|
186
|
+
<template>
|
|
187
|
+
⧉ **ASE**: ✪ skill: **ase-help-skill**, ▶ ERROR: unreadable manual page: **<name/>**
|
|
188
|
+
</template>
|
|
189
|
+
</if>
|
|
190
|
+
|
|
191
|
+
3. Treat <manual/> as *verbatim* Markdown. You *MUST* *NOT*
|
|
192
|
+
truncate, summarize, reformat, or partially show it. Only output
|
|
193
|
+
the following <template/>:
|
|
194
|
+
|
|
195
|
+
<template>
|
|
196
|
+
<ase-tpl-head title="MANUAL PAGE: <name/>"/>
|
|
197
|
+
<manual/>
|
|
198
|
+
<ase-tpl-foot title="MANUAL PAGE: <name/>"/>
|
|
199
|
+
</template>
|
|
200
|
+
|
|
201
|
+
</step>
|
|
202
|
+
|
|
203
|
+
</flow>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
|
|
2
|
+
⎈ **HELP**
|
|
3
|
+
○ `ase-help-skill`: Show the Manual Page of an ASE Skill
|
|
4
|
+
○ `ase-help-intent`: Match an Intent to an ASE Command
|
|
5
|
+
|
|
6
|
+
⎈ **RESEARCH**
|
|
7
|
+
○ `ase-meta-search`: Search the Internet/Web
|
|
8
|
+
○ `ase-meta-proximity`: Determine the Conceptual Proximity of a Topic
|
|
9
|
+
○ `ase-meta-brainstorm`: Collaboratively Brainstorm a Topic
|
|
10
|
+
○ `ase-meta-chat`: Query Foreign LLM for Chat
|
|
11
|
+
○ `ase-meta-quorum`: Query Multiple AIs for Quorum Answer
|
|
12
|
+
○ `ase-meta-diaboli`: Play "Devil's Advocate" (Latin: "Advocatus Diaboli")
|
|
13
|
+
○ `ase-meta-steelman`: Build the "Steelman" Argument
|
|
14
|
+
○ `ase-meta-eli5`: Explain a Topic Like I'm 5
|
|
15
|
+
○ `ase-meta-why`: Five-Whys Root-Cause Analysis
|
|
16
|
+
○ `ase-meta-evaluate`: Evaluate Alternatives
|
|
17
|
+
|
|
18
|
+
⎈ **ARCHITECTURE**
|
|
19
|
+
○ `ase-arch-analyze`: Review Software Architecture
|
|
20
|
+
○ `ase-arch-discover`: Discover Components
|
|
21
|
+
|
|
22
|
+
⎈ **CODING**
|
|
23
|
+
○ `ase-code-analyze`: Analyze Source Code
|
|
24
|
+
○ `ase-code-lint`: Lint Source Code
|
|
25
|
+
○ `ase-code-explain`: Explain Source Code
|
|
26
|
+
○ `ase-code-insight`: Project Insight
|
|
27
|
+
○ `ase-code-craft`: Craft Source Code
|
|
28
|
+
○ `ase-code-refactor`: Refactor Artifacts
|
|
29
|
+
○ `ase-code-resolve`: Resolve Problem
|
|
30
|
+
○ `ase-sync-import`: Import Foreign Sources into Artifact Set
|
|
31
|
+
○ `ase-sync-reconcile`: Reconcile Artifact Set to Artifact Set
|
|
32
|
+
○ `ase-sync-export`: Export Artifact Set to Side-by-Side Files
|
|
33
|
+
|
|
34
|
+
⎈ **DOCUMENTATION**
|
|
35
|
+
○ `ase-docs-distill`: Distill Document Key Points
|
|
36
|
+
○ `ase-docs-proofread`: Proofread Documents
|
|
37
|
+
|
|
38
|
+
⎈ **VERSION CONTROL**
|
|
39
|
+
○ `ase-meta-changelog`: Update ChangeLog Entries
|
|
40
|
+
○ `ase-meta-commit`: Git Commit Message
|
|
41
|
+
○ `ase-meta-diff`: Summarize Diff
|
|
42
|
+
○ `ase-meta-review`: Review Staged Changes
|
|
43
|
+
|
|
44
|
+
⎈ **TASK MANAGEMENT**
|
|
45
|
+
○ `ase-task-id`: Configure Task Id
|
|
46
|
+
○ `ase-task-list`: List Task Plans
|
|
47
|
+
○ `ase-task-view`: View a Task Plan
|
|
48
|
+
○ `ase-task-edit`: Iteratively Edit a Task Plan
|
|
49
|
+
○ `ase-task-grill`: Iteratively Grill a Task Plan
|
|
50
|
+
○ `ase-task-condense`: Condense a Task Plan
|
|
51
|
+
○ `ase-task-reboot`: Reboot a Task Plan
|
|
52
|
+
○ `ase-task-preflight`: Preflight a Task Plan
|
|
53
|
+
○ `ase-task-implement`: Implement a Task Plan
|
|
54
|
+
○ `ase-task-rename`: Rename a Task Plan
|
|
55
|
+
○ `ase-task-delete`: Delete a Task Plan
|
|
56
|
+
|
|
57
|
+
⎈ **OTHER SKILLS**
|
|
58
|
+
○ `ase-meta-persona`: Persona Configuration
|
|
59
|
+
○ `ase-meta-compat`: Self-Test ASE Compatibility
|
|
60
|
+
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
|
|
2
|
+
## NAME
|
|
3
|
+
|
|
4
|
+
`ase-help-skill` - Show the Manual Page of an ASE Skill
|
|
5
|
+
|
|
6
|
+
## SYNOPSIS
|
|
7
|
+
|
|
8
|
+
`ase-help-skill`
|
|
9
|
+
[`--help`|`-h`]
|
|
10
|
+
[*skill-name*]
|
|
11
|
+
|
|
12
|
+
## DESCRIPTION
|
|
13
|
+
|
|
14
|
+
The `ase-help-skill` skill renders the *manual page* (`help.md`) of the
|
|
15
|
+
ASE skill addressed by *skill-name*. The page is emitted verbatim,
|
|
16
|
+
framed between a `( HELP )` header and footer rule.
|
|
17
|
+
|
|
18
|
+
The *skill-name* is resolved against a *catalog* of all ASE skills --
|
|
19
|
+
one `name: purpose` entry per skill, generated into
|
|
20
|
+
`skills/ase-help-skill/data.md` by `npm start build` in `plugin/`.
|
|
21
|
+
Before resolving, any leading `/` and `ase:` prefix is stripped, so
|
|
22
|
+
`ase-code-lint`, `/ase-code-lint`, and `ase:ase-code-lint` are
|
|
23
|
+
equivalent. Resolution then proceeds in three tiers, each tried only if
|
|
24
|
+
all preceding ones found nothing: an *exact* match on the full skill
|
|
25
|
+
name wins outright, otherwise *every* skill name containing
|
|
26
|
+
*skill-name* as a substring becomes a candidate, and otherwise
|
|
27
|
+
*skill-name* is *fuzzily* matched against the skill *purposes* -- so
|
|
28
|
+
a topical phrase such as `root cause` still finds `ase-meta-why` --
|
|
29
|
+
with the candidates ordered by descending match quality.
|
|
30
|
+
|
|
31
|
+
A *skill-name* matching no skill in any tier is reported as an error. A
|
|
32
|
+
*skill-name* matching more than one skill opens an interactive dialog
|
|
33
|
+
listing the candidates with their purposes; as the dialog renders at
|
|
34
|
+
most nine answer lines, a broader abbreviation such as `task` shows the
|
|
35
|
+
first nine candidates and states how many exist in total.
|
|
36
|
+
|
|
37
|
+
If *skill-name* is omitted, the whole catalog is rendered as a
|
|
38
|
+
browsable `Skill`/`Purpose` table instead.
|
|
39
|
+
|
|
40
|
+
The skill exposes *no* option flags beyond `--help`/`-h`.
|
|
41
|
+
|
|
42
|
+
## ARGUMENTS
|
|
43
|
+
|
|
44
|
+
*skill-name*:
|
|
45
|
+
The full name of the ASE skill, any abbreviation of it, or a
|
|
46
|
+
description of its purpose. If omitted, the entire skill catalog is
|
|
47
|
+
listed.
|
|
48
|
+
|
|
49
|
+
## EXAMPLES
|
|
50
|
+
|
|
51
|
+
Show the manual page of `ase-code-lint` via its shortest abbreviation:
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
❯ /ase-help-skill lint
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Show the same manual page via its full, plugin-qualified name:
|
|
58
|
+
|
|
59
|
+
```text
|
|
60
|
+
❯ /ase-help-skill ase:ase-code-lint
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Pick a manual page from the candidates of an ambiguous abbreviation:
|
|
64
|
+
|
|
65
|
+
```text
|
|
66
|
+
❯ /ase-help-skill analyze
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Find a manual page by the purpose of the skill instead of its name:
|
|
70
|
+
|
|
71
|
+
```text
|
|
72
|
+
❯ /ase-help-skill root cause
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
List the entire skill catalog:
|
|
76
|
+
|
|
77
|
+
```text
|
|
78
|
+
❯ /ase-help-skill
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## SEE ALSO
|
|
82
|
+
|
|
83
|
+
[`ase-help-intent`](../ase-help-intent/help.md), [`ase-meta-proximity`](../ase-meta-proximity/help.md).
|
|
@@ -47,9 +47,9 @@ After condensing, the user is asked whether to stop or hand off to
|
|
|
47
47
|
Recognized tokens at this skill: `none` (default, interactive
|
|
48
48
|
answer required), `DONE` (stop), `EDIT` (hand off to
|
|
49
49
|
`ase-task-edit`), `IMPLEMENT` (hand off to `ase-task-implement`),
|
|
50
|
-
or `PREFLIGHT` (hand off to `ase-task-preflight`). Example:
|
|
51
|
-
EDIT,DONE` condenses, hands off to editing, and the editing
|
|
52
|
-
exit immediately.
|
|
50
|
+
or `PREFLIGHT` (hand off to `ase-task-preflight`). Example:
|
|
51
|
+
`--next EDIT,DONE` condenses, hands off to editing, and the editing
|
|
52
|
+
loop will exit immediately.
|
|
53
53
|
|
|
54
54
|
## ARGUMENTS
|
|
55
55
|
|
|
@@ -33,9 +33,9 @@ unless `--next` pre-selects this choice.
|
|
|
33
33
|
Recognized tokens at this skill: `none` (default, interactive
|
|
34
34
|
answer required), `DONE` (stop), `EDIT` (hand off to
|
|
35
35
|
`ase-task-edit`), `IMPLEMENT` (hand off to `ase-task-implement`),
|
|
36
|
-
or `PREFLIGHT` (hand off to `ase-task-preflight`). Example:
|
|
37
|
-
EDIT,DONE` reboots, hands off to editing, and the editing
|
|
38
|
-
exit immediately.
|
|
36
|
+
or `PREFLIGHT` (hand off to `ase-task-preflight`). Example:
|
|
37
|
+
`--next EDIT,DONE` reboots, hands off to editing, and the editing
|
|
38
|
+
loop will exit immediately.
|
|
39
39
|
|
|
40
40
|
## ARGUMENTS
|
|
41
41
|
|
|
@@ -26,9 +26,9 @@ the plan in full, without any truncation or summarization.
|
|
|
26
26
|
## OPTIONS
|
|
27
27
|
|
|
28
28
|
`--full`|`-f`:
|
|
29
|
-
Render the plan in full, without collapsing the
|
|
30
|
-
DRAFT` section. By default, that section is
|
|
31
|
-
for plans longer than 90 lines.
|
|
29
|
+
Render the plan in full, without collapsing the
|
|
30
|
+
`IMPLEMENTATION DRAFT` section. By default, that section is
|
|
31
|
+
replaced with `[...]` for plans longer than 90 lines.
|
|
32
32
|
|
|
33
33
|
## ARGUMENTS
|
|
34
34
|
|