@rse/ase 0.9.54 → 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 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-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,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`; 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`.",
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 task heading. " +
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.54",
9
+ "version": "0.9.55",
10
10
  "license": "Apache-2.0",
11
11
  "author": {
12
12
  "name": "Dr. Ralf S. Engelschall",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ase",
3
- "version": "0.9.54",
3
+ "version": "0.9.55",
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.55",
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.55",
4
4
  "description": "Agentic Software Engineering (ASE)",
5
5
  "keywords": [ "agentic", "software", "engineering" ],
6
6
  "homepage": "https://ase.tools",
@@ -15,7 +15,7 @@ and updating of code. Do not output anything.
15
15
  Determine the *kind of change* the task plan describes and internalize
16
16
  the corresponding tenet sets:
17
17
 
18
- - If <task-content/> contains a `☯ Kind: <text/>` header line
18
+ - If the frontmatter of <task-content/> carries a `Kind: <text/>` key
19
19
  and <text/> is one of `CRAFTING`, `REFACTORING`, or `RESOLVING`:
20
20
  Set <task-kind><text/></task-kind> (set task kind to the stated kind).
21
21
 
@@ -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:` line), set
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 <task-content/> and calculate
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:
@@ -5,12 +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 <task-id/>: <title/>
10
-
11
- ⎈ Created: <timestamp-created/>
12
- ⚙ Modified: <timestamp-modified/>
13
- ☯ Kind: <task-kind/>
17
+ # TASK: <title/>
14
18
 
15
19
  ## CONTEXT
16
20
 
@@ -34,8 +38,19 @@ Every *task* uses a strict and fixed format:
34
38
 
35
39
  You *MUST* honor the following hints on this *task* format:
36
40
 
37
- - You *MUST* always keep the first empty line and the last empty line.
38
- If one of them is missing, add it back.
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.
39
54
 
40
55
  - In all descriptions, highlight *code* as
41
56
  <template>`<code/>`</template> and *key aspects* as
@@ -47,25 +62,80 @@ You *MUST* honor the following hints on this *task* format:
47
62
  is changed, what benefit results or what the rationale is behind the
48
63
  change.
49
64
 
50
- - The <task-id/> has to be substituted with the current value of
51
- <ase-task-id/> in the current session context.
52
-
53
- - The `☯ Kind:` line states the *kind of change* the task plan
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
54
120
  describes, and hence which *operation-specific tenet set* of the
55
121
  **ASE Tenets** a subsequent preflight or implementation has to
56
122
  honor. The <task-kind/> value is *strictly* one of `CRAFTING`,
57
- `REFACTORING`, or `RESOLVING`, and the line is column-aligned with
58
- the `⎈ Created:` and `⚙ Modified:` lines above it.
123
+ `REFACTORING`, or `RESOLVING`.
59
124
 
60
- - The `☯ Kind:` line is *optional*: a skill *authoring* or
61
- *updating* a task plan *CAN* update an already present line or pass
125
+ - The `Kind:` frontmatter key is *optional*: a skill *authoring* or
126
+ *updating* a task plan *CAN* update an already present key or pass
62
127
  it through *verbatim* and *MAY* create a missing one by *inferring*
63
128
  the kind from the plan content (defaulting to `CRAFTING`). A
64
- `--dry` run *never* drops this line, as `--dry` only omits the
129
+ `--dry` run *never* drops this key, as `--dry` only omits the
65
130
  `## VERIFICATION` section.
66
131
 
67
- - The <timestamp-created/> is the timestamp when this task plan was
68
- created. The <timestamp-modified/> is the timestamp when this
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
69
139
  task plan was last modified. Both use an ISO-style format
70
140
  value. The value of both can be determined by a call to the
71
141
  `ase_timestamp(format: "yyyy-LL-dd HH:mm")` tool of the `ase` MCP
@@ -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.55",
10
10
  "license": "Apache-2.0",
11
11
  "author": {
12
12
  "name": "Dr. Ralf S. Engelschall",
@@ -64,8 +64,8 @@ A05 COMPLEXITY A10 SMELLS A15 PERFORMANCE A20 DEAD-CODE
64
64
 
65
65
  `--exclude`|`-e`=*aspect*[`,`...]:
66
66
  Remove the given comma-separated list of aspect ids from the checked
67
- code quality aspects. Applied *after* `--include`, so `-i A01,A02 -e
68
- A02` checks `A01` only.
67
+ code quality aspects. Applied *after* `--include`, so
68
+ `-i A01,A02 -e A02` checks `A01` only.
69
69
 
70
70
  ## ARGUMENTS
71
71
 
@@ -67,9 +67,9 @@ The following *operations* exist:
67
67
  The `init` and `edit` subcommands of the `ase config` CLI are
68
68
  deliberately *not* mirrored: `edit` is bound to the interactive `$EDITOR`
69
69
  and therefore has no meaning inside an assistant turn, and `init` is a
70
- preset-bootstrapping operation that stays a shell concern next to `ase
71
- setup`. Both remain available as `ase config init` and `ase config edit`
72
- on the command line.
70
+ preset-bootstrapping operation that stays a shell concern next to
71
+ `ase setup`. Both remain available as `ase config init` and
72
+ `ase config edit` on the command line.
73
73
 
74
74
  ## OPTIONS
75
75
 
@@ -39,9 +39,10 @@ without any quote is rendered as `(none)`.
39
39
 
40
40
  Every quote is rendered on its own line and carries, where applicable,
41
41
  the suffixes `— <author>, <origin>` (in the two `ATTRIBUTED`
42
- quadrants, omitting whichever part is unknown), `[from proximity:
43
- <source-topic>]` (when the quote was contributed by a neighborhood topic
44
- under `--proximity` instead of by *topic-keywords* itself), and
42
+ quadrants, omitting whichever part is unknown),
43
+ `[from proximity: <source-topic>]` (when the quote was contributed by a
44
+ neighborhood topic under `--proximity` instead of by *topic-keywords*
45
+ itself), and
45
46
  `(unverified)` (when the exact wording or the attribution could not be
46
47
  established with confidence). The `(unverified)` marker is dropped as
47
48
  soon as the Internet/Web search under `--ground` confirms the wording and
@@ -98,7 +98,7 @@ Set <args></args> (set args to empty).
98
98
  Set <words-before><words/></words-before> (remember the loaded
99
99
  word count for the strictly-smaller check in step 3).
100
100
 
101
- <if condition="<task-content/> contains '⎈ Created: <text/>'">
101
+ <if condition="the frontmatter of <task-content/> carries a `Created: <text/>` key">
102
102
  Set <timestamp-created><text/></timestamp-created> (extract the
103
103
  original creation timestamp so it can be re-inserted unchanged
104
104
  into the condensed <task-content/> in step 3).
@@ -125,10 +125,11 @@ Set <args></args> (set args to empty).
125
125
  and unchanged*. Honor the following ruleset *strictly*:
126
126
 
127
127
  1. *Preserve-exactly (never alter)*: the plan <format/>
128
- structure (the headings `#`/`##`, all three `## CONTEXT`,
129
- `## CHANGES`, and `## VERIFICATION` sections, the
130
- `Created:`/`Modified:` lines, and
131
- the `- **<aspect/>**:` bullet labels), all *code spans* and
128
+ structure (the frontmatter block with its `Id:`, `Created:`,
129
+ `Modified:`, `Status:`, `Properties:`, and `Kind:` keys,
130
+ the headings `#`/`##`, all
131
+ three `## CONTEXT`, `## CHANGES`, and `## VERIFICATION`
132
+ sections, and the `- **<aspect/>**:` bullet labels), all *code spans* and
132
133
  code blocks, technical terms, file paths, identifiers,
133
134
  numbers, severities (`LOW`/`MEDIUM`/`HIGH`/`ACCEPTED`), and
134
135
  the `*<aspect/>*` emphasis highlighting convention.
@@ -30,7 +30,7 @@ telegrapher-like even under the `writer` persona.
30
30
 
31
31
  The plan is saved *only* when condensing actually makes it smaller; if no
32
32
  further reduction is possible, the plan is left untouched (including its
33
- `⚙ Modified:` timestamp) and reported as *already condensed*.
33
+ `Modified:` frontmatter timestamp) and reported as *already condensed*.
34
34
 
35
35
  After condensing, the user is asked whether to stop or hand off to
36
36
  `ase-task-edit`, `ase-task-implement`, or `ase-task-preflight`, unless
@@ -322,13 +322,13 @@ Set <args></args> (set args to empty).
322
322
  `PREFLIGHT`, or declines/cancels in the dialog of step 3.4:
323
323
 
324
324
  1. *Update timestamp*:
325
- <if condition="<task-content/> contains '⚙ Modified:' AND <task-content-dirty/> is 'true'">
325
+ <if condition="the frontmatter of <task-content/> carries a `Modified:` key AND <task-content-dirty/> is 'true'">
326
326
  Update <timestamp-modified/> with the current time in
327
327
  ISO-style format, which has to be determined by calling the
328
328
  `ase_timestamp(format: "yyyy-LL-dd HH:mm")` tool of the `ase`
329
329
  MCP server and use the `text` field of its response. Update
330
- the `⚙ Modified: ...` line of <task-content/> with the new
331
- `⚙ Modified: <timestamp-modified/>`.
330
+ the `Modified: ...` frontmatter key of <task-content/> with the
331
+ new <timestamp-modified/> value.
332
332
  Do not output anything.
333
333
  </if>
334
334
 
@@ -346,6 +346,27 @@ Set <args></args> (set args to empty).
346
346
  </if>
347
347
 
348
348
  3. *Render plan*: Treat <task-content/> as *verbatim* Markdown.
349
+
350
+ For the *rendering only*, drop the leading *frontmatter* block --
351
+ both `---` delimiters and all of their keys -- and instead place
352
+ the following column-aligned glyph lines *before* the
353
+ `# TASK: <title/>` heading, separated from it by an empty line,
354
+ omitting the line of every key absent from the frontmatter. The
355
+ glyph lines *MUST* stay *above* the heading, exactly where the
356
+ frontmatter block sits in the plan file, and *MUST NOT* be moved
357
+ below it. This keeps the `---` delimiters from rendering as a
358
+ horizontal rule plus a *setext heading*. This rewrite is
359
+ *display-only* and *MUST NOT* change <task-content/> itself:
360
+
361
+ <format>
362
+ ◉ **Id:** <task-id/>
363
+ ⎈ **Created:** <timestamp-created/>
364
+ ⚙ **Modified:** <timestamp-modified/>
365
+ ◐ **Status:** <task-status/>
366
+ ⚑ **Properties:** <task-properties/>
367
+ ☯ **Kind:** <task-kind/>
368
+ </format>
369
+
349
370
  Only output the following <template/>, so the user
350
371
  can read the plan and react to it. If <task-content/> is longer
351
372
  than 90 lines and a `## IMPLEMENTATION DRAFT` section (from the
@@ -355,9 +376,9 @@ Set <args></args> (set args to empty).
355
376
  Use the following <template/>:
356
377
 
357
378
  <template>
358
- <ase-tpl-head title="TASK"/>
379
+ <ase-tpl-head title="TASK" subtitle="<task-id/>"/>
359
380
  <task-content/>
360
- <ase-tpl-foot title="TASK"/>
381
+ <ase-tpl-foot title="TASK" subtitle="<task-id/>"/>
361
382
  </template>
362
383
 
363
384
  4. *Determine next step*:
@@ -216,7 +216,7 @@ Set <args>--int-reuse-task</args>.
216
216
 
217
217
  2. Finally, update the plan in <plan/> based on all answers <answer-N/>.
218
218
 
219
- 3. <if condition="<plan/> contains '⎈ Created: <text/>'">
219
+ 3. <if condition="the frontmatter of <plan/> carries a `Created: <text/>` key">
220
220
  Set <timestamp-created><text/></timestamp-created> (set
221
221
  timestamp-created to extracted text)
222
222
  </if>
@@ -225,11 +225,15 @@ Set <args>--int-reuse-task</args>.
225
225
  `ase` MCP server and use the `text` field of its response for
226
226
  <timestamp-modified/> information. If <timestamp-created/> is
227
227
  still unset (because the previous <plan/> had no `Created:`
228
- line), set <timestamp-created><timestamp-modified/></timestamp-created>
228
+ frontmatter key), set <timestamp-created><timestamp-modified/></timestamp-created>
229
229
  (fall back to the modified timestamp). Then insert the current
230
230
  <ase-task-id/>, previous <timestamp-created/>, and refreshed
231
- <timestamp-modified/> information and calculate the number of
232
- words <words/> of <plan/>.
231
+ <timestamp-modified/> information into the frontmatter keys `Id:`,
232
+ `Created:`, and `Modified:` and calculate the number of
233
+ words <words/> of <plan/>. Additionally *add* the value `grilled`
234
+ to the `Properties:` frontmatter key if it is still absent,
235
+ keeping all already present values and *creating* the whole key
236
+ (with the single value `grilled`) if the plan carries none.
233
237
 
234
238
  5. Call the `ase_task_save(id: "<ase-task-id/>",
235
239
  text: "<plan/>")` tool of the `ase` MCP server to save the updated
@@ -24,7 +24,8 @@ the code base and world knowledge), marks the current-plan choice, and
24
24
  lets the user pick via an interactive dialog. It honors checks for
25
25
  *fuzzy language*, *conflicting terminology*, *conflicting code*, and
26
26
  *non-concrete scenarios*. Once all aspects are resolved, the plan is
27
- updated and persisted, and the user is offered a hand-off to editing,
27
+ updated and persisted, its `Properties:` frontmatter key gains the value
28
+ `grilled`, and the user is offered a hand-off to editing,
28
29
  implementation, or preflight.
29
30
 
30
31
  ## OPTIONS
@@ -189,13 +189,42 @@ Procedure
189
189
  way.
190
190
  </if>
191
191
 
192
- 2. Only output the following <template/>:
192
+ 2. Update the frontmatter of <task-content/> as follows, *creating*
193
+ each of the `Properties:`, `Status:`, and `Modified:` keys the
194
+ plan does not carry yet at its position in the key order of the
195
+ plan <format/>:
196
+
197
+ - *Add* the value `implemented` to the `Properties:` key if it
198
+ is still absent, keeping all already present values.
199
+
200
+ - *Add* the value `verified` to the `Properties:` key as well,
201
+ but *only* if the verification phase was actually performed
202
+ and succeeded -- hence *never* for a plan whose
203
+ `## VERIFICATION` section is deliberately omitted.
204
+
205
+ - Set the `Status:` key to `COMPLETED`, but *only* if the
206
+ change set was applied *completely* and *successfully* --
207
+ this traverses the `start` and `complete` transitions of the
208
+ state machine of the plan <format/> in one go. Otherwise
209
+ leave the `Status:` key *untouched*, as an incomplete run
210
+ transitioned nowhere.
211
+
212
+ - Refresh the `Modified:` key with the current time in
213
+ ISO-style format, determined by calling the
214
+ `ase_timestamp(format: "yyyy-LL-dd HH:mm")` tool of the `ase`
215
+ MCP server.
216
+
217
+ Then call the `ase_task_save(id: "<ase-task-id/>", text:
218
+ "<task-content/>")` tool of the `ase` MCP server to persist the
219
+ updated task plan. Do not output anything in this sub-step.
220
+
221
+ 3. Only output the following <template/>:
193
222
 
194
223
  <template>
195
224
  ⧉ **ASE**: ◉ task: **<ase-task-id/>**, ✪ plan: **<words/>** words, ▶ status: **plan implemented**
196
225
  </template>
197
226
 
198
- 3. <if condition="<worktree-dir/> is not empty">
227
+ 4. <if condition="<worktree-dir/> is not empty">
199
228
  Give the closing hint by expanding the following (which,
200
229
  depending on the configured <ase-guidance-level/>, may expand
201
230
  into nothing and hence emit no output at all):
@@ -17,13 +17,17 @@ The `ase-task-implement` skill performs the *final implementation* of
17
17
  a task plan by modifying the corresponding *artifacts* with a complete
18
18
  *change set*. The plan is loaded and any optional `IMPLEMENTATION DRAFT`
19
19
  section produced by `ase-task-preflight` is used as a hint - the plain
20
- plan content always overrules the draft.
20
+ plan content always overrules the draft. Afterwards the plan's
21
+ `Properties:` frontmatter key gains the value `implemented` (plus
22
+ `verified`, if the verification phase actually ran and succeeded), and
23
+ its `Status:` key becomes `COMPLETED` if the change set was applied
24
+ completely and successfully.
21
25
 
22
- The *kind of change* stated by the plan's `☯ Kind:` header line
26
+ The *kind of change* stated by the plan's `Kind:` frontmatter key
23
27
  (`CRAFTING`, `REFACTORING`, or `RESOLVING`) selects which
24
28
  *operation-specific tenet set* of the **ASE Tenets** is internalized
25
29
  before any artifact is touched, in addition to the always applying
26
- **GENERIC TENETS**. If a plan carries no such line, the kind is
30
+ **GENERIC TENETS**. If a plan carries no such key, the kind is
27
31
  *inferred* from the plan content, defaulting to `CRAFTING`.
28
32
 
29
33
  If the task plan deliberately *omits* the `## VERIFICATION` section
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: ase-task-list
3
- argument-hint: "[--help|-h] [--verbose|-v]"
3
+ argument-hint: "[--help|-h] [--verbose|-v] [--include|-i=<state>[,...]] [--exclude|-e=<state>[,...]]"
4
4
  description: >
5
5
  List all available task ids.
6
6
  Use when user wants to see all tasks.
@@ -19,7 +19,7 @@ List Task Plans
19
19
 
20
20
  <expand name="getopt"
21
21
  arg1="ase-task-list"
22
- arg2="--verbose|-v">
22
+ arg2="--verbose|-v --include|-i=none --exclude|-e=COMPLETED,CANCELLED">
23
23
  $ARGUMENTS
24
24
  </expand>
25
25
 
@@ -30,13 +30,40 @@ List Task Plans
30
30
  Procedure
31
31
  ---------
32
32
 
33
- 1. Call the `ase_task_list(verbose: <getopt-option-verbose/>)` tool from
33
+ 1. Determine the *effective state set* <states/>, i.e., the lifecycle
34
+ states a task plan has to be in to be listed at all. For this, parse
35
+ <getopt-option-include/> and <getopt-option-exclude/> as
36
+ comma-separated token lists, silently dropping the `none` sentinel
37
+ and any empty token. If a token <token/> is *not* one of the eight
38
+ `Status:` values `DRAFTED`, `REJECTED`, `APPROVED`, `DEFERRED`,
39
+ `STARTED`, `BLOCKED`, `COMPLETED`, or `CANCELLED`, only output the
40
+ following <template/> and then *STOP* processing the entire current
41
+ skill:
42
+
43
+ <template>
44
+ ⧉ **ASE**: ✪ skill: **ase-task-list**, ▶ ERROR: invalid state: **<token/>**
45
+ </template>
46
+
47
+ Otherwise set <states/> to *all* eight states if both lists are
48
+ empty, to the *include* list if only it is non-empty, to all eight
49
+ *minus* the *exclude* list if only it is non-empty, and to the
50
+ *include* list *minus* the *exclude* list if both are non-empty. If
51
+ the resulting <states/> is *empty*, only output the following
52
+ <template/> and then *STOP* processing the entire current skill:
53
+
54
+ <template>
55
+ ⧉ **ASE**: ✪ skill: **ase-task-list**, ▶ ERROR: options `--include` and `--exclude` cancel out to an empty state set
56
+ </template>
57
+
58
+ 2. Call the `ase_task_list(verbose: <getopt-option-verbose/>)` tool from
34
59
  the `ase` MCP server. The result is a structured object with a
35
- `tasks` array where each entry has an `id` field, and -- if
36
- <getopt-option-verbose/> is `true` -- additionally an `mtime` field
37
- (formatted as `YYYY-MM-DD HH:MM`).
60
+ `tasks` array where each entry has an `id` and a `status` field, and
61
+ -- if <getopt-option-verbose/> is `true` -- additionally an `mtime`
62
+ field (formatted as `YYYY-MM-DD HH:MM`). *Drop* from the `tasks`
63
+ array every entry whose `status` is *not* contained in <states/>. Do
64
+ not output anything.
38
65
 
39
- 2. If the `tasks` array is empty, output the following <template/>:
66
+ 3. If the `tasks` array is empty, output the following <template/>:
40
67
 
41
68
  <template>
42
69
  ⧉ **ASE**: ◉ tasks: *(none)*
@@ -45,16 +72,16 @@ Procedure
45
72
  Else, dispatch on <getopt-option-verbose/>:
46
73
 
47
74
  - If <getopt-option-verbose/> is `true`, output the list of tasks
48
- with the following <template/>, where each <id/> and <mtime/>
49
- correspond to an entry in the task list:
75
+ with the following <template/>, where each <id/>, <status/>, and
76
+ <mtime/> correspond to an entry in the task list:
50
77
 
51
78
  <template>
52
79
  ⧉ **ASE**: ◉ tasks:
53
80
 
54
- | *Task Id* | *Last Modified* |
55
- |-----------|--------------------|
56
- | **<id/>** | `<mtime/>` |
57
- | [...] | [...] |
81
+ | *Task Id* | *Status* | *Last Modified* |
82
+ |-----------|-------------|--------------------|
83
+ | **<id/>** | `<status/>` | `<mtime/>` |
84
+ | [...] | [...] | [...] |
58
85
 
59
86
  </template>
60
87
 
@@ -72,7 +99,7 @@ Procedure
72
99
 
73
100
  </template>
74
101
 
75
- 3. Finally, give the closing hints by expanding the following (which,
102
+ 4. Finally, give the closing hints by expanding the following (which,
76
103
  depending on the configured <ase-guidance-level/>, may each expand
77
104
  into nothing and hence emit no output at all):
78
105
 
@@ -81,6 +108,11 @@ Procedure
81
108
  Use `/ase-task-id <id>` to switch to one of the listed tasks and `/ase-task-view` to inspect its plan.
82
109
  </ase-tpl-hint>
83
110
  </if>
111
+ <elseif condition="entries were dropped by the state filtering of step 2">
112
+ <ase-tpl-hint level="normal">
113
+ All task plans were filtered out by the effective state set -- use `/ase-task-list --exclude none` to list them regardless of their status.
114
+ </ase-tpl-hint>
115
+ </elseif>
84
116
  <else>
85
117
  <ase-tpl-hint level="normal">
86
118
  No task plan exists yet -- use `/ase-task-edit` to create one through a conversational loop.
@@ -89,7 +121,11 @@ Procedure
89
121
 
90
122
  <if condition="<getopt-option-verbose/> is not equal `true`">
91
123
  <ase-tpl-hint level="verbose">
92
- Use `/ase-task-list --verbose` to additionally show the last-modified timestamp of each task plan.
124
+ Use `/ase-task-list --verbose` to additionally show the status and the last-modified timestamp of each task plan.
93
125
  </ase-tpl-hint>
94
126
  </if>
95
127
 
128
+ <ase-tpl-hint level="verbose">
129
+ Use `/ase-task-list --include`/`--exclude` to narrow the listing to certain lifecycle states, e.g. `--include STARTED,BLOCKED`.
130
+ </ase-tpl-hint>
131
+
@@ -8,35 +8,78 @@
8
8
  `ase-task-list`
9
9
  [`--help`|`-h`]
10
10
  [`--verbose`|`-v`]
11
+ [`--include`|`-i`=*state*[`,`...]]
12
+ [`--exclude`|`-e`=*state*[`,`...]]
11
13
 
12
14
  ## DESCRIPTION
13
15
 
14
16
  The `ase-task-list` skill lists all available *task ids* in the
15
17
  current project by calling the `ase_task_list` MCP tool. In the
16
18
  default mode, only the task ids are rendered as a single-column
17
- Markdown table. In verbose mode, the last-modified timestamp of
18
- each task plan is rendered as an additional column.
19
+ Markdown table. In verbose mode, the lifecycle status and the
20
+ last-modified timestamp of each task plan are rendered as additional
21
+ columns.
22
+
23
+ The listing is restricted to an *effective state set*, derived from the
24
+ `Status:` frontmatter key of each task plan (which defaults to `DRAFTED`
25
+ for a plan carrying no such key): with `--include` only, exactly the
26
+ listed states are shown; with `--exclude` only, all states except the
27
+ listed ones; with both, the included ones minus the excluded ones. An
28
+ unknown state, or a combination which cancels out to an empty set,
29
+ aborts the skill with an error. By default,
30
+ `--exclude COMPLETED,CANCELLED` is in effect, so finished and
31
+ abandoned task plans stay out of the way. The eight states are:
32
+
33
+ ```text
34
+ DRAFTED APPROVED STARTED COMPLETED
35
+ REJECTED DEFERRED BLOCKED CANCELLED
36
+ ```
19
37
 
20
38
  ## OPTIONS
21
39
 
22
40
  `--verbose`|`-v`:
23
- Render an additional `Last Modified` column with the
24
- `YYYY-MM-DD HH:MM` timestamp of each task plan.
41
+ Render an additional `Status` column with the lifecycle state and an
42
+ additional `Last Modified` column with the `YYYY-MM-DD HH:MM`
43
+ timestamp of each task plan.
44
+
45
+ `--include`|`-i`=*state*[`,`...]:
46
+ Restrict the listed task plans to the given comma-separated list of
47
+ lifecycle states (e.g. `STARTED,BLOCKED`). Without this option, all
48
+ eight states are listed. The `none` sentinel selects no state at all.
49
+
50
+ `--exclude`|`-e`=*state*[`,`...]:
51
+ Remove the given comma-separated list of lifecycle states from the
52
+ listed task plans. Applied *after* `--include`, so
53
+ `-i DRAFTED,STARTED -e STARTED` lists `DRAFTED` only. Defaults to
54
+ `COMPLETED,CANCELLED`; pass `--exclude none` to suppress the
55
+ default and list task plans in every state.
25
56
 
26
57
  ## EXAMPLES
27
58
 
28
- List all task ids:
59
+ List all unfinished task ids:
29
60
 
30
61
  ```text
31
62
  ❯ /ase-task-list
32
63
  ```
33
64
 
34
- List all task ids together with their last-modified timestamps:
65
+ List all task ids together with their status and last-modified timestamps:
35
66
 
36
67
  ```text
37
68
  ❯ /ase-task-list --verbose
38
69
  ```
39
70
 
71
+ List the task ids of every task plan, including the finished ones:
72
+
73
+ ```text
74
+ ❯ /ase-task-list --exclude none
75
+ ```
76
+
77
+ List only the task ids of the task plans currently under work:
78
+
79
+ ```text
80
+ ❯ /ase-task-list --include STARTED,BLOCKED
81
+ ```
82
+
40
83
  ## SEE ALSO
41
84
 
42
85
  [`ase-task-id`](../ase-task-id/help.md), [`ase-task-view`](../ase-task-view/help.md), [`ase-task-edit`](../ase-task-edit/help.md),
@@ -88,16 +88,22 @@ Procedure
88
88
 
89
89
  </template>
90
90
 
91
- 3. <if condition="<task-content/> contains '⚙ Modified:'">
91
+ 3. <if condition="the frontmatter of <task-content/> carries a `Modified:` key">
92
92
  Update <timestamp-modified/> with the current time in
93
93
  ISO-style format, which has to be determined by calling the
94
94
  `ase_timestamp(format: "yyyy-LL-dd HH:mm")` tool of the `ase`
95
95
  MCP server and use the `text` field of its response. Update
96
- the `⚙ Modified: ...` line of <task-content/> with the new
97
- `⚙ Modified: <timestamp-modified/>`.
96
+ the `Modified: ...` frontmatter key of <task-content/> with the
97
+ new <timestamp-modified/> value.
98
98
  Do not output anything.
99
99
  </if>
100
100
 
101
+ Additionally *add* the value `preflighted` to the `Properties:`
102
+ frontmatter key of <task-content/> if it is still absent, keeping
103
+ all already present values and *creating* the whole key (with the
104
+ single value `preflighted`) if the plan carries none. Do not
105
+ output anything.
106
+
101
107
  4. Finally, call the `ase_task_save(id: "<ase-task-id/>",
102
108
  text: "<task-content/>")` tool of the `ase` MCP server to save the updated
103
109
  task plan content. Calculate the number of words <words/> of
@@ -16,14 +16,15 @@ The `ase-task-preflight` skill performs a *preflight* (dry-run,
16
16
  test-drive) of the *implementation* of a task plan by creating a
17
17
  draft for a corresponding, complete *artifact change set* in
18
18
  *unified diff* format. The draft is appended to the task plan as
19
- an `IMPLEMENTATION DRAFT` section (replacing any previous draft).
19
+ an `IMPLEMENTATION DRAFT` section (replacing any previous draft) and
20
+ the plan's `Properties:` frontmatter key gains the value `preflighted`.
20
21
  No source files are modified.
21
22
 
22
- The *kind of change* stated by the plan's `☯ Kind:` header line
23
+ The *kind of change* stated by the plan's `Kind:` frontmatter key
23
24
  (`CRAFTING`, `REFACTORING`, or `RESOLVING`) selects which
24
25
  *operation-specific tenet set* of the **ASE Tenets** is internalized
25
26
  before the draft is produced, in addition to the always applying
26
- **GENERIC TENETS**. If a plan carries no such line, the kind is
27
+ **GENERIC TENETS**. If a plan carries no such key, the kind is
27
28
  *inferred* from the plan content, defaulting to `CRAFTING`.
28
29
 
29
30
  After the preflight, the user is asked whether to stop, hand
@@ -126,7 +126,7 @@ Procedure
126
126
  (append extracted text to instruction).
127
127
  </if>
128
128
 
129
- 6. <if condition="<task-content/> contains '⎈ Created: <text/>'">
129
+ 6. <if condition="the frontmatter of <task-content/> carries a `Created: <text/>` key">
130
130
  Set <timestamp-created><text/></timestamp-created> (set
131
131
  timestamp-created to extracted text)
132
132
  </if>
@@ -78,6 +78,27 @@ Procedure
78
78
 
79
79
  2. <if condition="<task-content/> is not empty">
80
80
  Treat <task-content/> as *verbatim* Markdown.
81
+
82
+ For the *rendering only*, drop the leading *frontmatter* block --
83
+ both `---` delimiters and all of their keys -- and instead place
84
+ the following column-aligned glyph lines *before* the
85
+ `# TASK: <title/>` heading, separated from it by an empty line,
86
+ omitting the line of every key absent from the frontmatter. The
87
+ glyph lines *MUST* stay *above* the heading, exactly where the
88
+ frontmatter block sits in the plan file, and *MUST NOT* be moved
89
+ below it. This keeps the `---` delimiters from rendering as a
90
+ horizontal rule plus a *setext heading*. This rewrite is
91
+ *display-only* and *MUST NOT* change <task-content/> itself:
92
+
93
+ <format>
94
+ ◉ **Id:** <task-id/>
95
+ ⎈ **Created:** <timestamp-created/>
96
+ ⚙ **Modified:** <timestamp-modified/>
97
+ ◐ **Status:** <task-status/>
98
+ ⚑ **Properties:** <task-properties/>
99
+ ☯ **Kind:** <task-kind/>
100
+ </format>
101
+
81
102
  *Render plan*: Only output the following <template/>. If
82
103
  <getopt-option-full/> is *not* `true`, <task-content/> is longer than
83
104
  90 lines, and a `## IMPLEMENTATION DRAFT` section (from the
@@ -87,9 +108,9 @@ Procedure
87
108
  Use the following <template/>:
88
109
 
89
110
  <template>
90
- <ase-tpl-head title="TASK"/>
111
+ <ase-tpl-head title="TASK" subtitle="<task-id/>"/>
91
112
  <task-content/>
92
- <ase-tpl-foot title="TASK"/>
113
+ <ase-tpl-foot title="TASK" subtitle="<task-id/>"/>
93
114
  </template>
94
115
  </if>
95
116