@rse/ase 0.9.30 → 0.9.32

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",
@@ -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");
@@ -251,6 +257,7 @@ export default class HookCommand {
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` +
@@ -299,7 +307,7 @@ 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
@@ -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);
@@ -435,7 +463,7 @@ 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
468
  Claude Code expects the decision nested in "hookSpecificOutput";
441
469
  Copilot CLI expects flat top-level fields. */
@@ -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
  }
@@ -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
+ }
@@ -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.30",
9
+ "version": "0.9.32",
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.30",
3
+ "version": "0.9.32",
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.30",
3
+ "version": "0.9.32",
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.30",
3
+ "version": "0.9.32",
4
4
  "description": "Agentic Software Engineering (ASE)",
5
5
  "keywords": [ "agentic", "software", "engineering" ],
6
6
  "homepage": "https://ase.tools",
@@ -1003,7 +1003,7 @@ its purpose, and the alternatives that were considered but not chosen.
1003
1003
  - In case the rationale is not present, the
1004
1004
  entire `, **BECAUSE** [...]` clause is omitted.
1005
1005
 
1006
- - The known *Client Aspects* which usually have to covered (at least for
1006
+ - The known *Client Aspects* which usually have to be covered (at least for
1007
1007
  clients in a Rich-Client architecture):
1008
1008
 
1009
1009
  - **Interface Theme**:
@@ -1079,7 +1079,7 @@ its purpose, and the alternatives that were considered but not chosen.
1079
1079
  - **Environment Detection**:
1080
1080
  Runtime Detection, Feature Detection.
1081
1081
 
1082
- - The known *Server Aspects* which usually have to covered (at least for
1082
+ - The known *Server Aspects* which usually have to be covered (at least for
1083
1083
  servers, sometimes called Thin-Servers, in a Rich-Client architecture):
1084
1084
 
1085
1085
  - **Environment Detection**:
@@ -143,10 +143,10 @@ Skill Sequential Processing
143
143
  - *IMPORTANT*: You *MUST* *strictly sequentially* execute every <step/> in
144
144
  a <flow/>. You *MUST* not implicitly skip any <step/> during
145
145
  processing, except you were explicitly requested to do this or the
146
- step is a configured to be a conditional step!
146
+ step is configured to be a conditional step!
147
147
 
148
148
  - *IMPORTANT*: You *MUST* *strictly sequentially* execute every numbered
149
- items in a <step/>. You *MUST* not implicitly skip any numbered item during
149
+ item in a <step/>. You *MUST* not implicitly skip any numbered item during
150
150
  processing, except you were explicitly requested to do this!
151
151
 
152
152
  - *IMPORTANT*: For any <step/> that specifies an *agent* in its
@@ -120,7 +120,7 @@ you *MUST* honor the following so-called "CRAFTING TENETS":
120
120
  RECONCILIATION TENETS
121
121
  ---------------------
122
122
 
123
- When *reconcile* a set of target artifacts to a set of source artifacts,
123
+ When *reconciling* a set of target artifacts to a set of source artifacts,
124
124
  you *MUST* honor the following so-called **RECONCILIATION TENETS**:
125
125
 
126
126
  - **Directional Authority**:
@@ -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.30",
9
+ "version": "0.9.32",
10
10
  "license": "Apache-2.0",
11
11
  "author": {
12
12
  "name": "Dr. Ralf S. Engelschall",
@@ -1,7 +1,7 @@
1
1
 
2
2
  ## NAME
3
3
 
4
- `ase-meta-diaboli` - Play "Devil's Advocate" (latin: "Advocatus Diaboli")
4
+ `ase-meta-diaboli` - Play "Devil's Advocate" (Latin: "Advocatus Diaboli")
5
5
 
6
6
  ## SYNOPSIS
7
7
 
@@ -12,7 +12,7 @@
12
12
 
13
13
  ## DESCRIPTION
14
14
 
15
- The `ase-meta-diaboli` skill plays *Devil's Advocate* (latin:
15
+ The `ase-meta-diaboli` skill plays *Devil's Advocate* (Latin:
16
16
  *Advocatus Diaboli*) by relentlessly challenging or criticising a
17
17
  supplied *thesis*. It applies a disciplined set of critical-thinking
18
18
  tenets - steelmanning, stress-testing fundamentals, surfacing implicit
@@ -130,7 +130,7 @@ Procedure
130
130
  For this process, determine the <n/> essential aspects <aspect-N/>
131
131
  (a one or two word long short identifier like `Foo` or `Bar-Baz`)
132
132
  and the corresponding decision/question <question-N/> where a shared
133
- understanding is required upon.
133
+ understanding is required.
134
134
 
135
135
  Honor also the following checks for identifying the problematic
136
136
  aspects: