@rse/ase 0.9.54 → 0.9.56
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-setup.js +4 -2
- package/dst/ase-statusline.js +6 -8
- package/dst/ase-task.js +152 -28
- package/package.json +5 -5
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.github/plugin/plugin.json +1 -1
- package/plugin/agents/ase-code-analyze.md +1 -1
- package/plugin/agents/ase-code-lint.md +4 -4
- package/plugin/agents/ase-meta-chat.md +1 -1
- package/plugin/agents/ase-meta-proximity.md +1 -1
- package/plugin/meta/ase-common-code.md +165 -2
- package/plugin/meta/ase-common-task.md +17 -15
- package/plugin/meta/ase-constitution.md +3 -3
- package/plugin/meta/ase-control.md +7 -7
- package/plugin/meta/ase-dialog.md +5 -4
- package/plugin/meta/ase-format-arch.md +7 -8
- package/plugin/meta/ase-format-spec.md +10 -11
- package/plugin/meta/ase-format-task.md +90 -20
- package/plugin/meta/ase-getopt.md +1 -1
- package/plugin/meta/ase-persona.md +3 -3
- package/plugin/meta/ase-skill.md +14 -24
- package/plugin/meta/ase-tenets.md +5 -5
- package/plugin/package.json +2 -2
- package/plugin/skills/ase-arch-analyze/SKILL.md +1 -1
- package/plugin/skills/ase-arch-analyze/help.md +1 -1
- package/plugin/skills/ase-arch-discover/SKILL.md +8 -6
- package/plugin/skills/ase-code-analyze/SKILL.md +1 -1
- package/plugin/skills/ase-code-analyze/help.md +1 -1
- package/plugin/skills/ase-code-craft/SKILL.md +2 -153
- package/plugin/skills/ase-code-craft/help.md +1 -1
- package/plugin/skills/ase-code-explain/SKILL.md +3 -3
- package/plugin/skills/ase-code-insight/SKILL.md +9 -7
- package/plugin/skills/ase-code-lint/SKILL.md +7 -5
- package/plugin/skills/ase-code-lint/help.md +2 -2
- package/plugin/skills/ase-code-refactor/SKILL.md +4 -155
- package/plugin/skills/ase-code-resolve/SKILL.md +8 -159
- package/plugin/skills/ase-docs-distill/SKILL.md +2 -2
- package/plugin/skills/ase-docs-distill/help.md +1 -1
- package/plugin/skills/ase-docs-proofread/SKILL.md +7 -3
- package/plugin/skills/ase-help-intent/SKILL.md +33 -9
- package/plugin/skills/ase-help-skill/SKILL.md +1 -1
- package/plugin/skills/ase-meta-brainstorm/SKILL.md +11 -2
- package/plugin/skills/ase-meta-changelog/SKILL.md +12 -8
- package/plugin/skills/ase-meta-commit/SKILL.md +11 -1
- package/plugin/skills/ase-meta-compat/SKILL.md +2 -2
- package/plugin/skills/ase-meta-config/help.md +4 -4
- package/plugin/skills/ase-meta-diaboli/SKILL.md +12 -3
- package/plugin/skills/ase-meta-diff/SKILL.md +11 -13
- package/plugin/skills/ase-meta-eli5/SKILL.md +3 -3
- package/plugin/skills/ase-meta-eli5/help.md +1 -1
- package/plugin/skills/ase-meta-evaluate/SKILL.md +3 -3
- package/plugin/skills/ase-meta-quorum/SKILL.md +17 -17
- package/plugin/skills/ase-meta-quotes/SKILL.md +1 -1
- package/plugin/skills/ase-meta-quotes/help.md +4 -3
- package/plugin/skills/ase-meta-review/SKILL.md +7 -5
- package/plugin/skills/ase-meta-review/help.md +2 -2
- package/plugin/skills/ase-meta-search/SKILL.md +18 -0
- package/plugin/skills/ase-meta-steelman/SKILL.md +11 -2
- package/plugin/skills/ase-meta-why/SKILL.md +15 -6
- package/plugin/skills/ase-meta-why/help.md +3 -3
- package/plugin/skills/ase-sync-export/SKILL.md +2 -1
- package/plugin/skills/ase-sync-export/help.md +1 -1
- package/plugin/skills/ase-sync-reconcile/SKILL.md +16 -2
- package/plugin/skills/ase-task-condense/SKILL.md +19 -72
- package/plugin/skills/ase-task-condense/help.md +1 -1
- package/plugin/skills/ase-task-delete/SKILL.md +1 -1
- package/plugin/skills/ase-task-dissect/SKILL.md +5 -18
- package/plugin/skills/ase-task-edit/SKILL.md +74 -26
- package/plugin/skills/ase-task-grill/SKILL.md +30 -124
- package/plugin/skills/ase-task-grill/help.md +2 -1
- package/plugin/skills/ase-task-id/SKILL.md +12 -2
- package/plugin/skills/ase-task-implement/SKILL.md +37 -4
- package/plugin/skills/ase-task-implement/help.md +9 -5
- package/plugin/skills/ase-task-list/SKILL.md +60 -15
- package/plugin/skills/ase-task-list/help.md +49 -6
- package/plugin/skills/ase-task-preflight/SKILL.md +20 -8
- package/plugin/skills/ase-task-preflight/help.md +4 -3
- package/plugin/skills/ase-task-reboot/SKILL.md +19 -62
- package/plugin/skills/ase-task-reboot/help.md +1 -1
- package/plugin/skills/ase-task-view/SKILL.md +27 -4
- package/dst/ase-guidance.js +0 -89
- package/dst/ase-persona.js +0 -87
- package/dst/ase-stdout.js +0 -18
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-setup.js
CHANGED
|
@@ -808,9 +808,11 @@ export default class SetupCommand {
|
|
|
808
808
|
/* default for --dev derived from ASE_SETUP_DEV environment variable */
|
|
809
809
|
const envDev = process.env.ASE_SETUP_DEV ?? "";
|
|
810
810
|
const devDflt = envDev !== "" && envDev !== "0" && envDev.toLowerCase() !== "false";
|
|
811
|
-
/* default for --tool derived from ASE_TOOL environment variable
|
|
811
|
+
/* default for --tool derived from ASE_TOOL environment variable
|
|
812
|
+
(validated lazily by "parseTool" in each action, so an invalid
|
|
813
|
+
value cannot break unrelated "ase" commands at startup) */
|
|
812
814
|
const envTool = process.env.ASE_TOOL ?? "";
|
|
813
|
-
const toolDflt = envTool !== "" ?
|
|
815
|
+
const toolDflt = envTool !== "" ? envTool : "claude";
|
|
814
816
|
/* register CLI top-level command "ase setup" */
|
|
815
817
|
const setupCmd = program
|
|
816
818
|
.command("setup")
|
package/dst/ase-statusline.js
CHANGED
|
@@ -388,25 +388,23 @@ export default class StatuslineCommand {
|
|
|
388
388
|
/* ==== RATE LIMITS ==== */
|
|
389
389
|
S: () => {
|
|
390
390
|
const pct5h = data.rate_limits?.five_hour?.used_percentage;
|
|
391
|
-
|
|
392
|
-
|
|
391
|
+
const s = pct5h !== undefined ? `${pct5h.toFixed(1)}%` : "-";
|
|
392
|
+
emit(`${prefix("⏲", "5h-usage")}${c.bold(s)}`);
|
|
393
393
|
},
|
|
394
394
|
D: () => {
|
|
395
395
|
const until5h = data.rate_limits?.five_hour?.resets_at;
|
|
396
396
|
const s = formatTimeUntil(until5h);
|
|
397
|
-
|
|
398
|
-
emit(`${prefix("⏱", "session-resets")}${c.bold(s)}`);
|
|
397
|
+
emit(`${prefix("⏱", "5h-resets")}${c.bold(s !== "" ? s : "-")}`);
|
|
399
398
|
},
|
|
400
399
|
W: () => {
|
|
401
400
|
const pctWk = data.rate_limits?.seven_day?.used_percentage;
|
|
402
|
-
|
|
403
|
-
|
|
401
|
+
const s = pctWk !== undefined ? `${pctWk.toFixed(1)}%` : "-";
|
|
402
|
+
emit(`${prefix("⏲", "7d-usage")}${c.bold(s)}`);
|
|
404
403
|
},
|
|
405
404
|
Q: () => {
|
|
406
405
|
const untilWk = data.rate_limits?.seven_day?.resets_at;
|
|
407
406
|
const s = formatTimeUntil(untilWk);
|
|
408
|
-
|
|
409
|
-
emit(`${prefix("⏱", "weekly-resets")}${c.bold(s)}`);
|
|
407
|
+
emit(`${prefix("⏱", "7d-resets")}${c.bold(s !== "" ? s : "-")}`);
|
|
410
408
|
},
|
|
411
409
|
/* ==== COSTS ==== */
|
|
412
410
|
H: () => {
|
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,15 +528,34 @@ 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`. " +
|
|
533
|
+
"The `variant` argument selects the returned form of the plan: " +
|
|
534
|
+
"`source` (the default) is the *authoring* form and the *only* form which may be " +
|
|
535
|
+
"edited and passed back into `ase_task_save`; " +
|
|
536
|
+
"`render` is the *rendering-prepared* form, which is for *display only* and " +
|
|
537
|
+
"MUST NOT be persisted; " +
|
|
538
|
+
"`both` returns the source form enclosed in `<task-plan-source>` delimiter lines, " +
|
|
539
|
+
"followed by the render form enclosed in `<task-plan-render>` delimiter lines.",
|
|
434
540
|
inputSchema: {
|
|
435
541
|
id: z.string()
|
|
436
|
-
.describe("task identifier (allowed characters: A-Z, a-z, 0-9, '_', '-')")
|
|
542
|
+
.describe("task identifier (allowed characters: A-Z, a-z, 0-9, '_', '-')"),
|
|
543
|
+
variant: z.enum(["source", "render", "both"]).optional()
|
|
544
|
+
.describe("returned form of the plan: `source` (authoring form, the default), " +
|
|
545
|
+
"`render` (rendering-prepared form, display only), or `both` " +
|
|
546
|
+
"(both forms, each enclosed in its delimiter lines)")
|
|
437
547
|
}
|
|
438
548
|
}, async (args) => {
|
|
439
549
|
try {
|
|
440
|
-
const
|
|
441
|
-
const
|
|
550
|
+
const source = Task.load(this.log, args.id);
|
|
551
|
+
const variant = args.variant ?? "source";
|
|
552
|
+
let text = source;
|
|
553
|
+
if (source !== "" && variant === "render")
|
|
554
|
+
text = Markdown.prepare(source);
|
|
555
|
+
else if (source !== "" && variant === "both")
|
|
556
|
+
text =
|
|
557
|
+
`<task-plan-source>\n${source}\n</task-plan-source>\n\n` +
|
|
558
|
+
`<task-plan-render>\n${Markdown.prepare(source)}\n</task-plan-render>\n`;
|
|
442
559
|
return {
|
|
443
560
|
content: [{ type: "text", text }]
|
|
444
561
|
};
|
|
@@ -451,21 +568,28 @@ export class TaskMCP {
|
|
|
451
568
|
mcp.registerTool("ase_task_save", {
|
|
452
569
|
title: "ASE task save",
|
|
453
570
|
description: "Persist a task as `text` under `id`. " +
|
|
454
|
-
"
|
|
455
|
-
"
|
|
571
|
+
"The `text` MUST be the *authoring* form of the plan (as returned by the " +
|
|
572
|
+
"`source` variant of `ase_task_load`) and hence MUST NOT carry any rendering " +
|
|
573
|
+
"artifacts. Overwrites any existing task for the same `id`. " +
|
|
574
|
+
"Returns a status `text` by default, or, if `render` is `true`, the " +
|
|
575
|
+
"*rendering-prepared* form of the just-saved plan, for display purposes only.",
|
|
456
576
|
inputSchema: {
|
|
457
577
|
id: z.string()
|
|
458
578
|
.describe("task identifier (allowed characters: A-Z, a-z, 0-9, '_', '-')"),
|
|
459
579
|
text: z.string()
|
|
460
|
-
.describe("text content of the task")
|
|
580
|
+
.describe("text content of the task, in its authoring form"),
|
|
581
|
+
render: z.boolean().optional()
|
|
582
|
+
.describe("if true, return the rendering-prepared form of the just-saved " +
|
|
583
|
+
"plan instead of a status message (default: false)")
|
|
461
584
|
}
|
|
462
585
|
}, async (args) => {
|
|
463
586
|
try {
|
|
464
587
|
Task.save(this.log, args.id, args.text);
|
|
465
|
-
/* return the prepared content, so a caller
|
|
466
|
-
just-saved plan
|
|
467
|
-
|
|
468
|
-
|
|
588
|
+
/* return the rendering-prepared content on demand, so a caller
|
|
589
|
+
displaying the just-saved plan does not have to re-load it */
|
|
590
|
+
const text = (args.render ?? false) ?
|
|
591
|
+
Markdown.prepare(args.text) :
|
|
592
|
+
`OK: saved task "${args.id}"`;
|
|
469
593
|
return {
|
|
470
594
|
content: [{ type: "text", text }]
|
|
471
595
|
};
|
|
@@ -501,7 +625,7 @@ export class TaskMCP {
|
|
|
501
625
|
mcp.registerTool("ase_task_rename", {
|
|
502
626
|
title: "ASE task rename",
|
|
503
627
|
description: "Rename a previously persisted task from `old` to `new` by moving the " +
|
|
504
|
-
"task `TASK-<id>.md` file and rewriting its embedded
|
|
628
|
+
"task `TASK-<id>.md` file and rewriting its embedded `Id:` frontmatter key. " +
|
|
505
629
|
"Returns a status `text` indicating whether the rename succeeded. " +
|
|
506
630
|
"Fails with an error if the target id already exists.",
|
|
507
631
|
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.56",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"author": {
|
|
12
12
|
"name": "Dr. Ralf S. Engelschall",
|
|
@@ -18,12 +18,12 @@
|
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"eslint": "9.39.4",
|
|
20
20
|
"@eslint/js": "9.39.4",
|
|
21
|
-
"@typescript-eslint/parser": "8.
|
|
22
|
-
"@typescript-eslint/eslint-plugin": "8.
|
|
21
|
+
"@typescript-eslint/parser": "8.66.0",
|
|
22
|
+
"@typescript-eslint/eslint-plugin": "8.66.0",
|
|
23
23
|
"eslint-plugin-promise": "7.3.0",
|
|
24
24
|
"eslint-plugin-import": "2.32.0",
|
|
25
25
|
"neostandard": "0.13.0",
|
|
26
|
-
"globals": "17.
|
|
26
|
+
"globals": "17.9.0",
|
|
27
27
|
"typescript": "6.0.3",
|
|
28
28
|
|
|
29
29
|
"@rse/stx": "1.1.6",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"commander": "15.0.0",
|
|
45
|
-
"@dotenvx/dotenvx": "2.19.
|
|
45
|
+
"@dotenvx/dotenvx": "2.19.2",
|
|
46
46
|
"yaml": "2.9.0",
|
|
47
47
|
"valibot": "1.4.2",
|
|
48
48
|
"execa": "10.0.1",
|
|
@@ -63,7 +63,7 @@ Workflow
|
|
|
63
63
|
- incorrect short-circuit evaluation or operator precedence
|
|
64
64
|
- logical vs. bitwise operator confusion
|
|
65
65
|
- negation mistakes in compound predicates
|
|
66
|
-
- wrong comparison operator (`==` instead of `===`, `<` vs. `<=`, etc)
|
|
66
|
+
- wrong comparison operator (`==` instead of `===`, `<` vs. `<=`, etc.)
|
|
67
67
|
- inverted condition or swapped if/else branches
|
|
68
68
|
- dead or duplicated conditional branches
|
|
69
69
|
- fall-through where a break or return was intended
|
|
@@ -65,10 +65,10 @@ Workflow
|
|
|
65
65
|
identifiers, string literals and comments.
|
|
66
66
|
|
|
67
67
|
Especially, for comments ensure English language only and
|
|
68
|
-
prefer short very brief one-line descriptions.
|
|
68
|
+
prefer short, very brief one-line descriptions.
|
|
69
69
|
|
|
70
70
|
- **A05 - COMPLEXITY**:
|
|
71
|
-
Check for extremely long functions
|
|
71
|
+
Check for extremely long functions and deeply nested code
|
|
72
72
|
constructs.
|
|
73
73
|
|
|
74
74
|
Especially, for functions prefer fewer than 100 lines, and for
|
|
@@ -185,8 +185,8 @@ Workflow
|
|
|
185
185
|
- **A15 - PERFORMANCE**:
|
|
186
186
|
Check for bad performance and inefficiency issues.
|
|
187
187
|
|
|
188
|
-
Especially, check for code constructs with a high (i.e., not
|
|
189
|
-
constant/O(1), or linear/O(n)
|
|
188
|
+
Especially, check for code constructs with a high complexity (i.e., not
|
|
189
|
+
constant/O(1), or linear/O(n)) in their execution time
|
|
190
190
|
and/or memory consumption.
|
|
191
191
|
|
|
192
192
|
- **A16 - SECURITY**:
|
|
@@ -56,5 +56,5 @@ tools:
|
|
|
56
56
|
4. Now call the MCP tool `query(prompt: <query/>)` from the MCP server
|
|
57
57
|
<server/> and then return its result `text` *verbatim* and *without
|
|
58
58
|
any modifications* as the last message. Especially, do *NOT* add or
|
|
59
|
-
remove any text to the MCP server response on your own and do not
|
|
59
|
+
remove any text to or from the MCP server response on your own and do not
|
|
60
60
|
interpret the result in any way.
|
|
@@ -102,7 +102,7 @@ store those facts in the placeholder named `<arg2/>`.
|
|
|
102
102
|
<child-4/>.
|
|
103
103
|
|
|
104
104
|
<expand name="gather-facts"
|
|
105
|
-
arg1="the
|
|
105
|
+
arg1="the CHILD topics (narrower topics that are specializations) of the following topic: <topic/>"
|
|
106
106
|
arg2="facts-children"></expand>
|
|
107
107
|
|
|
108
108
|
Ground the determination of the canonical names of the most
|