@rse/ase 0.9.47 → 0.9.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dst/ase-artifact.js +3 -1
  2. package/dst/ase-compat.js +1 -1
  3. package/dst/ase-config.js +76 -13
  4. package/dst/ase-diagram.js +10 -15
  5. package/dst/ase-getopt.js +66 -60
  6. package/dst/ase-guidance.js +89 -0
  7. package/dst/ase-hook.js +46 -66
  8. package/dst/ase-kv.js +18 -43
  9. package/dst/ase-mcp.js +1 -1
  10. package/dst/ase-meta.js +1 -1
  11. package/dst/ase-service.js +4 -6
  12. package/dst/ase-setup.js +24 -39
  13. package/dst/ase-skills.js +1 -1
  14. package/dst/ase-statusline.js +42 -18
  15. package/dst/ase-stdio.js +25 -0
  16. package/dst/ase-task.js +11 -11
  17. package/dst/ase.js +1 -1
  18. package/package.json +1 -1
  19. package/plugin/.claude-plugin/plugin.json +1 -1
  20. package/plugin/.codex-plugin/plugin.json +1 -1
  21. package/plugin/.github/plugin/plugin.json +1 -1
  22. package/plugin/meta/ase-common-task.md +9 -0
  23. package/plugin/meta/ase-constitution.md +17 -6
  24. package/plugin/meta/ase-getopt.md +11 -1
  25. package/plugin/meta/ase-skill.md +92 -4
  26. package/plugin/package.json +1 -1
  27. package/plugin/skills/ase-arch-analyze/SKILL.md +8 -6
  28. package/plugin/skills/ase-arch-discover/SKILL.md +12 -0
  29. package/plugin/skills/ase-code-analyze/SKILL.md +6 -4
  30. package/plugin/skills/ase-code-lint/SKILL.md +16 -0
  31. package/plugin/skills/ase-docs-proofread/SKILL.md +10 -0
  32. package/plugin/skills/ase-help-intent/SKILL.md +3 -8
  33. package/plugin/skills/ase-help-skill/SKILL.md +10 -2
  34. package/plugin/skills/ase-help-skill/catalog.md +1 -1
  35. package/plugin/skills/ase-meta-brainstorm/SKILL.md +8 -0
  36. package/plugin/skills/ase-meta-commit/SKILL.md +13 -0
  37. package/plugin/skills/ase-meta-config/SKILL.md +165 -0
  38. package/plugin/skills/ase-meta-config/help.md +136 -0
  39. package/plugin/skills/ase-meta-diaboli/help.md +1 -1
  40. package/plugin/skills/ase-meta-diff/SKILL.md +22 -0
  41. package/plugin/skills/ase-meta-review/SKILL.md +13 -1
  42. package/plugin/skills/ase-sync-export/SKILL.md +14 -0
  43. package/plugin/skills/ase-sync-import/SKILL.md +10 -0
  44. package/plugin/skills/ase-task-list/SKILL.md +21 -0
  45. package/plugin/skills/ase-task-view/SKILL.md +21 -0
  46. package/plugin/skills/ase-meta-persona/SKILL.md +0 -84
  47. package/plugin/skills/ase-meta-persona/help.md +0 -60
package/dst/ase-kv.js CHANGED
@@ -30,10 +30,10 @@ export class KV {
30
30
  KV.validateKey(key);
31
31
  return KV.store.has(key);
32
32
  }
33
- /* get a value by key; returns undefined if no value is stored */
33
+ /* get a deep copy of a value by key; returns undefined if no value is stored */
34
34
  static get(key) {
35
35
  KV.validateKey(key);
36
- return KV.store.get(key);
36
+ return structuredClone(KV.store.get(key));
37
37
  }
38
38
  /* set a value under the given key; overwrites any existing value */
39
39
  static set(key, val) {
@@ -103,6 +103,17 @@ export class KV {
103
103
  throw new Error(`invalid command "${String(c.command)}"`);
104
104
  }
105
105
  }
106
+ /* extract the message of a caught error */
107
+ const errorMessage = (err) => err instanceof Error ? err.message : String(err);
108
+ /* execute a single key/value command and render it as an MCP tool result */
109
+ const mcpToolExec = (c) => {
110
+ try {
111
+ return { content: [{ type: "text", text: KV.execute(c) }] };
112
+ }
113
+ catch (err) {
114
+ return { isError: true, content: [{ type: "text", text: `ERROR: ${errorMessage(err)}` }] };
115
+ }
116
+ };
106
117
  /* MCP registration entry point for in-memory key/value tools */
107
118
  export class KVMCP {
108
119
  register(mcp) {
@@ -115,16 +126,7 @@ export class KVMCP {
115
126
  key: z.string()
116
127
  .describe("key identifier (non-empty, no whitespace-only, no control characters, up to 1024 characters)")
117
128
  }
118
- }, async (args) => {
119
- try {
120
- const text = KV.execute({ command: "get", key: args.key });
121
- return { content: [{ type: "text", text }] };
122
- }
123
- catch (err) {
124
- const message = err instanceof Error ? err.message : String(err);
125
- return { isError: true, content: [{ type: "text", text: `ERROR: ${message}` }] };
126
- }
127
- });
129
+ }, async (args) => mcpToolExec({ command: "get", key: args.key }));
128
130
  /* key/value set */
129
131
  mcp.registerTool("ase_kv_set", {
130
132
  title: "ASE key/value set",
@@ -137,16 +139,7 @@ export class KVMCP {
137
139
  val: z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(z.unknown()), z.record(z.string(), z.unknown())])
138
140
  .describe("arbitrary JSON-compatible value to store under `key`")
139
141
  }
140
- }, async (args) => {
141
- try {
142
- const text = KV.execute({ command: "set", key: args.key, val: args.val });
143
- return { content: [{ type: "text", text }] };
144
- }
145
- catch (err) {
146
- const message = err instanceof Error ? err.message : String(err);
147
- return { isError: true, content: [{ type: "text", text: `ERROR: ${message}` }] };
148
- }
149
- });
142
+ }, async (args) => mcpToolExec({ command: "set", key: args.key, val: args.val }));
150
143
  /* key/value clear */
151
144
  mcp.registerTool("ase_kv_clear", {
152
145
  title: "ASE key/value clear",
@@ -158,16 +151,7 @@ export class KVMCP {
158
151
  prefix: z.string().optional()
159
152
  .describe("if given, only remove keys starting with this prefix (otherwise remove all keys)")
160
153
  }
161
- }, async (args) => {
162
- try {
163
- const text = KV.execute({ command: "clear", prefix: args.prefix });
164
- return { content: [{ type: "text", text }] };
165
- }
166
- catch (err) {
167
- const message = err instanceof Error ? err.message : String(err);
168
- return { isError: true, content: [{ type: "text", text: `ERROR: ${message}` }] };
169
- }
170
- });
154
+ }, async (args) => mcpToolExec({ command: "clear", prefix: args.prefix }));
171
155
  /* key/value delete */
172
156
  mcp.registerTool("ase_kv_delete", {
173
157
  title: "ASE key/value delete",
@@ -177,16 +161,7 @@ export class KVMCP {
177
161
  key: z.string()
178
162
  .describe("key identifier (non-empty, no whitespace-only, no control characters, up to 1024 characters)")
179
163
  }
180
- }, async (args) => {
181
- try {
182
- const text = KV.execute({ command: "delete", key: args.key });
183
- return { content: [{ type: "text", text }] };
184
- }
185
- catch (err) {
186
- const message = err instanceof Error ? err.message : String(err);
187
- return { isError: true, content: [{ type: "text", text: `ERROR: ${message}` }] };
188
- }
189
- });
164
+ }, async (args) => mcpToolExec({ command: "delete", key: args.key }));
190
165
  /* key/value batch */
191
166
  mcp.registerTool("ase_kv_batch", {
192
167
  title: "ASE key/value batch",
@@ -226,7 +201,7 @@ export class KVMCP {
226
201
  results.push(KV.execute(c));
227
202
  }
228
203
  catch (err) {
229
- const message = err instanceof Error ? err.message : String(err);
204
+ const message = errorMessage(err);
230
205
  if (tx) {
231
206
  if (snapshot !== null)
232
207
  KV.restore(snapshot);
package/dst/ase-mcp.js CHANGED
@@ -23,7 +23,7 @@ export default class MCPCommand {
23
23
  let ctx = this.loadContext();
24
24
  /* fast path: already running */
25
25
  if (ctx.port !== null) {
26
- const match = await probe(ctx.port, ctx.projectId);
26
+ const match = await probe(ctx.port, ctx.projectId).catch(() => null);
27
27
  if (match === true)
28
28
  return { projectId: ctx.projectId, port: ctx.port };
29
29
  }
package/dst/ase-meta.js CHANGED
@@ -6,7 +6,7 @@
6
6
  import path from "node:path";
7
7
  import fs from "node:fs";
8
8
  import { fileURLToPath } from "node:url";
9
- import { writeStdout } from "./ase-stdout.js";
9
+ import { writeStdout } from "./ase-stdio.js";
10
10
  /* reusable functionality: resolve and read plugin "meta/" files */
11
11
  export class Meta {
12
12
  /* determine the plugin "meta/" directory; the build process copies
@@ -22,7 +22,6 @@ import { TaskMCP } from "./ase-task.js";
22
22
  import { MarkdownMCP } from "./ase-markdown.js";
23
23
  import { ArtifactMCP } from "./ase-artifact.js";
24
24
  import { KVMCP } from "./ase-kv.js";
25
- import PersonaMCP from "./ase-persona.js";
26
25
  import { TimestampMCP } from "./ase-timestamp.js";
27
26
  import { GetoptMCP } from "./ase-getopt.js";
28
27
  import { SkillsMCP } from "./ase-skills.js";
@@ -243,7 +242,6 @@ export default class ServiceCommand {
243
242
  new MarkdownMCP().register(mcp);
244
243
  new ArtifactMCP(this.log).register(mcp);
245
244
  new KVMCP().register(mcp);
246
- new PersonaMCP(this.log).register(mcp);
247
245
  new TimestampMCP().register(mcp);
248
246
  new GetoptMCP().register(mcp);
249
247
  new SkillsMCP().register(mcp);
@@ -494,7 +492,7 @@ export default class ServiceCommand {
494
492
  process.stdout.write("service: not running (no port configured)\n");
495
493
  return 1;
496
494
  }
497
- const match = await probe(ctx.port, ctx.projectId);
495
+ const match = await probe(ctx.port, ctx.projectId).catch(() => null);
498
496
  if (match === true) {
499
497
  const r = await ofetch.raw(`http://${HOST}:${ctx.port}/command`, this.commandRequest("status", 2000));
500
498
  const d = r._data;
@@ -513,13 +511,13 @@ export default class ServiceCommand {
513
511
  /* send command: POST /command with the arbitrary cmd token */
514
512
  async doSend(cmd) {
515
513
  let ctx = this.loadContext();
516
- if (ctx.port === null || await probe(ctx.port, ctx.projectId) !== true) {
514
+ if (ctx.port === null || await probe(ctx.port, ctx.projectId).catch(() => null) !== true) {
517
515
  /* auto-start the service once, then re-check */
518
516
  await this.doStart();
519
517
  ctx = this.loadContext();
520
518
  if (ctx.port === null)
521
519
  throw new Error("service not running (no port configured after auto-start)");
522
- if (await probe(ctx.port, ctx.projectId) !== true)
520
+ if (await probe(ctx.port, ctx.projectId).catch(() => null) !== true)
523
521
  throw new Error(`service not responding on port ${ctx.port} after auto-start`);
524
522
  }
525
523
  const r = await ofetch.raw(`http://${HOST}:${ctx.port}/command`, { ...this.commandRequest(cmd, 5000), responseType: "text" });
@@ -536,7 +534,7 @@ export default class ServiceCommand {
536
534
  this.log.write("info", "service: not running (no port configured)");
537
535
  return 0;
538
536
  }
539
- const match = await probe(ctx.port, ctx.projectId);
537
+ const match = await probe(ctx.port, ctx.projectId).catch(() => null);
540
538
  if (match === false) {
541
539
  this.log.write("info", `service: not running (port ${ctx.port} in use by foreign service)`);
542
540
  return 1;
package/dst/ase-setup.js CHANGED
@@ -356,29 +356,13 @@ export default class SetupCommand {
356
356
  args.push(name, transport.url);
357
357
  }
358
358
  }
359
- else if (tool === "copilot") {
360
- /* GitHub Copilot CLI implies the stdio transport when the
361
- command is provided after "--"; only "http"/"sse" servers
362
- need an explicit "--transport" flag and take the URL as a
363
- positional argument */
364
- if (transport.type === "stdio") {
365
- args.push(name);
366
- for (const [key, val] of Object.entries(env))
367
- args.push("--env", `${key}=${val}`);
368
- args.push("--", ...transport.command);
369
- }
370
- else {
371
- args.push("--transport", "http");
372
- for (const [key, val] of Object.entries(transport.headers ?? {}))
373
- args.push("--header", `${key}: ${val}`);
374
- args.push(name, transport.url);
375
- }
376
- }
377
359
  else {
378
- /* OpenAI Codex CLI takes the server name as the first
379
- positional argument and implies the stdio transport when the
380
- command is provided after "--"; "http" servers take the URL
381
- via the "--url" option (with optional "--header" flags) */
360
+ /* the GitHub Copilot CLI and OpenAI Codex CLI both take the
361
+ server name as a positional argument and imply the stdio
362
+ transport when the command is provided after "--"; for "http"
363
+ servers the GitHub Copilot CLI needs an explicit
364
+ "--transport" flag and takes the URL positionally, while the
365
+ OpenAI Codex CLI takes it via the "--url" option */
382
366
  if (transport.type === "stdio") {
383
367
  args.push(name);
384
368
  for (const [key, val] of Object.entries(env))
@@ -386,9 +370,14 @@ export default class SetupCommand {
386
370
  args.push("--", ...transport.command);
387
371
  }
388
372
  else {
373
+ if (tool === "copilot")
374
+ args.push("--transport", "http");
389
375
  for (const [key, val] of Object.entries(transport.headers ?? {}))
390
376
  args.push("--header", `${key}: ${val}`);
391
- args.push(name, "--url", transport.url);
377
+ if (tool === "copilot")
378
+ args.push(name, transport.url);
379
+ else
380
+ args.push(name, "--url", transport.url);
392
381
  }
393
382
  }
394
383
  await this.run(toolSpecs[tool].cli, args);
@@ -586,7 +575,7 @@ export default class SetupCommand {
586
575
  statuslineFormatDflt = [
587
576
  "<blue>%u</blue> <red>%p</red> <black>%T</black> %s",
588
577
  "%m %e %t",
589
- "%P %c"
578
+ "%P %h %c"
590
579
  ];
591
580
  /* resolve the tool settings file for a given installation scope */
592
581
  statuslineSettingsFile(tool, scope) {
@@ -618,8 +607,9 @@ export default class SetupCommand {
618
607
  }
619
608
  /* build the "ase statusline ..." command string from the activate
620
609
  options and the effective format lines */
621
- statuslineCommand(opts, format) {
622
- const parts = ["ase", "statusline", "-w", String(opts.width), "-m", String(opts.margin)];
610
+ statuslineCommand(tool, opts, format) {
611
+ const parts = ["ase", "statusline", "--tool", tool, "-w", String(opts.width), "-m", String(opts.margin),
612
+ "-p", String(opts.padding)];
623
613
  if (!opts.icons)
624
614
  parts.push("--no-icons");
625
615
  if (!opts.labels)
@@ -657,24 +647,18 @@ export default class SetupCommand {
657
647
  AST, reproducing the exact whitespace tokens json-asty emits for a
658
648
  canonically 4-space-indented settings.json */
659
649
  statuslineBuildMember(root, command) {
660
- const strMember = (key, val, epilog) => {
661
- const k = root.create("string").set({ body: JSON.stringify(key), value: key, epilog: ": " });
662
- const v = root.create("string").set({ body: JSON.stringify(val), value: val });
663
- const m = root.create("member");
664
- if (epilog !== undefined)
665
- m.set({ epilog });
666
- return m.add(k, v);
667
- };
668
- const numMember = (key, val, epilog) => {
650
+ const member = (key, val, epilog) => {
651
+ const type = typeof val === "number" ? "number" : "string";
652
+ const body = typeof val === "number" ? String(val) : JSON.stringify(val);
669
653
  const k = root.create("string").set({ body: JSON.stringify(key), value: key, epilog: ": " });
670
- const v = root.create("number").set({ body: String(val), value: val });
654
+ const v = root.create(type).set({ body, value: val });
671
655
  const m = root.create("member");
672
656
  if (epilog !== undefined)
673
657
  m.set({ epilog });
674
658
  return m.add(k, v);
675
659
  };
676
660
  const obj = root.create("object").set({ prolog: "{\n ", epilog: "\n }\n" });
677
- obj.add(strMember("type", "command", ",\n "), strMember("command", command, ",\n "), numMember("padding", 0));
661
+ obj.add(member("type", "command", ",\n "), member("command", command, ",\n "), member("padding", 0));
678
662
  const key = root.create("string").set({
679
663
  body: JSON.stringify("statusLine"),
680
664
  value: "statusLine",
@@ -716,7 +700,7 @@ export default class SetupCommand {
716
700
  this.requireStatuslineTool(tool);
717
701
  this.requireStatuslineScope(tool, scope);
718
702
  const file = this.statuslineSettingsFile(tool, scope);
719
- const command = this.statuslineCommand(opts, format);
703
+ const command = this.statuslineCommand(tool, opts, format);
720
704
  const root = await this.statuslineReadAst(file);
721
705
  const existing = this.statuslineFindMember(root);
722
706
  if (existing !== undefined) {
@@ -940,10 +924,11 @@ export default class SetupCommand {
940
924
  .option("-s, --scope <scope>", "target scope (\"user\", \"project\", or \"local\")", "user")
941
925
  .option("-w, --width <n>", "force terminal width to <n> characters (0 = auto-detect via /dev/tty)", parseNonNeg("width"), 0)
942
926
  .option("-m, --margin <n>", "reduce maximum used terminal width by <n> characters on each side", parseNonNeg("margin"), 2)
927
+ .option("-p, --padding <n>", "pad each rendered line with <n> spaces on each side", parseNonNeg("padding"), 0)
943
928
  .option("--no-icons", "disable icons in placeholder rendering")
944
929
  .option("--no-labels", "disable labels in front of bold values")
945
930
  .action(async (format, opts) => {
946
- process.exit(await this.doStatuslineActivate(this.parseTool(opts.tool), this.parseScope(opts.scope), { width: opts.width, margin: opts.margin, icons: opts.icons, labels: opts.labels }, format ?? []));
931
+ process.exit(await this.doStatuslineActivate(this.parseTool(opts.tool), this.parseScope(opts.scope), { width: opts.width, margin: opts.margin, padding: opts.padding, icons: opts.icons, labels: opts.labels }, format ?? []));
947
932
  });
948
933
  /* register CLI sub-command "ase setup statusline deactivate" */
949
934
  statuslineCmd
package/dst/ase-skills.js CHANGED
@@ -54,7 +54,7 @@ export class Skills {
54
54
  }
55
55
  /* fetch GitHub stars given a repository URL (or empty string) */
56
56
  static async fetchStars(repository) {
57
- const m = /^.+?\/\/github\.com\/([^/]+\/[^/#?]+?)(?:\.git)?(?:[/#?].*)?$/.exec(repository);
57
+ const m = /^(?:.*?:\/\/)?(?:[^@/]*@)?github\.com[:/]([^/]+\/[^/#?]+?)(?:\.git)?(?:[/#?].*)?$/.exec(repository);
58
58
  if (m === null)
59
59
  return "N.A.";
60
60
  try {
@@ -11,7 +11,7 @@ import { InvalidArgumentError } from "commander";
11
11
  import { execaSync } from "execa";
12
12
  import { Chalk } from "chalk";
13
13
  import { Config, configSchema, parseScope } from "./ase-config.js";
14
- import { writeStdout } from "./ase-stdout.js";
14
+ import { readStdin, writeStdout } from "./ase-stdio.js";
15
15
  import pkg from "../package.json" with { type: "json" };
16
16
  /* forced-color chalk instance: stdout is a pipe under Anthropic Claude Code CLI,
17
17
  so chalk auto-detection would yield level 0; force level 1 to keep
@@ -28,13 +28,6 @@ const parseInteger = (name) => (value) => {
28
28
  throw new InvalidArgumentError(`${name} must be a non-negative integer`);
29
29
  return n;
30
30
  };
31
- /* read stdin into a single string */
32
- const readStdin = async () => {
33
- const chunks = [];
34
- for await (const chunk of process.stdin)
35
- chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
36
- return Buffer.concat(chunks).toString("utf8");
37
- };
38
31
  /* detect terminal column width via /dev/tty (stdout is a pipe under Anthropic Claude Code CLI) */
39
32
  const detectTermWidth = () => {
40
33
  let width = 0;
@@ -196,9 +189,10 @@ export default class StatuslineCommand {
196
189
  .option("-t, --tool <tool>", "target tool (\"claude\" or \"copilot\"; \"codex\" is unsupported)", toolDflt)
197
190
  .option("-w, --width <n>", "force terminal width to <n> characters (0 = auto-detect via /dev/tty)", parseInteger("--width"), 0)
198
191
  .option("-m, --margin <n>", "reduce maximum used terminal width by <n> characters on each side", parseInteger("--margin"), 2)
192
+ .option("-p, --padding <n>", "pad each rendered line with <n> spaces on each side", parseInteger("--padding"), 0)
199
193
  .option("--no-icons", "disable icons in placeholder rendering")
200
194
  .option("--no-labels", "disable labels in front of bold values")
201
- .argument("[lines...]", "one or more template lines with %u %p %T %s %m %e %t %O %P %c %C %a %r " +
195
+ .argument("[lines...]", "one or more template lines with %u %p %T %s %m %e %t %O %P %h %c %C %a %r " +
202
196
  "%S %D %W %Q %H %X %b %g %G %d %M %V placeholders and <color>...</color> markup " +
203
197
  "(color: black, red, green, yellow, blue, magenta, cyan, white, default) " +
204
198
  "(default: single line \"%m %e %t\")")
@@ -218,8 +212,7 @@ export default class StatuslineCommand {
218
212
  }
219
213
  catch (err) {
220
214
  const message = err instanceof Error ? err.message : String(err);
221
- this.log.write("error", `statusline: invalid JSON on stdin: ${message}`);
222
- process.exit(1);
215
+ throw new Error(`statusline: invalid JSON on stdin: ${message}`);
223
216
  }
224
217
  /* normalize Copilot CLI's top-level "cwd" into the
225
218
  "workspace.current_dir" structure shared with Anthropic Claude Code CLI */
@@ -228,9 +221,11 @@ export default class StatuslineCommand {
228
221
  && typeof data.cwd === "string" && data.cwd !== "") {
229
222
  data.workspace = { ...(data.workspace ?? {}), current_dir: data.cwd };
230
223
  }
231
- /* determine effective terminal width and budget */
224
+ /* determine effective terminal width and budget
225
+ (the padding spaces occupy terminal columns, too,
226
+ so they have to be subtracted from the budget) */
232
227
  const width = opts.width > 0 ? opts.width : detectTermWidth();
233
- const budget = width > 0 ? width - 2 * opts.margin : 0;
228
+ const budget = width > 0 ? width - 2 * (opts.margin + opts.padding) : 0;
234
229
  /* shared output state and append helper with auto-wrap;
235
230
  the helper itself strips ANSI CSI escape sequences to
236
231
  measure the raw visible width of the chunk */
@@ -272,20 +267,24 @@ export default class StatuslineCommand {
272
267
  const getCfg = memoize(() => {
273
268
  let taskId = process.env.ASE_TASK_ID ?? "";
274
269
  let persona = process.env.ASE_PERSONA_STYLE ?? "";
270
+ let guidance = process.env.ASE_GUIDANCE_LEVEL ?? "";
275
271
  try {
276
272
  const cfg = new Config("config", configSchema, this.log, parseScope(`session:${getSession()}`));
277
273
  cfg.read("lenient");
278
274
  const t = String(cfg.get("agent.task") ?? "").trim();
279
275
  const p = String(cfg.get("agent.persona") ?? "").trim();
276
+ const g = String(cfg.get("agent.guidance") ?? "").trim();
280
277
  if (t !== "")
281
278
  taskId = t;
282
279
  if (p !== "")
283
280
  persona = p;
281
+ if (g !== "")
282
+ guidance = g;
284
283
  }
285
284
  catch (_e) {
286
285
  /* cascade unavailable; keep env-var fallbacks */
287
286
  }
288
- return { taskId, persona };
287
+ return { taskId, persona, guidance };
289
288
  });
290
289
  const getGit = memoize(() => probeGit(data.workspace?.current_dir ?? ""));
291
290
  const getMem = memoize(() => probeMemory());
@@ -331,6 +330,11 @@ export default class StatuslineCommand {
331
330
  if (persona !== "")
332
331
  emit(`${prefix("☯", "persona")}${c.bold(persona)}`);
333
332
  },
333
+ h: () => {
334
+ const { guidance } = getCfg();
335
+ if (guidance !== "")
336
+ emit(`${prefix("▶", "guidance")}${c.bold(guidance)}`);
337
+ },
334
338
  /* ==== CONTEXT ==== */
335
339
  c: () => {
336
340
  const pct = Math.min(100, Math.max(0, Math.floor(data.context_window?.used_percentage ?? 0)));
@@ -340,10 +344,18 @@ export default class StatuslineCommand {
340
344
  emit(`${prefix("◔", "context")}${bar} ${pct}%`);
341
345
  },
342
346
  C: () => {
343
- const pct = data.context_window?.used_percentage ?? 0;
344
- const tokensCur = (data.context_window?.total_input_tokens ?? 0) +
345
- (data.context_window?.total_output_tokens ?? 0);
346
- const tokensLim = pct > 0 && tokensCur > 0 ? Math.round(tokensCur * 100 / pct) : 0;
347
+ /* probe the live-context fields first and fall back to the
348
+ cumulative ones, so each harness contributes its own view:
349
+ GitHub Copilot CLI reports the live window separately (its
350
+ "total_*" are session sums), whereas Anthropic Claude Code CLI
351
+ folds it into "total_input_tokens". Output tokens stay out --
352
+ "used_percentage" is input-side only. */
353
+ const cw = data.context_window;
354
+ const pct = cw?.used_percentage ?? 0;
355
+ const tokensCur = cw?.current_context_tokens ??
356
+ cw?.last_call_input_tokens ?? cw?.total_input_tokens ?? 0;
357
+ const tokensLim = cw?.displayed_context_limit ?? cw?.context_window_size ??
358
+ (pct > 0 && tokensCur > 0 ? Math.round(tokensCur * 100 / pct) : 0);
347
359
  if (tokensLim > 0)
348
360
  emit(`${prefix("◆", "tokens")}${c.bold(formatTokens(tokensCur) + "/" + formatTokens(tokensLim))}`);
349
361
  },
@@ -462,6 +474,18 @@ export default class StatuslineCommand {
462
474
  out += "\n";
463
475
  col = 0;
464
476
  }
477
+ /* optionally pad all non-empty lines with spaces on both sides
478
+ (the trailing empty chunk after the final line break and any
479
+ intentionally empty line are left untouched, so that no
480
+ whitespace-only line is produced) */
481
+ if (opts.padding > 0) {
482
+ const pad = " ".repeat(opts.padding);
483
+ out = out.split("\n").map((line) => line !== "" ? `${pad}${line}${pad}` : line).join("\n");
484
+ /* for Copilot, prefix with a zero-width non-breaking space
485
+ to prevent the first line to be unindented */
486
+ if (tool === "copilot")
487
+ out = "\u200B" + out;
488
+ }
465
489
  /* send output */
466
490
  await writeStdout(out);
467
491
  /* optionally publish task id to the calling tmux pane as a per-pane user
@@ -0,0 +1,25 @@
1
+ /*
2
+ ** Agentic Software Engineering (ASE)
3
+ ** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
+ ** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
5
+ */
6
+ import getStdin from "get-stdin";
7
+ /* read the entire stdin payload as a UTF-8 string, treating an
8
+ interactive terminal as empty input so callers never block on a
9
+ user which is not going to type anything */
10
+ export const readStdin = () => {
11
+ return getStdin();
12
+ };
13
+ /* write a string to stdout, awaiting the write callback which only
14
+ fires once the data has been flushed to the underlying pipe, so a
15
+ subsequent "process.exit" cannot truncate the output */
16
+ export const writeStdout = (text) => {
17
+ return new Promise((resolve, reject) => {
18
+ process.stdout.write(text, (err) => {
19
+ if (err)
20
+ reject(err);
21
+ else
22
+ resolve();
23
+ });
24
+ });
25
+ };
package/dst/ase-task.js CHANGED
@@ -13,7 +13,7 @@ import { z } from "zod";
13
13
  import { LRUCache } from "lru-cache";
14
14
  import { Config, configSchema, parseScope } from "./ase-config.js";
15
15
  import { Markdown } from "./ase-markdown.js";
16
- import { writeStdout } from "./ase-stdout.js";
16
+ import { readStdin, writeStdout } from "./ase-stdio.js";
17
17
  /* reusable functionality: persisted task plans under
18
18
  <project>/<basedir>/TASK-<id>.md (driven by the
19
19
  "project.artifact.task.{basedir,files}" configuration) */
@@ -56,11 +56,18 @@ export class Task {
56
56
  Task.projectRootCache.set(cwd, root);
57
57
  return root;
58
58
  }
59
+ /* cached task storage specification (TTL-bounded, mirroring the project
60
+ root cache, as each read parses the whole layered YAML config chain) */
61
+ static specCache = new LRUCache({ max: 4, ttl: 2 * 1000 });
59
62
  /* read the configured "basedir" anchor and "files" miniglob spec for
60
63
  task storage; "basedir" is project-root-relative (POSIX, defaults
61
64
  to ".ase/task") and "files" constrains the task filenames
62
65
  (defaults to "*.md") */
63
66
  static spec(log) {
67
+ const root = Task.projectRoot();
68
+ const cached = Task.specCache.get(root);
69
+ if (cached !== undefined)
70
+ return cached;
64
71
  const cfg = new Config("config", configSchema, log);
65
72
  cfg.read();
66
73
  const read = (key) => {
@@ -74,7 +81,9 @@ export class Task {
74
81
  if (basedir.split("/").includes(".."))
75
82
  throw new Error(`task: configured "basedir" "${basedir}" must not escape the project root`);
76
83
  const files = read("project.artifact.task.files") || "*.md";
77
- return { basedir, files };
84
+ const result = { basedir, files };
85
+ Task.specCache.set(root, result);
86
+ return result;
78
87
  }
79
88
  /* resolve the on-disk base directory for task storage */
80
89
  static baseDir(log) {
@@ -245,15 +254,6 @@ export class Task {
245
254
  });
246
255
  }
247
256
  }
248
- /* read all of stdin as a UTF-8 string */
249
- const readStdin = () => {
250
- return new Promise((resolve, reject) => {
251
- const chunks = [];
252
- process.stdin.on("data", (chunk) => chunks.push(chunk));
253
- process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
254
- process.stdin.on("error", (err) => reject(err));
255
- });
256
- };
257
257
  /* CLI command "ase task" */
258
258
  export default class TaskCommand {
259
259
  log;
package/dst/ase.js CHANGED
@@ -60,7 +60,7 @@ const main = async () => {
60
60
  await program.parseAsync(process.argv);
61
61
  /* gracefully terminate */
62
62
  await log.close();
63
- process.exit(0);
63
+ process.exit(process.exitCode ?? 0);
64
64
  };
65
65
  main().catch(async (err) => {
66
66
  if (err instanceof CommanderError) {
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.47",
9
+ "version": "0.9.49",
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.47",
3
+ "version": "0.9.49",
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.47",
3
+ "version": "0.9.49",
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.47",
3
+ "version": "0.9.49",
4
4
  "description": "Agentic Software Engineering (ASE)",
5
5
  "keywords": [ "agentic", "software", "engineering" ],
6
6
  "homepage": "https://ase.tools",
@@ -45,6 +45,15 @@ Task Skill Common Steps
45
45
  <template>
46
46
  ⧉ **ASE**: ☻ skill: **<arg1/>**, ▶ ERROR: expected single `[<id>]` argument
47
47
  </template>
48
+
49
+ Directly *after* this error <template/>, and *before* stopping,
50
+ give the corrective hint by expanding the following (which,
51
+ depending on the configured <ase-guidance-level/>, may expand into
52
+ nothing and hence emit no output at all):
53
+
54
+ <ase-tpl-hint level="verbose">
55
+ Run `/<arg1/> --help` for the accepted arguments of this skill, or use `/ase-help-intent` to have a fitting command proposed for a free-text intent.
56
+ </ase-tpl-hint>
48
57
  </elseif>
49
58
 
50
59
  </define>
@@ -6,17 +6,28 @@ You are an expert-level AI coding assistant.
6
6
  You have the **Agentic Software Engineering (ASE)** companion toolkit enabled,
7
7
  which boosts you to an expert-level Software Engineering AI agent.
8
8
 
9
- If ((<ase-agent-tool/> is equal to `codex`) *AND* (<ase-headless/> is empty
10
- or not set)) you *MUST* output the following <template/> *exactly once*
11
- as the very *first* thing in your *first* response of this session --
12
- *before* any other text, *before* any tool call, and *before* entering
13
- any skill flow:
9
+ If ((<ase-agent-tool/> is equal to `copilot`) *AND* (<ase-headless/> is empty
10
+ or not set) *AND* (<ase-guidance-level/> is not equal to `none`)) you *MUST* output
11
+ the following <template/> *exactly once* as the very *first* thing in
12
+ your *first* response of this session -- *before* any other text,
13
+ *before* any tool call, and *before* entering any skill flow:
14
14
 
15
15
  <template>
16
16
  ⧉ **ASE**: ⎈ version: **<ase-version/>** <ase-version-hint/>
17
17
  ⧉ **ASE**: ※ user: **<ase-user-id/>**, ⚑ project: **<ase-project-id/>**
18
18
  ⧉ **ASE**: ◉ task: **<ase-task-id/>**, ⏻ session: **<ase-session-id/>**
19
- ⧉ **ASE**: ☯ persona: **<ase-persona-style/>**, ▢ boxing: **<ase-project-boxing/>**
19
+ ⧉ **ASE**: ☯ persona: **<ase-persona-style/>**, ▶ guidance: **<ase-guidance-level/>**, ▢ boxing: **<ase-project-boxing/>**
20
+ </template>
21
+
22
+ If ((<ase-agent-tool/> is equal to `copilot`) *AND* (<ase-headless/> is
23
+ empty or not set) *AND* (<ase-guidance-level/> is equal to `normal` or
24
+ `verbose`)) you *MUST* output the following <template/> *exactly once*
25
+ as the very *second* thing in your *first* response of this session,
26
+ too:
27
+
28
+ <template>
29
+ ⧉ **ASE**: ▷ hint: use "/ase-help-intent *intent-description*" for skill command proposal
30
+ ⧉ **ASE**: ▷ hint: use "/ase-help-skill [*skill-name*]" for skill catalog or skill manpage
20
31
  </template>
21
32
 
22
33
  Prohibitions