@rse/ase 2.0.2 → 2.0.4

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 (236) hide show
  1. package/dst/ase-agent-definition-core.js +111 -87
  2. package/dst/ase-agent-fleet.js +1 -1
  3. package/dst/ase-agent-runtime-claim.js +5 -3
  4. package/dst/ase-agent-runtime-engine.js +1 -1
  5. package/dst/ase-agent-runtime-phase.js +1 -1
  6. package/dst/ase-agent-runtime-request.js +1 -1
  7. package/dst/ase-agent-runtime.js +3 -5
  8. package/dst/ase-artifact.js +3 -3
  9. package/dst/ase-config-core.js +2 -16
  10. package/dst/ase-config-schema.js +1 -3
  11. package/dst/ase-config.js +2 -0
  12. package/dst/ase-hook.js +13 -13
  13. package/dst/ase-lib-daemon-cli.js +117 -4
  14. package/dst/ase-lib-server-cli.js +4 -5
  15. package/dst/ase-lib-version.js +14 -5
  16. package/dst/ase-man-catalog.js +229 -0
  17. package/dst/ase-man-render.js +278 -0
  18. package/dst/ase-man-tui.js +636 -0
  19. package/dst/ase-man.js +33 -27
  20. package/dst/ase-mcp.js +4 -4
  21. package/dst/ase-service-getopt.js +2 -2
  22. package/dst/ase-service-otel.js +4 -4
  23. package/dst/ase-service-skills.js +5 -15
  24. package/dst/ase-service.js +87 -210
  25. package/dst/ase-setup-common.js +1 -2
  26. package/dst/ase-setup-settings.js +6 -9
  27. package/dst/ase-setup.js +4 -5
  28. package/dst/ase-spec.js +103 -20
  29. package/dst/ase-statusline.js +170 -170
  30. package/dst/ase-task-board-filter.js +5 -4
  31. package/dst/ase-task-board-flow.js +7 -6
  32. package/dst/ase-task-board-graph.js +5 -4
  33. package/dst/ase-task-board-tui-control.js +41 -35
  34. package/dst/ase-task-board-tui-model.js +185 -167
  35. package/dst/ase-task-board-tui-popup.js +2 -3
  36. package/dst/ase-task-board-tui-view.js +74 -70
  37. package/dst/ase-task-board-web-client.js +27 -27
  38. package/dst/ase-task-board-web-server.js +2 -2
  39. package/dst/ase-task-board.js +1 -1
  40. package/dst/ase-task-format.js +1 -1
  41. package/dst/ase-task-store-client.js +17 -5
  42. package/dst/ase-task-store-core.js +5 -7
  43. package/dst/ase-task-store-issues.js +16 -1
  44. package/dst/ase-task-store-plugin-ase.js +7 -3
  45. package/dst/ase-task-store-plugin-gitea.js +9 -32
  46. package/dst/ase-task-store-plugin-github.js +3 -12
  47. package/dst/ase-task-store-plugin-gitlab.js +2 -18
  48. package/dst/ase-task.js +51 -23
  49. package/dst/ase-util-compat.js +1 -1
  50. package/dst/ase-util-diagram.js +13 -37
  51. package/dst/ase.js +5 -5
  52. package/dst/man/ase-agent-adhoc.md +1165 -0
  53. package/dst/man/ase-agent-condenser.md +383 -0
  54. package/dst/man/ase-agent-editor.md +441 -0
  55. package/dst/man/ase-agent-estimator.md +477 -0
  56. package/dst/man/ase-agent-fleet.md +358 -0
  57. package/dst/man/ase-agent-implementer.md +521 -0
  58. package/dst/man/ase-agent-integrator.md +449 -0
  59. package/dst/man/ase-agent-qualifier.md +426 -0
  60. package/dst/man/ase-agent-rebooter.md +384 -0
  61. package/dst/man/ase-agent-relay.md +262 -0
  62. package/dst/man/ase-agent-researcher.md +478 -0
  63. package/dst/man/ase-agent.md +628 -0
  64. package/dst/man/ase-artifact-list.md +57 -0
  65. package/dst/man/ase-artifact-name.md +56 -0
  66. package/dst/man/ase-artifact.md +92 -0
  67. package/dst/man/ase-config-delete.md +74 -0
  68. package/dst/man/ase-config-edit.md +83 -0
  69. package/dst/man/ase-config-get.md +70 -0
  70. package/dst/man/ase-config-init.md +110 -0
  71. package/dst/man/ase-config-list.md +68 -0
  72. package/dst/man/ase-config-set.md +92 -0
  73. package/dst/man/ase-config.md +426 -0
  74. package/dst/man/ase-hook-permission-request.md +61 -0
  75. package/dst/man/ase-hook-pre-tool-use.md +96 -0
  76. package/dst/man/ase-hook-session-end.md +71 -0
  77. package/dst/man/ase-hook-session-persona.md +65 -0
  78. package/dst/man/ase-hook-session-start.md +184 -0
  79. package/dst/man/ase-hook-stop.md +52 -0
  80. package/dst/man/ase-hook-user-prompt-submit.md +54 -0
  81. package/dst/man/ase-hook.md +93 -0
  82. package/dst/man/ase-man.md +195 -0
  83. package/dst/man/ase-mcp.md +102 -0
  84. package/dst/man/ase-service-log.md +59 -0
  85. package/dst/man/ase-service-send.md +60 -0
  86. package/dst/man/ase-service-start.md +93 -0
  87. package/dst/man/ase-service-status.md +58 -0
  88. package/dst/man/ase-service-stop.md +52 -0
  89. package/dst/man/ase-service.md +124 -0
  90. package/dst/man/ase-setup-disable.md +67 -0
  91. package/dst/man/ase-setup-enable.md +66 -0
  92. package/dst/man/ase-setup-install.md +116 -0
  93. package/dst/man/ase-setup-mcp.md +213 -0
  94. package/dst/man/ase-setup-status.md +101 -0
  95. package/dst/man/ase-setup-statusline.md +162 -0
  96. package/dst/man/ase-setup-telemetry.md +166 -0
  97. package/dst/man/ase-setup-uninstall.md +85 -0
  98. package/dst/man/ase-setup-update.md +117 -0
  99. package/dst/man/ase-setup.md +156 -0
  100. package/dst/man/ase-spec-export.md +99 -0
  101. package/dst/man/ase-spec-init.md +57 -0
  102. package/dst/man/ase-spec-lint.md +74 -0
  103. package/dst/man/ase-spec-preview.md +57 -0
  104. package/dst/man/ase-spec.md +85 -0
  105. package/dst/man/ase-statusline.md +330 -0
  106. package/dst/man/ase-task-assign.md +138 -0
  107. package/dst/man/ase-task-board.md +651 -0
  108. package/dst/man/ase-task-delete.md +46 -0
  109. package/dst/man/ase-task-edit.md +73 -0
  110. package/dst/man/ase-task-idscheme.md +92 -0
  111. package/dst/man/ase-task-lifecycle.md +92 -0
  112. package/dst/man/ase-task-list.md +86 -0
  113. package/dst/man/ase-task-load.md +48 -0
  114. package/dst/man/ase-task-newid.md +100 -0
  115. package/dst/man/ase-task-purge.md +49 -0
  116. package/dst/man/ase-task-rename.md +69 -0
  117. package/dst/man/ase-task-request.md +108 -0
  118. package/dst/man/ase-task-save.md +78 -0
  119. package/dst/man/ase-task-status.md +79 -0
  120. package/dst/man/ase-task-store.md +377 -0
  121. package/dst/man/ase-task-view.md +50 -0
  122. package/dst/man/ase-task.md +335 -0
  123. package/dst/man/ase-util-compat.md +38 -0
  124. package/dst/man/ase-util-diagram.md +140 -0
  125. package/dst/man/ase-util-meta.md +68 -0
  126. package/dst/man/ase-util-metric.md +74 -0
  127. package/dst/man/ase-util-mint.md +85 -0
  128. package/dst/man/ase-util-worktree.md +109 -0
  129. package/dst/man/ase-util.md +81 -0
  130. package/dst/man/ase.md +228 -0
  131. package/package.json +12 -17
  132. package/plugin/.claude-plugin/plugin.json +1 -1
  133. package/plugin/.codex-plugin/plugin.json +1 -1
  134. package/plugin/.github/plugin/plugin.json +1 -1
  135. package/plugin/agents/ase-code-lint.md +3 -3
  136. package/plugin/agents/ase-docs-proofread.md +3 -3
  137. package/plugin/agents/ase-docs-refine.md +3 -3
  138. package/plugin/agents/ase-spec-lint.md +338 -0
  139. package/plugin/meta/ase-skill.md +7 -0
  140. package/plugin/mods/ase-mods.ts +10 -7
  141. package/plugin/mods/claude-code.d.ts +26 -3
  142. package/plugin/skills/ase-code-analyze/SKILL.md +18 -10
  143. package/plugin/skills/ase-code-analyze/help.md +11 -6
  144. package/plugin/skills/ase-code-lint/SKILL.md +37 -23
  145. package/plugin/skills/ase-code-lint/help.md +10 -5
  146. package/plugin/skills/ase-help-intent/data.md +204 -13
  147. package/plugin/skills/ase-help-skill/catalog.md +1 -0
  148. package/plugin/skills/ase-meta-workflow/workflow.txt +2 -0
  149. package/plugin/skills/ase-repo-commit/SKILL.md +1 -0
  150. package/plugin/skills/ase-repo-merge/SKILL.md +1 -0
  151. package/plugin/skills/ase-repo-review/SKILL.md +5 -3
  152. package/plugin/skills/ase-repo-review/help.md +3 -1
  153. package/plugin/skills/ase-spec-edit/SKILL.md +3 -3
  154. package/plugin/skills/ase-spec-edit/help.md +1 -1
  155. package/plugin/skills/ase-spec-lint/SKILL.md +695 -0
  156. package/plugin/skills/ase-spec-lint/help.md +171 -0
  157. package/plugin/skills/ase-task-implement/SKILL.md +24 -6
  158. package/plugin/skills/ase-task-implement/help.md +11 -1
  159. package/dst/man/ase-agent-adhoc.1 +0 -515
  160. package/dst/man/ase-agent-condenser.1 +0 -193
  161. package/dst/man/ase-agent-editor.1 +0 -212
  162. package/dst/man/ase-agent-estimator.1 +0 -214
  163. package/dst/man/ase-agent-fleet.1 +0 -185
  164. package/dst/man/ase-agent-implementer.1 +0 -233
  165. package/dst/man/ase-agent-integrator.1 +0 -213
  166. package/dst/man/ase-agent-qualifier.1 +0 -214
  167. package/dst/man/ase-agent-rebooter.1 +0 -193
  168. package/dst/man/ase-agent-relay.1 +0 -128
  169. package/dst/man/ase-agent-researcher.1 +0 -217
  170. package/dst/man/ase-agent.1 +0 -264
  171. package/dst/man/ase-artifact-list.1 +0 -38
  172. package/dst/man/ase-artifact-name.1 +0 -39
  173. package/dst/man/ase-artifact.1 +0 -56
  174. package/dst/man/ase-config-delete.1 +0 -45
  175. package/dst/man/ase-config-edit.1 +0 -48
  176. package/dst/man/ase-config-get.1 +0 -46
  177. package/dst/man/ase-config-init.1 +0 -60
  178. package/dst/man/ase-config-list.1 +0 -42
  179. package/dst/man/ase-config-set.1 +0 -52
  180. package/dst/man/ase-config.1 +0 -231
  181. package/dst/man/ase-hook-permission-request.1 +0 -36
  182. package/dst/man/ase-hook-pre-tool-use.1 +0 -59
  183. package/dst/man/ase-hook-session-end.1 +0 -47
  184. package/dst/man/ase-hook-session-persona.1 +0 -37
  185. package/dst/man/ase-hook-session-start.1 +0 -81
  186. package/dst/man/ase-hook-stop.1 +0 -34
  187. package/dst/man/ase-hook-user-prompt-submit.1 +0 -34
  188. package/dst/man/ase-hook.1 +0 -51
  189. package/dst/man/ase-man.1 +0 -67
  190. package/dst/man/ase-mcp.1 +0 -61
  191. package/dst/man/ase-service-log.1 +0 -43
  192. package/dst/man/ase-service-send.1 +0 -47
  193. package/dst/man/ase-service-start.1 +0 -54
  194. package/dst/man/ase-service-status.1 +0 -46
  195. package/dst/man/ase-service-stop.1 +0 -37
  196. package/dst/man/ase-service.1 +0 -85
  197. package/dst/man/ase-setup-disable.1 +0 -44
  198. package/dst/man/ase-setup-enable.1 +0 -45
  199. package/dst/man/ase-setup-install.1 +0 -73
  200. package/dst/man/ase-setup-mcp.1 +0 -124
  201. package/dst/man/ase-setup-status.1 +0 -60
  202. package/dst/man/ase-setup-statusline.1 +0 -92
  203. package/dst/man/ase-setup-telemetry.1 +0 -92
  204. package/dst/man/ase-setup-uninstall.1 +0 -59
  205. package/dst/man/ase-setup-update.1 +0 -64
  206. package/dst/man/ase-setup.1 +0 -93
  207. package/dst/man/ase-spec-export.1 +0 -62
  208. package/dst/man/ase-spec-lint.1 +0 -40
  209. package/dst/man/ase-spec-preview.1 +0 -40
  210. package/dst/man/ase-spec.1 +0 -46
  211. package/dst/man/ase-statusline.1 +0 -189
  212. package/dst/man/ase-task-assign.1 +0 -71
  213. package/dst/man/ase-task-board.1 +0 -384
  214. package/dst/man/ase-task-delete.1 +0 -35
  215. package/dst/man/ase-task-edit.1 +0 -46
  216. package/dst/man/ase-task-idscheme.1 +0 -65
  217. package/dst/man/ase-task-lifecycle.1 +0 -57
  218. package/dst/man/ase-task-list.1 +0 -47
  219. package/dst/man/ase-task-load.1 +0 -36
  220. package/dst/man/ase-task-newid.1 +0 -61
  221. package/dst/man/ase-task-purge.1 +0 -37
  222. package/dst/man/ase-task-rename.1 +0 -44
  223. package/dst/man/ase-task-request.1 +0 -61
  224. package/dst/man/ase-task-save.1 +0 -48
  225. package/dst/man/ase-task-status.1 +0 -55
  226. package/dst/man/ase-task-store.1 +0 -194
  227. package/dst/man/ase-task-view.1 +0 -40
  228. package/dst/man/ase-task.1 +0 -160
  229. package/dst/man/ase-util-compat.1 +0 -25
  230. package/dst/man/ase-util-diagram.1 +0 -76
  231. package/dst/man/ase-util-meta.1 +0 -45
  232. package/dst/man/ase-util-metric.1 +0 -54
  233. package/dst/man/ase-util-mint.1 +0 -59
  234. package/dst/man/ase-util-worktree.1 +0 -72
  235. package/dst/man/ase-util.1 +0 -50
  236. package/dst/man/ase.1 +0 -124
@@ -252,7 +252,6 @@ export class SetupSettings {
252
252
  /* the OpenAI Codex CLI has no scriptable statusline mechanism at all */
253
253
  if (tool === "codex")
254
254
  return [];
255
- const home = os.homedir();
256
255
  const scopes = tool === "claude" ? ["user", "project", "local"] : ["user"];
257
256
  const entries = [];
258
257
  for (const scope of scopes) {
@@ -269,7 +268,7 @@ export class SetupSettings {
269
268
  if (member === undefined)
270
269
  continue;
271
270
  entries.push({
272
- file: file.startsWith(home + path.sep) ? `~${file.slice(home.length)}` : file,
271
+ file: this.fileDisplay(file),
273
272
  scope: tool === "claude" ? scope : "(n/a)",
274
273
  status: this.statuslineIsOwned(member) ? "activated" : "foreign"
275
274
  });
@@ -336,7 +335,6 @@ export class SetupSettings {
336
335
  async outputStyleStatus(tool) {
337
336
  if (tool !== "claude")
338
337
  return [];
339
- const home = os.homedir();
340
338
  const scopes = ["user", "project", "local"];
341
339
  const entries = [];
342
340
  for (const scope of scopes) {
@@ -353,7 +351,7 @@ export class SetupSettings {
353
351
  if (member === undefined)
354
352
  continue;
355
353
  entries.push({
356
- file: file.startsWith(home + path.sep) ? `~${file.slice(home.length)}` : file,
354
+ file: this.fileDisplay(file),
357
355
  scope,
358
356
  status: this.outputStyleIsOwned(member) ? "selected" : "foreign"
359
357
  });
@@ -389,8 +387,7 @@ export class SetupSettings {
389
387
  /* a match is the presence of the "ase@ase" key under
390
388
  "enabledPlugins", regardless of its true/false value */
391
389
  const plugins = settings?.enabledPlugins;
392
- if (plugins !== undefined && plugins !== null
393
- && Object.prototype.hasOwnProperty.call(plugins, "ase@ase")) {
390
+ if (plugins !== undefined && plugins !== null && Object.hasOwn(plugins, "ase@ase")) {
394
391
  if (scope !== "user")
395
392
  this.log.write("info", `setup: detected ASE plugin installation in scope "${scope}"`);
396
393
  return scope;
@@ -488,7 +485,7 @@ export class SetupSettings {
488
485
  return files;
489
486
  }
490
487
  /* abbreviate the home directory of a file path as "~" */
491
- telemetryFileDisplay(file) {
488
+ fileDisplay(file) {
492
489
  const home = os.homedir();
493
490
  return file.startsWith(home + path.sep) ? `~${file.slice(home.length)}` : file;
494
491
  }
@@ -796,7 +793,7 @@ export class SetupSettings {
796
793
  const present = env !== undefined && env.type() === "object" ? this.telemetryClaudePresent(env) : [];
797
794
  if (env !== undefined && present.length > 0)
798
795
  entries.push({
799
- file: this.telemetryFileDisplay(file),
796
+ file: this.fileDisplay(file),
800
797
  scope: "user",
801
798
  status: this.telemetryClaudeIsOwned(env) ? "activated" : "foreign"
802
799
  });
@@ -822,7 +819,7 @@ export class SetupSettings {
822
819
  else if ([...block.head, ...block.tail].some((line) => foreign.test(line)))
823
820
  status = "foreign";
824
821
  if (status !== "")
825
- entries.push({ file: this.telemetryFileDisplay(file), scope: "(n/a)", status });
822
+ entries.push({ file: this.fileDisplay(file), scope: "(n/a)", status });
826
823
  }
827
824
  if (tool === "copilot" && this.telemetryCopilotForeignEnv() !== undefined)
828
825
  entries.push({ file: "$OTEL_EXPORTER_OTLP_ENDPOINT", scope: "(n/a)", status: "foreign" });
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;
@@ -347,10 +347,9 @@ export default class SetupCommand {
347
347
  });
348
348
  /* parser for the non-negative integer "ase setup statusline" options */
349
349
  const parseNonNeg = (name) => (value) => {
350
- const n = Number.parseInt(value, 10);
351
- if (!Number.isFinite(n) || n < 0)
350
+ if (!/^\d+$/.test(value))
352
351
  throw new Error(`invalid --${name} value: "${value}" (expected a non-negative integer)`);
353
- return n;
352
+ return Number.parseInt(value, 10);
354
353
  };
355
354
  /* register CLI sub-command "ase setup statusline" */
356
355
  const statuslineCmd = setupCmd
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 ?