@rse/ase 0.9.5 → 0.9.7
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 +331 -0
- package/dst/ase-config.js +56 -10
- package/dst/ase-markdown.js +235 -0
- package/dst/ase-service.js +4 -0
- package/dst/ase-task.js +142 -64
- package/dst/ase.js +2 -0
- package/package.json +9 -7
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.github/plugin/plugin.json +1 -1
- package/plugin/meta/ase-format-meta.md +5 -2
- package/plugin/meta/{ase-format-plan.md → ase-format-task.md} +18 -17
- package/plugin/package.json +2 -2
- package/plugin/skills/ase-code-craft/SKILL.md +8 -8
- package/plugin/skills/ase-code-craft/help.md +2 -2
- package/plugin/skills/ase-code-refactor/SKILL.md +8 -8
- package/plugin/skills/ase-code-refactor/help.md +2 -2
- package/plugin/skills/ase-code-resolve/SKILL.md +8 -8
- package/plugin/skills/ase-code-resolve/help.md +2 -2
- package/plugin/skills/ase-task-condense/SKILL.md +1 -1
- package/plugin/skills/ase-task-delete/help.md +0 -4
- package/plugin/skills/ase-task-edit/SKILL.md +9 -8
- package/plugin/skills/ase-task-edit/help.md +1 -5
- package/plugin/skills/ase-task-grill/SKILL.md +8 -2
- package/plugin/skills/ase-task-grill/help.md +0 -4
- package/plugin/skills/ase-task-implement/SKILL.md +3 -3
- package/plugin/skills/ase-task-implement/help.md +7 -8
- package/plugin/skills/ase-task-preflight/SKILL.md +3 -3
- package/plugin/skills/ase-task-reboot/SKILL.md +6 -6
- package/plugin/skills/ase-task-view/SKILL.md +19 -8
- package/dst/ase-bash.js +0 -618
- package/dst/ase-hello.js +0 -24
|
@@ -0,0 +1,331 @@
|
|
|
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
|
+
import path from "node:path";
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import picomatch from "picomatch";
|
|
9
|
+
import { isScalar } from "yaml";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { Config, configSchema } from "./ase-config.js";
|
|
12
|
+
import { Task } from "./ase-task.js";
|
|
13
|
+
/* the recognized artifact kinds, in descending precedence order;
|
|
14
|
+
"othr" is the implicit catch-all and is always resolved last */
|
|
15
|
+
export const artifactKinds = ["spec", "arch", "code", "docs", "infr", "othr"];
|
|
16
|
+
/* the five configured kinds (i.e. all kinds except the implicit "othr") */
|
|
17
|
+
const configuredKinds = ["spec", "arch", "code", "docs", "infr"];
|
|
18
|
+
/* reusable functionality: resolve artifact kinds to project-relative
|
|
19
|
+
file lists, driven by the "project.artifact.<kind>" configuration */
|
|
20
|
+
export class Artifact {
|
|
21
|
+
/* validate a requested kind against the known set */
|
|
22
|
+
static validateKind(kind) {
|
|
23
|
+
if (!artifactKinds.includes(kind))
|
|
24
|
+
throw new Error(`artifact: unknown kind "${kind}" ` +
|
|
25
|
+
`(expected one of: ${artifactKinds.join(", ")})`);
|
|
26
|
+
return kind;
|
|
27
|
+
}
|
|
28
|
+
/* translate a single ".gitignore" line into a picomatch-backed rule,
|
|
29
|
+
honoring the anchored-vs-floating, directory-only, and negation
|
|
30
|
+
semantics of ".gitignore" patterns */
|
|
31
|
+
static compileIgnoreRule(line) {
|
|
32
|
+
let pattern = line.trim();
|
|
33
|
+
if (pattern === "" || pattern.startsWith("#"))
|
|
34
|
+
return null;
|
|
35
|
+
let negated = false;
|
|
36
|
+
if (pattern.startsWith("!")) {
|
|
37
|
+
negated = true;
|
|
38
|
+
pattern = pattern.slice(1);
|
|
39
|
+
}
|
|
40
|
+
let dirOnly = false;
|
|
41
|
+
if (pattern.endsWith("/")) {
|
|
42
|
+
dirOnly = true;
|
|
43
|
+
pattern = pattern.slice(0, -1);
|
|
44
|
+
}
|
|
45
|
+
const anchored = pattern.includes("/");
|
|
46
|
+
if (pattern.startsWith("/"))
|
|
47
|
+
pattern = pattern.slice(1);
|
|
48
|
+
const glob = anchored ? pattern : `**/${pattern}`;
|
|
49
|
+
const isMatch = picomatch(glob, { dot: true });
|
|
50
|
+
return { matcher: (p) => isMatch(p), negated, dirOnly };
|
|
51
|
+
}
|
|
52
|
+
/* load the ".gitignore" rules located directly in a directory */
|
|
53
|
+
static loadIgnoreRules(dir) {
|
|
54
|
+
const file = path.join(dir, ".gitignore");
|
|
55
|
+
if (!fs.existsSync(file))
|
|
56
|
+
return [];
|
|
57
|
+
const rules = [];
|
|
58
|
+
for (const line of fs.readFileSync(file, "utf8").split(/\r?\n/)) {
|
|
59
|
+
const rule = Artifact.compileIgnoreRule(line);
|
|
60
|
+
if (rule !== null)
|
|
61
|
+
rules.push(rule);
|
|
62
|
+
}
|
|
63
|
+
return rules;
|
|
64
|
+
}
|
|
65
|
+
/* decide whether a project-relative path is ignored by the given
|
|
66
|
+
ordered ".gitignore" rule set (last matching rule wins) */
|
|
67
|
+
static isIgnored(rel, isDir, rules) {
|
|
68
|
+
let ignored = false;
|
|
69
|
+
for (const rule of rules) {
|
|
70
|
+
if (rule.dirOnly && !isDir)
|
|
71
|
+
continue;
|
|
72
|
+
if (rule.matcher(rel))
|
|
73
|
+
ignored = !rule.negated;
|
|
74
|
+
}
|
|
75
|
+
return ignored;
|
|
76
|
+
}
|
|
77
|
+
/* build the file universe by walking the project tree from the
|
|
78
|
+
project root, honoring ".gitignore" rules (root plus nested) and
|
|
79
|
+
always pruning ".git". Yields POSIX project-relative, sorted,
|
|
80
|
+
de-duplicated file paths */
|
|
81
|
+
static universe() {
|
|
82
|
+
const root = Task.projectRoot();
|
|
83
|
+
const files = new Set();
|
|
84
|
+
const walk = (dir, relDir, inherited) => {
|
|
85
|
+
const rules = [...inherited, ...Artifact.loadIgnoreRules(dir)];
|
|
86
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
87
|
+
if (entry.name === ".git")
|
|
88
|
+
continue;
|
|
89
|
+
const rel = relDir === "" ? entry.name : `${relDir}/${entry.name}`;
|
|
90
|
+
const isDir = entry.isDirectory();
|
|
91
|
+
if (Artifact.isIgnored(rel, isDir, rules))
|
|
92
|
+
continue;
|
|
93
|
+
if (isDir)
|
|
94
|
+
walk(path.join(dir, entry.name), rel, rules);
|
|
95
|
+
else if (entry.isFile())
|
|
96
|
+
files.add(rel);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
walk(root, "", []);
|
|
100
|
+
return [...files].sort((a, b) => a.localeCompare(b));
|
|
101
|
+
}
|
|
102
|
+
/* raw-resolve a single kind's configuration spec against the file
|
|
103
|
+
universe via "seed-then-mutate" miniglob semantics: the spec is a
|
|
104
|
+
whitespace-separated list of tokens. */
|
|
105
|
+
static rawResolve(spec, all) {
|
|
106
|
+
const tokens = spec.split(/\s+/).filter((t) => t !== "");
|
|
107
|
+
if (tokens.length === 0)
|
|
108
|
+
return new Set();
|
|
109
|
+
const result = new Set(tokens[0].startsWith("!") ? all : []);
|
|
110
|
+
for (const token of tokens) {
|
|
111
|
+
const negated = token.startsWith("!");
|
|
112
|
+
const glob = negated ? token.slice(1) : token;
|
|
113
|
+
if (glob === "")
|
|
114
|
+
continue;
|
|
115
|
+
const isMatch = picomatch(glob, { dot: true });
|
|
116
|
+
for (const file of all) {
|
|
117
|
+
if (!isMatch(file))
|
|
118
|
+
continue;
|
|
119
|
+
if (negated)
|
|
120
|
+
result.delete(file);
|
|
121
|
+
else
|
|
122
|
+
result.add(file);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
/* read a single scalar configuration value as a plain string */
|
|
128
|
+
static configString(cfg, key) {
|
|
129
|
+
const val = cfg.get(key);
|
|
130
|
+
if (val === undefined)
|
|
131
|
+
return "";
|
|
132
|
+
return String(isScalar(val) ? val.value : val);
|
|
133
|
+
}
|
|
134
|
+
/* read the configured "basedir" anchor and "files" miniglob spec
|
|
135
|
+
for a single kind; "basedir" is project-root-relative (POSIX,
|
|
136
|
+
"" ≡ project root) and "files" resolves relative to "basedir" */
|
|
137
|
+
static spec(log, kind) {
|
|
138
|
+
const cfg = new Config("config", configSchema, log);
|
|
139
|
+
cfg.read();
|
|
140
|
+
const basedir = Artifact.configString(cfg, `project.artifact.${kind}.basedir`)
|
|
141
|
+
.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "")
|
|
142
|
+
.replace(/^\.$/, "");
|
|
143
|
+
const files = Artifact.configString(cfg, `project.artifact.${kind}.files`);
|
|
144
|
+
return { basedir, files };
|
|
145
|
+
}
|
|
146
|
+
/* raw-resolve a single kind's "basedir"/"files" spec against the
|
|
147
|
+
file universe: the "files" miniglob resolves relative to
|
|
148
|
+
"basedir", then matches are re-prefixed with "basedir" to stay
|
|
149
|
+
project-relative */
|
|
150
|
+
static resolveKind(basedir, files, all) {
|
|
151
|
+
if (basedir === "")
|
|
152
|
+
return Artifact.rawResolve(files, all);
|
|
153
|
+
const prefix = `${basedir}/`;
|
|
154
|
+
const local = all
|
|
155
|
+
.filter((file) => file.startsWith(prefix))
|
|
156
|
+
.map((file) => file.slice(prefix.length));
|
|
157
|
+
const result = new Set();
|
|
158
|
+
for (const file of Artifact.rawResolve(files, local))
|
|
159
|
+
result.add(`${prefix}${file}`);
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
/* resolve the requested kinds to project-relative file lists */
|
|
163
|
+
static list(log, kinds) {
|
|
164
|
+
const all = Artifact.universe();
|
|
165
|
+
/* raw-resolve all five configured kinds */
|
|
166
|
+
const raw = new Map();
|
|
167
|
+
for (const kind of configuredKinds) {
|
|
168
|
+
const { basedir, files } = Artifact.spec(log, kind);
|
|
169
|
+
raw.set(kind, Artifact.resolveKind(basedir, files, all));
|
|
170
|
+
}
|
|
171
|
+
/* partition the universe by descending precedence */
|
|
172
|
+
const claimed = new Set();
|
|
173
|
+
const part = new Map();
|
|
174
|
+
for (const kind of configuredKinds) {
|
|
175
|
+
const own = [];
|
|
176
|
+
for (const file of raw.get(kind).size > 0 ? all : []) {
|
|
177
|
+
if (raw.get(kind).has(file) && !claimed.has(file)) {
|
|
178
|
+
own.push(file);
|
|
179
|
+
claimed.add(file);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
part.set(kind, own);
|
|
183
|
+
}
|
|
184
|
+
part.set("othr", all.filter((file) => !claimed.has(file)));
|
|
185
|
+
/* project onto the requested kinds */
|
|
186
|
+
return kinds.map((kind) => ({
|
|
187
|
+
kind,
|
|
188
|
+
files: part.get(kind) ?? []
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
191
|
+
/* resolve a base-relative "filename" within a kind's "basedir" to a
|
|
192
|
+
project-root-relative POSIX path; the implicit "othr" catch-all
|
|
193
|
+
has no configured "basedir" and is therefore rejected */
|
|
194
|
+
static name(log, kind, filename) {
|
|
195
|
+
if (kind === "othr")
|
|
196
|
+
throw new Error("artifact: kind \"othr\" has no configured basedir");
|
|
197
|
+
const file = filename.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
198
|
+
if (file === "")
|
|
199
|
+
throw new Error("artifact: filename must not be empty");
|
|
200
|
+
const { basedir } = Artifact.spec(log, kind);
|
|
201
|
+
return basedir === "" ? file : `${basedir}/${file}`;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/* CLI command "ase artifact" */
|
|
205
|
+
export default class ArtifactCommand {
|
|
206
|
+
log;
|
|
207
|
+
constructor(log) {
|
|
208
|
+
this.log = log;
|
|
209
|
+
}
|
|
210
|
+
/* register commands */
|
|
211
|
+
register(program) {
|
|
212
|
+
/* register CLI top-level command "ase artifact" */
|
|
213
|
+
const artifact = program
|
|
214
|
+
.command("artifact")
|
|
215
|
+
.description("Resolve project artifact kinds to project-relative file lists")
|
|
216
|
+
.action(() => {
|
|
217
|
+
artifact.outputHelp();
|
|
218
|
+
process.exit(1);
|
|
219
|
+
});
|
|
220
|
+
/* register CLI sub-command "ase artifact list" */
|
|
221
|
+
artifact
|
|
222
|
+
.command("list")
|
|
223
|
+
.description("Resolve one or more artifact kinds to project-relative file paths")
|
|
224
|
+
.option("--kind <kinds>", "comma-separated list of artifact kinds " +
|
|
225
|
+
`(${artifactKinds.join("|")})`, artifactKinds.join(","))
|
|
226
|
+
.action((opts) => {
|
|
227
|
+
const kinds = opts.kind.split(",").map((k) => Artifact.validateKind(k.trim()));
|
|
228
|
+
const result = Artifact.list(this.log, kinds);
|
|
229
|
+
const single = result.length === 1;
|
|
230
|
+
for (const { kind, files } of result) {
|
|
231
|
+
if (!single)
|
|
232
|
+
process.stdout.write(`# ${kind}:\n`);
|
|
233
|
+
for (const file of files)
|
|
234
|
+
process.stdout.write(`- ${file}\n`);
|
|
235
|
+
}
|
|
236
|
+
process.exit(0);
|
|
237
|
+
});
|
|
238
|
+
/* register CLI sub-command "ase artifact name" */
|
|
239
|
+
artifact
|
|
240
|
+
.command("name")
|
|
241
|
+
.description("Resolve a base-relative filename within an artifact kind to a project-relative path")
|
|
242
|
+
.argument("<filename>", "base-relative filename within the kind's basedir")
|
|
243
|
+
.option("--kind <kind>", "artifact kind " +
|
|
244
|
+
`(${configuredKinds.join("|")})`, "code")
|
|
245
|
+
.action((filename, opts) => {
|
|
246
|
+
const kind = Artifact.validateKind(opts.kind.trim());
|
|
247
|
+
process.stdout.write(`${Artifact.name(this.log, kind, filename)}\n`);
|
|
248
|
+
process.exit(0);
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/* MCP registration entry point for artifact tools */
|
|
253
|
+
export class ArtifactMCP {
|
|
254
|
+
log;
|
|
255
|
+
constructor(log) {
|
|
256
|
+
this.log = log;
|
|
257
|
+
}
|
|
258
|
+
/* register MCP tools */
|
|
259
|
+
register(mcp) {
|
|
260
|
+
mcp.registerTool("ase_artifact_list", {
|
|
261
|
+
title: "ASE artifact list",
|
|
262
|
+
description: "Resolve one or more artifact `kind`s to project-relative file lists. " +
|
|
263
|
+
"Recognized kinds are `spec`, `arch`, `code`, `docs`, `infr`, and `othr`. " +
|
|
264
|
+
"Returns an `artifacts` array of `{ kind, files }` objects. " +
|
|
265
|
+
"If `kind` is omitted or empty, all kinds are resolved.",
|
|
266
|
+
inputSchema: {
|
|
267
|
+
kind: z.array(z.string()).optional()
|
|
268
|
+
.describe("list of artifact kinds (`spec`, `arch`, `code`, `docs`, `infr`, " +
|
|
269
|
+
"`othr`); if omitted or empty, all kinds are resolved")
|
|
270
|
+
},
|
|
271
|
+
outputSchema: {
|
|
272
|
+
artifacts: z.array(z.object({
|
|
273
|
+
kind: z.string().describe("artifact kind"),
|
|
274
|
+
files: z.array(z.string()).describe("project-relative file paths for the kind")
|
|
275
|
+
})).describe("resolved artifacts, one entry per requested kind")
|
|
276
|
+
}
|
|
277
|
+
}, async (args) => {
|
|
278
|
+
try {
|
|
279
|
+
const requested = args.kind !== undefined && args.kind.length > 0 ?
|
|
280
|
+
args.kind : [...artifactKinds];
|
|
281
|
+
const kinds = requested.map((k) => Artifact.validateKind(k));
|
|
282
|
+
const result = { artifacts: Artifact.list(this.log, kinds) };
|
|
283
|
+
return {
|
|
284
|
+
structuredContent: result,
|
|
285
|
+
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
290
|
+
return {
|
|
291
|
+
isError: true,
|
|
292
|
+
content: [{ type: "text", text: `ERROR: ${message}` }]
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
mcp.registerTool("ase_artifact_name", {
|
|
297
|
+
title: "ASE artifact name",
|
|
298
|
+
description: "Resolve a base-relative `filename` within an artifact `kind` to a project-relative path " +
|
|
299
|
+
"by prefixing it with the kind's configured `basedir`. " +
|
|
300
|
+
"Recognized kinds are `spec`, `arch`, `code`, `docs`, and `infr` " +
|
|
301
|
+
"(the implicit `othr` catch-all has no basedir and is rejected). " +
|
|
302
|
+
"If `kind` is omitted, it defaults to `code`. " +
|
|
303
|
+
"Returns the resolved path as `name`.",
|
|
304
|
+
inputSchema: {
|
|
305
|
+
kind: z.string().optional()
|
|
306
|
+
.describe("artifact kind (`spec`, `arch`, `code`, `docs`, `infr`); defaults to `code`"),
|
|
307
|
+
filename: z.string()
|
|
308
|
+
.describe("base-relative filename within the kind's basedir")
|
|
309
|
+
},
|
|
310
|
+
outputSchema: {
|
|
311
|
+
name: z.string().describe("project-relative file path")
|
|
312
|
+
}
|
|
313
|
+
}, async (args) => {
|
|
314
|
+
try {
|
|
315
|
+
const kind = Artifact.validateKind(args.kind ?? "code");
|
|
316
|
+
const result = { name: Artifact.name(this.log, kind, args.filename) };
|
|
317
|
+
return {
|
|
318
|
+
structuredContent: result,
|
|
319
|
+
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
catch (err) {
|
|
323
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
324
|
+
return {
|
|
325
|
+
isError: true,
|
|
326
|
+
content: [{ type: "text", text: `ERROR: ${message}` }]
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
package/dst/ase-config.js
CHANGED
|
@@ -25,29 +25,41 @@ export const agentClassification = {
|
|
|
25
25
|
/* classification presets */
|
|
26
26
|
export const projectClassificationPresets = {
|
|
27
27
|
vibe: {
|
|
28
|
+
"agent.persona": "writer",
|
|
28
29
|
"project.id": "example",
|
|
29
30
|
"project.name": "Example Project",
|
|
30
|
-
"project.boxing": "black"
|
|
31
|
-
"agent.persona": "writer"
|
|
31
|
+
"project.boxing": "black"
|
|
32
32
|
},
|
|
33
33
|
pro: {
|
|
34
|
+
"agent.persona": "engineer",
|
|
34
35
|
"project.id": "example",
|
|
35
36
|
"project.name": "Example Project",
|
|
36
|
-
"project.boxing": "white"
|
|
37
|
-
"agent.persona": "engineer"
|
|
37
|
+
"project.boxing": "white"
|
|
38
38
|
},
|
|
39
39
|
default: {
|
|
40
|
+
"agent.task": "default",
|
|
41
|
+
"agent.persona": "engineer",
|
|
40
42
|
"project.id": "example",
|
|
41
43
|
"project.name": "Example Project",
|
|
42
44
|
"project.boxing": "white",
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
+
"project.artifact.task.basedir": ".ase/task",
|
|
46
|
+
"project.artifact.task.files": "*.md",
|
|
47
|
+
"project.artifact.spec.basedir": "doc/spec",
|
|
48
|
+
"project.artifact.spec.files": "*.{md,txt}",
|
|
49
|
+
"project.artifact.arch.basedir": "doc/arch",
|
|
50
|
+
"project.artifact.arch.files": "*.{md,txt}",
|
|
51
|
+
"project.artifact.code.basedir": "src",
|
|
52
|
+
"project.artifact.code.files": "** !**/etc/** !**/{.gitignore,.npmignore,package.json}",
|
|
53
|
+
"project.artifact.docs.basedir": "doc",
|
|
54
|
+
"project.artifact.docs.files": "** **/{README,LICENSE,CHANGELOG}.{md,txt} !{spec,arch}/**",
|
|
55
|
+
"project.artifact.infr.basedir": "",
|
|
56
|
+
"project.artifact.infr.files": "**/{.github,.claude*,etc}/** **/{AGENTS.md,{package,tsconfig*}.json,.{git,npm}ignore}"
|
|
45
57
|
},
|
|
46
58
|
industry: {
|
|
59
|
+
"agent.persona": "engineer",
|
|
47
60
|
"project.id": "example",
|
|
48
61
|
"project.name": "Example Project",
|
|
49
|
-
"project.boxing": "grey"
|
|
50
|
-
"agent.persona": "engineer"
|
|
62
|
+
"project.boxing": "grey"
|
|
51
63
|
}
|
|
52
64
|
};
|
|
53
65
|
/* hard-coded map: which scope kinds each variable may be SET on
|
|
@@ -55,7 +67,19 @@ export const projectClassificationPresets = {
|
|
|
55
67
|
keys absent from this map default to all non-"default" scope kinds */
|
|
56
68
|
export const configWritableScopes = {
|
|
57
69
|
"agent.task": ["session"],
|
|
58
|
-
"agent.skill": ["session"]
|
|
70
|
+
"agent.skill": ["session"],
|
|
71
|
+
"project.artifact.task.basedir": ["user", "project"],
|
|
72
|
+
"project.artifact.task.files": ["user", "project"],
|
|
73
|
+
"project.artifact.spec.basedir": ["user", "project"],
|
|
74
|
+
"project.artifact.spec.files": ["user", "project"],
|
|
75
|
+
"project.artifact.arch.basedir": ["user", "project"],
|
|
76
|
+
"project.artifact.arch.files": ["user", "project"],
|
|
77
|
+
"project.artifact.code.basedir": ["user", "project"],
|
|
78
|
+
"project.artifact.code.files": ["user", "project"],
|
|
79
|
+
"project.artifact.docs.basedir": ["user", "project"],
|
|
80
|
+
"project.artifact.docs.files": ["user", "project"],
|
|
81
|
+
"project.artifact.infr.basedir": ["user", "project"],
|
|
82
|
+
"project.artifact.infr.files": ["user", "project"]
|
|
59
83
|
};
|
|
60
84
|
/* default set of scope kinds writable for any unrestricted key */
|
|
61
85
|
const configWritableScopesDefault = ["user", "project", "task", "session"];
|
|
@@ -130,7 +154,15 @@ export const configSchema = v.nullish(v.strictObject({
|
|
|
130
154
|
project: v.optional(v.strictObject({
|
|
131
155
|
id: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
132
156
|
name: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
133
|
-
boxing: v.optional(v.picklist(projectClassification.boxing))
|
|
157
|
+
boxing: v.optional(v.picklist(projectClassification.boxing)),
|
|
158
|
+
artifact: v.optional(v.strictObject({
|
|
159
|
+
spec: v.optional(v.strictObject({ basedir: v.optional(v.string()), files: v.optional(v.string()) })),
|
|
160
|
+
arch: v.optional(v.strictObject({ basedir: v.optional(v.string()), files: v.optional(v.string()) })),
|
|
161
|
+
code: v.optional(v.strictObject({ basedir: v.optional(v.string()), files: v.optional(v.string()) })),
|
|
162
|
+
docs: v.optional(v.strictObject({ basedir: v.optional(v.string()), files: v.optional(v.string()) })),
|
|
163
|
+
infr: v.optional(v.strictObject({ basedir: v.optional(v.string()), files: v.optional(v.string()) })),
|
|
164
|
+
task: v.optional(v.strictObject({ basedir: v.optional(v.string()), files: v.optional(v.string()) }))
|
|
165
|
+
}))
|
|
134
166
|
})),
|
|
135
167
|
agent: v.optional(v.strictObject({
|
|
136
168
|
persona: v.optional(v.picklist(agentClassification.persona)),
|
|
@@ -614,6 +646,20 @@ export default class ConfigCommand {
|
|
|
614
646
|
cfg.write();
|
|
615
647
|
});
|
|
616
648
|
});
|
|
649
|
+
/* register CLI sub-command "ase config delete" */
|
|
650
|
+
configCmd
|
|
651
|
+
.command("delete")
|
|
652
|
+
.description("delete the value at a dotted configuration key")
|
|
653
|
+
.argument("<key>", "configuration key (dotted path)")
|
|
654
|
+
.action((key, _opts, cmd) => {
|
|
655
|
+
const scope = parseScope(cmd.optsWithGlobals().scope);
|
|
656
|
+
const cfg = new Config("config", configSchema, this.log, scope);
|
|
657
|
+
cfg.lock(() => {
|
|
658
|
+
cfg.read();
|
|
659
|
+
cfg.delete(key);
|
|
660
|
+
cfg.write();
|
|
661
|
+
});
|
|
662
|
+
});
|
|
617
663
|
}
|
|
618
664
|
}
|
|
619
665
|
/* MCP registration entry point for layered YAML configuration access */
|
|
@@ -0,0 +1,235 @@
|
|
|
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
|
+
import { z } from "zod";
|
|
7
|
+
/* the reusable functionality */
|
|
8
|
+
export class Markdown {
|
|
9
|
+
/* prepare Markdown for improved rendering by rewriting
|
|
10
|
+
unordered bullet paragraphs -- replacing the "-"/"*" bullet marker with
|
|
11
|
+
"◯" and splitting multi-line inline code spans into per-line single-line
|
|
12
|
+
spans (so each physical line carries its own closed backtick span).
|
|
13
|
+
Fenced code blocks (``` / ~~~) are detected line-wise and passed
|
|
14
|
+
through entirely verbatim, so neither the inline-span splitting nor the
|
|
15
|
+
bullet-marker rewriting ever touches their contents. */
|
|
16
|
+
static prepare(text) {
|
|
17
|
+
if (typeof text !== "string")
|
|
18
|
+
throw new Error("markdown: text must be a string");
|
|
19
|
+
/* segment the input line-wise into alternating non-fenced and
|
|
20
|
+
fenced regions: a fenced code block opens with a line whose first
|
|
21
|
+
non-whitespace content is a run of 3+ backticks or tildes and
|
|
22
|
+
closes with a matching-or-longer run of the same marker; fenced
|
|
23
|
+
regions are emitted verbatim while only non-fenced regions are
|
|
24
|
+
handed to the rewriting passes below */
|
|
25
|
+
const lines = text.split("\n");
|
|
26
|
+
let result = "";
|
|
27
|
+
let buf = "";
|
|
28
|
+
let inFence = false;
|
|
29
|
+
let fenceCh = "";
|
|
30
|
+
let fenceLen = 0;
|
|
31
|
+
const flush = () => {
|
|
32
|
+
if (buf !== "") {
|
|
33
|
+
result += Markdown.rewrite(buf);
|
|
34
|
+
buf = "";
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
for (let li = 0; li < lines.length; li++) {
|
|
38
|
+
const line = lines[li];
|
|
39
|
+
const nl = li < lines.length - 1 ? "\n" : "";
|
|
40
|
+
const m = line.match(/^(\s*)(`{3,}|~{3,})/);
|
|
41
|
+
if (!inFence && m) {
|
|
42
|
+
/* a fence-opening line: flush the pending non-fenced buffer,
|
|
43
|
+
enter fenced mode, and emit the opener verbatim */
|
|
44
|
+
flush();
|
|
45
|
+
inFence = true;
|
|
46
|
+
fenceCh = m[2][0];
|
|
47
|
+
fenceLen = m[2].length;
|
|
48
|
+
result += line + nl;
|
|
49
|
+
}
|
|
50
|
+
else if (inFence) {
|
|
51
|
+
/* inside a fenced block: emit verbatim and, on a matching
|
|
52
|
+
closing fence line, leave fenced mode */
|
|
53
|
+
result += line + nl;
|
|
54
|
+
const c = line.match(/^(\s*)(`{3,}|~{3,})\s*$/);
|
|
55
|
+
if (c && c[2][0] === fenceCh && c[2].length >= fenceLen)
|
|
56
|
+
inFence = false;
|
|
57
|
+
}
|
|
58
|
+
else
|
|
59
|
+
/* a regular non-fenced line: accumulate for the passes */
|
|
60
|
+
buf += line + nl;
|
|
61
|
+
}
|
|
62
|
+
flush();
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
/* apply the inline-span and bullet-marker rewriting passes to a chunk of
|
|
66
|
+
non-fenced Markdown text (see prepare() for fenced-block handling) */
|
|
67
|
+
static rewrite(text) {
|
|
68
|
+
/* PASS 1: rewrite single-backtick inline code spans that carry
|
|
69
|
+
backslash-escaped backticks (`\``) into CommonMark-correct spans. */
|
|
70
|
+
{
|
|
71
|
+
let pre = "";
|
|
72
|
+
let j = 0;
|
|
73
|
+
while (j < text.length) {
|
|
74
|
+
const ch = text[j];
|
|
75
|
+
if (ch !== "`") {
|
|
76
|
+
/* literal backslash-escaped backtick *outside* any span
|
|
77
|
+
is left verbatim for later scanning */
|
|
78
|
+
pre += ch;
|
|
79
|
+
j++;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
/* scan an opening single-backtick span, capturing its raw
|
|
83
|
+
inner content up to the matching unescaped close backtick */
|
|
84
|
+
let inner = "";
|
|
85
|
+
let k = j + 1;
|
|
86
|
+
let closed = false;
|
|
87
|
+
let escaped = false;
|
|
88
|
+
while (k < text.length) {
|
|
89
|
+
const c = text[k];
|
|
90
|
+
if (c === "\\" && k + 1 < text.length && text[k + 1] === "`") {
|
|
91
|
+
inner += "\\`";
|
|
92
|
+
escaped = true;
|
|
93
|
+
k += 2;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (c === "`") {
|
|
97
|
+
closed = true;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
inner += c;
|
|
101
|
+
k++;
|
|
102
|
+
}
|
|
103
|
+
if (!closed || !escaped) {
|
|
104
|
+
/* not an escaped-backtick span: emit the opening backtick
|
|
105
|
+
verbatim and continue scanning from just after it */
|
|
106
|
+
pre += "`";
|
|
107
|
+
j++;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
/* un-escape inner `\`` into literal backticks, then choose a
|
|
111
|
+
fence longer than the longest internal backtick run */
|
|
112
|
+
const content = inner.replace(/\\`/g, "`");
|
|
113
|
+
let maxRun = 0;
|
|
114
|
+
let run = 0;
|
|
115
|
+
for (const c of content) {
|
|
116
|
+
if (c === "`") {
|
|
117
|
+
run++;
|
|
118
|
+
if (run > maxRun)
|
|
119
|
+
maxRun = run;
|
|
120
|
+
}
|
|
121
|
+
else
|
|
122
|
+
run = 0;
|
|
123
|
+
}
|
|
124
|
+
const fence = "`".repeat(maxRun + 1);
|
|
125
|
+
const pad = (content.startsWith("`") || content.endsWith("`")) ? " " : "";
|
|
126
|
+
pre += `${fence}${pad}${content}${pad}${fence}`;
|
|
127
|
+
j = k + 1;
|
|
128
|
+
}
|
|
129
|
+
text = pre;
|
|
130
|
+
}
|
|
131
|
+
/* PASS 2: split multi-line inline code spans by scanning the
|
|
132
|
+
entire text character-by-character while tracking whether we
|
|
133
|
+
are currently inside an active backtick (U+0060) span; on
|
|
134
|
+
every "<newline><whitespaces>" sequence encountered while
|
|
135
|
+
inside a span, close the span before the break and re-open
|
|
136
|
+
it after the indentation, so each physical line becomes its
|
|
137
|
+
own single-line span */
|
|
138
|
+
let out = "";
|
|
139
|
+
let fence = 0;
|
|
140
|
+
let i = 0;
|
|
141
|
+
while (i < text.length) {
|
|
142
|
+
const ch = text[i];
|
|
143
|
+
if (ch === "`") {
|
|
144
|
+
/* measure the full backtick run (a code-span delimiter is a
|
|
145
|
+
*run* of backticks; a span opened by N backticks is closed
|
|
146
|
+
only by a run of exactly N backticks) */
|
|
147
|
+
let n = 0;
|
|
148
|
+
while (i < text.length && text[i] === "`") {
|
|
149
|
+
n++;
|
|
150
|
+
i++;
|
|
151
|
+
}
|
|
152
|
+
if (fence === 0)
|
|
153
|
+
/* open a span of fence width n */
|
|
154
|
+
fence = n;
|
|
155
|
+
else if (n === fence)
|
|
156
|
+
/* close the active span */
|
|
157
|
+
fence = 0;
|
|
158
|
+
out += "`".repeat(n);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (fence > 0 && (ch === "\r" || ch === "\n")) {
|
|
162
|
+
/* consume optional CR followed by mandatory LF */
|
|
163
|
+
let nl = "";
|
|
164
|
+
if (ch === "\r" && i + 1 < text.length && text[i + 1] === "\n") {
|
|
165
|
+
nl = "\r\n";
|
|
166
|
+
i += 2;
|
|
167
|
+
}
|
|
168
|
+
else if (ch === "\n") {
|
|
169
|
+
nl = "\n";
|
|
170
|
+
i++;
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
/* bare CR: not a recognized newline, pass through */
|
|
174
|
+
out += ch;
|
|
175
|
+
i++;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
/* consume the following run of SPACE/TAB whitespace */
|
|
179
|
+
let ws = "";
|
|
180
|
+
while (i < text.length && (text[i] === " " || text[i] === "\t")) {
|
|
181
|
+
ws += text[i];
|
|
182
|
+
i++;
|
|
183
|
+
}
|
|
184
|
+
/* close span before the break, re-open after indentation,
|
|
185
|
+
preserving the active fence width on both sides */
|
|
186
|
+
const bars = "`".repeat(fence);
|
|
187
|
+
out += `${bars}${nl}${ws}${bars}`;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
out += ch;
|
|
191
|
+
i++;
|
|
192
|
+
}
|
|
193
|
+
/* PASS 3: replace the leading "-"/"*" marker of every unordered
|
|
194
|
+
bullet paragraph with "◯", preserving the leading indentation
|
|
195
|
+
and the following whitespace; operate line-by-line so only the
|
|
196
|
+
actual list markers (at a line start) are affected */
|
|
197
|
+
out = out
|
|
198
|
+
.split("\n")
|
|
199
|
+
.map((line) => line.replace(/^(\s*)[-*]([ \t]+)/, "$1◯$2"))
|
|
200
|
+
.join("\n");
|
|
201
|
+
return out;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/* MCP registration entry point */
|
|
205
|
+
export class MarkdownMCP {
|
|
206
|
+
register(mcp) {
|
|
207
|
+
mcp.registerTool("ase_markdown_prepare", {
|
|
208
|
+
title: "ASE markdown prepare",
|
|
209
|
+
description: "Prepare Markdown `text` for improved rendering by rewriting " +
|
|
210
|
+
"unordered bullet paragraphs: replace the `-`/`*` bullet markers with `◯` " +
|
|
211
|
+
"and split multi-line inline code spans into per-line single-line spans. " +
|
|
212
|
+
"Single-backtick spans containing backslash-escaped backticks are rewritten " +
|
|
213
|
+
"into CommonMark-correct spans with a widened backtick fence and literal inner " +
|
|
214
|
+
"backticks. Returns the prepared Markdown as `text`.",
|
|
215
|
+
inputSchema: {
|
|
216
|
+
text: z.string()
|
|
217
|
+
.describe("Markdown text to prepare for improved rendering")
|
|
218
|
+
}
|
|
219
|
+
}, async (args) => {
|
|
220
|
+
try {
|
|
221
|
+
const text = Markdown.prepare(args.text);
|
|
222
|
+
return {
|
|
223
|
+
content: [{ type: "text", text }]
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
228
|
+
return {
|
|
229
|
+
isError: true,
|
|
230
|
+
content: [{ type: "text", text: `ERROR: ${message}` }]
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|