@rse/ase 0.9.57 → 0.9.59

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.
@@ -24,8 +24,10 @@ import { MarkdownMCP } from "./ase-markdown.js";
24
24
  import { ArtifactMCP } from "./ase-artifact.js";
25
25
  import { KVMCP } from "./ase-kv.js";
26
26
  import { TimestampMCP } from "./ase-timestamp.js";
27
+ import { SleepMCP } from "./ase-sleep.js";
27
28
  import { GetoptMCP } from "./ase-getopt.js";
28
29
  import { SkillsMCP } from "./ase-skills.js";
30
+ import { WorktreeMCP } from "./ase-worktree.js";
29
31
  import pkg from "../package.json" with { type: "json" };
30
32
  /* shared service host */
31
33
  export const SERVICE_HOST = "127.0.0.1";
@@ -268,8 +270,10 @@ export default class ServiceCommand {
268
270
  new ArtifactMCP(this.log).register(mcp);
269
271
  new KVMCP().register(mcp);
270
272
  new TimestampMCP().register(mcp);
273
+ new SleepMCP().register(mcp);
271
274
  new GetoptMCP().register(mcp);
272
275
  new SkillsMCP().register(mcp);
276
+ new WorktreeMCP().register(mcp);
273
277
  new ConfigMCP(this.log).register(mcp);
274
278
  return mcp;
275
279
  };
@@ -0,0 +1,26 @@
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 { z } from "zod";
7
+ /* MCP registration entry point for sleep tool */
8
+ export class SleepMCP {
9
+ register(mcp) {
10
+ mcp.registerTool("ase_sleep", {
11
+ title: "ASE sleep",
12
+ description: "Wait once for `duration` seconds and then return. " +
13
+ "The duration can be fractional (e.g. `1.5`) and is at most 120 (default: 60). " +
14
+ "Returns `OK: slept <duration> seconds` as `text` after the duration elapsed.",
15
+ inputSchema: {
16
+ duration: z.number().positive().max(120).default(60)
17
+ .describe("wait duration in seconds (fractional values allowed, at most 120, default 60)")
18
+ }
19
+ }, async (args) => {
20
+ await new Promise((resolve) => setTimeout(resolve, args.duration * 1000));
21
+ return {
22
+ content: [{ type: "text", text: `OK: slept ${args.duration} seconds` }]
23
+ };
24
+ });
25
+ }
26
+ }
@@ -0,0 +1,162 @@
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 fs from "node:fs";
8
+ import { execaSync } from "execa";
9
+ import { z } from "zod";
10
+ import { writeStdout } from "./ase-stdio.js";
11
+ /* the repository-root-relative path components of the base directory
12
+ which holds all ASE-managed Git worktrees */
13
+ const baseComponents = [".ase", "worktree"];
14
+ /* reusable functionality: safe resolution of the ASE worktree
15
+ directories under <repo-root>/.ase/worktree/<id> */
16
+ export class Worktree {
17
+ /* validate a worktree id to keep it safe both as a single path
18
+ component and as the Git branch name derived from it */
19
+ static validateId(id) {
20
+ if (typeof id !== "string" || id.length === 0)
21
+ throw new Error("worktree: id must be a non-empty string");
22
+ if (!/^[A-Za-z0-9_-]+$/.test(id))
23
+ throw new Error("worktree: id must match [A-Za-z0-9_-]+");
24
+ }
25
+ /* determine the fully resolved top-level directory of the Git
26
+ working tree, as a Git worktree can only be created from
27
+ inside one */
28
+ static repoRoot() {
29
+ let top = "";
30
+ try {
31
+ top = execaSync("git", ["rev-parse", "--show-toplevel"], { stderr: "ignore" }).stdout.trim();
32
+ }
33
+ catch {
34
+ /* not inside a Git working tree */
35
+ }
36
+ if (top === "")
37
+ throw new Error("worktree: not inside a Git working tree -- cannot create a worktree");
38
+ return fs.realpathSync(top);
39
+ }
40
+ /* assert that an already existing path is a real, non-aliased
41
+ directory: "git worktree add" silently follows a symlinked path
42
+ component, so a repository carrying a committed ".ase" or
43
+ ".ase/worktree" symlink would make it write outside the
44
+ repository entirely */
45
+ static assertRealDir(dir) {
46
+ let st;
47
+ try {
48
+ st = fs.lstatSync(dir);
49
+ }
50
+ catch {
51
+ /* a not yet existing path component is created later on */
52
+ return;
53
+ }
54
+ if (st.isSymbolicLink())
55
+ throw new Error(`worktree: "${dir}" is a symbolic link -- ` +
56
+ "refusing to create a worktree through it");
57
+ if (!st.isDirectory())
58
+ throw new Error(`worktree: "${dir}" exists but is not a directory`);
59
+ const real = fs.realpathSync(dir);
60
+ if (real !== dir)
61
+ throw new Error(`worktree: "${dir}" resolves to "${real}" -- ` +
62
+ "refusing to create a worktree through an aliased path");
63
+ }
64
+ /* resolve the base directory holding all ASE worktrees, asserting
65
+ that every path component below the repository root is a real
66
+ directory; the base directory is created on demand only */
67
+ static baseDir(create = false) {
68
+ let dir = Worktree.repoRoot();
69
+ for (const component of baseComponents) {
70
+ dir = path.join(dir, component);
71
+ Worktree.assertRealDir(dir);
72
+ }
73
+ if (create) {
74
+ fs.mkdirSync(dir, { recursive: true });
75
+ Worktree.assertRealDir(dir);
76
+ }
77
+ return dir;
78
+ }
79
+ /* resolve the worktree directory of a single id; the leaf itself is
80
+ created by "git worktree add" and hence only has to be free of an
81
+ aliasing entry left behind by an earlier run */
82
+ static dir(id, create = false) {
83
+ Worktree.validateId(id);
84
+ const dir = path.join(Worktree.baseDir(create), id);
85
+ Worktree.assertRealDir(dir);
86
+ return dir;
87
+ }
88
+ }
89
+ /* CLI command "ase worktree" */
90
+ export default class WorktreeCommand {
91
+ /* register commands */
92
+ register(program) {
93
+ /* register CLI top-level command "ase worktree" */
94
+ const worktree = program
95
+ .command("worktree")
96
+ .description("Safely resolve the ASE worktree directories under <repo-root>/.ase/worktree")
97
+ .action(() => {
98
+ worktree.outputHelp();
99
+ process.exit(1);
100
+ });
101
+ /* register CLI sub-command "ase worktree base" */
102
+ worktree
103
+ .command("base")
104
+ .description("Print the validated base directory holding all ASE worktrees")
105
+ .option("-c, --create", "create the base directory if it does not exist yet")
106
+ .action(async (opts) => {
107
+ await writeStdout(`${Worktree.baseDir(opts.create ?? false)}\n`);
108
+ });
109
+ /* register CLI sub-command "ase worktree path" */
110
+ worktree
111
+ .command("path")
112
+ .description("Print the validated worktree directory of a single <id>")
113
+ .argument("<id>", "Worktree identifier")
114
+ .option("-c, --create", "create the base directory if it does not exist yet")
115
+ .action(async (id, opts) => {
116
+ await writeStdout(`${Worktree.dir(id, opts.create ?? false)}\n`);
117
+ });
118
+ }
119
+ }
120
+ /* render a caught error as an MCP tool error result */
121
+ const mcpToolError = (err) => ({
122
+ isError: true,
123
+ content: [{ type: "text", text: `ERROR: ${err instanceof Error ? err.message : String(err)}` }]
124
+ });
125
+ /* MCP registration entry point for worktree tools */
126
+ export class WorktreeMCP {
127
+ register(mcp) {
128
+ mcp.registerTool("ase_worktree_path", {
129
+ title: "ASE worktree path",
130
+ description: "Resolve the absolute, validated directory of an ASE-managed Git worktree. " +
131
+ "Returns `<repo-root>/.ase/worktree/<id>` as `text` if `id` is given, and the " +
132
+ "base directory `<repo-root>/.ase/worktree` otherwise. " +
133
+ "You MUST call this to obtain the directory of every worktree instead of " +
134
+ "assembling the path yourself: it rejects a path leading through a symbolic " +
135
+ "link, through a non-directory, or out of the repository, which `git worktree " +
136
+ "add` would otherwise silently follow and thereby write outside the repository. " +
137
+ "Set `create` to `true` to also create the base directory. " +
138
+ "Fails with an error if the path is unsafe or the current directory is not a " +
139
+ "Git working tree; in that case you MUST NOT create the worktree at all.",
140
+ inputSchema: {
141
+ id: z.string().optional()
142
+ .describe("worktree identifier (allowed characters: A-Z, a-z, 0-9, '_', '-'); " +
143
+ "if omitted, the base directory holding all ASE worktrees is returned"),
144
+ create: z.boolean().optional()
145
+ .describe("if true, create the base directory if it does not exist yet (default: false)")
146
+ }
147
+ }, async (args) => {
148
+ try {
149
+ const create = args.create ?? false;
150
+ const text = args.id !== undefined ?
151
+ Worktree.dir(args.id, create) :
152
+ Worktree.baseDir(create);
153
+ return {
154
+ content: [{ type: "text", text }]
155
+ };
156
+ }
157
+ catch (err) {
158
+ return mcpToolError(err);
159
+ }
160
+ });
161
+ }
162
+ }
package/dst/ase.js CHANGED
@@ -17,6 +17,7 @@ import ArtifactCommand from "./ase-artifact.js";
17
17
  import MetaCommand from "./ase-meta.js";
18
18
  import CompatCommand from "./ase-compat.js";
19
19
  import DiagramCommand from "./ase-diagram.js";
20
+ import WorktreeCommand from "./ase-worktree.js";
20
21
  import pkg from "../package.json" with { type: "json" };
21
22
  /* globally initialize logger */
22
23
  const log = new Log("ase", "info", "-");
@@ -56,6 +57,7 @@ const main = async () => {
56
57
  new MetaCommand(log).register(program);
57
58
  new CompatCommand().register(program);
58
59
  new DiagramCommand(log).register(program);
60
+ new WorktreeCommand().register(program);
59
61
  /* parse program arguments */
60
62
  await program.parseAsync(process.argv);
61
63
  /* 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.57",
9
+ "version": "0.9.59",
10
10
  "license": "Apache-2.0",
11
11
  "author": {
12
12
  "name": "Dr. Ralf S. Engelschall",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ase",
3
- "version": "0.9.57",
3
+ "version": "0.9.59",
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.9.57",
3
+ "version": "0.9.59",
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.9.57",
3
+ "version": "0.9.59",
4
4
  "description": "Agentic Software Engineering (ASE)",
5
5
  "keywords": [ "agentic", "software", "engineering" ],
6
6
  "homepage": "https://ase.tools",
@@ -334,6 +334,12 @@ Workflow
334
334
  pure-insertion hunk at its new location - *never* by
335
335
  deleting and re-adding the unchanged lines in between.
336
336
 
337
+ This minimality rule applies *only* to <old-text/> and
338
+ <new-text/>. It *MUST* *NOT* be understood as a reason to
339
+ also drop the surrounding context of the two following
340
+ substeps - that context is *mandatory* and is reported
341
+ *separately* from the changed lines.
342
+
337
343
  3. Set <context-before/> to exactly *up to two* lines of
338
344
  *unchanged* code context which occurs in <file/>
339
345
  directly *before* <old-text/>, i.e., the lines (<line/>
@@ -365,6 +371,15 @@ Workflow
365
371
  }
366
372
  </template>
367
373
 
374
+ Here `line` is a *number* and all four other fields are
375
+ *JSON strings* carrying the *verbatim* source lines
376
+ (embedded newlines escaped as `\n`, no line-number
377
+ prefixes, original indentation preserved). The empty
378
+ string `""` is allowed for `context_before` and
379
+ `context_after` *only* at the very start or end of the
380
+ file - for every other hunk both *MUST* carry their
381
+ context lines.
382
+
368
383
  3. If <change-set/> is not empty, set
369
384
  <change-set><change-set/>,</change-set> (append a comma).
370
385
  Then append the following <template/> to <change-set/>:
@@ -50,6 +50,12 @@ Workflow
50
50
  as *two separate* problems instead of one large change
51
51
  which re-states the unchanged lines.
52
52
 
53
+ This minimality rule applies *only* to <old-text/> and
54
+ <new-text/>. It *MUST* *NOT* be understood as a reason to
55
+ also drop the surrounding context of the substeps 5 and 6
56
+ below - that context is *mandatory* and is reported
57
+ *separately* from the changed lines.
58
+
53
59
  4. Set <description/> to an ultra-brief and concise
54
60
  Markdown-formatted description of the problem with
55
61
  a hint of what is wrong and why it is wrong. In
@@ -94,6 +100,15 @@ Workflow
94
100
  }
95
101
  </template>
96
102
 
103
+ Here `line` is a *number* and all other fields are *JSON
104
+ strings*, where <context-before/>, <old-text/>, <new-text/>,
105
+ and <context-after/> carry the *verbatim* document lines
106
+ (embedded newlines escaped as `\n`, no line-number prefixes,
107
+ original indentation preserved). The empty string `""` is
108
+ allowed for `context_before` and `context_after` *only* at
109
+ the very start or end of the document - for every other
110
+ problem both *MUST* carry their context lines.
111
+
97
112
  3. You *MUST* *NOT* propose, apply, or render any document
98
113
  changes yourself. Instead, return *exclusively* as the last message
99
114
  a single JSON block (no markdown, no prose, no preamble, no summary)
@@ -31,6 +31,10 @@ which boosts you to an expert-level Software Engineering AI agent.
31
31
  - Place a *blank line before any comment line*,
32
32
  but not when it is the first line of a block or an end-of-line comment.
33
33
  - Keep code and comment *formatting exactly as in the existing code*.
34
+ - Keep comments *brief*: target *1-2 lines*, only as an exception use up to *4 lines*,
35
+ and only for *very complex algorithms* go up to at most *8 lines*.
36
+ If an existing comment is already at its limit and still has to be expanded,
37
+ first *compact* its wording before adding new content.
34
38
  - Use *regular comments* `/* [...] */` instead of end-of-line comments `// [...]`.
35
39
  - Use *two leading/trailing spaces within comments* as in `/* [...] */`.
36
40
  - Always use *parentheses around arrow function parameters*, even for a single parameter.
@@ -95,6 +95,45 @@ Control Flow Constructs
95
95
  is finished and no further repetitions are performed. This construct
96
96
  is expanded into nothing. Do not output anything.
97
97
 
98
+ - *IMPORTANT*: You *MUST* honor the following control flow construct:
99
+ <sleep duration="<sleep-duration/>"/>:
100
+
101
+ This specifies a single *wait* of <sleep-duration/> seconds
102
+ (fractional values like `1.5` are allowed).
103
+
104
+ The wait is performed with the `ase_sleep(duration:
105
+ <chunk-duration/>)` tool of the `ase` MCP server, which returns once
106
+ <chunk-duration/> has elapsed. To stay safely inside the tool's own
107
+ duration limit, <chunk-duration/> is *never* above `60`: a
108
+ <sleep-duration/> above `60` is therefore *split* into consecutive
109
+ calls -- as many `60` second calls as fit, followed by one final
110
+ call carrying the remaining seconds -- so the calls sum up to
111
+ exactly <sleep-duration/>.
112
+
113
+ This construct is expanded into nothing. Do not output anything.
114
+
115
+ - *IMPORTANT*: You *MUST* honor the following control flow construct:
116
+ <await condition="<await-condition/>" [interval="<await-interval/>"]><await-body/></await>:
117
+
118
+ This specifies an <await-body/> whose execution *waits* until
119
+ <await-condition/> is met: if <await-condition/> is met, the
120
+ <await-body/> is executed once and the construct is finished. If
121
+ <await-condition/> is *not* met, wait for <await-interval/> seconds
122
+ (default: `60`) and then *start over* by re-evaluating
123
+ <await-condition/>.
124
+
125
+ A single wait is performed with the `ase_sleep(duration:
126
+ <chunk-duration/>)` tool of the `ase` MCP server. To stay safely
127
+ inside the tool's own duration limit, <chunk-duration/> is *never*
128
+ above `60`: an <await-interval/> above `60` is therefore *split* into
129
+ consecutive calls -- as many `60` second calls as fit, followed by
130
+ one final call carrying the remaining seconds -- so the calls sum up
131
+ to exactly <await-interval/>. You *MUST* *NOT* re-evaluate
132
+ <await-condition/> between the calls of one such wait.
133
+
134
+ This construct is expanded to its <await-body/>. Do not output
135
+ anything else.
136
+
98
137
  - *IMPORTANT*: You *MUST* honor the following control flow construct:
99
138
  <agent <attr/>="<value/>" [...]><agent-body/></agent>:
100
139
 
@@ -292,8 +292,10 @@ Artifact Boxing Transparency
292
292
  - Internals exposed: *none* (no diffs, no code, no per-artifact explanation)
293
293
 
294
294
  - *IMPORTANT*: Precedence rule: a skill's *explicit* `black` branches
295
+ and a skill's *explicit* output suppression instructions
295
296
  deterministically skip or suppress and *win* over the implicit decisions
296
- above. Where no such branches exist, the decisions above apply.
297
+ above. Where no such branches or instructions exist, the decisions
298
+ above apply.
297
299
 
298
300
  Guidance Hint Level
299
301
  -------------------