@rse/ase 2.0.3 → 2.0.5

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 (198) hide show
  1. package/dst/ase-hook.js +13 -4
  2. package/dst/ase-lib-version.js +14 -5
  3. package/dst/ase-man-catalog.js +229 -0
  4. package/dst/ase-man-render.js +278 -0
  5. package/dst/ase-man-tui.js +636 -0
  6. package/dst/ase-man.js +33 -27
  7. package/dst/ase-setup.js +2 -2
  8. package/dst/ase-spec.js +103 -20
  9. package/dst/ase-task-board-filter.js +5 -4
  10. package/dst/ase-task-board-web-client.js +26 -26
  11. package/dst/man/ase-agent-adhoc.md +1165 -0
  12. package/dst/man/ase-agent-condenser.md +383 -0
  13. package/dst/man/ase-agent-editor.md +441 -0
  14. package/dst/man/ase-agent-estimator.md +477 -0
  15. package/dst/man/ase-agent-fleet.md +358 -0
  16. package/dst/man/ase-agent-implementer.md +521 -0
  17. package/dst/man/ase-agent-integrator.md +449 -0
  18. package/dst/man/ase-agent-qualifier.md +426 -0
  19. package/dst/man/ase-agent-rebooter.md +384 -0
  20. package/dst/man/ase-agent-relay.md +262 -0
  21. package/dst/man/ase-agent-researcher.md +478 -0
  22. package/dst/man/ase-agent.md +628 -0
  23. package/dst/man/ase-artifact-list.md +57 -0
  24. package/dst/man/ase-artifact-name.md +56 -0
  25. package/dst/man/ase-artifact.md +92 -0
  26. package/dst/man/ase-config-delete.md +74 -0
  27. package/dst/man/ase-config-edit.md +83 -0
  28. package/dst/man/ase-config-get.md +70 -0
  29. package/dst/man/ase-config-init.md +110 -0
  30. package/dst/man/ase-config-list.md +68 -0
  31. package/dst/man/ase-config-set.md +92 -0
  32. package/dst/man/ase-config.md +426 -0
  33. package/dst/man/ase-hook-permission-request.md +61 -0
  34. package/dst/man/ase-hook-pre-tool-use.md +96 -0
  35. package/dst/man/ase-hook-session-end.md +71 -0
  36. package/dst/man/ase-hook-session-persona.md +65 -0
  37. package/dst/man/ase-hook-session-start.md +184 -0
  38. package/dst/man/ase-hook-stop.md +52 -0
  39. package/dst/man/ase-hook-user-prompt-submit.md +54 -0
  40. package/dst/man/ase-hook.md +93 -0
  41. package/dst/man/ase-man.md +195 -0
  42. package/dst/man/ase-mcp.md +102 -0
  43. package/dst/man/ase-service-log.md +59 -0
  44. package/dst/man/ase-service-send.md +60 -0
  45. package/dst/man/ase-service-start.md +93 -0
  46. package/dst/man/ase-service-status.md +58 -0
  47. package/dst/man/ase-service-stop.md +52 -0
  48. package/dst/man/ase-service.md +124 -0
  49. package/dst/man/ase-setup-disable.md +67 -0
  50. package/dst/man/ase-setup-enable.md +66 -0
  51. package/dst/man/ase-setup-install.md +116 -0
  52. package/dst/man/ase-setup-mcp.md +213 -0
  53. package/dst/man/ase-setup-status.md +101 -0
  54. package/dst/man/ase-setup-statusline.md +162 -0
  55. package/dst/man/ase-setup-telemetry.md +166 -0
  56. package/dst/man/ase-setup-uninstall.md +85 -0
  57. package/dst/man/ase-setup-update.md +117 -0
  58. package/dst/man/ase-setup.md +156 -0
  59. package/dst/man/ase-spec-export.md +99 -0
  60. package/dst/man/ase-spec-init.md +57 -0
  61. package/dst/man/ase-spec-lint.md +74 -0
  62. package/dst/man/ase-spec-preview.md +57 -0
  63. package/dst/man/ase-spec.md +85 -0
  64. package/dst/man/ase-statusline.md +330 -0
  65. package/dst/man/ase-task-assign.md +138 -0
  66. package/dst/man/ase-task-board.md +651 -0
  67. package/dst/man/ase-task-delete.md +46 -0
  68. package/dst/man/ase-task-edit.md +73 -0
  69. package/dst/man/ase-task-idscheme.md +92 -0
  70. package/dst/man/ase-task-lifecycle.md +92 -0
  71. package/dst/man/ase-task-list.md +86 -0
  72. package/dst/man/ase-task-load.md +48 -0
  73. package/dst/man/ase-task-newid.md +100 -0
  74. package/dst/man/ase-task-purge.md +49 -0
  75. package/dst/man/ase-task-rename.md +69 -0
  76. package/dst/man/ase-task-request.md +108 -0
  77. package/dst/man/ase-task-save.md +78 -0
  78. package/dst/man/ase-task-status.md +79 -0
  79. package/dst/man/ase-task-store.md +377 -0
  80. package/dst/man/ase-task-view.md +50 -0
  81. package/dst/man/ase-task.md +335 -0
  82. package/dst/man/ase-util-compat.md +38 -0
  83. package/dst/man/ase-util-diagram.md +140 -0
  84. package/dst/man/ase-util-meta.md +68 -0
  85. package/dst/man/ase-util-metric.md +74 -0
  86. package/dst/man/ase-util-mint.md +85 -0
  87. package/dst/man/ase-util-worktree.md +109 -0
  88. package/dst/man/ase-util.md +81 -0
  89. package/dst/man/ase.md +228 -0
  90. package/package.json +16 -21
  91. package/plugin/.claude-plugin/plugin.json +1 -1
  92. package/plugin/.codex-plugin/plugin.json +1 -1
  93. package/plugin/.github/plugin/plugin.json +1 -1
  94. package/plugin/agents/ase-code-lint.md +3 -3
  95. package/plugin/agents/ase-docs-proofread.md +3 -3
  96. package/plugin/agents/ase-docs-refine.md +3 -3
  97. package/plugin/agents/ase-spec-lint.md +338 -0
  98. package/plugin/mods/ase-mods-common.ts +444 -0
  99. package/plugin/mods/ase-mods-grill.ts +312 -0
  100. package/plugin/mods/ase-mods-help-browse.ts +405 -0
  101. package/plugin/mods/ase-mods-help-compose.ts +561 -0
  102. package/plugin/mods/ase-mods-help-find.ts +426 -0
  103. package/plugin/mods/ase-mods-help-hint.ts +134 -0
  104. package/plugin/mods/ase-mods-help-show.ts +142 -0
  105. package/plugin/mods/ase-mods.ts +214 -381
  106. package/plugin/mods/claude-code.d.ts +26 -3
  107. package/plugin/skills/ase-code-lint/SKILL.md +15 -15
  108. package/plugin/skills/ase-code-lint/help.md +21 -8
  109. package/plugin/skills/ase-help-intent/SKILL.md +2 -1
  110. package/plugin/skills/ase-help-intent/data.md +214 -9
  111. package/plugin/skills/ase-help-skill/catalog.md +1 -0
  112. package/plugin/skills/ase-meta-workflow/workflow.txt +2 -0
  113. package/plugin/skills/ase-repo-commit/SKILL.md +1 -0
  114. package/plugin/skills/ase-repo-merge/SKILL.md +1 -0
  115. package/plugin/skills/ase-spec-edit/SKILL.md +3 -3
  116. package/plugin/skills/ase-spec-edit/help.md +1 -1
  117. package/plugin/skills/ase-spec-lint/SKILL.md +695 -0
  118. package/plugin/skills/ase-spec-lint/help.md +184 -0
  119. package/plugin/skills/ase-task-implement/SKILL.md +24 -6
  120. package/plugin/skills/ase-task-implement/help.md +11 -1
  121. package/dst/man/ase-agent-adhoc.1 +0 -515
  122. package/dst/man/ase-agent-condenser.1 +0 -193
  123. package/dst/man/ase-agent-editor.1 +0 -212
  124. package/dst/man/ase-agent-estimator.1 +0 -214
  125. package/dst/man/ase-agent-fleet.1 +0 -185
  126. package/dst/man/ase-agent-implementer.1 +0 -233
  127. package/dst/man/ase-agent-integrator.1 +0 -213
  128. package/dst/man/ase-agent-qualifier.1 +0 -214
  129. package/dst/man/ase-agent-rebooter.1 +0 -193
  130. package/dst/man/ase-agent-relay.1 +0 -128
  131. package/dst/man/ase-agent-researcher.1 +0 -217
  132. package/dst/man/ase-agent.1 +0 -264
  133. package/dst/man/ase-artifact-list.1 +0 -38
  134. package/dst/man/ase-artifact-name.1 +0 -39
  135. package/dst/man/ase-artifact.1 +0 -56
  136. package/dst/man/ase-config-delete.1 +0 -45
  137. package/dst/man/ase-config-edit.1 +0 -48
  138. package/dst/man/ase-config-get.1 +0 -46
  139. package/dst/man/ase-config-init.1 +0 -60
  140. package/dst/man/ase-config-list.1 +0 -42
  141. package/dst/man/ase-config-set.1 +0 -52
  142. package/dst/man/ase-config.1 +0 -231
  143. package/dst/man/ase-hook-permission-request.1 +0 -36
  144. package/dst/man/ase-hook-pre-tool-use.1 +0 -59
  145. package/dst/man/ase-hook-session-end.1 +0 -47
  146. package/dst/man/ase-hook-session-persona.1 +0 -37
  147. package/dst/man/ase-hook-session-start.1 +0 -81
  148. package/dst/man/ase-hook-stop.1 +0 -34
  149. package/dst/man/ase-hook-user-prompt-submit.1 +0 -34
  150. package/dst/man/ase-hook.1 +0 -51
  151. package/dst/man/ase-man.1 +0 -67
  152. package/dst/man/ase-mcp.1 +0 -61
  153. package/dst/man/ase-service-log.1 +0 -43
  154. package/dst/man/ase-service-send.1 +0 -47
  155. package/dst/man/ase-service-start.1 +0 -54
  156. package/dst/man/ase-service-status.1 +0 -46
  157. package/dst/man/ase-service-stop.1 +0 -37
  158. package/dst/man/ase-service.1 +0 -85
  159. package/dst/man/ase-setup-disable.1 +0 -44
  160. package/dst/man/ase-setup-enable.1 +0 -45
  161. package/dst/man/ase-setup-install.1 +0 -73
  162. package/dst/man/ase-setup-mcp.1 +0 -124
  163. package/dst/man/ase-setup-status.1 +0 -60
  164. package/dst/man/ase-setup-statusline.1 +0 -92
  165. package/dst/man/ase-setup-telemetry.1 +0 -92
  166. package/dst/man/ase-setup-uninstall.1 +0 -59
  167. package/dst/man/ase-setup-update.1 +0 -64
  168. package/dst/man/ase-setup.1 +0 -93
  169. package/dst/man/ase-spec-export.1 +0 -62
  170. package/dst/man/ase-spec-lint.1 +0 -40
  171. package/dst/man/ase-spec-preview.1 +0 -40
  172. package/dst/man/ase-spec.1 +0 -46
  173. package/dst/man/ase-statusline.1 +0 -189
  174. package/dst/man/ase-task-assign.1 +0 -71
  175. package/dst/man/ase-task-board.1 +0 -384
  176. package/dst/man/ase-task-delete.1 +0 -35
  177. package/dst/man/ase-task-edit.1 +0 -46
  178. package/dst/man/ase-task-idscheme.1 +0 -65
  179. package/dst/man/ase-task-lifecycle.1 +0 -57
  180. package/dst/man/ase-task-list.1 +0 -47
  181. package/dst/man/ase-task-load.1 +0 -36
  182. package/dst/man/ase-task-newid.1 +0 -61
  183. package/dst/man/ase-task-purge.1 +0 -37
  184. package/dst/man/ase-task-rename.1 +0 -44
  185. package/dst/man/ase-task-request.1 +0 -61
  186. package/dst/man/ase-task-save.1 +0 -48
  187. package/dst/man/ase-task-status.1 +0 -55
  188. package/dst/man/ase-task-store.1 +0 -194
  189. package/dst/man/ase-task-view.1 +0 -40
  190. package/dst/man/ase-task.1 +0 -160
  191. package/dst/man/ase-util-compat.1 +0 -25
  192. package/dst/man/ase-util-diagram.1 +0 -76
  193. package/dst/man/ase-util-meta.1 +0 -45
  194. package/dst/man/ase-util-metric.1 +0 -54
  195. package/dst/man/ase-util-mint.1 +0 -59
  196. package/dst/man/ase-util-worktree.1 +0 -72
  197. package/dst/man/ase-util.1 +0 -50
  198. package/dst/man/ase.1 +0 -124
package/dst/ase-man.js CHANGED
@@ -5,13 +5,14 @@
5
5
  */
6
6
  import path from "node:path";
7
7
  import fs from "node:fs";
8
- import { spawnSync } from "node:child_process";
9
8
  import { fileURLToPath } from "node:url";
10
9
  import { writeStdout } from "./ase-lib-stdio.js";
10
+ /* the width of the manual pages printed as plain text */
11
+ const WIDTH = 80;
11
12
  /* reusable functionality: resolve the internally shipped manual pages */
12
13
  export class Man {
13
14
  /* determine the directory of the manual pages; the build process
14
- renders "man/*.md" into "dst/man/*.1" (see the "build-man" build
15
+ copies "man/*.md" into "dst/man/*.md" (see the "build-man" build
15
16
  step), right next to the compiled tool */
16
17
  static dir() {
17
18
  return path.join(path.dirname(fileURLToPath(import.meta.url)), "man");
@@ -26,15 +27,15 @@ export class Man {
26
27
  throw new Error(`man: no manual pages found in "${Man.dir()}" (tool not completely built?)`, { cause: err });
27
28
  }
28
29
  return files
29
- .filter((file) => /^ase(?:-[a-z0-9-]+)?\.\d$/.test(file))
30
- .map((file) => file.replace(/\.\d$/, ""))
30
+ .filter((file) => /^ase(?:-[a-z0-9-]+)?\.md$/.test(file))
31
+ .map((file) => file.replace(/\.md$/, ""))
31
32
  .sort((a, b) => a.localeCompare(b, "en", { numeric: true }));
32
33
  }
33
- /* resolve the words of a requested manual page to the absolute path of
34
- its file: the words are joined with dashes, the "ase" prefix is optional,
35
- and a command path deeper than the shipped pages (e.g. "ase task store
36
- start") falls back to the page of its nearest parent (e.g. "ase-task-store"),
37
- as the pages of a group with third-level commands cover them */
34
+ /* resolve the words of a requested manual page to its name: the words
35
+ are joined with dashes, the "ase" prefix is optional, and a command path
36
+ deeper than the shipped pages (e.g. "ase task store start") falls back to
37
+ the page of its nearest parent (e.g. "ase-task-store"), as the pages of a
38
+ group with third-level commands cover them */
38
39
  static resolve(words) {
39
40
  const name = words.join("-").toLowerCase();
40
41
  if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name))
@@ -46,7 +47,7 @@ export class Man {
46
47
  while (parts.length > 0) {
47
48
  const page = parts.join("-");
48
49
  if (pages.has(page))
49
- return path.join(Man.dir(), `${page}.1`);
50
+ return page;
50
51
  parts.pop();
51
52
  if (parts.length === 1)
52
53
  break;
@@ -64,33 +65,38 @@ export default class ManCommand {
64
65
  register(program) {
65
66
  program
66
67
  .command("man")
67
- .description("Show an internally shipped manual page with man(1)")
68
+ .description("Browse all manual pages interactively, or print a manual page as plain text")
68
69
  .argument("[page...]", "manual page name, either as \"ase-<command>[-<sub-command>]\" " +
69
- "or as the words of the command path (\"ase\" prefix optional, default: \"ase\")")
70
+ "or as the words of the command path (\"ase\" prefix optional), selected in the " +
71
+ "interactive browser on a terminal (or searched for, if no manual page fits exactly), " +
72
+ "else printed as plain text (default: \"ase\")")
70
73
  .option("-l, --list", "list the names of all shipped manual pages instead", false)
71
- .option("-p, --path", "print the path of the manual page file instead", false)
72
74
  .action(async (words, opts) => {
73
75
  /* list all manual pages */
74
76
  if (opts.list) {
75
77
  await writeStdout(Man.list().map((page) => `${page}\n`).join(""));
76
78
  return;
77
79
  }
78
- /* resolve manual page */
79
- const file = Man.resolve(words.length > 0 ? words : ["ase"]);
80
- if (opts.path) {
81
- await writeStdout(`${file}\n`);
80
+ /* browse all manual pages interactively (only on a terminal), with
81
+ the given manual page selected resp. searched for (if any) */
82
+ if (process.stdin.isTTY && process.stdout.isTTY) {
83
+ const { runManTUI } = await import("./ase-man-tui.js");
84
+ await runManTUI(this.log, words);
82
85
  return;
83
86
  }
84
- /* show manual page with man(1), passing the file by its path,
85
- which all common man(1) implementations support */
86
- this.log.write("debug", `man: showing manual page "${file}"`);
87
- const result = spawnSync("man", [file], { stdio: "inherit" });
88
- if (result.error !== undefined) {
89
- const message = result.error.message;
90
- throw new Error(`man: failed to run man(1): ${message} ` +
91
- `(the manual page file is "${file}")`, { cause: result.error });
92
- }
93
- process.exitCode = result.status ?? 1;
87
+ /* resolve manual page, where a skill or command fitting exactly
88
+ is preferred over the fallback onto the nearest parent command */
89
+ const { loadCatalog, findPage } = await import("./ase-man-catalog.js");
90
+ const { renderPage } = await import("./ase-man-render.js");
91
+ const catalog = loadCatalog();
92
+ const request = words.length > 0 ? words : ["ase"];
93
+ const page = findPage(catalog, request) ?? catalog.pages.get(`${Man.resolve(request)}.1`);
94
+ if (page === undefined)
95
+ throw new Error(`man: no manual page found for "${request.join(" ")}" (see "ase man --list")`);
96
+ /* print manual page as plain text */
97
+ this.log.write("debug", `man: printing manual page "${page.id}"`);
98
+ const { lines } = renderPage(page.md, WIDTH, page.id, (id) => catalog.pages.has(id));
99
+ await writeStdout(lines.map((line) => line.map((span) => span.text).join("").trimEnd() + "\n").join(""));
94
100
  });
95
101
  }
96
102
  }
package/dst/ase-setup.js CHANGED
@@ -71,10 +71,10 @@ export default class SetupCommand {
71
71
  await this.runner.run(spec.cli, ["plugin", spec.pInstall, "ase@ase", ...scopeArgs], { retries: 3 });
72
72
  }
73
73
  else {
74
- /* perform NPM version check */
74
+ /* perform direct NPM registry version check */
75
75
  const current = Version.current();
76
76
  this.log.write("info", `setup: update: used ASE version: ${current}`);
77
- const latest = await Version.latest();
77
+ const latest = await Version.latest(true);
78
78
  if (!force && latest !== "" && latest === current) {
79
79
  this.log.write("info", `setup: update: ASE already at latest version ${current}`);
80
80
  return 0;
package/dst/ase-spec.js CHANGED
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import path from "node:path";
7
7
  import fs from "node:fs";
8
+ import os from "node:os";
8
9
  import { isScalar } from "yaml";
9
10
  import { z } from "zod";
10
11
  import sourceCodeError from "source-code-error";
@@ -14,11 +15,11 @@ import { Config } from "./ase-config-core.js";
14
15
  import { configSchema } from "./ase-config-schema.js";
15
16
  import { Task } from "./ase-task.js";
16
17
  import { Artifact } from "./ase-artifact.js";
17
- import { Meta } from "./ase-util-meta.js";
18
18
  import { writeStdout } from "./ase-lib-stdio.js";
19
- /* reusable functionality: lint, export, and preview the SpecBook-based
20
- project specification, located via the "project.artifact.spec.basedir"
21
- and "project.artifact.spec.schema" configuration */
19
+ /* reusable functionality: initialize, lint, export, and preview the
20
+ SpecBook-based project specification, located via the
21
+ "project.artifact.spec.basedir" and "project.artifact.spec.schema"
22
+ configuration */
22
23
  export class Spec {
23
24
  /* the SpecBook export formats, mirrored here (and checked for completeness
24
25
  against SpecBook at compile-time) to not load SpecBook for the MCP schema */
@@ -28,8 +29,9 @@ export class Spec {
28
29
  /* resolve the YAML schema configuration files: the whitespace-separated
29
30
  entries of the configured "project.artifact.spec.schema", merged in
30
31
  the given order, where each entry is either the literal "std" for the
31
- bundled standard "ase-format-specbook.yaml" plugin meta file or a file
32
- path relative to the project root, and an unset or empty value means "std" */
32
+ full SpecBook standard schema (not the agent-compressed plugin meta file
33
+ "ase-format-specbook.yaml") or a file path relative to the project
34
+ root, and an unset or empty value means "std" */
33
35
  static configFiles(log) {
34
36
  const cfg = new Config("config", configSchema, log);
35
37
  cfg.read();
@@ -39,7 +41,7 @@ export class Spec {
39
41
  if (entries.length === 0)
40
42
  entries.push("std");
41
43
  return entries.map((entry) => entry === "std" ?
42
- Meta.resolve("ase-format-specbook.yaml") :
44
+ "std" :
43
45
  path.resolve(Task.projectRoot(), entry));
44
46
  }
45
47
  /* the ASE log level each SpecBook verbosity level maps onto: the
@@ -93,21 +95,81 @@ export class Spec {
93
95
  return file;
94
96
  return rel.replace(/\\/g, "/");
95
97
  }
96
- /* every specification command honors the Git exclude rules, so that
98
+ /* every reading specification command honors the Git exclude rules, so that
97
99
  SpecBook resolves the very same artifact files as "ase artifact
98
100
  list --kind spec": both consult ".gitignore", so a specification
99
101
  the project does not track is invisible to both instead of only
100
102
  to one of them */
101
103
  static gitignore = true;
104
+ /* initialize the absent artifact files of the schema configuration below
105
+ the "spec" artifact base directory, letting SpecBook persist its project
106
+ options file ".specbook.yaml" into a throw-away working directory, as
107
+ ASE keeps these options in its own configuration */
108
+ static async init(log) {
109
+ const basedir = Artifact.basedir(log, "spec");
110
+ const cwd = await fs.promises.mkdtemp(path.join(os.tmpdir(), "ase-spec-"));
111
+ try {
112
+ const result = await Spec.unmarked(Spec.api(log).then((api) => api.init({
113
+ config: Spec.configFiles(log),
114
+ basedir,
115
+ cwd
116
+ })));
117
+ return result.files.map((file) => Spec.relativize(path.join(basedir, file)));
118
+ }
119
+ finally {
120
+ await fs.promises.rm(cwd, { recursive: true, force: true });
121
+ }
122
+ }
102
123
  /* lint the specification Markdown files below the "spec" artifact
103
- base directory against the schema configuration */
104
- static async lint(log) {
124
+ base directory against the schema configuration, optionally
125
+ followed by the generic Markdown linting */
126
+ static async lint(log, markdown = false) {
105
127
  const result = await Spec.unmarked(Spec.api(log).then((api) => api.lint({
106
128
  config: Spec.configFiles(log),
107
129
  basedir: Artifact.basedir(log, "spec"),
108
130
  gitignore: Spec.gitignore
109
131
  })));
110
- return result.diagnostics.map((d) => ({ ...d, file: Spec.relativize(d.file) }));
132
+ const diagnostics = result.diagnostics.map((d) => ({ ...d, file: Spec.relativize(d.file) }));
133
+ return markdown ? diagnostics.concat(await Spec.markdownlint(log)) : diagnostics;
134
+ }
135
+ /* the markdownlint rules conflicting with the SpecBook format
136
+ (trailing blank line, mixed ATX and Setext headings, free ordered list numbering,
137
+ padded list markers and headings, unwrapped lines, frontmatter first, images without
138
+ alternate text, and multiple artifacts or same-named objects per file) */
139
+ static markdownConfig = {
140
+ "no-multiple-blanks": { maximum: 2 },
141
+ "heading-style": false,
142
+ "ol-prefix": false,
143
+ "no-alt-text": false,
144
+ "list-marker-space": false,
145
+ "ul-indent": false,
146
+ "no-multiple-space-atx": false,
147
+ "line-length": false,
148
+ "first-line-heading": false,
149
+ "single-title": false,
150
+ "no-duplicate-heading": false,
151
+ "no-inline-html": false
152
+ };
153
+ /* the message prefix of the markdownlint diagnostics */
154
+ static markdownPrefix = "markdownlint: ";
155
+ /* lint the specification Markdown files with markdownlint, reporting
156
+ its findings as warnings, as they never break the SpecBook parsing
157
+ (markdownlint itself is loaded on first use only) */
158
+ static async markdownlint(log) {
159
+ const { lint } = await import("markdownlint/promise");
160
+ const files = Artifact.list(log, ["spec"]).flatMap((entry) => entry.files)
161
+ .filter((file) => /\.md$/i.test(file))
162
+ .map((file) => path.resolve(Task.projectRoot(), file));
163
+ const results = await lint({ files, config: Spec.markdownConfig });
164
+ return Object.entries(results).flatMap(([file, errors]) => errors.map((e) => ({
165
+ file: Spec.relativize(file),
166
+ line: e.lineNumber,
167
+ column: e.errorRange?.[0] ?? 1,
168
+ severity: "warning",
169
+ message: `${Spec.markdownPrefix}${e.ruleNames.join("/")}: ${e.ruleDescription}` +
170
+ (e.errorDetail !== null ? ` [${e.errorDetail}]` : "") +
171
+ (e.errorContext !== null ? ` [context: "${e.errorContext}"]` : "")
172
+ })));
111
173
  }
112
174
  /* render a diagnostic as a multi-line message with the affected
113
175
  source snippet (like the verbose SpecBook CLI), falling back to
@@ -182,22 +244,39 @@ export default class SpecCommand {
182
244
  /* register CLI top-level command "ase spec" */
183
245
  const spec = program
184
246
  .command("spec")
185
- .description("Lint, export, and preview the SpecBook-based project specification")
247
+ .description("Initialize, lint, export, and preview the SpecBook-based project specification")
186
248
  .action(() => {
187
249
  spec.outputHelp();
188
250
  process.exit(1);
189
251
  });
252
+ /* register CLI sub-command "ase spec init" */
253
+ spec
254
+ .command("init")
255
+ .description("Initialize the absent specification Markdown files of the SpecBook schema configuration")
256
+ .action(async () => {
257
+ const files = await Spec.init(this.log);
258
+ await writeStdout(files.length > 0 ?
259
+ `initialized artifact file(s): ${files.join(", ")}\n` :
260
+ "no artifact files were created\n");
261
+ });
190
262
  /* register CLI sub-command "ase spec lint" */
191
263
  spec
192
264
  .command("lint")
193
265
  .description("Lint the specification Markdown files against the SpecBook schema configuration")
266
+ .option("-m, --markdown", "additionally lint the generic Markdown formatting via markdownlint")
194
267
  .option("-v, --verbose", "print each diagnostic with its affected source snippet")
195
268
  .action(async (opts) => {
196
- const diagnostics = await Spec.lint(this.log);
197
- for (const diagnostic of diagnostics)
198
- await writeStdout(opts.verbose === true ?
199
- Spec.render(diagnostic, process.stdout.isTTY === true) :
200
- `${renderDiagnostic(diagnostic)}\n`);
269
+ const diagnostics = await Spec.lint(this.log, opts.markdown === true);
270
+ for (const diagnostic of diagnostics) {
271
+ /* report a diagnostic as a log message of its severity, tagged with its linter */
272
+ const { file, line, column, severity, message } = diagnostic;
273
+ const markdown = message.startsWith(Spec.markdownPrefix);
274
+ if (opts.verbose === true)
275
+ await writeStdout(Spec.render(diagnostic, process.stdout.isTTY === true));
276
+ else
277
+ this.log.write(severity, `lint: ${markdown ? "markdownlint" : "specbook"}: ` +
278
+ `${file}:${line}:${column}: ${markdown ? message.slice(Spec.markdownPrefix.length) : message}`);
279
+ }
201
280
  if (diagnostics.some((diagnostic) => diagnostic.severity === "error"))
202
281
  process.exitCode = 1;
203
282
  });
@@ -272,8 +351,10 @@ export class SpecMCP {
272
351
  description: "Lint the SpecBook specification Markdown files of the project (located via the " +
273
352
  "`project.artifact.spec.basedir` configuration) against the SpecBook YAML schema " +
274
353
  "configuration (`project.artifact.spec.schema`, a whitespace-separated list of schema files " +
275
- "merged in order, with `std` naming the bundled standard `ase-format-specbook.yaml`, " +
276
- "which is also the default). " +
354
+ "merged in order, with `std` naming the standard schema bundled with SpecBook, " +
355
+ "which is also the default). With `markdown`, the files are additionally linted " +
356
+ "for their generic Markdown formatting via markdownlint, whose findings are reported " +
357
+ "as `warning` diagnostics with a message prefixed by `markdownlint: `. " +
277
358
  "Returns a `diagnostics` array of `{ file, line, column, severity, message }` objects (with " +
278
359
  "project-relative `file` and a `severity` of `error` or `warning`), rendered as bullet " +
279
360
  "points in `text`. With `verbose`, " +
@@ -282,6 +363,8 @@ export class SpecMCP {
282
363
  "(`text` of `specification valid`) means the specification is valid; only " +
283
364
  "diagnostics of `error` severity make an export fail.",
284
365
  inputSchema: {
366
+ markdown: z.boolean().optional()
367
+ .describe("if true, additionally lint the generic Markdown formatting via markdownlint (default: false)"),
285
368
  verbose: z.boolean().optional()
286
369
  .describe("if true, render each diagnostic with its affected source snippet (default: false)")
287
370
  },
@@ -299,7 +382,7 @@ export class SpecMCP {
299
382
  }, async (args) => {
300
383
  try {
301
384
  const verbose = args.verbose ?? false;
302
- const diagnostics = (await Spec.lint(this.log))
385
+ const diagnostics = (await Spec.lint(this.log, args.markdown ?? false))
303
386
  .map((d) => verbose ? { ...d, snippet: Spec.render(d, false) } : d);
304
387
  const text = diagnostics.length === 0 ? "specification valid" :
305
388
  verbose ?
@@ -5,10 +5,11 @@
5
5
  */
6
6
  import { effortSizes } from "./ase-task-format.js";
7
7
  /* shortest keyword still eligible for fuzzy matching: below it the
8
- edit distance would equate it with almost any short word */
9
- const fuzzyMin = 4;
8
+ edit distance would equate it with almost any short word
9
+ (shared with the search of the manual page browser) */
10
+ export const fuzzyMin = 4;
10
11
  /* the Levenshtein edit distance of two words */
11
- const levenshtein = (a, b) => {
12
+ export const levenshtein = (a, b) => {
12
13
  let prev = [];
13
14
  let curr = [];
14
15
  for (let j = 0; j <= b.length; j++)
@@ -24,7 +25,7 @@ const levenshtein = (a, b) => {
24
25
  return prev[b.length];
25
26
  };
26
27
  /* the Sorensen-Dice bigram similarity coefficient of two words */
27
- const diceCoefficient = (a, b) => {
28
+ export const diceCoefficient = (a, b) => {
28
29
  if (a === b)
29
30
  return 1;
30
31
  if (a.length < 2 || b.length < 2)