@rse/ase 0.9.31 → 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.
- package/dst/ase-artifact.js +6 -7
- package/dst/ase-hook.js +50 -22
- package/dst/ase-meta.js +65 -0
- package/dst/ase-stdout.js +18 -0
- package/dst/ase-task.js +6 -7
- package/dst/ase.js +2 -0
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.github/plugin/plugin.json +1 -1
- package/plugin/package.json +1 -1
package/dst/ase-artifact.js
CHANGED
|
@@ -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
|
-
|
|
233
|
+
await writeStdout(`# ${kind}:\n`);
|
|
233
234
|
for (const file of files)
|
|
234
|
-
|
|
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
|
-
|
|
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
|
-
/*
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
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
|
|
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
|
|
509
|
+
await writeStdout(JSON.stringify(payload));
|
|
482
510
|
}
|
|
483
511
|
return 0;
|
|
484
512
|
}
|
package/dst/ase-meta.js
ADDED
|
@@ -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
|
-
|
|
277
|
+
await writeStdout(`${item.id}\t(${item.mtime})\n`);
|
|
277
278
|
else
|
|
278
|
-
|
|
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
|
-
|
|
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.
|
|
9
|
+
"version": "0.9.32",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"author": {
|
|
12
12
|
"name": "Dr. Ralf S. Engelschall",
|
package/plugin/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.
|
|
9
|
+
"version": "0.9.32",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"author": {
|
|
12
12
|
"name": "Dr. Ralf S. Engelschall",
|