@rse/ase 0.9.11 → 0.9.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dst/ase-compat.js +68 -0
- package/dst/ase-diagram.js +5 -2
- package/dst/ase-getopt.js +2 -1
- package/dst/ase-hook.js +158 -67
- package/dst/ase-mcp.js +2 -0
- package/dst/ase-persona.js +4 -1
- package/dst/ase-service.js +2 -0
- package/dst/ase-setup.js +52 -23
- package/dst/ase-skills.js +13 -3
- package/dst/ase-statusline.js +9 -5
- package/dst/ase.js +3 -1
- package/package.json +2 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/mcp.json +8 -0
- package/plugin/.codex-plugin/plugin.json +17 -0
- package/plugin/.github/plugin/plugin.json +1 -1
- package/plugin/hooks/hooks-codex.json +67 -0
- package/plugin/meta/ase-dialog.md +21 -1
- package/plugin/meta/ase-format-arch.md +26 -9
- package/plugin/meta/ase-format-meta.md +7 -3
- package/plugin/meta/ase-format-spec.md +11 -6
- package/plugin/meta/ase-getopt.md +6 -2
- package/plugin/meta/ase-skill.md +43 -19
- package/plugin/package.json +1 -1
- package/plugin/skills/ase-arch-analyze/SKILL.md +17 -10
- package/plugin/skills/ase-code-craft/SKILL.md +17 -5
- package/plugin/skills/ase-code-explain/SKILL.md +0 -1
- package/plugin/skills/ase-code-insight/SKILL.md +1 -1
- package/plugin/skills/ase-code-lint/SKILL.md +13 -14
- package/plugin/skills/ase-code-refactor/SKILL.md +17 -5
- package/plugin/skills/ase-code-resolve/SKILL.md +18 -5
- package/plugin/skills/ase-docs-proofread/SKILL.md +11 -12
- package/plugin/skills/ase-meta-compat/SKILL.md +278 -0
- package/plugin/skills/ase-meta-compat/help.md +67 -0
- package/plugin/skills/ase-meta-diff/SKILL.md +3 -3
- package/plugin/skills/ase-meta-evaluate/SKILL.md +20 -18
- package/plugin/skills/ase-meta-quorum/SKILL.md +3 -3
- package/plugin/skills/ase-task-condense/SKILL.md +6 -0
- package/plugin/skills/ase-task-edit/SKILL.md +6 -2
- package/plugin/skills/ase-task-grill/SKILL.md +10 -7
- package/plugin/skills/ase-task-id/SKILL.md +3 -3
- package/plugin/skills/ase-task-implement/SKILL.md +6 -2
- package/plugin/skills/ase-task-preflight/SKILL.md +5 -1
- package/plugin/skills/ase-task-rename/SKILL.md +9 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** Agentic Software Engineering (ASE)
|
|
3
|
+
** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
|
+
*/
|
|
6
|
+
/* the canonical expected values for every ase-meta-compat probe,
|
|
7
|
+
keyed by "<category>/<probe-name>" as used in the skill */
|
|
8
|
+
const EXPECTED = {
|
|
9
|
+
/* xml-placeholders */
|
|
10
|
+
"xml-placeholders/get-and-set": "42",
|
|
11
|
+
"xml-placeholders/self-ref": "pre_42",
|
|
12
|
+
"xml-placeholders/overwrite": "99",
|
|
13
|
+
"xml-placeholders/indexed": "+1,-1",
|
|
14
|
+
"xml-placeholders/nested-attr": "20",
|
|
15
|
+
"xml-placeholders/entity": "⦿",
|
|
16
|
+
/* control-flow */
|
|
17
|
+
"control-flow/branch": "mid",
|
|
18
|
+
"control-flow/while-sum": "15",
|
|
19
|
+
"control-flow/for-order": "-x-y-z",
|
|
20
|
+
"control-flow/while-break": "4",
|
|
21
|
+
"control-flow/step-skip": "X",
|
|
22
|
+
"control-flow/expand-subst": "[K:V]",
|
|
23
|
+
/* regex */
|
|
24
|
+
"regex/getopt-dash": "yes",
|
|
25
|
+
"regex/getopt-nodash": "no",
|
|
26
|
+
"regex/anchored-int": "yes,no",
|
|
27
|
+
"regex/alternation": "yes",
|
|
28
|
+
"regex/capture": "foo",
|
|
29
|
+
"regex/whitespace": "yes",
|
|
30
|
+
"regex/complex": "yes",
|
|
31
|
+
/* arithmetic */
|
|
32
|
+
"arithmetic/increment": "9",
|
|
33
|
+
"arithmetic/product-sum": "5.00",
|
|
34
|
+
"arithmetic/percentage": "0.43",
|
|
35
|
+
"arithmetic/bar-width": "54",
|
|
36
|
+
"arithmetic/threshold": "yes",
|
|
37
|
+
"arithmetic/round-half": "3"
|
|
38
|
+
};
|
|
39
|
+
/* format the expected values as "<id>: <value>\n" lines */
|
|
40
|
+
const formatExpected = () => Object.entries(EXPECTED).map(([id, value]) => `${id}: ${value}`).join("\n") + "\n";
|
|
41
|
+
/* CLI command "ase compat" */
|
|
42
|
+
export default class CompatCommand {
|
|
43
|
+
register(program) {
|
|
44
|
+
program
|
|
45
|
+
.command("compat")
|
|
46
|
+
.description("Output expected probe values for the ase-meta-compat self-test skill")
|
|
47
|
+
.action(() => {
|
|
48
|
+
process.stdout.write(formatExpected());
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/* MCP tool "ase_compat" — lets the skill call this without a Bash tool */
|
|
53
|
+
export class CompatMCP {
|
|
54
|
+
register(mcp) {
|
|
55
|
+
mcp.registerTool("ase_compat", {
|
|
56
|
+
title: "ASE compat expected values",
|
|
57
|
+
description: "Return the canonical expected probe values for the ase-meta-compat " +
|
|
58
|
+
"self-test skill as \"<id>: <value>\" lines, one per probe. " +
|
|
59
|
+
"Call this AFTER recording all actual probe results so the LLM " +
|
|
60
|
+
"cannot see the expected values before running the probes.",
|
|
61
|
+
inputSchema: {}
|
|
62
|
+
}, async () => {
|
|
63
|
+
return {
|
|
64
|
+
content: [{ type: "text", text: formatExpected() }]
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
package/dst/ase-diagram.js
CHANGED
|
@@ -117,12 +117,15 @@ export class Diagram {
|
|
|
117
117
|
/* detect terminal color capability */
|
|
118
118
|
static detectColorMode() {
|
|
119
119
|
let mode = "none";
|
|
120
|
+
let explicit = false;
|
|
120
121
|
/* attempt 1: query environment variable (explicitly) */
|
|
121
122
|
if (process.env.ASE_TERM_COLORS !== undefined)
|
|
122
|
-
if (/^(?:none|ansi16|ansi256)$/.test(process.env.ASE_TERM_COLORS))
|
|
123
|
+
if (/^(?:none|ansi16|ansi256)$/.test(process.env.ASE_TERM_COLORS)) {
|
|
123
124
|
mode = process.env.ASE_TERM_COLORS;
|
|
125
|
+
explicit = true;
|
|
126
|
+
}
|
|
124
127
|
/* attempt 2: query stdout */
|
|
125
|
-
if (
|
|
128
|
+
if (!explicit && process.stdout.isTTY) {
|
|
126
129
|
const depth = process.stdout.getColorDepth();
|
|
127
130
|
if (depth >= 8)
|
|
128
131
|
mode = "ansi256";
|
package/dst/ase-getopt.js
CHANGED
|
@@ -108,7 +108,8 @@ export class GetoptMCP {
|
|
|
108
108
|
if (listOpts.length > 0) {
|
|
109
109
|
const opts = cmd.opts();
|
|
110
110
|
for (const { long, choices } of listOpts) {
|
|
111
|
-
const
|
|
111
|
+
const key = long.replace(/-(.)/g, (_, c) => c.toUpperCase());
|
|
112
|
+
const v = opts[key];
|
|
112
113
|
if (typeof v !== "string")
|
|
113
114
|
continue;
|
|
114
115
|
const items = v.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
package/dst/ase-hook.js
CHANGED
|
@@ -8,6 +8,7 @@ import fs from "node:fs";
|
|
|
8
8
|
import os from "node:os";
|
|
9
9
|
import { execaSync } from "execa";
|
|
10
10
|
import { quote } from "shell-quote";
|
|
11
|
+
import getStdin from "get-stdin";
|
|
11
12
|
import * as v from "valibot";
|
|
12
13
|
import Version from "./ase-version.js";
|
|
13
14
|
import { Config, configSchema, parseScope } from "./ase-config.js";
|
|
@@ -41,7 +42,8 @@ const toolSpecs = {
|
|
|
41
42
|
mcpToolNamePattern: /^mcp__plugin_ase_ase__.+/,
|
|
42
43
|
addonMcpToolNamePattern: addonMcpToolNamePattern("mcp__", "__.+"),
|
|
43
44
|
preToolUseWrapped: true,
|
|
44
|
-
preToolUseEvent: "PreToolUse"
|
|
45
|
+
preToolUseEvent: "PreToolUse",
|
|
46
|
+
approvalEvent: "PreToolUse"
|
|
45
47
|
},
|
|
46
48
|
"copilot": {
|
|
47
49
|
toolNameField: "toolName",
|
|
@@ -51,7 +53,19 @@ const toolSpecs = {
|
|
|
51
53
|
mcpToolNamePattern: /^ase-.+/,
|
|
52
54
|
addonMcpToolNamePattern: addonMcpToolNamePattern("", "-.+"),
|
|
53
55
|
preToolUseWrapped: false,
|
|
54
|
-
preToolUseEvent: "preToolUse"
|
|
56
|
+
preToolUseEvent: "preToolUse",
|
|
57
|
+
approvalEvent: "PreToolUse"
|
|
58
|
+
},
|
|
59
|
+
"codex": {
|
|
60
|
+
toolNameField: "tool_name",
|
|
61
|
+
toolInputField: "tool_input",
|
|
62
|
+
toolInputIsString: false,
|
|
63
|
+
bashToolName: "Bash",
|
|
64
|
+
mcpToolNamePattern: /^mcp__ase__.+/,
|
|
65
|
+
addonMcpToolNamePattern: addonMcpToolNamePattern("mcp__", "__.+"),
|
|
66
|
+
preToolUseWrapped: true,
|
|
67
|
+
preToolUseEvent: "PreToolUse",
|
|
68
|
+
approvalEvent: "PermissionRequest"
|
|
55
69
|
}
|
|
56
70
|
};
|
|
57
71
|
/* CLI command "ase hook" */
|
|
@@ -64,6 +78,27 @@ export default class HookCommand {
|
|
|
64
78
|
isValidSessionId(id) {
|
|
65
79
|
return /^[A-Za-z0-9._-]+$/.test(id);
|
|
66
80
|
}
|
|
81
|
+
/* read the entire stdin payload asynchronously */
|
|
82
|
+
readStdin() {
|
|
83
|
+
/* best-effort: treat an unreadable/closed stdin as empty */
|
|
84
|
+
return getStdin().catch(() => "");
|
|
85
|
+
}
|
|
86
|
+
/* drain and discard the stdin event payload */
|
|
87
|
+
async drainStdin() {
|
|
88
|
+
await this.readStdin();
|
|
89
|
+
}
|
|
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
|
+
}
|
|
67
102
|
/* best-effort JSON parse with valibot schema validation: returns
|
|
68
103
|
an empty object on blank input, malformed JSON, or schema
|
|
69
104
|
mismatch, so callers can treat the result uniformly. Extra
|
|
@@ -110,13 +145,24 @@ export default class HookCommand {
|
|
|
110
145
|
return this.expandReferences(content, path.dirname(abs), next);
|
|
111
146
|
});
|
|
112
147
|
}
|
|
113
|
-
/* handler for "ase hook session-start" (
|
|
148
|
+
/* handler for "ase hook session-start" (all tools) */
|
|
114
149
|
async doSessionStart(tool) {
|
|
115
150
|
/* determine plugin root (env var name differs per tool) */
|
|
116
|
-
|
|
117
|
-
|
|
151
|
+
let pluginRootVars;
|
|
152
|
+
if (tool === "copilot")
|
|
153
|
+
pluginRootVars = ["COPILOT_PLUGIN_ROOT"];
|
|
154
|
+
else if (tool === "codex")
|
|
155
|
+
pluginRootVars = ["PLUGIN_ROOT", "CLAUDE_PLUGIN_ROOT"];
|
|
156
|
+
else
|
|
157
|
+
pluginRootVars = ["CLAUDE_PLUGIN_ROOT"];
|
|
158
|
+
let pluginRoot = "";
|
|
159
|
+
for (const pluginRootVar of pluginRootVars)
|
|
160
|
+
if ((process.env[pluginRootVar] ?? "") !== "") {
|
|
161
|
+
pluginRoot = process.env[pluginRootVar];
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
118
164
|
if (pluginRoot === "")
|
|
119
|
-
throw new Error(`${
|
|
165
|
+
throw new Error(`${pluginRootVars.join("/")} environment variable is not set`);
|
|
120
166
|
/* determine path to external files */
|
|
121
167
|
const filePkg = path.join(pluginRoot, ".claude-plugin", "plugin.json");
|
|
122
168
|
const fileMd = path.join(pluginRoot, "meta", "ase-constitution.md");
|
|
@@ -152,7 +198,7 @@ export default class HookCommand {
|
|
|
152
198
|
const versionHint = versionHints.length > 0 ? "(" + versionHints.join(", ") + ")" : "";
|
|
153
199
|
/* read session information (Claude Code uses snake_case fields,
|
|
154
200
|
Copilot CLI uses camelCase fields) */
|
|
155
|
-
const stdin =
|
|
201
|
+
const stdin = await this.readStdin();
|
|
156
202
|
const input = this.parseJSON(stdin, v.object({
|
|
157
203
|
session_id: v.optional(v.string()),
|
|
158
204
|
sessionId: v.optional(v.string()),
|
|
@@ -228,9 +274,10 @@ export default class HookCommand {
|
|
|
228
274
|
/* expand all @<file> references manually */
|
|
229
275
|
md = this.expandReferences(md, path.dirname(fileMd));
|
|
230
276
|
/* inject markdown into session context.
|
|
231
|
-
Claude Code
|
|
232
|
-
Copilot CLI expects a flat top-level
|
|
233
|
-
|
|
277
|
+
Claude Code and OpenAI Codex CLI expect the context nested in
|
|
278
|
+
"hookSpecificOutput"; Copilot CLI expects a flat top-level
|
|
279
|
+
"additionalContext" field. */
|
|
280
|
+
const payload = tool !== "copilot" ? {
|
|
234
281
|
"hookSpecificOutput": {
|
|
235
282
|
"hookEventName": "SessionStart",
|
|
236
283
|
"additionalContext": md
|
|
@@ -238,7 +285,7 @@ export default class HookCommand {
|
|
|
238
285
|
} : {
|
|
239
286
|
"additionalContext": md
|
|
240
287
|
};
|
|
241
|
-
|
|
288
|
+
await this.writeStdout(JSON.stringify(payload));
|
|
242
289
|
return 0;
|
|
243
290
|
}
|
|
244
291
|
/* publish the agent activity marker to tmux as a per-pane user
|
|
@@ -258,19 +305,21 @@ export default class HookCommand {
|
|
|
258
305
|
}
|
|
259
306
|
}
|
|
260
307
|
/* handler for "ase hook user-prompt-submit" (both tools) */
|
|
261
|
-
doUserPromptSubmit(_tool) {
|
|
308
|
+
async doUserPromptSubmit(_tool) {
|
|
309
|
+
await this.drainStdin();
|
|
262
310
|
this.writeAgentStatus("busy");
|
|
263
311
|
return 0;
|
|
264
312
|
}
|
|
265
313
|
/* handler for "ase hook stop" (both tools) */
|
|
266
|
-
doStop(_tool) {
|
|
314
|
+
async doStop(_tool) {
|
|
315
|
+
await this.drainStdin();
|
|
267
316
|
this.writeAgentStatus("ready");
|
|
268
317
|
return 0;
|
|
269
318
|
}
|
|
270
319
|
/* handler for "ase hook session-end" (both tools) */
|
|
271
|
-
doSessionEnd(_tool) {
|
|
320
|
+
async doSessionEnd(_tool) {
|
|
272
321
|
/* determine session id */
|
|
273
|
-
const sessionId = this.readSessionIdFromStdin();
|
|
322
|
+
const sessionId = await this.readSessionIdFromStdin();
|
|
274
323
|
/* remove the session directory ~/.ase/session/<id> (only for a valid sessionId) */
|
|
275
324
|
if (this.isValidSessionId(sessionId)) {
|
|
276
325
|
const dir = path.join(os.homedir(), ".ase", "session", sessionId);
|
|
@@ -289,8 +338,8 @@ export default class HookCommand {
|
|
|
289
338
|
return input.session_id ?? input.sessionId ?? "";
|
|
290
339
|
}
|
|
291
340
|
/* read session id from stdin JSON payload */
|
|
292
|
-
readSessionIdFromStdin() {
|
|
293
|
-
const stdin =
|
|
341
|
+
async readSessionIdFromStdin() {
|
|
342
|
+
const stdin = await this.readStdin();
|
|
294
343
|
const input = this.parseJSON(stdin, v.object({
|
|
295
344
|
session_id: v.optional(v.string()),
|
|
296
345
|
sessionId: v.optional(v.string())
|
|
@@ -319,17 +368,12 @@ export default class HookCommand {
|
|
|
319
368
|
/* the edit-capable skills whose active state lets the pre-tool-use
|
|
320
369
|
hook auto-approve subsequent "Edit" invocations */
|
|
321
370
|
editCapableSkills = ["ase-code-lint", "ase-docs-proofread"];
|
|
322
|
-
/*
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
session_id: v.optional(v.string()),
|
|
329
|
-
sessionId: v.optional(v.string())
|
|
330
|
-
}));
|
|
331
|
-
/* determine whether to auto-approve the tool invocation
|
|
332
|
-
(field names and value shapes differ between tools) */
|
|
371
|
+
/* determine whether an ASE tool invocation described by the parsed
|
|
372
|
+
hook input should be auto-approved, and (if so) the human-readable
|
|
373
|
+
reason. The input field names and value shapes differ between
|
|
374
|
+
tools, but the decision logic is shared by the "pre-tool-use" and
|
|
375
|
+
"permission-request" handlers. */
|
|
376
|
+
decideApproval(spec, input) {
|
|
333
377
|
const toolName = typeof input[spec.toolNameField] === "string" ?
|
|
334
378
|
input[spec.toolNameField] : "";
|
|
335
379
|
let toolInput = {};
|
|
@@ -341,32 +385,43 @@ export default class HookCommand {
|
|
|
341
385
|
}));
|
|
342
386
|
else if (!spec.toolInputIsString && typeof rawInput === "object" && rawInput !== null)
|
|
343
387
|
toolInput = rawInput;
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
if (toolName ===
|
|
347
|
-
approve
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
else if (
|
|
351
|
-
approve
|
|
352
|
-
reason = "ASE skill invocation auto-approved";
|
|
353
|
-
}
|
|
354
|
-
else if (spec.mcpToolNamePattern.test(toolName)) {
|
|
355
|
-
approve = true;
|
|
356
|
-
reason = "ASE MCP tool invocation auto-approved";
|
|
357
|
-
}
|
|
358
|
-
else if (spec.addonMcpToolNamePattern.test(toolName)) {
|
|
359
|
-
approve = true;
|
|
360
|
-
reason = "ASE addon MCP tool invocation auto-approved";
|
|
361
|
-
}
|
|
388
|
+
if (toolName === spec.bashToolName && /^ase(\s|$)/.test(toolInput.command ?? ""))
|
|
389
|
+
return { approve: true, reason: "ASE CLI invocation auto-approved" };
|
|
390
|
+
else if (toolName === "Skill" && /^(?:ase:)?ase-.+/.test(toolInput.skill ?? ""))
|
|
391
|
+
return { approve: true, reason: "ASE skill invocation auto-approved" };
|
|
392
|
+
else if (spec.mcpToolNamePattern.test(toolName))
|
|
393
|
+
return { approve: true, reason: "ASE MCP tool invocation auto-approved" };
|
|
394
|
+
else if (spec.addonMcpToolNamePattern.test(toolName))
|
|
395
|
+
return { approve: true, reason: "ASE addon MCP tool invocation auto-approved" };
|
|
362
396
|
else if (toolName === "Edit") {
|
|
363
397
|
const sessionId = this.pickSessionId(input);
|
|
364
398
|
const activeSkill = this.readActiveSkill(sessionId);
|
|
365
|
-
if (this.editCapableSkills.includes(activeSkill))
|
|
366
|
-
approve
|
|
367
|
-
reason = `${activeSkill}: edit auto-approved for active edit-capable skill`;
|
|
368
|
-
}
|
|
399
|
+
if (this.editCapableSkills.includes(activeSkill))
|
|
400
|
+
return { approve: true, reason: `${activeSkill}: edit auto-approved for active edit-capable skill` };
|
|
369
401
|
}
|
|
402
|
+
return { approve: false, reason: "" };
|
|
403
|
+
}
|
|
404
|
+
/* handler for "ase hook pre-tool-use" (all tools).
|
|
405
|
+
For Claude Code and Copilot CLI this is where ASE tool
|
|
406
|
+
invocations are auto-approved (via "permissionDecision: allow").
|
|
407
|
+
OpenAI Codex CLI rejects that mechanism in "PreToolUse", so for
|
|
408
|
+
Codex this handler stays silent and approval is granted in the
|
|
409
|
+
separate "permission-request" handler instead -- the handler must
|
|
410
|
+
still drain stdin, as Codex treats a non-draining hook as a hard
|
|
411
|
+
error. */
|
|
412
|
+
async doPreToolUse(tool) {
|
|
413
|
+
const spec = toolSpecs[tool];
|
|
414
|
+
/* read tool invocation information */
|
|
415
|
+
const stdin = await this.readStdin();
|
|
416
|
+
const input = this.parseJSON(stdin, v.looseObject({
|
|
417
|
+
session_id: v.optional(v.string()),
|
|
418
|
+
sessionId: v.optional(v.string())
|
|
419
|
+
}));
|
|
420
|
+
/* Codex auto-approves through "PermissionRequest", not here */
|
|
421
|
+
if (spec.approvalEvent !== "PreToolUse")
|
|
422
|
+
return 0;
|
|
423
|
+
/* determine whether to auto-approve the tool invocation */
|
|
424
|
+
const { approve, reason } = this.decideApproval(spec, input);
|
|
370
425
|
/* emit permission decision (or stay silent to defer to default flow).
|
|
371
426
|
Claude Code expects the decision nested in "hookSpecificOutput";
|
|
372
427
|
Copilot CLI expects flat top-level fields. */
|
|
@@ -381,14 +436,42 @@ export default class HookCommand {
|
|
|
381
436
|
"permissionDecision": "allow",
|
|
382
437
|
"permissionDecisionReason": reason
|
|
383
438
|
};
|
|
384
|
-
|
|
439
|
+
await this.writeStdout(JSON.stringify(payload));
|
|
440
|
+
}
|
|
441
|
+
return 0;
|
|
442
|
+
}
|
|
443
|
+
/* handler for "ase hook permission-request" (OpenAI Codex CLI only).
|
|
444
|
+
Codex fires this event only when a tool invocation would otherwise
|
|
445
|
+
require interactive user approval, and -- unlike "PreToolUse" --
|
|
446
|
+
honors an auto-approval here through "decision.behavior: allow".
|
|
447
|
+
Staying silent (or returning a non-approval) defers to Codex's
|
|
448
|
+
normal approval flow. */
|
|
449
|
+
async doPermissionRequest(tool) {
|
|
450
|
+
const spec = toolSpecs[tool];
|
|
451
|
+
/* read tool invocation information */
|
|
452
|
+
const stdin = await this.readStdin();
|
|
453
|
+
const input = this.parseJSON(stdin, v.looseObject({
|
|
454
|
+
session_id: v.optional(v.string()),
|
|
455
|
+
sessionId: v.optional(v.string())
|
|
456
|
+
}));
|
|
457
|
+
/* determine whether to auto-approve the tool invocation */
|
|
458
|
+
const { approve } = this.decideApproval(spec, input);
|
|
459
|
+
/* emit the Codex "PermissionRequest" approval decision */
|
|
460
|
+
if (approve) {
|
|
461
|
+
const payload = {
|
|
462
|
+
"hookSpecificOutput": {
|
|
463
|
+
"hookEventName": "PermissionRequest",
|
|
464
|
+
"decision": { "behavior": "allow" }
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
await this.writeStdout(JSON.stringify(payload));
|
|
385
468
|
}
|
|
386
469
|
return 0;
|
|
387
470
|
}
|
|
388
471
|
/* parse and validate the --tool option */
|
|
389
472
|
parseTool(value) {
|
|
390
|
-
if (value !== "claude" && value !== "copilot")
|
|
391
|
-
throw new Error(`invalid --tool value: "${value}" (expected "claude" or "
|
|
473
|
+
if (value !== "claude" && value !== "copilot" && value !== "codex")
|
|
474
|
+
throw new Error(`invalid --tool value: "${value}" (expected "claude", "copilot", or "codex")`);
|
|
392
475
|
return value;
|
|
393
476
|
}
|
|
394
477
|
/* register commands */
|
|
@@ -408,41 +491,49 @@ export default class HookCommand {
|
|
|
408
491
|
hookCmd
|
|
409
492
|
.command("session-start")
|
|
410
493
|
.description("handle SessionStart hook event")
|
|
411
|
-
.option("-t, --tool <tool>", "target tool (\"claude\" or \"
|
|
494
|
+
.option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
|
|
412
495
|
.action(async (opts) => {
|
|
413
|
-
process.
|
|
496
|
+
process.exitCode = await this.doSessionStart(this.parseTool(opts.tool));
|
|
414
497
|
});
|
|
415
498
|
/* register CLI sub-command "ase hook session-end" */
|
|
416
499
|
hookCmd
|
|
417
500
|
.command("session-end")
|
|
418
501
|
.description("handle SessionEnd hook event")
|
|
419
|
-
.option("-t, --tool <tool>", "target tool (\"claude\" or \"
|
|
420
|
-
.action((opts) => {
|
|
421
|
-
process.
|
|
502
|
+
.option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
|
|
503
|
+
.action(async (opts) => {
|
|
504
|
+
process.exitCode = await this.doSessionEnd(this.parseTool(opts.tool));
|
|
422
505
|
});
|
|
423
506
|
/* register CLI sub-command "ase hook pre-tool-use" */
|
|
424
507
|
hookCmd
|
|
425
508
|
.command("pre-tool-use")
|
|
426
509
|
.description("handle tool PreToolUse hook event")
|
|
427
|
-
.option("-t, --tool <tool>", "target tool (\"claude\" or \"
|
|
428
|
-
.action((opts) => {
|
|
429
|
-
process.
|
|
510
|
+
.option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
|
|
511
|
+
.action(async (opts) => {
|
|
512
|
+
process.exitCode = await this.doPreToolUse(this.parseTool(opts.tool));
|
|
513
|
+
});
|
|
514
|
+
/* register CLI sub-command "ase hook permission-request" */
|
|
515
|
+
hookCmd
|
|
516
|
+
.command("permission-request")
|
|
517
|
+
.description("handle tool PermissionRequest hook event (Codex CLI)")
|
|
518
|
+
.option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
|
|
519
|
+
.action(async (opts) => {
|
|
520
|
+
process.exitCode = await this.doPermissionRequest(this.parseTool(opts.tool));
|
|
430
521
|
});
|
|
431
522
|
/* register CLI sub-command "ase hook user-prompt-submit" */
|
|
432
523
|
hookCmd
|
|
433
524
|
.command("user-prompt-submit")
|
|
434
525
|
.description("handle UserPromptSubmit hook event (mark agent as busy)")
|
|
435
|
-
.option("-t, --tool <tool>", "target tool (\"claude\" or \"
|
|
436
|
-
.action((opts) => {
|
|
437
|
-
process.
|
|
526
|
+
.option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
|
|
527
|
+
.action(async (opts) => {
|
|
528
|
+
process.exitCode = await this.doUserPromptSubmit(this.parseTool(opts.tool));
|
|
438
529
|
});
|
|
439
530
|
/* register CLI sub-command "ase hook stop" */
|
|
440
531
|
hookCmd
|
|
441
532
|
.command("stop")
|
|
442
533
|
.description("handle Stop hook event (mark agent as ready)")
|
|
443
|
-
.option("-t, --tool <tool>", "target tool (\"claude\" or \"
|
|
444
|
-
.action((opts) => {
|
|
445
|
-
process.
|
|
534
|
+
.option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
|
|
535
|
+
.action(async (opts) => {
|
|
536
|
+
process.exitCode = await this.doStop(this.parseTool(opts.tool));
|
|
446
537
|
});
|
|
447
538
|
}
|
|
448
539
|
}
|
package/dst/ase-mcp.js
CHANGED
|
@@ -133,6 +133,8 @@ export default class MCPCommand {
|
|
|
133
133
|
};
|
|
134
134
|
/* trigger a reconnect chain (idempotent while one is active) */
|
|
135
135
|
const triggerReconnect = (reason) => {
|
|
136
|
+
if (reconnecting)
|
|
137
|
+
return;
|
|
136
138
|
reconnecting = true;
|
|
137
139
|
this.log.write("warning", `mcp: ${reason} — reconnecting`);
|
|
138
140
|
reconnect(0).catch(() => { });
|
package/dst/ase-persona.js
CHANGED
|
@@ -21,7 +21,10 @@ export class Persona {
|
|
|
21
21
|
const val = cfg.get("agent.persona");
|
|
22
22
|
if (val === undefined)
|
|
23
23
|
return "engineer";
|
|
24
|
-
|
|
24
|
+
const style = String(isScalar(val) ? val.value : val);
|
|
25
|
+
if (!Persona.styles.includes(style))
|
|
26
|
+
return "engineer";
|
|
27
|
+
return style;
|
|
25
28
|
}
|
|
26
29
|
/* set the persona style on the strongest scope of an optional session */
|
|
27
30
|
static set(log, style, session) {
|
package/dst/ase-service.js
CHANGED
|
@@ -16,6 +16,7 @@ import * as v from "valibot";
|
|
|
16
16
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
17
17
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
18
18
|
import { Config, configSchema, ConfigMCP } from "./ase-config.js";
|
|
19
|
+
import { CompatMCP } from "./ase-compat.js";
|
|
19
20
|
import { DiagramMCP } from "./ase-diagram.js";
|
|
20
21
|
import { TaskMCP } from "./ase-task.js";
|
|
21
22
|
import { MarkdownMCP } from "./ase-markdown.js";
|
|
@@ -236,6 +237,7 @@ export default class ServiceCommand {
|
|
|
236
237
|
const buildMcpServer = () => {
|
|
237
238
|
const mcp = new McpServer({ name: "ase", version: pkg.version });
|
|
238
239
|
new ServiceMCP({ projectId: ctx.projectId, port: ctx.port, startTime }).register(mcp);
|
|
240
|
+
new CompatMCP().register(mcp);
|
|
239
241
|
new DiagramMCP().register(mcp);
|
|
240
242
|
new TaskMCP(this.log).register(mcp);
|
|
241
243
|
new MarkdownMCP().register(mcp);
|