@rse/ase 0.9.7 → 0.9.9

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 (85) hide show
  1. package/dst/ase-getopt.js +71 -5
  2. package/dst/ase-hook.js +6 -21
  3. package/dst/ase-markdown.js +32 -11
  4. package/dst/ase-mcp.js +22 -8
  5. package/dst/ase-notify.js +32 -0
  6. package/dst/ase-service.js +5 -2
  7. package/dst/ase-setup.js +45 -131
  8. package/dst/ase-skills.js +17 -13
  9. package/dst/ase-statusline.js +8 -12
  10. package/dst/ase-task.js +32 -23
  11. package/package.json +3 -3
  12. package/plugin/.claude-plugin/plugin.json +1 -1
  13. package/plugin/.github/plugin/plugin.json +1 -1
  14. package/plugin/agents/ase-docs-proofread.md +2 -2
  15. package/plugin/meta/ase-constitution.md +7 -0
  16. package/plugin/meta/ase-control.md +24 -3
  17. package/plugin/meta/ase-dialog.md +105 -7
  18. package/plugin/meta/ase-format-task.md +2 -2
  19. package/plugin/meta/ase-getopt.md +31 -22
  20. package/plugin/meta/ase-skill.md +87 -12
  21. package/plugin/package.json +2 -2
  22. package/plugin/skills/ase-arch-analyze/SKILL.md +88 -89
  23. package/plugin/skills/ase-arch-analyze/help.md +2 -2
  24. package/plugin/skills/ase-arch-discover/SKILL.md +56 -34
  25. package/plugin/skills/ase-arch-discover/help.md +1 -1
  26. package/plugin/skills/ase-code-analyze/SKILL.md +6 -5
  27. package/plugin/skills/ase-code-analyze/help.md +2 -2
  28. package/plugin/skills/ase-code-craft/SKILL.md +83 -71
  29. package/plugin/skills/ase-code-craft/help.md +2 -2
  30. package/plugin/skills/ase-code-explain/SKILL.md +1 -1
  31. package/plugin/skills/ase-code-explain/help.md +1 -1
  32. package/plugin/skills/ase-code-insight/SKILL.md +1 -1
  33. package/plugin/skills/ase-code-insight/help.md +1 -1
  34. package/plugin/skills/ase-code-lint/SKILL.md +35 -18
  35. package/plugin/skills/ase-code-lint/help.md +2 -2
  36. package/plugin/skills/ase-code-refactor/SKILL.md +81 -70
  37. package/plugin/skills/ase-code-refactor/help.md +2 -2
  38. package/plugin/skills/ase-code-resolve/SKILL.md +83 -70
  39. package/plugin/skills/ase-code-resolve/help.md +3 -3
  40. package/plugin/skills/ase-docs-distill/SKILL.md +1 -1
  41. package/plugin/skills/ase-docs-distill/help.md +4 -4
  42. package/plugin/skills/ase-docs-proofread/SKILL.md +36 -19
  43. package/plugin/skills/ase-docs-proofread/help.md +1 -1
  44. package/plugin/skills/ase-meta-brainstorm/SKILL.md +29 -8
  45. package/plugin/skills/ase-meta-brainstorm/help.md +7 -11
  46. package/plugin/skills/ase-meta-changelog/help.md +1 -1
  47. package/plugin/skills/ase-meta-chat/help.md +1 -1
  48. package/plugin/skills/ase-meta-commit/help.md +1 -1
  49. package/plugin/skills/ase-meta-diaboli/help.md +2 -2
  50. package/plugin/skills/ase-meta-diff/SKILL.md +6 -5
  51. package/plugin/skills/ase-meta-diff/help.md +11 -12
  52. package/plugin/skills/ase-meta-evaluate/SKILL.md +10 -9
  53. package/plugin/skills/ase-meta-evaluate/help.md +2 -2
  54. package/plugin/skills/ase-meta-persona/help.md +1 -1
  55. package/plugin/skills/ase-meta-quorum/SKILL.md +15 -5
  56. package/plugin/skills/ase-meta-quorum/help.md +1 -1
  57. package/plugin/skills/ase-meta-review/SKILL.md +3 -4
  58. package/plugin/skills/ase-meta-review/help.md +5 -5
  59. package/plugin/skills/ase-meta-search/SKILL.md +9 -8
  60. package/plugin/skills/ase-meta-search/help.md +1 -1
  61. package/plugin/skills/ase-meta-steelman/SKILL.md +1 -1
  62. package/plugin/skills/ase-meta-steelman/help.md +2 -2
  63. package/plugin/skills/ase-meta-why/SKILL.md +16 -10
  64. package/plugin/skills/ase-meta-why/help.md +1 -1
  65. package/plugin/skills/ase-task-condense/SKILL.md +36 -19
  66. package/plugin/skills/ase-task-condense/help.md +3 -3
  67. package/plugin/skills/ase-task-delete/SKILL.md +6 -3
  68. package/plugin/skills/ase-task-delete/help.md +2 -2
  69. package/plugin/skills/ase-task-edit/SKILL.md +61 -36
  70. package/plugin/skills/ase-task-edit/help.md +4 -4
  71. package/plugin/skills/ase-task-grill/SKILL.md +57 -26
  72. package/plugin/skills/ase-task-grill/help.md +3 -3
  73. package/plugin/skills/ase-task-id/SKILL.md +11 -2
  74. package/plugin/skills/ase-task-id/help.md +2 -2
  75. package/plugin/skills/ase-task-implement/SKILL.md +40 -17
  76. package/plugin/skills/ase-task-implement/help.md +2 -2
  77. package/plugin/skills/ase-task-list/SKILL.md +1 -1
  78. package/plugin/skills/ase-task-list/help.md +2 -2
  79. package/plugin/skills/ase-task-preflight/SKILL.md +44 -22
  80. package/plugin/skills/ase-task-preflight/help.md +3 -3
  81. package/plugin/skills/ase-task-reboot/SKILL.md +31 -20
  82. package/plugin/skills/ase-task-reboot/help.md +2 -2
  83. package/plugin/skills/ase-task-rename/SKILL.md +5 -3
  84. package/plugin/skills/ase-task-rename/help.md +2 -2
  85. package/plugin/skills/ase-task-view/help.md +26 -7
package/dst/ase-skills.js CHANGED
@@ -242,27 +242,31 @@ export class Skills {
242
242
  return [];
243
243
  }
244
244
  /* compute composite rank score from weighted metrics:
245
- downloads x
246
- stars x
245
+ (downloads + 1) x
246
+ (stars + 1) x
247
247
  ([lifespan =] (updated - created)) x
248
248
  ([recentness =] exp(-(now - updated) / halfLife))
249
- `"N.A."` factors are treated as neutral `1` so that stacks for which
250
- a particular metric is structurally unavailable (e.g. Maven Central
251
- exposes no per-artifact download counts) can still be ranked by the
252
- remaining metrics, instead of collapsing the entire product to zero. */
249
+ Numeric count metrics are shifted by `+1` so that a genuine `0`
250
+ (e.g. a real package with zero downloads or stars) contributes a
251
+ neutral `1` instead of collapsing the entire product to zero, while
252
+ still ordering `0 < 1 < 2 ...`. The `"N.A."` sentinel (a metric that
253
+ is structurally unavailable, e.g. Maven Central exposes no
254
+ per-artifact download counts) is likewise treated as neutral `1`, so
255
+ such stacks can still be ranked by the remaining metrics. */
253
256
  static computeRank(downloads, stars, created, updated) {
254
- const d = typeof downloads === "number" ? downloads : 1;
255
- const s = typeof stars === "number" ? stars : 1;
257
+ const d = typeof downloads === "number" ? downloads + 1 : 1;
258
+ const s = typeof stars === "number" ? stars + 1 : 1;
256
259
  const cMs = created !== "" ? Date.parse(created) : NaN;
257
260
  const uMs = updated !== "" ? Date.parse(updated) : NaN;
258
- if (Number.isNaN(cMs) || Number.isNaN(uMs))
259
- return 0;
260
261
  const now = Date.now();
261
262
  const msPerDay = 1000 * 60 * 60 * 24;
262
263
  const halfLife = 365 / 2;
263
- const lifespan = Math.max(0, uMs - cMs);
264
- const ageDays = Math.max(0, (now - uMs) / msPerDay);
265
- const recentness = Math.exp(-ageDays / halfLife);
264
+ /* lifespan requires both timestamps; recentness requires the
265
+ updated timestamp -- any unavailable date-derived factor is
266
+ treated as neutral `1` so the entry can still be ranked by the
267
+ remaining metrics, instead of collapsing the product to zero */
268
+ const lifespan = (!Number.isNaN(cMs) && !Number.isNaN(uMs)) ? Math.max(0, uMs - cMs) : 1;
269
+ const recentness = !Number.isNaN(uMs) ? Math.exp(-Math.max(0, (now - uMs) / msPerDay) / halfLife) : 1;
266
270
  return d * s * lifespan * recentness;
267
271
  }
268
272
  /* compute the per-alternative product-sum (rating) row from a
@@ -245,12 +245,14 @@ export default class StatuslineCommand {
245
245
  out += ansi;
246
246
  col += raw.length;
247
247
  };
248
- /* active <color> span state: when non-null, renderer/literal output is buffered
249
- instead of appended directly, and flushed via c[color](buf) on </color> */
248
+ /* active <color> span state: when non-null, each emitted chunk is colored
249
+ individually and passed straight through appendOutput, so column
250
+ accounting and line-wrapping continue to work per-chunk inside the span
251
+ instead of treating the whole colored run as one atomic chunk */
250
252
  let span = null;
251
253
  const emit = (chunk) => {
252
- if (span !== null)
253
- span.buf += chunk;
254
+ if (span !== null && span.color !== "default")
255
+ appendOutput((c[span.color])(chunk));
254
256
  else
255
257
  appendOutput(chunk);
256
258
  };
@@ -446,13 +448,7 @@ export default class StatuslineCommand {
446
448
  };
447
449
  /* walk each template line and render */
448
450
  const closeSpan = () => {
449
- if (span !== null) {
450
- const wrapped = span.color === "default" ?
451
- span.buf :
452
- (c[span.color])(span.buf);
453
- span = null;
454
- appendOutput(wrapped);
455
- }
451
+ span = null;
456
452
  };
457
453
  for (const line of tmpl) {
458
454
  let i = 0;
@@ -465,7 +461,7 @@ export default class StatuslineCommand {
465
461
  if (m[1] === "/")
466
462
  closeSpan();
467
463
  else if (span === null)
468
- span = { color: m[2], buf: "" };
464
+ span = { color: m[2] };
469
465
  i += m[0].length;
470
466
  continue;
471
467
  }
package/dst/ase-task.js CHANGED
@@ -20,8 +20,8 @@ export class Task {
20
20
  static validateId(id) {
21
21
  if (typeof id !== "string" || id.length === 0)
22
22
  throw new Error("task: id must be a non-empty string");
23
- if (!/^[A-Za-z0-9-]+$/.test(id))
24
- throw new Error("task: id must match [A-Za-z0-9-]+");
23
+ if (!/^[A-Za-z0-9_-]+$/.test(id))
24
+ throw new Error("task: id must match [A-Za-z0-9_-]+");
25
25
  }
26
26
  /* determine the project root (Git top-level if inside a Git
27
27
  working tree, otherwise the current working directory) */
@@ -59,12 +59,22 @@ export class Task {
59
59
  static baseDir(log) {
60
60
  return path.join(Task.projectRoot(), Task.spec(log).basedir);
61
61
  }
62
+ /* ensure a task id's "TASK-<id>.md" filename satisfies
63
+ the configured "files" miniglob */
64
+ static enforceFiles(log, id) {
65
+ const { files } = Task.spec(log);
66
+ const filename = `TASK-${id}.md`;
67
+ if (!picomatch(files, { dot: true })(filename))
68
+ throw new Error(`task: id "${id}" yields filename "${filename}" ` +
69
+ `which does not match the configured "files" glob "${files}"`);
70
+ }
62
71
  /* resolve the on-disk path for a given task id; as a side effect,
63
72
  eagerly migrate any legacy <basedir>/<id>/plan.md files to the
64
73
  current <basedir>/TASK-<id>.md layout on first access (guarded by
65
74
  a cheap check, so it is a no-op once the store is migrated) */
66
75
  static path(log, id) {
67
76
  Task.validateId(id);
77
+ Task.enforceFiles(log, id);
68
78
  if (Task.needsMigration(log))
69
79
  Task.migrateAll(log);
70
80
  return path.join(Task.baseDir(log), `TASK-${id}.md`);
@@ -76,7 +86,7 @@ export class Task {
76
86
  if (!fs.existsSync(dir))
77
87
  return false;
78
88
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
79
- if (!entry.isDirectory() || !/^[A-Za-z0-9-]+$/.test(entry.name))
89
+ if (!entry.isDirectory() || !/^[A-Za-z0-9_-]+$/.test(entry.name))
80
90
  continue;
81
91
  if (fs.existsSync(path.join(dir, entry.name, "plan.md")))
82
92
  return true;
@@ -93,7 +103,7 @@ export class Task {
93
103
  return [];
94
104
  const migrated = [];
95
105
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
96
- if (!entry.isDirectory() || !/^[A-Za-z0-9-]+$/.test(entry.name))
106
+ if (!entry.isDirectory() || !/^[A-Za-z0-9_-]+$/.test(entry.name))
97
107
  continue;
98
108
  const id = entry.name;
99
109
  const oldFile = path.join(dir, id, "plan.md");
@@ -138,7 +148,7 @@ export class Task {
138
148
  }
139
149
  /* rename a task by moving its <project>/<basedir>/TASK-<oldId>.md file
140
150
  to <project>/<basedir>/TASK-<newId>.md; the embedded
141
- "#TASK <id>:" heading inside the plan content is rewritten to
151
+ "# TASK <id>:" heading inside the plan content is rewritten to
142
152
  the new id; returns true on success, false if the source task does
143
153
  not exist; throws if the target id already exists */
144
154
  static rename(log, oldId, newId) {
@@ -149,7 +159,7 @@ export class Task {
149
159
  if (fs.existsSync(newFile))
150
160
  throw new Error(`task: target id "${newId}" already exists`);
151
161
  const text = fs.readFileSync(oldFile, "utf8");
152
- const updated = text.replace(/(^#\s+(?:✪\s+)?TASK\s+)[A-Za-z0-9-]+(\s*:)/m, `$1${newId}$2`);
162
+ const updated = text.replace(/(^#\s+TASK\s+)[A-Za-z0-9_-]+(\s*:)/m, `$1${newId}$2`);
153
163
  fs.mkdirSync(path.dirname(newFile), { recursive: true });
154
164
  fs.writeFileSync(newFile, updated, "utf8");
155
165
  fs.rmSync(oldFile, { force: true });
@@ -168,7 +178,7 @@ export class Task {
168
178
  const isMatch = picomatch(files, { dot: true });
169
179
  const out = [];
170
180
  for (const entry of fs.readdirSync(dir)) {
171
- const m = /^TASK-([A-Za-z0-9-]+)\.md$/.exec(entry);
181
+ const m = /^TASK-([A-Za-z0-9_-]+)\.md$/.exec(entry);
172
182
  if (m === null || !isMatch(entry))
173
183
  continue;
174
184
  const file = path.join(dir, entry);
@@ -194,7 +204,7 @@ export class Task {
194
204
  const cutoff = Date.now() - maxAgeMs;
195
205
  const removed = [];
196
206
  for (const entry of fs.readdirSync(dir)) {
197
- const m = /^TASK-([A-Za-z0-9-]+)\.md$/.exec(entry);
207
+ const m = /^TASK-([A-Za-z0-9_-]+)\.md$/.exec(entry);
198
208
  if (m === null || !isMatch(entry))
199
209
  continue;
200
210
  const file = path.join(dir, entry);
@@ -417,7 +427,7 @@ export class TaskMCP {
417
427
  "Returns the task as `text`; returns an empty string if no task exists for the `id`.",
418
428
  inputSchema: {
419
429
  id: z.string()
420
- .describe("task identifier (allowed characters: A-Z, a-z, 0-9, '-')")
430
+ .describe("task identifier (allowed characters: A-Z, a-z, 0-9, '_', '-')")
421
431
  }
422
432
  }, async (args) => {
423
433
  try {
@@ -442,16 +452,15 @@ export class TaskMCP {
442
452
  "Overwrites any existing task for the same `id`.",
443
453
  inputSchema: {
444
454
  id: z.string()
445
- .describe("task identifier (allowed characters: A-Z, a-z, 0-9, '-')"),
455
+ .describe("task identifier (allowed characters: A-Z, a-z, 0-9, '_', '-')"),
446
456
  text: z.string()
447
457
  .describe("text content of the task")
448
458
  }
449
459
  }, async (args) => {
450
460
  try {
451
- const text = Markdown.prepare(args.text);
452
- Task.save(this.log, args.id, text);
461
+ Task.save(this.log, args.id, args.text);
453
462
  return {
454
- content: [{ type: "text", text: `task_save: OK: saved task "${args.id}"` }]
463
+ content: [{ type: "text", text: `OK: saved task "${args.id}"` }]
455
464
  };
456
465
  }
457
466
  catch (err) {
@@ -469,14 +478,14 @@ export class TaskMCP {
469
478
  "Returns a status `text` indicating whether a task existed and was removed.",
470
479
  inputSchema: {
471
480
  id: z.string()
472
- .describe("task identifier (allowed characters: A-Z, a-z, 0-9, '-')")
481
+ .describe("task identifier (allowed characters: A-Z, a-z, 0-9, '_', '-')")
473
482
  }
474
483
  }, async (args) => {
475
484
  try {
476
485
  const removed = Task.delete(this.log, args.id);
477
486
  const msg = removed ?
478
- `task_delete: OK: removed task "${args.id}"` :
479
- `task_delete: WARNING: no task "${args.id}" to remove`;
487
+ `OK: removed task "${args.id}"` :
488
+ `WARNING: no task "${args.id}" to remove`;
480
489
  return {
481
490
  content: [{ type: "text", text: msg }]
482
491
  };
@@ -497,16 +506,16 @@ export class TaskMCP {
497
506
  "Fails with an error if the target id already exists.",
498
507
  inputSchema: {
499
508
  old: z.string()
500
- .describe("old task identifier (allowed characters: A-Z, a-z, 0-9, '-')"),
509
+ .describe("old task identifier (allowed characters: A-Z, a-z, 0-9, '_', '-')"),
501
510
  new: z.string()
502
- .describe("new task identifier (allowed characters: A-Z, a-z, 0-9, '-')")
511
+ .describe("new task identifier (allowed characters: A-Z, a-z, 0-9, '_', '-')")
503
512
  }
504
513
  }, async (args) => {
505
514
  try {
506
515
  const renamed = Task.rename(this.log, args.old, args.new);
507
516
  const msg = renamed ?
508
- `task_rename: OK: renamed task "${args.old}" to "${args.new}"` :
509
- `task_rename: WARNING: no task "${args.old}" to rename`;
517
+ `OK: renamed task "${args.old}" to "${args.new}"` :
518
+ `WARNING: no task "${args.old}" to rename`;
510
519
  return {
511
520
  content: [{ type: "text", text: msg }]
512
521
  };
@@ -527,16 +536,16 @@ export class TaskMCP {
527
536
  "otherwise it returns the current task `id` of the `session`.",
528
537
  inputSchema: {
529
538
  id: z.string().optional()
530
- .describe("task identifier to set (allowed characters: A-Z, a-z, 0-9, '-'); " +
539
+ .describe("task identifier to set (allowed characters: A-Z, a-z, 0-9, '_', '-'); " +
531
540
  "if omitted, the current task id is returned"),
532
541
  session: z.string()
533
- .describe("session identifier (allowed characters: A-Z, a-z, 0-9, '-')")
542
+ .describe("session identifier (allowed characters: A-Z, a-z, 0-9, '_', '-')")
534
543
  }
535
544
  }, async (args) => {
536
545
  try {
537
546
  if (args.id !== undefined) {
538
547
  Task.setId(this.log, args.session, args.id);
539
- const msg = `task_id: OK: set agent.task to "${args.id}" ` +
548
+ const msg = `OK: set agent.task to "${args.id}" ` +
540
549
  `for session "${args.session}"`;
541
550
  return {
542
551
  content: [{ type: "text", text: msg }]
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.7",
9
+ "version": "0.9.9",
10
10
  "license": "GPL-3.0-only",
11
11
  "author": {
12
12
  "name": "Dr. Ralf S. Engelschall",
@@ -30,7 +30,7 @@
30
30
  "nodemon": "3.1.14",
31
31
  "shx": "0.4.0",
32
32
 
33
- "@types/node": "25.9.2",
33
+ "@types/node": "25.9.3",
34
34
  "@types/luxon": "3.7.1",
35
35
  "@types/which": "3.0.4",
36
36
  "@types/update-notifier": "6.0.8",
@@ -42,7 +42,7 @@
42
42
  },
43
43
  "dependencies": {
44
44
  "commander": "15.0.0",
45
- "@dotenvx/dotenvx": "1.71.2",
45
+ "@dotenvx/dotenvx": "1.71.3",
46
46
  "yaml": "2.9.0",
47
47
  "valibot": "1.4.1",
48
48
  "execa": "9.6.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ase",
3
- "version": "0.9.7",
3
+ "version": "0.9.9",
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.7",
3
+ "version": "0.9.9",
4
4
  "description": "Agentic Software Engineering (ASE)",
5
5
  "keywords": [ "agentic", "software", "engineering" ],
6
6
  "homepage": "https://ase.tools",
@@ -62,9 +62,9 @@ Workflow
62
62
  6. Set <context-after/> to exactly *up to two* lines of
63
63
  *unchanged* text content which occurs in the document
64
64
  directly *after* <old-text/>, i.e., the lines (<line/>
65
- + <n/> + 1) and (<line/> + <n/> + 2), where <n/> is the
65
+ + <n/>) and (<line/> + <n/> + 1), where <n/> is the
66
66
  number of lines in <old-text/>. Reduce to just one line
67
- (<line/> + <n/> + 1) if <old-text/> is the second-last
67
+ (<line/> + <n/>) if <old-text/> is the second-last
68
68
  line of the document. Set <context-after/> to empty if
69
69
  <old-text/> is the last line in the document.
70
70
 
@@ -16,6 +16,13 @@ you *MUST* once and immediately output the following <template/> now:
16
16
  ⧉ **ASE**: ☯ persona: **<ase-persona-style/>**
17
17
  </template>
18
18
 
19
+ In case your harness instructions indicate that the user is shown
20
+ only the *final* text message of each turn (e.g. *Claude Code* with
21
+ "focus mode" enabled), you *MUST* ensure this <template/> output (and
22
+ *every* <template/> output requested by ASE skills) lands in a final
23
+ text message (after the last tool call of a turn) instead of between
24
+ tool calls -- repeat it there if necessary.
25
+
19
26
  Prohibitions
20
27
  ------------
21
28
 
@@ -14,7 +14,7 @@ Control Flow Constructs
14
14
  Do not output anything.
15
15
 
16
16
  - *IMPORTANT*: You *MUST* honor the following control flow construct:
17
- <expand name="<define-name/>" [arg1="<expand-arg1/>" [arg2="<expand-arg2/>" [...]]]><expand-content/></expand>:
17
+ <expand name="<define-name/>" [arg1="<expand-arg1/>" [arg2="<expand-arg2/>" [...]]]>[<expand-content/>]</expand>:
18
18
 
19
19
  This specifies the *expansion* of previous <define/>. This
20
20
  construct is expanded to the <define-body/> of <define/> with
@@ -48,14 +48,35 @@ Control Flow Constructs
48
48
 
49
49
  This specifies a simple condition which is expanded to <if-body/>
50
50
  if <if-condition/> is met, or to empty string if <if-condition/> is
51
- *not* met. Do not output anything else.
51
+ *not* met. It can be optionally followed by one or more <elseif/>
52
+ constructs and/or one final <else/> construct. Do not output anything else.
53
+
54
+ - *IMPORTANT*: You *MUST* honor the following control flow construct:
55
+ <elseif condition="<elseif-condition/>"><elseif-body/></elseif>:
56
+
57
+ This specifies an *alternative condition* and has to directly
58
+ follow an <if/> or another <elseif/> construct. It is expanded
59
+ to <elseif-body/> if the conditions of all preceding <if/> and
60
+ <elseif/> constructs of the chain were *not* met and its own
61
+ <elseif-condition/> is met, or to the empty string otherwise.
62
+ Do not output anything else.
63
+
64
+ - *IMPORTANT*: You *MUST* honor the following control flow construct:
65
+ <else><else-body/></else>:
66
+
67
+ This specifies the *fallback alternative* and has to directly
68
+ follow an <if/> or <elseif/> construct. It is expanded to
69
+ <else-body/> if the conditions of all preceding <if/> and
70
+ <elseif/> constructs of the chain were *not* met, or to the empty
71
+ string otherwise. Do not output anything else.
52
72
 
53
73
  - *IMPORTANT*: You *MUST* honor the following control flow construct:
54
74
  <while condition="<while-condition/>"><while-body/></while>:
55
75
 
56
76
  This specifies a <while-body/> which is *repeated* as long as
57
77
  <while-condition/> is met. This construct is expanded to the
58
- repetition of <while-body/>. Do not output anything else.
78
+ repetition of <while-body/>. A <break/> in <while-body/> can stop
79
+ the repetition early. Do not output anything else.
59
80
 
60
81
  - *IMPORTANT*: You *MUST* honor the following control flow construct:
61
82
  <for items="<for-item/> [...]"><for-body/></for>:
@@ -1,5 +1,4 @@
1
1
 
2
-
3
2
  User Dialog
4
3
  ===========
5
4
 
@@ -15,13 +14,16 @@ User Dialog
15
14
  Let the *user interactively choose* an answer.
16
15
 
17
16
  1. Take the following question specification:
17
+
18
18
  <spec>
19
19
  <content/>
20
20
  </spec>
21
21
 
22
- Each line of <spec/> (separated by newlines) is of the format:
22
+ The first line of <spec/> (separated by newlines) is of the format:
23
+ `<question-label/>: <question-description/>`
23
24
 
24
- `<label/>: <description/>`.
25
+ The second and following lines of <spec/> (separated by newlines) are of the format:
26
+ `<label/>: <description/>`
25
27
 
26
28
  The first line provides the question label and the question
27
29
  description. The second and following lines each provide an
@@ -49,8 +51,8 @@ Let the *user interactively choose* an answer.
49
51
 
50
52
  If <n/> is less than 2:
51
53
  Set <result>ERROR: user-dialog requires 2-4 answer lines, got <n/></result>
52
- and *SKIP* the following step 2 (do not call `AskUserQuestion`)
53
- and continue with step 3 dispatch.
54
+ and *SKIP* the following step 2.2 (do not call `AskUserQuestion`)
55
+ and continue with step 2.3 dispatch.
54
56
 
55
57
  2. Call the `AskUserQuestion` tool of the agent harness with:
56
58
 
@@ -103,8 +105,8 @@ Let the *user interactively choose* an answer.
103
105
 
104
106
  If <n/> is less than 2:
105
107
  Set <result>ERROR: user-dialog requires 2-4 answer lines, got <n/></result>
106
- and *SKIP* the following step 2 (do not call `ask_user`)
107
- and continue with step 3 dispatch.
108
+ and *SKIP* the following step 2.2 (do not call `ask_user`)
109
+ and continue with step 2.3 dispatch.
108
110
 
109
111
  2. Call the `ask_user` tool of the agent harness with:
110
112
 
@@ -137,3 +139,99 @@ Let the *user interactively choose* an answer.
137
139
 
138
140
  </define>
139
141
 
142
+ <define name="custom-dialog">
143
+ Let the *user interactively choose* an answer.
144
+
145
+ 1. Take the following question specification:
146
+
147
+ <spec>
148
+ <content/>
149
+ </spec>
150
+
151
+ The first line of <spec/> (separated by newlines) is of the format:
152
+ `<question-label/>: <question-description/>`
153
+
154
+ The second and following lines of <spec/> (separated by newlines) are of the format:
155
+ `<label/>: <description/>`
156
+
157
+ The first line provides the question label and the question
158
+ description. The second and following lines each provide an
159
+ answer label and an answer description.
160
+
161
+ Do not output anything in this step!
162
+
163
+ 2. Dispatch according to the agent tool:
164
+
165
+ 1. You *MUST* not output anything in this step.
166
+
167
+ Set <text></text> (set to empty).
168
+ Set <keys></keys> (set to empty).
169
+ Set <n>1</n> (set entry count to one).
170
+ Set <width/> to the maximum length plus 3 of all <label/> strings in <spec/>.
171
+
172
+ <for items="2 3 4 5 6 7 8 9">
173
+ Take from <spec/> the line number <item/>.
174
+ If this line does not exist, <break/>.
175
+ If this line exists, parse it according to the format `<label/>: <description/>`.
176
+ Set <label-key/> to <ase-tpl-key digit="<n/>"/>.
177
+ Set <label-text/> to `<ase-tpl-pad width="<width/>" text="<label/>:"/>`.
178
+ Append an entry to <text/>:
179
+
180
+ <text>
181
+ <text/>
182
+ <ase-tpl-boxline><label-key/> ▶ **<label-text/>** <description/></ase-tpl-boxline>
183
+ </text>
184
+
185
+ Set <n/> to <n/> + 1 (increment entry count).
186
+ <if condition="<keys/> is empty">
187
+ Set <keys><label-key/></keys>
188
+ </if>
189
+ <else>
190
+ Set <keys><keys/>/<label-key/></keys>
191
+ </else>
192
+ </for>
193
+
194
+ Set:
195
+
196
+ <text>
197
+ <ase-tpl-boxed title="QUESTION" subtitle="<question-label/>">
198
+
199
+ <ase-tpl-boxline>**<question-description/>**</ase-tpl-boxline>
200
+
201
+ <text/>
202
+
203
+ Please choose *one* option by typing <keys/>/**CANCEL** or free-text instruction.
204
+
205
+ </ase-tpl-boxed>
206
+ </text>
207
+
208
+ If <n/> is less than 3:
209
+ Set <result>ERROR: user-dialog requires 2-8 answer lines, got less</result>
210
+ and *SKIP* the following step 2 and continue with step 3 dispatch.
211
+
212
+ 2. Output the following <template/>, end the current turn, wait for the
213
+ user input, store the user input in <result/> and then continue with step 3:
214
+
215
+ <template>
216
+ <text/>
217
+ </template>
218
+
219
+ 3. Check the result and dispatch accordingly:
220
+
221
+ - If <result/> indicates that the user doesn't want to proceed,
222
+ or the user declined to answer the question, or that the dialog
223
+ was cancelled, rejected or skipped, set <result>CANCEL</result>.
224
+
225
+ - Otherwise, determine the selected <label/>
226
+ by mapping the <result/> (usually containing one of the
227
+ "key" or "label" strings) to one of the answer labels. Set
228
+ <result><label/></result>.
229
+
230
+ If <result/> is then *NOT* one of the "label" values from
231
+ <spec/>, set <result>OTHER: <result/></result> (prefix
232
+ result with "OTHER").
233
+
234
+ Do not output anything in this step!
235
+
236
+ </define>
237
+
@@ -8,8 +8,8 @@ Every *task* uses a strict and fixed format:
8
8
 
9
9
  # TASK <task-id/>: <title/>
10
10
 
11
- Created: <timestamp-created/>
12
- Modified: <timestamp-modified/>
11
+ Created: <timestamp-created/>
12
+ Modified: <timestamp-modified/>
13
13
 
14
14
  ## CONTEXT
15
15
 
@@ -8,30 +8,39 @@ Do not output anything in the following steps. The entire purpose is to
8
8
  set placeholders into the context as a side-effect.
9
9
 
10
10
  1. **Determine Parameters**:
11
- Set <getopt-skill><arg1/></getopt-skill>.
12
- Set <getopt-spec>--help|-h <arg2/></getopt-spec>.
13
- Set <getopt-args><content/></getopt-args>.
11
+ Set <getopt-skill><arg1/></getopt-skill>
12
+ Set <getopt-spec>--help|-h <arg2/></getopt-spec>
13
+ Set <getopt-args><content/></getopt-args>
14
14
 
15
15
  2. **Short-Circuit Processing**:
16
- If <getopt-args/> does *NOT* match the regexp `(^|\s)-` (i.e.
17
- contains no options at all):
18
-
19
- For each option token in <getopt-spec/> of the form
20
- `--<long/>[|-<short/>][=<default/>|=(<c1/>|<c2/>|...)[...]]`, set
21
- <getopt-option-<long/>/> to <default/> (for `=<default/>`
22
- form), or to <c1/> (the first choice, for `=(<c1/>|<c2>/|...)`
23
- form, or for the list form `=(<c1/>|<c2>/|...)...`),
24
- or to `false` (for value-less options). Then set
25
- <getopt-arguments><getopt-args/></getopt-arguments>.
26
-
27
- Additionally, simulate <getopt-info/> as a comma-separated
28
- markdown rendering of the parsed options in the form `<longN/>:
29
- **<valueN/>**, [...]` (joined with `, `, with each value
30
- shell-quoted if value contains spaces or special characters, and
31
- excluding the `help` option).
32
-
33
- Then silently *SKIP* only the following steps 3-6
34
- and proceed directly to step 7 to display the results.
16
+ You *MUST* decide here, via the following *mandatory* <if/> control
17
+ construct, whether the options are parsed *locally* (no MCP call) or
18
+ *remotely* (via the MCP call in steps 3-6). This is a *hard branch*,
19
+ not an optional optimization: when the <if/> branch is taken, steps
20
+ 3-6 are *structurally unreachable* and you *MUST NOT* call the
21
+ `ase_getopt` MCP tool under any circumstances.
22
+
23
+ <if condition="<getopt-args/> does *NOT* match the regexp `(^|\s)-` (i.e. it does not start with an option)">
24
+ Parse the options *locally*, without any MCP call:
25
+
26
+ For each option token in <getopt-spec/> of the form
27
+ `--<long/>[|-<short/>][=<default/>|=(<c1/>|<c2/>|...)[...]]`, set
28
+ <getopt-option-<long/>/> to <default/> (for `=<default/>`
29
+ form), or to <c1/> (the first choice, for `=(<c1/>|<c2>/|...)`
30
+ form, or for the list form `=(<c1/>|<c2>/|...)...`),
31
+ or to `false` (for value-less options). Then set
32
+ <getopt-arguments><getopt-args/></getopt-arguments>.
33
+
34
+ Additionally, simulate <getopt-info/> as a comma-separated
35
+ markdown rendering of the parsed options in the form `<longN/>:
36
+ **<valueN/>**, [...]` (joined with `, `, with each value
37
+ shell-quoted if value contains spaces or special characters, and
38
+ excluding the `help` option and any *internal* option whose long
39
+ name starts with `int-`).
40
+
41
+ You then *MUST* silently *SKIP* the steps 3-6 below
42
+ and proceed directly to step 7 to display the results.
43
+ </if>
35
44
 
36
45
  3. **MCP Call**:
37
46
  Call the `ase_getopt(name: "<getopt-skill/>", spec: