@rse/ase 0.9.47 → 0.9.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dst/ase-artifact.js +3 -1
- package/dst/ase-compat.js +1 -1
- package/dst/ase-config.js +76 -13
- package/dst/ase-diagram.js +10 -15
- package/dst/ase-getopt.js +66 -60
- package/dst/ase-guidance.js +89 -0
- package/dst/ase-hook.js +46 -66
- package/dst/ase-kv.js +18 -43
- package/dst/ase-mcp.js +1 -1
- package/dst/ase-meta.js +1 -1
- package/dst/ase-service.js +4 -6
- package/dst/ase-setup.js +24 -39
- package/dst/ase-skills.js +1 -1
- package/dst/ase-statusline.js +42 -18
- package/dst/ase-stdio.js +25 -0
- package/dst/ase-task.js +11 -11
- package/dst/ase.js +1 -1
- 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/meta/ase-common-task.md +9 -0
- package/plugin/meta/ase-constitution.md +17 -6
- package/plugin/meta/ase-getopt.md +11 -1
- package/plugin/meta/ase-skill.md +92 -4
- package/plugin/package.json +1 -1
- package/plugin/skills/ase-arch-analyze/SKILL.md +8 -6
- package/plugin/skills/ase-arch-discover/SKILL.md +12 -0
- package/plugin/skills/ase-code-analyze/SKILL.md +6 -4
- package/plugin/skills/ase-code-lint/SKILL.md +16 -0
- package/plugin/skills/ase-docs-proofread/SKILL.md +10 -0
- package/plugin/skills/ase-help-intent/SKILL.md +3 -8
- package/plugin/skills/ase-help-skill/SKILL.md +10 -2
- package/plugin/skills/ase-help-skill/catalog.md +1 -1
- package/plugin/skills/ase-meta-brainstorm/SKILL.md +8 -0
- package/plugin/skills/ase-meta-commit/SKILL.md +13 -0
- package/plugin/skills/ase-meta-config/SKILL.md +165 -0
- package/plugin/skills/ase-meta-config/help.md +136 -0
- package/plugin/skills/ase-meta-diaboli/help.md +1 -1
- package/plugin/skills/ase-meta-diff/SKILL.md +22 -0
- package/plugin/skills/ase-meta-review/SKILL.md +13 -1
- package/plugin/skills/ase-sync-export/SKILL.md +14 -0
- package/plugin/skills/ase-sync-import/SKILL.md +10 -0
- package/plugin/skills/ase-task-list/SKILL.md +21 -0
- package/plugin/skills/ase-task-view/SKILL.md +21 -0
- package/plugin/skills/ase-meta-persona/SKILL.md +0 -84
- package/plugin/skills/ase-meta-persona/help.md +0 -60
package/dst/ase-artifact.js
CHANGED
|
@@ -10,7 +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-
|
|
13
|
+
import { writeStdout } from "./ase-stdio.js";
|
|
14
14
|
/* the recognized artifact kinds, in descending precedence order;
|
|
15
15
|
"othr" is the implicit catch-all and is always resolved last */
|
|
16
16
|
export const artifactKinds = ["spec", "arch", "code", "docs", "infr", "othr"];
|
|
@@ -141,6 +141,8 @@ export class Artifact {
|
|
|
141
141
|
const basedir = Artifact.configString(cfg, `project.artifact.${kind}.basedir`)
|
|
142
142
|
.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "")
|
|
143
143
|
.replace(/^\.$/, "");
|
|
144
|
+
if (basedir.split("/").includes(".."))
|
|
145
|
+
throw new Error(`artifact: configured "basedir" "${basedir}" must not escape the project root`);
|
|
144
146
|
const files = Artifact.configString(cfg, `project.artifact.${kind}.files`);
|
|
145
147
|
return { basedir, files };
|
|
146
148
|
}
|
package/dst/ase-compat.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
4
|
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
5
|
*/
|
|
6
|
-
import { writeStdout } from "./ase-
|
|
6
|
+
import { writeStdout } from "./ase-stdio.js";
|
|
7
7
|
/* the canonical expected values for every ase-meta-compat probe,
|
|
8
8
|
keyed by "<category>/<probe-name>" as used in the skill */
|
|
9
9
|
const EXPECTED = {
|
package/dst/ase-config.js
CHANGED
|
@@ -14,14 +14,16 @@ import Table from "cli-table3";
|
|
|
14
14
|
import writeFileAtomic from "write-file-atomic";
|
|
15
15
|
import lockfile from "proper-lockfile";
|
|
16
16
|
import { z } from "zod";
|
|
17
|
-
import {
|
|
17
|
+
import { LRUCache } from "lru-cache";
|
|
18
|
+
import { writeStdout } from "./ase-stdio.js";
|
|
18
19
|
/* classification taxonomy */
|
|
19
20
|
export const projectClassification = {
|
|
20
21
|
boxing: ["white", "grey", "black"]
|
|
21
22
|
};
|
|
22
23
|
/* agent classification taxonomy */
|
|
23
24
|
export const agentClassification = {
|
|
24
|
-
persona: ["writer", "engineer", "journalist", "telegrapher", "caveman"]
|
|
25
|
+
persona: ["writer", "engineer", "journalist", "telegrapher", "caveman"],
|
|
26
|
+
guidance: ["none", "minimal", "normal", "verbose"]
|
|
25
27
|
};
|
|
26
28
|
/* classification presets */
|
|
27
29
|
export const projectClassificationPresets = {
|
|
@@ -40,6 +42,7 @@ export const projectClassificationPresets = {
|
|
|
40
42
|
default: {
|
|
41
43
|
"agent.task": "default",
|
|
42
44
|
"agent.persona": "engineer",
|
|
45
|
+
"agent.guidance": "normal",
|
|
43
46
|
"project.id": "example",
|
|
44
47
|
"project.name": "Example Project",
|
|
45
48
|
"project.boxing": "white",
|
|
@@ -93,20 +96,33 @@ const parseScopeTerm = (value) => {
|
|
|
93
96
|
else if (value === "project")
|
|
94
97
|
return { kind: "project" };
|
|
95
98
|
const m = /^(session|task):([A-Za-z0-9._-]+)$/.exec(value);
|
|
96
|
-
if (m !== null)
|
|
99
|
+
if (m !== null) {
|
|
100
|
+
if (m[2] === "." || m[2] === "..")
|
|
101
|
+
throw new Error(`invalid --scope term "${value}": id must not be "." or ".."`);
|
|
97
102
|
return { kind: m[1], id: m[2] };
|
|
103
|
+
}
|
|
98
104
|
throw new Error(`invalid --scope term "${value}" ` +
|
|
99
105
|
"(expected: \"user\", \"project\", \"task:<id>\", or \"session:<id>\")");
|
|
100
106
|
};
|
|
101
|
-
/* determine the Git top-level directory, if inside a Git repository
|
|
107
|
+
/* determine the Git top-level directory, if inside a Git repository;
|
|
108
|
+
cached per working directory ("" ≡ not inside a Git working tree), as
|
|
109
|
+
each determination spawns a Git subprocess and the Git context can
|
|
110
|
+
change over the lifetime of the long-running ASE service */
|
|
111
|
+
const gitToplevelCache = new LRUCache({ max: 4, ttl: 2 * 1000 });
|
|
102
112
|
const gitToplevel = () => {
|
|
113
|
+
const cwd = process.cwd();
|
|
114
|
+
const cached = gitToplevelCache.get(cwd);
|
|
115
|
+
if (cached !== undefined)
|
|
116
|
+
return cached === "" ? null : cached;
|
|
117
|
+
let top = "";
|
|
103
118
|
try {
|
|
104
|
-
|
|
105
|
-
return result.stdout.trim() || null;
|
|
119
|
+
top = execaSync("git", ["rev-parse", "--show-toplevel"], { stderr: "ignore" }).stdout.trim();
|
|
106
120
|
}
|
|
107
121
|
catch {
|
|
108
|
-
|
|
122
|
+
/* not inside a Git working tree */
|
|
109
123
|
}
|
|
124
|
+
gitToplevelCache.set(cwd, top);
|
|
125
|
+
return top === "" ? null : top;
|
|
110
126
|
};
|
|
111
127
|
/* detect whether a project context exists, i.e. either we are inside
|
|
112
128
|
a Git working tree or a ".ase" directory is present at or above cwd */
|
|
@@ -154,6 +170,11 @@ export const parseScope = (value) => {
|
|
|
154
170
|
terms.unshift({ kind: "default" });
|
|
155
171
|
return terms;
|
|
156
172
|
};
|
|
173
|
+
/* schema for a single artifact kind's "basedir"/"files" specification */
|
|
174
|
+
const artifactSchema = v.optional(v.strictObject({
|
|
175
|
+
basedir: v.optional(v.string()),
|
|
176
|
+
files: v.optional(v.string())
|
|
177
|
+
}));
|
|
157
178
|
/* schema for ".ase/config.yaml" */
|
|
158
179
|
export const configSchema = v.nullish(v.strictObject({
|
|
159
180
|
project: v.optional(v.strictObject({
|
|
@@ -161,16 +182,17 @@ export const configSchema = v.nullish(v.strictObject({
|
|
|
161
182
|
name: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
162
183
|
boxing: v.optional(v.picklist(projectClassification.boxing)),
|
|
163
184
|
artifact: v.optional(v.strictObject({
|
|
164
|
-
spec:
|
|
165
|
-
arch:
|
|
166
|
-
code:
|
|
167
|
-
docs:
|
|
168
|
-
infr:
|
|
169
|
-
task:
|
|
185
|
+
spec: artifactSchema,
|
|
186
|
+
arch: artifactSchema,
|
|
187
|
+
code: artifactSchema,
|
|
188
|
+
docs: artifactSchema,
|
|
189
|
+
infr: artifactSchema,
|
|
190
|
+
task: artifactSchema
|
|
170
191
|
}))
|
|
171
192
|
})),
|
|
172
193
|
agent: v.optional(v.strictObject({
|
|
173
194
|
persona: v.optional(v.picklist(agentClassification.persona)),
|
|
195
|
+
guidance: v.optional(v.picklist(agentClassification.guidance)),
|
|
174
196
|
task: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
175
197
|
skill: v.optional(v.pipe(v.string(), v.minLength(1)))
|
|
176
198
|
}))
|
|
@@ -749,5 +771,46 @@ export class ConfigMCP {
|
|
|
749
771
|
return mcpToolError(err);
|
|
750
772
|
}
|
|
751
773
|
});
|
|
774
|
+
/* config list */
|
|
775
|
+
mcp.registerTool("ase_config_list", {
|
|
776
|
+
title: "ASE config list",
|
|
777
|
+
description: "List all effective configuration entries of the layered configuration, " +
|
|
778
|
+
"cascading through the default/user/project/task/session chain up to and " +
|
|
779
|
+
"including the requested `scope`. Returns an `entries` array (in lexicographic " +
|
|
780
|
+
"`key` order) where each item has the dotted `key`, its effective `value`, and " +
|
|
781
|
+
"the `scope` label (\"default\", \"user\", \"project\", \"task:<id>\", or " +
|
|
782
|
+
"\"session:<id>\") that supplied it. For overlapping keys only the value of the " +
|
|
783
|
+
"strongest scope is reported.",
|
|
784
|
+
inputSchema: {
|
|
785
|
+
scope: z.string()
|
|
786
|
+
.describe("scope chain (e.g. \"session:<id>\", \"task:<id>\", \"project\", \"user\")")
|
|
787
|
+
},
|
|
788
|
+
outputSchema: {
|
|
789
|
+
entries: z.array(z.object({
|
|
790
|
+
key: z.string().describe("dotted configuration key"),
|
|
791
|
+
value: z.string().describe("effective configuration value"),
|
|
792
|
+
scope: z.string().describe("scope label which supplied the value")
|
|
793
|
+
})).describe("all effective configuration entries in lexicographic key order")
|
|
794
|
+
}
|
|
795
|
+
}, async (args) => {
|
|
796
|
+
try {
|
|
797
|
+
const scope = parseScope(args.scope);
|
|
798
|
+
const cfg = new Config("config", configSchema, this.log, scope);
|
|
799
|
+
cfg.read();
|
|
800
|
+
const entries = cfg.entries().map((e) => ({
|
|
801
|
+
key: e.key,
|
|
802
|
+
value: String(isScalar(e.value) ? e.value.value : e.value),
|
|
803
|
+
scope: Config.scopeLabel(e.scope)
|
|
804
|
+
}));
|
|
805
|
+
const result = { entries };
|
|
806
|
+
return {
|
|
807
|
+
structuredContent: result,
|
|
808
|
+
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
catch (err) {
|
|
812
|
+
return mcpToolError(err);
|
|
813
|
+
}
|
|
814
|
+
});
|
|
752
815
|
}
|
|
753
816
|
}
|
package/dst/ase-diagram.js
CHANGED
|
@@ -7,7 +7,7 @@ import fs from "node:fs";
|
|
|
7
7
|
import { InvalidArgumentError } from "commander";
|
|
8
8
|
import { renderMermaidASCII, renderMermaidSVG } from "beautiful-mermaid";
|
|
9
9
|
import { z } from "zod";
|
|
10
|
-
import
|
|
10
|
+
import { readStdin } from "./ase-stdio.js";
|
|
11
11
|
/* custom argument parser for Commander: non-negative integer */
|
|
12
12
|
const parseInteger = (name) => (value) => {
|
|
13
13
|
const n = Number.parseInt(value, 10);
|
|
@@ -150,6 +150,8 @@ export class Diagram {
|
|
|
150
150
|
clipping below are meaningful only for ASCII art) */
|
|
151
151
|
if (opts.format === "svg")
|
|
152
152
|
return renderMermaidSVG(src);
|
|
153
|
+
/* determine theme colors (gray shades once colors are enabled) */
|
|
154
|
+
const colored = opts.colorMode !== "none";
|
|
153
155
|
/* create diagram rendering */
|
|
154
156
|
let out = renderMermaidASCII(src, {
|
|
155
157
|
useAscii: opts.ascii,
|
|
@@ -157,20 +159,13 @@ export class Diagram {
|
|
|
157
159
|
paddingY: opts.nodeMarginY,
|
|
158
160
|
boxBorderPadding: opts.nodePadding,
|
|
159
161
|
colorMode: opts.colorMode,
|
|
160
|
-
theme:
|
|
162
|
+
theme: {
|
|
161
163
|
fg: "#000000",
|
|
162
|
-
border: "#a0a0a0",
|
|
163
|
-
junction: "#a0a0a0",
|
|
164
|
-
arrow: "#404040",
|
|
165
|
-
line: "#707070",
|
|
166
|
-
corner: "#707070"
|
|
167
|
-
} : {
|
|
168
|
-
fg: "#000000",
|
|
169
|
-
border: "#000000",
|
|
170
|
-
junction: "#000000",
|
|
171
|
-
arrow: "#000000",
|
|
172
|
-
line: "#000000",
|
|
173
|
-
corner: "#000000"
|
|
164
|
+
border: colored ? "#a0a0a0" : "#000000",
|
|
165
|
+
junction: colored ? "#a0a0a0" : "#000000",
|
|
166
|
+
arrow: colored ? "#404040" : "#000000",
|
|
167
|
+
line: colored ? "#707070" : "#000000",
|
|
168
|
+
corner: colored ? "#707070" : "#000000"
|
|
174
169
|
}
|
|
175
170
|
});
|
|
176
171
|
/* optionally clip diagram rendering */
|
|
@@ -248,7 +243,7 @@ export default class DiagramCommand {
|
|
|
248
243
|
}
|
|
249
244
|
}
|
|
250
245
|
else
|
|
251
|
-
src = await
|
|
246
|
+
src = await readStdin();
|
|
252
247
|
if (src.trim() === "") {
|
|
253
248
|
this.log.write("error", "diagram: empty Mermaid diagram specification");
|
|
254
249
|
process.exit(1);
|
package/dst/ase-getopt.js
CHANGED
|
@@ -6,6 +6,70 @@
|
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { Command, Option } from "commander";
|
|
8
8
|
import { parse as shParse, quote as shQuote } from "shell-quote";
|
|
9
|
+
/* tokenize a raw input string into [start,end) token ranges, preserving
|
|
10
|
+
the quoting so the original text can later be sliced verbatim */
|
|
11
|
+
const tokenizeRanges = (input) => {
|
|
12
|
+
const ranges = [];
|
|
13
|
+
let i = 0;
|
|
14
|
+
while (i < input.length) {
|
|
15
|
+
while (i < input.length && /\s/.test(input[i]))
|
|
16
|
+
i++;
|
|
17
|
+
if (i >= input.length)
|
|
18
|
+
break;
|
|
19
|
+
const start = i;
|
|
20
|
+
while (i < input.length && !/\s/.test(input[i])) {
|
|
21
|
+
const ch = input[i];
|
|
22
|
+
if (ch === "\"" || ch === "'") {
|
|
23
|
+
const quote = ch;
|
|
24
|
+
i++;
|
|
25
|
+
while (i < input.length && input[i] !== quote) {
|
|
26
|
+
if (input[i] === "\\" && i + 1 < input.length)
|
|
27
|
+
i++;
|
|
28
|
+
i++;
|
|
29
|
+
}
|
|
30
|
+
if (i < input.length)
|
|
31
|
+
i++;
|
|
32
|
+
}
|
|
33
|
+
else
|
|
34
|
+
i++;
|
|
35
|
+
}
|
|
36
|
+
ranges.push({ start, end: i });
|
|
37
|
+
}
|
|
38
|
+
return ranges;
|
|
39
|
+
};
|
|
40
|
+
/* strip surrounding quotes/escapes from a raw token range so it can be
|
|
41
|
+
compared against an option flag spelling */
|
|
42
|
+
const unquote = (s) => {
|
|
43
|
+
let out = "";
|
|
44
|
+
let j = 0;
|
|
45
|
+
while (j < s.length) {
|
|
46
|
+
const ch = s[j];
|
|
47
|
+
if (ch === "\"" || ch === "'") {
|
|
48
|
+
const quote = ch;
|
|
49
|
+
j++;
|
|
50
|
+
while (j < s.length && s[j] !== quote) {
|
|
51
|
+
if (s[j] === "\\" && j + 1 < s.length) {
|
|
52
|
+
out += s[j + 1];
|
|
53
|
+
j += 2;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
out += s[j];
|
|
57
|
+
j++;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
j++;
|
|
61
|
+
}
|
|
62
|
+
else if (ch === "\\" && j + 1 < s.length) {
|
|
63
|
+
out += s[j + 1];
|
|
64
|
+
j += 2;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
out += ch;
|
|
68
|
+
j++;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
};
|
|
9
73
|
/* MCP registration entry point for the option-parser tool */
|
|
10
74
|
export class GetoptMCP {
|
|
11
75
|
register(mcp) {
|
|
@@ -117,65 +181,7 @@ export class GetoptMCP {
|
|
|
117
181
|
let argsVerbatim = "";
|
|
118
182
|
if (argsRaw !== null) {
|
|
119
183
|
/* tokenize raw input into [start,end) ranges, preserving quotes */
|
|
120
|
-
const ranges =
|
|
121
|
-
let i = 0;
|
|
122
|
-
while (i < argsRaw.length) {
|
|
123
|
-
while (i < argsRaw.length && /\s/.test(argsRaw[i]))
|
|
124
|
-
i++;
|
|
125
|
-
if (i >= argsRaw.length)
|
|
126
|
-
break;
|
|
127
|
-
const start = i;
|
|
128
|
-
while (i < argsRaw.length && !/\s/.test(argsRaw[i])) {
|
|
129
|
-
const ch = argsRaw[i];
|
|
130
|
-
if (ch === "\"" || ch === "'") {
|
|
131
|
-
const quote = ch;
|
|
132
|
-
i++;
|
|
133
|
-
while (i < argsRaw.length && argsRaw[i] !== quote) {
|
|
134
|
-
if (argsRaw[i] === "\\" && i + 1 < argsRaw.length)
|
|
135
|
-
i++;
|
|
136
|
-
i++;
|
|
137
|
-
}
|
|
138
|
-
if (i < argsRaw.length)
|
|
139
|
-
i++;
|
|
140
|
-
}
|
|
141
|
-
else
|
|
142
|
-
i++;
|
|
143
|
-
}
|
|
144
|
-
ranges.push({ start, end: i });
|
|
145
|
-
}
|
|
146
|
-
/* helper function: strip surrounding quotes/escapes from a raw
|
|
147
|
-
range so it can be compared against an option flag spelling */
|
|
148
|
-
const unquote = (s) => {
|
|
149
|
-
let out = "";
|
|
150
|
-
let j = 0;
|
|
151
|
-
while (j < s.length) {
|
|
152
|
-
const ch = s[j];
|
|
153
|
-
if (ch === "\"" || ch === "'") {
|
|
154
|
-
const quote = ch;
|
|
155
|
-
j++;
|
|
156
|
-
while (j < s.length && s[j] !== quote) {
|
|
157
|
-
if (s[j] === "\\" && j + 1 < s.length) {
|
|
158
|
-
out += s[j + 1];
|
|
159
|
-
j += 2;
|
|
160
|
-
}
|
|
161
|
-
else {
|
|
162
|
-
out += s[j];
|
|
163
|
-
j++;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
j++;
|
|
167
|
-
}
|
|
168
|
-
else if (ch === "\\" && j + 1 < s.length) {
|
|
169
|
-
out += s[j + 1];
|
|
170
|
-
j += 2;
|
|
171
|
-
}
|
|
172
|
-
else {
|
|
173
|
-
out += ch;
|
|
174
|
-
j++;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
return out;
|
|
178
|
-
};
|
|
184
|
+
const ranges = tokenizeRanges(argsRaw);
|
|
179
185
|
/* walk the raw ranges, consuming leading option tokens (and any
|
|
180
186
|
separate value tokens they take) until the first positional
|
|
181
187
|
is reached, then slice the original input from there -- this
|
|
@@ -202,7 +208,7 @@ export class GetoptMCP {
|
|
|
202
208
|
argsVerbatim = argsRaw.slice(ranges[idx].start);
|
|
203
209
|
}
|
|
204
210
|
else
|
|
205
|
-
argsVerbatim = cmd.args
|
|
211
|
+
argsVerbatim = shQuote(cmd.args);
|
|
206
212
|
/* build markdown info rendering of parsed options */
|
|
207
213
|
const opts = cmd.opts();
|
|
208
214
|
const info = Object.entries(opts)
|
|
@@ -0,0 +1,89 @@
|
|
|
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 { isScalar } from "yaml";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { Config, configSchema, parseScope, agentClassification } from "./ase-config.js";
|
|
9
|
+
/* reusable functionality: ASE agent guidance level get/set */
|
|
10
|
+
export class Guidance {
|
|
11
|
+
/* allowed guidance level values */
|
|
12
|
+
static levels = agentClassification.guidance;
|
|
13
|
+
/* get the effective guidance level for an optional session;
|
|
14
|
+
returns the default "normal" if nothing is configured */
|
|
15
|
+
static get(log, session) {
|
|
16
|
+
const scope = parseScope(session !== undefined ? `session:${session}` : undefined);
|
|
17
|
+
const cfg = new Config("config", configSchema, log, scope);
|
|
18
|
+
cfg.read();
|
|
19
|
+
const val = cfg.get("agent.guidance");
|
|
20
|
+
if (val === undefined)
|
|
21
|
+
return "normal";
|
|
22
|
+
const level = String(isScalar(val) ? val.value : val);
|
|
23
|
+
return Guidance.levels.find((l) => l === level) ?? "normal";
|
|
24
|
+
}
|
|
25
|
+
/* set the guidance level on the strongest scope of an optional session */
|
|
26
|
+
static set(log, level, session) {
|
|
27
|
+
const scope = parseScope(session !== undefined ? `session:${session}` : undefined);
|
|
28
|
+
const cfg = new Config("config", configSchema, log, scope);
|
|
29
|
+
cfg.lock(() => {
|
|
30
|
+
cfg.read();
|
|
31
|
+
cfg.set("agent.guidance", level);
|
|
32
|
+
cfg.write();
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/* MCP registration entry point for guidance tools */
|
|
37
|
+
export default class GuidanceMCP {
|
|
38
|
+
log;
|
|
39
|
+
constructor(log) {
|
|
40
|
+
this.log = log;
|
|
41
|
+
}
|
|
42
|
+
register(mcp) {
|
|
43
|
+
mcp.registerTool("ase_guidance", {
|
|
44
|
+
title: "ASE guidance level get/set",
|
|
45
|
+
description: "Get or set the active ASE agent guidance `level`. " +
|
|
46
|
+
"If `level` is provided, it sets the guidance level, " +
|
|
47
|
+
"otherwise it returns the current guidance `level`. " +
|
|
48
|
+
"If `session` is provided, the operation is scoped to that session, " +
|
|
49
|
+
"otherwise it operates on the strongest/closest scope (user/project cascade). " +
|
|
50
|
+
"The guidance level controls the amount of unsolicited help hints -- " +
|
|
51
|
+
"pointers to the available ASE skills, their options, and the recommended " +
|
|
52
|
+
"next operations -- the agent emits. " +
|
|
53
|
+
"Allowed levels: \"none\" (no help hints at all), " +
|
|
54
|
+
"\"minimal\" (the single most essential help hint only), " +
|
|
55
|
+
"\"normal\" (the essential help hints only), " +
|
|
56
|
+
"\"verbose\" (all available help hints).",
|
|
57
|
+
inputSchema: {
|
|
58
|
+
level: z.enum(Guidance.levels).optional()
|
|
59
|
+
.describe("guidance level to set; if omitted, the current guidance level is returned"),
|
|
60
|
+
session: z.string().regex(/^[A-Za-z0-9._-]+$/).optional()
|
|
61
|
+
.describe("session identifier (allowed characters: A-Z, a-z, 0-9, '.', '_', '-'); " +
|
|
62
|
+
"if omitted, the operation is not scoped to a specific session")
|
|
63
|
+
}
|
|
64
|
+
}, async (args) => {
|
|
65
|
+
try {
|
|
66
|
+
if (args.level !== undefined) {
|
|
67
|
+
Guidance.set(this.log, args.level, args.session);
|
|
68
|
+
const where = args.session !== undefined ?
|
|
69
|
+
` for session "${args.session}"` : "";
|
|
70
|
+
const msg = `OK: set agent.guidance to "${args.level}"${where}`;
|
|
71
|
+
return {
|
|
72
|
+
content: [{ type: "text", text: msg }]
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const text = Guidance.get(this.log, args.session);
|
|
76
|
+
return {
|
|
77
|
+
content: [{ type: "text", text }]
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
82
|
+
return {
|
|
83
|
+
isError: true,
|
|
84
|
+
content: [{ type: "text", text: `ERROR: ${message}` }]
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
package/dst/ase-hook.js
CHANGED
|
@@ -8,11 +8,10 @@ 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";
|
|
12
11
|
import * as v from "valibot";
|
|
13
12
|
import Version from "./ase-version.js";
|
|
14
13
|
import { Config, configSchema, parseScope } from "./ase-config.js";
|
|
15
|
-
import { writeStdout } from "./ase-
|
|
14
|
+
import { readStdin, writeStdout } from "./ase-stdio.js";
|
|
16
15
|
const addonMcpServers = [
|
|
17
16
|
"chat-alibaba-qwen",
|
|
18
17
|
"chat-deepseek",
|
|
@@ -86,14 +85,9 @@ export default class HookCommand {
|
|
|
86
85
|
isValidSessionId(id) {
|
|
87
86
|
return /^[A-Za-z0-9._-]+$/.test(id);
|
|
88
87
|
}
|
|
89
|
-
/* read the entire stdin payload asynchronously */
|
|
90
|
-
readStdin() {
|
|
91
|
-
/* best-effort: treat an unreadable/closed stdin as empty */
|
|
92
|
-
return getStdin().catch(() => "");
|
|
93
|
-
}
|
|
94
88
|
/* drain and discard the stdin event payload */
|
|
95
89
|
async drainStdin() {
|
|
96
|
-
await
|
|
90
|
+
await readStdin().catch(() => "");
|
|
97
91
|
}
|
|
98
92
|
/* best-effort JSON parse with valibot schema validation: returns
|
|
99
93
|
an empty object on blank input, malformed JSON, or schema
|
|
@@ -211,7 +205,7 @@ export default class HookCommand {
|
|
|
211
205
|
const versionHint = versionHints.length > 0 ? "(" + versionHints.join(", ") + ")" : "";
|
|
212
206
|
/* read session information (Anthropic Claude Code CLI uses snake_case fields,
|
|
213
207
|
GitHub Copilot CLI uses camelCase fields) */
|
|
214
|
-
const stdin = await
|
|
208
|
+
const stdin = await readStdin().catch(() => "");
|
|
215
209
|
const input = this.parseJSON(stdin, v.object({
|
|
216
210
|
session_id: v.optional(v.string()),
|
|
217
211
|
sessionId: v.optional(v.string()),
|
|
@@ -255,8 +249,9 @@ export default class HookCommand {
|
|
|
255
249
|
const val = cfg.get(key);
|
|
256
250
|
return typeof val === "string" ? val : (process.env[envVar] ?? dflt);
|
|
257
251
|
};
|
|
258
|
-
/* determine agent persona style and project boxing transparency */
|
|
252
|
+
/* determine agent persona style, agent guidance level, and project boxing transparency */
|
|
259
253
|
const persona = setting("agent.persona", "ASE_PERSONA_STYLE", "engineer");
|
|
254
|
+
const guidance = setting("agent.guidance", "ASE_GUIDANCE_LEVEL", "normal");
|
|
260
255
|
const boxing = setting("project.boxing", "ASE_PROJECT_BOXING", "white");
|
|
261
256
|
/* determine headless mode */
|
|
262
257
|
const headless = (process.env.ASE_HEADLESS ?? "false") === "true" ? "true" : "false";
|
|
@@ -273,7 +268,13 @@ export default class HookCommand {
|
|
|
273
268
|
`export ASE_SESSION_ID=${quote([sessionId])}\n` +
|
|
274
269
|
`export ASE_HEADLESS=${quote([headless])}\n` +
|
|
275
270
|
`export ASE_AGENT_TOOL=${quote([tool])}\n`;
|
|
276
|
-
|
|
271
|
+
try {
|
|
272
|
+
fs.appendFileSync(envFile, script, "utf8");
|
|
273
|
+
}
|
|
274
|
+
catch (err) {
|
|
275
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
276
|
+
this.log.write("warning", `hook: failed to write environment file: ${message}`);
|
|
277
|
+
}
|
|
277
278
|
}
|
|
278
279
|
/* prepend ASE information to constitution markdown */
|
|
279
280
|
md =
|
|
@@ -281,6 +282,7 @@ export default class HookCommand {
|
|
|
281
282
|
`<ase-version-hint>${versionHint}</ase-version-hint>\n` +
|
|
282
283
|
`<ase-plugin-root>${pluginRoot}</ase-plugin-root>\n` +
|
|
283
284
|
`<ase-persona-style>${persona}</ase-persona-style>\n` +
|
|
285
|
+
`<ase-guidance-level>${guidance}</ase-guidance-level>\n` +
|
|
284
286
|
`<ase-user-id>${userId}</ase-user-id>\n` +
|
|
285
287
|
`<ase-project-id>${projectId}</ase-project-id>\n` +
|
|
286
288
|
`<ase-project-boxing>${boxing}</ase-project-boxing>\n` +
|
|
@@ -295,11 +297,18 @@ export default class HookCommand {
|
|
|
295
297
|
agent harness, independent of any model decision, so it is
|
|
296
298
|
guaranteed to appear once in every non-headless session;
|
|
297
299
|
Anthropic Claude Code CLI and OpenAI Codex CLI surface a top-level
|
|
298
|
-
"systemMessage" field for this -- GitHub Copilot CLI has no equivalent)
|
|
299
|
-
|
|
300
|
+
"systemMessage" field for this -- GitHub Copilot CLI has no equivalent);
|
|
301
|
+
the trailing help hint is emitted only if the guidance level asks for it */
|
|
302
|
+
const banner = "\n" +
|
|
303
|
+
`\n⧉ ASE: ⎈ version: ${versionCurrentPlugin}${versionHint !== "" ? " " + versionHint.replaceAll(/\*/g, "") : ""}` +
|
|
300
304
|
`\n⧉ ASE: ※ user: ${userId}, ⚑ project: ${projectId}` +
|
|
301
305
|
`\n⧉ ASE: ◉ task: ${taskId}, ⏻ session: ${sessionId}` +
|
|
302
|
-
`\n⧉ ASE: ☯ persona: ${persona}, ▢ boxing: ${boxing}
|
|
306
|
+
`\n⧉ ASE: ☯ persona: ${persona}, ▶ guidance: ${guidance}, ▢ boxing: ${boxing}` +
|
|
307
|
+
(guidance === "normal" || guidance === "verbose" ?
|
|
308
|
+
"\n" +
|
|
309
|
+
"\n⧉ ASE: ▷ hint: use \"/ase-help-intent <intent-description>\" for skill command proposal" +
|
|
310
|
+
"\n⧉ ASE: ▷ hint: use \"/ase-help-skill [<skill-name>]\" for skill catalog or skill manpage" : "") +
|
|
311
|
+
"\n";
|
|
303
312
|
/* inject markdown into session context.
|
|
304
313
|
Anthropic Claude Code CLI and OpenAI Codex CLI expect the context nested in
|
|
305
314
|
"hookSpecificOutput"; GitHub Copilot CLI expects a flat top-level
|
|
@@ -314,8 +323,10 @@ export default class HookCommand {
|
|
|
314
323
|
};
|
|
315
324
|
/* attach the deterministic banner as a top-level "systemMessage"
|
|
316
325
|
(only for the harnesses that support it and only when not
|
|
317
|
-
running headless --
|
|
318
|
-
|
|
326
|
+
running headless -- complementing the constitution box condition,
|
|
327
|
+
which covers exactly the remaining harness GitHub Copilot CLI
|
|
328
|
+
by letting the model emit the banner itself) */
|
|
329
|
+
if ((tool === "claude" || tool === "codex") && headless !== "true" && guidance !== "none")
|
|
319
330
|
payload.systemMessage = banner;
|
|
320
331
|
await writeStdout(JSON.stringify(payload));
|
|
321
332
|
return 0;
|
|
@@ -371,7 +382,7 @@ export default class HookCommand {
|
|
|
371
382
|
}
|
|
372
383
|
/* read session id from stdin JSON payload */
|
|
373
384
|
async readSessionIdFromStdin() {
|
|
374
|
-
const stdin = await
|
|
385
|
+
const stdin = await readStdin().catch(() => "");
|
|
375
386
|
const input = this.parseJSON(stdin, v.object({
|
|
376
387
|
session_id: v.optional(v.string()),
|
|
377
388
|
sessionId: v.optional(v.string())
|
|
@@ -458,7 +469,7 @@ export default class HookCommand {
|
|
|
458
469
|
loosely-typed input object shared by the tool-approval handlers */
|
|
459
470
|
async readHookInput(tool) {
|
|
460
471
|
const spec = toolSpecs[tool];
|
|
461
|
-
const stdin = await
|
|
472
|
+
const stdin = await readStdin().catch(() => "");
|
|
462
473
|
const input = this.parseJSON(stdin, v.looseObject({
|
|
463
474
|
session_id: v.optional(v.string()),
|
|
464
475
|
sessionId: v.optional(v.string())
|
|
@@ -543,53 +554,22 @@ export default class HookCommand {
|
|
|
543
554
|
hookCmd.outputHelp();
|
|
544
555
|
process.exit(1);
|
|
545
556
|
});
|
|
546
|
-
/* register CLI sub-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
hookCmd
|
|
564
|
-
.command("pre-tool-use")
|
|
565
|
-
.description("handle tool PreToolUse hook event")
|
|
566
|
-
.option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
|
|
567
|
-
.action(async (opts) => {
|
|
568
|
-
process.exitCode = await this.doPreToolUse(this.parseTool(opts.tool));
|
|
569
|
-
});
|
|
570
|
-
/* register CLI sub-command "ase hook permission-request" */
|
|
571
|
-
hookCmd
|
|
572
|
-
.command("permission-request")
|
|
573
|
-
.description("handle tool PermissionRequest hook event (Codex CLI)")
|
|
574
|
-
.option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
|
|
575
|
-
.action(async (opts) => {
|
|
576
|
-
process.exitCode = await this.doPermissionRequest(this.parseTool(opts.tool));
|
|
577
|
-
});
|
|
578
|
-
/* register CLI sub-command "ase hook user-prompt-submit" */
|
|
579
|
-
hookCmd
|
|
580
|
-
.command("user-prompt-submit")
|
|
581
|
-
.description("handle UserPromptSubmit hook event (mark agent as busy)")
|
|
582
|
-
.option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
|
|
583
|
-
.action(async (opts) => {
|
|
584
|
-
process.exitCode = await this.doUserPromptSubmit(this.parseTool(opts.tool));
|
|
585
|
-
});
|
|
586
|
-
/* register CLI sub-command "ase hook stop" */
|
|
587
|
-
hookCmd
|
|
588
|
-
.command("stop")
|
|
589
|
-
.description("handle Stop hook event (mark agent as ready)")
|
|
590
|
-
.option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
|
|
591
|
-
.action(async (opts) => {
|
|
592
|
-
process.exitCode = await this.doStop(this.parseTool(opts.tool));
|
|
593
|
-
});
|
|
557
|
+
/* register CLI sub-commands "ase hook <event>" */
|
|
558
|
+
const subCmds = [
|
|
559
|
+
{ name: "session-start", desc: "handle SessionStart hook event", handler: (tool) => this.doSessionStart(tool) },
|
|
560
|
+
{ name: "session-end", desc: "handle SessionEnd hook event", handler: (tool) => this.doSessionEnd(tool) },
|
|
561
|
+
{ name: "pre-tool-use", desc: "handle tool PreToolUse hook event", handler: (tool) => this.doPreToolUse(tool) },
|
|
562
|
+
{ name: "permission-request", desc: "handle tool PermissionRequest hook event", handler: (tool) => this.doPermissionRequest(tool) },
|
|
563
|
+
{ name: "user-prompt-submit", desc: "handle UserPromptSubmit hook event", handler: (tool) => this.doUserPromptSubmit(tool) },
|
|
564
|
+
{ name: "stop", desc: "handle Stop hook event", handler: (tool) => this.doStop(tool) }
|
|
565
|
+
];
|
|
566
|
+
for (const { name, desc, handler } of subCmds)
|
|
567
|
+
hookCmd
|
|
568
|
+
.command(name)
|
|
569
|
+
.description(desc)
|
|
570
|
+
.option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
|
|
571
|
+
.action(async (opts) => {
|
|
572
|
+
process.exitCode = await handler(this.parseTool(opts.tool));
|
|
573
|
+
});
|
|
594
574
|
}
|
|
595
575
|
}
|