@rse/ase 0.0.44 → 0.0.48

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 (36) hide show
  1. package/dst/ase-config.js +103 -3
  2. package/dst/ase-getopt.js +5 -1
  3. package/dst/ase-hook.js +48 -1
  4. package/dst/ase-service.js +2 -1
  5. package/dst/ase-skills.js +184 -44
  6. package/dst/ase-statusline.js +1 -1
  7. package/package.json +1 -1
  8. package/plugin/.claude-plugin/plugin.json +1 -1
  9. package/plugin/.github/plugin/plugin.json +1 -1
  10. package/plugin/meta/ase-constitution.md +7 -7
  11. package/plugin/meta/ase-control.md +1 -1
  12. package/plugin/meta/ase-dialog.md +2 -2
  13. package/plugin/meta/ase-persona.md +6 -5
  14. package/plugin/meta/ase-plan.md +3 -3
  15. package/plugin/meta/ase-skill.md +2 -2
  16. package/plugin/package.json +1 -1
  17. package/plugin/skills/ase-arch-analyze/SKILL.md +2 -2
  18. package/plugin/skills/ase-arch-discover/SKILL.md +14 -3
  19. package/plugin/skills/ase-code-craft/SKILL.md +4 -4
  20. package/plugin/skills/ase-code-explain/SKILL.md +1 -1
  21. package/plugin/skills/ase-code-insight/SKILL.md +1 -1
  22. package/plugin/skills/ase-code-lint/SKILL.md +8 -8
  23. package/plugin/skills/ase-code-refactor/SKILL.md +4 -4
  24. package/plugin/skills/ase-code-resolve/SKILL.md +7 -7
  25. package/plugin/skills/ase-docs-proofread/SKILL.md +293 -0
  26. package/plugin/skills/ase-meta-changes/SKILL.md +6 -6
  27. package/plugin/skills/ase-meta-chat/SKILL.md +1 -1
  28. package/plugin/skills/ase-meta-commit/SKILL.md +1 -1
  29. package/plugin/skills/ase-meta-diagram/SKILL.md +5 -5
  30. package/plugin/skills/ase-meta-evaluate/SKILL.md +4 -4
  31. package/plugin/skills/ase-meta-persona/SKILL.md +1 -1
  32. package/plugin/skills/ase-meta-quorum/SKILL.md +1 -1
  33. package/plugin/skills/ase-meta-why/SKILL.md +2 -2
  34. package/plugin/skills/ase-task-edit/SKILL.md +1 -1
  35. package/plugin/skills/ase-task-preflight/SKILL.md +1 -1
  36. package/plugin/skills/ase-task-view/SKILL.md +1 -1
package/dst/ase-config.js CHANGED
@@ -13,6 +13,7 @@ import * as v from "valibot";
13
13
  import Table from "cli-table3";
14
14
  import writeFileAtomic from "write-file-atomic";
15
15
  import lockfile from "proper-lockfile";
16
+ import { z } from "zod";
16
17
  /* classification taxonomy */
17
18
  export const projectClassification = {
18
19
  boxing: ["white", "grey", "black"]
@@ -50,10 +51,11 @@ export const projectClassificationPresets = {
50
51
  }
51
52
  };
52
53
  /* hard-coded map: which scope kinds each variable may be SET on
53
- (reads always cascade through the full chain, this restricts writes only);
54
+ (reads always cascade through the full chain; this restricts writes only);
54
55
  keys absent from this map default to all non-"default" scope kinds */
55
56
  export const configWritableScopes = {
56
- "agent.task": ["session"]
57
+ "agent.task": ["session"],
58
+ "agent.skill": ["session"]
57
59
  };
58
60
  /* default set of scope kinds writable for any unrestricted key */
59
61
  const configWritableScopesDefault = ["user", "project", "task", "session"];
@@ -132,7 +134,8 @@ export const configSchema = v.nullish(v.strictObject({
132
134
  })),
133
135
  agent: v.optional(v.strictObject({
134
136
  persona: v.optional(v.picklist(agentClassification.persona)),
135
- task: v.optional(v.pipe(v.string(), v.minLength(1)))
137
+ task: v.optional(v.pipe(v.string(), v.minLength(1))),
138
+ skill: v.optional(v.pipe(v.string(), v.minLength(1)))
136
139
  }))
137
140
  }));
138
141
  /* encapsulate read/write access to a stack of "<name>.yaml" configuration files,
@@ -626,3 +629,100 @@ export default class ConfigCommand {
626
629
  });
627
630
  }
628
631
  }
632
+ /* MCP registration entry point for layered YAML configuration access */
633
+ export class ConfigMCP {
634
+ log;
635
+ constructor(log) {
636
+ this.log = log;
637
+ }
638
+ /* register the MCP tools */
639
+ register(mcp) {
640
+ /* config get */
641
+ mcp.registerTool("config_get", {
642
+ title: "ASE config get",
643
+ description: "Read the effective value of a dotted configuration `key` from the layered " +
644
+ "configuration, cascading through default/user/project/task/session chain up to and " +
645
+ "including the requested `scope`. Returns the value as JSON-encoded `text`; " +
646
+ "returns an empty string if no value is set.",
647
+ inputSchema: {
648
+ key: z.string()
649
+ .describe("dotted configuration key (e.g. \"agent.skill\")"),
650
+ scope: z.string()
651
+ .describe("scope chain (e.g. \"session:<id>\", \"task:<id>\", \"project\", \"user\")")
652
+ }
653
+ }, async (args) => {
654
+ try {
655
+ const scope = parseScope(args.scope);
656
+ const cfg = new Config("config", configSchema, this.log, scope);
657
+ let text = "";
658
+ cfg.lock(() => {
659
+ cfg.read();
660
+ const val = cfg.get(args.key);
661
+ text = val === undefined ? "" : JSON.stringify(val);
662
+ });
663
+ return { content: [{ type: "text", text }] };
664
+ }
665
+ catch (err) {
666
+ const message = err instanceof Error ? err.message : String(err);
667
+ return { isError: true, content: [{ type: "text", text: `config_get: ERROR: ${message}` }] };
668
+ }
669
+ });
670
+ /* config set */
671
+ mcp.registerTool("config_set", {
672
+ title: "ASE config set",
673
+ description: "Write `val` to a dotted configuration `key` at the target `scope` " +
674
+ "(the strongest scope term in the chain). The value is validated against " +
675
+ "the configuration schema before being persisted.",
676
+ inputSchema: {
677
+ key: z.string()
678
+ .describe("dotted configuration key (e.g. \"agent.skill\")"),
679
+ val: z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(z.any()), z.record(z.string(), z.any())])
680
+ .describe("value to store under `key`"),
681
+ scope: z.string()
682
+ .describe("scope chain (e.g. \"session:<id>\", \"task:<id>\", \"project\", \"user\")")
683
+ }
684
+ }, async (args) => {
685
+ try {
686
+ const scope = parseScope(args.scope);
687
+ const cfg = new Config("config", configSchema, this.log, scope);
688
+ cfg.lock(() => {
689
+ cfg.read();
690
+ cfg.set(args.key, args.val);
691
+ cfg.write();
692
+ });
693
+ return { content: [{ type: "text", text: `config_set: OK: stored "${args.key}" on scope "${args.scope}"` }] };
694
+ }
695
+ catch (err) {
696
+ const message = err instanceof Error ? err.message : String(err);
697
+ return { isError: true, content: [{ type: "text", text: `config_set: ERROR: ${message}` }] };
698
+ }
699
+ });
700
+ /* config delete */
701
+ mcp.registerTool("config_delete", {
702
+ title: "ASE config delete",
703
+ description: "Delete the value at a dotted configuration `key` from the target `scope` " +
704
+ "(the strongest scope term in the chain). No-op if the key is not present.",
705
+ inputSchema: {
706
+ key: z.string()
707
+ .describe("dotted configuration key (e.g. \"agent.skill\")"),
708
+ scope: z.string()
709
+ .describe("scope chain (e.g. \"session:<id>\", \"task:<id>\", \"project\", \"user\")")
710
+ }
711
+ }, async (args) => {
712
+ try {
713
+ const scope = parseScope(args.scope);
714
+ const cfg = new Config("config", configSchema, this.log, scope);
715
+ cfg.lock(() => {
716
+ cfg.read();
717
+ cfg.delete(args.key);
718
+ cfg.write();
719
+ });
720
+ return { content: [{ type: "text", text: `config_delete: OK: removed "${args.key}" on scope "${args.scope}"` }] };
721
+ }
722
+ catch (err) {
723
+ const message = err instanceof Error ? err.message : String(err);
724
+ return { isError: true, content: [{ type: "text", text: `config_delete: ERROR: ${message}` }] };
725
+ }
726
+ });
727
+ }
728
+ }
package/dst/ase-getopt.js CHANGED
@@ -34,7 +34,11 @@ export class GetoptMCP {
34
34
  /* normalize args */
35
35
  const argsRaw = typeof args.args === "string" ? args.args : null;
36
36
  const argsVec = typeof args.args === "string" ?
37
- shParse(args.args).filter((e) => typeof e === "string") :
37
+ shParse(args.args)
38
+ .map((e) => typeof e === "string" ? e :
39
+ (e !== null && typeof e === "object" && "op" in e && e.op === "glob" ?
40
+ e.pattern : null))
41
+ .filter((e) => e !== null) :
38
42
  args.args;
39
43
  /* build a fresh commander program */
40
44
  const cmd = new Command(args.name)
package/dst/ase-hook.js CHANGED
@@ -177,7 +177,7 @@ export default class HookCommand {
177
177
  #{@ase_agent_status} (refreshed on tmux's own interval,
178
178
  independent of Claude Code's statusline repaint cadence).
179
179
  Notice: the Claude Code statusline is not usable for this case
180
- here at all, as it is not repainted during agent processing! */
180
+ at all, as it is not repainted during agent processing! */
181
181
  writeAgentStatus(status) {
182
182
  const icon = status === "busy" ? "▶" : "⏸";
183
183
  if (process.env.TMUX !== undefined
@@ -196,6 +196,26 @@ export default class HookCommand {
196
196
  /* handler for "ase hook stop" (both tools) */
197
197
  doStop(_tool) {
198
198
  this.writeAgentStatus("ready");
199
+ /* safety net: clear any lingering "agent.skill" marker so a
200
+ crashed or aborted skill loop does not leave information active */
201
+ const stdin = fs.readFileSync(0, "utf8");
202
+ const input = stdin.trim() !== "" ? JSON.parse(stdin) : {};
203
+ const sessionId = input.session_id ?? input.sessionId ?? "";
204
+ if (/^[A-Za-z0-9._-]+$/.test(sessionId)) {
205
+ try {
206
+ const cfg = new Config("config", configSchema, this.log, parseScope(`session:${sessionId}`));
207
+ cfg.lock(() => {
208
+ cfg.read();
209
+ if (typeof cfg.get("agent.skill") === "string") {
210
+ cfg.delete("agent.skill");
211
+ cfg.write();
212
+ }
213
+ });
214
+ }
215
+ catch (_e) {
216
+ /* best-effort: ignore failures */
217
+ }
218
+ }
199
219
  return 0;
200
220
  }
201
221
  /* handler for "ase hook session-end" (both tools) */
@@ -218,6 +238,25 @@ export default class HookCommand {
218
238
  }
219
239
  return 0;
220
240
  }
241
+ /* read the session-scoped "agent.skill" config value */
242
+ readActiveSkill(sessionId) {
243
+ if (!/^[A-Za-z0-9._-]+$/.test(sessionId))
244
+ return "";
245
+ try {
246
+ const cfg = new Config("config", configSchema, this.log, parseScope(`session:${sessionId}`));
247
+ let val = "";
248
+ cfg.lock(() => {
249
+ cfg.read();
250
+ const v = cfg.get("agent.skill");
251
+ if (typeof v === "string")
252
+ val = v;
253
+ });
254
+ return val;
255
+ }
256
+ catch (_e) {
257
+ return "";
258
+ }
259
+ }
221
260
  /* handler for "ase hook pre-tool-use" (both tools) */
222
261
  doPreToolUse(tool) {
223
262
  const spec = toolSpecs[tool];
@@ -254,6 +293,14 @@ export default class HookCommand {
254
293
  approve = true;
255
294
  reason = "ASE MCP tool invocation auto-approved";
256
295
  }
296
+ else if (toolName === "Edit") {
297
+ const sessionId = input.session_id ?? input.sessionId ?? "";
298
+ const activeSkill = this.readActiveSkill(sessionId);
299
+ if (activeSkill === "ase-docs-proofread") {
300
+ approve = true;
301
+ reason = `${activeSkill}: user already consented via AskUserQuestion`;
302
+ }
303
+ }
257
304
  /* emit permission decision (or stay silent to defer to default flow).
258
305
  Claude Code expects the decision nested in "hookSpecificOutput";
259
306
  Copilot CLI expects flat top-level fields. */
@@ -15,7 +15,7 @@ import prettyMs from "pretty-ms";
15
15
  import * as v from "valibot";
16
16
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
17
17
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
18
- import { Config, configSchema } from "./ase-config.js";
18
+ import { Config, configSchema, ConfigMCP } from "./ase-config.js";
19
19
  import { DiagramMCP } from "./ase-diagram.js";
20
20
  import { TaskMCP } from "./ase-task.js";
21
21
  import { KVMCP } from "./ase-kv.js";
@@ -241,6 +241,7 @@ export default class ServiceCommand {
241
241
  new TimestampMCP().register(mcp);
242
242
  new GetoptMCP().register(mcp);
243
243
  new SkillsMCP().register(mcp);
244
+ new ConfigMCP(this.log).register(mcp);
244
245
  return mcp;
245
246
  };
246
247
  /* listen to HTTP/REST endpoints */
package/dst/ase-skills.js CHANGED
@@ -6,7 +6,7 @@
6
6
  import { z } from "zod";
7
7
  import { ofetch } from "ofetch";
8
8
  import pacote from "pacote";
9
- /* reusable functionality: gather per-package metadata in maximum parallel */
9
+ /* reusable functionality: gather per-package metadata with maximum parallelism */
10
10
  export class Skills {
11
11
  /* HTTP timeout for the GitHub/npm-downloads side calls */
12
12
  static HTTP_TIMEOUT_MS = 10_000;
@@ -75,50 +75,184 @@ export class Skills {
75
75
  return "N.A.";
76
76
  }
77
77
  }
78
- /* gather metadata for all given components in maximum parallel,
78
+ /* fetch Maven Central metadata for a single coordinate `groupId:artifactId`:
79
+ the Solr search endpoint provides the latest version + a `timestamp` (ms
80
+ epoch) of that release; a second `core=gav` query yields the earliest
81
+ known release, which we treat as the "created" date. The latest POM is
82
+ then downloaded directly from the repo to extract the SCM/project URL. */
83
+ static async fetchMavenInfo(coord) {
84
+ const [groupId, artifactId] = coord.split(":", 2);
85
+ if (groupId === undefined || artifactId === undefined || groupId === "" || artifactId === "")
86
+ return { version: "", created: "", updated: "", repository: "" };
87
+ try {
88
+ const latest = await Skills.httpLimit(() => ofetch("https://search.maven.org/solrsearch/select" +
89
+ `?q=g:%22${encodeURIComponent(groupId)}%22+AND+a:%22${encodeURIComponent(artifactId)}%22` +
90
+ "&rows=1&wt=json", { timeout: Skills.HTTP_TIMEOUT_MS, ignoreResponseError: true }));
91
+ const doc = latest?.response?.docs?.[0];
92
+ const version = doc?.latestVersion ?? doc?.v ?? "";
93
+ const updated = typeof doc?.timestamp === "number" ? new Date(doc.timestamp).toISOString() : "";
94
+ /* Maven Central caps `rows` at 20 and ignores client `sort`,
95
+ so paginate via `start=` to walk all versions, accumulate the
96
+ minimum timestamp client-side */
97
+ const baseUrl = "https://search.maven.org/solrsearch/select" +
98
+ `?q=g:%22${encodeURIComponent(groupId)}%22+AND+a:%22${encodeURIComponent(artifactId)}%22` +
99
+ "&core=gav&rows=20&wt=json";
100
+ let firstTs;
101
+ let start = 0;
102
+ for (let page = 0; page < 50; page++) {
103
+ const chunk = await Skills.httpLimit(() => ofetch(`${baseUrl}&start=${start}`, { timeout: Skills.HTTP_TIMEOUT_MS, ignoreResponseError: true }));
104
+ const docs = chunk?.response?.docs ?? [];
105
+ for (const d of docs) {
106
+ if (typeof d.timestamp === "number" && (firstTs === undefined || d.timestamp < firstTs))
107
+ firstTs = d.timestamp;
108
+ }
109
+ start += docs.length;
110
+ const total = chunk?.response?.numFound ?? 0;
111
+ if (docs.length === 0 || start >= total)
112
+ break;
113
+ }
114
+ const created = typeof firstTs === "number" ? new Date(firstTs).toISOString() : updated;
115
+ let repository = "";
116
+ if (version !== "") {
117
+ try {
118
+ const pom = await Skills.httpLimit(() => ofetch(`https://repo1.maven.org/maven2/${groupId.replace(/\./g, "/")}` +
119
+ `/${artifactId}/${version}/${artifactId}-${version}.pom`, { timeout: Skills.HTTP_TIMEOUT_MS, ignoreResponseError: true, responseType: "text" }));
120
+ if (typeof pom === "string") {
121
+ const scm = /<scm>[\s\S]*?<url>\s*([^<\s]+)\s*<\/url>[\s\S]*?<\/scm>/i.exec(pom);
122
+ if (scm !== null)
123
+ repository = scm[1];
124
+ else {
125
+ const url = /<project\b[\s\S]*?<url>\s*([^<\s]+)\s*<\/url>/i.exec(pom);
126
+ if (url !== null)
127
+ repository = url[1];
128
+ }
129
+ }
130
+ }
131
+ catch {
132
+ repository = "";
133
+ }
134
+ }
135
+ return { version, created, updated, repository };
136
+ }
137
+ catch {
138
+ return { version: "", created: "", updated: "", repository: "" };
139
+ }
140
+ }
141
+ /* fetch the "Used By" count from `mvnrepository.com` as a downloads proxy
142
+ for a Maven coordinate (Maven Central exposes no per-artifact download
143
+ counts). The label `Used By (NNK)` on the artifact landing page denotes
144
+ the number of other published artifacts depending on it. Note: the site
145
+ is fronted by *Cloudflare* and frequently serves a JS-challenge to
146
+ non-browser clients regardless of `User-Agent` (the block is driven by
147
+ TLS/HTTP2 fingerprinting, not headers). A realistic browser UA is
148
+ best-effort and degrades gracefully to `"N.A."` when blocked. */
149
+ static async fetchMavenDownloads(coord) {
150
+ const [groupId, artifactId] = coord.split(":", 2);
151
+ if (groupId === undefined || artifactId === undefined || groupId === "" || artifactId === "")
152
+ return "N.A.";
153
+ try {
154
+ const html = await Skills.httpLimit(() => ofetch(`https://mvnrepository.com/artifact/${encodeURIComponent(groupId)}/${encodeURIComponent(artifactId)}`, {
155
+ timeout: Skills.HTTP_TIMEOUT_MS,
156
+ ignoreResponseError: true,
157
+ responseType: "text",
158
+ headers: {
159
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " +
160
+ "AppleWebKit/537.36 (KHTML, like Gecko) " +
161
+ "Chrome/131.0.0.0 Safari/537.36"
162
+ }
163
+ }));
164
+ if (typeof html !== "string")
165
+ return "N.A.";
166
+ const m = /Used\s+By\s*\(\s*([\d.,]+)\s*([KMB]?)\s*\)/i.exec(html);
167
+ if (m === null)
168
+ return "N.A.";
169
+ const base = parseFloat(m[1].replace(/,/g, ""));
170
+ const mult = m[2] === "K" ? 1_000 : m[2] === "M" ? 1_000_000 : m[2] === "B" ? 1_000_000_000 : 1;
171
+ const n = Math.round(base * mult);
172
+ return Number.isFinite(n) ? n : "N.A.";
173
+ }
174
+ catch {
175
+ return "N.A.";
176
+ }
177
+ }
178
+ /* gather metadata for all given components with maximum parallelism,
79
179
  dispatching on the technology stack:
80
180
  - "JavaScript"/"TypeScript": NPM registry (pacote) + GitHub stars + npm-downloads
81
- - "Java"/"Kotlin"/"Unknown": not yet supported -- return empty result */
181
+ - "Java"/"Kotlin": Maven Central + GitHub stars + mvnrepository.com "Used By"
182
+ - "Unknown": not supported -- return empty result */
82
183
  static async info(stack, components) {
83
- /* FIXME: currently just limited technology stack support */
84
- if (stack !== "JavaScript" && stack !== "TypeScript")
184
+ if (stack === "JavaScript" || stack === "TypeScript") {
185
+ /* per package: kick off packument and downloads in parallel,
186
+ then stars as soon as the packument resolves; across packages
187
+ everything runs concurrently via Promise.all */
188
+ const results = await Promise.all(components.map(async (name) => {
189
+ const packumentPromise = Skills.fetchPackument(name);
190
+ const downloadsPromise = Skills.fetchDownloads(name);
191
+ const starsPromise = packumentPromise.then((p) => Skills.fetchStars(p.repository));
192
+ const [p, downloads, stars] = await Promise.all([
193
+ packumentPromise, downloadsPromise, starsPromise
194
+ ]);
195
+ const created = p.time.created ?? "";
196
+ const updated = p.version !== "" ? (p.time[p.version] ?? "") : "";
197
+ const rank = Skills.computeRank(downloads, stars, created, updated);
198
+ return {
199
+ name,
200
+ version: p.version,
201
+ created,
202
+ updated,
203
+ repository: p.repository,
204
+ stars,
205
+ downloads,
206
+ rank
207
+ };
208
+ }));
209
+ /* sort by rank in descending order (best first) */
210
+ results.sort((a, b) => b.rank - a.rank);
211
+ return results;
212
+ }
213
+ else if (stack === "Java" || stack === "Kotlin") {
214
+ /* per coordinate: kick off Maven Central info and mvnrepository
215
+ downloads in parallel, then stars as soon as the POM-derived
216
+ repository is known; across coordinates everything runs
217
+ concurrently via Promise.all */
218
+ const results = await Promise.all(components.map(async (name) => {
219
+ const infoPromise = Skills.fetchMavenInfo(name);
220
+ const downloadsPromise = Skills.fetchMavenDownloads(name);
221
+ const starsPromise = infoPromise.then((i) => Skills.fetchStars(i.repository));
222
+ const [i, downloads, stars] = await Promise.all([
223
+ infoPromise, downloadsPromise, starsPromise
224
+ ]);
225
+ const rank = Skills.computeRank(downloads, stars, i.created, i.updated);
226
+ return {
227
+ name,
228
+ version: i.version,
229
+ created: i.created,
230
+ updated: i.updated,
231
+ repository: i.repository,
232
+ stars,
233
+ downloads,
234
+ rank
235
+ };
236
+ }));
237
+ /* sort by rank in descending order (best first) */
238
+ results.sort((a, b) => b.rank - a.rank);
239
+ return results;
240
+ }
241
+ else
85
242
  return [];
86
- /* per package: kick off packument and downloads in parallel,
87
- then stars as soon as the packument resolves; across packages
88
- everything runs concurrently via Promise.all */
89
- const results = await Promise.all(components.map(async (name) => {
90
- const packumentPromise = Skills.fetchPackument(name);
91
- const downloadsPromise = Skills.fetchDownloads(name);
92
- const starsPromise = packumentPromise.then((p) => Skills.fetchStars(p.repository));
93
- const [p, downloads, stars] = await Promise.all([
94
- packumentPromise, downloadsPromise, starsPromise
95
- ]);
96
- const created = p.time.created ?? "";
97
- const updated = p.version !== "" ? (p.time[p.version] ?? "") : "";
98
- const rank = Skills.computeRank(downloads, stars, created, updated);
99
- return {
100
- name,
101
- version: p.version,
102
- created,
103
- updated,
104
- repository: p.repository,
105
- stars,
106
- downloads,
107
- rank
108
- };
109
- }));
110
- /* sort by rank in descending order (best first) */
111
- results.sort((a, b) => b.rank - a.rank);
112
- return results;
113
243
  }
114
244
  /* compute composite rank score from weighted metrics:
115
245
  downloads x
116
246
  stars x
117
247
  ([lifespan =] (updated - created)) x
118
- ([recentness =] exp(-(now - updated) / halfLife)) */
248
+ ([recentness =] exp(-(now - updated) / halfLife))
249
+ `"N.A."` factors are treated as neutral `1` so that stacks for which
250
+ a particular metric is structurally unavailable (e.g. Maven Central
251
+ exposes no per-artifact download counts) can still be ranked by the
252
+ remaining metrics, instead of collapsing the entire product to zero. */
119
253
  static computeRank(downloads, stars, created, updated) {
120
- const d = typeof downloads === "number" ? downloads : 0;
121
- const s = typeof stars === "number" ? stars : 0;
254
+ const d = typeof downloads === "number" ? downloads : 1;
255
+ const s = typeof stars === "number" ? stars : 1;
122
256
  const cMs = created !== "" ? Date.parse(created) : NaN;
123
257
  const uMs = updated !== "" ? Date.parse(updated) : NaN;
124
258
  if (Number.isNaN(cMs) || Number.isNaN(uMs))
@@ -160,20 +294,26 @@ export class SkillsMCP {
160
294
  register(mcp) {
161
295
  mcp.registerTool("component_info", {
162
296
  title: "ASE component info",
163
- description: "Gather metadata for a list of NPM packages in maximum parallel. " +
164
- "For each package, fetches the full registry packument via `pacote` " +
165
- "(in-process, no `npm view` subprocess), the GitHub `stargazers_count` " +
297
+ description: "Gather metadata for a list of packages with maximum parallelism, " +
298
+ "dispatching on the technology `stack`. For `JavaScript`/`TypeScript`, " +
299
+ "components are NPM package names: fetches the full registry packument via " +
300
+ "`pacote` (in-process, no `npm view` subprocess), the GitHub `stargazers_count` " +
166
301
  "from `api.github.com` (if the repository points to GitHub), and the " +
167
- "last-month downloads from `api.npmjs.org`. Returns a JSON `text` array " +
168
- "of `{ name, version, created, updated, repository, stars, downloads, rank }` " +
169
- "objects, sorted in descending order by `rank`. Failures of " +
170
- "individual side calls are isolated and reported as `\"N.A.\"` or empty " +
171
- "string so every entry has the full shape.",
302
+ "last-month downloads from `api.npmjs.org`. For `Java`/`Kotlin`, components " +
303
+ "are Maven coordinates of the form `groupId:artifactId`: fetches the latest " +
304
+ "version + earliest/latest release timestamps from the Maven Central Solr " +
305
+ "search endpoint, the SCM/project URL from the latest POM at `repo1.maven.org`, " +
306
+ "GitHub stars (if applicable), and the \"Used By\" count from `mvnrepository.com` " +
307
+ "as the `downloads` proxy. Returns a JSON `text` array of " +
308
+ "`{ name, version, created, updated, repository, stars, downloads, rank }` " +
309
+ "objects, sorted in descending order by `rank`. Failures of individual side " +
310
+ "calls are isolated and reported as `\"N.A.\"` or empty string so every entry " +
311
+ "has the full shape.",
172
312
  inputSchema: {
173
313
  stack: z.string()
174
314
  .describe("Technology stack: \"JavaScript\", \"TypeScript\", \"Java\", \"Kotlin\", or \"Unknown\""),
175
315
  components: z.array(z.string())
176
- .describe("List of package names to gather metadata for")
316
+ .describe("List of package names (NPM) or Maven coordinates `groupId:artifactId` (Java/Kotlin)")
177
317
  }
178
318
  }, async (args) => {
179
319
  try {
@@ -481,7 +481,7 @@ export default class StatuslineCommand {
481
481
  /* send output */
482
482
  process.stdout.write(out);
483
483
  /* optionally publish task id to the calling tmux pane as a per-pane user
484
- option, so someone (like claudeX) can pick it up via #{@ase_task_id} */
484
+ option, so something (like claudeX) can pick it up via #{@ase_task_id} */
485
485
  if (process.env.TMUX !== undefined
486
486
  && process.env.TMUX !== ""
487
487
  && process.env.TMUX_PANE !== undefined
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "homepage": "http://github.com/rse/ase",
7
7
  "repository": { "url": "git+https://github.com/rse/ase.git", "type": "git" },
8
8
  "bugs": { "url": "http://github.com/rse/ase/issues" },
9
- "version": "0.0.44",
9
+ "version": "0.0.48",
10
10
  "license": "GPL-3.0-only",
11
11
  "author": {
12
12
  "name": "Dr. Ralf S. Engelschall",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ase",
3
- "version": "0.0.44",
3
+ "version": "0.0.48",
4
4
  "description": "Agentic Software Engineering (ASE)",
5
5
  "keywords": [ "agentic", "software", "engineering" ],
6
6
  "homepage": "https://ase.tools",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ase",
3
- "version": "0.0.44",
3
+ "version": "0.0.48",
4
4
  "description": "Agentic Software Engineering (ASE)",
5
5
  "keywords": [ "agentic", "software", "engineering" ],
6
6
  "homepage": "https://ase.tools",
@@ -18,14 +18,14 @@ you *MUST* once and immediately output the following <template/> now:
18
18
  Prohibitions
19
19
  ------------
20
20
 
21
- - Do *not* factor out (move) code into own functions without good reason, like necessary reusability.
22
- - Do *not* use braces around single statement blocks in "if" and "while" constructs unless the language requires them.
21
+ - Do *not* factor out (move) code blocks into their own functions without good reason, such as necessary reusability.
22
+ - Do *not* use braces around single-statement blocks in "if" and "while" constructs unless the language requires them.
23
23
  - Do *not* insist on early "return" in "if" blocks if an "else" block exists.
24
- - Do *not* remove any whitespaces in the code formatting -- keep whitespaces align with code base.
24
+ - Do *not* remove any whitespace in the code formatting -- keep whitespace aligned with code base.
25
25
  - Do *not* show just partial code changes -- always show complete changes.
26
26
  - Do *not* produce any line which consists of just one or more space characters before the newline -- use just the newline.
27
27
  - Do *not* use semicolons where they are syntactically optional, except inside `for` clauses.
28
- - Do *not* split continuous chunks of code less than 100 lines into individual functions.
28
+ - Do *not* split continuous chunks of code fewer than 100 lines into individual functions.
29
29
  - Do *not* refactor deeply nested code constructs into individual functions.
30
30
  - Do *not* answer with the "You're absolutely right", "You are
31
31
  absolutely right", "You're absolutely correct", or "You are absolutely
@@ -41,16 +41,16 @@ Commandments
41
41
  - Be very *pedantic* on code style.
42
42
  - Before proposing any code changes, explain *WHAT* the proposed changes do and *WHY* it is necessary.
43
43
  - Propose *entire, complete, and necessary code change sets* for each solution.
44
- - Place a *blank line before a comment line*, but not when it is the first line of a block or an end of line comment.
44
+ - Place a *blank line before a comment line*, but not when it is the first line of a block or an end-of-line comment.
45
45
  - Keep code and comment *formatting exactly as in the existing code*.
46
46
  - Use *regular comments* `/* [...] */` instead of end-of-line comments `// [...]`.
47
47
  - Use *two leading/trailing spaces within comments* as in `/* [...] */`.
48
- - Always use *parenthesis around arrow function parameters*, even for a single parameter.
48
+ - Always use *parentheses around arrow function parameters*, even for a single parameter.
49
49
  - Make a line break before the keywords "else", "catch", and "finally".
50
50
  - Try to *vertically align similar operators* on consecutive, similar lines.
51
51
  - Place spaces after opening and before closing angle brackets and braces.
52
52
  - Use *double-quotes* (`"[...]"`) instead of single-quotes (`'[...]'`) for all strings.
53
- - Use K&R coding style with *opening braces* at end of lines and *closing braces* at the begin of lines.
53
+ - Use K&R coding style with *opening braces* at the end of lines and *closing braces* at the beginning of lines.
54
54
  - When a language has a *more strongly-typed variant*, prefer that
55
55
  variant (e.g., TypeScript over JavaScript, Python with type hints
56
56
  over untyped Python).
@@ -14,7 +14,7 @@ Control Flow Constructs
14
14
  Do not output anything.
15
15
 
16
16
  - *IMPORTANT*: You *MUST* honor the following control flow construct:
17
- <expand name="<define-name/>" [arg1="<expand-arg1/>" [arg2="<expand-arg2/>]" [...]]]><expand-content/></expand>:
17
+ <expand name="<define-name/>" [arg1="<expand-arg1/>" [arg2="<expand-arg2/>" [...]]]><expand-content/></expand>:
18
18
 
19
19
  This specifies the *expansion* of previous <define/>.
20
20
  This construct is expanded into the <define-body/> of <define/>
@@ -79,7 +79,7 @@ Let the *user interactively choose* an answer.
79
79
  tool result `"<question-description/>"="<answer/>"`.
80
80
  Set <result><answer/></result>.
81
81
  If <result/> is then NOT one
82
- the "label" values from <config/>, set
82
+ of the "label" values from <config/>, set
83
83
  <result>OTHER: <result/></result>
84
84
  (prefix result with "OTHER").
85
85
 
@@ -109,7 +109,7 @@ Let the *user interactively choose* an answer.
109
109
  2. Call the `ask_user` tool of the agent harness with:
110
110
 
111
111
  `ask_user({
112
- question: "<question-label>: <question-description/>",
112
+ question: "<question-label/>: <question-description/>",
113
113
  allow_freeform: true,
114
114
  choices: [
115
115
  <config/>
@@ -19,7 +19,8 @@ Persona Ruleset Levels
19
19
  ("sure", "certainly", "of course", "happy to", etc).
20
20
  - You *MUST* *drop* hedging
21
21
  ("I think", "maybe", "perhaps", "it seems", "sort of", "probably", "I'm not sure but...", "it might be", etc).
22
- - You *MUST* *prefer* bullet points with one or two sentences each, instead of long prose paragraphs.
22
+ - You *MUST* *prefer* lists with bullet points, instead of long prose paragraphs.
23
+ - You *MUST* *prefer* bullet points with one or two sentences.
23
24
  </define>
24
25
 
25
26
  ### Level 2
@@ -46,12 +47,12 @@ Persona Ruleset Levels
46
47
  - You *MUST* *use* only one word, when one word is clear enough.
47
48
  - You *MUST* *use* only two words, when two words are clear enough.
48
49
  - You *MUST* *use* the three sentence patterns
49
- (dependending what information has to be expressed):
50
+ (depending what information has to be expressed):
50
51
  - `<subject/> <action/> <object/>, <reason/>.` → e.g. "Cat eats fish, hungry."
51
52
  - `<subject/> <action/> <object/>.` → e.g. "Dog chases ball."
52
53
  - `<subject/> <action/>.` → e.g. "Birds fly."
53
- - You *MUST* *drop* even all bullet points and just
54
- provide very short subsequent sentences.
54
+ - You *MUST* *drop* all lists and their bullet points and instead
55
+ provide very short subsequent sentences only.
55
56
  </define>
56
57
 
57
58
  Apply Persona
@@ -79,7 +80,7 @@ rules even if later skill rules say different:
79
80
  </if>
80
81
 
81
82
  - <if condition="<ase-persona-style/> is 'caveman'">
82
- - You *MUST* use the terse, rough and stuttering communication style of a *caveman*.
83
+ - You *MUST* use the terse, rough, and stuttering communication style of a *caveman*.
83
84
  - Apply ruleset "level0": <expand name="level0"/>
84
85
  - Apply ruleset "level1": <expand name="level1"/>
85
86
  - Apply ruleset "level2": <expand name="level2"/>