@rse/ase 0.9.53 → 0.9.55
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-log.js +5 -0
- package/dst/ase-service.js +45 -6
- package/dst/ase-task.js +117 -18
- package/package.json +2 -2
- 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-common-code.md +34 -0
- package/plugin/meta/ase-common-task.md +3 -2
- package/plugin/meta/ase-constitution.md +6 -1
- package/plugin/meta/ase-format-task.md +96 -11
- package/plugin/package.json +1 -1
- package/plugin/skills/ase-code-craft/SKILL.md +8 -7
- package/plugin/skills/ase-code-lint/help.md +2 -2
- package/plugin/skills/ase-code-refactor/SKILL.md +8 -7
- package/plugin/skills/ase-code-resolve/SKILL.md +8 -7
- package/plugin/skills/ase-meta-config/help.md +3 -3
- package/plugin/skills/ase-meta-quotes/help.md +4 -3
- package/plugin/skills/ase-task-condense/SKILL.md +6 -5
- package/plugin/skills/ase-task-condense/help.md +1 -1
- package/plugin/skills/ase-task-edit/SKILL.md +26 -5
- package/plugin/skills/ase-task-grill/SKILL.md +8 -4
- package/plugin/skills/ase-task-grill/help.md +2 -1
- package/plugin/skills/ase-task-implement/SKILL.md +150 -5
- package/plugin/skills/ase-task-implement/help.md +40 -1
- package/plugin/skills/ase-task-list/SKILL.md +51 -15
- package/plugin/skills/ase-task-list/help.md +49 -6
- package/plugin/skills/ase-task-preflight/SKILL.md +15 -3
- package/plugin/skills/ase-task-preflight/help.md +9 -1
- package/plugin/skills/ase-task-reboot/SKILL.md +1 -1
- package/plugin/skills/ase-task-view/SKILL.md +23 -2
package/dst/ase-log.js
CHANGED
|
@@ -12,6 +12,8 @@ const levels = [
|
|
|
12
12
|
{ name: "info", style: chalk.blue },
|
|
13
13
|
{ name: "debug", style: chalk.green }
|
|
14
14
|
];
|
|
15
|
+
/* check whether an arbitrary string is a valid log level */
|
|
16
|
+
export const isLogLevel = (level) => levels.some((l) => l.name === level);
|
|
15
17
|
export default class Log {
|
|
16
18
|
_program;
|
|
17
19
|
_logLevel;
|
|
@@ -36,11 +38,14 @@ export default class Log {
|
|
|
36
38
|
return stream;
|
|
37
39
|
}
|
|
38
40
|
logLevel(level) {
|
|
41
|
+
if (level === undefined)
|
|
42
|
+
return this._logLevel;
|
|
39
43
|
const idx = levels.findIndex((l) => l.name === level);
|
|
40
44
|
if (idx === -1)
|
|
41
45
|
throw new RangeError(`invalid log level "${level}" (expected one of: ${levels.map((l) => l.name).join(", ")})`);
|
|
42
46
|
this._logLevel = level;
|
|
43
47
|
this.logLevelIdx = idx;
|
|
48
|
+
return this._logLevel;
|
|
44
49
|
}
|
|
45
50
|
logFile(file) {
|
|
46
51
|
if (file === this._logFile)
|
package/dst/ase-service.js
CHANGED
|
@@ -16,6 +16,7 @@ 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
18
|
import { Config, configSchema, ConfigMCP } from "./ase-config.js";
|
|
19
|
+
import { isLogLevel } from "./ase-log.js";
|
|
19
20
|
import { CompatMCP } from "./ase-compat.js";
|
|
20
21
|
import { DiagramMCP } from "./ase-diagram.js";
|
|
21
22
|
import { TaskMCP } from "./ase-task.js";
|
|
@@ -61,11 +62,18 @@ export const probe = async (port, projectId) => {
|
|
|
61
62
|
};
|
|
62
63
|
const SERVE_ENV = "ASE_SERVICE_SERVE";
|
|
63
64
|
const PORT_ENV = "ASE_SERVICE_PORT";
|
|
65
|
+
const LEVEL_ENV = "ASE_SERVICE_LOG_LEVEL";
|
|
64
66
|
const IDLE_MS = 30 * 60 * 1000;
|
|
65
67
|
const TICK_MS = 60 * 1000;
|
|
66
68
|
const PORT_MIN = 42000;
|
|
67
69
|
const PORT_MAX = 44000;
|
|
68
70
|
const PORT_TRIES = 20;
|
|
71
|
+
/* bounds for the append-only ".ase/service.log" file:
|
|
72
|
+
maximum tolerated size, number of tail lines surviving a trim,
|
|
73
|
+
and maximum length of the MCP tool call arguments logged per request */
|
|
74
|
+
const LOG_MAX_SIZE = 1024 * 1024;
|
|
75
|
+
const LOG_KEEP_LINES = 2000;
|
|
76
|
+
const LOG_ARGS_MAX = 200;
|
|
69
77
|
/* load the optional "config.yaml" and "service.yaml" files and derive
|
|
70
78
|
the service identity context (project id, port, service config) */
|
|
71
79
|
export const loadServiceContext = (log) => {
|
|
@@ -130,15 +138,32 @@ export class Service {
|
|
|
130
138
|
svc.write();
|
|
131
139
|
});
|
|
132
140
|
}
|
|
141
|
+
/* trim the log file down to its tail if it grew beyond the size limit */
|
|
142
|
+
static trimLog(logFile) {
|
|
143
|
+
try {
|
|
144
|
+
if (!fs.existsSync(logFile) || fs.statSync(logFile).size <= LOG_MAX_SIZE)
|
|
145
|
+
return;
|
|
146
|
+
const tail = Service.readLogTail(logFile, LOG_KEEP_LINES);
|
|
147
|
+
fs.writeFileSync(logFile, tail.length > 0 ? `${tail}\n` : "");
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
/* intentionally ignore all trimming errors, as a
|
|
151
|
+
non-trimmable log file must never block the service */
|
|
152
|
+
}
|
|
153
|
+
}
|
|
133
154
|
/* spawn the current executable detached as a background service */
|
|
134
|
-
static spawnDetached(aseDir, port) {
|
|
155
|
+
static spawnDetached(aseDir, port, logLevel) {
|
|
135
156
|
fs.mkdirSync(aseDir, { recursive: true });
|
|
136
157
|
const logFile = path.join(aseDir, "service.log");
|
|
158
|
+
/* trim the log before handing it to the service, as the detached
|
|
159
|
+
service inherits the file descriptor for its entire lifetime and
|
|
160
|
+
hence cannot rotate the file itself while it is running */
|
|
161
|
+
Service.trimLog(logFile);
|
|
137
162
|
const fd = fs.openSync(logFile, "a");
|
|
138
163
|
const entry = fileURLToPath(new URL("./ase.js", import.meta.url));
|
|
139
164
|
const child = spawn(process.execPath, [entry, "service", "start"], {
|
|
140
165
|
detached: true,
|
|
141
|
-
env: { ...process.env, [SERVE_ENV]: "1", [PORT_ENV]: String(port) },
|
|
166
|
+
env: { ...process.env, [SERVE_ENV]: "1", [PORT_ENV]: String(port), [LEVEL_ENV]: logLevel },
|
|
142
167
|
stdio: ["ignore", fd, fd]
|
|
143
168
|
});
|
|
144
169
|
fs.closeSync(fd);
|
|
@@ -285,11 +310,20 @@ export default class ServiceCommand {
|
|
|
285
310
|
bodyInfo = ` [${bMethod}]`;
|
|
286
311
|
if (bName !== null) {
|
|
287
312
|
bodyInfo += ` ${bName}`;
|
|
288
|
-
if (bArgs !== null)
|
|
289
|
-
|
|
313
|
+
if (bArgs !== null) {
|
|
314
|
+
/* cap the arguments, as payload-carrying tool calls
|
|
315
|
+
(task plans, key/value batches, etc) would
|
|
316
|
+
otherwise dominate the entire log file */
|
|
317
|
+
const args = JSON.stringify(bArgs);
|
|
318
|
+
bodyInfo += ` ${args.length > LOG_ARGS_MAX ? `${args.slice(0, LOG_ARGS_MAX)}…` : args}`;
|
|
319
|
+
}
|
|
290
320
|
}
|
|
291
321
|
}
|
|
292
|
-
|
|
322
|
+
/* log tool calls regularly, but all remaining MCP traffic
|
|
323
|
+
(session handshakes, notifications, SSE stream opens) at
|
|
324
|
+
debug level only, as it carries no diagnostic value */
|
|
325
|
+
const level = bMethod === "tools/call" ? "info" : "debug";
|
|
326
|
+
this.log.write(level, `mcp: ${request.method.toUpperCase()} ${request.path}${bodyInfo}`);
|
|
293
327
|
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
294
328
|
const mcp = buildMcpServer();
|
|
295
329
|
request.raw.res.on("close", () => {
|
|
@@ -394,6 +428,11 @@ export default class ServiceCommand {
|
|
|
394
428
|
const ctx = this.loadContext();
|
|
395
429
|
let port = ctx.port;
|
|
396
430
|
if (process.env[SERVE_ENV] === "1") {
|
|
431
|
+
/* adopt the log level of the spawning process, as the
|
|
432
|
+
detached service is started without any CLI options */
|
|
433
|
+
const level = process.env[LEVEL_ENV];
|
|
434
|
+
if (level !== undefined && isLogLevel(level))
|
|
435
|
+
this.log.logLevel(level);
|
|
397
436
|
const raw = process.env[PORT_ENV];
|
|
398
437
|
port = raw !== undefined ? Number(raw) : await Service.allocatePort();
|
|
399
438
|
await this.runService({ ...ctx, port });
|
|
@@ -411,7 +450,7 @@ export default class ServiceCommand {
|
|
|
411
450
|
let lastErr = new Error("service failed to start within timeout");
|
|
412
451
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
413
452
|
port = await Service.allocatePort();
|
|
414
|
-
const { child, logFile } = Service.spawnDetached(ctx.aseDir, port);
|
|
453
|
+
const { child, logFile } = Service.spawnDetached(ctx.aseDir, port, this.log.logLevel());
|
|
415
454
|
let exited = false;
|
|
416
455
|
let exitCode = null;
|
|
417
456
|
let resolveExit = () => { };
|
package/dst/ase-task.js
CHANGED
|
@@ -14,6 +14,12 @@ import { LRUCache } from "lru-cache";
|
|
|
14
14
|
import { Config, configSchema, parseScope } from "./ase-config.js";
|
|
15
15
|
import { Markdown } from "./ase-markdown.js";
|
|
16
16
|
import { readStdin, writeStdout } from "./ase-stdio.js";
|
|
17
|
+
/* the lifecycle states a task plan can be in, i.e., the accepted
|
|
18
|
+
values of the "Status:" frontmatter key of a task plan */
|
|
19
|
+
export const taskStates = [
|
|
20
|
+
"DRAFTED", "REJECTED", "APPROVED", "DEFERRED",
|
|
21
|
+
"STARTED", "BLOCKED", "COMPLETED", "CANCELLED"
|
|
22
|
+
];
|
|
17
23
|
/* reusable functionality: persisted task plans under
|
|
18
24
|
<project>/<basedir>/TASK-<id>.md (driven by the
|
|
19
25
|
"project.artifact.task.{basedir,files}" configuration) */
|
|
@@ -141,12 +147,54 @@ export class Task {
|
|
|
141
147
|
migrated.sort((a, b) => a.localeCompare(b));
|
|
142
148
|
return migrated;
|
|
143
149
|
}
|
|
144
|
-
/*
|
|
150
|
+
/* the legacy task plan header lines, each mapped onto the
|
|
151
|
+
frontmatter key which superseded it */
|
|
152
|
+
static legacy = [
|
|
153
|
+
{ key: "Created", re: /^⎈[ \t]+Created:[ \t]*(.*)$/m },
|
|
154
|
+
{ key: "Modified", re: /^⚙[ \t]+Modified:[ \t]*(.*)$/m },
|
|
155
|
+
{ key: "Kind", re: /^☯[ \t]+Kind:[ \t]*(.*)$/m }
|
|
156
|
+
];
|
|
157
|
+
/* render a single frontmatter line with a column-aligned key */
|
|
158
|
+
static frontLine(key, value) {
|
|
159
|
+
return (key + ":").padEnd(12) + value;
|
|
160
|
+
}
|
|
161
|
+
/* normalize a legacy task plan -- one carrying its metadata in the
|
|
162
|
+
"# TASK <id>: <title>" heading and the "⎈"/"⚙"/"☯" glyph header
|
|
163
|
+
lines -- into the current Markdown frontmatter shape, so every
|
|
164
|
+
consumer sees a single plan shape only; a plan already carrying a
|
|
165
|
+
frontmatter block, and any content without a task heading at all,
|
|
166
|
+
is passed through verbatim -- in particular, absent optional keys
|
|
167
|
+
are never materialized, as they read as their default value */
|
|
168
|
+
static normalize(id, text) {
|
|
169
|
+
if (text === "" || /^---\r?\n/.test(text))
|
|
170
|
+
return text;
|
|
171
|
+
const heading = /^#[ \t]+TASK(?:[ \t]+[A-Za-z0-9_-]+)?[ \t]*:[ \t]*(.*)$/m.exec(text);
|
|
172
|
+
if (heading === null)
|
|
173
|
+
return text;
|
|
174
|
+
/* lift the glyph header lines into their frontmatter keys, with
|
|
175
|
+
the task id taken from the authoritative filename-derived id */
|
|
176
|
+
let body = text.replace(heading[0], "");
|
|
177
|
+
const front = [Task.frontLine("Id", id)];
|
|
178
|
+
for (const legacy of Task.legacy) {
|
|
179
|
+
const m = legacy.re.exec(body);
|
|
180
|
+
if (m === null)
|
|
181
|
+
continue;
|
|
182
|
+
front.push(Task.frontLine(legacy.key, m[1].trim()));
|
|
183
|
+
body = body.replace(m[0], "");
|
|
184
|
+
}
|
|
185
|
+
/* re-assemble the plan from the frontmatter block, the reduced
|
|
186
|
+
heading, and the body stripped of its now leading blank lines */
|
|
187
|
+
return `---\n${front.join("\n")}\n---\n\n` +
|
|
188
|
+
`# TASK: ${heading[1].trim()}\n\n` +
|
|
189
|
+
body.replace(/^(?:[ \t]*\r?\n)+/, "");
|
|
190
|
+
}
|
|
191
|
+
/* load a task, normalized into the current Markdown frontmatter
|
|
192
|
+
shape; returns empty string if no task exists */
|
|
145
193
|
static load(log, id) {
|
|
146
194
|
const file = Task.path(log, id);
|
|
147
195
|
if (!fs.existsSync(file))
|
|
148
196
|
return "";
|
|
149
|
-
return fs.readFileSync(file, "utf8");
|
|
197
|
+
return Task.normalize(id, fs.readFileSync(file, "utf8"));
|
|
150
198
|
}
|
|
151
199
|
/* save a task as UTF-8 text under the given id into the
|
|
152
200
|
<project>/<basedir>/TASK-<id>.md file */
|
|
@@ -167,10 +215,11 @@ export class Task {
|
|
|
167
215
|
return true;
|
|
168
216
|
}
|
|
169
217
|
/* rename a task by moving its <project>/<basedir>/TASK-<oldId>.md file
|
|
170
|
-
to <project>/<basedir>/TASK-<newId>.md; the embedded
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
not
|
|
218
|
+
to <project>/<basedir>/TASK-<newId>.md; the embedded "Id:"
|
|
219
|
+
frontmatter key inside the plan content is rewritten to the new id
|
|
220
|
+
(falling back to the "# TASK <id>:" heading of a still legacy,
|
|
221
|
+
not yet normalized plan); returns true on success, false if the
|
|
222
|
+
source task does not exist; throws if the target id already exists */
|
|
174
223
|
static rename(log, oldId, newId) {
|
|
175
224
|
const oldFile = Task.path(log, oldId);
|
|
176
225
|
const newFile = Task.path(log, newId);
|
|
@@ -179,7 +228,9 @@ export class Task {
|
|
|
179
228
|
if (fs.existsSync(newFile))
|
|
180
229
|
throw new Error(`task: target id "${newId}" already exists`);
|
|
181
230
|
const text = fs.readFileSync(oldFile, "utf8");
|
|
182
|
-
const updated = text
|
|
231
|
+
const updated = /^---\r?\n/.test(text) ?
|
|
232
|
+
text.replace(/^(Id:[ \t]*)[A-Za-z0-9_-]+[ \t]*$/m, `$1${newId}`) :
|
|
233
|
+
text.replace(/(^#\s+TASK\s+)[A-Za-z0-9_-]+(\s*:)/m, `$1${newId}$2`);
|
|
183
234
|
fs.mkdirSync(path.dirname(newFile), { recursive: true });
|
|
184
235
|
fs.writeFileSync(newFile, updated, "utf8");
|
|
185
236
|
fs.rmSync(oldFile, { force: true });
|
|
@@ -207,17 +258,55 @@ export class Task {
|
|
|
207
258
|
}
|
|
208
259
|
return out;
|
|
209
260
|
}
|
|
210
|
-
/*
|
|
211
|
-
|
|
212
|
-
|
|
261
|
+
/* read the "Status:" frontmatter key of a task plan file, falling back
|
|
262
|
+
to the "DRAFTED" default of the task plan format for a plan whose
|
|
263
|
+
frontmatter is absent or carries no such key */
|
|
264
|
+
static status(file) {
|
|
265
|
+
const fm = /^---\r?\n([\s\S]*?\r?\n)---\r?\n/.exec(fs.readFileSync(file, "utf8"));
|
|
266
|
+
if (fm === null)
|
|
267
|
+
return "DRAFTED";
|
|
268
|
+
const m = /^Status:[ \t]*(\S+)[ \t]*$/m.exec(fm[1]);
|
|
269
|
+
if (m === null)
|
|
270
|
+
return "DRAFTED";
|
|
271
|
+
return m[1];
|
|
272
|
+
}
|
|
273
|
+
/* list all persisted tasks in lexicographic id order, each with the
|
|
274
|
+
`status` of its plan; if verbose is true, each entry's `mtime` is
|
|
275
|
+
set to the task file's modification time formatted as
|
|
276
|
+
"YYYY-MM-DD HH:MM", otherwise it is left undefined */
|
|
213
277
|
static list(log, verbose = false) {
|
|
214
278
|
const out = Task.scan(log).map((entry) => ({
|
|
215
279
|
id: entry.id,
|
|
280
|
+
status: Task.status(entry.file),
|
|
216
281
|
mtime: verbose ? DateTime.fromJSDate(entry.st.mtime).toFormat("yyyy-LL-dd HH:mm") : undefined
|
|
217
282
|
}));
|
|
218
283
|
out.sort((a, b) => a.id.localeCompare(b.id));
|
|
219
284
|
return out;
|
|
220
285
|
}
|
|
286
|
+
/* resolve an "include" and an "exclude" comma-separated lifecycle
|
|
287
|
+
state list into the effective state set a task plan has to be in
|
|
288
|
+
to be listed at all; the "none" sentinel and empty tokens are
|
|
289
|
+
silently dropped, an empty "include" list means all states, and
|
|
290
|
+
the "exclude" list is applied after the "include" list */
|
|
291
|
+
static states(include, exclude) {
|
|
292
|
+
const parse = (list) => list.split(",")
|
|
293
|
+
.map((token) => token.trim())
|
|
294
|
+
.filter((token) => token !== "" && token.toUpperCase() !== "NONE")
|
|
295
|
+
.map((token) => {
|
|
296
|
+
const state = token.toUpperCase();
|
|
297
|
+
if (!taskStates.includes(state))
|
|
298
|
+
throw new Error(`task: invalid state "${token}" ` +
|
|
299
|
+
`(expected one of: ${taskStates.join(", ")})`);
|
|
300
|
+
return state;
|
|
301
|
+
});
|
|
302
|
+
const included = parse(include);
|
|
303
|
+
const excluded = parse(exclude);
|
|
304
|
+
const states = (included.length > 0 ? included : taskStates)
|
|
305
|
+
.filter((state) => !excluded.includes(state));
|
|
306
|
+
if (states.length === 0)
|
|
307
|
+
throw new Error("task: options \"--include\" and \"--exclude\" cancel out to an empty state set");
|
|
308
|
+
return states;
|
|
309
|
+
}
|
|
221
310
|
/* purge tasks whose modification time is older than the given cutoff in
|
|
222
311
|
milliseconds; returns the list of removed task ids */
|
|
223
312
|
static purge(log, maxAgeMs) {
|
|
@@ -274,12 +363,19 @@ export default class TaskCommand {
|
|
|
274
363
|
task
|
|
275
364
|
.command("list")
|
|
276
365
|
.description("List all persisted task ids, one per line")
|
|
277
|
-
.option("-v, --verbose", "also show the task
|
|
366
|
+
.option("-v, --verbose", "also show the task plan status and the task file " +
|
|
367
|
+
"modification time as (YYYY-MM-DD HH:MM)")
|
|
368
|
+
.option("-i, --include <states>", "comma-separated list of lifecycle states to list " +
|
|
369
|
+
`(${taskStates.join("|")}), or "none" for no restriction`, "none")
|
|
370
|
+
.option("-e, --exclude <states>", "comma-separated list of lifecycle states to not list " +
|
|
371
|
+
`(${taskStates.join("|")}), or "none" for no exclusion`, "COMPLETED,CANCELLED")
|
|
278
372
|
.action(async (opts) => {
|
|
279
|
-
const
|
|
373
|
+
const states = Task.states(opts.include, opts.exclude);
|
|
374
|
+
const items = Task.list(this.log, opts.verbose ?? false)
|
|
375
|
+
.filter((item) => states.includes(item.status));
|
|
280
376
|
for (const item of items) {
|
|
281
377
|
if (opts.verbose)
|
|
282
|
-
await writeStdout(`${item.id}\t(${item.mtime})\n`);
|
|
378
|
+
await writeStdout(`${item.id}\t${item.status}\t(${item.mtime})\n`);
|
|
283
379
|
else
|
|
284
380
|
await writeStdout(`${item.id}\n`);
|
|
285
381
|
}
|
|
@@ -395,7 +491,8 @@ export class TaskMCP {
|
|
|
395
491
|
title: "ASE task list",
|
|
396
492
|
description: "List all persisted tasks. " +
|
|
397
493
|
"Returns a `tasks` array (in lexicographic `id` order) where each item has the " +
|
|
398
|
-
"task `id
|
|
494
|
+
"task `id` and the `status` of its plan (the `Status:` frontmatter key, defaulting " +
|
|
495
|
+
"to `DRAFTED`). If `verbose` is `true`, each item additionally has an `mtime` field " +
|
|
399
496
|
"(last modification time of the task's `TASK-<id>.md` file, formatted as `YYYY-MM-DD HH:MM`). " +
|
|
400
497
|
"Returns an empty array if no tasks exist.",
|
|
401
498
|
inputSchema: {
|
|
@@ -405,6 +502,7 @@ export class TaskMCP {
|
|
|
405
502
|
outputSchema: {
|
|
406
503
|
tasks: z.array(z.object({
|
|
407
504
|
id: z.string().describe("task identifier"),
|
|
505
|
+
status: z.string().describe("task plan lifecycle status (`Status:` frontmatter key, default `DRAFTED`)"),
|
|
408
506
|
mtime: z.string().optional()
|
|
409
507
|
.describe("`TASK-<id>.md` modification time (`YYYY-MM-DD HH:MM`); only present if `verbose` is true")
|
|
410
508
|
})).describe("all persisted tasks in lexicographic id order")
|
|
@@ -414,8 +512,8 @@ export class TaskMCP {
|
|
|
414
512
|
const verbose = args.verbose ?? false;
|
|
415
513
|
const items = Task.list(this.log, verbose);
|
|
416
514
|
const tasks = verbose ?
|
|
417
|
-
items.map((item) => ({ id: item.id, mtime: item.mtime ?? "" })) :
|
|
418
|
-
items.map((item) => ({ id: item.id }));
|
|
515
|
+
items.map((item) => ({ id: item.id, status: item.status, mtime: item.mtime ?? "" })) :
|
|
516
|
+
items.map((item) => ({ id: item.id, status: item.status }));
|
|
419
517
|
const result = { tasks };
|
|
420
518
|
return {
|
|
421
519
|
structuredContent: result,
|
|
@@ -430,7 +528,8 @@ export class TaskMCP {
|
|
|
430
528
|
mcp.registerTool("ase_task_load", {
|
|
431
529
|
title: "ASE task load",
|
|
432
530
|
description: "Load a previously persisted task by `id`. " +
|
|
433
|
-
"Returns the task as `text
|
|
531
|
+
"Returns the task as `text`, normalized into the current Markdown frontmatter shape; " +
|
|
532
|
+
"returns an empty string if no task exists for the `id`.",
|
|
434
533
|
inputSchema: {
|
|
435
534
|
id: z.string()
|
|
436
535
|
.describe("task identifier (allowed characters: A-Z, a-z, 0-9, '_', '-')")
|
|
@@ -501,7 +600,7 @@ export class TaskMCP {
|
|
|
501
600
|
mcp.registerTool("ase_task_rename", {
|
|
502
601
|
title: "ASE task rename",
|
|
503
602
|
description: "Rename a previously persisted task from `old` to `new` by moving the " +
|
|
504
|
-
"task `TASK-<id>.md` file and rewriting its embedded
|
|
603
|
+
"task `TASK-<id>.md` file and rewriting its embedded `Id:` frontmatter key. " +
|
|
505
604
|
"Returns a status `text` indicating whether the rename succeeded. " +
|
|
506
605
|
"Fails with an error if the target id already exists.",
|
|
507
606
|
inputSchema: {
|
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.55",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"author": {
|
|
12
12
|
"name": "Dr. Ralf S. Engelschall",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"shx": "0.4.0",
|
|
32
32
|
|
|
33
33
|
"@types/node": "26.1.2",
|
|
34
|
-
"@types/luxon": "3.7.
|
|
34
|
+
"@types/luxon": "3.7.3",
|
|
35
35
|
"@types/which": "3.0.4",
|
|
36
36
|
"@types/update-notifier": "6.0.8",
|
|
37
37
|
"@types/shell-quote": "1.7.5",
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
|
|
2
|
+
Code Skill Common Steps
|
|
3
|
+
=======================
|
|
4
|
+
|
|
5
|
+
<define name="code-tenets">
|
|
6
|
+
|
|
7
|
+
You *MUST* internalize and strictly honor the **GENERIC TENETS**, and
|
|
8
|
+
the **<arg1/> TENETS** of the **ASE Tenets** in the following creation
|
|
9
|
+
and updating of code. Do not output anything.
|
|
10
|
+
|
|
11
|
+
</define>
|
|
12
|
+
|
|
13
|
+
<define name="code-tenets-from-plan">
|
|
14
|
+
|
|
15
|
+
Determine the *kind of change* the task plan describes and internalize
|
|
16
|
+
the corresponding tenet sets:
|
|
17
|
+
|
|
18
|
+
- If the frontmatter of <task-content/> carries a `Kind: <text/>` key
|
|
19
|
+
and <text/> is one of `CRAFTING`, `REFACTORING`, or `RESOLVING`:
|
|
20
|
+
Set <task-kind><text/></task-kind> (set task kind to the stated kind).
|
|
21
|
+
|
|
22
|
+
- Else:
|
|
23
|
+
The plan states no kind at all, or an unrecognized one, so *infer*
|
|
24
|
+
the kind from the plan content itself: `RESOLVING` if the plan
|
|
25
|
+
predominantly fixes a defect, `REFACTORING` if it predominantly
|
|
26
|
+
re-structures existing artifacts without changing their observable
|
|
27
|
+
behavior, and `CRAFTING` otherwise. Set <task-kind/> to the inferred
|
|
28
|
+
kind, defaulting to `CRAFTING` if the inference stays inconclusive.
|
|
29
|
+
|
|
30
|
+
Then honor the tenet sets of <task-kind/>:
|
|
31
|
+
|
|
32
|
+
<expand name="code-tenets" arg1="<task-kind/>"></expand>
|
|
33
|
+
|
|
34
|
+
</define>
|
|
@@ -103,11 +103,12 @@ format, which has to be determined by calling the
|
|
|
103
103
|
`ase_timestamp(format: "yyyy-LL-dd HH:mm")` tool of the `ase`
|
|
104
104
|
MCP server and use the `text` field of its response. If
|
|
105
105
|
<timestamp-created/> is still unset (because the plan content
|
|
106
|
-
had no `Created:`
|
|
106
|
+
had no `Created:` frontmatter key), set
|
|
107
107
|
<timestamp-created><timestamp-modified/></timestamp-created>
|
|
108
108
|
(fall back to the modified timestamp). Re-insert the current
|
|
109
109
|
<ase-task-id/>, the original <timestamp-created/>, and the
|
|
110
|
-
refreshed <timestamp-modified/> into
|
|
110
|
+
refreshed <timestamp-modified/> into the frontmatter keys `Id:`,
|
|
111
|
+
`Created:`, and `Modified:` of <task-content/> and calculate
|
|
111
112
|
the number of words <words/> of <task-content/>.
|
|
112
113
|
|
|
113
114
|
Call the `ase_task_save(id: "<ase-task-id/>", text:
|
|
@@ -14,12 +14,16 @@ which boosts you to an expert-level Software Engineering AI agent.
|
|
|
14
14
|
- Do *not* insist on early "return" in "if" blocks, if an "else" block exists.
|
|
15
15
|
- Do *not* remove any whitespace in the code formatting -- keep whitespace aligned with code base.
|
|
16
16
|
- Do *not* produce any trailing white-spaces on any lines.
|
|
17
|
+
- Do *not* guess missing tool call parameters or fill them with invented placeholder values.
|
|
17
18
|
|
|
18
19
|
## Commandments
|
|
19
20
|
|
|
20
21
|
- Be *honest* and *transparent* in all your responses.
|
|
21
22
|
- *Ground* factual and technical claims in verifiable evidence (code base, local files, or web)
|
|
22
23
|
with a reference, rather than unverified model knowledge; state explicitly when a claim cannot be verified.
|
|
24
|
+
- Assume your *internal knowledge of dependencies* (libraries, frameworks, tools, and their implementations)
|
|
25
|
+
is *outdated*; always verify the current API, version, and usage pattern against the local sources
|
|
26
|
+
or the web before writing any code against them.
|
|
23
27
|
- Before proposing any code changes, explain *WHAT* the proposed changes do and *WHY* it is necessary.
|
|
24
28
|
- Use *concise* and *type-safe code* only.
|
|
25
29
|
- Use *precise* and *surgical code changes* only.
|
|
@@ -37,7 +41,8 @@ which boosts you to an expert-level Software Engineering AI agent.
|
|
|
37
41
|
- Use *double-quotes* (`"[...]"`) instead of single-quotes (`'[...]'`) for all strings.
|
|
38
42
|
- Use K&R coding style with *opening braces* at the end of lines and *closing braces* at the beginning of lines.
|
|
39
43
|
- When a language has a *more strongly-typed variant*, prefer that variant.
|
|
40
|
-
- When generating temporary helper programs, prefer the *target project's primary
|
|
44
|
+
- When generating temporary helper programs or scratch test files, prefer the *target project's primary
|
|
45
|
+
programming language* and *clean them up* once they are no longer needed.
|
|
41
46
|
|
|
42
47
|
@./ase-persona.md
|
|
43
48
|
|
|
@@ -5,11 +5,16 @@ Task
|
|
|
5
5
|
Every *task* uses a strict and fixed format:
|
|
6
6
|
|
|
7
7
|
<format>
|
|
8
|
+
---
|
|
9
|
+
Id: <task-id/>
|
|
10
|
+
Created: <timestamp-created/>
|
|
11
|
+
Modified: <timestamp-modified/>
|
|
12
|
+
Status: <task-status/>
|
|
13
|
+
Properties: <task-properties/>
|
|
14
|
+
Kind: <task-kind/>
|
|
15
|
+
---
|
|
8
16
|
|
|
9
|
-
# TASK <
|
|
10
|
-
|
|
11
|
-
⎈ Created: <timestamp-created/>
|
|
12
|
-
⚙ Modified: <timestamp-modified/>
|
|
17
|
+
# TASK: <title/>
|
|
13
18
|
|
|
14
19
|
## CONTEXT
|
|
15
20
|
|
|
@@ -33,8 +38,19 @@ Every *task* uses a strict and fixed format:
|
|
|
33
38
|
|
|
34
39
|
You *MUST* honor the following hints on this *task* format:
|
|
35
40
|
|
|
36
|
-
-
|
|
37
|
-
|
|
41
|
+
- The content *MUST* begin with the `---` opening delimiter of the
|
|
42
|
+
*Markdown frontmatter* as its very *first* line -- there is *no*
|
|
43
|
+
leading empty line, as any line before the `---` would degrade the
|
|
44
|
+
frontmatter into ordinary Markdown. You *MUST* always keep the empty
|
|
45
|
+
line between the closing `---` delimiter and the `#` heading, and
|
|
46
|
+
always keep the last empty line. If one of them is missing, add it
|
|
47
|
+
back.
|
|
48
|
+
|
|
49
|
+
- The *frontmatter* carries the keys `Id`, `Created`, `Modified`,
|
|
50
|
+
`Status`, `Properties`, and `Kind`, in exactly this order, with their
|
|
51
|
+
values being *unquoted* plain scalars and vertically aligned one
|
|
52
|
+
space after the longest key. Only `Id` is *mandatory* -- every other
|
|
53
|
+
key is *optional* and, when absent, falls back to its default value.
|
|
38
54
|
|
|
39
55
|
- In all descriptions, highlight *code* as
|
|
40
56
|
<template>`<code/>`</template> and *key aspects* as
|
|
@@ -46,11 +62,80 @@ You *MUST* honor the following hints on this *task* format:
|
|
|
46
62
|
is changed, what benefit results or what the rationale is behind the
|
|
47
63
|
change.
|
|
48
64
|
|
|
49
|
-
- The <task-id/>
|
|
50
|
-
<ase-task-id/> in the current session
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
65
|
+
- The <task-id/> of the `Id:` frontmatter key has to be substituted
|
|
66
|
+
with the current value of <ase-task-id/> in the current session
|
|
67
|
+
context.
|
|
68
|
+
|
|
69
|
+
- The `Status:` frontmatter key states the current *lifecycle state*
|
|
70
|
+
of the task plan. The key is *optional* and defaults to `DRAFTED`:
|
|
71
|
+
an *absent* key reads as `DRAFTED`, and a *newly created* plan
|
|
72
|
+
carries `DRAFTED` explicitly. The <task-status/> value is *strictly*
|
|
73
|
+
one of the following eight states:
|
|
74
|
+
|
|
75
|
+
- `DRAFTED`: plan exists but is still provisional and not yet
|
|
76
|
+
cleared for implementation.
|
|
77
|
+
- `REJECTED`: plan was reviewed and refused, and has to be reworked
|
|
78
|
+
before it can be approved.
|
|
79
|
+
- `APPROVED`: plan is accepted as authoritative and cleared for
|
|
80
|
+
implementation, but no work has begun.
|
|
81
|
+
- `DEFERRED`: plan is approved and unobstructed, but its start was
|
|
82
|
+
deliberately postponed.
|
|
83
|
+
- `STARTED`: implementation of the plan is actively underway.
|
|
84
|
+
- `BLOCKED`: implementation is halted by an impediment which
|
|
85
|
+
someone has to remove before it can resume.
|
|
86
|
+
- `COMPLETED`: plan was implemented in full and reached its
|
|
87
|
+
intended outcome.
|
|
88
|
+
- `CANCELLED`: plan was terminated before completion, because it
|
|
89
|
+
failed, was called off, or became obsolete.
|
|
90
|
+
|
|
91
|
+
- The eight states form a *state machine*. Whoever sets the `Status:`
|
|
92
|
+
key *MUST* only move along one of the following transitions, whereby
|
|
93
|
+
a *single* operation *MAY* traverse *several* transitions at once if
|
|
94
|
+
it performs the corresponding stages in one go:
|
|
95
|
+
|
|
96
|
+
```text
|
|
97
|
+
DRAFTED ──reject───▶ REJECTED STARTED ──block────▶ BLOCKED
|
|
98
|
+
REJECTED ──redraft──▶ DRAFTED BLOCKED ──unblock──▶ STARTED
|
|
99
|
+
DRAFTED ──approve──▶ APPROVED STARTED ──complete─▶ COMPLETED
|
|
100
|
+
APPROVED ──defer────▶ DEFERRED
|
|
101
|
+
DEFERRED ──resume───▶ APPROVED any non-terminal state
|
|
102
|
+
APPROVED ──start────▶ STARTED ──cancel───▶ CANCELLED
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`COMPLETED` and `CANCELLED` are the two *terminal* states: a plan
|
|
106
|
+
which reached one of them is *finished* and leaves the state machine.
|
|
107
|
+
|
|
108
|
+
- The `Properties:` frontmatter key states which *stages* the task
|
|
109
|
+
plan already passed through. The <task-properties/> value is
|
|
110
|
+
*strictly* either `none` or a comma-separated list of the values
|
|
111
|
+
`grilled`, `preflighted`, `implemented`, and `verified`, listed in
|
|
112
|
+
exactly this order, each at most once. The key is *optional* and
|
|
113
|
+
defaults to `none`: an *absent* key reads as `none`, and a *newly
|
|
114
|
+
created* plan carries `none` explicitly. The list only ever
|
|
115
|
+
*accumulates*: a skill *adds* its own value if still absent and
|
|
116
|
+
*MUST NOT* drop any value already present, except when a plan is
|
|
117
|
+
*recreated from scratch*, which resets it to `none`.
|
|
118
|
+
|
|
119
|
+
- The `Kind:` frontmatter key states the *kind of change* the task plan
|
|
120
|
+
describes, and hence which *operation-specific tenet set* of the
|
|
121
|
+
**ASE Tenets** a subsequent preflight or implementation has to
|
|
122
|
+
honor. The <task-kind/> value is *strictly* one of `CRAFTING`,
|
|
123
|
+
`REFACTORING`, or `RESOLVING`.
|
|
124
|
+
|
|
125
|
+
- The `Kind:` frontmatter key is *optional*: a skill *authoring* or
|
|
126
|
+
*updating* a task plan *CAN* update an already present key or pass
|
|
127
|
+
it through *verbatim* and *MAY* create a missing one by *inferring*
|
|
128
|
+
the kind from the plan content (defaulting to `CRAFTING`). A
|
|
129
|
+
`--dry` run *never* drops this key, as `--dry` only omits the
|
|
130
|
+
`## VERIFICATION` section.
|
|
131
|
+
|
|
132
|
+
- A skill *writing* an optional key which is still *absent* inserts it
|
|
133
|
+
at its position in the key order above and re-aligns the values of
|
|
134
|
+
the whole frontmatter block.
|
|
135
|
+
|
|
136
|
+
- The <timestamp-created/> of the `Created:` frontmatter key is the
|
|
137
|
+
timestamp when this task plan was created. The <timestamp-modified/>
|
|
138
|
+
of the `Modified:` frontmatter key is the timestamp when this
|
|
54
139
|
task plan was last modified. Both use an ISO-style format
|
|
55
140
|
value. The value of both can be determined by a call to the
|
|
56
141
|
`ase_timestamp(format: "yyyy-LL-dd HH:mm")` tool of the `ase` MCP
|
package/plugin/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.55",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"author": {
|
|
12
12
|
"name": "Dr. Ralf S. Engelschall",
|