@rse/ase 0.9.53 → 0.9.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dst/ase-log.js +5 -0
  2. package/dst/ase-service.js +45 -6
  3. package/dst/ase-task.js +117 -18
  4. package/package.json +2 -2
  5. package/plugin/.claude-plugin/plugin.json +1 -1
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/.github/plugin/plugin.json +1 -1
  8. package/plugin/meta/ase-common-code.md +34 -0
  9. package/plugin/meta/ase-common-task.md +3 -2
  10. package/plugin/meta/ase-constitution.md +6 -1
  11. package/plugin/meta/ase-format-task.md +96 -11
  12. package/plugin/package.json +1 -1
  13. package/plugin/skills/ase-code-craft/SKILL.md +8 -7
  14. package/plugin/skills/ase-code-lint/help.md +2 -2
  15. package/plugin/skills/ase-code-refactor/SKILL.md +8 -7
  16. package/plugin/skills/ase-code-resolve/SKILL.md +8 -7
  17. package/plugin/skills/ase-meta-config/help.md +3 -3
  18. package/plugin/skills/ase-meta-quotes/help.md +4 -3
  19. package/plugin/skills/ase-task-condense/SKILL.md +6 -5
  20. package/plugin/skills/ase-task-condense/help.md +1 -1
  21. package/plugin/skills/ase-task-edit/SKILL.md +26 -5
  22. package/plugin/skills/ase-task-grill/SKILL.md +8 -4
  23. package/plugin/skills/ase-task-grill/help.md +2 -1
  24. package/plugin/skills/ase-task-implement/SKILL.md +150 -5
  25. package/plugin/skills/ase-task-implement/help.md +40 -1
  26. package/plugin/skills/ase-task-list/SKILL.md +51 -15
  27. package/plugin/skills/ase-task-list/help.md +49 -6
  28. package/plugin/skills/ase-task-preflight/SKILL.md +15 -3
  29. package/plugin/skills/ase-task-preflight/help.md +9 -1
  30. package/plugin/skills/ase-task-reboot/SKILL.md +1 -1
  31. package/plugin/skills/ase-task-view/SKILL.md +23 -2
package/dst/ase-log.js CHANGED
@@ -12,6 +12,8 @@ const levels = [
12
12
  { name: "info", style: chalk.blue },
13
13
  { name: "debug", style: chalk.green }
14
14
  ];
15
+ /* check whether an arbitrary string is a valid log level */
16
+ export const isLogLevel = (level) => levels.some((l) => l.name === level);
15
17
  export default class Log {
16
18
  _program;
17
19
  _logLevel;
@@ -36,11 +38,14 @@ export default class Log {
36
38
  return stream;
37
39
  }
38
40
  logLevel(level) {
41
+ if (level === undefined)
42
+ return this._logLevel;
39
43
  const idx = levels.findIndex((l) => l.name === level);
40
44
  if (idx === -1)
41
45
  throw new RangeError(`invalid log level "${level}" (expected one of: ${levels.map((l) => l.name).join(", ")})`);
42
46
  this._logLevel = level;
43
47
  this.logLevelIdx = idx;
48
+ return this._logLevel;
44
49
  }
45
50
  logFile(file) {
46
51
  if (file === this._logFile)
@@ -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.53",
9
+ "version": "0.9.55",
10
10
  "license": "Apache-2.0",
11
11
  "author": {
12
12
  "name": "Dr. Ralf S. Engelschall",
@@ -31,7 +31,7 @@
31
31
  "shx": "0.4.0",
32
32
 
33
33
  "@types/node": "26.1.2",
34
- "@types/luxon": "3.7.2",
34
+ "@types/luxon": "3.7.3",
35
35
  "@types/which": "3.0.4",
36
36
  "@types/update-notifier": "6.0.8",
37
37
  "@types/shell-quote": "1.7.5",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ase",
3
- "version": "0.9.53",
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.53",
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.53",
3
+ "version": "0.9.55",
4
4
  "description": "Agentic Software Engineering (ASE)",
5
5
  "keywords": [ "agentic", "software", "engineering" ],
6
6
  "homepage": "https://ase.tools",
@@ -0,0 +1,34 @@
1
+
2
+ Code Skill Common Steps
3
+ =======================
4
+
5
+ <define name="code-tenets">
6
+
7
+ You *MUST* internalize and strictly honor the **GENERIC TENETS**, and
8
+ the **<arg1/> TENETS** of the **ASE Tenets** in the following creation
9
+ and updating of code. Do not output anything.
10
+
11
+ </define>
12
+
13
+ <define name="code-tenets-from-plan">
14
+
15
+ Determine the *kind of change* the task plan describes and internalize
16
+ the corresponding tenet sets:
17
+
18
+ - If the frontmatter of <task-content/> carries a `Kind: <text/>` key
19
+ and <text/> is one of `CRAFTING`, `REFACTORING`, or `RESOLVING`:
20
+ Set <task-kind><text/></task-kind> (set task kind to the stated kind).
21
+
22
+ - Else:
23
+ The plan states no kind at all, or an unrecognized one, so *infer*
24
+ the kind from the plan content itself: `RESOLVING` if the plan
25
+ predominantly fixes a defect, `REFACTORING` if it predominantly
26
+ re-structures existing artifacts without changing their observable
27
+ behavior, and `CRAFTING` otherwise. Set <task-kind/> to the inferred
28
+ kind, defaulting to `CRAFTING` if the inference stays inconclusive.
29
+
30
+ Then honor the tenet sets of <task-kind/>:
31
+
32
+ <expand name="code-tenets" arg1="<task-kind/>"></expand>
33
+
34
+ </define>
@@ -103,11 +103,12 @@ format, which has to be determined by calling the
103
103
  `ase_timestamp(format: "yyyy-LL-dd HH:mm")` tool of the `ase`
104
104
  MCP server and use the `text` field of its response. If
105
105
  <timestamp-created/> is still unset (because the plan content
106
- had no `Created:` 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:
@@ -14,12 +14,16 @@ which boosts you to an expert-level Software Engineering AI agent.
14
14
  - Do *not* insist on early "return" in "if" blocks, if an "else" block exists.
15
15
  - Do *not* remove any whitespace in the code formatting -- keep whitespace aligned with code base.
16
16
  - Do *not* produce any trailing white-spaces on any lines.
17
+ - Do *not* guess missing tool call parameters or fill them with invented placeholder values.
17
18
 
18
19
  ## Commandments
19
20
 
20
21
  - Be *honest* and *transparent* in all your responses.
21
22
  - *Ground* factual and technical claims in verifiable evidence (code base, local files, or web)
22
23
  with a reference, rather than unverified model knowledge; state explicitly when a claim cannot be verified.
24
+ - Assume your *internal knowledge of dependencies* (libraries, frameworks, tools, and their implementations)
25
+ is *outdated*; always verify the current API, version, and usage pattern against the local sources
26
+ or the web before writing any code against them.
23
27
  - Before proposing any code changes, explain *WHAT* the proposed changes do and *WHY* it is necessary.
24
28
  - Use *concise* and *type-safe code* only.
25
29
  - Use *precise* and *surgical code changes* only.
@@ -37,7 +41,8 @@ which boosts you to an expert-level Software Engineering AI agent.
37
41
  - Use *double-quotes* (`"[...]"`) instead of single-quotes (`'[...]'`) for all strings.
38
42
  - Use K&R coding style with *opening braces* at the end of lines and *closing braces* at the beginning of lines.
39
43
  - When a language has a *more strongly-typed variant*, prefer that variant.
40
- - When generating temporary helper programs, prefer the *target project's primary programming language*.
44
+ - When generating temporary helper programs or scratch test files, prefer the *target project's primary
45
+ programming language* and *clean them up* once they are no longer needed.
41
46
 
42
47
  @./ase-persona.md
43
48
 
@@ -5,11 +5,16 @@ Task
5
5
  Every *task* uses a strict and fixed format:
6
6
 
7
7
  <format>
8
+ ---
9
+ Id: <task-id/>
10
+ Created: <timestamp-created/>
11
+ Modified: <timestamp-modified/>
12
+ Status: <task-status/>
13
+ Properties: <task-properties/>
14
+ Kind: <task-kind/>
15
+ ---
8
16
 
9
- # TASK <task-id/>: <title/>
10
-
11
- ⎈ Created: <timestamp-created/>
12
- ⚙ Modified: <timestamp-modified/>
17
+ # TASK: <title/>
13
18
 
14
19
  ## CONTEXT
15
20
 
@@ -33,8 +38,19 @@ Every *task* uses a strict and fixed format:
33
38
 
34
39
  You *MUST* honor the following hints on this *task* format:
35
40
 
36
- - You *MUST* always keep the first empty line and the last empty line.
37
- 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.
38
54
 
39
55
  - In all descriptions, highlight *code* as
40
56
  <template>`<code/>`</template> and *key aspects* as
@@ -46,11 +62,80 @@ You *MUST* honor the following hints on this *task* format:
46
62
  is changed, what benefit results or what the rationale is behind the
47
63
  change.
48
64
 
49
- - The <task-id/> has to be substituted with the current value of
50
- <ase-task-id/> in the current session context.
51
-
52
- - The <timestamp-created/> is the timestamp when this task plan was
53
- created. The <timestamp-modified/> is the timestamp when this
65
+ - The <task-id/> of the `Id:` frontmatter key has to be substituted
66
+ with the current value of <ase-task-id/> in the current session
67
+ context.
68
+
69
+ - The `Status:` frontmatter key states the current *lifecycle state*
70
+ of the task plan. The key is *optional* and defaults to `DRAFTED`:
71
+ an *absent* key reads as `DRAFTED`, and a *newly created* plan
72
+ carries `DRAFTED` explicitly. The <task-status/> value is *strictly*
73
+ one of the following eight states:
74
+
75
+ - `DRAFTED`: plan exists but is still provisional and not yet
76
+ cleared for implementation.
77
+ - `REJECTED`: plan was reviewed and refused, and has to be reworked
78
+ before it can be approved.
79
+ - `APPROVED`: plan is accepted as authoritative and cleared for
80
+ implementation, but no work has begun.
81
+ - `DEFERRED`: plan is approved and unobstructed, but its start was
82
+ deliberately postponed.
83
+ - `STARTED`: implementation of the plan is actively underway.
84
+ - `BLOCKED`: implementation is halted by an impediment which
85
+ someone has to remove before it can resume.
86
+ - `COMPLETED`: plan was implemented in full and reached its
87
+ intended outcome.
88
+ - `CANCELLED`: plan was terminated before completion, because it
89
+ failed, was called off, or became obsolete.
90
+
91
+ - The eight states form a *state machine*. Whoever sets the `Status:`
92
+ key *MUST* only move along one of the following transitions, whereby
93
+ a *single* operation *MAY* traverse *several* transitions at once if
94
+ it performs the corresponding stages in one go:
95
+
96
+ ```text
97
+ DRAFTED ──reject───▶ REJECTED STARTED ──block────▶ BLOCKED
98
+ REJECTED ──redraft──▶ DRAFTED BLOCKED ──unblock──▶ STARTED
99
+ DRAFTED ──approve──▶ APPROVED STARTED ──complete─▶ COMPLETED
100
+ APPROVED ──defer────▶ DEFERRED
101
+ DEFERRED ──resume───▶ APPROVED any non-terminal state
102
+ APPROVED ──start────▶ STARTED ──cancel───▶ CANCELLED
103
+ ```
104
+
105
+ `COMPLETED` and `CANCELLED` are the two *terminal* states: a plan
106
+ which reached one of them is *finished* and leaves the state machine.
107
+
108
+ - The `Properties:` frontmatter key states which *stages* the task
109
+ plan already passed through. The <task-properties/> value is
110
+ *strictly* either `none` or a comma-separated list of the values
111
+ `grilled`, `preflighted`, `implemented`, and `verified`, listed in
112
+ exactly this order, each at most once. The key is *optional* and
113
+ defaults to `none`: an *absent* key reads as `none`, and a *newly
114
+ created* plan carries `none` explicitly. The list only ever
115
+ *accumulates*: a skill *adds* its own value if still absent and
116
+ *MUST NOT* drop any value already present, except when a plan is
117
+ *recreated from scratch*, which resets it to `none`.
118
+
119
+ - The `Kind:` frontmatter key states the *kind of change* the task plan
120
+ describes, and hence which *operation-specific tenet set* of the
121
+ **ASE Tenets** a subsequent preflight or implementation has to
122
+ honor. The <task-kind/> value is *strictly* one of `CRAFTING`,
123
+ `REFACTORING`, or `RESOLVING`.
124
+
125
+ - The `Kind:` frontmatter key is *optional*: a skill *authoring* or
126
+ *updating* a task plan *CAN* update an already present key or pass
127
+ it through *verbatim* and *MAY* create a missing one by *inferring*
128
+ the kind from the plan content (defaulting to `CRAFTING`). A
129
+ `--dry` run *never* drops this key, as `--dry` only omits the
130
+ `## VERIFICATION` section.
131
+
132
+ - A skill *writing* an optional key which is still *absent* inserts it
133
+ at its position in the key order above and re-aligns the values of
134
+ the whole frontmatter block.
135
+
136
+ - The <timestamp-created/> of the `Created:` frontmatter key is the
137
+ timestamp when this task plan was created. The <timestamp-modified/>
138
+ of the `Modified:` frontmatter key is the timestamp when this
54
139
  task plan was last modified. Both use an ISO-style format
55
140
  value. The value of both can be determined by a call to the
56
141
  `ase_timestamp(format: "yyyy-LL-dd HH:mm")` tool of the `ase` MCP
@@ -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.53",
9
+ "version": "0.9.55",
10
10
  "license": "Apache-2.0",
11
11
  "author": {
12
12
  "name": "Dr. Ralf S. Engelschall",