@rse/ase 0.9.64 → 0.9.66

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 (79) hide show
  1. package/dst/ase-hook.js +1 -1
  2. package/dst/ase-metric.js +103 -0
  3. package/dst/ase-mint.js +106 -0
  4. package/dst/ase-service.js +4 -0
  5. package/dst/ase-setup.js +1 -1
  6. package/dst/ase-spec.js +104 -40
  7. package/dst/ase-statusline.js +11 -2
  8. package/dst/ase.js +4 -0
  9. package/package.json +13 -12
  10. package/plugin/.claude-plugin/plugin.json +1 -1
  11. package/plugin/.codex-plugin/plugin.json +1 -1
  12. package/plugin/.github/plugin/plugin.json +1 -1
  13. package/plugin/agents/ase-docs-proofread.md +13 -7
  14. package/plugin/agents/ase-docs-refine.md +165 -0
  15. package/plugin/agents/ase-docs-shorten.md +182 -0
  16. package/plugin/meta/ase-constitution.md +2 -0
  17. package/plugin/meta/ase-tenets.md +12 -0
  18. package/plugin/package.json +4 -4
  19. package/plugin/skills/ase-arch-analyze/help.md +1 -1
  20. package/plugin/skills/ase-arch-discover/help.md +4 -4
  21. package/plugin/skills/ase-code-analyze/help.md +1 -1
  22. package/plugin/skills/ase-code-craft/help.md +7 -7
  23. package/plugin/skills/ase-code-dissect/help.md +5 -5
  24. package/plugin/skills/ase-code-edit/help.md +7 -7
  25. package/plugin/skills/ase-code-explain/help.md +1 -1
  26. package/plugin/skills/ase-code-insight/help.md +1 -1
  27. package/plugin/skills/ase-code-lint/help.md +5 -5
  28. package/plugin/skills/ase-code-refactor/help.md +6 -6
  29. package/plugin/skills/ase-code-resolve/help.md +6 -6
  30. package/plugin/skills/ase-docs-distill/help.md +2 -2
  31. package/plugin/skills/ase-docs-proofread/SKILL.md +16 -12
  32. package/plugin/skills/ase-docs-proofread/help.md +18 -8
  33. package/plugin/skills/ase-docs-refine/SKILL.md +396 -0
  34. package/plugin/skills/ase-docs-refine/help.md +104 -0
  35. package/plugin/skills/ase-docs-shorten/SKILL.md +527 -0
  36. package/plugin/skills/ase-docs-shorten/help.md +120 -0
  37. package/plugin/skills/ase-help-intent/help.md +1 -1
  38. package/plugin/skills/ase-help-skill/catalog.md +4 -1
  39. package/plugin/skills/ase-help-skill/help.md +1 -1
  40. package/plugin/skills/ase-meta-brainstorm/help.md +5 -5
  41. package/plugin/skills/ase-meta-chat/help.md +2 -2
  42. package/plugin/skills/ase-meta-config/help.md +3 -3
  43. package/plugin/skills/ase-meta-diaboli/help.md +2 -2
  44. package/plugin/skills/ase-meta-diff/help.md +3 -3
  45. package/plugin/skills/ase-meta-eli5/help.md +2 -2
  46. package/plugin/skills/ase-meta-evaluate/help.md +1 -1
  47. package/plugin/skills/ase-meta-mint/SKILL.md +157 -0
  48. package/plugin/skills/ase-meta-mint/help.md +114 -0
  49. package/plugin/skills/ase-meta-proximity/help.md +3 -3
  50. package/plugin/skills/ase-meta-quorum/help.md +2 -2
  51. package/plugin/skills/ase-meta-quotes/help.md +4 -4
  52. package/plugin/skills/ase-meta-review/help.md +1 -1
  53. package/plugin/skills/ase-meta-search/help.md +2 -2
  54. package/plugin/skills/ase-meta-steelman/help.md +3 -3
  55. package/plugin/skills/ase-meta-why/help.md +3 -3
  56. package/plugin/skills/ase-meta-workflow/SKILL.md +3 -2
  57. package/plugin/skills/ase-meta-workflow/help.md +4 -4
  58. package/plugin/skills/ase-meta-workflow/workflow.txt +8 -2
  59. package/plugin/skills/ase-spec-edit/SKILL.md +1 -1
  60. package/plugin/skills/ase-spec-edit/help.md +6 -6
  61. package/plugin/skills/ase-sync-export/SKILL.md +3 -1
  62. package/plugin/skills/ase-sync-export/help.md +1 -1
  63. package/plugin/skills/ase-sync-import/SKILL.md +1 -1
  64. package/plugin/skills/ase-sync-import/help.md +2 -2
  65. package/plugin/skills/ase-sync-reconcile/SKILL.md +1 -1
  66. package/plugin/skills/ase-sync-reconcile/help.md +4 -4
  67. package/plugin/skills/ase-task-condense/SKILL.md +7 -4
  68. package/plugin/skills/ase-task-condense/help.md +2 -2
  69. package/plugin/skills/ase-task-delete/help.md +1 -1
  70. package/plugin/skills/ase-task-dissect/help.md +5 -5
  71. package/plugin/skills/ase-task-edit/help.md +4 -4
  72. package/plugin/skills/ase-task-grill/help.md +3 -3
  73. package/plugin/skills/ase-task-id/help.md +1 -1
  74. package/plugin/skills/ase-task-implement/help.md +3 -3
  75. package/plugin/skills/ase-task-list/help.md +3 -3
  76. package/plugin/skills/ase-task-preflight/help.md +2 -2
  77. package/plugin/skills/ase-task-reboot/help.md +2 -2
  78. package/plugin/skills/ase-task-rename/help.md +1 -1
  79. package/plugin/skills/ase-task-view/help.md +2 -2
package/dst/ase-hook.js CHANGED
@@ -473,7 +473,7 @@ export default class HookCommand {
473
473
  }
474
474
  /* the edit-capable skills whose active state lets the pre-tool-use
475
475
  hook auto-approve subsequent "Edit" invocations */
476
- editCapableSkills = ["ase-code-lint", "ase-docs-proofread"];
476
+ editCapableSkills = ["ase-code-lint", "ase-docs-shorten", "ase-docs-refine", "ase-docs-proofread"];
477
477
  /* determine whether an ASE tool invocation described by the parsed
478
478
  hook input should be auto-approved, and (if so) the human-readable
479
479
  reason. The input field names and value shapes differ between
@@ -0,0 +1,103 @@
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 fs from "node:fs";
7
+ import { z } from "zod";
8
+ import { writeStdout } from "./ase-stdio.js";
9
+ /* measure the metrics of the text "content", where "words" are the
10
+ whitespace-separated tokens, "lines" count a trailing line without a
11
+ final newline as a full line, "chars" are Unicode characters (code
12
+ points), and "bytes" are the UTF-8 octets of the content itself,
13
+ overridable with the raw octet size of an underlying file */
14
+ export const measure = (content, bytes = Buffer.byteLength(content, "utf8")) => {
15
+ const words = content.split(/\s+/).filter((word) => word !== "").length;
16
+ const lines = content === "" ? 0 : content.replace(/\n$/, "").split("\n").length;
17
+ const chars = Array.from(content).length;
18
+ return { words, lines, chars, bytes };
19
+ };
20
+ /* measure the metrics of the content of a file, counting its raw
21
+ octets as "bytes", as the file encoding is not necessarily UTF-8 */
22
+ export const measureFile = (file) => {
23
+ let buffer;
24
+ try {
25
+ buffer = fs.readFileSync(file);
26
+ }
27
+ catch (err) {
28
+ const message = err instanceof Error ? err.message : String(err);
29
+ throw new Error(`metric: cannot read file "${file}": ${message}`, { cause: err });
30
+ }
31
+ return measure(buffer.toString("utf8"), buffer.length);
32
+ };
33
+ /* measure the metrics of the mutually exclusive inputs "file" and
34
+ "text", of which exactly one has to be given */
35
+ export const measureInput = (file, text) => {
36
+ if (file !== undefined && text !== undefined)
37
+ throw new Error("metric: \"file\" and \"text\" are mutually exclusive");
38
+ else if (file !== undefined)
39
+ return measureFile(file);
40
+ else if (text !== undefined)
41
+ return measure(text);
42
+ else
43
+ throw new Error("metric: either \"file\" or \"text\" has to be given");
44
+ };
45
+ /* CLI command "ase metric" */
46
+ export default class MetricCommand {
47
+ /* register commands */
48
+ register(program) {
49
+ program
50
+ .command("metric")
51
+ .description("Measure the words, lines, characters, and bytes of a file or a text")
52
+ .option("-f, --file <file>", "measure the content of <file> instead of the arguments")
53
+ .argument("[text...]", "text to measure (mutually exclusive with --file)")
54
+ .action(async (text, opts) => {
55
+ const result = measureInput(opts.file, text.length > 0 ? text.join(" ") : undefined);
56
+ await writeStdout(`${JSON.stringify(result)}\n`);
57
+ });
58
+ }
59
+ }
60
+ /* render a caught error as an MCP tool error result */
61
+ const mcpToolError = (err) => ({
62
+ isError: true,
63
+ content: [{ type: "text", text: `ERROR: ${err instanceof Error ? err.message : String(err)}` }]
64
+ });
65
+ /* MCP registration entry point for metric tool */
66
+ export class MetricMCP {
67
+ register(mcp) {
68
+ mcp.registerTool("ase_text_metric", {
69
+ title: "ASE text metric",
70
+ description: "Measure the length metrics of a text, given either as the path `file` or as the " +
71
+ "literal `text`, which are mutually exclusive and of which exactly one has to be given. " +
72
+ "Returns `words` (the whitespace-separated tokens), `lines` (the newline-separated " +
73
+ "lines, where a trailing line without a final newline still counts as one line), " +
74
+ "`chars` (the Unicode characters, i.e. code points), and `bytes` (the octets: the raw " +
75
+ "file size for `file`, the UTF-8 length for `text`). " +
76
+ "You MUST call this tool whenever a text length is needed, instead of counting or " +
77
+ "estimating the length yourself.",
78
+ inputSchema: {
79
+ file: z.string().optional()
80
+ .describe("path of the file whose content is measured (mutually exclusive with `text`)"),
81
+ text: z.string().optional()
82
+ .describe("literal text which is measured (mutually exclusive with `file`)")
83
+ },
84
+ outputSchema: {
85
+ words: z.number().describe("number of whitespace-separated tokens"),
86
+ lines: z.number().describe("number of lines"),
87
+ chars: z.number().describe("number of Unicode characters (code points)"),
88
+ bytes: z.number().describe("number of octets")
89
+ }
90
+ }, async (args) => {
91
+ try {
92
+ const result = measureInput(args.file, args.text);
93
+ return {
94
+ structuredContent: result,
95
+ content: [{ type: "text", text: JSON.stringify(result) }]
96
+ };
97
+ }
98
+ catch (err) {
99
+ return mcpToolError(err);
100
+ }
101
+ });
102
+ }
103
+ }
@@ -0,0 +1,106 @@
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 { createHash } from "node:crypto";
7
+ import { Option } from "commander";
8
+ import { z } from "zod";
9
+ import UUID from "pure-uuid";
10
+ import { writeStdout } from "./ase-stdio.js";
11
+ /* the fixed ASE namespace UUID, itself a UUID V5 over the ASE home URL
12
+ within the standard URL namespace, so that all hint-derived UUIDs
13
+ stay stable over time and disjoint from those of other applications */
14
+ const NAMESPACE = new UUID(5, "ns:URL", "https://ase.tools").format("std");
15
+ /* mint "count" identifiers of "type" out of "hint": an empty hint
16
+ yields "count" random UUIDs (V4), while a non-empty hint yields
17
+ exactly one deterministic hash (UUID V5, SHA-1), as hashing is a
18
+ pure function of the hint and hence cannot produce distinct results
19
+ for it -- a larger count is clamped and reported back as a warning */
20
+ export const mint = (type, hint, count) => {
21
+ if (!Number.isInteger(count) || count < 1)
22
+ throw new Error("mint: count has to be a positive integer");
23
+ if (type === "sha1" && hint === "")
24
+ throw new Error("mint: type \"sha1\" requires a non-empty hint");
25
+ let warning = "";
26
+ if (hint !== "" && count > 1) {
27
+ warning = `WARNING: type "${type}" is deterministic for a non-empty hint, ` +
28
+ `so the requested count ${count} was reduced to 1`;
29
+ count = 1;
30
+ }
31
+ const ids = [];
32
+ for (let i = 0; i < count; i++) {
33
+ if (type === "uuid")
34
+ ids.push(hint === "" ?
35
+ new UUID(4).format("std") :
36
+ new UUID(5, NAMESPACE, hint).format("std"));
37
+ else
38
+ ids.push(createHash("sha1").update(hint).digest("hex"));
39
+ }
40
+ return { ids, warning };
41
+ };
42
+ /* CLI command "ase mint" */
43
+ export default class MintCommand {
44
+ /* register commands */
45
+ register(program) {
46
+ program
47
+ .command("mint")
48
+ .description("Mint hash-derived identifiers (UUID, SHA-1) out of a hint")
49
+ .addOption(new Option("-t, --type <type>", "identifier type")
50
+ .choices(["uuid", "sha1"]).default("uuid"))
51
+ .option("-c, --count <count>", "number of identifiers to mint (empty hint only)", "1")
52
+ .argument("[hint...]", "hint the identifiers are derived from")
53
+ .action(async (hint, opts) => {
54
+ const count = Number(opts.count);
55
+ const result = mint(opts.type, hint.join(" "), count);
56
+ if (result.warning !== "")
57
+ process.stderr.write(`ase mint: ${result.warning}\n`);
58
+ await writeStdout(`${result.ids.join("\n")}\n`);
59
+ });
60
+ }
61
+ }
62
+ /* MCP registration entry point for mint tool */
63
+ export class MintMCP {
64
+ register(mcp) {
65
+ mcp.registerTool("ase_mint", {
66
+ title: "ASE mint",
67
+ description: "Mint one or more hash-derived identifiers out of a free-text `hint`. " +
68
+ "For `type` `uuid`, a deterministic UUID V5 within the fixed ASE namespace " +
69
+ "is derived from the hint, or a random UUID V4 if the hint is empty. " +
70
+ "For `type` `sha1`, the 40-character hexadecimal SHA-1 digest of the hint " +
71
+ "is derived, which requires a non-empty hint. " +
72
+ "Pass `count` to mint more than one random UUID V4 at once, which is " +
73
+ "possible for an empty hint only, as hashing a non-empty hint is " +
74
+ "deterministic and hence always yields exactly one identifier. " +
75
+ "Returns the minted identifiers as `text`, one per line, optionally " +
76
+ "preceded by a line starting with `WARNING:`.",
77
+ inputSchema: {
78
+ type: z.enum(["uuid", "sha1"]).default("uuid")
79
+ .describe("identifier type: `uuid` (UUID V5, or V4 for an empty hint) or " +
80
+ "`sha1` (SHA-1 hexadecimal digest), default `uuid`"),
81
+ hint: z.string().default("")
82
+ .describe("free-text hint the identifiers are derived from " +
83
+ "(an empty hint is allowed for `uuid` only)"),
84
+ count: z.number().int().positive().max(100).default(1)
85
+ .describe("number of identifiers to mint (at most 100, default 1); " +
86
+ "honored for an empty hint only, else clamped to 1")
87
+ }
88
+ }, async (args) => {
89
+ try {
90
+ const result = mint(args.type, args.hint, args.count);
91
+ const text = (result.warning !== "" ? [result.warning] : [])
92
+ .concat(result.ids).join("\n");
93
+ return {
94
+ content: [{ type: "text", text }]
95
+ };
96
+ }
97
+ catch (err) {
98
+ const message = err instanceof Error ? err.message : String(err);
99
+ return {
100
+ isError: true,
101
+ content: [{ type: "text", text: `ERROR: ${message}` }]
102
+ };
103
+ }
104
+ });
105
+ }
106
+ }
@@ -29,6 +29,8 @@ import { SleepMCP } from "./ase-sleep.js";
29
29
  import { GetoptMCP } from "./ase-getopt.js";
30
30
  import { SkillsMCP } from "./ase-skills.js";
31
31
  import { WorktreeMCP } from "./ase-worktree.js";
32
+ import { MintMCP } from "./ase-mint.js";
33
+ import { MetricMCP } from "./ase-metric.js";
32
34
  import pkg from "../package.json" with { type: "json" };
33
35
  /* shared service host */
34
36
  export const SERVICE_HOST = "127.0.0.1";
@@ -276,6 +278,8 @@ export default class ServiceCommand {
276
278
  new GetoptMCP().register(mcp);
277
279
  new SkillsMCP().register(mcp);
278
280
  new WorktreeMCP().register(mcp);
281
+ new MintMCP().register(mcp);
282
+ new MetricMCP().register(mcp);
279
283
  new ConfigMCP(this.log).register(mcp);
280
284
  return mcp;
281
285
  };
package/dst/ase-setup.js CHANGED
@@ -575,7 +575,7 @@ export default class SetupCommand {
575
575
  statuslineFormatDflt = [
576
576
  "<blue>%u</blue> <red>%p</red> <black>%T</black> %s",
577
577
  "%m %e %t",
578
- "%P %h %c"
578
+ "%P %h %B %c"
579
579
  ];
580
580
  /* resolve the tool settings file for a given installation scope */
581
581
  statuslineSettingsFile(tool, scope) {
package/dst/ase-spec.js CHANGED
@@ -8,43 +8,53 @@ import fs from "node:fs";
8
8
  import { isScalar } from "yaml";
9
9
  import { z } from "zod";
10
10
  import sourceCodeError from "source-code-error";
11
- import { SpecBook, renderDiagnostic, renderVerbose, formats, parseOutputSpec } from "@rse/specbook";
11
+ import { SpecBook, renderDiagnostic, renderVerbose, formats, parseOutputSpec, previewAddr, previewPort } from "@rse/specbook";
12
12
  import { Config, configSchema } from "./ase-config.js";
13
13
  import { Task } from "./ase-task.js";
14
14
  import { Artifact } from "./ase-artifact.js";
15
15
  import { Meta } from "./ase-meta.js";
16
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 */
17
+ /* reusable functionality: lint, export, and preview the SpecBook-based
18
+ project specification, located via the "project.artifact.spec.basedir"
19
+ and "project.artifact.spec.schema" configuration */
20
20
  export class Spec {
21
- /* resolve the YAML schema configuration file: the configured
21
+ /* resolve the YAML schema configuration files: the configured
22
22
  "project.artifact.spec.schema" relative to the project root, or
23
23
  the bundled standard "ase-format-specbook.yaml" plugin meta file
24
24
  if unset or empty */
25
- static configFile(log) {
25
+ static configFiles(log) {
26
26
  const cfg = new Config("config", configSchema, log);
27
27
  cfg.read();
28
28
  const val = cfg.get("project.artifact.spec.schema");
29
29
  const file = val === undefined ? "" : String(isScalar(val) ? val.value : val);
30
30
  if (file === "")
31
- return Meta.resolve("ase-format-specbook.yaml");
32
- return path.resolve(Task.projectRoot(), file);
31
+ return [Meta.resolve("ase-format-specbook.yaml")];
32
+ return [path.resolve(Task.projectRoot(), file)];
33
33
  }
34
+ /* the ASE log level each SpecBook verbosity level maps onto: the
35
+ "none" level carries the messages a bare SpecBook CLI run always
36
+ prints, which are mere progress chatter under ASE and hence are
37
+ not logged at all, while the regular and detailed processing
38
+ messages reach the info log and the tracing ones the debug log */
39
+ static logLevel = {
40
+ none: null,
41
+ notice: "info",
42
+ detail: "info",
43
+ trace: "debug"
44
+ };
34
45
  /* 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) {
46
+ messages into the log according to their verbosity level, while
47
+ its "none" level messages, for consumers which never see the log,
48
+ additionally reach the given collector */
49
+ static api(log, notices) {
39
50
  return new SpecBook({
40
51
  verbose: (cmd, msg, level) => {
41
52
  const text = renderVerbose(msg);
42
- if (level === "notice") {
43
- log.write("warning", `specbook: ${cmd}: ${text}`);
53
+ if (level === "none")
44
54
  notices?.push(text);
45
- }
46
- else
47
- log.write(verbose ? "info" : "debug", `specbook: ${cmd}: ${text}`);
55
+ const logLevel = Spec.logLevel[level];
56
+ if (logLevel !== null)
57
+ log.write(logLevel, `specbook: ${cmd}: ${text}`);
48
58
  }
49
59
  });
50
60
  }
@@ -72,9 +82,9 @@ export class Spec {
72
82
  }
73
83
  /* lint the specification Markdown files below the "spec" artifact
74
84
  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),
85
+ static async lint(log) {
86
+ const result = await Spec.unmarked(Spec.api(log).lint({
87
+ config: Spec.configFiles(log),
78
88
  basedir: Artifact.basedir(log, "spec")
79
89
  }));
80
90
  return result.diagnostics.map((d) => ({ ...d, file: Spec.relativize(d.file) }));
@@ -94,6 +104,7 @@ export class Spec {
94
104
  if (code === "")
95
105
  return `${renderDiagnostic(diagnostic)}\n`;
96
106
  return sourceCodeError({
107
+ type: diagnostic.severity === "warning" ? "WARNING" : "ERROR",
97
108
  message: diagnostic.message,
98
109
  filename: diagnostic.file,
99
110
  code,
@@ -105,13 +116,37 @@ export class Spec {
105
116
  /* export the specification Markdown files below the "spec" artifact
106
117
  base directory into the requested formats, one buffer per format,
107
118
  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),
119
+ static export(log, formats, notices) {
120
+ return Spec.unmarked(Spec.api(log, notices).export({
121
+ config: Spec.configFiles(log),
111
122
  basedir: Artifact.basedir(log, "spec"),
112
123
  formats
113
124
  }));
114
125
  }
126
+ /* export the specification like "export" and then keep the export
127
+ in sync with its sources, handing every fresh set of buffers to
128
+ "onExport", where "outputs" names the files "onExport" writes,
129
+ so an output which is itself an observed source can be refused */
130
+ static watch(log, formats, outputs, onExport) {
131
+ return Spec.unmarked(Spec.api(log).watch({
132
+ config: Spec.configFiles(log),
133
+ basedir: Artifact.basedir(log, "spec"),
134
+ formats,
135
+ outputs,
136
+ onExport
137
+ }));
138
+ }
139
+ /* serve the HTML export of the specification as a live preview,
140
+ kept in sync with its sources and pushed into the connected
141
+ browsers as an in-place document update */
142
+ static preview(log, addr, port) {
143
+ return Spec.unmarked(Spec.api(log).preview({
144
+ config: Spec.configFiles(log),
145
+ basedir: Artifact.basedir(log, "spec"),
146
+ addr,
147
+ port
148
+ }));
149
+ }
115
150
  }
116
151
  /* CLI command "ase spec" */
117
152
  export default class SpecCommand {
@@ -124,7 +159,7 @@ export default class SpecCommand {
124
159
  /* register CLI top-level command "ase spec" */
125
160
  const spec = program
126
161
  .command("spec")
127
- .description("Lint and export the SpecBook-based project specification")
162
+ .description("Lint, export, and preview the SpecBook-based project specification")
128
163
  .action(() => {
129
164
  spec.outputHelp();
130
165
  process.exit(1);
@@ -133,14 +168,14 @@ export default class SpecCommand {
133
168
  spec
134
169
  .command("lint")
135
170
  .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")
171
+ .option("-v, --verbose", "print each diagnostic with its affected source snippet")
137
172
  .action(async (opts) => {
138
- const diagnostics = await Spec.lint(this.log, opts.verbose === true);
173
+ const diagnostics = await Spec.lint(this.log);
139
174
  for (const diagnostic of diagnostics)
140
175
  await writeStdout(opts.verbose === true ?
141
176
  Spec.render(diagnostic, process.stdout.isTTY === true) :
142
177
  `${renderDiagnostic(diagnostic)}\n`);
143
- if (diagnostics.length > 0)
178
+ if (diagnostics.some((diagnostic) => diagnostic.severity === "error"))
144
179
  process.exitCode = 1;
145
180
  });
146
181
  /* register CLI sub-command "ase spec export" */
@@ -150,21 +185,47 @@ export default class SpecCommand {
150
185
  .option("-o, --output <[format:]file>", "output file (\"-\" for stdout, repeatable), with the format inferred " +
151
186
  "from the filename extension unless explicitly prefixed " +
152
187
  "(default: \"index.html\" inside the specification base directory)", (value, previous) => previous.concat(value), new Array())
153
- .option("-v, --verbose", "print verbose processing information")
188
+ .option("-w, --watch", "keep the outputs in sync by re-exporting on every source change")
154
189
  .action(async (opts) => {
155
190
  const outputs = (opts.output.length > 0 ? opts.output :
156
191
  [path.join(Artifact.basedir(this.log, "spec"), "index.html")]).map(parseOutputSpec);
192
+ /* a re-export has to land somewhere it can be picked up again,
193
+ which a one-shot stdout stream cannot provide */
194
+ if (opts.watch === true && outputs.some(({ output }) => output === "-"))
195
+ throw new Error("the watch mode requires regular output files " +
196
+ "(\"-\" for stdout is not supported)");
197
+ /* parse the input once and export each distinct format once */
157
198
  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)`);
199
+ const write = async (buffers) => {
200
+ for (const { format, output } of outputs) {
201
+ const data = buffers[distinct.indexOf(format)];
202
+ if (output === "-")
203
+ await writeStdout(data);
204
+ else {
205
+ await fs.promises.writeFile(output, data);
206
+ this.log.write("info", `spec: exported specification into "${output}" (${data.length} bytes)`);
207
+ }
166
208
  }
209
+ };
210
+ if (opts.watch === true) {
211
+ await Spec.watch(this.log, distinct, outputs.map(({ output }) => output), write);
212
+ await new Promise(() => { });
167
213
  }
214
+ else
215
+ await write(await Spec.export(this.log, distinct));
216
+ });
217
+ /* register CLI sub-command "ase spec preview" */
218
+ spec
219
+ .command("preview")
220
+ .description("Serve the HTML export of the specification Markdown files as a live preview")
221
+ .option("-a, --addr <ip-addr>", "IP address to listen on", previewAddr)
222
+ .option("-p, --port <tcp-port>", "TCP port to listen on", String(previewPort))
223
+ .action(async (opts) => {
224
+ const port = Number(opts.port);
225
+ if (!Number.isInteger(port) || port < 1 || port > 65535)
226
+ throw new Error(`invalid TCP port "${opts.port}"`);
227
+ await Spec.preview(this.log, opts.addr, port);
228
+ await new Promise(() => { });
168
229
  });
169
230
  }
170
231
  }
@@ -186,11 +247,13 @@ export class SpecMCP {
186
247
  description: "Lint the SpecBook specification Markdown files of the project (located via the " +
187
248
  "`project.artifact.spec.basedir` configuration) against the SpecBook YAML schema " +
188
249
  "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`, " +
250
+ "Returns a `diagnostics` array of `{ file, line, column, severity, message }` objects (with " +
251
+ "project-relative `file` and a `severity` of `error` or `warning`), rendered as bullet " +
252
+ "points in `text`. With `verbose`, " +
191
253
  "each diagnostic additionally carries a multi-line `snippet` rendering with the " +
192
254
  "affected source lines, which is also used for `text`. An empty array " +
193
- "(`text` of `specification valid`) means the specification is valid.",
255
+ "(`text` of `specification valid`) means the specification is valid; only " +
256
+ "diagnostics of `error` severity make an export fail.",
194
257
  inputSchema: {
195
258
  verbose: z.boolean().optional()
196
259
  .describe("if true, render each diagnostic with its affected source snippet (default: false)")
@@ -200,6 +263,7 @@ export class SpecMCP {
200
263
  file: z.string().describe("project-relative file path"),
201
264
  line: z.number().describe("line number (1-based)"),
202
265
  column: z.number().describe("column number (1-based)"),
266
+ severity: z.enum(["error", "warning"]).describe("diagnostic severity"),
203
267
  message: z.string().describe("diagnostic message"),
204
268
  snippet: z.string().optional()
205
269
  .describe("multi-line rendering with the affected source snippet (with `verbose` only)")
@@ -251,7 +315,7 @@ export class SpecMCP {
251
315
  const spec = args.format !== undefined || args.output === undefined ?
252
316
  { format: args.format ?? "json", output: args.output } :
253
317
  parseOutputSpec(args.output);
254
- const [data] = await Spec.export(this.log, [spec.format], false, notices);
318
+ const [data] = await Spec.export(this.log, [spec.format], notices);
255
319
  if (spec.output !== undefined && spec.output !== "-") {
256
320
  await fs.promises.writeFile(path.resolve(Task.projectRoot(), spec.output), data);
257
321
  return {
@@ -209,7 +209,7 @@ export default class StatuslineCommand {
209
209
  .option("-p, --padding <n>", "pad each rendered line with <n> spaces on each side", parseInteger("--padding"), 0)
210
210
  .option("--no-icons", "disable icons in placeholder rendering")
211
211
  .option("--no-labels", "disable labels in front of bold values")
212
- .argument("[lines...]", "one or more template lines with %u %p %T %s %m %e %t %O %P %h %c %C %a %r " +
212
+ .argument("[lines...]", "one or more template lines with %u %p %T %s %m %e %t %O %P %h %B %c %C %a %r " +
213
213
  "%S %D %W %Q %H %X %b %g %G %d %M %V placeholders and <color>...</color> markup " +
214
214
  "(color: black, red, green, yellow, blue, magenta, cyan, white, default) " +
215
215
  "(default: single line \"%m %e %t\")")
@@ -285,23 +285,27 @@ export default class StatuslineCommand {
285
285
  let taskId = process.env.ASE_TASK_ID ?? "";
286
286
  let persona = process.env.ASE_PERSONA_STYLE ?? "";
287
287
  let guidance = process.env.ASE_GUIDANCE_LEVEL ?? "";
288
+ let boxing = process.env.ASE_PROJECT_BOXING ?? "";
288
289
  try {
289
290
  const cfg = new Config("config", configSchema, this.log, parseScope(`session:${getSession()}`));
290
291
  cfg.read("lenient");
291
292
  const t = String(cfg.get("agent.task") ?? "").trim();
292
293
  const p = String(cfg.get("agent.persona") ?? "").trim();
293
294
  const g = String(cfg.get("agent.guidance") ?? "").trim();
295
+ const b = String(cfg.get("project.boxing") ?? "").trim();
294
296
  if (t !== "")
295
297
  taskId = t;
296
298
  if (p !== "")
297
299
  persona = p;
298
300
  if (g !== "")
299
301
  guidance = g;
302
+ if (b !== "")
303
+ boxing = b;
300
304
  }
301
305
  catch (_e) {
302
306
  /* cascade unavailable; keep env-var fallbacks */
303
307
  }
304
- return { taskId, persona, guidance };
308
+ return { taskId, persona, guidance, boxing };
305
309
  });
306
310
  const getModel = memoize(() => {
307
311
  const display = data.model?.display_name ?? "";
@@ -361,6 +365,11 @@ export default class StatuslineCommand {
361
365
  if (guidance !== "")
362
366
  emit(`${prefix("▶", "guidance")}${c.bold(guidance)}`);
363
367
  },
368
+ B: () => {
369
+ const { boxing } = getCfg();
370
+ if (boxing !== "")
371
+ emit(`${prefix("▢", "boxing")}${c.bold(boxing)}`);
372
+ },
364
373
  /* ==== CONTEXT ==== */
365
374
  c: () => {
366
375
  const pct = Math.min(100, Math.max(0, Math.floor(data.context_window?.used_percentage ?? 0)));
package/dst/ase.js CHANGED
@@ -19,6 +19,8 @@ import MetaCommand from "./ase-meta.js";
19
19
  import CompatCommand from "./ase-compat.js";
20
20
  import DiagramCommand from "./ase-diagram.js";
21
21
  import WorktreeCommand from "./ase-worktree.js";
22
+ import MintCommand from "./ase-mint.js";
23
+ import MetricCommand from "./ase-metric.js";
22
24
  import pkg from "../package.json" with { type: "json" };
23
25
  /* globally initialize logger */
24
26
  const log = new Log("ase", "info", "-");
@@ -60,6 +62,8 @@ const main = async () => {
60
62
  new CompatCommand().register(program);
61
63
  new DiagramCommand(log).register(program);
62
64
  new WorktreeCommand().register(program);
65
+ new MintCommand().register(program);
66
+ new MetricCommand().register(program);
63
67
  /* parse program arguments */
64
68
  await program.parseAsync(process.argv);
65
69
  /* gracefully terminate */
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.64",
9
+ "version": "0.9.66",
10
10
  "license": "Apache-2.0",
11
11
  "author": {
12
12
  "name": "Dr. Ralf S. Engelschall",
@@ -16,21 +16,21 @@
16
16
  "type": "module",
17
17
  "bin": { "ase": "bin/ase" },
18
18
  "devDependencies": {
19
- "eslint": "10.9.1",
19
+ "eslint": "10.10.0",
20
20
  "@eslint/js": "10.0.1",
21
- "@typescript-eslint/parser": "8.68.0",
22
- "@typescript-eslint/eslint-plugin": "8.68.0",
23
- "typescript-eslint": "8.68.0",
21
+ "@typescript-eslint/parser": "8.69.0",
22
+ "@typescript-eslint/eslint-plugin": "8.69.0",
23
+ "typescript-eslint": "8.69.0",
24
24
  "eslint-plugin-promise": "7.3.0",
25
25
  "neostandard": "0.14.0-next.1",
26
- "globals": "17.11.0",
26
+ "globals": "17.12.0",
27
27
  "typescript": "6.0.3",
28
28
 
29
29
  "@rse/stx": "1.1.6",
30
30
  "nodemon": "3.1.14",
31
31
  "shx": "0.4.0",
32
32
 
33
- "@types/node": "26.3.0",
33
+ "@types/node": "26.4.1",
34
34
  "@types/luxon": "3.7.5",
35
35
  "@types/which": "3.0.4",
36
36
  "@types/update-notifier": "6.0.8",
@@ -42,7 +42,7 @@
42
42
  },
43
43
  "dependencies": {
44
44
  "commander": "15.0.0",
45
- "@dotenvx/dotenvx": "2.21.0",
45
+ "@dotenvx/dotenvx": "2.23.0",
46
46
  "yaml": "2.9.0",
47
47
  "valibot": "1.4.2",
48
48
  "execa": "10.0.1",
@@ -51,11 +51,11 @@
51
51
  "beautiful-mermaid": "1.1.3",
52
52
  "cli-table3": "0.6.5",
53
53
  "chalk": "6.0.0",
54
- "pretty-ms": "9.3.0",
54
+ "pretty-ms": "9.3.1",
55
55
  "luxon": "3.7.2",
56
56
  "@modelcontextprotocol/sdk": "1.30.0",
57
57
  "json-asty": "1.3.4",
58
- "zod": "4.4.3",
58
+ "zod": "4.5.4",
59
59
  "which": "7.0.0",
60
60
  "update-notifier": "7.3.1",
61
61
  "shell-quote": "1.10.0",
@@ -66,8 +66,9 @@
66
66
  "ofetch": "1.5.1",
67
67
  "picomatch": "4.0.7",
68
68
  "lru-cache": "11.5.2",
69
- "@rse/specbook": "1.0.3",
70
- "source-code-error": "1.2.0"
69
+ "@rse/specbook": "1.2.7",
70
+ "source-code-error": "1.2.0",
71
+ "pure-uuid": "2.0.0"
71
72
  },
72
73
  "allowScripts": {
73
74
  "fsevents": true
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ase",
3
- "version": "0.9.64",
3
+ "version": "0.9.66",
4
4
  "description": "Agentic Software Engineering (ASE)",
5
5
  "keywords": [ "agentic", "software", "engineering" ],
6
6
  "homepage": "https://ase.tools",