@rse/ase 0.9.61 → 0.9.63

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 (37) hide show
  1. package/dst/ase-artifact.js +19 -8
  2. package/dst/ase-config.js +12 -8
  3. package/dst/ase-hook.js +8 -0
  4. package/dst/ase-service.js +2 -0
  5. package/dst/ase-spec.js +241 -0
  6. package/dst/ase.js +2 -0
  7. package/package.json +10 -8
  8. package/plugin/.claude-plugin/plugin.json +1 -1
  9. package/plugin/.codex-plugin/plugin.json +1 -1
  10. package/plugin/.github/plugin/plugin.json +1 -1
  11. package/plugin/agents/ase-code-lint.md +47 -3
  12. package/plugin/etc/stx.conf +5 -3
  13. package/plugin/meta/ase-common-grill.md +89 -0
  14. package/plugin/meta/ase-dialog.md +6 -0
  15. package/plugin/meta/ase-format-meta.md +23 -105
  16. package/plugin/meta/ase-format-spec.md +22 -1326
  17. package/plugin/meta/ase-tenets.md +63 -4
  18. package/plugin/package.json +6 -2
  19. package/plugin/skills/ase-arch-analyze/SKILL.md +1 -1
  20. package/plugin/skills/ase-code-analyze/SKILL.md +1 -1
  21. package/plugin/skills/ase-code-analyze/help.md +2 -1
  22. package/plugin/skills/ase-code-edit/SKILL.md +142 -140
  23. package/plugin/skills/ase-code-lint/SKILL.md +5 -5
  24. package/plugin/skills/ase-code-lint/help.md +23 -8
  25. package/plugin/skills/ase-help-skill/catalog.md +3 -0
  26. package/plugin/skills/ase-meta-review/help.md +1 -1
  27. package/plugin/skills/ase-spec-edit/SKILL.md +520 -0
  28. package/plugin/skills/ase-spec-edit/help.md +130 -0
  29. package/plugin/skills/ase-sync-export/SKILL.md +57 -116
  30. package/plugin/skills/ase-sync-export/help.md +30 -42
  31. package/plugin/skills/ase-sync-import/SKILL.md +37 -15
  32. package/plugin/skills/ase-sync-import/help.md +14 -10
  33. package/plugin/skills/ase-sync-reconcile/SKILL.md +37 -16
  34. package/plugin/skills/ase-sync-reconcile/help.md +19 -16
  35. package/plugin/skills/ase-task-grill/SKILL.md +120 -59
  36. package/plugin/skills/ase-task-grill/help.md +42 -12
  37. package/plugin/meta/ase-format-arch.md +0 -1164
@@ -13,9 +13,9 @@ import { Task } from "./ase-task.js";
13
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
- export const artifactKinds = ["spec", "arch", "code", "docs", "infr", "othr"];
17
- /* the five configured kinds (i.e. all kinds except the implicit "othr") */
18
- const configuredKinds = ["spec", "arch", "code", "docs", "infr"];
16
+ export const artifactKinds = ["spec", "code", "docs", "infr", "othr"];
17
+ /* the four configured kinds (i.e. all kinds except the implicit "othr") */
18
+ const configuredKinds = ["spec", "code", "docs", "infr"];
19
19
  /* reusable functionality: resolve artifact kinds to project-relative
20
20
  file lists, driven by the "project.artifact.<kind>" configuration */
21
21
  export class Artifact {
@@ -167,7 +167,7 @@ export class Artifact {
167
167
  const all = Artifact.universe();
168
168
  const cfg = new Config("config", configSchema, log);
169
169
  cfg.read();
170
- /* raw-resolve all five configured kinds */
170
+ /* raw-resolve all four configured kinds */
171
171
  const raw = new Map();
172
172
  for (const kind of configuredKinds) {
173
173
  const { basedir, files } = Artifact.spec(cfg, kind);
@@ -210,6 +210,17 @@ export class Artifact {
210
210
  const { basedir } = Artifact.spec(cfg, kind);
211
211
  return basedir === "" ? file : `${basedir}/${file}`;
212
212
  }
213
+ /* resolve the configured "basedir" of a kind to an absolute path;
214
+ the implicit "othr" catch-all has no configured "basedir" and is
215
+ therefore rejected */
216
+ static basedir(log, kind) {
217
+ if (kind === "othr")
218
+ throw new Error("artifact: kind \"othr\" has no configured basedir");
219
+ const cfg = new Config("config", configSchema, log);
220
+ cfg.read();
221
+ const { basedir } = Artifact.spec(cfg, kind);
222
+ return path.join(Task.projectRoot(), basedir);
223
+ }
213
224
  }
214
225
  /* CLI command "ase artifact" */
215
226
  export default class ArtifactCommand {
@@ -273,12 +284,12 @@ export class ArtifactMCP {
273
284
  mcp.registerTool("ase_artifact_list", {
274
285
  title: "ASE artifact list",
275
286
  description: "Resolve one or more artifact `kind`s to project-relative file lists. " +
276
- "Recognized kinds are `spec`, `arch`, `code`, `docs`, `infr`, and `othr`. " +
287
+ "Recognized kinds are `spec`, `code`, `docs`, `infr`, and `othr`. " +
277
288
  "Returns an `artifacts` array of `{ kind, files }` objects. " +
278
289
  "If `kind` is omitted or empty, all kinds are resolved.",
279
290
  inputSchema: {
280
291
  kind: z.array(z.string()).optional()
281
- .describe("list of artifact kinds (`spec`, `arch`, `code`, `docs`, `infr`, " +
292
+ .describe("list of artifact kinds (`spec`, `code`, `docs`, `infr`, " +
282
293
  "`othr`); if omitted or empty, all kinds are resolved")
283
294
  },
284
295
  outputSchema: {
@@ -306,13 +317,13 @@ export class ArtifactMCP {
306
317
  title: "ASE artifact name",
307
318
  description: "Resolve a base-relative `filename` within an artifact `kind` to a project-relative path " +
308
319
  "by prefixing it with the kind's configured `basedir`. " +
309
- "Recognized kinds are `spec`, `arch`, `code`, `docs`, and `infr` " +
320
+ "Recognized kinds are `spec`, `code`, `docs`, and `infr` " +
310
321
  "(the implicit `othr` catch-all has no basedir and is rejected). " +
311
322
  "If `kind` is omitted, it defaults to `code`. " +
312
323
  "Returns the resolved path as `name`.",
313
324
  inputSchema: {
314
325
  kind: z.string().optional()
315
- .describe("artifact kind (`spec`, `arch`, `code`, `docs`, `infr`); defaults to `code`"),
326
+ .describe("artifact kind (`spec`, `code`, `docs`, `infr`); defaults to `code`"),
316
327
  filename: z.string()
317
328
  .describe("base-relative filename within the kind's basedir")
318
329
  },
package/dst/ase-config.js CHANGED
@@ -48,10 +48,9 @@ export const projectClassificationPresets = {
48
48
  "project.boxing": "white",
49
49
  "project.artifact.task.basedir": ".ase/task",
50
50
  "project.artifact.task.files": "*.md",
51
- "project.artifact.spec.basedir": "doc/spec",
52
- "project.artifact.spec.files": "*.{md,txt}",
53
- "project.artifact.arch.basedir": "doc/arch",
54
- "project.artifact.arch.files": "*.{md,txt}",
51
+ "project.artifact.spec.basedir": "docs/specbook",
52
+ "project.artifact.spec.files": "*.{md,txt,svg,png,jpg}",
53
+ "project.artifact.spec.schema": "",
55
54
  "project.artifact.code.basedir": "src",
56
55
  "project.artifact.code.files": "** !**/etc/** !**/{.gitignore,.npmignore,package.json}",
57
56
  "project.artifact.docs.basedir": "doc",
@@ -76,8 +75,7 @@ export const configWritableScopes = {
76
75
  "project.artifact.task.files": ["user", "project"],
77
76
  "project.artifact.spec.basedir": ["user", "project"],
78
77
  "project.artifact.spec.files": ["user", "project"],
79
- "project.artifact.arch.basedir": ["user", "project"],
80
- "project.artifact.arch.files": ["user", "project"],
78
+ "project.artifact.spec.schema": ["user", "project"],
81
79
  "project.artifact.code.basedir": ["user", "project"],
82
80
  "project.artifact.code.files": ["user", "project"],
83
81
  "project.artifact.docs.basedir": ["user", "project"],
@@ -185,6 +183,13 @@ const artifactSchema = v.optional(v.strictObject({
185
183
  basedir: v.optional(v.string()),
186
184
  files: v.optional(v.string())
187
185
  }));
186
+ /* schema for the "spec" artifact kind, additionally carrying the
187
+ SpecBook YAML "schema" configuration file (empty: bundled standard) */
188
+ const artifactSpecSchema = v.optional(v.strictObject({
189
+ basedir: v.optional(v.string()),
190
+ files: v.optional(v.string()),
191
+ schema: v.optional(v.string())
192
+ }));
188
193
  /* schema for ".ase/config.yaml" */
189
194
  export const configSchema = v.nullish(v.strictObject({
190
195
  project: v.optional(v.strictObject({
@@ -192,8 +197,7 @@ export const configSchema = v.nullish(v.strictObject({
192
197
  name: v.optional(v.pipe(v.string(), v.minLength(1))),
193
198
  boxing: v.optional(v.picklist(projectClassification.boxing)),
194
199
  artifact: v.optional(v.strictObject({
195
- spec: artifactSchema,
196
- arch: artifactSchema,
200
+ spec: artifactSpecSchema,
197
201
  code: artifactSchema,
198
202
  docs: artifactSchema,
199
203
  infr: artifactSchema,
package/dst/ase-hook.js CHANGED
@@ -292,6 +292,10 @@ export default class HookCommand {
292
292
  const persona = setting("agent.persona", "ASE_PERSONA_STYLE", "engineer");
293
293
  const guidance = setting("agent.guidance", "ASE_GUIDANCE_LEVEL", "normal");
294
294
  const boxing = setting("project.boxing", "ASE_PROJECT_BOXING", "white");
295
+ /* determine the specification base directory and the SpecBook
296
+ schema configuration (empty: the bundled standard schema) */
297
+ const specBasedir = String(cfg.get("project.artifact.spec.basedir") ?? "");
298
+ const specSchema = String(cfg.get("project.artifact.spec.schema") ?? "");
295
299
  /* determine headless mode */
296
300
  const headless = process.env.ASE_HEADLESS === "true" ? "true" : "false";
297
301
  /* provide ASE information to Anthropic Claude Code CLI shell commands
@@ -303,6 +307,8 @@ export default class HookCommand {
303
307
  `export ASE_USER_ID=${quote([userId])}\n` +
304
308
  `export ASE_PROJECT_ID=${quote([projectId])}\n` +
305
309
  `export ASE_PROJECT_BOXING=${quote([boxing])}\n` +
310
+ `export ASE_SPEC_BASEDIR=${quote([specBasedir])}\n` +
311
+ `export ASE_SPEC_SCHEMA=${quote([specSchema])}\n` +
306
312
  `export ASE_TASK_ID=${quote([taskId])}\n` +
307
313
  `export ASE_SESSION_ID=${quote([sessionId])}\n` +
308
314
  `export ASE_HEADLESS=${quote([headless])}\n` +
@@ -325,6 +331,8 @@ export default class HookCommand {
325
331
  `<ase-user-id>${userId}</ase-user-id>\n` +
326
332
  `<ase-project-id>${projectId}</ase-project-id>\n` +
327
333
  `<ase-project-boxing>${boxing}</ase-project-boxing>\n` +
334
+ `<ase-spec-basedir>${specBasedir}</ase-spec-basedir>\n` +
335
+ `<ase-spec-schema>${specSchema}</ase-spec-schema>\n` +
328
336
  `<ase-task-id>${taskId}</ase-task-id>\n` +
329
337
  `<ase-session-id>${sessionId}</ase-session-id>\n` +
330
338
  `<ase-headless>${headless}</ase-headless>\n` +
@@ -22,6 +22,7 @@ import { DiagramMCP } from "./ase-diagram.js";
22
22
  import { TaskMCP } from "./ase-task.js";
23
23
  import { MarkdownMCP } from "./ase-markdown.js";
24
24
  import { ArtifactMCP } from "./ase-artifact.js";
25
+ import { SpecMCP } from "./ase-spec.js";
25
26
  import { KVMCP } from "./ase-kv.js";
26
27
  import { TimestampMCP } from "./ase-timestamp.js";
27
28
  import { SleepMCP } from "./ase-sleep.js";
@@ -268,6 +269,7 @@ export default class ServiceCommand {
268
269
  new TaskMCP(this.log).register(mcp);
269
270
  new MarkdownMCP().register(mcp);
270
271
  new ArtifactMCP(this.log).register(mcp);
272
+ new SpecMCP(this.log).register(mcp);
271
273
  new KVMCP().register(mcp);
272
274
  new TimestampMCP().register(mcp);
273
275
  new SleepMCP().register(mcp);
@@ -0,0 +1,241 @@
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 path from "node:path";
7
+ import fs from "node:fs";
8
+ import { isScalar } from "yaml";
9
+ import { z } from "zod";
10
+ import sourceCodeError from "source-code-error";
11
+ import { SpecBook, renderDiagnostic, renderVerbose, formats, parseOutputSpec } from "@rse/specbook";
12
+ import { Config, configSchema } from "./ase-config.js";
13
+ import { Task } from "./ase-task.js";
14
+ import { Artifact } from "./ase-artifact.js";
15
+ import { Meta } from "./ase-meta.js";
16
+ import { writeStdout } from "./ase-stdio.js";
17
+ /* reusable functionality: lint and export the SpecBook-based project
18
+ specification, located via the "project.artifact.spec.basedir" and
19
+ "project.artifact.spec.schema" configuration */
20
+ export class Spec {
21
+ /* resolve the YAML schema configuration file: the configured
22
+ "project.artifact.spec.schema" relative to the project root, or
23
+ the bundled standard "ase-format-specbook.yaml" plugin meta file
24
+ if unset or empty */
25
+ static configFile(log) {
26
+ const cfg = new Config("config", configSchema, log);
27
+ cfg.read();
28
+ const val = cfg.get("project.artifact.spec.schema");
29
+ const file = val === undefined ? "" : String(isScalar(val) ? val.value : val);
30
+ if (file === "")
31
+ return Meta.resolve("ase-format-specbook.yaml");
32
+ return path.resolve(Task.projectRoot(), file);
33
+ }
34
+ /* create the SpecBook API instance, routing its verbose processing
35
+ messages into the info log if requested, else into the debug log */
36
+ static api(log, verbose) {
37
+ return new SpecBook({
38
+ verbose: (cmd, msg) => log.write(verbose ? "info" : "debug", `specbook: ${cmd}: ${renderVerbose(msg)}`)
39
+ });
40
+ }
41
+ /* render a diagnostic file path relative to the project root,
42
+ keeping paths outside the project (like the bundled schema) as-is */
43
+ static relativize(file) {
44
+ const rel = path.relative(Task.projectRoot(), file);
45
+ if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel))
46
+ return file;
47
+ return rel.replace(/\\/g, "/");
48
+ }
49
+ /* lint the specification Markdown files below the "spec" artifact
50
+ base directory against the schema configuration */
51
+ static async lint(log, verbose = false) {
52
+ const result = await Spec.api(log, verbose).lint({
53
+ config: Spec.configFile(log),
54
+ basedir: Artifact.basedir(log, "spec")
55
+ });
56
+ return result.diagnostics.map((d) => ({ ...d, file: Spec.relativize(d.file) }));
57
+ }
58
+ /* render a diagnostic as a multi-line message with the affected
59
+ source snippet (like the verbose SpecBook CLI), falling back to
60
+ the single-line message when the source file is unreadable or
61
+ empty (as there is no snippet to show) */
62
+ static render(diagnostic, colors) {
63
+ let code;
64
+ try {
65
+ code = fs.readFileSync(path.resolve(Task.projectRoot(), diagnostic.file), "utf8");
66
+ }
67
+ catch {
68
+ return `${renderDiagnostic(diagnostic)}\n`;
69
+ }
70
+ if (code === "")
71
+ return `${renderDiagnostic(diagnostic)}\n`;
72
+ return sourceCodeError({
73
+ message: diagnostic.message,
74
+ filename: diagnostic.file,
75
+ code,
76
+ line: diagnostic.line,
77
+ column: diagnostic.column,
78
+ colors
79
+ });
80
+ }
81
+ /* export the specification Markdown files below the "spec" artifact
82
+ base directory into the requested formats, one buffer per format */
83
+ static export(log, formats, verbose = false) {
84
+ return Spec.api(log, verbose).export({
85
+ config: Spec.configFile(log),
86
+ basedir: Artifact.basedir(log, "spec"),
87
+ formats
88
+ });
89
+ }
90
+ }
91
+ /* CLI command "ase spec" */
92
+ export default class SpecCommand {
93
+ log;
94
+ constructor(log) {
95
+ this.log = log;
96
+ }
97
+ /* register commands */
98
+ register(program) {
99
+ /* register CLI top-level command "ase spec" */
100
+ const spec = program
101
+ .command("spec")
102
+ .description("Lint and export the SpecBook-based project specification")
103
+ .action(() => {
104
+ spec.outputHelp();
105
+ process.exit(1);
106
+ });
107
+ /* register CLI sub-command "ase spec lint" */
108
+ spec
109
+ .command("lint")
110
+ .description("Lint the specification Markdown files against the SpecBook schema configuration")
111
+ .option("-v, --verbose", "print verbose processing information and each diagnostic with its affected source snippet")
112
+ .action(async (opts) => {
113
+ const diagnostics = await Spec.lint(this.log, opts.verbose === true);
114
+ for (const diagnostic of diagnostics)
115
+ await writeStdout(opts.verbose === true ?
116
+ Spec.render(diagnostic, process.stdout.isTTY === true) :
117
+ `${renderDiagnostic(diagnostic)}\n`);
118
+ if (diagnostics.length > 0)
119
+ process.exitCode = 1;
120
+ });
121
+ /* register CLI sub-command "ase spec export" */
122
+ spec
123
+ .command("export")
124
+ .description("Export the specification Markdown files as JSON, JSON5, YAML, TOON, HTML, PDF, or normalized Markdown")
125
+ .option("-o, --output <[format:]file>", "output file (\"-\" for stdout, repeatable), with the format inferred " +
126
+ "from the filename extension unless explicitly prefixed " +
127
+ "(default: \"index.html\" inside the specification base directory)", (value, previous) => previous.concat(value), new Array())
128
+ .option("-v, --verbose", "print verbose processing information")
129
+ .action(async (opts) => {
130
+ const outputs = (opts.output.length > 0 ? opts.output :
131
+ [path.join(Artifact.basedir(this.log, "spec"), "index.html")]).map(parseOutputSpec);
132
+ const distinct = Array.from(new Set(outputs.map(({ format }) => format)));
133
+ const buffers = await Spec.export(this.log, distinct, opts.verbose === true);
134
+ for (const { format, output } of outputs) {
135
+ const data = buffers[distinct.indexOf(format)];
136
+ if (output === "-")
137
+ await writeStdout(data);
138
+ else {
139
+ await fs.promises.writeFile(output, data);
140
+ this.log.write("info", `spec: exported specification into "${output}" (${data.length} bytes)`);
141
+ }
142
+ }
143
+ });
144
+ }
145
+ }
146
+ /* render a caught error as an MCP tool error result */
147
+ const mcpToolError = (err) => ({
148
+ isError: true,
149
+ content: [{ type: "text", text: `ERROR: ${err instanceof Error ? err.message : String(err)}` }]
150
+ });
151
+ /* MCP registration entry point for SpecBook tools */
152
+ export class SpecMCP {
153
+ log;
154
+ constructor(log) {
155
+ this.log = log;
156
+ }
157
+ /* register MCP tools */
158
+ register(mcp) {
159
+ mcp.registerTool("ase_specbook_lint", {
160
+ title: "ASE SpecBook lint",
161
+ description: "Lint the SpecBook specification Markdown files of the project (located via the " +
162
+ "`project.artifact.spec.basedir` configuration) against the SpecBook YAML schema " +
163
+ "configuration (`project.artifact.spec.schema`, defaulting to the bundled `ase-format-specbook.yaml`). " +
164
+ "Returns a `diagnostics` array of `{ file, line, column, message }` objects (with " +
165
+ "project-relative `file`), rendered as bullet points in `text`. With `verbose`, " +
166
+ "each diagnostic additionally carries a multi-line `snippet` rendering with the " +
167
+ "affected source lines, which is also used for `text`. An empty array " +
168
+ "(`text` of `specification valid`) means the specification is valid.",
169
+ inputSchema: {
170
+ verbose: z.boolean().optional()
171
+ .describe("if true, render each diagnostic with its affected source snippet (default: false)")
172
+ },
173
+ outputSchema: {
174
+ diagnostics: z.array(z.object({
175
+ file: z.string().describe("project-relative file path"),
176
+ line: z.number().describe("line number (1-based)"),
177
+ column: z.number().describe("column number (1-based)"),
178
+ message: z.string().describe("diagnostic message"),
179
+ snippet: z.string().optional()
180
+ .describe("multi-line rendering with the affected source snippet (with `verbose` only)")
181
+ })).describe("lint diagnostics, empty if the specification is valid")
182
+ }
183
+ }, async (args) => {
184
+ try {
185
+ const verbose = args.verbose ?? false;
186
+ const diagnostics = (await Spec.lint(this.log))
187
+ .map((d) => verbose ? { ...d, snippet: Spec.render(d, false) } : d);
188
+ const text = diagnostics.length === 0 ? "specification valid" :
189
+ verbose ?
190
+ diagnostics.map((d) => d.snippet ?? "").join("").replace(/\n$/, "") :
191
+ diagnostics.map((d) => `- ${renderDiagnostic(d)}`).join("\n");
192
+ return {
193
+ structuredContent: { diagnostics },
194
+ content: [{ type: "text", text }]
195
+ };
196
+ }
197
+ catch (err) {
198
+ return mcpToolError(err);
199
+ }
200
+ });
201
+ mcp.registerTool("ase_specbook_export", {
202
+ title: "ASE SpecBook export",
203
+ description: "Export the SpecBook specification Markdown files of the project (located via the " +
204
+ "`project.artifact.spec.basedir` configuration) as JSON, JSON5, YAML, TOON, HTML, PDF, " +
205
+ "or normalized Markdown. The result is written to the `output` file (a relative path " +
206
+ "resolves against the project root) if given, else it is returned directly " +
207
+ "(PDF as a base64-encoded resource). The export fails on any lint diagnostic.",
208
+ inputSchema: {
209
+ format: z.enum(formats).optional()
210
+ .describe("output format (default: inferred from the `output` file extension, else `json`)"),
211
+ output: z.string().optional()
212
+ .describe("output file path (\"-\" or omitted returns the result directly)")
213
+ }
214
+ }, async (args) => {
215
+ try {
216
+ /* an explicit format takes the output as a plain file path, while
217
+ otherwise the output is an "[<format>:]<file>" specification */
218
+ const spec = args.format !== undefined || args.output === undefined ?
219
+ { format: args.format ?? "json", output: args.output } :
220
+ parseOutputSpec(args.output);
221
+ const [data] = await Spec.export(this.log, [spec.format]);
222
+ if (spec.output !== undefined && spec.output !== "-") {
223
+ await fs.promises.writeFile(path.resolve(Task.projectRoot(), spec.output), data);
224
+ return { content: [{ type: "text", text: `exported specification into "${spec.output}" (${data.length} bytes)` }] };
225
+ }
226
+ else if (spec.format === "pdf")
227
+ return {
228
+ content: [{
229
+ type: "resource",
230
+ resource: { uri: "ase:specbook-export.pdf", mimeType: "application/pdf", blob: data.toString("base64") }
231
+ }]
232
+ };
233
+ else
234
+ return { content: [{ type: "text", text: data.toString("utf8") }] };
235
+ }
236
+ catch (err) {
237
+ return mcpToolError(err);
238
+ }
239
+ });
240
+ }
241
+ }
package/dst/ase.js CHANGED
@@ -14,6 +14,7 @@ import HookCommand from "./ase-hook.js";
14
14
  import StatuslineCommand from "./ase-statusline.js";
15
15
  import TaskCommand from "./ase-task.js";
16
16
  import ArtifactCommand from "./ase-artifact.js";
17
+ import SpecCommand from "./ase-spec.js";
17
18
  import MetaCommand from "./ase-meta.js";
18
19
  import CompatCommand from "./ase-compat.js";
19
20
  import DiagramCommand from "./ase-diagram.js";
@@ -54,6 +55,7 @@ const main = async () => {
54
55
  new StatuslineCommand(log).register(program);
55
56
  new TaskCommand(log).register(program);
56
57
  new ArtifactCommand(log).register(program);
58
+ new SpecCommand(log).register(program);
57
59
  new MetaCommand(log).register(program);
58
60
  new CompatCommand().register(program);
59
61
  new DiagramCommand(log).register(program);
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "homepage": "https://ase.tools",
7
7
  "repository": { "url": "git+https://github.com/rse/ase.git", "type": "git" },
8
8
  "bugs": { "url": "https://github.com/rse/ase/issues" },
9
- "version": "0.9.61",
9
+ "version": "0.9.63",
10
10
  "license": "Apache-2.0",
11
11
  "author": {
12
12
  "name": "Dr. Ralf S. Engelschall",
@@ -16,11 +16,11 @@
16
16
  "type": "module",
17
17
  "bin": { "ase": "bin/ase" },
18
18
  "devDependencies": {
19
- "eslint": "10.9.0",
19
+ "eslint": "10.9.1",
20
20
  "@eslint/js": "10.0.1",
21
- "@typescript-eslint/parser": "8.67.0",
22
- "@typescript-eslint/eslint-plugin": "8.67.0",
23
- "typescript-eslint": "8.67.0",
21
+ "@typescript-eslint/parser": "8.68.0",
22
+ "@typescript-eslint/eslint-plugin": "8.68.0",
23
+ "typescript-eslint": "8.68.0",
24
24
  "eslint-plugin-promise": "7.3.0",
25
25
  "neostandard": "0.14.0-next.1",
26
26
  "globals": "17.11.0",
@@ -30,7 +30,7 @@
30
30
  "nodemon": "3.1.14",
31
31
  "shx": "0.4.0",
32
32
 
33
- "@types/node": "26.2.0",
33
+ "@types/node": "26.3.0",
34
34
  "@types/luxon": "3.7.5",
35
35
  "@types/which": "3.0.4",
36
36
  "@types/update-notifier": "6.0.8",
@@ -64,8 +64,10 @@
64
64
  "write-file-atomic": "8.0.0",
65
65
  "pacote": "22.0.0",
66
66
  "ofetch": "1.5.1",
67
- "picomatch": "4.0.5",
68
- "lru-cache": "11.5.2"
67
+ "picomatch": "4.0.7",
68
+ "lru-cache": "11.5.2",
69
+ "@rse/specbook": "1.0.2",
70
+ "source-code-error": "1.2.0"
69
71
  },
70
72
  "allowScripts": {
71
73
  "fsevents": true
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ase",
3
- "version": "0.9.61",
3
+ "version": "0.9.63",
4
4
  "description": "Agentic Software Engineering (ASE)",
5
5
  "keywords": [ "agentic", "software", "engineering" ],
6
6
  "homepage": "https://ase.tools",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ase",
3
- "version": "0.9.61",
3
+ "version": "0.9.63",
4
4
  "description": "Agentic Software Engineering (ASE)",
5
5
  "keywords": [ "agentic", "software", "engineering" ],
6
6
  "homepage": "https://ase.tools",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ase",
3
- "version": "0.9.61",
3
+ "version": "0.9.63",
4
4
  "description": "Agentic Software Engineering (ASE)",
5
5
  "keywords": [ "agentic", "software", "engineering" ],
6
6
  "homepage": "https://ase.tools",
@@ -15,7 +15,7 @@ Workflow
15
15
  1. Set the requested context: <context>$ARGUMENTS</context>.
16
16
  The *first* whitespace-separated token of <context/> is the
17
17
  comma-separated *aspect set* <aspects/> (a non-empty subset of the
18
- aspect ids `A01`...`A20`). The *remaining* tokens are the source
18
+ aspect ids `A01`...`A21`). The *remaining* tokens are the source
19
19
  code files to check.
20
20
 
21
21
  2. Use the `Read` tool to read all source code files referenced by
@@ -28,7 +28,7 @@ Workflow
28
28
 
29
29
  4. Set <problems/> to empty.
30
30
  Then check the read source code for the following aspects (each
31
- aspect is uniquely identified by its `aspect` id `A01 - XXX`...`A20
31
+ aspect is uniquely identified by its `aspect` id `A01 - XXX`...`A21
32
32
  - XXX`), but *strictly limited* to those aspects whose id is
33
33
  contained in the aspect set <aspects/> -- all other aspects are
34
34
  *not* checked and their problems are *never* reported:
@@ -266,6 +266,50 @@ Workflow
266
266
  *masks* another bug (e.g., unreachable code after a misplaced
267
267
  `return` that skips cleanup logic).
268
268
 
269
+ - **A21 - DOCUMENTATION**:
270
+ Check for *incomplete* and for *excessive* code documentation
271
+ across the following sub-aspects. The yardstick is a *minimal*
272
+ description: one or two lines stating WHAT the construct does,
273
+ optimally written in the *idiomatic documentation convention* of
274
+ the project or the target programming language.
275
+
276
+ - **C1 MISSING-DOCUMENTATION**: functions, methods, classes,
277
+ interfaces, or modules without any documentation comment
278
+ describing their purpose -- *private* and *internal*
279
+ constructs included, not just the public API surface.
280
+ Exclude trivial constructs whose name already fully conveys
281
+ the purpose (plain getters/setters, one-line lambdas,
282
+ delegating overloads) and constructs inheriting the
283
+ documentation of an overridden or implemented declaration.
284
+ Propose *adding* those missing comments.
285
+
286
+ - **C2 EXCESSIVE-DOCUMENTATION**: comments going far beyond
287
+ the minimal description: narrated decision logs ("chose X
288
+ over Y because ..."), change history ("now uses X instead of
289
+ Y"), line-by-line explanations of the obvious, or comment
290
+ blocks substantially longer than the code they describe.
291
+ Propose *condensing* to a brief 1-2 line description (up to
292
+ 4 lines only for genuinely non-obvious constraints).
293
+
294
+ - **C3 RESTATING-DOCUMENTATION**: comments merely repeating
295
+ the code or the identifier verbatim without adding any
296
+ information (e.g., "increment i" above `i++`). Propose
297
+ *removing* them.
298
+
299
+ - **C4 DRIFTED-DOCUMENTATION**: comments contradicting the
300
+ code they describe (stale parameter lists, outdated behavior
301
+ claims). Propose *correcting* the *comment* only -- *never*
302
+ change the code under this aspect.
303
+
304
+ Keep intact comments stating a *constraint the code cannot
305
+ show* (brief WHY-comments on non-obvious decisions) -- they are
306
+ neither excessive nor restating.
307
+
308
+ Severity guidance: C1 defaults to MEDIUM for non-trivial
309
+ constructs, else LOW; C2 and C3 default to LOW, escalating to
310
+ MEDIUM when the noise dominates the file; and C4 defaults to
311
+ MEDIUM (it actively misleads).
312
+
269
313
  Be conservative - only report clear, well-grounded issues
270
314
  that require an actual *code change*. Think twice to avoid
271
315
  *false positives*.
@@ -277,7 +321,7 @@ Workflow
277
321
 
278
322
  For *each* found problem which requires a code change:
279
323
 
280
- 1. Set <aspect/> to the identifier `A01 - XXX`...`A20 - XXX`,
324
+ 1. Set <aspect/> to the identifier `A01 - XXX`...`A21 - XXX`,
281
325
  indicating the aspect under which the problem was detected.
282
326
 
283
327
  2. Set <severity/> to the string `LOW`, `MEDIUM`, or `HIGH`,
@@ -6,8 +6,8 @@
6
6
 
7
7
  # [plugin] lint project
8
8
  lint
9
- markdownlint-cli2 --config etc/markdownlint.yaml meta/*.md skills/**/*.md && \
10
- eslint --config etc/eslint.mjs meta skills
9
+ markdownlint-cli2 --config etc/markdownlint.yaml meta/*.md skills/**/*.md '!meta/ase-format-specbook.md' && \
10
+ eslint --config etc/eslint.mjs --ignore-pattern meta/ase-format-specbook.md meta skills
11
11
 
12
12
  # [plugin] build project
13
13
  build : lint
@@ -17,10 +17,12 @@ build : lint
17
17
  echo "</purpose>"
18
18
  done
19
19
  ) >skills/ase-help-intent/data.md
20
+ specbook describe -p meta -f raw >meta/ase-format-specbook.md
21
+ specbook describe -p schema -f raw >meta/ase-format-specbook.yaml
20
22
 
21
23
  # [plugin] remove all generated files
22
24
  clean
23
- true
25
+ shx rm -f meta/ase-format-specbook.md meta/ase-format-specbook.yaml
24
26
 
25
27
  # [plugin] remove all built files
26
28
  distclean: clean