@rse/ase 0.9.31 → 0.9.33

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.
@@ -10,6 +10,7 @@ import { isScalar } from "yaml";
10
10
  import { z } from "zod";
11
11
  import { Config, configSchema } from "./ase-config.js";
12
12
  import { Task } from "./ase-task.js";
13
+ import { writeStdout } from "./ase-stdout.js";
13
14
  /* the recognized artifact kinds, in descending precedence order;
14
15
  "othr" is the implicit catch-all and is always resolved last */
15
16
  export const artifactKinds = ["spec", "arch", "code", "docs", "infr", "othr"];
@@ -223,17 +224,16 @@ export default class ArtifactCommand {
223
224
  .description("Resolve one or more artifact kinds to project-relative file paths")
224
225
  .option("--kind <kinds>", "comma-separated list of artifact kinds " +
225
226
  `(${artifactKinds.join("|")})`, artifactKinds.join(","))
226
- .action((opts) => {
227
+ .action(async (opts) => {
227
228
  const kinds = opts.kind.split(",").map((k) => Artifact.validateKind(k.trim()));
228
229
  const result = Artifact.list(this.log, kinds);
229
230
  const single = result.length === 1;
230
231
  for (const { kind, files } of result) {
231
232
  if (!single)
232
- process.stdout.write(`# ${kind}:\n`);
233
+ await writeStdout(`# ${kind}:\n`);
233
234
  for (const file of files)
234
- process.stdout.write(`- ${file}\n`);
235
+ await writeStdout(`- ${file}\n`);
235
236
  }
236
- process.exit(0);
237
237
  });
238
238
  /* register CLI sub-command "ase artifact name" */
239
239
  artifact
@@ -242,10 +242,9 @@ export default class ArtifactCommand {
242
242
  .argument("<filename>", "base-relative filename within the kind's basedir")
243
243
  .option("--kind <kind>", "artifact kind " +
244
244
  `(${configuredKinds.join("|")})`, "code")
245
- .action((filename, opts) => {
245
+ .action(async (filename, opts) => {
246
246
  const kind = Artifact.validateKind(opts.kind.trim());
247
- process.stdout.write(`${Artifact.name(this.log, kind, filename)}\n`);
248
- process.exit(0);
247
+ await writeStdout(`${Artifact.name(this.log, kind, filename)}\n`);
249
248
  });
250
249
  }
251
250
  }
package/dst/ase-hook.js CHANGED
@@ -12,6 +12,7 @@ import getStdin from "get-stdin";
12
12
  import * as v from "valibot";
13
13
  import Version from "./ase-version.js";
14
14
  import { Config, configSchema, parseScope } from "./ase-config.js";
15
+ import { writeStdout } from "./ase-stdout.js";
15
16
  const addonMcpServers = [
16
17
  "chat-alibaba-qwen",
17
18
  "chat-deepseek",
@@ -24,8 +25,8 @@ const addonMcpServers = [
24
25
  "search-perplexity"
25
26
  ];
26
27
  /* build a per-tool regular expression matching the tool names exposed
27
- by the addon MCP servers: Claude Code prefixes them as
28
- "mcp__<server>__<tool>", whereas Copilot CLI prefixes them as
28
+ by the addon MCP servers: Anthropic Claude Code CLI prefixes them as
29
+ "mcp__<server>__<tool>", whereas GitHub Copilot CLI prefixes them as
29
30
  "<server>-<tool>" */
30
31
  const addonMcpToolNamePattern = (prefix, suffix) => {
31
32
  const alternatives = addonMcpServers
@@ -87,18 +88,6 @@ export default class HookCommand {
87
88
  async drainStdin() {
88
89
  await this.readStdin();
89
90
  }
90
- /* write to stdout and resolve only once it has been fully
91
- flushed to the underlying file descriptor */
92
- writeStdout(text) {
93
- return new Promise((resolve, reject) => {
94
- process.stdout.write(text, (err) => {
95
- if (err)
96
- reject(err);
97
- else
98
- resolve();
99
- });
100
- });
101
- }
102
91
  /* best-effort JSON parse with valibot schema validation: returns
103
92
  an empty object on blank input, malformed JSON, or schema
104
93
  mismatch, so callers can treat the result uniformly. Extra
@@ -145,9 +134,9 @@ export default class HookCommand {
145
134
  return this.expandReferences(content, path.dirname(abs), next);
146
135
  });
147
136
  }
148
- /* handler for "ase hook session-start" (all tools) */
149
- async doSessionStart(tool) {
150
- /* determine plugin root (env var name differs per tool) */
137
+ /* determine the plugin root directory (the environment variable
138
+ carrying it differs per tool), throwing if it cannot be found */
139
+ pluginRoot(tool) {
151
140
  let pluginRootVars;
152
141
  if (tool === "copilot")
153
142
  pluginRootVars = ["COPILOT_PLUGIN_ROOT"];
@@ -163,6 +152,23 @@ export default class HookCommand {
163
152
  }
164
153
  if (pluginRoot === "")
165
154
  throw new Error(`${pluginRootVars.join("/")} environment variable is not set`);
155
+ return pluginRoot;
156
+ }
157
+ /* determine the plugin root directory like "pluginRoot", but return
158
+ an empty string instead of throwing, so callers on the hot path of
159
+ a tool-approval decision can silently decline to auto-approve */
160
+ pluginRootSafe(tool) {
161
+ try {
162
+ return this.pluginRoot(tool);
163
+ }
164
+ catch (_e) {
165
+ return "";
166
+ }
167
+ }
168
+ /* handler for "ase hook session-start" (all tools) */
169
+ async doSessionStart(tool) {
170
+ /* determine plugin root (env var name differs per tool) */
171
+ const pluginRoot = this.pluginRoot(tool);
166
172
  /* determine path to external files */
167
173
  const filePkg = path.join(pluginRoot, ".claude-plugin", "plugin.json");
168
174
  const fileMd = path.join(pluginRoot, "meta", "ase-constitution.md");
@@ -196,8 +202,8 @@ export default class HookCommand {
196
202
  if (process.env.ASE_SETUP_DEV !== undefined)
197
203
  versionHints.push("**NOTICE:** *development* setup");
198
204
  const versionHint = versionHints.length > 0 ? "(" + versionHints.join(", ") + ")" : "";
199
- /* read session information (Claude Code uses snake_case fields,
200
- Copilot CLI uses camelCase fields) */
205
+ /* read session information (Anthropic Claude Code CLI uses snake_case fields,
206
+ GitHub Copilot CLI uses camelCase fields) */
201
207
  const stdin = await this.readStdin();
202
208
  const input = this.parseJSON(stdin, v.object({
203
209
  session_id: v.optional(v.string()),
@@ -246,11 +252,12 @@ export default class HookCommand {
246
252
  persona = val;
247
253
  /* determine headless mode */
248
254
  const headless = (process.env.ASE_HEADLESS ?? "false") === "true" ? "true" : "false";
249
- /* provide ASE information to Claude Code shell commands
250
- (Claude Code only -- Copilot CLI has no equivalent mechanism) */
255
+ /* provide ASE information to Anthropic Claude Code CLI shell commands
256
+ (Anthropic Claude Code CLI only -- GitHub Copilot CLI has no equivalent mechanism) */
251
257
  const envFile = tool === "claude" ? (process.env.CLAUDE_ENV_FILE ?? "") : "";
252
258
  if (envFile !== "") {
253
259
  const script = `export ASE_VERSION=${quote([versionCurrentPlugin])}\n` +
260
+ `export ASE_PLUGIN_ROOT=${quote([pluginRoot])}\n` +
254
261
  `export ASE_USER_ID=${quote([userId])}\n` +
255
262
  `export ASE_PROJECT_ID=${quote([projectId])}\n` +
256
263
  `export ASE_TASK_ID=${quote([taskId])}\n` +
@@ -263,6 +270,7 @@ export default class HookCommand {
263
270
  md =
264
271
  `<ase-version>${versionCurrentPlugin}</ase-version>\n` +
265
272
  `<ase-version-hint>${versionHint}</ase-version-hint>\n` +
273
+ `<ase-plugin-root>${pluginRoot}</ase-plugin-root>\n` +
266
274
  `<ase-persona-style>${persona}</ase-persona-style>\n` +
267
275
  `<ase-user-id>${userId}</ase-user-id>\n` +
268
276
  `<ase-project-id>${projectId}</ase-project-id>\n` +
@@ -276,15 +284,15 @@ export default class HookCommand {
276
284
  /* build the deterministic ASE banner (rendered directly by the
277
285
  agent harness, independent of any model decision, so it is
278
286
  guaranteed to appear once in every non-headless session;
279
- Claude Code and OpenAI Codex CLI surface a top-level
280
- "systemMessage" field for this -- Copilot CLI has no equivalent) */
287
+ Anthropic Claude Code CLI and OpenAI Codex CLI surface a top-level
288
+ "systemMessage" field for this -- GitHub Copilot CLI has no equivalent) */
281
289
  const banner = `\n⧉ ASE: ⎈ version: ${versionCurrentPlugin}${versionHint !== "" ? " " + versionHint.replaceAll(/\*/g, "") : ""}` +
282
290
  `\n⧉ ASE: ※ user: ${userId}, ⚑ project: ${projectId}` +
283
291
  `\n⧉ ASE: ◉ task: ${taskId}, ⏻ session: ${sessionId}` +
284
292
  `\n⧉ ASE: ☯ persona: ${persona}`;
285
293
  /* inject markdown into session context.
286
- Claude Code and OpenAI Codex CLI expect the context nested in
287
- "hookSpecificOutput"; Copilot CLI expects a flat top-level
294
+ Anthropic Claude Code CLI and OpenAI Codex CLI expect the context nested in
295
+ "hookSpecificOutput"; GitHub Copilot CLI expects a flat top-level
288
296
  "additionalContext" field. */
289
297
  const payload = tool !== "copilot" ? {
290
298
  "hookSpecificOutput": {
@@ -299,14 +307,14 @@ export default class HookCommand {
299
307
  running headless -- mirroring the constitution box condition) */
300
308
  if ((tool === "claude" || tool === "codex") && headless !== "true")
301
309
  payload.systemMessage = banner;
302
- await this.writeStdout(JSON.stringify(payload));
310
+ await writeStdout(JSON.stringify(payload));
303
311
  return 0;
304
312
  }
305
313
  /* publish the agent activity marker to tmux as a per-pane user
306
314
  option, so tmux can render the live state via
307
315
  #{@ase_agent_status} (refreshed on tmux's own interval,
308
- independent of Claude Code's statusline repaint cadence).
309
- Notice: the Claude Code statusline is not usable for this case
316
+ independent of Anthropic Claude Code CLI's statusline repaint cadence).
317
+ Notice: the Anthropic Claude Code CLI statusline is not usable for this case
310
318
  at all, as it is not repainted during agent processing! */
311
319
  writeAgentStatus(status) {
312
320
  const icon = status === "busy" ? "▶" : "⏸";
@@ -346,8 +354,8 @@ export default class HookCommand {
346
354
  }
347
355
  return 0;
348
356
  }
349
- /* pick the session id from a parsed payload (Claude Code uses
350
- snake_case fields, Copilot CLI uses camelCase fields) */
357
+ /* pick the session id from a parsed payload (Anthropic Claude Code CLI uses
358
+ snake_case fields, GitHub Copilot CLI uses camelCase fields) */
351
359
  pickSessionId(input) {
352
360
  return input.session_id ?? input.sessionId ?? "";
353
361
  }
@@ -379,6 +387,23 @@ export default class HookCommand {
379
387
  return "";
380
388
  }
381
389
  }
390
+ /* determine whether a "Read" target "filePath" resolves to a
391
+ location inside the plugin root, so the recurring loads of ASE
392
+ skill include files (e.g. "@${CLAUDE_SKILL_DIR}/../../meta/...")
393
+ can be auto-approved instead of prompting the user on every skill
394
+ invocation. The path is normalized and compared with a trailing
395
+ separator to prevent a sibling directory sharing the root's name
396
+ prefix (or a "../" escape) from being mistaken for a descendant. */
397
+ isUnderPluginRoot(tool, filePath) {
398
+ if (filePath === "")
399
+ return false;
400
+ const pluginRoot = this.pluginRootSafe(tool);
401
+ if (pluginRoot === "")
402
+ return false;
403
+ const root = path.resolve(pluginRoot) + path.sep;
404
+ const abs = path.resolve(filePath);
405
+ return (abs + path.sep).startsWith(root);
406
+ }
382
407
  /* the edit-capable skills whose active state lets the pre-tool-use
383
408
  hook auto-approve subsequent "Edit" invocations */
384
409
  editCapableSkills = ["ase-code-lint", "ase-docs-proofread"];
@@ -387,7 +412,7 @@ export default class HookCommand {
387
412
  reason. The input field names and value shapes differ between
388
413
  tools, but the decision logic is shared by the "pre-tool-use" and
389
414
  "permission-request" handlers. */
390
- decideApproval(spec, input) {
415
+ decideApproval(tool, spec, input) {
391
416
  const toolName = typeof input[spec.toolNameField] === "string" ?
392
417
  input[spec.toolNameField] : "";
393
418
  let toolInput = {};
@@ -395,7 +420,8 @@ export default class HookCommand {
395
420
  if (spec.toolInputIsString && typeof rawInput === "string")
396
421
  toolInput = this.parseJSON(rawInput, v.object({
397
422
  command: v.optional(v.string()),
398
- skill: v.optional(v.string())
423
+ skill: v.optional(v.string()),
424
+ file_path: v.optional(v.string())
399
425
  }));
400
426
  else if (!spec.toolInputIsString && typeof rawInput === "object" && rawInput !== null)
401
427
  toolInput = rawInput;
@@ -407,6 +433,8 @@ export default class HookCommand {
407
433
  return { approve: true, reason: "ASE MCP tool invocation auto-approved" };
408
434
  else if (spec.addonMcpToolNamePattern.test(toolName))
409
435
  return { approve: true, reason: "ASE addon MCP tool invocation auto-approved" };
436
+ else if (toolName === "Read" && this.isUnderPluginRoot(tool, toolInput.file_path ?? ""))
437
+ return { approve: true, reason: "ASE plugin file read auto-approved" };
410
438
  else if (toolName === "Edit") {
411
439
  const sessionId = this.pickSessionId(input);
412
440
  const activeSkill = this.readActiveSkill(sessionId);
@@ -416,7 +444,7 @@ export default class HookCommand {
416
444
  return { approve: false, reason: "" };
417
445
  }
418
446
  /* handler for "ase hook pre-tool-use" (all tools).
419
- For Claude Code and Copilot CLI this is where ASE tool
447
+ For Anthropic Claude Code CLI and GitHub Copilot CLI this is where ASE tool
420
448
  invocations are auto-approved (via "permissionDecision: allow").
421
449
  OpenAI Codex CLI rejects that mechanism in "PreToolUse", so for
422
450
  Codex this handler stays silent and approval is granted in the
@@ -435,10 +463,10 @@ export default class HookCommand {
435
463
  if (spec.approvalEvent !== "PreToolUse")
436
464
  return 0;
437
465
  /* determine whether to auto-approve the tool invocation */
438
- const { approve, reason } = this.decideApproval(spec, input);
466
+ const { approve, reason } = this.decideApproval(tool, spec, input);
439
467
  /* emit permission decision (or stay silent to defer to default flow).
440
- Claude Code expects the decision nested in "hookSpecificOutput";
441
- Copilot CLI expects flat top-level fields. */
468
+ Anthropic Claude Code CLI expects the decision nested in "hookSpecificOutput";
469
+ GitHub Copilot CLI expects flat top-level fields. */
442
470
  if (approve) {
443
471
  const payload = spec.preToolUseWrapped ? {
444
472
  "hookSpecificOutput": {
@@ -450,7 +478,7 @@ export default class HookCommand {
450
478
  "permissionDecision": "allow",
451
479
  "permissionDecisionReason": reason
452
480
  };
453
- await this.writeStdout(JSON.stringify(payload));
481
+ await writeStdout(JSON.stringify(payload));
454
482
  }
455
483
  return 0;
456
484
  }
@@ -469,7 +497,7 @@ export default class HookCommand {
469
497
  sessionId: v.optional(v.string())
470
498
  }));
471
499
  /* determine whether to auto-approve the tool invocation */
472
- const { approve } = this.decideApproval(spec, input);
500
+ const { approve } = this.decideApproval(tool, spec, input);
473
501
  /* emit the Codex "PermissionRequest" approval decision */
474
502
  if (approve) {
475
503
  const payload = {
@@ -478,7 +506,7 @@ export default class HookCommand {
478
506
  "decision": { "behavior": "allow" }
479
507
  }
480
508
  };
481
- await this.writeStdout(JSON.stringify(payload));
509
+ await writeStdout(JSON.stringify(payload));
482
510
  }
483
511
  return 0;
484
512
  }
@@ -496,7 +524,7 @@ export default class HookCommand {
496
524
  /* register CLI top-level command "ase hook" */
497
525
  const hookCmd = program
498
526
  .command("hook")
499
- .description("Claude Code and Copilot CLI hook entry points")
527
+ .description("Anthropic Claude Code CLI and GitHub Copilot CLI hook entry points")
500
528
  .action(() => {
501
529
  hookCmd.outputHelp();
502
530
  process.exit(1);
package/dst/ase-kv.js CHANGED
@@ -7,7 +7,7 @@ import { z } from "zod";
7
7
  /* reusable functionality: in-memory key/value store living inside the
8
8
  "ase service" process; per-project (one service per project) and
9
9
  not persisted; intended for sharing information between skills
10
- across multiple Claude Code instances connected to the same service */
10
+ across multiple Anthropic Claude Code CLI instances connected to the same service */
11
11
  export class KV {
12
12
  /* the actual in-memory store */
13
13
  static store = new Map();
@@ -0,0 +1,65 @@
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 path from "node:path";
7
+ import fs from "node:fs";
8
+ import { fileURLToPath } from "node:url";
9
+ import { writeStdout } from "./ase-stdout.js";
10
+ /* reusable functionality: resolve and read plugin "meta/" files */
11
+ export class Meta {
12
+ /* determine the plugin "meta/" directory; the build process copies
13
+ the sibling "plugin/" tree into the tool package (see the "copy
14
+ plugin directory into package for bundling" build step), so a
15
+ bundled "<pkgdir>/plugin/meta/" is always available right next to
16
+ the compiled tool, independent of any environment variables */
17
+ static dir() {
18
+ const pkgdir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
19
+ return path.join(pkgdir, "plugin", "meta");
20
+ }
21
+ /* resolve a single requested meta name to an absolute path within
22
+ the "meta/" directory; the name is reduced to its basename to
23
+ reject path traversal and absolute paths, then expanded into the
24
+ canonical "ase-<name>.md" file name (the "ase-" prefix and ".md"
25
+ extension are added when not already present) */
26
+ static resolve(name) {
27
+ let base = path.basename(name.replace(/\\/g, "/"));
28
+ if (base === "" || base === "." || base === "..")
29
+ throw new Error(`meta: invalid file name "${name}"`);
30
+ if (path.extname(base) === "")
31
+ base = `${base}.md`;
32
+ if (!base.startsWith("ase-"))
33
+ base = `ase-${base}`;
34
+ return path.join(Meta.dir(), base);
35
+ }
36
+ /* read the contents of a single requested meta file */
37
+ static read(name) {
38
+ const abs = Meta.resolve(name);
39
+ try {
40
+ return fs.readFileSync(abs, "utf8");
41
+ }
42
+ catch (_e) {
43
+ throw new Error(`meta: failed to read file: ${abs}`);
44
+ }
45
+ }
46
+ }
47
+ /* CLI command "ase meta" */
48
+ export default class MetaCommand {
49
+ log;
50
+ constructor(log) {
51
+ this.log = log;
52
+ }
53
+ /* register command */
54
+ register(program) {
55
+ program
56
+ .command("meta")
57
+ .description("Output the contents of one or more plugin meta files")
58
+ .argument("<name...>", "meta file name(s) (\"ase-\" prefix and \".md\" extension optional)")
59
+ .action(async (names) => {
60
+ this.log.write("debug", `meta: reading ${names.length} file(s)`);
61
+ for (const name of names)
62
+ await writeStdout(Meta.read(name));
63
+ });
64
+ }
65
+ }
package/dst/ase-setup.js CHANGED
@@ -13,8 +13,8 @@ import Table from "cli-table3";
13
13
  import chalk from "chalk";
14
14
  import Version from "./ase-version.js";
15
15
  const toolSpecs = {
16
- "claude": { cli: "claude", label: "Claude Code", pInstall: "install", pRemove: "uninstall", pUpdate: "update" },
17
- "copilot": { cli: "copilot", label: "Copilot CLI", pInstall: "install", pRemove: "uninstall", pUpdate: "update" },
16
+ "claude": { cli: "claude", label: "Anthropic Claude Code CLI", pInstall: "install", pRemove: "uninstall", pUpdate: "update" },
17
+ "copilot": { cli: "copilot", label: "GitHub Copilot CLI", pInstall: "install", pRemove: "uninstall", pUpdate: "update" },
18
18
  "codex": { cli: "codex", label: "OpenAI Codex CLI", pInstall: "add", pRemove: "remove", pUpdate: "upgrade" }
19
19
  };
20
20
  /* CLI command "ase setup" */
@@ -361,7 +361,7 @@ export default class SetupCommand {
361
361
  /* register an MCP server with the tool, supporting both the "stdio"
362
362
  (a local subprocess command) and "http" (a remote URL, optionally
363
363
  with HTTP headers) transports; the per-tool command line differs
364
- between Claude Code, GitHub Copilot CLI, and OpenAI Codex CLI */
364
+ between Anthropic Claude Code CLI, GitHub Copilot CLI, and OpenAI Codex CLI */
365
365
  async mcpAdd(tool, name, env, transport) {
366
366
  const args = ["mcp", "add"];
367
367
  if (tool === "claude") {
@@ -416,7 +416,7 @@ export default class SetupCommand {
416
416
  await this.run(toolSpecs[tool].cli, args);
417
417
  }
418
418
  /* unregister an MCP server from the tool; the per-tool command line
419
- differs between Claude Code, GitHub Copilot CLI, and OpenAI Codex CLI */
419
+ differs between Anthropic Claude Code CLI, GitHub Copilot CLI, and OpenAI Codex CLI */
420
420
  async mcpRemove(tool, name) {
421
421
  const args = tool === "claude" ?
422
422
  ["mcp", "remove", "--scope", "user", name] :
@@ -12,7 +12,7 @@ import { execaSync } from "execa";
12
12
  import { Chalk } from "chalk";
13
13
  import { Config, configSchema, parseScope } from "./ase-config.js";
14
14
  import pkg from "../package.json" with { type: "json" };
15
- /* forced-color chalk instance: stdout is a pipe under Claude Code,
15
+ /* forced-color chalk instance: stdout is a pipe under Anthropic Claude Code CLI,
16
16
  so chalk auto-detection would yield level 0; force level 1 to keep
17
17
  emitting ANSI sequences as the original implementation did */
18
18
  const c = new Chalk({ level: 1 });
@@ -34,7 +34,7 @@ const readStdin = async () => {
34
34
  chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
35
35
  return Buffer.concat(chunks).toString("utf8");
36
36
  };
37
- /* detect terminal column width via /dev/tty (stdout is a pipe under Claude Code) */
37
+ /* detect terminal column width via /dev/tty (stdout is a pipe under Anthropic Claude Code CLI) */
38
38
  const detectTermWidth = () => {
39
39
  let width = 0;
40
40
  let tty = null;
@@ -195,7 +195,7 @@ export default class StatuslineCommand {
195
195
  const toolDflt = envTool !== "" ? this.parseTool(envTool) : "claude";
196
196
  program
197
197
  .command("statusline")
198
- .description("Render Claude Code or GitHub Copilot CLI statusline from stdin JSON")
198
+ .description("Render Anthropic Claude Code CLI or GitHub Copilot CLI statusline from stdin JSON")
199
199
  .option("-t, --tool <tool>", "target tool (\"claude\" or \"copilot\"; \"codex\" is unsupported)", toolDflt)
200
200
  .option("-w, --width <n>", "force terminal width to <n> characters (0 = auto-detect via /dev/tty)", parseInteger("--width"), 0)
201
201
  .option("-m, --margin <n>", "reduce maximum used terminal width by <n> characters on each side", parseInteger("--margin"), 2)
@@ -225,7 +225,7 @@ export default class StatuslineCommand {
225
225
  process.exit(1);
226
226
  }
227
227
  /* normalize Copilot CLI's top-level "cwd" into the
228
- "workspace.current_dir" structure shared with Claude Code */
228
+ "workspace.current_dir" structure shared with Anthropic Claude Code CLI */
229
229
  if (tool === "copilot"
230
230
  && (data.workspace?.current_dir === undefined || data.workspace.current_dir === "")
231
231
  && typeof data.cwd === "string" && data.cwd !== "") {
@@ -0,0 +1,18 @@
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
+ /* write a string to stdout, awaiting the write callback which only
7
+ fires once the data has been flushed to the underlying pipe, so a
8
+ subsequent "process.exit" cannot truncate the output */
9
+ export const writeStdout = (text) => {
10
+ return new Promise((resolve, reject) => {
11
+ process.stdout.write(text, (err) => {
12
+ if (err)
13
+ reject(err);
14
+ else
15
+ resolve();
16
+ });
17
+ });
18
+ };
package/dst/ase-task.js CHANGED
@@ -12,6 +12,7 @@ import { isScalar } from "yaml";
12
12
  import { z } from "zod";
13
13
  import { Config, configSchema, parseScope } from "./ase-config.js";
14
14
  import { Markdown } from "./ase-markdown.js";
15
+ import { writeStdout } from "./ase-stdout.js";
15
16
  /* reusable functionality: persisted task plans under
16
17
  <project>/<basedir>/TASK-<id>.md (driven by the
17
18
  "project.artifact.task.{basedir,files}" configuration) */
@@ -269,25 +270,23 @@ export default class TaskCommand {
269
270
  .command("list")
270
271
  .description("List all persisted task ids, one per line")
271
272
  .option("-v, --verbose", "also show the task file modification time as (YYYY-MM-DD HH:MM)")
272
- .action((opts) => {
273
+ .action(async (opts) => {
273
274
  const items = Task.list(this.log, opts.verbose ?? false);
274
275
  for (const item of items) {
275
276
  if (opts.verbose)
276
- process.stdout.write(`${item.id}\t(${item.mtime})\n`);
277
+ await writeStdout(`${item.id}\t(${item.mtime})\n`);
277
278
  else
278
- process.stdout.write(`${item.id}\n`);
279
+ await writeStdout(`${item.id}\n`);
279
280
  }
280
- process.exit(0);
281
281
  });
282
282
  /* register CLI sub-command "ase task load" */
283
283
  task
284
284
  .command("load")
285
285
  .description("Load a task by id and write it to stdout")
286
286
  .argument("<id>", "Task identifier")
287
- .action((id) => {
287
+ .action(async (id) => {
288
288
  const text = Task.load(this.log, id);
289
- process.stdout.write(text);
290
- process.exit(0);
289
+ await writeStdout(text);
291
290
  });
292
291
  /* register CLI sub-command "ase task edit" */
293
292
  task
package/dst/ase.js CHANGED
@@ -15,6 +15,7 @@ import SetupCommand from "./ase-setup.js";
15
15
  import StatuslineCommand from "./ase-statusline.js";
16
16
  import TaskCommand from "./ase-task.js";
17
17
  import ArtifactCommand from "./ase-artifact.js";
18
+ import MetaCommand from "./ase-meta.js";
18
19
  import CompatCommand from "./ase-compat.js";
19
20
  import pkg from "../package.json" with { type: "json" };
20
21
  /* globally initialize logger */
@@ -52,6 +53,7 @@ const main = async () => {
52
53
  new StatuslineCommand(log).register(program);
53
54
  new TaskCommand(log).register(program);
54
55
  new ArtifactCommand(log).register(program);
56
+ new MetaCommand(log).register(program);
55
57
  new CompatCommand().register(program);
56
58
  new DiagramCommand(log).register(program);
57
59
  /* parse program arguments */
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "homepage": "http://github.com/rse/ase",
7
7
  "repository": { "url": "git+https://github.com/rse/ase.git", "type": "git" },
8
8
  "bugs": { "url": "http://github.com/rse/ase/issues" },
9
- "version": "0.9.31",
9
+ "version": "0.9.33",
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.31",
3
+ "version": "0.9.33",
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.31",
3
+ "version": "0.9.33",
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.31",
3
+ "version": "0.9.33",
4
4
  "description": "Agentic Software Engineering (ASE)",
5
5
  "keywords": [ "agentic", "software", "engineering" ],
6
6
  "homepage": "https://ase.tools",
@@ -2,8 +2,8 @@
2
2
  ASE Constitution
3
3
  ================
4
4
 
5
- You are **Claude Code**, an expert-level AI coding assistant.
6
- You have the **Agentic Software Engineering (ASE)** facility enabled,
5
+ You are an expert-level AI coding assistant.
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
9
  If ((<ase-agent-tool/> is equal `codex`) *AND* (<ase-headless/> is empty
@@ -28,7 +28,7 @@ Skill Output
28
28
  - *IMPORTANT*: For *Final-Message-Only Display* ("focus mode"):
29
29
  some agent harnesses show the user only the *final* text message of
30
30
  a turn and *hide* all text emitted *between* tool calls. If your
31
- harness instructions indicate such a mode (e.g. *Claude Code* with
31
+ harness instructions indicate such a mode (e.g. *Anthropic Claude Code CLI* with
32
32
  "focus mode" enabled), you *MUST* repeat *all* <template/> outputs
33
33
  emitted since the last shown final text message -- *verbatim*, in
34
34
  their *original order*, and each only *once* -- at the *top* of the
@@ -6,7 +6,7 @@
6
6
  "homepage": "http://github.com/rse/ase",
7
7
  "repository": { "url": "git+https://github.com/rse/ase.git", "type": "git" },
8
8
  "bugs": { "url": "http://github.com/rse/ase/issues" },
9
- "version": "0.9.31",
9
+ "version": "0.9.33",
10
10
  "license": "Apache-2.0",
11
11
  "author": {
12
12
  "name": "Dr. Ralf S. Engelschall",