@rse/ase 0.9.4 → 0.9.6
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-service.js +2 -0
- package/dst/ase-task.js +139 -64
- package/dst/ase.js +2 -0
- package/package.json +5 -3
- 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} +7 -6
- package/plugin/package.json +1 -1
- package/plugin/skills/ase-arch-analyze/SKILL.md +1 -1
- package/plugin/skills/ase-arch-discover/SKILL.md +17 -9
- package/plugin/skills/ase-arch-discover/help.md +14 -0
- package/plugin/skills/ase-code-craft/SKILL.md +2 -2
- package/plugin/skills/ase-code-insight/SKILL.md +1 -1
- package/plugin/skills/ase-code-lint/SKILL.md +1 -1
- package/plugin/skills/ase-code-refactor/SKILL.md +2 -2
- package/plugin/skills/ase-code-resolve/SKILL.md +3 -3
- package/plugin/skills/ase-docs-proofread/SKILL.md +1 -1
- package/plugin/skills/ase-meta-brainstorm/SKILL.md +22 -21
- package/plugin/skills/ase-meta-brainstorm/help.md +38 -12
- package/plugin/skills/ase-meta-chat/SKILL.md +1 -1
- package/plugin/skills/ase-meta-diaboli/SKILL.md +1 -1
- package/plugin/skills/ase-meta-diff/SKILL.md +1 -1
- package/plugin/skills/ase-meta-quorum/SKILL.md +37 -5
- package/plugin/skills/ase-meta-quorum/help.md +18 -0
- package/plugin/skills/ase-meta-review/SKILL.md +1 -1
- package/plugin/skills/ase-meta-search/SKILL.md +35 -6
- package/plugin/skills/ase-meta-search/help.md +14 -2
- package/plugin/skills/ase-meta-steelman/SKILL.md +1 -1
- package/plugin/skills/ase-meta-why/SKILL.md +117 -25
- package/plugin/skills/ase-meta-why/help.md +30 -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 +4 -4
- package/plugin/skills/ase-task-edit/help.md +0 -4
- package/plugin/skills/ase-task-grill/SKILL.md +1 -1
- package/plugin/skills/ase-task-grill/help.md +0 -4
- package/plugin/skills/ase-task-implement/SKILL.md +1 -1
- package/plugin/skills/ase-task-implement/help.md +5 -6
- package/plugin/skills/ase-task-preflight/SKILL.md +1 -1
- package/plugin/skills/ase-task-reboot/SKILL.md +1 -1
|
@@ -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 */
|
package/dst/ase-service.js
CHANGED
|
@@ -18,6 +18,7 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
|
|
|
18
18
|
import { Config, configSchema, ConfigMCP } from "./ase-config.js";
|
|
19
19
|
import { DiagramMCP } from "./ase-diagram.js";
|
|
20
20
|
import { TaskMCP } from "./ase-task.js";
|
|
21
|
+
import { ArtifactMCP } from "./ase-artifact.js";
|
|
21
22
|
import { KVMCP } from "./ase-kv.js";
|
|
22
23
|
import PersonaMCP from "./ase-persona.js";
|
|
23
24
|
import { TimestampMCP } from "./ase-timestamp.js";
|
|
@@ -236,6 +237,7 @@ export default class ServiceCommand {
|
|
|
236
237
|
new ServiceMCP({ projectId: ctx.projectId, port: ctx.port, startTime }).register(mcp);
|
|
237
238
|
new DiagramMCP().register(mcp);
|
|
238
239
|
new TaskMCP(this.log).register(mcp);
|
|
240
|
+
new ArtifactMCP(this.log).register(mcp);
|
|
239
241
|
new KVMCP().register(mcp);
|
|
240
242
|
new PersonaMCP(this.log).register(mcp);
|
|
241
243
|
new TimestampMCP().register(mcp);
|