@rse/ase 0.9.5 → 0.9.6

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.
@@ -0,0 +1,331 @@
1
+ /*
2
+ ** Agentic Software Engineering (ASE)
3
+ ** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
+ ** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
5
+ */
6
+ import path from "node:path";
7
+ import fs from "node:fs";
8
+ import picomatch from "picomatch";
9
+ import { isScalar } from "yaml";
10
+ import { z } from "zod";
11
+ import { Config, configSchema } from "./ase-config.js";
12
+ import { Task } from "./ase-task.js";
13
+ /* the recognized artifact kinds, in descending precedence order;
14
+ "othr" is the implicit catch-all and is always resolved last */
15
+ export const artifactKinds = ["spec", "arch", "code", "docs", "infr", "othr"];
16
+ /* the five configured kinds (i.e. all kinds except the implicit "othr") */
17
+ const configuredKinds = ["spec", "arch", "code", "docs", "infr"];
18
+ /* reusable functionality: resolve artifact kinds to project-relative
19
+ file lists, driven by the "project.artifact.<kind>" configuration */
20
+ export class Artifact {
21
+ /* validate a requested kind against the known set */
22
+ static validateKind(kind) {
23
+ if (!artifactKinds.includes(kind))
24
+ throw new Error(`artifact: unknown kind "${kind}" ` +
25
+ `(expected one of: ${artifactKinds.join(", ")})`);
26
+ return kind;
27
+ }
28
+ /* translate a single ".gitignore" line into a picomatch-backed rule,
29
+ honoring the anchored-vs-floating, directory-only, and negation
30
+ semantics of ".gitignore" patterns */
31
+ static compileIgnoreRule(line) {
32
+ let pattern = line.trim();
33
+ if (pattern === "" || pattern.startsWith("#"))
34
+ return null;
35
+ let negated = false;
36
+ if (pattern.startsWith("!")) {
37
+ negated = true;
38
+ pattern = pattern.slice(1);
39
+ }
40
+ let dirOnly = false;
41
+ if (pattern.endsWith("/")) {
42
+ dirOnly = true;
43
+ pattern = pattern.slice(0, -1);
44
+ }
45
+ const anchored = pattern.includes("/");
46
+ if (pattern.startsWith("/"))
47
+ pattern = pattern.slice(1);
48
+ const glob = anchored ? pattern : `**/${pattern}`;
49
+ const isMatch = picomatch(glob, { dot: true });
50
+ return { matcher: (p) => isMatch(p), negated, dirOnly };
51
+ }
52
+ /* load the ".gitignore" rules located directly in a directory */
53
+ static loadIgnoreRules(dir) {
54
+ const file = path.join(dir, ".gitignore");
55
+ if (!fs.existsSync(file))
56
+ return [];
57
+ const rules = [];
58
+ for (const line of fs.readFileSync(file, "utf8").split(/\r?\n/)) {
59
+ const rule = Artifact.compileIgnoreRule(line);
60
+ if (rule !== null)
61
+ rules.push(rule);
62
+ }
63
+ return rules;
64
+ }
65
+ /* decide whether a project-relative path is ignored by the given
66
+ ordered ".gitignore" rule set (last matching rule wins) */
67
+ static isIgnored(rel, isDir, rules) {
68
+ let ignored = false;
69
+ for (const rule of rules) {
70
+ if (rule.dirOnly && !isDir)
71
+ continue;
72
+ if (rule.matcher(rel))
73
+ ignored = !rule.negated;
74
+ }
75
+ return ignored;
76
+ }
77
+ /* build the file universe by walking the project tree from the
78
+ project root, honoring ".gitignore" rules (root plus nested) and
79
+ always pruning ".git". Yields POSIX project-relative, sorted,
80
+ de-duplicated file paths */
81
+ static universe() {
82
+ const root = Task.projectRoot();
83
+ const files = new Set();
84
+ const walk = (dir, relDir, inherited) => {
85
+ const rules = [...inherited, ...Artifact.loadIgnoreRules(dir)];
86
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
87
+ if (entry.name === ".git")
88
+ continue;
89
+ const rel = relDir === "" ? entry.name : `${relDir}/${entry.name}`;
90
+ const isDir = entry.isDirectory();
91
+ if (Artifact.isIgnored(rel, isDir, rules))
92
+ continue;
93
+ if (isDir)
94
+ walk(path.join(dir, entry.name), rel, rules);
95
+ else if (entry.isFile())
96
+ files.add(rel);
97
+ }
98
+ };
99
+ walk(root, "", []);
100
+ return [...files].sort((a, b) => a.localeCompare(b));
101
+ }
102
+ /* raw-resolve a single kind's configuration spec against the file
103
+ universe via "seed-then-mutate" miniglob semantics: the spec is a
104
+ whitespace-separated list of tokens. */
105
+ static rawResolve(spec, all) {
106
+ const tokens = spec.split(/\s+/).filter((t) => t !== "");
107
+ if (tokens.length === 0)
108
+ return new Set();
109
+ const result = new Set(tokens[0].startsWith("!") ? all : []);
110
+ for (const token of tokens) {
111
+ const negated = token.startsWith("!");
112
+ const glob = negated ? token.slice(1) : token;
113
+ if (glob === "")
114
+ continue;
115
+ const isMatch = picomatch(glob, { dot: true });
116
+ for (const file of all) {
117
+ if (!isMatch(file))
118
+ continue;
119
+ if (negated)
120
+ result.delete(file);
121
+ else
122
+ result.add(file);
123
+ }
124
+ }
125
+ return result;
126
+ }
127
+ /* read a single scalar configuration value as a plain string */
128
+ static configString(cfg, key) {
129
+ const val = cfg.get(key);
130
+ if (val === undefined)
131
+ return "";
132
+ return String(isScalar(val) ? val.value : val);
133
+ }
134
+ /* read the configured "basedir" anchor and "files" miniglob spec
135
+ for a single kind; "basedir" is project-root-relative (POSIX,
136
+ "" ≡ project root) and "files" resolves relative to "basedir" */
137
+ static spec(log, kind) {
138
+ const cfg = new Config("config", configSchema, log);
139
+ cfg.read();
140
+ const basedir = Artifact.configString(cfg, `project.artifact.${kind}.basedir`)
141
+ .replace(/\\/g, "/").replace(/^\/+|\/+$/g, "")
142
+ .replace(/^\.$/, "");
143
+ const files = Artifact.configString(cfg, `project.artifact.${kind}.files`);
144
+ return { basedir, files };
145
+ }
146
+ /* raw-resolve a single kind's "basedir"/"files" spec against the
147
+ file universe: the "files" miniglob resolves relative to
148
+ "basedir", then matches are re-prefixed with "basedir" to stay
149
+ project-relative */
150
+ static resolveKind(basedir, files, all) {
151
+ if (basedir === "")
152
+ return Artifact.rawResolve(files, all);
153
+ const prefix = `${basedir}/`;
154
+ const local = all
155
+ .filter((file) => file.startsWith(prefix))
156
+ .map((file) => file.slice(prefix.length));
157
+ const result = new Set();
158
+ for (const file of Artifact.rawResolve(files, local))
159
+ result.add(`${prefix}${file}`);
160
+ return result;
161
+ }
162
+ /* resolve the requested kinds to project-relative file lists */
163
+ static list(log, kinds) {
164
+ const all = Artifact.universe();
165
+ /* raw-resolve all five configured kinds */
166
+ const raw = new Map();
167
+ for (const kind of configuredKinds) {
168
+ const { basedir, files } = Artifact.spec(log, kind);
169
+ raw.set(kind, Artifact.resolveKind(basedir, files, all));
170
+ }
171
+ /* partition the universe by descending precedence */
172
+ const claimed = new Set();
173
+ const part = new Map();
174
+ for (const kind of configuredKinds) {
175
+ const own = [];
176
+ for (const file of raw.get(kind).size > 0 ? all : []) {
177
+ if (raw.get(kind).has(file) && !claimed.has(file)) {
178
+ own.push(file);
179
+ claimed.add(file);
180
+ }
181
+ }
182
+ part.set(kind, own);
183
+ }
184
+ part.set("othr", all.filter((file) => !claimed.has(file)));
185
+ /* project onto the requested kinds */
186
+ return kinds.map((kind) => ({
187
+ kind,
188
+ files: part.get(kind) ?? []
189
+ }));
190
+ }
191
+ /* resolve a base-relative "filename" within a kind's "basedir" to a
192
+ project-root-relative POSIX path; the implicit "othr" catch-all
193
+ has no configured "basedir" and is therefore rejected */
194
+ static name(log, kind, filename) {
195
+ if (kind === "othr")
196
+ throw new Error("artifact: kind \"othr\" has no configured basedir");
197
+ const file = filename.replace(/\\/g, "/").replace(/^\/+/, "");
198
+ if (file === "")
199
+ throw new Error("artifact: filename must not be empty");
200
+ const { basedir } = Artifact.spec(log, kind);
201
+ return basedir === "" ? file : `${basedir}/${file}`;
202
+ }
203
+ }
204
+ /* CLI command "ase artifact" */
205
+ export default class ArtifactCommand {
206
+ log;
207
+ constructor(log) {
208
+ this.log = log;
209
+ }
210
+ /* register commands */
211
+ register(program) {
212
+ /* register CLI top-level command "ase artifact" */
213
+ const artifact = program
214
+ .command("artifact")
215
+ .description("Resolve project artifact kinds to project-relative file lists")
216
+ .action(() => {
217
+ artifact.outputHelp();
218
+ process.exit(1);
219
+ });
220
+ /* register CLI sub-command "ase artifact list" */
221
+ artifact
222
+ .command("list")
223
+ .description("Resolve one or more artifact kinds to project-relative file paths")
224
+ .option("--kind <kinds>", "comma-separated list of artifact kinds " +
225
+ `(${artifactKinds.join("|")})`, artifactKinds.join(","))
226
+ .action((opts) => {
227
+ const kinds = opts.kind.split(",").map((k) => Artifact.validateKind(k.trim()));
228
+ const result = Artifact.list(this.log, kinds);
229
+ const single = result.length === 1;
230
+ for (const { kind, files } of result) {
231
+ if (!single)
232
+ process.stdout.write(`# ${kind}:\n`);
233
+ for (const file of files)
234
+ process.stdout.write(`- ${file}\n`);
235
+ }
236
+ process.exit(0);
237
+ });
238
+ /* register CLI sub-command "ase artifact name" */
239
+ artifact
240
+ .command("name")
241
+ .description("Resolve a base-relative filename within an artifact kind to a project-relative path")
242
+ .argument("<filename>", "base-relative filename within the kind's basedir")
243
+ .option("--kind <kind>", "artifact kind " +
244
+ `(${configuredKinds.join("|")})`, "code")
245
+ .action((filename, opts) => {
246
+ const kind = Artifact.validateKind(opts.kind.trim());
247
+ process.stdout.write(`${Artifact.name(this.log, kind, filename)}\n`);
248
+ process.exit(0);
249
+ });
250
+ }
251
+ }
252
+ /* MCP registration entry point for artifact tools */
253
+ export class ArtifactMCP {
254
+ log;
255
+ constructor(log) {
256
+ this.log = log;
257
+ }
258
+ /* register MCP tools */
259
+ register(mcp) {
260
+ mcp.registerTool("ase_artifact_list", {
261
+ title: "ASE artifact list",
262
+ description: "Resolve one or more artifact `kind`s to project-relative file lists. " +
263
+ "Recognized kinds are `spec`, `arch`, `code`, `docs`, `infr`, and `othr`. " +
264
+ "Returns an `artifacts` array of `{ kind, files }` objects. " +
265
+ "If `kind` is omitted or empty, all kinds are resolved.",
266
+ inputSchema: {
267
+ kind: z.array(z.string()).optional()
268
+ .describe("list of artifact kinds (`spec`, `arch`, `code`, `docs`, `infr`, " +
269
+ "`othr`); if omitted or empty, all kinds are resolved")
270
+ },
271
+ outputSchema: {
272
+ artifacts: z.array(z.object({
273
+ kind: z.string().describe("artifact kind"),
274
+ files: z.array(z.string()).describe("project-relative file paths for the kind")
275
+ })).describe("resolved artifacts, one entry per requested kind")
276
+ }
277
+ }, async (args) => {
278
+ try {
279
+ const requested = args.kind !== undefined && args.kind.length > 0 ?
280
+ args.kind : [...artifactKinds];
281
+ const kinds = requested.map((k) => Artifact.validateKind(k));
282
+ const result = { artifacts: Artifact.list(this.log, kinds) };
283
+ return {
284
+ structuredContent: result,
285
+ content: [{ type: "text", text: JSON.stringify(result) }]
286
+ };
287
+ }
288
+ catch (err) {
289
+ const message = err instanceof Error ? err.message : String(err);
290
+ return {
291
+ isError: true,
292
+ content: [{ type: "text", text: `ERROR: ${message}` }]
293
+ };
294
+ }
295
+ });
296
+ mcp.registerTool("ase_artifact_name", {
297
+ title: "ASE artifact name",
298
+ description: "Resolve a base-relative `filename` within an artifact `kind` to a project-relative path " +
299
+ "by prefixing it with the kind's configured `basedir`. " +
300
+ "Recognized kinds are `spec`, `arch`, `code`, `docs`, and `infr` " +
301
+ "(the implicit `othr` catch-all has no basedir and is rejected). " +
302
+ "If `kind` is omitted, it defaults to `code`. " +
303
+ "Returns the resolved path as `name`.",
304
+ inputSchema: {
305
+ kind: z.string().optional()
306
+ .describe("artifact kind (`spec`, `arch`, `code`, `docs`, `infr`); defaults to `code`"),
307
+ filename: z.string()
308
+ .describe("base-relative filename within the kind's basedir")
309
+ },
310
+ outputSchema: {
311
+ name: z.string().describe("project-relative file path")
312
+ }
313
+ }, async (args) => {
314
+ try {
315
+ const kind = Artifact.validateKind(args.kind ?? "code");
316
+ const result = { name: Artifact.name(this.log, kind, args.filename) };
317
+ return {
318
+ structuredContent: result,
319
+ content: [{ type: "text", text: JSON.stringify(result) }]
320
+ };
321
+ }
322
+ catch (err) {
323
+ const message = err instanceof Error ? err.message : String(err);
324
+ return {
325
+ isError: true,
326
+ content: [{ type: "text", text: `ERROR: ${message}` }]
327
+ };
328
+ }
329
+ });
330
+ }
331
+ }
package/dst/ase-config.js CHANGED
@@ -25,29 +25,41 @@ export const agentClassification = {
25
25
  /* classification presets */
26
26
  export const projectClassificationPresets = {
27
27
  vibe: {
28
+ "agent.persona": "writer",
28
29
  "project.id": "example",
29
30
  "project.name": "Example Project",
30
- "project.boxing": "black",
31
- "agent.persona": "writer"
31
+ "project.boxing": "black"
32
32
  },
33
33
  pro: {
34
+ "agent.persona": "engineer",
34
35
  "project.id": "example",
35
36
  "project.name": "Example Project",
36
- "project.boxing": "white",
37
- "agent.persona": "engineer"
37
+ "project.boxing": "white"
38
38
  },
39
39
  default: {
40
+ "agent.task": "default",
41
+ "agent.persona": "engineer",
40
42
  "project.id": "example",
41
43
  "project.name": "Example Project",
42
44
  "project.boxing": "white",
43
- "agent.persona": "engineer",
44
- "agent.task": "default"
45
+ "project.artifact.task.basedir": ".ase/task",
46
+ "project.artifact.task.files": "*.md",
47
+ "project.artifact.spec.basedir": "doc/spec",
48
+ "project.artifact.spec.files": "*.{md,txt}",
49
+ "project.artifact.arch.basedir": "doc/arch",
50
+ "project.artifact.arch.files": "*.{md,txt}",
51
+ "project.artifact.code.basedir": "src",
52
+ "project.artifact.code.files": "** !**/etc/** !**/{.gitignore,.npmignore,package.json}",
53
+ "project.artifact.docs.basedir": "doc",
54
+ "project.artifact.docs.files": "** **/{README,LICENSE,CHANGELOG}.{md,txt} !{spec,arch}/**",
55
+ "project.artifact.infr.basedir": "",
56
+ "project.artifact.infr.files": "**/{.github,.claude*,etc}/** **/{AGENTS.md,{package,tsconfig*}.json,.{git,npm}ignore}"
45
57
  },
46
58
  industry: {
59
+ "agent.persona": "engineer",
47
60
  "project.id": "example",
48
61
  "project.name": "Example Project",
49
- "project.boxing": "grey",
50
- "agent.persona": "engineer"
62
+ "project.boxing": "grey"
51
63
  }
52
64
  };
53
65
  /* hard-coded map: which scope kinds each variable may be SET on
@@ -55,7 +67,19 @@ export const projectClassificationPresets = {
55
67
  keys absent from this map default to all non-"default" scope kinds */
56
68
  export const configWritableScopes = {
57
69
  "agent.task": ["session"],
58
- "agent.skill": ["session"]
70
+ "agent.skill": ["session"],
71
+ "project.artifact.task.basedir": ["user", "project"],
72
+ "project.artifact.task.files": ["user", "project"],
73
+ "project.artifact.spec.basedir": ["user", "project"],
74
+ "project.artifact.spec.files": ["user", "project"],
75
+ "project.artifact.arch.basedir": ["user", "project"],
76
+ "project.artifact.arch.files": ["user", "project"],
77
+ "project.artifact.code.basedir": ["user", "project"],
78
+ "project.artifact.code.files": ["user", "project"],
79
+ "project.artifact.docs.basedir": ["user", "project"],
80
+ "project.artifact.docs.files": ["user", "project"],
81
+ "project.artifact.infr.basedir": ["user", "project"],
82
+ "project.artifact.infr.files": ["user", "project"]
59
83
  };
60
84
  /* default set of scope kinds writable for any unrestricted key */
61
85
  const configWritableScopesDefault = ["user", "project", "task", "session"];
@@ -130,7 +154,15 @@ export const configSchema = v.nullish(v.strictObject({
130
154
  project: v.optional(v.strictObject({
131
155
  id: v.optional(v.pipe(v.string(), v.minLength(1))),
132
156
  name: v.optional(v.pipe(v.string(), v.minLength(1))),
133
- boxing: v.optional(v.picklist(projectClassification.boxing))
157
+ boxing: v.optional(v.picklist(projectClassification.boxing)),
158
+ artifact: v.optional(v.strictObject({
159
+ spec: v.optional(v.strictObject({ basedir: v.optional(v.string()), files: v.optional(v.string()) })),
160
+ arch: v.optional(v.strictObject({ basedir: v.optional(v.string()), files: v.optional(v.string()) })),
161
+ code: v.optional(v.strictObject({ basedir: v.optional(v.string()), files: v.optional(v.string()) })),
162
+ docs: v.optional(v.strictObject({ basedir: v.optional(v.string()), files: v.optional(v.string()) })),
163
+ infr: v.optional(v.strictObject({ basedir: v.optional(v.string()), files: v.optional(v.string()) })),
164
+ task: v.optional(v.strictObject({ basedir: v.optional(v.string()), files: v.optional(v.string()) }))
165
+ }))
134
166
  })),
135
167
  agent: v.optional(v.strictObject({
136
168
  persona: v.optional(v.picklist(agentClassification.persona)),
@@ -614,6 +646,20 @@ export default class ConfigCommand {
614
646
  cfg.write();
615
647
  });
616
648
  });
649
+ /* register CLI sub-command "ase config delete" */
650
+ configCmd
651
+ .command("delete")
652
+ .description("delete the value at a dotted configuration key")
653
+ .argument("<key>", "configuration key (dotted path)")
654
+ .action((key, _opts, cmd) => {
655
+ const scope = parseScope(cmd.optsWithGlobals().scope);
656
+ const cfg = new Config("config", configSchema, this.log, scope);
657
+ cfg.lock(() => {
658
+ cfg.read();
659
+ cfg.delete(key);
660
+ cfg.write();
661
+ });
662
+ });
617
663
  }
618
664
  }
619
665
  /* MCP registration entry point for layered YAML configuration access */
@@ -18,6 +18,7 @@ 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 { ArtifactMCP } from "./ase-artifact.js";
21
22
  import { KVMCP } from "./ase-kv.js";
22
23
  import PersonaMCP from "./ase-persona.js";
23
24
  import { TimestampMCP } from "./ase-timestamp.js";
@@ -236,6 +237,7 @@ export default class ServiceCommand {
236
237
  new ServiceMCP({ projectId: ctx.projectId, port: ctx.port, startTime }).register(mcp);
237
238
  new DiagramMCP().register(mcp);
238
239
  new TaskMCP(this.log).register(mcp);
240
+ new ArtifactMCP(this.log).register(mcp);
239
241
  new KVMCP().register(mcp);
240
242
  new PersonaMCP(this.log).register(mcp);
241
243
  new TimestampMCP().register(mcp);
package/dst/ase-task.js CHANGED
@@ -7,11 +7,13 @@ 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";
13
14
  /* reusable functionality: persisted task plans under
14
- <project>/.ase/task/<id>/plan.md */
15
+ <project>/<basedir>/TASK-<id>.md (driven by the
16
+ "project.artifact.task.{basedir,files}" configuration) */
15
17
  export class Task {
16
18
  /* validate the task id to keep it safe as a filename component */
17
19
  static validateId(id) {
@@ -34,100 +36,173 @@ export class Task {
34
36
  }
35
37
  return process.cwd();
36
38
  }
39
+ /* read the configured "basedir" anchor and "files" miniglob spec for
40
+ task storage; "basedir" is project-root-relative (POSIX, defaults
41
+ to ".ase/task") and "files" constrains the task filenames
42
+ (defaults to "*.md") */
43
+ static spec(log) {
44
+ const cfg = new Config("config", configSchema, log);
45
+ cfg.read();
46
+ const read = (key) => {
47
+ const val = cfg.get(key);
48
+ if (val === undefined)
49
+ return "";
50
+ return String(isScalar(val) ? val.value : val);
51
+ };
52
+ const basedir = (read("project.artifact.task.basedir") || ".ase/task")
53
+ .replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
54
+ const files = read("project.artifact.task.files") || "*.md";
55
+ return { basedir, files };
56
+ }
37
57
  /* resolve the on-disk base directory for task storage */
38
- static baseDir() {
39
- return path.join(Task.projectRoot(), ".ase", "task");
58
+ static baseDir(log) {
59
+ return path.join(Task.projectRoot(), Task.spec(log).basedir);
40
60
  }
41
- /* resolve the on-disk path for a given task id */
42
- static path(id) {
61
+ /* resolve the on-disk path for a given task id; as a side effect,
62
+ eagerly migrate any legacy <basedir>/<id>/plan.md files to the
63
+ current <basedir>/TASK-<id>.md layout on first access (guarded by
64
+ a cheap check, so it is a no-op once the store is migrated) */
65
+ static path(log, id) {
43
66
  Task.validateId(id);
44
- return path.join(Task.baseDir(), id, "plan.md");
67
+ if (Task.needsMigration(log))
68
+ Task.migrateAll(log);
69
+ return path.join(Task.baseDir(log), `TASK-${id}.md`);
70
+ }
71
+ /* cheaply check whether any legacy <basedir>/<id>/plan.md file still
72
+ exists in the task base directory and thus needs migration */
73
+ static needsMigration(log) {
74
+ const dir = Task.baseDir(log);
75
+ if (!fs.existsSync(dir))
76
+ return false;
77
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
78
+ if (!entry.isDirectory() || !/^[A-Za-z0-9-]+$/.test(entry.name))
79
+ continue;
80
+ if (fs.existsSync(path.join(dir, entry.name, "plan.md")))
81
+ return true;
82
+ }
83
+ return false;
84
+ }
85
+ /* migrate all legacy <basedir>/<id>/plan.md task files to the current
86
+ <basedir>/TASK-<id>.md layout; an existing TASK-<id>.md is never
87
+ overwritten; the now-empty <id>/ directory is removed afterwards;
88
+ returns the list of migrated task ids in lexicographic order */
89
+ static migrateAll(log) {
90
+ const dir = Task.baseDir(log);
91
+ if (!fs.existsSync(dir))
92
+ return [];
93
+ const migrated = [];
94
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
95
+ if (!entry.isDirectory() || !/^[A-Za-z0-9-]+$/.test(entry.name))
96
+ continue;
97
+ const id = entry.name;
98
+ const oldFile = path.join(dir, id, "plan.md");
99
+ const newFile = path.join(dir, `TASK-${id}.md`);
100
+ if (!fs.existsSync(oldFile))
101
+ continue;
102
+ if (fs.existsSync(newFile)) {
103
+ log.write("warning", `task: not migrating "${id}": target "TASK-${id}.md" already exists`);
104
+ continue;
105
+ }
106
+ fs.renameSync(oldFile, newFile);
107
+ fs.rmSync(path.join(dir, id), { recursive: true, force: true });
108
+ migrated.push(id);
109
+ }
110
+ migrated.sort((a, b) => a.localeCompare(b));
111
+ return migrated;
45
112
  }
46
113
  /* load a task; returns empty string if no task exists */
47
- static load(id) {
48
- const file = Task.path(id);
114
+ static load(log, id) {
115
+ const file = Task.path(log, id);
49
116
  if (!fs.existsSync(file))
50
117
  return "";
51
118
  return fs.readFileSync(file, "utf8");
52
119
  }
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) {
120
+ /* save a task as UTF-8 text under the given id into the
121
+ <project>/<basedir>/TASK-<id>.md file */
122
+ static save(log, id, text) {
57
123
  if (typeof text !== "string")
58
124
  throw new Error("task: text must be a string");
59
- const file = Task.path(id);
125
+ const file = Task.path(log, id);
60
126
  fs.mkdirSync(path.dirname(file), { recursive: true });
61
127
  fs.writeFileSync(file, text, "utf8");
62
128
  }
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);
129
+ /* delete a task by id; removes the single
130
+ <project>/<basedir>/TASK-<id>.md file; returns true if a task existed */
131
+ static delete(log, id) {
132
+ const file = Task.path(log, id);
67
133
  if (!fs.existsSync(file))
68
134
  return false;
69
- fs.rmSync(path.dirname(file), { recursive: true, force: true });
135
+ fs.rmSync(file, { force: true });
70
136
  return true;
71
137
  }
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))
138
+ /* rename a task by moving its <project>/<basedir>/TASK-<oldId>.md file
139
+ to <project>/<basedir>/TASK-<newId>.md; the embedded
140
+ "# TASK <id>:" heading inside the plan content is rewritten to
141
+ the new id; returns true on success, false if the source task does
142
+ not exist; throws if the target id already exists */
143
+ static rename(log, oldId, newId) {
144
+ const oldFile = Task.path(log, oldId);
145
+ const newFile = Task.path(log, newId);
146
+ if (!fs.existsSync(oldFile))
80
147
  return false;
81
- if (fs.existsSync(newDir))
148
+ if (fs.existsSync(newFile))
82
149
  throw new Error(`task: target id "${newId}" already exists`);
83
- fs.mkdirSync(path.dirname(newDir), { recursive: true });
84
- fs.renameSync(oldDir, newDir);
150
+ const text = fs.readFileSync(oldFile, "utf8");
151
+ const updated = text.replace(/(^#\s+(?:✪\s+)?TASK\s+)[A-Za-z0-9-]+(\s*:)/m, `$1${newId}$2`);
152
+ fs.mkdirSync(path.dirname(newFile), { recursive: true });
153
+ fs.writeFileSync(newFile, updated, "utf8");
154
+ fs.rmSync(oldFile, { force: true });
85
155
  return true;
86
156
  }
87
157
  /* 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();
158
+ each entry's `mtime` is set to the task file's modification time
159
+ formatted as "YYYY-MM-DD HH:MM", otherwise it is left undefined */
160
+ static list(log, verbose = false) {
161
+ if (Task.needsMigration(log))
162
+ Task.migrateAll(log);
163
+ const { basedir, files } = Task.spec(log);
164
+ const dir = path.join(Task.projectRoot(), basedir);
92
165
  if (!fs.existsSync(dir))
93
166
  return [];
167
+ const isMatch = picomatch(files, { dot: true });
94
168
  const out = [];
95
169
  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))
170
+ const m = /^TASK-([A-Za-z0-9-]+)\.md$/.exec(entry);
171
+ if (m === null || !isMatch(entry))
100
172
  continue;
173
+ const file = path.join(dir, entry);
101
174
  const st = fs.statSync(file);
102
175
  if (!st.isFile())
103
176
  continue;
104
177
  const mtime = verbose ? DateTime.fromJSDate(st.mtime).toFormat("yyyy-LL-dd HH:mm") : undefined;
105
- out.push({ id: entry, mtime });
178
+ out.push({ id: m[1], mtime });
106
179
  }
107
180
  out.sort((a, b) => a.id.localeCompare(b.id));
108
181
  return out;
109
182
  }
110
183
  /* purge tasks whose modification time is older than the given cutoff in
111
184
  milliseconds; returns the list of removed task ids */
112
- static purge(maxAgeMs) {
113
- const dir = Task.baseDir();
185
+ static purge(log, maxAgeMs) {
186
+ if (Task.needsMigration(log))
187
+ Task.migrateAll(log);
188
+ const { basedir, files } = Task.spec(log);
189
+ const dir = path.join(Task.projectRoot(), basedir);
114
190
  if (!fs.existsSync(dir))
115
191
  return [];
192
+ const isMatch = picomatch(files, { dot: true });
116
193
  const cutoff = Date.now() - maxAgeMs;
117
194
  const removed = [];
118
195
  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))
196
+ const m = /^TASK-([A-Za-z0-9-]+)\.md$/.exec(entry);
197
+ if (m === null || !isMatch(entry))
124
198
  continue;
199
+ const file = path.join(dir, entry);
125
200
  const st = fs.statSync(file);
126
201
  if (!st.isFile())
127
202
  continue;
128
203
  if (st.mtimeMs < cutoff) {
129
- fs.rmSync(sub, { recursive: true, force: true });
130
- removed.push(entry);
204
+ fs.rmSync(file, { force: true });
205
+ removed.push(m[1]);
131
206
  }
132
207
  }
133
208
  return removed;
@@ -173,7 +248,7 @@ export default class TaskCommand {
173
248
  /* register CLI top-level command "ase task" */
174
249
  const task = program
175
250
  .command("task")
176
- .description("Manage persisted tasks under <project>/.ase/task/<id>/plan.md")
251
+ .description("Manage persisted tasks under <project>/<basedir>/TASK-<id>.md")
177
252
  .action(() => {
178
253
  task.outputHelp();
179
254
  process.exit(1);
@@ -182,9 +257,9 @@ export default class TaskCommand {
182
257
  task
183
258
  .command("list")
184
259
  .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)")
260
+ .option("-v, --verbose", "also show the task file modification time as (YYYY-MM-DD HH:MM)")
186
261
  .action((opts) => {
187
- const items = Task.list(opts.verbose ?? false);
262
+ const items = Task.list(this.log, opts.verbose ?? false);
188
263
  for (const item of items) {
189
264
  if (opts.verbose)
190
265
  process.stdout.write(`${item.id}\t(${item.mtime})\n`);
@@ -199,7 +274,7 @@ export default class TaskCommand {
199
274
  .description("Load a task by id and write it to stdout")
200
275
  .argument("<id>", "Task identifier")
201
276
  .action((id) => {
202
- const text = Task.load(id);
277
+ const text = Task.load(this.log, id);
203
278
  process.stdout.write(text);
204
279
  process.exit(0);
205
280
  });
@@ -209,7 +284,7 @@ export default class TaskCommand {
209
284
  .description("Edit a task by id with $EDITOR")
210
285
  .argument("<id>", "Task identifier")
211
286
  .action((id) => {
212
- const file = Task.path(id);
287
+ const file = Task.path(this.log, id);
213
288
  const editor = process.env.EDITOR ?? process.env.VISUAL ?? "vi";
214
289
  fs.mkdirSync(path.dirname(file), { recursive: true });
215
290
  if (!fs.existsSync(file))
@@ -225,7 +300,7 @@ export default class TaskCommand {
225
300
  .argument("<id>", "Task identifier")
226
301
  .action(async (id) => {
227
302
  const text = await readStdin();
228
- Task.save(id, text);
303
+ Task.save(this.log, id, text);
229
304
  this.log.write("info", `task: saved "${id}"`);
230
305
  process.exit(0);
231
306
  });
@@ -235,7 +310,7 @@ export default class TaskCommand {
235
310
  .description("Delete a task by id")
236
311
  .argument("<id>", "Task identifier")
237
312
  .action((id) => {
238
- const removed = Task.delete(id);
313
+ const removed = Task.delete(this.log, id);
239
314
  if (removed)
240
315
  this.log.write("info", `task: removed "${id}"`);
241
316
  else
@@ -249,7 +324,7 @@ export default class TaskCommand {
249
324
  .argument("<old>", "Old task identifier")
250
325
  .argument("<new>", "New task identifier")
251
326
  .action((oldId, newId) => {
252
- const renamed = Task.rename(oldId, newId);
327
+ const renamed = Task.rename(this.log, oldId, newId);
253
328
  if (renamed)
254
329
  this.log.write("info", `task: renamed "${oldId}" to "${newId}"`);
255
330
  else
@@ -276,7 +351,7 @@ export default class TaskCommand {
276
351
  unit === "d" ? day :
277
352
  unit === "m" ? month :
278
353
  year;
279
- const removed = Task.purge(n * factor);
354
+ const removed = Task.purge(this.log, n * factor);
280
355
  if (removed.length === 0)
281
356
  this.log.write("info", "task: no tasks to purge");
282
357
  else
@@ -300,7 +375,7 @@ export class TaskMCP {
300
375
  description: "List all persisted tasks. " +
301
376
  "Returns a `tasks` array (in lexicographic `id` order) where each item has the " +
302
377
  "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`). " +
378
+ "(last modification time of the task's `TASK-<id>.md` file, formatted as `YYYY-MM-DD HH:MM`). " +
304
379
  "Returns an empty array if no tasks exist.",
305
380
  inputSchema: {
306
381
  verbose: z.boolean().optional()
@@ -310,13 +385,13 @@ export class TaskMCP {
310
385
  tasks: z.array(z.object({
311
386
  id: z.string().describe("task identifier"),
312
387
  mtime: z.string().optional()
313
- .describe("plan.md modification time (`YYYY-MM-DD HH:MM`); only present if `verbose` is true")
388
+ .describe("`TASK-<id>.md` modification time (`YYYY-MM-DD HH:MM`); only present if `verbose` is true")
314
389
  })).describe("all persisted tasks in lexicographic id order")
315
390
  }
316
391
  }, async (args) => {
317
392
  try {
318
393
  const verbose = args.verbose ?? false;
319
- const items = Task.list(verbose);
394
+ const items = Task.list(this.log, verbose);
320
395
  const tasks = verbose ?
321
396
  items.map((item) => ({ id: item.id, mtime: item.mtime ?? "" })) :
322
397
  items.map((item) => ({ id: item.id }));
@@ -345,7 +420,7 @@ export class TaskMCP {
345
420
  }
346
421
  }, async (args) => {
347
422
  try {
348
- const text = Task.load(args.id);
423
+ const text = Task.load(this.log, args.id);
349
424
  return {
350
425
  content: [{ type: "text", text }]
351
426
  };
@@ -371,7 +446,7 @@ export class TaskMCP {
371
446
  }
372
447
  }, async (args) => {
373
448
  try {
374
- Task.save(args.id, args.text);
449
+ Task.save(this.log, args.id, args.text);
375
450
  return {
376
451
  content: [{ type: "text", text: `task_save: OK: saved task "${args.id}"` }]
377
452
  };
@@ -395,7 +470,7 @@ export class TaskMCP {
395
470
  }
396
471
  }, async (args) => {
397
472
  try {
398
- const removed = Task.delete(args.id);
473
+ const removed = Task.delete(this.log, args.id);
399
474
  const msg = removed ?
400
475
  `task_delete: OK: removed task "${args.id}"` :
401
476
  `task_delete: WARNING: no task "${args.id}" to remove`;
@@ -415,7 +490,7 @@ export class TaskMCP {
415
490
  mcp.registerTool("ase_task_rename", {
416
491
  title: "ASE task rename",
417
492
  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. " +
493
+ "task `TASK-<id>.md` file. Returns a status `text` indicating whether the rename succeeded. " +
419
494
  "Fails with an error if the target id already exists.",
420
495
  inputSchema: {
421
496
  old: z.string()
@@ -425,7 +500,7 @@ export class TaskMCP {
425
500
  }
426
501
  }, async (args) => {
427
502
  try {
428
- const renamed = Task.rename(args.old, args.new);
503
+ const renamed = Task.rename(this.log, args.old, args.new);
429
504
  const msg = renamed ?
430
505
  `task_rename: OK: renamed task "${args.old}" to "${args.new}"` :
431
506
  `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.6",
10
10
  "license": "GPL-3.0-only",
11
11
  "author": {
12
12
  "name": "Dr. Ralf S. Engelschall",
@@ -37,7 +37,8 @@
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",
@@ -60,7 +61,8 @@
60
61
  "proper-lockfile": "4.1.2",
61
62
  "write-file-atomic": "8.0.0",
62
63
  "pacote": "21.5.0",
63
- "ofetch": "1.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.6",
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.6",
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,14 +1,15 @@
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
14
  ## ※ CONTEXT
14
15
 
@@ -30,7 +31,7 @@ Every *task plan* uses a strict and fixed format:
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.
@@ -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.6",
10
10
  "license": "GPL-3.0-only",
11
11
  "author": {
12
12
  "name": "Dr. Ralf S. Engelschall",
@@ -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
  ---------
@@ -38,7 +38,7 @@ to `true`, <getopt-option-dry/> to `true`, and <getopt-option-next/> to
38
38
  <request><getopt-arguments/></request>
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
  ---------
@@ -38,7 +38,7 @@ to `true`, <getopt-option-dry/> to `true`, and <getopt-option-next/> to
38
38
  <problem><getopt-arguments/></problem>
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
  ---------
@@ -30,7 +30,7 @@ Condense a Task Plan
30
30
  semantics exactly.
31
31
  </objective>
32
32
 
33
- @${CLAUDE_SKILL_DIR}/../../meta/ase-format-plan.md
33
+ @${CLAUDE_SKILL_DIR}/../../meta/ase-format-task.md
34
34
 
35
35
  Procedure
36
36
  ---------
@@ -17,10 +17,6 @@ session context) is used. When the deleted task is the current task
17
17
  and not the `default` task, the current task id is automatically
18
18
  switched back to `default`.
19
19
 
20
- The task plan is stored in `.ase/tasks/`*id*`/plan.md` files of the
21
- project and can be alternatively deleted with the
22
- "`ase task delete` *id*" command from outside the agent tool.
23
-
24
20
  ## ARGUMENTS
25
21
 
26
22
  *id*:
@@ -34,7 +34,7 @@ continued refinement, finalization, or hand-off to implementation or
34
34
  preflight.
35
35
  </objective>
36
36
 
37
- @${CLAUDE_SKILL_DIR}/../../meta/ase-format-plan.md
37
+ @${CLAUDE_SKILL_DIR}/../../meta/ase-format-task.md
38
38
 
39
39
  Procedure
40
40
  ---------
@@ -271,7 +271,7 @@ Set <content-dirty>true</content-dirty>.
271
271
  </template>
272
272
  </if>
273
273
 
274
- 4. <if condition="<content/> does not contain '# ✪ TASK PLAN:' AND <instruction/> is empty">
274
+ 4. <if condition="<content/> does not contain '# ✪ TASK' AND <instruction/> is empty">
275
275
  Set <instruction><content/></instruction> (set instruction to content).
276
276
  Set <content></content> (set content to empty).
277
277
  Set <content-dirty>true</content-dirty>.
@@ -327,9 +327,9 @@ Set <content-dirty>true</content-dirty>.
327
327
  Use the following <template/>:
328
328
 
329
329
  <template>
330
- <ase-tpl-head title="TASK-PLAN"/>
330
+ <ase-tpl-head title="TASK"/>
331
331
  <content/>
332
- <ase-tpl-foot title="TASK-PLAN"/>
332
+ <ase-tpl-foot title="TASK"/>
333
333
  </template>
334
334
 
335
335
  4. *Determine next step*:
@@ -19,10 +19,6 @@ through a *chat-driven loop*. The user steers each round via an
19
19
  interactive dialog that offers continued refinement, finalization, or
20
20
  hand-off to implementation or preflight.
21
21
 
22
- The task plan is stored in `.ase/tasks/`*id*`/plan.md` files of the
23
- project and can be alternatively edited with the "`ase task edit` *id*"
24
- command from outside the agent tool.
25
-
26
22
  ## OPTIONS
27
23
 
28
24
  `--plan`|`-p` *option*:
@@ -31,7 +31,7 @@ Interview the user relentlessly about every essential aspect of the
31
31
  task plan until reaching a shared understanding.
32
32
  </objective>
33
33
 
34
- @${CLAUDE_SKILL_DIR}/../../meta/ase-format-plan.md
34
+ @${CLAUDE_SKILL_DIR}/../../meta/ase-format-task.md
35
35
 
36
36
  Procedure
37
37
  ---------
@@ -27,10 +27,6 @@ lets the user pick via an interactive dialog. It honors checks for
27
27
  updated and persisted, and the user is offered a hand-off to editing,
28
28
  implementation, or preflight.
29
29
 
30
- The task plan is stored in `.ase/tasks/`*id*`/plan.md` files of the
31
- project and can be alternatively edited with the "`ase task edit` *id*"
32
- command from outside the agent tool.
33
-
34
30
  ## OPTIONS
35
31
 
36
32
  `--next`|`-n` *option*[,...]:
@@ -30,7 +30,7 @@ Implement a Task Plan
30
30
  with a corresponding, complete *change set*.
31
31
  </objective>
32
32
 
33
- @${CLAUDE_SKILL_DIR}/../../meta/ase-format-plan.md
33
+ @${CLAUDE_SKILL_DIR}/../../meta/ase-format-task.md
34
34
 
35
35
  Procedure
36
36
  ---------
@@ -12,12 +12,11 @@
12
12
 
13
13
  ## DESCRIPTION
14
14
 
15
- The `ase-task-implement` skill performs the *final implementation*
16
- of a task plan by modifying the corresponding *artifacts* with a
17
- complete *change set*. The plan is loaded from
18
- `.ase/tasks/`*id*`/plan.md`, and any optional `IMPLEMENTATION DRAFT`
19
- section produced by `ase-task-preflight` is used as a hint - the
20
- plain plan content always overrules the draft.
15
+ The `ase-task-implement` skill performs the *final implementation* of
16
+ a task plan by modifying the corresponding *artifacts* with a complete
17
+ *change set*. The plan is loaded and any optional `IMPLEMENTATION DRAFT`
18
+ section produced by `ase-task-preflight` is used as a hint - the plain
19
+ plan content always overrules the draft.
21
20
 
22
21
  If the task plan deliberately *omits* the `※ VERIFICATION` section
23
22
  (as produced by `ase-code-craft`, `ase-code-refactor`,
@@ -30,7 +30,7 @@ Preflight a Task Plan
30
30
  for a corresponding, *complete source code change set*.
31
31
  </objective>
32
32
 
33
- @${CLAUDE_SKILL_DIR}/../../meta/ase-format-plan.md
33
+ @${CLAUDE_SKILL_DIR}/../../meta/ase-format-task.md
34
34
 
35
35
  Procedure
36
36
  ---------
@@ -30,7 +30,7 @@ Reboot a Task Plan
30
30
  based on the existing *WHAT* and *WHY*.
31
31
  </objective>
32
32
 
33
- @${CLAUDE_SKILL_DIR}/../../meta/ase-format-plan.md
33
+ @${CLAUDE_SKILL_DIR}/../../meta/ase-format-task.md
34
34
 
35
35
  Procedure
36
36
  ---------