@rse/ase 0.9.5 → 0.9.7

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.
@@ -18,6 +18,8 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
18
18
  import { Config, configSchema, ConfigMCP } from "./ase-config.js";
19
19
  import { DiagramMCP } from "./ase-diagram.js";
20
20
  import { TaskMCP } from "./ase-task.js";
21
+ import { MarkdownMCP } from "./ase-markdown.js";
22
+ import { ArtifactMCP } from "./ase-artifact.js";
21
23
  import { KVMCP } from "./ase-kv.js";
22
24
  import PersonaMCP from "./ase-persona.js";
23
25
  import { TimestampMCP } from "./ase-timestamp.js";
@@ -236,6 +238,8 @@ export default class ServiceCommand {
236
238
  new ServiceMCP({ projectId: ctx.projectId, port: ctx.port, startTime }).register(mcp);
237
239
  new DiagramMCP().register(mcp);
238
240
  new TaskMCP(this.log).register(mcp);
241
+ new MarkdownMCP().register(mcp);
242
+ new ArtifactMCP(this.log).register(mcp);
239
243
  new KVMCP().register(mcp);
240
244
  new PersonaMCP(this.log).register(mcp);
241
245
  new TimestampMCP().register(mcp);
package/dst/ase-task.js CHANGED
@@ -7,11 +7,14 @@ import path from "node:path";
7
7
  import fs from "node:fs";
8
8
  import { execaSync } from "execa";
9
9
  import { DateTime } from "luxon";
10
+ import picomatch from "picomatch";
10
11
  import { isScalar } from "yaml";
11
12
  import { z } from "zod";
12
13
  import { Config, configSchema, parseScope } from "./ase-config.js";
14
+ import { Markdown } from "./ase-markdown.js";
13
15
  /* reusable functionality: persisted task plans under
14
- <project>/.ase/task/<id>/plan.md */
16
+ <project>/<basedir>/TASK-<id>.md (driven by the
17
+ "project.artifact.task.{basedir,files}" configuration) */
15
18
  export class Task {
16
19
  /* validate the task id to keep it safe as a filename component */
17
20
  static validateId(id) {
@@ -34,100 +37,173 @@ export class Task {
34
37
  }
35
38
  return process.cwd();
36
39
  }
40
+ /* read the configured "basedir" anchor and "files" miniglob spec for
41
+ task storage; "basedir" is project-root-relative (POSIX, defaults
42
+ to ".ase/task") and "files" constrains the task filenames
43
+ (defaults to "*.md") */
44
+ static spec(log) {
45
+ const cfg = new Config("config", configSchema, log);
46
+ cfg.read();
47
+ const read = (key) => {
48
+ const val = cfg.get(key);
49
+ if (val === undefined)
50
+ return "";
51
+ return String(isScalar(val) ? val.value : val);
52
+ };
53
+ const basedir = (read("project.artifact.task.basedir") || ".ase/task")
54
+ .replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
55
+ const files = read("project.artifact.task.files") || "*.md";
56
+ return { basedir, files };
57
+ }
37
58
  /* resolve the on-disk base directory for task storage */
38
- static baseDir() {
39
- return path.join(Task.projectRoot(), ".ase", "task");
59
+ static baseDir(log) {
60
+ return path.join(Task.projectRoot(), Task.spec(log).basedir);
40
61
  }
41
- /* resolve the on-disk path for a given task id */
42
- static path(id) {
62
+ /* resolve the on-disk path for a given task id; as a side effect,
63
+ eagerly migrate any legacy <basedir>/<id>/plan.md files to the
64
+ current <basedir>/TASK-<id>.md layout on first access (guarded by
65
+ a cheap check, so it is a no-op once the store is migrated) */
66
+ static path(log, id) {
43
67
  Task.validateId(id);
44
- return path.join(Task.baseDir(), id, "plan.md");
68
+ if (Task.needsMigration(log))
69
+ Task.migrateAll(log);
70
+ return path.join(Task.baseDir(log), `TASK-${id}.md`);
71
+ }
72
+ /* cheaply check whether any legacy <basedir>/<id>/plan.md file still
73
+ exists in the task base directory and thus needs migration */
74
+ static needsMigration(log) {
75
+ const dir = Task.baseDir(log);
76
+ if (!fs.existsSync(dir))
77
+ return false;
78
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
79
+ if (!entry.isDirectory() || !/^[A-Za-z0-9-]+$/.test(entry.name))
80
+ continue;
81
+ if (fs.existsSync(path.join(dir, entry.name, "plan.md")))
82
+ return true;
83
+ }
84
+ return false;
85
+ }
86
+ /* migrate all legacy <basedir>/<id>/plan.md task files to the current
87
+ <basedir>/TASK-<id>.md layout; an existing TASK-<id>.md is never
88
+ overwritten; the now-empty <id>/ directory is removed afterwards;
89
+ returns the list of migrated task ids in lexicographic order */
90
+ static migrateAll(log) {
91
+ const dir = Task.baseDir(log);
92
+ if (!fs.existsSync(dir))
93
+ return [];
94
+ const migrated = [];
95
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
96
+ if (!entry.isDirectory() || !/^[A-Za-z0-9-]+$/.test(entry.name))
97
+ continue;
98
+ const id = entry.name;
99
+ const oldFile = path.join(dir, id, "plan.md");
100
+ const newFile = path.join(dir, `TASK-${id}.md`);
101
+ if (!fs.existsSync(oldFile))
102
+ continue;
103
+ if (fs.existsSync(newFile)) {
104
+ log.write("warning", `task: not migrating "${id}": target "TASK-${id}.md" already exists`);
105
+ continue;
106
+ }
107
+ fs.renameSync(oldFile, newFile);
108
+ fs.rmSync(path.join(dir, id), { recursive: true, force: true });
109
+ migrated.push(id);
110
+ }
111
+ migrated.sort((a, b) => a.localeCompare(b));
112
+ return migrated;
45
113
  }
46
114
  /* load a task; returns empty string if no task exists */
47
- static load(id) {
48
- const file = Task.path(id);
115
+ static load(log, id) {
116
+ const file = Task.path(log, id);
49
117
  if (!fs.existsSync(file))
50
118
  return "";
51
119
  return fs.readFileSync(file, "utf8");
52
120
  }
53
- /* save a task as UTF-8 text under the given id; the task's home
54
- directory <project>/.ase/task/<id>/ is owned by ASE and removed
55
- in full by Task.delete, so callers must not place foreign files there */
56
- static save(id, text) {
121
+ /* save a task as UTF-8 text under the given id into the
122
+ <project>/<basedir>/TASK-<id>.md file */
123
+ static save(log, id, text) {
57
124
  if (typeof text !== "string")
58
125
  throw new Error("task: text must be a string");
59
- const file = Task.path(id);
126
+ const file = Task.path(log, id);
60
127
  fs.mkdirSync(path.dirname(file), { recursive: true });
61
128
  fs.writeFileSync(file, text, "utf8");
62
129
  }
63
- /* delete a task by id; removes the entire task home directory
64
- <project>/.ase/task/<id>/ (owned by ASE); returns true if a task existed */
65
- static delete(id) {
66
- const file = Task.path(id);
130
+ /* delete a task by id; removes the single
131
+ <project>/<basedir>/TASK-<id>.md file; returns true if a task existed */
132
+ static delete(log, id) {
133
+ const file = Task.path(log, id);
67
134
  if (!fs.existsSync(file))
68
135
  return false;
69
- fs.rmSync(path.dirname(file), { recursive: true, force: true });
136
+ fs.rmSync(file, { force: true });
70
137
  return true;
71
138
  }
72
- /* rename a task by moving the entire task home directory
73
- <project>/.ase/task/<oldId>/ to <project>/.ase/task/<newId>/;
74
- returns true on success, false if the source task does not exist;
75
- throws if the target id already exists */
76
- static rename(oldId, newId) {
77
- const oldDir = path.dirname(Task.path(oldId));
78
- const newDir = path.dirname(Task.path(newId));
79
- if (!fs.existsSync(oldDir))
139
+ /* rename a task by moving its <project>/<basedir>/TASK-<oldId>.md file
140
+ to <project>/<basedir>/TASK-<newId>.md; the embedded
141
+ "# TASK <id>:" heading inside the plan content is rewritten to
142
+ the new id; returns true on success, false if the source task does
143
+ not exist; throws if the target id already exists */
144
+ static rename(log, oldId, newId) {
145
+ const oldFile = Task.path(log, oldId);
146
+ const newFile = Task.path(log, newId);
147
+ if (!fs.existsSync(oldFile))
80
148
  return false;
81
- if (fs.existsSync(newDir))
149
+ if (fs.existsSync(newFile))
82
150
  throw new Error(`task: target id "${newId}" already exists`);
83
- fs.mkdirSync(path.dirname(newDir), { recursive: true });
84
- fs.renameSync(oldDir, newDir);
151
+ const text = fs.readFileSync(oldFile, "utf8");
152
+ const updated = text.replace(/(^#\s+(?:✪\s+)?TASK\s+)[A-Za-z0-9-]+(\s*:)/m, `$1${newId}$2`);
153
+ fs.mkdirSync(path.dirname(newFile), { recursive: true });
154
+ fs.writeFileSync(newFile, updated, "utf8");
155
+ fs.rmSync(oldFile, { force: true });
85
156
  return true;
86
157
  }
87
158
  /* list all persisted tasks in lexicographic id order; if verbose is true,
88
- each entry's `mtime` is set to the `plan.md` modification time formatted
89
- as "YYYY-MM-DD HH:MM", otherwise it is left undefined */
90
- static list(verbose = false) {
91
- const dir = Task.baseDir();
159
+ each entry's `mtime` is set to the task file's modification time
160
+ formatted as "YYYY-MM-DD HH:MM", otherwise it is left undefined */
161
+ static list(log, verbose = false) {
162
+ if (Task.needsMigration(log))
163
+ Task.migrateAll(log);
164
+ const { basedir, files } = Task.spec(log);
165
+ const dir = path.join(Task.projectRoot(), basedir);
92
166
  if (!fs.existsSync(dir))
93
167
  return [];
168
+ const isMatch = picomatch(files, { dot: true });
94
169
  const out = [];
95
170
  for (const entry of fs.readdirSync(dir)) {
96
- if (!/^[A-Za-z0-9-]+$/.test(entry))
97
- continue;
98
- const file = path.join(dir, entry, "plan.md");
99
- if (!fs.existsSync(file))
171
+ const m = /^TASK-([A-Za-z0-9-]+)\.md$/.exec(entry);
172
+ if (m === null || !isMatch(entry))
100
173
  continue;
174
+ const file = path.join(dir, entry);
101
175
  const st = fs.statSync(file);
102
176
  if (!st.isFile())
103
177
  continue;
104
178
  const mtime = verbose ? DateTime.fromJSDate(st.mtime).toFormat("yyyy-LL-dd HH:mm") : undefined;
105
- out.push({ id: entry, mtime });
179
+ out.push({ id: m[1], mtime });
106
180
  }
107
181
  out.sort((a, b) => a.id.localeCompare(b.id));
108
182
  return out;
109
183
  }
110
184
  /* purge tasks whose modification time is older than the given cutoff in
111
185
  milliseconds; returns the list of removed task ids */
112
- static purge(maxAgeMs) {
113
- const dir = Task.baseDir();
186
+ static purge(log, maxAgeMs) {
187
+ if (Task.needsMigration(log))
188
+ Task.migrateAll(log);
189
+ const { basedir, files } = Task.spec(log);
190
+ const dir = path.join(Task.projectRoot(), basedir);
114
191
  if (!fs.existsSync(dir))
115
192
  return [];
193
+ const isMatch = picomatch(files, { dot: true });
116
194
  const cutoff = Date.now() - maxAgeMs;
117
195
  const removed = [];
118
196
  for (const entry of fs.readdirSync(dir)) {
119
- if (!/^[A-Za-z0-9-]+$/.test(entry))
120
- continue;
121
- const sub = path.join(dir, entry);
122
- const file = path.join(sub, "plan.md");
123
- if (!fs.existsSync(file))
197
+ const m = /^TASK-([A-Za-z0-9-]+)\.md$/.exec(entry);
198
+ if (m === null || !isMatch(entry))
124
199
  continue;
200
+ const file = path.join(dir, entry);
125
201
  const st = fs.statSync(file);
126
202
  if (!st.isFile())
127
203
  continue;
128
204
  if (st.mtimeMs < cutoff) {
129
- fs.rmSync(sub, { recursive: true, force: true });
130
- removed.push(entry);
205
+ fs.rmSync(file, { force: true });
206
+ removed.push(m[1]);
131
207
  }
132
208
  }
133
209
  return removed;
@@ -173,7 +249,7 @@ export default class TaskCommand {
173
249
  /* register CLI top-level command "ase task" */
174
250
  const task = program
175
251
  .command("task")
176
- .description("Manage persisted tasks under <project>/.ase/task/<id>/plan.md")
252
+ .description("Manage persisted tasks under <project>/<basedir>/TASK-<id>.md")
177
253
  .action(() => {
178
254
  task.outputHelp();
179
255
  process.exit(1);
@@ -182,9 +258,9 @@ export default class TaskCommand {
182
258
  task
183
259
  .command("list")
184
260
  .description("List all persisted task ids, one per line")
185
- .option("-v, --verbose", "also show the plan.md modification time as (YYYY-MM-DD HH:MM)")
261
+ .option("-v, --verbose", "also show the task file modification time as (YYYY-MM-DD HH:MM)")
186
262
  .action((opts) => {
187
- const items = Task.list(opts.verbose ?? false);
263
+ const items = Task.list(this.log, opts.verbose ?? false);
188
264
  for (const item of items) {
189
265
  if (opts.verbose)
190
266
  process.stdout.write(`${item.id}\t(${item.mtime})\n`);
@@ -199,7 +275,7 @@ export default class TaskCommand {
199
275
  .description("Load a task by id and write it to stdout")
200
276
  .argument("<id>", "Task identifier")
201
277
  .action((id) => {
202
- const text = Task.load(id);
278
+ const text = Task.load(this.log, id);
203
279
  process.stdout.write(text);
204
280
  process.exit(0);
205
281
  });
@@ -209,7 +285,7 @@ export default class TaskCommand {
209
285
  .description("Edit a task by id with $EDITOR")
210
286
  .argument("<id>", "Task identifier")
211
287
  .action((id) => {
212
- const file = Task.path(id);
288
+ const file = Task.path(this.log, id);
213
289
  const editor = process.env.EDITOR ?? process.env.VISUAL ?? "vi";
214
290
  fs.mkdirSync(path.dirname(file), { recursive: true });
215
291
  if (!fs.existsSync(file))
@@ -225,7 +301,7 @@ export default class TaskCommand {
225
301
  .argument("<id>", "Task identifier")
226
302
  .action(async (id) => {
227
303
  const text = await readStdin();
228
- Task.save(id, text);
304
+ Task.save(this.log, id, text);
229
305
  this.log.write("info", `task: saved "${id}"`);
230
306
  process.exit(0);
231
307
  });
@@ -235,7 +311,7 @@ export default class TaskCommand {
235
311
  .description("Delete a task by id")
236
312
  .argument("<id>", "Task identifier")
237
313
  .action((id) => {
238
- const removed = Task.delete(id);
314
+ const removed = Task.delete(this.log, id);
239
315
  if (removed)
240
316
  this.log.write("info", `task: removed "${id}"`);
241
317
  else
@@ -249,7 +325,7 @@ export default class TaskCommand {
249
325
  .argument("<old>", "Old task identifier")
250
326
  .argument("<new>", "New task identifier")
251
327
  .action((oldId, newId) => {
252
- const renamed = Task.rename(oldId, newId);
328
+ const renamed = Task.rename(this.log, oldId, newId);
253
329
  if (renamed)
254
330
  this.log.write("info", `task: renamed "${oldId}" to "${newId}"`);
255
331
  else
@@ -276,7 +352,7 @@ export default class TaskCommand {
276
352
  unit === "d" ? day :
277
353
  unit === "m" ? month :
278
354
  year;
279
- const removed = Task.purge(n * factor);
355
+ const removed = Task.purge(this.log, n * factor);
280
356
  if (removed.length === 0)
281
357
  this.log.write("info", "task: no tasks to purge");
282
358
  else
@@ -300,7 +376,7 @@ export class TaskMCP {
300
376
  description: "List all persisted tasks. " +
301
377
  "Returns a `tasks` array (in lexicographic `id` order) where each item has the " +
302
378
  "task `id`. If `verbose` is `true`, each item additionally has an `mtime` field " +
303
- "(last modification time of the task's `plan.md`, formatted as `YYYY-MM-DD HH:MM`). " +
379
+ "(last modification time of the task's `TASK-<id>.md` file, formatted as `YYYY-MM-DD HH:MM`). " +
304
380
  "Returns an empty array if no tasks exist.",
305
381
  inputSchema: {
306
382
  verbose: z.boolean().optional()
@@ -310,13 +386,13 @@ export class TaskMCP {
310
386
  tasks: z.array(z.object({
311
387
  id: z.string().describe("task identifier"),
312
388
  mtime: z.string().optional()
313
- .describe("plan.md modification time (`YYYY-MM-DD HH:MM`); only present if `verbose` is true")
389
+ .describe("`TASK-<id>.md` modification time (`YYYY-MM-DD HH:MM`); only present if `verbose` is true")
314
390
  })).describe("all persisted tasks in lexicographic id order")
315
391
  }
316
392
  }, async (args) => {
317
393
  try {
318
394
  const verbose = args.verbose ?? false;
319
- const items = Task.list(verbose);
395
+ const items = Task.list(this.log, verbose);
320
396
  const tasks = verbose ?
321
397
  items.map((item) => ({ id: item.id, mtime: item.mtime ?? "" })) :
322
398
  items.map((item) => ({ id: item.id }));
@@ -345,7 +421,8 @@ export class TaskMCP {
345
421
  }
346
422
  }, async (args) => {
347
423
  try {
348
- const text = Task.load(args.id);
424
+ const raw = Task.load(this.log, args.id);
425
+ const text = Markdown.prepare(raw);
349
426
  return {
350
427
  content: [{ type: "text", text }]
351
428
  };
@@ -371,7 +448,8 @@ export class TaskMCP {
371
448
  }
372
449
  }, async (args) => {
373
450
  try {
374
- Task.save(args.id, args.text);
451
+ const text = Markdown.prepare(args.text);
452
+ Task.save(this.log, args.id, text);
375
453
  return {
376
454
  content: [{ type: "text", text: `task_save: OK: saved task "${args.id}"` }]
377
455
  };
@@ -395,7 +473,7 @@ export class TaskMCP {
395
473
  }
396
474
  }, async (args) => {
397
475
  try {
398
- const removed = Task.delete(args.id);
476
+ const removed = Task.delete(this.log, args.id);
399
477
  const msg = removed ?
400
478
  `task_delete: OK: removed task "${args.id}"` :
401
479
  `task_delete: WARNING: no task "${args.id}" to remove`;
@@ -415,7 +493,7 @@ export class TaskMCP {
415
493
  mcp.registerTool("ase_task_rename", {
416
494
  title: "ASE task rename",
417
495
  description: "Rename a previously persisted task from `old` to `new` by atomically moving the " +
418
- "task home directory. Returns a status `text` indicating whether the rename succeeded. " +
496
+ "task `TASK-<id>.md` file. Returns a status `text` indicating whether the rename succeeded. " +
419
497
  "Fails with an error if the target id already exists.",
420
498
  inputSchema: {
421
499
  old: z.string()
@@ -425,7 +503,7 @@ export class TaskMCP {
425
503
  }
426
504
  }, async (args) => {
427
505
  try {
428
- const renamed = Task.rename(args.old, args.new);
506
+ const renamed = Task.rename(this.log, args.old, args.new);
429
507
  const msg = renamed ?
430
508
  `task_rename: OK: renamed task "${args.old}" to "${args.new}"` :
431
509
  `task_rename: WARNING: no task "${args.old}" to rename`;
package/dst/ase.js CHANGED
@@ -14,6 +14,7 @@ import DiagramCommand from "./ase-diagram.js";
14
14
  import SetupCommand from "./ase-setup.js";
15
15
  import StatuslineCommand from "./ase-statusline.js";
16
16
  import TaskCommand from "./ase-task.js";
17
+ import ArtifactCommand from "./ase-artifact.js";
17
18
  import pkg from "../package.json" with { type: "json" };
18
19
  /* globally initialize logger */
19
20
  const log = new Log("ase", "warning", "-");
@@ -49,6 +50,7 @@ const main = async () => {
49
50
  new HookCommand(log).register(program);
50
51
  new StatuslineCommand(log).register(program);
51
52
  new TaskCommand(log).register(program);
53
+ new ArtifactCommand(log).register(program);
52
54
  new DiagramCommand(log).register(program);
53
55
  /* parse program arguments */
54
56
  await program.parseAsync(process.argv);
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "homepage": "http://github.com/rse/ase",
7
7
  "repository": { "url": "git+https://github.com/rse/ase.git", "type": "git" },
8
8
  "bugs": { "url": "http://github.com/rse/ase/issues" },
9
- "version": "0.9.5",
9
+ "version": "0.9.7",
10
10
  "license": "GPL-3.0-only",
11
11
  "author": {
12
12
  "name": "Dr. Ralf S. Engelschall",
@@ -18,8 +18,8 @@
18
18
  "devDependencies": {
19
19
  "eslint": "9.39.4",
20
20
  "@eslint/js": "9.39.4",
21
- "@typescript-eslint/parser": "8.60.1",
22
- "@typescript-eslint/eslint-plugin": "8.60.1",
21
+ "@typescript-eslint/parser": "8.61.0",
22
+ "@typescript-eslint/eslint-plugin": "8.61.0",
23
23
  "eslint-plugin-promise": "7.3.0",
24
24
  "eslint-plugin-import": "2.32.0",
25
25
  "neostandard": "0.13.0",
@@ -37,11 +37,12 @@
37
37
  "@types/shell-quote": "1.7.5",
38
38
  "@types/proper-lockfile": "4.1.4",
39
39
  "@types/write-file-atomic": "4.0.3",
40
- "@types/pacote": "11.1.8"
40
+ "@types/pacote": "11.1.8",
41
+ "@types/picomatch": "4.0.3"
41
42
  },
42
43
  "dependencies": {
43
44
  "commander": "15.0.0",
44
- "@dotenvx/dotenvx": "1.71.0",
45
+ "@dotenvx/dotenvx": "1.71.2",
45
46
  "yaml": "2.9.0",
46
47
  "valibot": "1.4.1",
47
48
  "execa": "9.6.1",
@@ -59,8 +60,9 @@
59
60
  "shell-quote": "1.8.4",
60
61
  "proper-lockfile": "4.1.2",
61
62
  "write-file-atomic": "8.0.0",
62
- "pacote": "21.5.0",
63
- "ofetch": "1.5.1"
63
+ "pacote": "21.5.1",
64
+ "ofetch": "1.5.1",
65
+ "picomatch": "4.0.4"
64
66
  },
65
67
  "engines": {
66
68
  "npm": ">=10.0.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ase",
3
- "version": "0.9.5",
3
+ "version": "0.9.7",
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.5",
3
+ "version": "0.9.7",
4
4
  "description": "Agentic Software Engineering (ASE)",
5
5
  "keywords": [ "agentic", "software", "engineering" ],
6
6
  "homepage": "https://ase.tools",
@@ -16,12 +16,15 @@ Artifact Meta Information
16
16
  (SAS)", "Architecture Description", or "Architecture Decision
17
17
  Record (ADR)".
18
18
 
19
- - `Software` (`SOFT`), aka "Software Implementation Results (IMP)".
19
+ - `Source Code` (`CODE`), aka "Software Implementation Results (IMP)",
20
+ "Code", or "Software".
20
21
 
21
22
  - `Documentation` (`DOCS`), aka "Software Documentation Results (DOC)".
22
23
 
24
+ - `Tasks` (`TASK`), aka "Task Plans", "Issues", or "User Stories".
25
+
23
26
  Each **Artifact Set** has a unique identifier <artifact-set-id/>,
24
- which is one of `SPEC`, `ARCH`, `SOFT`, or `DOCS`.
27
+ which is one of `SPEC`, `ARCH`, `CODE`, `DOCS`, or `TASK`.
25
28
 
26
29
  - **Artifact**:
27
30
 
@@ -1,36 +1,37 @@
1
1
 
2
- Plan Format
3
- -----------
2
+ Task
3
+ ----
4
4
 
5
- Every *task plan* uses a strict and fixed format:
5
+ Every *task* uses a strict and fixed format:
6
6
 
7
7
  <format>
8
8
 
9
- #TASK PLAN: **<title/>**
9
+ # TASK <task-id/>: <title/>
10
10
 
11
- ◉ task id: **<task-id/>** // ✳ created: **<timestamp-created/>** // ✎ modified: **<timestamp-modified/>**
11
+ Created: <timestamp-created/>
12
+ Modified: <timestamp-modified/>
12
13
 
13
- ##CONTEXT
14
+ ## CONTEXT
14
15
 
15
- - **WHAT**: <summary-what/>
16
+ - **WHAT**: <summary-what/>
16
17
 
17
- - **WHY**: <summary-why/>
18
+ - **WHY**: <summary-why/>
18
19
 
19
- ##CHANGES
20
+ ## CHANGES
20
21
 
21
- - [...]
22
+ - [...]
22
23
 
23
- - [...]
24
+ - [...]
24
25
 
25
- ##VERIFICATION
26
+ ## VERIFICATION
26
27
 
27
- - [...]
28
+ - [...]
28
29
 
29
- - [...]
30
+ - [...]
30
31
 
31
32
  </format>
32
33
 
33
- You *MUST* honor the following hints on this *task plan* format:
34
+ You *MUST* honor the following hints on this *task* format:
34
35
 
35
36
  - You *MUST* always keep the first empty line and the last empty line.
36
37
  If one of them is missing, add it back.
@@ -60,7 +61,7 @@ You *MUST* honor the following hints on this *task plan* format:
60
61
  - The <title/> is a short summary of the <summary-what/>, no longer than
61
62
  50 characters.
62
63
 
63
- - The sections `※ CHANGES` and `※ VERIFICATION` all are just a short
64
+ - The sections `## CHANGES` and `## VERIFICATION` all are just a short
64
65
  list of 1-5 bullet points. Each bullet point is formatted as
65
66
  `- **<aspect/>**: <specification/>` where <aspect/> indicates
66
67
  the aspect of the section and <specification/> is 1-3 sentences
@@ -68,6 +69,6 @@ You *MUST* honor the following hints on this *task plan* format:
68
69
  description of the aspect.
69
70
 
70
71
  - In all sections, break all lines with a newline character
71
- after about 120 characters per line for better subsequent
72
+ after about 100 characters per line for better subsequent
72
73
  manual editing.
73
74
 
@@ -6,7 +6,7 @@
6
6
  "homepage": "http://github.com/rse/ase",
7
7
  "repository": { "url": "git+https://github.com/rse/ase.git", "type": "git" },
8
8
  "bugs": { "url": "http://github.com/rse/ase/issues" },
9
- "version": "0.9.5",
9
+ "version": "0.9.7",
10
10
  "license": "GPL-3.0-only",
11
11
  "author": {
12
12
  "name": "Dr. Ralf S. Engelschall",
@@ -19,7 +19,7 @@
19
19
  "markdownlint-cli2": "0.22.1",
20
20
  "eslint": "10.4.1",
21
21
  "@eslint/markdown": "8.0.2",
22
- "eslint-markdown": "0.10.0"
22
+ "eslint-markdown": "0.11.0"
23
23
  },
24
24
  "engines": {
25
25
  "npm": ">=10.0.0",
@@ -38,7 +38,7 @@ From scratch *craft* the following feature:
38
38
  <feature><getopt-arguments/></feature>
39
39
  </objective>
40
40
 
41
- @${CLAUDE_SKILL_DIR}/../../meta/ase-format-plan.md
41
+ @${CLAUDE_SKILL_DIR}/../../meta/ase-format-task.md
42
42
 
43
43
  Procedure
44
44
  ---------
@@ -206,12 +206,12 @@ permitted way to persist artifacts is via `ase_task_save(...)`.
206
206
  inlining its pros/cons derived in sub-step 2:
207
207
 
208
208
  <template>
209
- **APPROACH A<n/>**<annotation/>: *<summary/>*
210
- [...]
211
- [...]
212
- [...]
213
- *pro*: [...]
214
- *con*: [...]
209
+ **APPROACH A<n/>**<annotation/>: *<summary/>*
210
+ [...]
211
+ [...]
212
+ [...]
213
+ *PRO*: [...]
214
+ *CON*: [...]
215
215
  <optional-diagram/>
216
216
  </template>
217
217
 
@@ -278,7 +278,7 @@ permitted way to persist artifacts is via `ase_task_save(...)`.
278
278
  file, aligned with its existing style and conventions.
279
279
 
280
280
  <if condition="<getopt-option-dry/> is equal `true`">
281
- You *MUST* completely omit the `##VERIFICATION` section
281
+ You *MUST* completely omit the `## VERIFICATION` section
282
282
  (including its heading and all of its bullet points) from
283
283
  <content/>.
284
284
  </if>
@@ -34,7 +34,7 @@ plan via `ase_task_save` and then hands off to `ase-task-edit`,
34
34
  asking the user via the interactive dialog.
35
35
 
36
36
  `--dry`|`-d`:
37
- Compose the plan *without* the `※ VERIFICATION` section. When
37
+ Compose the plan *without* the `## VERIFICATION` section. When
38
38
  `ase-task-implement` later applies such a plan, it strictly skips
39
39
  the entire verification phase (no build, tests, linter,
40
40
  type-checker, or program execution) once the source files have
@@ -43,7 +43,7 @@ plan via `ase_task_save` and then hands off to `ase-task-edit`,
43
43
  `--quick`|`-Q`:
44
44
  Shorthand alias for `-a -d -n IMPLEMENT,DELETE`: automatically pick
45
45
  the recommended feature approach, compose the plan *without* the
46
- `※ VERIFICATION` section, immediately hand off to `ase-task-implement`,
46
+ `## VERIFICATION` section, immediately hand off to `ase-task-implement`,
47
47
  and finally `ase-task-delete` the now-consumed plan. This gives a
48
48
  single, fast *one-shot* crafting mode.
49
49