@rse/ase 0.9.55 → 0.9.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/dst/ase-config.js +72 -31
  2. package/dst/ase-getopt.js +3 -1
  3. package/dst/ase-hook.js +5 -3
  4. package/dst/ase-markdown.js +7 -4
  5. package/dst/ase-service.js +1 -1
  6. package/dst/ase-setup.js +4 -2
  7. package/dst/ase-statusline.js +6 -8
  8. package/dst/ase-task.js +37 -11
  9. package/package.json +5 -5
  10. package/plugin/.claude-plugin/plugin.json +1 -1
  11. package/plugin/.codex-plugin/plugin.json +1 -1
  12. package/plugin/.github/plugin/plugin.json +1 -1
  13. package/plugin/agents/ase-code-analyze.md +1 -1
  14. package/plugin/agents/ase-code-lint.md +4 -4
  15. package/plugin/agents/ase-meta-chat.md +1 -1
  16. package/plugin/agents/ase-meta-proximity.md +1 -1
  17. package/plugin/meta/ase-common-code.md +168 -1
  18. package/plugin/meta/ase-common-task.md +31 -13
  19. package/plugin/meta/ase-constitution.md +3 -3
  20. package/plugin/meta/ase-control.md +7 -7
  21. package/plugin/meta/ase-dialog.md +5 -4
  22. package/plugin/meta/ase-format-arch.md +6 -21
  23. package/plugin/meta/ase-format-meta.md +15 -2
  24. package/plugin/meta/ase-format-spec.md +9 -24
  25. package/plugin/meta/ase-format-task.md +2 -2
  26. package/plugin/meta/ase-getopt.md +1 -1
  27. package/plugin/meta/ase-persona.md +3 -3
  28. package/plugin/meta/ase-skill.md +14 -24
  29. package/plugin/meta/ase-tenets.md +5 -5
  30. package/plugin/package.json +2 -2
  31. package/plugin/skills/ase-arch-analyze/SKILL.md +2 -2
  32. package/plugin/skills/ase-arch-analyze/help.md +1 -1
  33. package/plugin/skills/ase-arch-discover/SKILL.md +9 -7
  34. package/plugin/skills/ase-code-analyze/SKILL.md +1 -1
  35. package/plugin/skills/ase-code-analyze/help.md +1 -1
  36. package/plugin/skills/ase-code-craft/SKILL.md +2 -153
  37. package/plugin/skills/ase-code-craft/help.md +1 -1
  38. package/plugin/skills/ase-code-explain/SKILL.md +3 -3
  39. package/plugin/skills/ase-code-insight/SKILL.md +9 -7
  40. package/plugin/skills/ase-code-lint/SKILL.md +7 -5
  41. package/plugin/skills/ase-code-refactor/SKILL.md +4 -155
  42. package/plugin/skills/ase-code-resolve/SKILL.md +48 -173
  43. package/plugin/skills/ase-code-resolve/help.md +15 -4
  44. package/plugin/skills/ase-docs-distill/SKILL.md +2 -2
  45. package/plugin/skills/ase-docs-distill/help.md +1 -1
  46. package/plugin/skills/ase-docs-proofread/SKILL.md +7 -3
  47. package/plugin/skills/ase-help-intent/SKILL.md +33 -9
  48. package/plugin/skills/ase-help-skill/SKILL.md +1 -1
  49. package/plugin/skills/ase-meta-brainstorm/SKILL.md +11 -2
  50. package/plugin/skills/ase-meta-changelog/SKILL.md +13 -8
  51. package/plugin/skills/ase-meta-chat/SKILL.md +9 -0
  52. package/plugin/skills/ase-meta-commit/SKILL.md +11 -1
  53. package/plugin/skills/ase-meta-compat/SKILL.md +2 -2
  54. package/plugin/skills/ase-meta-config/help.md +3 -2
  55. package/plugin/skills/ase-meta-diaboli/SKILL.md +12 -3
  56. package/plugin/skills/ase-meta-diff/SKILL.md +11 -13
  57. package/plugin/skills/ase-meta-eli5/SKILL.md +3 -3
  58. package/plugin/skills/ase-meta-eli5/help.md +1 -1
  59. package/plugin/skills/ase-meta-evaluate/SKILL.md +3 -3
  60. package/plugin/skills/ase-meta-quorum/SKILL.md +17 -17
  61. package/plugin/skills/ase-meta-quotes/SKILL.md +1 -1
  62. package/plugin/skills/ase-meta-review/SKILL.md +7 -5
  63. package/plugin/skills/ase-meta-review/help.md +2 -2
  64. package/plugin/skills/ase-meta-search/SKILL.md +18 -0
  65. package/plugin/skills/ase-meta-steelman/SKILL.md +11 -2
  66. package/plugin/skills/ase-meta-why/SKILL.md +15 -6
  67. package/plugin/skills/ase-meta-why/help.md +3 -3
  68. package/plugin/skills/ase-sync-export/SKILL.md +2 -1
  69. package/plugin/skills/ase-sync-export/help.md +1 -1
  70. package/plugin/skills/ase-sync-reconcile/SKILL.md +16 -2
  71. package/plugin/skills/ase-task-condense/SKILL.md +13 -67
  72. package/plugin/skills/ase-task-delete/SKILL.md +1 -1
  73. package/plugin/skills/ase-task-dissect/SKILL.md +5 -18
  74. package/plugin/skills/ase-task-edit/SKILL.md +49 -22
  75. package/plugin/skills/ase-task-grill/SKILL.md +31 -129
  76. package/plugin/skills/ase-task-id/SKILL.md +12 -2
  77. package/plugin/skills/ase-task-implement/SKILL.md +11 -7
  78. package/plugin/skills/ase-task-implement/help.md +1 -1
  79. package/plugin/skills/ase-task-list/SKILL.md +9 -0
  80. package/plugin/skills/ase-task-preflight/SKILL.md +17 -33
  81. package/plugin/skills/ase-task-reboot/SKILL.md +24 -100
  82. package/plugin/skills/ase-task-reboot/help.md +1 -1
  83. package/plugin/skills/ase-task-view/SKILL.md +4 -2
  84. package/dst/ase-guidance.js +0 -89
  85. package/dst/ase-persona.js +0 -87
  86. package/dst/ase-stdout.js +0 -18
package/dst/ase-config.js CHANGED
@@ -124,27 +124,33 @@ const gitToplevel = () => {
124
124
  gitToplevelCache.set(cwd, top);
125
125
  return top === "" ? null : top;
126
126
  };
127
- /* detect whether a project context exists, i.e. either we are inside
128
- a Git working tree or a ".ase" directory is present at or above cwd */
129
- const hasProjectContext = () => {
130
- if (gitToplevel() !== null)
131
- return true;
127
+ /* determine the project root directory, i.e. either the top-level
128
+ directory of the Git working tree or the nearest directory at or
129
+ above cwd which carries a ".ase" directory */
130
+ const projectRoot = () => {
131
+ const top = gitToplevel();
132
+ if (top !== null)
133
+ return top;
132
134
  let dir = fs.realpathSync(process.cwd());
133
135
  for (;;) {
134
136
  if (fs.existsSync(path.join(dir, ".ase")))
135
- return true;
137
+ return dir;
136
138
  const parent = path.dirname(dir);
137
139
  if (parent === dir)
138
- return false;
140
+ return null;
139
141
  dir = parent;
140
142
  }
141
143
  };
144
+ /* detect whether a project context exists, i.e. either we are inside
145
+ a Git working tree or a ".ase" directory is present at or above cwd */
146
+ const hasProjectContext = () => projectRoot() !== null;
142
147
  /* parse a raw "--scope" option value into a canonical Scope chain;
143
148
  accepts a comma-separated list of terms in any order. The "user"
144
149
  term is always implicitly added at the bottom of the chain; the
145
150
  "project" term is implicitly added only when a project context
146
- exists (Git repository or ".ase" directory at or above cwd), and
147
- an explicit "project" term requires that same context */
151
+ exists (Git repository or ".ase" directory at or above cwd) and it
152
+ stays weaker than the strongest explicitly requested term, and an
153
+ explicit "project" term requires that same context */
148
154
  export const parseScope = (value) => {
149
155
  const projectActive = hasProjectContext();
150
156
  const input = (value === undefined || value === "") ?
@@ -162,7 +168,11 @@ export const parseScope = (value) => {
162
168
  if (seen.has("project") && !projectActive)
163
169
  throw new Error("invalid --scope: \"project\" requires a project context " +
164
170
  "(a Git repository or a \".ase\" directory at or above the current directory)");
165
- if (!seen.has("project") && projectActive)
171
+ /* the strongest term of the chain is the write target, so an implicitly
172
+ added "project" term must never outrank the strongest explicitly
173
+ requested term, as this would silently retarget the caller's request */
174
+ const rankMax = Math.max(...terms.map((t) => scopeRank(t.kind)));
175
+ if (!seen.has("project") && projectActive && rankMax > scopeRank("project"))
166
176
  terms.unshift({ kind: "project" });
167
177
  if (!seen.has("user"))
168
178
  terms.unshift({ kind: "user" });
@@ -210,6 +220,7 @@ export class Config {
210
220
  log;
211
221
  docs;
212
222
  target;
223
+ pruned;
213
224
  /* creation */
214
225
  constructor(name, schema, log, scope = [{ kind: "user" }, { kind: "project" }]) {
215
226
  if (scope.length === 0)
@@ -222,6 +233,7 @@ export class Config {
222
233
  this.filename = this.resolveFilename(name, tgt);
223
234
  this.docs = [{ scope: tgt, filename: this.filename, doc: new Document() }];
224
235
  this.target = 0;
236
+ this.pruned = [];
225
237
  }
226
238
  /* render a scope term as a short textual label */
227
239
  static scopeLabel(term) {
@@ -252,16 +264,13 @@ export class Config {
252
264
  return path.join(this.userConfigDir(), `${name}.yaml`);
253
265
  else if (term.kind === "project") {
254
266
  const rel = path.join(".ase", `${name}.yaml`);
255
- const cwd = process.cwd();
256
- const top = gitToplevel();
257
- const found = top !== null ?
258
- this.findUpward(cwd, top, rel) :
259
- (fs.existsSync(path.join(cwd, rel)) ? path.join(cwd, rel) : null);
260
- return found ?? path.join(top ?? cwd, rel);
267
+ const root = projectRoot() ?? process.cwd();
268
+ const found = this.findUpward(process.cwd(), root, rel);
269
+ return found ?? path.join(root, rel);
261
270
  }
262
271
  else if (term.kind === "task") {
263
- const top = gitToplevel() ?? process.cwd();
264
- return path.join(top, ".ase", "task", term.id, `${name}.yaml`);
272
+ const root = projectRoot() ?? process.cwd();
273
+ return path.join(root, ".ase", "task", term.id, `${name}.yaml`);
265
274
  }
266
275
  else
267
276
  return path.join(os.homedir(), ".ase", "session", term.id, `${name}.yaml`);
@@ -290,6 +299,7 @@ export class Config {
290
299
  read(mode = "lenient") {
291
300
  const chain = this.scope;
292
301
  const docs = [];
302
+ this.pruned = [];
293
303
  for (let i = 0; i < chain.length; i++) {
294
304
  const sc = chain[i];
295
305
  if (sc.kind === "default") {
@@ -321,6 +331,8 @@ export class Config {
321
331
  if (perDocMode === "strict")
322
332
  throw new Error(msg);
323
333
  this.log.write("warning", msg);
334
+ if (isTarget)
335
+ this.pruned.push(`unparsable YAML (${doc.errors[0].message.split("\n")[0]})`);
324
336
  doc = new Document();
325
337
  }
326
338
  docs.push({ scope: sc, filename, doc });
@@ -330,7 +342,9 @@ export class Config {
330
342
  for (let i = 0; i < docs.length; i++) {
331
343
  const isTarget = (i === this.target);
332
344
  const perDocMode = isTarget ? mode : "lenient";
333
- this.validateDoc(docs[i].doc, docs[i].filename, perDocMode);
345
+ const removed = this.validateDoc(docs[i].doc, docs[i].filename, perDocMode);
346
+ if (isTarget)
347
+ this.pruned.push(...removed);
334
348
  }
335
349
  }
336
350
  /* acquire a cross-process advisory lock on the target scope's file,
@@ -355,18 +369,28 @@ export class Config {
355
369
  const td = this.docs[this.target];
356
370
  if (td.scope.kind === "default")
357
371
  throw new Error("internal error: \"default\" scope is not writable");
372
+ /* a lenient read physically removes the invalid content from the in-memory
373
+ target document, so writing that document back would silently erase the
374
+ very same content from the file on disk */
375
+ if (this.pruned.length > 0)
376
+ throw new Error(`refusing to overwrite ${td.filename}: it carries invalid content ` +
377
+ `(${this.pruned.join(", ")}) which was skipped on reading and hence would be ` +
378
+ "lost on writing -- repair the file first");
358
379
  this.validateDoc(td.doc, td.filename, "strict");
359
380
  fs.mkdirSync(path.dirname(td.filename), { recursive: true });
360
381
  writeFileAtomic.sync(td.filename, td.doc.toString({ indent: 4 }), { encoding: "utf8" });
361
382
  }
362
- /* validate a single YAML document against the optional schema */
383
+ /* validate a single YAML document against the optional schema; in "strict"
384
+ mode all invalid entries are reported as a thrown error, in "lenient" mode
385
+ they are removed from the document and their dotted paths are returned */
363
386
  validateDoc(doc, filename, mode = "strict") {
364
387
  if (this.schema === null)
365
- return;
388
+ return [];
389
+ const removed = [];
366
390
  for (;;) {
367
391
  const result = v.safeParse(this.schema, doc.toJS());
368
392
  if (result.success)
369
- return;
393
+ return removed;
370
394
  if (mode === "strict") {
371
395
  const issues = result.issues.map((i) => {
372
396
  const dotPath = (i.path ?? []).map((p) => String(p.key)).join(".");
@@ -374,19 +398,20 @@ export class Config {
374
398
  }).join("; ");
375
399
  throw new Error(`invalid configuration in ${filename}: ${issues}`);
376
400
  }
377
- let progressed = false;
401
+ const before = removed.length;
378
402
  for (const i of result.issues) {
379
403
  const segs = (i.path ?? []).map((p) => String(p.key));
380
404
  const dotPath = segs.join(".");
381
405
  this.log.write("warning", `invalid entry in ${filename}: ${dotPath ? `${dotPath}: ` : ""}${i.message}`);
382
- if (segs.length > 0) {
383
- doc.deleteIn(segs);
384
- progressed = true;
385
- }
386
- /* root-level issues cannot be deleted; processing continues with the remaining issues */
406
+ if (segs.length > 0 && doc.deleteIn(segs))
407
+ removed.push(dotPath);
408
+ /* issues at the document root and issues whose stringified path does not
409
+ address a deletable node (e.g. a non-string YAML key like "404:", which
410
+ "toJS" stringifies) cannot be removed; processing continues with the
411
+ remaining issues */
387
412
  }
388
- if (!progressed)
389
- return;
413
+ if (removed.length === before)
414
+ return removed;
390
415
  }
391
416
  }
392
417
  /* enumerate all full dotted leaf paths from the attached valibot schema */
@@ -444,6 +469,21 @@ export class Config {
444
469
  }
445
470
  return undefined;
446
471
  }
472
+ /* retrieve the explicitly configured value at a dotted key, i.e. the same
473
+ cascade as "get", but skipping the built-in "default" scope layer, so
474
+ callers can distinguish a deliberately configured value from a merely
475
+ preset one */
476
+ getExplicit(key) {
477
+ const segs = this.resolveKey(key).split(".");
478
+ for (let i = this.docs.length - 1; i >= 0; i--) {
479
+ if (this.docs[i].scope.kind === "default")
480
+ continue;
481
+ const node = this.docs[i].doc.getIn(segs);
482
+ if (node !== undefined)
483
+ return node;
484
+ }
485
+ return undefined;
486
+ }
447
487
  /* enumerate the effective leaf entries across the full scope chain;
448
488
  each returned entry identifies the originating scope */
449
489
  entries() {
@@ -551,7 +591,8 @@ export default class ConfigCommand {
551
591
  .option("--scope <scope>", "configuration scope chain: comma-separated list of \"user\", \"project\", " +
552
592
  "\"task:<id>\", and/or \"session:<id>\" terms (e.g. \"task:N,session:M\"); " +
553
593
  "\"user\" is always implicitly included and \"project\" is implicitly " +
554
- "included whenever a project context (Git repo or upward \".ase\" directory) exists")
594
+ "included whenever a project context (Git repo or upward \".ase\" directory) exists, " +
595
+ "but never above the strongest explicitly requested term")
555
596
  .description("manage ASE configuration")
556
597
  .action(() => {
557
598
  configCmd.outputHelp();
package/dst/ase-getopt.js CHANGED
@@ -7,7 +7,7 @@ import { z } from "zod";
7
7
  import { Command, Option } from "commander";
8
8
  import { parse as shParse, quote as shQuote } from "shell-quote";
9
9
  /* tokenize a raw input string into [start,end) token ranges, preserving
10
- the quoting so the original text can later be sliced verbatim */
10
+ the quoting and escaping so the original text can later be sliced verbatim */
11
11
  const tokenizeRanges = (input) => {
12
12
  const ranges = [];
13
13
  let i = 0;
@@ -30,6 +30,8 @@ const tokenizeRanges = (input) => {
30
30
  if (i < input.length)
31
31
  i++;
32
32
  }
33
+ else if (ch === "\\" && i + 1 < input.length)
34
+ i += 2;
33
35
  else
34
36
  i++;
35
37
  }
package/dst/ase-hook.js CHANGED
@@ -240,10 +240,12 @@ export default class HookCommand {
240
240
  const projectId = path.basename(projectDir);
241
241
  /* determine user id */
242
242
  const userId = process.env.USER ?? process.env.LOGNAME ?? "unknown";
243
- /* helper function: determine a setting from the configuration,
244
- falling back to an environment variable and a default */
243
+ /* helper function: determine a setting from the explicitly configured
244
+ scopes, falling back to an environment variable and a default; the
245
+ built-in "default" scope layer is deliberately skipped, as its preset
246
+ value is always present and would hence shadow the environment variable */
245
247
  const setting = (key, envVar, dflt) => {
246
- const val = cfg.get(key);
248
+ const val = cfg.getExplicit(key);
247
249
  return typeof val === "string" ? val : (process.env[envVar] ?? dflt);
248
250
  };
249
251
  /* determine agent persona style, agent guidance level, and project boxing transparency */
@@ -19,9 +19,12 @@ export class Markdown {
19
19
  /* segment the input line-wise into alternating non-fenced and
20
20
  fenced regions: a fenced code block opens with a line whose first
21
21
  non-whitespace content is a run of 3+ backticks or tildes and
22
- closes with a matching-or-longer run of the same marker; fenced
23
- regions are emitted verbatim while only non-fenced regions are
24
- handed to the rewriting passes below */
22
+ closes with a matching-or-longer run of the same marker; for a
23
+ backtick fence the info string trailing the run must not carry any
24
+ further backtick, so an inline code span delimited by a run of 3+
25
+ backticks never opens a fence; fenced regions are emitted verbatim
26
+ while only non-fenced regions are handed to the rewriting passes
27
+ below */
25
28
  const lines = text.split("\n");
26
29
  let result = "";
27
30
  let buf = "";
@@ -37,7 +40,7 @@ export class Markdown {
37
40
  for (let li = 0; li < lines.length; li++) {
38
41
  const line = lines[li];
39
42
  const nl = li < lines.length - 1 ? "\n" : "";
40
- const m = line.match(/^\s*(`{3,}|~{3,})/);
43
+ const m = line.match(/^\s*(`{3,}(?![^`]*`)|~{3,})/);
41
44
  if (!inFence && m) {
42
45
  /* a fence-opening line: flush the pending non-fenced buffer,
43
46
  enter fenced mode, and emit the opener verbatim */
@@ -439,7 +439,7 @@ export default class ServiceCommand {
439
439
  return await new Promise(() => { });
440
440
  }
441
441
  if (port !== null) {
442
- const match = await probe(port, ctx.projectId);
442
+ const match = await probe(port, ctx.projectId).catch(() => null);
443
443
  if (match === true) {
444
444
  this.log.write("info", `service: already running on port ${port}`);
445
445
  return 0;
package/dst/ase-setup.js CHANGED
@@ -808,9 +808,11 @@ export default class SetupCommand {
808
808
  /* default for --dev derived from ASE_SETUP_DEV environment variable */
809
809
  const envDev = process.env.ASE_SETUP_DEV ?? "";
810
810
  const devDflt = envDev !== "" && envDev !== "0" && envDev.toLowerCase() !== "false";
811
- /* default for --tool derived from ASE_TOOL environment variable */
811
+ /* default for --tool derived from ASE_TOOL environment variable
812
+ (validated lazily by "parseTool" in each action, so an invalid
813
+ value cannot break unrelated "ase" commands at startup) */
812
814
  const envTool = process.env.ASE_TOOL ?? "";
813
- const toolDflt = envTool !== "" ? this.parseTool(envTool) : "claude";
815
+ const toolDflt = envTool !== "" ? envTool : "claude";
814
816
  /* register CLI top-level command "ase setup" */
815
817
  const setupCmd = program
816
818
  .command("setup")
@@ -388,25 +388,23 @@ export default class StatuslineCommand {
388
388
  /* ==== RATE LIMITS ==== */
389
389
  S: () => {
390
390
  const pct5h = data.rate_limits?.five_hour?.used_percentage;
391
- if (pct5h !== undefined)
392
- emit(`${prefix("⏲", "session-usage")}${c.bold(`${pct5h.toFixed(1)}%`)}`);
391
+ const s = pct5h !== undefined ? `${pct5h.toFixed(1)}%` : "-";
392
+ emit(`${prefix("⏲", "5h-usage")}${c.bold(s)}`);
393
393
  },
394
394
  D: () => {
395
395
  const until5h = data.rate_limits?.five_hour?.resets_at;
396
396
  const s = formatTimeUntil(until5h);
397
- if (s !== "")
398
- emit(`${prefix("⏱", "session-resets")}${c.bold(s)}`);
397
+ emit(`${prefix("⏱", "5h-resets")}${c.bold(s !== "" ? s : "-")}`);
399
398
  },
400
399
  W: () => {
401
400
  const pctWk = data.rate_limits?.seven_day?.used_percentage;
402
- if (pctWk !== undefined)
403
- emit(`${prefix("⏲", "weekly-usage")}${c.bold(`${pctWk.toFixed(1)}%`)}`);
401
+ const s = pctWk !== undefined ? `${pctWk.toFixed(1)}%` : "-";
402
+ emit(`${prefix("⏲", "7d-usage")}${c.bold(s)}`);
404
403
  },
405
404
  Q: () => {
406
405
  const untilWk = data.rate_limits?.seven_day?.resets_at;
407
406
  const s = formatTimeUntil(untilWk);
408
- if (s !== "")
409
- emit(`${prefix("⏱", "weekly-resets")}${c.bold(s)}`);
407
+ emit(`${prefix("⏱", "7d-resets")}${c.bold(s !== "" ? s : "-")}`);
410
408
  },
411
409
  /* ==== COSTS ==== */
412
410
  H: () => {
package/dst/ase-task.js CHANGED
@@ -334,6 +334,7 @@ export class Task {
334
334
  /* set the active task id for a given session */
335
335
  static setId(log, session, id) {
336
336
  Task.validateSession(session);
337
+ Task.validateId(id);
337
338
  const scope = parseScope(`session:${session}`);
338
339
  const cfg = new Config("config", configSchema, log, scope);
339
340
  cfg.lock(() => {
@@ -529,15 +530,33 @@ export class TaskMCP {
529
530
  title: "ASE task load",
530
531
  description: "Load a previously persisted task by `id`. " +
531
532
  "Returns the task as `text`, normalized into the current Markdown frontmatter shape; " +
532
- "returns an empty string if no task exists for the `id`.",
533
+ "returns an empty string if no task exists for the `id`. " +
534
+ "The `variant` argument selects the returned form of the plan: " +
535
+ "`source` (the default) is the *authoring* form and the *only* form which may be " +
536
+ "edited and passed back into `ase_task_save`; " +
537
+ "`render` is the *rendering-prepared* form, which is for *display only* and " +
538
+ "MUST NOT be persisted; " +
539
+ "`both` returns the source form enclosed in `<task-plan-source>` delimiter lines, " +
540
+ "followed by the render form enclosed in `<task-plan-render>` delimiter lines.",
533
541
  inputSchema: {
534
542
  id: z.string()
535
- .describe("task identifier (allowed characters: A-Z, a-z, 0-9, '_', '-')")
543
+ .describe("task identifier (allowed characters: A-Z, a-z, 0-9, '_', '-')"),
544
+ variant: z.enum(["source", "render", "both"]).optional()
545
+ .describe("returned form of the plan: `source` (authoring form, the default), " +
546
+ "`render` (rendering-prepared form, display only), or `both` " +
547
+ "(both forms, each enclosed in its delimiter lines)")
536
548
  }
537
549
  }, async (args) => {
538
550
  try {
539
- const raw = Task.load(this.log, args.id);
540
- const text = Markdown.prepare(raw);
551
+ const source = Task.load(this.log, args.id);
552
+ const variant = args.variant ?? "source";
553
+ let text = source;
554
+ if (source !== "" && variant === "render")
555
+ text = Markdown.prepare(source);
556
+ else if (source !== "" && variant === "both")
557
+ text =
558
+ `<task-plan-source>\n${source}\n</task-plan-source>\n\n` +
559
+ `<task-plan-render>\n${Markdown.prepare(source)}\n</task-plan-render>\n`;
541
560
  return {
542
561
  content: [{ type: "text", text }]
543
562
  };
@@ -550,21 +569,28 @@ export class TaskMCP {
550
569
  mcp.registerTool("ase_task_save", {
551
570
  title: "ASE task save",
552
571
  description: "Persist a task as `text` under `id`. " +
553
- "Overwrites any existing task for the same `id`. " +
554
- "Returns the persisted task as `text`, prepared for improved rendering.",
572
+ "The `text` MUST be the *authoring* form of the plan (as returned by the " +
573
+ "`source` variant of `ase_task_load`) and hence MUST NOT carry any rendering " +
574
+ "artifacts. Overwrites any existing task for the same `id`. " +
575
+ "Returns a status `text` by default, or, if `render` is `true`, the " +
576
+ "*rendering-prepared* form of the just-saved plan, for display purposes only.",
555
577
  inputSchema: {
556
578
  id: z.string()
557
579
  .describe("task identifier (allowed characters: A-Z, a-z, 0-9, '_', '-')"),
558
580
  text: z.string()
559
- .describe("text content of the task")
581
+ .describe("text content of the task, in its authoring form"),
582
+ render: z.boolean().optional()
583
+ .describe("if true, return the rendering-prepared form of the just-saved " +
584
+ "plan instead of a status message (default: false)")
560
585
  }
561
586
  }, async (args) => {
562
587
  try {
563
588
  Task.save(this.log, args.id, args.text);
564
- /* return the prepared content, so a caller reusing the
565
- just-saved plan instead of re-loading it still receives
566
- the rendering-prepared variant */
567
- const text = Markdown.prepare(args.text);
589
+ /* return the rendering-prepared content on demand, so a caller
590
+ displaying the just-saved plan does not have to re-load it */
591
+ const text = (args.render ?? false) ?
592
+ Markdown.prepare(args.text) :
593
+ `OK: saved task "${args.id}"`;
568
594
  return {
569
595
  content: [{ type: "text", text }]
570
596
  };
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "homepage": "https://ase.tools",
7
7
  "repository": { "url": "git+https://github.com/rse/ase.git", "type": "git" },
8
8
  "bugs": { "url": "https://github.com/rse/ase/issues" },
9
- "version": "0.9.55",
9
+ "version": "0.9.57",
10
10
  "license": "Apache-2.0",
11
11
  "author": {
12
12
  "name": "Dr. Ralf S. Engelschall",
@@ -18,12 +18,12 @@
18
18
  "devDependencies": {
19
19
  "eslint": "9.39.4",
20
20
  "@eslint/js": "9.39.4",
21
- "@typescript-eslint/parser": "8.65.0",
22
- "@typescript-eslint/eslint-plugin": "8.65.0",
21
+ "@typescript-eslint/parser": "8.66.0",
22
+ "@typescript-eslint/eslint-plugin": "8.66.0",
23
23
  "eslint-plugin-promise": "7.3.0",
24
24
  "eslint-plugin-import": "2.32.0",
25
25
  "neostandard": "0.13.0",
26
- "globals": "17.8.0",
26
+ "globals": "17.9.0",
27
27
  "typescript": "6.0.3",
28
28
 
29
29
  "@rse/stx": "1.1.6",
@@ -42,7 +42,7 @@
42
42
  },
43
43
  "dependencies": {
44
44
  "commander": "15.0.0",
45
- "@dotenvx/dotenvx": "2.19.1",
45
+ "@dotenvx/dotenvx": "2.19.2",
46
46
  "yaml": "2.9.0",
47
47
  "valibot": "1.4.2",
48
48
  "execa": "10.0.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ase",
3
- "version": "0.9.55",
3
+ "version": "0.9.57",
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.55",
3
+ "version": "0.9.57",
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.55",
3
+ "version": "0.9.57",
4
4
  "description": "Agentic Software Engineering (ASE)",
5
5
  "keywords": [ "agentic", "software", "engineering" ],
6
6
  "homepage": "https://ase.tools",
@@ -63,7 +63,7 @@ Workflow
63
63
  - incorrect short-circuit evaluation or operator precedence
64
64
  - logical vs. bitwise operator confusion
65
65
  - negation mistakes in compound predicates
66
- - wrong comparison operator (`==` instead of `===`, `<` vs. `<=`, etc)
66
+ - wrong comparison operator (`==` instead of `===`, `<` vs. `<=`, etc.)
67
67
  - inverted condition or swapped if/else branches
68
68
  - dead or duplicated conditional branches
69
69
  - fall-through where a break or return was intended
@@ -65,10 +65,10 @@ Workflow
65
65
  identifiers, string literals and comments.
66
66
 
67
67
  Especially, for comments ensure English language only and
68
- prefer short very brief one-line descriptions.
68
+ prefer short, very brief one-line descriptions.
69
69
 
70
70
  - **A05 - COMPLEXITY**:
71
- Check for extremely long functions, and deeply nested code
71
+ Check for extremely long functions and deeply nested code
72
72
  constructs.
73
73
 
74
74
  Especially, for functions prefer fewer than 100 lines, and for
@@ -185,8 +185,8 @@ Workflow
185
185
  - **A15 - PERFORMANCE**:
186
186
  Check for bad performance and inefficiency issues.
187
187
 
188
- Especially, check for code constructs with a high (i.e., not
189
- constant/O(1), or linear/O(n) complexity) in its execution time
188
+ Especially, check for code constructs with a high complexity (i.e., not
189
+ constant/O(1), or linear/O(n)) in their execution time
190
190
  and/or memory consumption.
191
191
 
192
192
  - **A16 - SECURITY**:
@@ -56,5 +56,5 @@ tools:
56
56
  4. Now call the MCP tool `query(prompt: <query/>)` from the MCP server
57
57
  <server/> and then return its result `text` *verbatim* and *without
58
58
  any modifications* as the last message. Especially, do *NOT* add or
59
- remove any text to the MCP server response on your own and do not
59
+ remove any text to or from the MCP server response on your own and do not
60
60
  interpret the result in any way.
@@ -102,7 +102,7 @@ store those facts in the placeholder named `<arg2/>`.
102
102
  <child-4/>.
103
103
 
104
104
  <expand name="gather-facts"
105
- arg1="the CHILDREN topics (narrower topics that are specializations) of the following topic: <topic/>"
105
+ arg1="the CHILD topics (narrower topics that are specializations) of the following topic: <topic/>"
106
106
  arg2="facts-children"></expand>
107
107
 
108
108
  Ground the determination of the canonical names of the most