@rse/ase 0.9.62 → 0.9.64
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 +19 -8
- package/dst/ase-config.js +12 -8
- package/dst/ase-hook.js +9 -1
- package/dst/ase-service.js +2 -0
- package/dst/ase-spec.js +281 -0
- package/dst/ase.js +2 -0
- package/package.json +10 -8
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.github/plugin/plugin.json +1 -1
- package/plugin/etc/stx.conf +5 -3
- package/plugin/meta/ase-format-meta.md +23 -105
- package/plugin/meta/ase-format-spec.md +22 -1326
- package/plugin/meta/ase-tenets.md +63 -4
- package/plugin/package.json +6 -2
- package/plugin/skills/ase-arch-analyze/help.md +7 -0
- package/plugin/skills/ase-arch-discover/help.md +7 -0
- package/plugin/skills/ase-code-analyze/help.md +8 -0
- package/plugin/skills/ase-code-craft/help.md +7 -0
- package/plugin/skills/ase-code-dissect/help.md +7 -0
- package/plugin/skills/ase-code-edit/SKILL.md +14 -9
- package/plugin/skills/ase-code-edit/help.md +7 -0
- package/plugin/skills/ase-code-explain/help.md +7 -0
- package/plugin/skills/ase-code-insight/help.md +7 -0
- package/plugin/skills/ase-code-lint/help.md +8 -0
- package/plugin/skills/ase-code-refactor/help.md +7 -0
- package/plugin/skills/ase-code-resolve/help.md +7 -0
- package/plugin/skills/ase-docs-distill/help.md +7 -0
- package/plugin/skills/ase-docs-proofread/help.md +7 -0
- package/plugin/skills/ase-help-intent/SKILL.md +66 -43
- package/plugin/skills/ase-help-intent/help.md +27 -16
- package/plugin/skills/ase-help-skill/catalog.md +3 -0
- package/plugin/skills/ase-help-skill/help.md +7 -0
- package/plugin/skills/ase-meta-brainstorm/help.md +8 -0
- package/plugin/skills/ase-meta-changelog/help.md +6 -0
- package/plugin/skills/ase-meta-chat/help.md +6 -0
- package/plugin/skills/ase-meta-commit/help.md +6 -0
- package/plugin/skills/ase-meta-compat/help.md +6 -0
- package/plugin/skills/ase-meta-config/help.md +7 -0
- package/plugin/skills/ase-meta-diaboli/help.md +7 -0
- package/plugin/skills/ase-meta-diff/help.md +7 -0
- package/plugin/skills/ase-meta-eli5/help.md +6 -0
- package/plugin/skills/ase-meta-evaluate/help.md +7 -0
- package/plugin/skills/ase-meta-proximity/help.md +7 -0
- package/plugin/skills/ase-meta-quorum/help.md +6 -0
- package/plugin/skills/ase-meta-quotes/help.md +7 -0
- package/plugin/skills/ase-meta-review/help.md +8 -1
- package/plugin/skills/ase-meta-search/help.md +6 -0
- package/plugin/skills/ase-meta-steelman/help.md +6 -0
- package/plugin/skills/ase-meta-why/help.md +7 -0
- package/plugin/skills/ase-meta-workflow/help.md +7 -0
- package/plugin/skills/ase-spec-edit/SKILL.md +520 -0
- package/plugin/skills/ase-spec-edit/help.md +137 -0
- package/plugin/skills/ase-sync-export/SKILL.md +66 -110
- package/plugin/skills/ase-sync-export/help.md +43 -40
- package/plugin/skills/ase-sync-import/SKILL.md +37 -15
- package/plugin/skills/ase-sync-import/help.md +21 -10
- package/plugin/skills/ase-sync-reconcile/SKILL.md +37 -16
- package/plugin/skills/ase-sync-reconcile/help.md +26 -16
- package/plugin/skills/ase-task-condense/help.md +6 -0
- package/plugin/skills/ase-task-delete/help.md +6 -0
- package/plugin/skills/ase-task-dissect/help.md +7 -0
- package/plugin/skills/ase-task-edit/help.md +7 -0
- package/plugin/skills/ase-task-grill/SKILL.md +5 -4
- package/plugin/skills/ase-task-grill/help.md +7 -0
- package/plugin/skills/ase-task-id/help.md +6 -0
- package/plugin/skills/ase-task-implement/help.md +7 -0
- package/plugin/skills/ase-task-list/help.md +6 -0
- package/plugin/skills/ase-task-preflight/help.md +7 -0
- package/plugin/skills/ase-task-reboot/help.md +6 -0
- package/plugin/skills/ase-task-rename/help.md +6 -0
- package/plugin/skills/ase-task-view/help.md +6 -0
- package/plugin/meta/ase-format-arch.md +0 -1164
package/dst/ase-artifact.js
CHANGED
|
@@ -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", "
|
|
17
|
-
/* the
|
|
18
|
-
const configuredKinds = ["spec", "
|
|
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
|
|
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`, `
|
|
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`, `
|
|
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`, `
|
|
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`, `
|
|
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": "
|
|
52
|
-
"project.artifact.spec.files": "*.{md,txt}",
|
|
53
|
-
"project.artifact.
|
|
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.
|
|
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:
|
|
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` +
|
|
@@ -345,7 +353,7 @@ export default class HookCommand {
|
|
|
345
353
|
`\n⧉ ASE: ☯ persona: ${persona}, ▶ guidance: ${guidance}, ▢ boxing: ${boxing}` +
|
|
346
354
|
(guidance === "normal" || guidance === "verbose" ?
|
|
347
355
|
"\n" +
|
|
348
|
-
"\n⧉ ASE: ▷ hint: use \"/ase-help-intent <intent-description>\" for skill command
|
|
356
|
+
"\n⧉ ASE: ▷ hint: use \"/ase-help-intent <intent-description>\" for skill command proposals" +
|
|
349
357
|
"\n⧉ ASE: ▷ hint: use \"/ase-help-skill [<skill-name>]\" for skill catalog or skill manpage" : "");
|
|
350
358
|
/* inject markdown into session context.
|
|
351
359
|
Anthropic Claude Code CLI and OpenAI Codex CLI expect the context nested in
|
package/dst/ase-service.js
CHANGED
|
@@ -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);
|
package/dst/ase-spec.js
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
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
|
+
while its "notice" messages always reach the warning log and, for
|
|
37
|
+
consumers which never see the log, the given collector */
|
|
38
|
+
static api(log, verbose, notices) {
|
|
39
|
+
return new SpecBook({
|
|
40
|
+
verbose: (cmd, msg, level) => {
|
|
41
|
+
const text = renderVerbose(msg);
|
|
42
|
+
if (level === "notice") {
|
|
43
|
+
log.write("warning", `specbook: ${cmd}: ${text}`);
|
|
44
|
+
notices?.push(text);
|
|
45
|
+
}
|
|
46
|
+
else
|
|
47
|
+
log.write(verbose ? "info" : "debug", `specbook: ${cmd}: ${text}`);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/* SpecBook marks the literal values inside its messages, but only
|
|
52
|
+
its verbose messages ever reach a renderer, so strip the markers
|
|
53
|
+
off the error messages here at the API boundary -- unstyled, as
|
|
54
|
+
they travel on into the log and the MCP tool results */
|
|
55
|
+
static async unmarked(result) {
|
|
56
|
+
try {
|
|
57
|
+
return await result;
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
if (err instanceof Error)
|
|
61
|
+
err.message = renderVerbose(err.message);
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/* render a diagnostic file path relative to the project root,
|
|
66
|
+
keeping paths outside the project (like the bundled schema) as-is */
|
|
67
|
+
static relativize(file) {
|
|
68
|
+
const rel = path.relative(Task.projectRoot(), file);
|
|
69
|
+
if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel))
|
|
70
|
+
return file;
|
|
71
|
+
return rel.replace(/\\/g, "/");
|
|
72
|
+
}
|
|
73
|
+
/* lint the specification Markdown files below the "spec" artifact
|
|
74
|
+
base directory against the schema configuration */
|
|
75
|
+
static async lint(log, verbose = false) {
|
|
76
|
+
const result = await Spec.unmarked(Spec.api(log, verbose).lint({
|
|
77
|
+
config: Spec.configFile(log),
|
|
78
|
+
basedir: Artifact.basedir(log, "spec")
|
|
79
|
+
}));
|
|
80
|
+
return result.diagnostics.map((d) => ({ ...d, file: Spec.relativize(d.file) }));
|
|
81
|
+
}
|
|
82
|
+
/* render a diagnostic as a multi-line message with the affected
|
|
83
|
+
source snippet (like the verbose SpecBook CLI), falling back to
|
|
84
|
+
the single-line message when the source file is unreadable or
|
|
85
|
+
empty (as there is no snippet to show) */
|
|
86
|
+
static render(diagnostic, colors) {
|
|
87
|
+
let code;
|
|
88
|
+
try {
|
|
89
|
+
code = fs.readFileSync(path.resolve(Task.projectRoot(), diagnostic.file), "utf8");
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return `${renderDiagnostic(diagnostic)}\n`;
|
|
93
|
+
}
|
|
94
|
+
if (code === "")
|
|
95
|
+
return `${renderDiagnostic(diagnostic)}\n`;
|
|
96
|
+
return sourceCodeError({
|
|
97
|
+
message: diagnostic.message,
|
|
98
|
+
filename: diagnostic.file,
|
|
99
|
+
code,
|
|
100
|
+
line: diagnostic.line,
|
|
101
|
+
column: diagnostic.column,
|
|
102
|
+
colors
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/* export the specification Markdown files below the "spec" artifact
|
|
106
|
+
base directory into the requested formats, one buffer per format,
|
|
107
|
+
collecting the emitted environment notices if requested */
|
|
108
|
+
static export(log, formats, verbose = false, notices) {
|
|
109
|
+
return Spec.unmarked(Spec.api(log, verbose, notices).export({
|
|
110
|
+
config: Spec.configFile(log),
|
|
111
|
+
basedir: Artifact.basedir(log, "spec"),
|
|
112
|
+
formats
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/* CLI command "ase spec" */
|
|
117
|
+
export default class SpecCommand {
|
|
118
|
+
log;
|
|
119
|
+
constructor(log) {
|
|
120
|
+
this.log = log;
|
|
121
|
+
}
|
|
122
|
+
/* register commands */
|
|
123
|
+
register(program) {
|
|
124
|
+
/* register CLI top-level command "ase spec" */
|
|
125
|
+
const spec = program
|
|
126
|
+
.command("spec")
|
|
127
|
+
.description("Lint and export the SpecBook-based project specification")
|
|
128
|
+
.action(() => {
|
|
129
|
+
spec.outputHelp();
|
|
130
|
+
process.exit(1);
|
|
131
|
+
});
|
|
132
|
+
/* register CLI sub-command "ase spec lint" */
|
|
133
|
+
spec
|
|
134
|
+
.command("lint")
|
|
135
|
+
.description("Lint the specification Markdown files against the SpecBook schema configuration")
|
|
136
|
+
.option("-v, --verbose", "print verbose processing information and each diagnostic with its affected source snippet")
|
|
137
|
+
.action(async (opts) => {
|
|
138
|
+
const diagnostics = await Spec.lint(this.log, opts.verbose === true);
|
|
139
|
+
for (const diagnostic of diagnostics)
|
|
140
|
+
await writeStdout(opts.verbose === true ?
|
|
141
|
+
Spec.render(diagnostic, process.stdout.isTTY === true) :
|
|
142
|
+
`${renderDiagnostic(diagnostic)}\n`);
|
|
143
|
+
if (diagnostics.length > 0)
|
|
144
|
+
process.exitCode = 1;
|
|
145
|
+
});
|
|
146
|
+
/* register CLI sub-command "ase spec export" */
|
|
147
|
+
spec
|
|
148
|
+
.command("export")
|
|
149
|
+
.description("Export the specification Markdown files as JSON, JSON5, YAML, TOON, HTML, PDF, or normalized Markdown")
|
|
150
|
+
.option("-o, --output <[format:]file>", "output file (\"-\" for stdout, repeatable), with the format inferred " +
|
|
151
|
+
"from the filename extension unless explicitly prefixed " +
|
|
152
|
+
"(default: \"index.html\" inside the specification base directory)", (value, previous) => previous.concat(value), new Array())
|
|
153
|
+
.option("-v, --verbose", "print verbose processing information")
|
|
154
|
+
.action(async (opts) => {
|
|
155
|
+
const outputs = (opts.output.length > 0 ? opts.output :
|
|
156
|
+
[path.join(Artifact.basedir(this.log, "spec"), "index.html")]).map(parseOutputSpec);
|
|
157
|
+
const distinct = Array.from(new Set(outputs.map(({ format }) => format)));
|
|
158
|
+
const buffers = await Spec.export(this.log, distinct, opts.verbose === true);
|
|
159
|
+
for (const { format, output } of outputs) {
|
|
160
|
+
const data = buffers[distinct.indexOf(format)];
|
|
161
|
+
if (output === "-")
|
|
162
|
+
await writeStdout(data);
|
|
163
|
+
else {
|
|
164
|
+
await fs.promises.writeFile(output, data);
|
|
165
|
+
this.log.write("info", `spec: exported specification into "${output}" (${data.length} bytes)`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/* render a caught error as an MCP tool error result */
|
|
172
|
+
const mcpToolError = (err) => ({
|
|
173
|
+
isError: true,
|
|
174
|
+
content: [{ type: "text", text: `ERROR: ${err instanceof Error ? err.message : String(err)}` }]
|
|
175
|
+
});
|
|
176
|
+
/* MCP registration entry point for SpecBook tools */
|
|
177
|
+
export class SpecMCP {
|
|
178
|
+
log;
|
|
179
|
+
constructor(log) {
|
|
180
|
+
this.log = log;
|
|
181
|
+
}
|
|
182
|
+
/* register MCP tools */
|
|
183
|
+
register(mcp) {
|
|
184
|
+
mcp.registerTool("ase_specbook_lint", {
|
|
185
|
+
title: "ASE SpecBook lint",
|
|
186
|
+
description: "Lint the SpecBook specification Markdown files of the project (located via the " +
|
|
187
|
+
"`project.artifact.spec.basedir` configuration) against the SpecBook YAML schema " +
|
|
188
|
+
"configuration (`project.artifact.spec.schema`, defaulting to the bundled `ase-format-specbook.yaml`). " +
|
|
189
|
+
"Returns a `diagnostics` array of `{ file, line, column, message }` objects (with " +
|
|
190
|
+
"project-relative `file`), rendered as bullet points in `text`. With `verbose`, " +
|
|
191
|
+
"each diagnostic additionally carries a multi-line `snippet` rendering with the " +
|
|
192
|
+
"affected source lines, which is also used for `text`. An empty array " +
|
|
193
|
+
"(`text` of `specification valid`) means the specification is valid.",
|
|
194
|
+
inputSchema: {
|
|
195
|
+
verbose: z.boolean().optional()
|
|
196
|
+
.describe("if true, render each diagnostic with its affected source snippet (default: false)")
|
|
197
|
+
},
|
|
198
|
+
outputSchema: {
|
|
199
|
+
diagnostics: z.array(z.object({
|
|
200
|
+
file: z.string().describe("project-relative file path"),
|
|
201
|
+
line: z.number().describe("line number (1-based)"),
|
|
202
|
+
column: z.number().describe("column number (1-based)"),
|
|
203
|
+
message: z.string().describe("diagnostic message"),
|
|
204
|
+
snippet: z.string().optional()
|
|
205
|
+
.describe("multi-line rendering with the affected source snippet (with `verbose` only)")
|
|
206
|
+
})).describe("lint diagnostics, empty if the specification is valid")
|
|
207
|
+
}
|
|
208
|
+
}, async (args) => {
|
|
209
|
+
try {
|
|
210
|
+
const verbose = args.verbose ?? false;
|
|
211
|
+
const diagnostics = (await Spec.lint(this.log))
|
|
212
|
+
.map((d) => verbose ? { ...d, snippet: Spec.render(d, false) } : d);
|
|
213
|
+
const text = diagnostics.length === 0 ? "specification valid" :
|
|
214
|
+
verbose ?
|
|
215
|
+
diagnostics.map((d) => d.snippet ?? "").join("").replace(/\n$/, "") :
|
|
216
|
+
diagnostics.map((d) => `- ${renderDiagnostic(d)}`).join("\n");
|
|
217
|
+
return {
|
|
218
|
+
structuredContent: { diagnostics },
|
|
219
|
+
content: [{ type: "text", text }]
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
return mcpToolError(err);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
mcp.registerTool("ase_specbook_export", {
|
|
227
|
+
title: "ASE SpecBook export",
|
|
228
|
+
description: "Export the SpecBook specification Markdown files of the project (located via the " +
|
|
229
|
+
"`project.artifact.spec.basedir` configuration) as JSON, JSON5, YAML, TOON, HTML, PDF, " +
|
|
230
|
+
"or normalized Markdown. The result is written to the `output` file (a relative path " +
|
|
231
|
+
"resolves against the project root) if given, else it is returned directly " +
|
|
232
|
+
"(PDF as a base64-encoded resource). The export fails on any lint diagnostic. " +
|
|
233
|
+
"The `notices` array carries the environment notices emitted during the export " +
|
|
234
|
+
"(like a PDF rendering falling back onto a system-installed browser), which are " +
|
|
235
|
+
"worth reporting to the user verbatim.",
|
|
236
|
+
inputSchema: {
|
|
237
|
+
format: z.enum(formats).optional()
|
|
238
|
+
.describe("output format (default: inferred from the `output` file extension, else `json`)"),
|
|
239
|
+
output: z.string().optional()
|
|
240
|
+
.describe("output file path (\"-\" or omitted returns the result directly)")
|
|
241
|
+
},
|
|
242
|
+
outputSchema: {
|
|
243
|
+
notices: z.array(z.string())
|
|
244
|
+
.describe("environment notices emitted during the export, empty if there were none")
|
|
245
|
+
}
|
|
246
|
+
}, async (args) => {
|
|
247
|
+
const notices = new Array();
|
|
248
|
+
try {
|
|
249
|
+
/* an explicit format takes the output as a plain file path, while
|
|
250
|
+
otherwise the output is an "[<format>:]<file>" specification */
|
|
251
|
+
const spec = args.format !== undefined || args.output === undefined ?
|
|
252
|
+
{ format: args.format ?? "json", output: args.output } :
|
|
253
|
+
parseOutputSpec(args.output);
|
|
254
|
+
const [data] = await Spec.export(this.log, [spec.format], false, notices);
|
|
255
|
+
if (spec.output !== undefined && spec.output !== "-") {
|
|
256
|
+
await fs.promises.writeFile(path.resolve(Task.projectRoot(), spec.output), data);
|
|
257
|
+
return {
|
|
258
|
+
structuredContent: { notices },
|
|
259
|
+
content: [{ type: "text", text: `exported specification into "${spec.output}" (${data.length} bytes)` }]
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
else if (spec.format === "pdf")
|
|
263
|
+
return {
|
|
264
|
+
structuredContent: { notices },
|
|
265
|
+
content: [{
|
|
266
|
+
type: "resource",
|
|
267
|
+
resource: { uri: "ase:specbook-export.pdf", mimeType: "application/pdf", blob: data.toString("base64") }
|
|
268
|
+
}]
|
|
269
|
+
};
|
|
270
|
+
else
|
|
271
|
+
return {
|
|
272
|
+
structuredContent: { notices },
|
|
273
|
+
content: [{ type: "text", text: data.toString("utf8") }]
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
catch (err) {
|
|
277
|
+
return mcpToolError(err);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
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.
|
|
9
|
+
"version": "0.9.64",
|
|
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.
|
|
19
|
+
"eslint": "10.9.1",
|
|
20
20
|
"@eslint/js": "10.0.1",
|
|
21
|
-
"@typescript-eslint/parser": "8.
|
|
22
|
-
"@typescript-eslint/eslint-plugin": "8.
|
|
23
|
-
"typescript-eslint": "8.
|
|
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.
|
|
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.
|
|
68
|
-
"lru-cache": "11.5.2"
|
|
67
|
+
"picomatch": "4.0.7",
|
|
68
|
+
"lru-cache": "11.5.2",
|
|
69
|
+
"@rse/specbook": "1.0.3",
|
|
70
|
+
"source-code-error": "1.2.0"
|
|
69
71
|
},
|
|
70
72
|
"allowScripts": {
|
|
71
73
|
"fsevents": true
|
package/plugin/etc/stx.conf
CHANGED
|
@@ -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
|
-
|
|
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
|