@rse/ase 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dst/ase-artifact.js +7 -56
- package/dst/ase-config.js +6 -9
- package/dst/ase-hook.js +16 -2
- package/dst/ase-ignore.js +129 -0
- package/dst/ase-setup.js +161 -71
- package/dst/ase-spec.js +26 -10
- package/dst/ase-table.js +80 -0
- package/package.json +7 -7
- 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/meta/ase-format-spec.md +19 -14
- package/plugin/meta/ase-format-specbook.yaml +23 -20
- package/plugin/meta/ase-skill.md +7 -0
- package/plugin/output-styles/ase-terse.md +58 -0
- package/plugin/skills/ase-help-intent/data.md +7 -6
- package/plugin/skills/ase-spec-activate/SKILL.md +14 -12
- package/plugin/skills/ase-spec-activate/help.md +7 -6
package/dst/ase-artifact.js
CHANGED
|
@@ -10,6 +10,7 @@ import { isScalar } from "yaml";
|
|
|
10
10
|
import { z } from "zod";
|
|
11
11
|
import { Config, configSchema } from "./ase-config.js";
|
|
12
12
|
import { Task } from "./ase-task.js";
|
|
13
|
+
import { Ignore } from "./ase-ignore.js";
|
|
13
14
|
import { writeStdout } from "./ase-stdio.js";
|
|
14
15
|
/* the recognized artifact kinds, in descending precedence order;
|
|
15
16
|
"othr" is the implicit catch-all and is always resolved last */
|
|
@@ -26,72 +27,22 @@ export class Artifact {
|
|
|
26
27
|
`(expected one of: ${artifactKinds.join(", ")})`);
|
|
27
28
|
return kind;
|
|
28
29
|
}
|
|
29
|
-
/* translate a single ".gitignore" line into a picomatch-backed rule,
|
|
30
|
-
honoring the anchored-vs-floating, directory-only, and negation
|
|
31
|
-
semantics of ".gitignore" patterns; anchored patterns resolve
|
|
32
|
-
relative to the "base" directory holding the ".gitignore" */
|
|
33
|
-
static compileIgnoreRule(line, base) {
|
|
34
|
-
let pattern = line.trim();
|
|
35
|
-
if (pattern === "" || pattern.startsWith("#"))
|
|
36
|
-
return null;
|
|
37
|
-
let negated = false;
|
|
38
|
-
if (pattern.startsWith("!")) {
|
|
39
|
-
negated = true;
|
|
40
|
-
pattern = pattern.slice(1);
|
|
41
|
-
}
|
|
42
|
-
let dirOnly = false;
|
|
43
|
-
if (pattern.endsWith("/")) {
|
|
44
|
-
dirOnly = true;
|
|
45
|
-
pattern = pattern.slice(0, -1);
|
|
46
|
-
}
|
|
47
|
-
const anchored = pattern.includes("/");
|
|
48
|
-
if (pattern.startsWith("/"))
|
|
49
|
-
pattern = pattern.slice(1);
|
|
50
|
-
const glob = anchored ? (base === "" ? pattern : `${base}/${pattern}`) : `**/${pattern}`;
|
|
51
|
-
const isMatch = picomatch(glob, { dot: true });
|
|
52
|
-
return { matcher: (p) => isMatch(p), negated, dirOnly };
|
|
53
|
-
}
|
|
54
|
-
/* load the ".gitignore" rules located directly in a directory,
|
|
55
|
-
given as absolute "dir" and project-relative "relDir" */
|
|
56
|
-
static loadIgnoreRules(dir, relDir) {
|
|
57
|
-
const file = path.join(dir, ".gitignore");
|
|
58
|
-
if (!fs.existsSync(file))
|
|
59
|
-
return [];
|
|
60
|
-
const rules = [];
|
|
61
|
-
for (const line of fs.readFileSync(file, "utf8").split(/\r?\n/)) {
|
|
62
|
-
const rule = Artifact.compileIgnoreRule(line, relDir);
|
|
63
|
-
if (rule !== null)
|
|
64
|
-
rules.push(rule);
|
|
65
|
-
}
|
|
66
|
-
return rules;
|
|
67
|
-
}
|
|
68
|
-
/* decide whether a project-relative path is ignored by the given
|
|
69
|
-
ordered ".gitignore" rule set (last matching rule wins) */
|
|
70
|
-
static isIgnored(rel, isDir, rules) {
|
|
71
|
-
let ignored = false;
|
|
72
|
-
for (const rule of rules) {
|
|
73
|
-
if (rule.dirOnly && !isDir)
|
|
74
|
-
continue;
|
|
75
|
-
if (rule.matcher(rel))
|
|
76
|
-
ignored = !rule.negated;
|
|
77
|
-
}
|
|
78
|
-
return ignored;
|
|
79
|
-
}
|
|
80
30
|
/* build the file universe by walking the project tree from the
|
|
81
|
-
project root, honoring
|
|
82
|
-
|
|
31
|
+
project root, honoring the Git exclude rules (the repository-wide
|
|
32
|
+
ones plus the root and nested ".gitignore" files) and always
|
|
33
|
+
pruning ".git". Yields POSIX project-relative, sorted,
|
|
83
34
|
de-duplicated file paths */
|
|
84
35
|
static universe() {
|
|
85
36
|
const root = Task.projectRoot();
|
|
86
37
|
const files = new Set();
|
|
87
38
|
const walk = (dir, relDir, inherited) => {
|
|
88
|
-
const rules = [...inherited, ...
|
|
39
|
+
const rules = [...inherited, ...Ignore.rules(dir, relDir)];
|
|
89
40
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
90
41
|
if (entry.name === ".git")
|
|
91
42
|
continue;
|
|
92
43
|
const rel = relDir === "" ? entry.name : `${relDir}/${entry.name}`;
|
|
93
44
|
const isDir = entry.isDirectory();
|
|
94
|
-
if (
|
|
45
|
+
if (Ignore.matches(rel, isDir, rules))
|
|
95
46
|
continue;
|
|
96
47
|
if (isDir)
|
|
97
48
|
walk(path.join(dir, entry.name), rel, rules);
|
|
@@ -99,7 +50,7 @@ export class Artifact {
|
|
|
99
50
|
files.add(rel);
|
|
100
51
|
}
|
|
101
52
|
};
|
|
102
|
-
walk(root, "",
|
|
53
|
+
walk(root, "", Ignore.base(root));
|
|
103
54
|
return [...files].sort((a, b) => a.localeCompare(b));
|
|
104
55
|
}
|
|
105
56
|
/* raw-resolve a single kind's configuration spec against the file
|
package/dst/ase-config.js
CHANGED
|
@@ -10,12 +10,12 @@ import readline from "node:readline/promises";
|
|
|
10
10
|
import { Document, parseDocument, isMap, isScalar } from "yaml";
|
|
11
11
|
import { execaSync } from "execa";
|
|
12
12
|
import * as v from "valibot";
|
|
13
|
-
import Table from "cli-table3";
|
|
14
13
|
import writeFileAtomic from "write-file-atomic";
|
|
15
14
|
import lockfile from "proper-lockfile";
|
|
16
15
|
import { z } from "zod";
|
|
17
16
|
import { LRUCache } from "lru-cache";
|
|
18
17
|
import { writeStdout } from "./ase-stdio.js";
|
|
18
|
+
import { renderTable } from "./ase-table.js";
|
|
19
19
|
/* classification taxonomy */
|
|
20
20
|
export const projectClassification = {
|
|
21
21
|
boxing: ["white", "grey", "black"]
|
|
@@ -184,7 +184,8 @@ const artifactSchema = v.optional(v.strictObject({
|
|
|
184
184
|
files: v.optional(v.string())
|
|
185
185
|
}));
|
|
186
186
|
/* schema for the "spec" artifact kind, additionally carrying the
|
|
187
|
-
SpecBook YAML "schema" configuration
|
|
187
|
+
whitespace-separated list of SpecBook YAML "schema" configuration
|
|
188
|
+
files, with "std" naming the bundled standard one (empty: "std") */
|
|
188
189
|
const artifactSpecSchema = v.optional(v.strictObject({
|
|
189
190
|
basedir: v.optional(v.string()),
|
|
190
191
|
files: v.optional(v.string()),
|
|
@@ -632,16 +633,12 @@ export default class ConfigCommand {
|
|
|
632
633
|
const scope = parseScope(cmd.optsWithGlobals().scope);
|
|
633
634
|
const cfg = new Config("config", configSchema, this.log, scope);
|
|
634
635
|
cfg.read();
|
|
635
|
-
const
|
|
636
|
-
head: ["KEY", "VALUE", "SCOPE"],
|
|
637
|
-
chars: { "mid": "", "left-mid": "", "mid-mid": "", "right-mid": "" },
|
|
638
|
-
style: { head: ["blue"] }
|
|
639
|
-
});
|
|
636
|
+
const rows = [];
|
|
640
637
|
for (const e of cfg.entries()) {
|
|
641
638
|
const val = isScalar(e.value) ? e.value.value : e.value;
|
|
642
|
-
|
|
639
|
+
rows.push([e.key, String(val), Config.scopeLabel(e.scope)]);
|
|
643
640
|
}
|
|
644
|
-
await writeStdout(
|
|
641
|
+
await writeStdout(renderTable(["KEY", "VALUE", "SCOPE"], rows));
|
|
645
642
|
});
|
|
646
643
|
/* register CLI sub-command "ase config edit" */
|
|
647
644
|
configCmd
|
package/dst/ase-hook.js
CHANGED
|
@@ -208,9 +208,11 @@ export default class HookCommand {
|
|
|
208
208
|
/* determine path to external files */
|
|
209
209
|
const filePkg = path.join(pluginRoot, ".claude-plugin", "plugin.json");
|
|
210
210
|
const fileMd = path.join(pluginRoot, "meta", "ase-constitution.md");
|
|
211
|
+
const fileStyle = path.join(pluginRoot, "output-styles", "ase-terse.md");
|
|
211
212
|
/* read external files */
|
|
212
213
|
let pkg;
|
|
213
214
|
let md;
|
|
215
|
+
let style;
|
|
214
216
|
try {
|
|
215
217
|
pkg = fs.readFileSync(filePkg, "utf8");
|
|
216
218
|
}
|
|
@@ -223,6 +225,12 @@ export default class HookCommand {
|
|
|
223
225
|
catch (err) {
|
|
224
226
|
throw new Error(`failed to read constitution file: ${fileMd}`, { cause: err });
|
|
225
227
|
}
|
|
228
|
+
try {
|
|
229
|
+
style = fs.readFileSync(fileStyle, "utf8");
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
throw new Error(`failed to read output style file: ${fileStyle}`, { cause: err });
|
|
233
|
+
}
|
|
226
234
|
/* determine own version */
|
|
227
235
|
const pkgObj = this.parseJSON(pkg, v.object({ version: v.optional(v.string()) }));
|
|
228
236
|
const versionCurrentPlugin = pkgObj.version ?? "";
|
|
@@ -292,8 +300,8 @@ export default class HookCommand {
|
|
|
292
300
|
const persona = setting("agent.persona", "ASE_PERSONA_STYLE", "engineer");
|
|
293
301
|
const guidance = setting("agent.guidance", "ASE_GUIDANCE_LEVEL", "normal");
|
|
294
302
|
const boxing = setting("project.boxing", "ASE_PROJECT_BOXING", "white");
|
|
295
|
-
/* determine the specification base directory and the
|
|
296
|
-
schema
|
|
303
|
+
/* determine the specification base directory and the whitespace-separated
|
|
304
|
+
list of SpecBook schema configurations (empty: the bundled standard schema) */
|
|
297
305
|
const specBasedir = String(cfg.get("project.artifact.spec.basedir") ?? "");
|
|
298
306
|
const specSchema = String(cfg.get("project.artifact.spec.schema") ?? "");
|
|
299
307
|
/* determine headless mode */
|
|
@@ -340,6 +348,12 @@ export default class HookCommand {
|
|
|
340
348
|
"\n" + md;
|
|
341
349
|
/* expand all @<file> references manually */
|
|
342
350
|
md = this.expandReferences(md, path.dirname(fileMd));
|
|
351
|
+
/* append the output style to the constitution markdown (GitHub
|
|
352
|
+
Copilot CLI and OpenAI Codex CLI only -- Anthropic Claude Code CLI
|
|
353
|
+
applies the plugin output style natively), stripping the
|
|
354
|
+
YAML frontmatter which only Anthropic Claude Code CLI understands */
|
|
355
|
+
if (tool !== "claude")
|
|
356
|
+
md += "\n" + style.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "");
|
|
343
357
|
/* build the deterministic ASE banner (rendered directly by the
|
|
344
358
|
agent harness, independent of any model decision, so it is
|
|
345
359
|
guaranteed to appear once in every non-headless session;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** Agentic Software Engineering (ASE)
|
|
3
|
+
** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
|
+
*/
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import { execaSync } from "execa";
|
|
10
|
+
import picomatch from "picomatch";
|
|
11
|
+
import { LRUCache } from "lru-cache";
|
|
12
|
+
/* reusable functionality: decide which project-relative paths Git
|
|
13
|
+
excludes from the project, honoring the same three rule sources and
|
|
14
|
+
the same precedence order as Git itself -- the global excludes file,
|
|
15
|
+
the repository-local "info/exclude", and the per-directory
|
|
16
|
+
".gitignore" files from the project root down to the path */
|
|
17
|
+
export class Ignore {
|
|
18
|
+
/* translate a single Git exclude line into a picomatch-backed rule,
|
|
19
|
+
honoring the anchored-vs-floating, directory-only, and negation
|
|
20
|
+
semantics of the pattern; an anchored pattern resolves relative
|
|
21
|
+
to the "base" directory its rule file governs */
|
|
22
|
+
static compile(line, base) {
|
|
23
|
+
let pattern = line.trim();
|
|
24
|
+
if (pattern === "" || pattern.startsWith("#"))
|
|
25
|
+
return null;
|
|
26
|
+
let negated = false;
|
|
27
|
+
if (pattern.startsWith("!")) {
|
|
28
|
+
negated = true;
|
|
29
|
+
pattern = pattern.slice(1);
|
|
30
|
+
}
|
|
31
|
+
let dirOnly = false;
|
|
32
|
+
if (pattern.endsWith("/")) {
|
|
33
|
+
dirOnly = true;
|
|
34
|
+
pattern = pattern.slice(0, -1);
|
|
35
|
+
}
|
|
36
|
+
const anchored = pattern.includes("/");
|
|
37
|
+
if (pattern.startsWith("/"))
|
|
38
|
+
pattern = pattern.slice(1);
|
|
39
|
+
const glob = anchored ? (base === "" ? pattern : `${base}/${pattern}`) : `**/${pattern}`;
|
|
40
|
+
const isMatch = picomatch(glob, { dot: true });
|
|
41
|
+
return { matcher: (p) => isMatch(p), negated, dirOnly };
|
|
42
|
+
}
|
|
43
|
+
/* load the rules of a single exclude file, given as absolute "file"
|
|
44
|
+
and the project-relative "base" its anchored patterns resolve
|
|
45
|
+
against; a missing file simply contributes no rules */
|
|
46
|
+
static load(file, base) {
|
|
47
|
+
if (file === "" || !fs.existsSync(file))
|
|
48
|
+
return [];
|
|
49
|
+
const rules = [];
|
|
50
|
+
for (const line of fs.readFileSync(file, "utf8").split(/\r?\n/)) {
|
|
51
|
+
const rule = Ignore.compile(line, base);
|
|
52
|
+
if (rule !== null)
|
|
53
|
+
rules.push(rule);
|
|
54
|
+
}
|
|
55
|
+
return rules;
|
|
56
|
+
}
|
|
57
|
+
/* load the ".gitignore" rules located directly in a directory,
|
|
58
|
+
given as absolute "dir" and project-relative "relDir" */
|
|
59
|
+
static rules(dir, relDir) {
|
|
60
|
+
return Ignore.load(path.join(dir, ".gitignore"), relDir);
|
|
61
|
+
}
|
|
62
|
+
/* resolve the global Git excludes file: the configured
|
|
63
|
+
"core.excludesFile", or else the XDG location Git falls back
|
|
64
|
+
onto when that configuration value is unset */
|
|
65
|
+
static globalFile() {
|
|
66
|
+
let file = "";
|
|
67
|
+
try {
|
|
68
|
+
file = execaSync("git", ["config", "--get", "core.excludesFile"], { stderr: "ignore" }).stdout.trim();
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
/* no such configuration value */
|
|
72
|
+
}
|
|
73
|
+
if (file === "") {
|
|
74
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
75
|
+
return xdg !== undefined && xdg !== "" ?
|
|
76
|
+
path.join(xdg, "git", "ignore") :
|
|
77
|
+
path.join(os.homedir(), ".config", "git", "ignore");
|
|
78
|
+
}
|
|
79
|
+
if (file.startsWith("~/"))
|
|
80
|
+
file = path.join(os.homedir(), file.slice(2));
|
|
81
|
+
return file;
|
|
82
|
+
}
|
|
83
|
+
/* resolve the repository-local exclude file, held by the Git
|
|
84
|
+
*common* directory, so that a Git worktree shares the exclude
|
|
85
|
+
file of the main working tree it was created from */
|
|
86
|
+
static infoFile(root) {
|
|
87
|
+
try {
|
|
88
|
+
const dir = execaSync("git", ["rev-parse", "--git-common-dir"], { cwd: root, stderr: "ignore" }).stdout.trim();
|
|
89
|
+
if (dir !== "")
|
|
90
|
+
return path.join(path.resolve(root, dir), "info", "exclude");
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
/* not inside a Git working tree */
|
|
94
|
+
}
|
|
95
|
+
return "";
|
|
96
|
+
}
|
|
97
|
+
/* cached repository-wide rules (TTL-bounded, as the ASE service is
|
|
98
|
+
long-running and both the Git configuration and the exclude files
|
|
99
|
+
can change underneath it; each determination spawns Git, too) */
|
|
100
|
+
static baseCache = new LRUCache({ max: 4, ttl: 10 * 1000 });
|
|
101
|
+
/* the rules which no directory of the project owns and which hence
|
|
102
|
+
govern the project as a whole, in ascending Git precedence: the
|
|
103
|
+
global excludes file first, the repository-local "info/exclude"
|
|
104
|
+
second, so a later rule overrides an earlier one as it does in
|
|
105
|
+
Git; both anchor their patterns at the project root */
|
|
106
|
+
static base(root) {
|
|
107
|
+
const cached = Ignore.baseCache.get(root);
|
|
108
|
+
if (cached !== undefined)
|
|
109
|
+
return cached;
|
|
110
|
+
const rules = [
|
|
111
|
+
...Ignore.load(Ignore.globalFile(), ""),
|
|
112
|
+
...Ignore.load(Ignore.infoFile(root), "")
|
|
113
|
+
];
|
|
114
|
+
Ignore.baseCache.set(root, rules);
|
|
115
|
+
return rules;
|
|
116
|
+
}
|
|
117
|
+
/* decide whether a project-relative path is excluded by the given
|
|
118
|
+
ordered rule set, where the last matching rule wins */
|
|
119
|
+
static matches(rel, isDir, rules) {
|
|
120
|
+
let ignored = false;
|
|
121
|
+
for (const rule of rules) {
|
|
122
|
+
if (rule.dirOnly && !isDir)
|
|
123
|
+
continue;
|
|
124
|
+
if (rule.matcher(rel))
|
|
125
|
+
ignored = !rule.negated;
|
|
126
|
+
}
|
|
127
|
+
return ignored;
|
|
128
|
+
}
|
|
129
|
+
}
|