@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.
Files changed (86) hide show
  1. package/dst/ase-log.js +5 -0
  2. package/dst/ase-service.js +45 -6
  3. package/dst/ase-setup.js +4 -2
  4. package/dst/ase-statusline.js +6 -8
  5. package/dst/ase-task.js +152 -28
  6. package/package.json +5 -5
  7. package/plugin/.claude-plugin/plugin.json +1 -1
  8. package/plugin/.codex-plugin/plugin.json +1 -1
  9. package/plugin/.github/plugin/plugin.json +1 -1
  10. package/plugin/agents/ase-code-analyze.md +1 -1
  11. package/plugin/agents/ase-code-lint.md +4 -4
  12. package/plugin/agents/ase-meta-chat.md +1 -1
  13. package/plugin/agents/ase-meta-proximity.md +1 -1
  14. package/plugin/meta/ase-common-code.md +165 -2
  15. package/plugin/meta/ase-common-task.md +17 -15
  16. package/plugin/meta/ase-constitution.md +3 -3
  17. package/plugin/meta/ase-control.md +7 -7
  18. package/plugin/meta/ase-dialog.md +5 -4
  19. package/plugin/meta/ase-format-arch.md +7 -8
  20. package/plugin/meta/ase-format-spec.md +10 -11
  21. package/plugin/meta/ase-format-task.md +90 -20
  22. package/plugin/meta/ase-getopt.md +1 -1
  23. package/plugin/meta/ase-persona.md +3 -3
  24. package/plugin/meta/ase-skill.md +14 -24
  25. package/plugin/meta/ase-tenets.md +5 -5
  26. package/plugin/package.json +2 -2
  27. package/plugin/skills/ase-arch-analyze/SKILL.md +1 -1
  28. package/plugin/skills/ase-arch-analyze/help.md +1 -1
  29. package/plugin/skills/ase-arch-discover/SKILL.md +8 -6
  30. package/plugin/skills/ase-code-analyze/SKILL.md +1 -1
  31. package/plugin/skills/ase-code-analyze/help.md +1 -1
  32. package/plugin/skills/ase-code-craft/SKILL.md +2 -153
  33. package/plugin/skills/ase-code-craft/help.md +1 -1
  34. package/plugin/skills/ase-code-explain/SKILL.md +3 -3
  35. package/plugin/skills/ase-code-insight/SKILL.md +9 -7
  36. package/plugin/skills/ase-code-lint/SKILL.md +7 -5
  37. package/plugin/skills/ase-code-lint/help.md +2 -2
  38. package/plugin/skills/ase-code-refactor/SKILL.md +4 -155
  39. package/plugin/skills/ase-code-resolve/SKILL.md +8 -159
  40. package/plugin/skills/ase-docs-distill/SKILL.md +2 -2
  41. package/plugin/skills/ase-docs-distill/help.md +1 -1
  42. package/plugin/skills/ase-docs-proofread/SKILL.md +7 -3
  43. package/plugin/skills/ase-help-intent/SKILL.md +33 -9
  44. package/plugin/skills/ase-help-skill/SKILL.md +1 -1
  45. package/plugin/skills/ase-meta-brainstorm/SKILL.md +11 -2
  46. package/plugin/skills/ase-meta-changelog/SKILL.md +12 -8
  47. package/plugin/skills/ase-meta-commit/SKILL.md +11 -1
  48. package/plugin/skills/ase-meta-compat/SKILL.md +2 -2
  49. package/plugin/skills/ase-meta-config/help.md +4 -4
  50. package/plugin/skills/ase-meta-diaboli/SKILL.md +12 -3
  51. package/plugin/skills/ase-meta-diff/SKILL.md +11 -13
  52. package/plugin/skills/ase-meta-eli5/SKILL.md +3 -3
  53. package/plugin/skills/ase-meta-eli5/help.md +1 -1
  54. package/plugin/skills/ase-meta-evaluate/SKILL.md +3 -3
  55. package/plugin/skills/ase-meta-quorum/SKILL.md +17 -17
  56. package/plugin/skills/ase-meta-quotes/SKILL.md +1 -1
  57. package/plugin/skills/ase-meta-quotes/help.md +4 -3
  58. package/plugin/skills/ase-meta-review/SKILL.md +7 -5
  59. package/plugin/skills/ase-meta-review/help.md +2 -2
  60. package/plugin/skills/ase-meta-search/SKILL.md +18 -0
  61. package/plugin/skills/ase-meta-steelman/SKILL.md +11 -2
  62. package/plugin/skills/ase-meta-why/SKILL.md +15 -6
  63. package/plugin/skills/ase-meta-why/help.md +3 -3
  64. package/plugin/skills/ase-sync-export/SKILL.md +2 -1
  65. package/plugin/skills/ase-sync-export/help.md +1 -1
  66. package/plugin/skills/ase-sync-reconcile/SKILL.md +16 -2
  67. package/plugin/skills/ase-task-condense/SKILL.md +19 -72
  68. package/plugin/skills/ase-task-condense/help.md +1 -1
  69. package/plugin/skills/ase-task-delete/SKILL.md +1 -1
  70. package/plugin/skills/ase-task-dissect/SKILL.md +5 -18
  71. package/plugin/skills/ase-task-edit/SKILL.md +74 -26
  72. package/plugin/skills/ase-task-grill/SKILL.md +30 -124
  73. package/plugin/skills/ase-task-grill/help.md +2 -1
  74. package/plugin/skills/ase-task-id/SKILL.md +12 -2
  75. package/plugin/skills/ase-task-implement/SKILL.md +37 -4
  76. package/plugin/skills/ase-task-implement/help.md +9 -5
  77. package/plugin/skills/ase-task-list/SKILL.md +60 -15
  78. package/plugin/skills/ase-task-list/help.md +49 -6
  79. package/plugin/skills/ase-task-preflight/SKILL.md +20 -8
  80. package/plugin/skills/ase-task-preflight/help.md +4 -3
  81. package/plugin/skills/ase-task-reboot/SKILL.md +19 -62
  82. package/plugin/skills/ase-task-reboot/help.md +1 -1
  83. package/plugin/skills/ase-task-view/SKILL.md +27 -4
  84. package/dst/ase-guidance.js +0 -89
  85. package/dst/ase-persona.js +0 -87
  86. 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)
@@ -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
- bodyInfo += ` ${JSON.stringify(bArgs)}`;
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
- this.log.write("info", `mcp: ${request.method.toUpperCase()} ${request.path}${bodyInfo}`);
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 !== "" ? this.parseTool(envTool) : "claude";
815
+ const toolDflt = envTool !== "" ? envTool : "claude";
814
816
  /* register CLI top-level command "ase setup" */
815
817
  const setupCmd = program
816
818
  .command("setup")
@@ -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
- if (pct5h !== undefined)
392
- emit(`${prefix("⏲", "session-usage")}${c.bold(`${pct5h.toFixed(1)}%`)}`);
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
- if (s !== "")
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
- if (pctWk !== undefined)
403
- emit(`${prefix("⏲", "weekly-usage")}${c.bold(`${pctWk.toFixed(1)}%`)}`);
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
- if (s !== "")
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
- /* load a task; returns empty string if no task exists */
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
- "# TASK <id>:" heading inside the plan content is rewritten to
172
- the new id; returns true on success, false if the source task does
173
- not exist; throws if the target id already exists */
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.replace(/(^#\s+TASK\s+)[A-Za-z0-9_-]+(\s*:)/m, `$1${newId}$2`);
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
- /* list all persisted tasks in lexicographic id order; if verbose is true,
211
- each entry's `mtime` is set to the task file's modification time
212
- formatted as "YYYY-MM-DD HH:MM", otherwise it is left undefined */
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 file modification time as (YYYY-MM-DD HH:MM)")
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 items = Task.list(this.log, opts.verbose ?? false);
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`. If `verbose` is `true`, each item additionally has an `mtime` field " +
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`; returns an empty string if no task exists for the `id`.",
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 raw = Task.load(this.log, args.id);
441
- const text = Markdown.prepare(raw);
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
- "Overwrites any existing task for the same `id`. " +
455
- "Returns the persisted task as `text`, prepared for improved rendering.",
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 reusing the
466
- just-saved plan instead of re-loading it still receives
467
- the rendering-prepared variant */
468
- const text = Markdown.prepare(args.text);
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 task heading. " +
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.54",
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.65.0",
22
- "@typescript-eslint/eslint-plugin": "8.65.0",
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.8.0",
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.1",
45
+ "@dotenvx/dotenvx": "2.19.2",
46
46
  "yaml": "2.9.0",
47
47
  "valibot": "1.4.2",
48
48
  "execa": "10.0.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ase",
3
- "version": "0.9.54",
3
+ "version": "0.9.56",
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.54",
3
+ "version": "0.9.56",
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.54",
3
+ "version": "0.9.56",
4
4
  "description": "Agentic Software Engineering (ASE)",
5
5
  "keywords": [ "agentic", "software", "engineering" ],
6
6
  "homepage": "https://ase.tools",
@@ -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, and deeply nested code
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) complexity) in its execution time
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 CHILDREN topics (narrower topics that are specializations) of the following topic: <topic/>"
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