@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.
Files changed (43) hide show
  1. package/dst/ase-artifact.js +331 -0
  2. package/dst/ase-config.js +56 -10
  3. package/dst/ase-service.js +2 -0
  4. package/dst/ase-task.js +139 -64
  5. package/dst/ase.js +2 -0
  6. package/package.json +5 -3
  7. package/plugin/.claude-plugin/plugin.json +1 -1
  8. package/plugin/.github/plugin/plugin.json +1 -1
  9. package/plugin/meta/ase-format-meta.md +5 -2
  10. package/plugin/meta/{ase-format-plan.md → ase-format-task.md} +7 -6
  11. package/plugin/package.json +1 -1
  12. package/plugin/skills/ase-arch-analyze/SKILL.md +1 -1
  13. package/plugin/skills/ase-arch-discover/SKILL.md +17 -9
  14. package/plugin/skills/ase-arch-discover/help.md +14 -0
  15. package/plugin/skills/ase-code-craft/SKILL.md +2 -2
  16. package/plugin/skills/ase-code-insight/SKILL.md +1 -1
  17. package/plugin/skills/ase-code-lint/SKILL.md +1 -1
  18. package/plugin/skills/ase-code-refactor/SKILL.md +2 -2
  19. package/plugin/skills/ase-code-resolve/SKILL.md +3 -3
  20. package/plugin/skills/ase-docs-proofread/SKILL.md +1 -1
  21. package/plugin/skills/ase-meta-brainstorm/SKILL.md +22 -21
  22. package/plugin/skills/ase-meta-brainstorm/help.md +38 -12
  23. package/plugin/skills/ase-meta-chat/SKILL.md +1 -1
  24. package/plugin/skills/ase-meta-diaboli/SKILL.md +1 -1
  25. package/plugin/skills/ase-meta-diff/SKILL.md +1 -1
  26. package/plugin/skills/ase-meta-quorum/SKILL.md +37 -5
  27. package/plugin/skills/ase-meta-quorum/help.md +18 -0
  28. package/plugin/skills/ase-meta-review/SKILL.md +1 -1
  29. package/plugin/skills/ase-meta-search/SKILL.md +35 -6
  30. package/plugin/skills/ase-meta-search/help.md +14 -2
  31. package/plugin/skills/ase-meta-steelman/SKILL.md +1 -1
  32. package/plugin/skills/ase-meta-why/SKILL.md +117 -25
  33. package/plugin/skills/ase-meta-why/help.md +30 -2
  34. package/plugin/skills/ase-task-condense/SKILL.md +1 -1
  35. package/plugin/skills/ase-task-delete/help.md +0 -4
  36. package/plugin/skills/ase-task-edit/SKILL.md +4 -4
  37. package/plugin/skills/ase-task-edit/help.md +0 -4
  38. package/plugin/skills/ase-task-grill/SKILL.md +1 -1
  39. package/plugin/skills/ase-task-grill/help.md +0 -4
  40. package/plugin/skills/ase-task-implement/SKILL.md +1 -1
  41. package/plugin/skills/ase-task-implement/help.md +5 -6
  42. package/plugin/skills/ase-task-preflight/SKILL.md +1 -1
  43. 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
- "agent.persona": "engineer",
44
- "agent.task": "default"
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 */
@@ -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);