@rse/ase 0.9.65 → 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.
- package/dst/ase-hook.js +1 -1
- package/dst/ase-metric.js +103 -0
- package/dst/ase-mint.js +106 -0
- package/dst/ase-service.js +4 -0
- package/dst/ase-setup.js +1 -1
- package/dst/ase-spec.js +104 -40
- package/dst/ase-statusline.js +11 -2
- package/dst/ase.js +4 -0
- package/package.json +13 -12
- 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/agents/ase-docs-proofread.md +13 -7
- package/plugin/agents/ase-docs-refine.md +165 -0
- package/plugin/agents/ase-docs-shorten.md +182 -0
- package/plugin/package.json +4 -4
- package/plugin/skills/ase-arch-analyze/help.md +1 -1
- package/plugin/skills/ase-arch-discover/help.md +4 -4
- package/plugin/skills/ase-code-analyze/help.md +1 -1
- package/plugin/skills/ase-code-craft/help.md +7 -7
- package/plugin/skills/ase-code-dissect/help.md +5 -5
- package/plugin/skills/ase-code-edit/help.md +7 -7
- package/plugin/skills/ase-code-explain/help.md +1 -1
- package/plugin/skills/ase-code-insight/help.md +1 -1
- package/plugin/skills/ase-code-lint/help.md +5 -5
- package/plugin/skills/ase-code-refactor/help.md +6 -6
- package/plugin/skills/ase-code-resolve/help.md +6 -6
- package/plugin/skills/ase-docs-distill/help.md +2 -2
- package/plugin/skills/ase-docs-proofread/SKILL.md +16 -12
- package/plugin/skills/ase-docs-proofread/help.md +18 -8
- package/plugin/skills/ase-docs-refine/SKILL.md +396 -0
- package/plugin/skills/ase-docs-refine/help.md +104 -0
- package/plugin/skills/ase-docs-shorten/SKILL.md +527 -0
- package/plugin/skills/ase-docs-shorten/help.md +120 -0
- package/plugin/skills/ase-help-intent/help.md +1 -1
- package/plugin/skills/ase-help-skill/catalog.md +4 -1
- package/plugin/skills/ase-help-skill/help.md +1 -1
- package/plugin/skills/ase-meta-brainstorm/help.md +5 -5
- package/plugin/skills/ase-meta-chat/help.md +2 -2
- package/plugin/skills/ase-meta-config/help.md +3 -3
- package/plugin/skills/ase-meta-diaboli/help.md +2 -2
- package/plugin/skills/ase-meta-diff/help.md +3 -3
- package/plugin/skills/ase-meta-eli5/help.md +2 -2
- package/plugin/skills/ase-meta-evaluate/help.md +1 -1
- package/plugin/skills/ase-meta-mint/SKILL.md +157 -0
- package/plugin/skills/ase-meta-mint/help.md +114 -0
- package/plugin/skills/ase-meta-proximity/help.md +3 -3
- package/plugin/skills/ase-meta-quorum/help.md +2 -2
- package/plugin/skills/ase-meta-quotes/help.md +4 -4
- package/plugin/skills/ase-meta-review/help.md +1 -1
- package/plugin/skills/ase-meta-search/help.md +2 -2
- package/plugin/skills/ase-meta-steelman/help.md +3 -3
- package/plugin/skills/ase-meta-why/help.md +3 -3
- package/plugin/skills/ase-meta-workflow/SKILL.md +3 -2
- package/plugin/skills/ase-meta-workflow/help.md +4 -4
- package/plugin/skills/ase-meta-workflow/workflow.txt +8 -2
- package/plugin/skills/ase-spec-edit/SKILL.md +1 -1
- package/plugin/skills/ase-spec-edit/help.md +6 -6
- package/plugin/skills/ase-sync-export/SKILL.md +3 -1
- package/plugin/skills/ase-sync-export/help.md +1 -1
- package/plugin/skills/ase-sync-import/SKILL.md +1 -1
- package/plugin/skills/ase-sync-import/help.md +2 -2
- package/plugin/skills/ase-sync-reconcile/SKILL.md +1 -1
- package/plugin/skills/ase-sync-reconcile/help.md +4 -4
- package/plugin/skills/ase-task-condense/SKILL.md +7 -4
- package/plugin/skills/ase-task-condense/help.md +2 -2
- package/plugin/skills/ase-task-delete/help.md +1 -1
- package/plugin/skills/ase-task-dissect/help.md +5 -5
- package/plugin/skills/ase-task-edit/help.md +4 -4
- package/plugin/skills/ase-task-grill/help.md +3 -3
- package/plugin/skills/ase-task-id/help.md +1 -1
- package/plugin/skills/ase-task-implement/help.md +3 -3
- package/plugin/skills/ase-task-list/help.md +3 -3
- package/plugin/skills/ase-task-preflight/help.md +2 -2
- package/plugin/skills/ase-task-reboot/help.md +2 -2
- package/plugin/skills/ase-task-rename/help.md +1 -1
- 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
|
+
}
|
package/dst/ase-mint.js
ADDED
|
@@ -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
|
+
}
|
package/dst/ase-service.js
CHANGED
|
@@ -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
|
|
18
|
-
specification, located via the "project.artifact.spec.basedir"
|
|
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
|
|
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
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
static api(log,
|
|
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 === "
|
|
43
|
-
log.write("warning", `specbook: ${cmd}: ${text}`);
|
|
53
|
+
if (level === "none")
|
|
44
54
|
notices?.push(text);
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
log.write(
|
|
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
|
|
76
|
-
const result = await Spec.unmarked(Spec.api(log
|
|
77
|
-
config: Spec.
|
|
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,
|
|
109
|
-
return Spec.unmarked(Spec.api(log,
|
|
110
|
-
config: Spec.
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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("-
|
|
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
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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`
|
|
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],
|
|
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 {
|
package/dst/ase-statusline.js
CHANGED
|
@@ -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.
|
|
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.
|
|
19
|
+
"eslint": "10.10.0",
|
|
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.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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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
|