@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.
- package/dst/ase-artifact.js +331 -0
- package/dst/ase-config.js +56 -10
- package/dst/ase-markdown.js +235 -0
- package/dst/ase-service.js +4 -0
- package/dst/ase-task.js +142 -64
- package/dst/ase.js +2 -0
- package/package.json +9 -7
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.github/plugin/plugin.json +1 -1
- package/plugin/meta/ase-format-meta.md +5 -2
- package/plugin/meta/{ase-format-plan.md → ase-format-task.md} +18 -17
- package/plugin/package.json +2 -2
- package/plugin/skills/ase-code-craft/SKILL.md +8 -8
- package/plugin/skills/ase-code-craft/help.md +2 -2
- package/plugin/skills/ase-code-refactor/SKILL.md +8 -8
- package/plugin/skills/ase-code-refactor/help.md +2 -2
- package/plugin/skills/ase-code-resolve/SKILL.md +8 -8
- package/plugin/skills/ase-code-resolve/help.md +2 -2
- package/plugin/skills/ase-task-condense/SKILL.md +1 -1
- package/plugin/skills/ase-task-delete/help.md +0 -4
- package/plugin/skills/ase-task-edit/SKILL.md +9 -8
- package/plugin/skills/ase-task-edit/help.md +1 -5
- package/plugin/skills/ase-task-grill/SKILL.md +8 -2
- package/plugin/skills/ase-task-grill/help.md +0 -4
- package/plugin/skills/ase-task-implement/SKILL.md +3 -3
- package/plugin/skills/ase-task-implement/help.md +7 -8
- package/plugin/skills/ase-task-preflight/SKILL.md +3 -3
- package/plugin/skills/ase-task-reboot/SKILL.md +6 -6
- package/plugin/skills/ase-task-view/SKILL.md +19 -8
- package/dst/ase-bash.js +0 -618
- package/dst/ase-hello.js +0 -24
package/dst/ase-service.js
CHANGED
|
@@ -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
|
|
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(),
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
64
|
-
<project
|
|
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(
|
|
136
|
+
fs.rmSync(file, { force: true });
|
|
70
137
|
return true;
|
|
71
138
|
}
|
|
72
|
-
/* rename a task by moving
|
|
73
|
-
<project
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
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(
|
|
149
|
+
if (fs.existsSync(newFile))
|
|
82
150
|
throw new Error(`task: target id "${newId}" already exists`);
|
|
83
|
-
fs.
|
|
84
|
-
|
|
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
|
|
89
|
-
as "YYYY-MM-DD HH:MM", otherwise it is left undefined */
|
|
90
|
-
static list(verbose = false) {
|
|
91
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
120
|
-
|
|
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(
|
|
130
|
-
removed.push(
|
|
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
|
|
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
|
|
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 `
|
|
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("
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
22
|
-
"@typescript-eslint/eslint-plugin": "8.
|
|
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.
|
|
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.
|
|
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",
|
|
@@ -16,12 +16,15 @@ Artifact Meta Information
|
|
|
16
16
|
(SAS)", "Architecture Description", or "Architecture Decision
|
|
17
17
|
Record (ADR)".
|
|
18
18
|
|
|
19
|
-
- `
|
|
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`, `
|
|
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
|
-
|
|
3
|
-
|
|
2
|
+
Task
|
|
3
|
+
----
|
|
4
4
|
|
|
5
|
-
Every *task
|
|
5
|
+
Every *task* uses a strict and fixed format:
|
|
6
6
|
|
|
7
7
|
<format>
|
|
8
8
|
|
|
9
|
-
#
|
|
9
|
+
# TASK <task-id/>: <title/>
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Created: <timestamp-created/>
|
|
12
|
+
Modified: <timestamp-modified/>
|
|
12
13
|
|
|
13
|
-
##
|
|
14
|
+
## CONTEXT
|
|
14
15
|
|
|
15
|
-
-
|
|
16
|
+
- **WHAT**: <summary-what/>
|
|
16
17
|
|
|
17
|
-
-
|
|
18
|
+
- **WHY**: <summary-why/>
|
|
18
19
|
|
|
19
|
-
##
|
|
20
|
+
## CHANGES
|
|
20
21
|
|
|
21
|
-
-
|
|
22
|
+
- [...]
|
|
22
23
|
|
|
23
|
-
-
|
|
24
|
+
- [...]
|
|
24
25
|
|
|
25
|
-
##
|
|
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
|
|
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
|
|
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
|
|
72
|
+
after about 100 characters per line for better subsequent
|
|
72
73
|
manual editing.
|
|
73
74
|
|
package/plugin/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.
|
|
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.
|
|
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-
|
|
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
|
-
●
|
|
210
|
-
○
|
|
211
|
-
○
|
|
212
|
-
○
|
|
213
|
-
⊕
|
|
214
|
-
⊖
|
|
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 `##
|
|
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
|
|
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
|
-
|
|
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
|
|