@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 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
- process.stdout.write(formatExpected());
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
- process.stdout.write(`${table.toString()}\n`);
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
- process.stdout.write(`${isScalar(val) ? val.value : val}\n`);
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 agent persona style */
246
- let persona = process.env.ASE_PERSONA_STYLE ?? "engineer";
247
- const val = cfg.get("agent.persona");
248
- if (typeof val === "string")
249
- persona = val;
250
- /* determine project boxing transparency */
251
- let boxing = process.env.ASE_PROJECT_BOXING ?? "white";
252
- const valBoxing = cfg.get("project.boxing");
253
- if (typeof valBoxing === "string")
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, v.object({
426
- command: v.optional(v.string()),
427
- skill: v.optional(v.string()),
428
- file_path: v.optional(v.string())
429
- }));
430
- else if (!spec.toolInputIsString && typeof rawInput === "object" && rawInput !== null)
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
- if (!KV.has(args.key))
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(args.key, args.val);
116
- return { content: [{ type: "text", text: `OK: stored key "${args.key}"` }] };
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 n = KV.clear(args.prefix);
137
- return { content: [{ type: "text", text: `OK: removed ${n} key(s)` }] };
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 removed = KV.delete(args.key);
156
- const msg = removed ?
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
- if (c.command === "clear") {
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
- /* log messages */
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
  }
@@ -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" && /^[ \t]*\r?(?:\n|$)/.test(text.slice(k + 1)))
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" && /^[ \t]*\r?(?:\n|$)/.test(text.slice(p + 1)))
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
- /* wire stdio server */
136
- server.onmessage = (msg) => {
137
- client?.send(msg).catch((err) => {
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
  };
@@ -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
- if (!Persona.styles.includes(style))
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) {
@@ -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
- await this.doStart();
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
- match = await probe(ctx.port, ctx.projectId);
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
- try {
68
- await fs.access(dir, fs.constants.W_OK);
69
- }
70
- catch {
71
- /* directory exists but not writable, or does not exist
72
- inside a non-writable parent: require sudo */
73
- try {
74
- await fs.access(dir, fs.constants.F_OK);
75
- return true;
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
- if (quiet) {
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) {
@@ -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
- let sessCache = null;
267
- const getSession = () => {
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
- cfgCache = { taskId, persona };
292
- return cfgCache;
293
- };
294
- let gitCache = null;
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 ccVersion = data.version ?? "";
421
+ const toolVersion = data.version ?? "";
436
422
  const aseVersion = pkg.version ?? "";
437
423
  let version = "";
438
- if (ccVersion !== "")
439
- version += `claude/${ccVersion}`;
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 on first access (guarded by
93
- a cheap check, so it is a no-op once the store is migrated) */
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
- if (Task.needsMigration(log))
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 now-empty <id>/ directory is removed afterwards;
118
- returns the list of migrated task ids in lexicographic order */
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
- fs.rmSync(path.join(dir, id), { recursive: true, force: true });
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
- if (Task.needsMigration(log))
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.46",
9
+ "version": "0.9.47",
10
10
  "license": "Apache-2.0",
11
11
  "author": {
12
12
  "name": "Dr. Ralf S. Engelschall",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ase",
3
- "version": "0.9.46",
3
+ "version": "0.9.47",
4
4
  "description": "Agentic Software Engineering (ASE)",
5
5
  "keywords": [ "agentic", "software", "engineering" ],
6
6
  "homepage": "https://ase.tools",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ase",
3
- "version": "0.9.46",
3
+ "version": "0.9.47",
4
4
  "description": "Agentic Software Engineering (ASE)",
5
5
  "keywords": [ "agentic", "software", "engineering" ],
6
6
  "homepage": "https://ase.tools",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ase",
3
- "version": "0.9.46",
3
+ "version": "0.9.47",
4
4
  "description": "Agentic Software Engineering (ASE)",
5
5
  "keywords": [ "agentic", "software", "engineering" ],
6
6
  "homepage": "https://ase.tools",
@@ -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-meta-intent | sort); do
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-meta-intent/data.md
19
+ ) >skills/ase-help-intent/data.md
20
20
 
21
21
  # [plugin] remove all generated files
22
22
  clean
@@ -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
@@ -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.46",
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-meta-intent
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-meta-intent">
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-meta-intent"
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-meta-intent**, ▶ ERROR: expected a `<intent>` argument
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-meta-intent` - Match an Intent to an ASE Command
4
+ `ase-help-intent` - Match an Intent to an ASE Command
5
5
 
6
6
  ## SYNOPSIS
7
7
 
8
- `ase-meta-intent`
8
+ `ase-help-intent`
9
9
  [`--help`|`-h`]
10
10
  *intent*
11
11
 
12
12
  ## DESCRIPTION
13
13
 
14
- The `ase-meta-intent` skill matches a free-text *intent* against the
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-meta-intent/data.md`, built by
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-meta-intent lint the TypeScript sources for high-severity issues only
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-meta-intent explain how the authentication module works
51
+ ❯ /ase-help-intent explain how the authentication module works
52
52
  ```
53
53
 
54
54
  ## SEE ALSO
55
55
 
56
- [`ase-meta-proximity`](../ase-meta-proximity/help.md), [`ase-meta-search`](../ase-meta-search/help.md), [`ase-code-craft`](../ase-code-craft/help.md).
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: `--next
51
- EDIT,DONE` condenses, hands off to editing, and the editing loop will
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: `--next
37
- EDIT,DONE` reboots, hands off to editing, and the editing loop will
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 `IMPLEMENTATION
30
- DRAFT` section. By default, that section is replaced with `[...]`
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