@rse/ase 2.0.1 → 2.0.3

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 (155) hide show
  1. package/dst/ase-agent-definition-core.js +111 -87
  2. package/dst/ase-agent-fleet.js +1 -1
  3. package/dst/ase-agent-runtime-claim.js +5 -3
  4. package/dst/ase-agent-runtime-engine.js +1 -1
  5. package/dst/ase-agent-runtime-phase.js +1 -1
  6. package/dst/ase-agent-runtime-request.js +1 -1
  7. package/dst/ase-agent-runtime.js +3 -5
  8. package/dst/ase-artifact.js +3 -3
  9. package/dst/ase-config-core.js +2 -16
  10. package/dst/ase-config-schema.js +1 -3
  11. package/dst/ase-config.js +4 -1
  12. package/dst/ase-hook.js +14 -19
  13. package/dst/ase-lib-daemon-cli.js +117 -4
  14. package/dst/ase-lib-server-cli.js +10 -10
  15. package/dst/ase-mcp.js +4 -4
  16. package/dst/ase-service-getopt.js +2 -2
  17. package/dst/ase-service-otel.js +4 -4
  18. package/dst/ase-service-skills.js +5 -15
  19. package/dst/ase-service.js +87 -210
  20. package/dst/ase-setup-common.js +1 -2
  21. package/dst/ase-setup-settings.js +6 -9
  22. package/dst/ase-setup.js +2 -3
  23. package/dst/ase-statusline.js +170 -170
  24. package/dst/ase-task-board-core.js +1 -1
  25. package/dst/ase-task-board-flow.js +7 -6
  26. package/dst/ase-task-board-graph.js +5 -4
  27. package/dst/ase-task-board-tui-control.js +51 -45
  28. package/dst/ase-task-board-tui-model.js +195 -181
  29. package/dst/ase-task-board-tui-popup.js +2 -3
  30. package/dst/ase-task-board-tui-view.js +88 -79
  31. package/dst/ase-task-board-web-client.css +1 -1
  32. package/dst/ase-task-board-web-client.js +2 -2
  33. package/dst/ase-task-board-web-server.js +4 -27
  34. package/dst/ase-task-board.js +1 -1
  35. package/dst/ase-task-format.js +1 -1
  36. package/dst/ase-task-store-client.js +17 -5
  37. package/dst/ase-task-store-core.js +5 -7
  38. package/dst/ase-task-store-issues.js +91 -0
  39. package/dst/ase-task-store-plugin-ase.js +7 -3
  40. package/dst/ase-task-store-plugin-gitea.js +11 -97
  41. package/dst/ase-task-store-plugin-github.js +5 -77
  42. package/dst/ase-task-store-plugin-gitlab.js +4 -83
  43. package/dst/ase-task.js +51 -23
  44. package/dst/ase-util-compat.js +1 -1
  45. package/dst/ase-util-diagram.js +16 -42
  46. package/dst/ase.js +5 -5
  47. package/dst/man/ase-agent-adhoc.1 +1 -1
  48. package/dst/man/ase-agent-condenser.1 +1 -1
  49. package/dst/man/ase-agent-editor.1 +1 -1
  50. package/dst/man/ase-agent-estimator.1 +1 -1
  51. package/dst/man/ase-agent-fleet.1 +1 -1
  52. package/dst/man/ase-agent-implementer.1 +1 -1
  53. package/dst/man/ase-agent-integrator.1 +1 -1
  54. package/dst/man/ase-agent-qualifier.1 +1 -1
  55. package/dst/man/ase-agent-rebooter.1 +1 -1
  56. package/dst/man/ase-agent-relay.1 +1 -1
  57. package/dst/man/ase-agent-researcher.1 +1 -1
  58. package/dst/man/ase-agent.1 +1 -1
  59. package/dst/man/ase-artifact-list.1 +1 -1
  60. package/dst/man/ase-artifact-name.1 +1 -1
  61. package/dst/man/ase-artifact.1 +1 -1
  62. package/dst/man/ase-config-delete.1 +1 -1
  63. package/dst/man/ase-config-edit.1 +1 -1
  64. package/dst/man/ase-config-get.1 +1 -1
  65. package/dst/man/ase-config-init.1 +1 -1
  66. package/dst/man/ase-config-list.1 +1 -1
  67. package/dst/man/ase-config-set.1 +1 -1
  68. package/dst/man/ase-config.1 +1 -1
  69. package/dst/man/ase-hook-permission-request.1 +1 -1
  70. package/dst/man/ase-hook-pre-tool-use.1 +1 -1
  71. package/dst/man/ase-hook-session-end.1 +1 -1
  72. package/dst/man/ase-hook-session-persona.1 +1 -1
  73. package/dst/man/ase-hook-session-start.1 +1 -1
  74. package/dst/man/ase-hook-stop.1 +1 -1
  75. package/dst/man/ase-hook-user-prompt-submit.1 +1 -1
  76. package/dst/man/ase-hook.1 +1 -1
  77. package/dst/man/ase-man.1 +1 -1
  78. package/dst/man/ase-mcp.1 +1 -1
  79. package/dst/man/ase-service-log.1 +1 -1
  80. package/dst/man/ase-service-send.1 +1 -1
  81. package/dst/man/ase-service-start.1 +1 -1
  82. package/dst/man/ase-service-status.1 +1 -1
  83. package/dst/man/ase-service-stop.1 +1 -1
  84. package/dst/man/ase-service.1 +1 -1
  85. package/dst/man/ase-setup-disable.1 +1 -1
  86. package/dst/man/ase-setup-enable.1 +1 -1
  87. package/dst/man/ase-setup-install.1 +1 -1
  88. package/dst/man/ase-setup-mcp.1 +1 -1
  89. package/dst/man/ase-setup-status.1 +1 -1
  90. package/dst/man/ase-setup-statusline.1 +1 -1
  91. package/dst/man/ase-setup-telemetry.1 +1 -1
  92. package/dst/man/ase-setup-uninstall.1 +1 -1
  93. package/dst/man/ase-setup-update.1 +1 -1
  94. package/dst/man/ase-setup.1 +1 -1
  95. package/dst/man/ase-spec-export.1 +1 -1
  96. package/dst/man/ase-spec-lint.1 +1 -1
  97. package/dst/man/ase-spec-preview.1 +1 -1
  98. package/dst/man/ase-spec.1 +1 -1
  99. package/dst/man/ase-statusline.1 +1 -1
  100. package/dst/man/ase-task-assign.1 +1 -1
  101. package/dst/man/ase-task-board.1 +3 -3
  102. package/dst/man/ase-task-delete.1 +1 -1
  103. package/dst/man/ase-task-edit.1 +1 -1
  104. package/dst/man/ase-task-idscheme.1 +1 -1
  105. package/dst/man/ase-task-lifecycle.1 +1 -1
  106. package/dst/man/ase-task-list.1 +1 -1
  107. package/dst/man/ase-task-load.1 +1 -1
  108. package/dst/man/ase-task-newid.1 +1 -1
  109. package/dst/man/ase-task-purge.1 +1 -1
  110. package/dst/man/ase-task-rename.1 +1 -1
  111. package/dst/man/ase-task-request.1 +1 -1
  112. package/dst/man/ase-task-save.1 +1 -1
  113. package/dst/man/ase-task-status.1 +1 -1
  114. package/dst/man/ase-task-store.1 +1 -1
  115. package/dst/man/ase-task-view.1 +1 -1
  116. package/dst/man/ase-task.1 +1 -1
  117. package/dst/man/ase-util-compat.1 +1 -1
  118. package/dst/man/ase-util-diagram.1 +1 -1
  119. package/dst/man/ase-util-meta.1 +1 -1
  120. package/dst/man/ase-util-metric.1 +1 -1
  121. package/dst/man/ase-util-mint.1 +1 -1
  122. package/dst/man/ase-util-worktree.1 +1 -1
  123. package/dst/man/ase-util.1 +1 -1
  124. package/dst/man/ase.1 +1 -1
  125. package/package.json +1 -1
  126. package/plugin/.claude-plugin/plugin.json +1 -1
  127. package/plugin/.codex-plugin/plugin.json +1 -1
  128. package/plugin/.github/plugin/plugin.json +1 -1
  129. package/plugin/meta/ase-common-resolve.md +2 -2
  130. package/plugin/meta/ase-skill.md +7 -0
  131. package/plugin/mods/ase-mods.ts +16 -12
  132. package/plugin/skills/ase-arch-analyze/SKILL.md +3 -1
  133. package/plugin/skills/ase-code-analyze/SKILL.md +26 -10
  134. package/plugin/skills/ase-code-analyze/help.md +11 -6
  135. package/plugin/skills/ase-code-edit/SKILL.md +5 -4
  136. package/plugin/skills/ase-code-edit/help.md +1 -1
  137. package/plugin/skills/ase-code-lint/SKILL.md +52 -27
  138. package/plugin/skills/ase-code-lint/help.md +10 -5
  139. package/plugin/skills/ase-help-intent/data.md +60 -30
  140. package/plugin/skills/ase-help-skill/help.md +3 -4
  141. package/plugin/skills/ase-meta-quorum/SKILL.md +9 -0
  142. package/plugin/skills/ase-meta-quorum/help.md +5 -3
  143. package/plugin/skills/ase-meta-workflow/sample.md +5 -3
  144. package/plugin/skills/ase-repo-changelog/SKILL.md +8 -9
  145. package/plugin/skills/ase-repo-changelog/help.md +2 -2
  146. package/plugin/skills/ase-repo-review/SKILL.md +5 -3
  147. package/plugin/skills/ase-repo-review/help.md +3 -1
  148. package/plugin/skills/ase-spec-edit/SKILL.md +2 -1
  149. package/plugin/skills/ase-spec-edit/help.md +2 -1
  150. package/plugin/skills/ase-sync-reconcile/help.md +4 -4
  151. package/plugin/skills/ase-task-dissect/SKILL.md +10 -6
  152. package/plugin/skills/ase-task-grill/help.md +1 -1
  153. package/plugin/skills/ase-task-implement/SKILL.md +58 -1
  154. package/plugin/skills/ase-task-implement/help.md +16 -0
  155. package/plugin/skills/ase-task-list/help.md +2 -2
@@ -47,7 +47,7 @@ export const discoverKinds = (log, reserved) => {
47
47
  }
48
48
  const seen = new Set();
49
49
  const rank = (entry) => definitionExts.indexOf(path.extname(entry).toLowerCase());
50
- for (const entry of entries.filter((entry) => rank(entry) >= 0).sort((a, b) => rank(a) - rank(b))) {
50
+ for (const entry of entries.filter((e) => rank(e) >= 0).sort((a, b) => rank(a) - rank(b))) {
51
51
  const kind = path.basename(entry, path.extname(entry));
52
52
  const file = path.join(dir, entry);
53
53
  if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(kind) || kind.length > 64)
@@ -403,11 +403,11 @@ const loadDefinition = async (spec, chain = []) => {
403
403
  if (spec.startsWith("builtin:")) {
404
404
  const where = `agent: option "--definition": definition "${spec}"`;
405
405
  const m = /^builtin:([a-z]+)(?:-([a-z]+))?$/.exec(spec);
406
- const kind = builtinKinds.find((kind) => kind.kind === m?.[1]);
406
+ const kind = builtinKinds.find((k) => k.kind === m?.[1]);
407
407
  const mode = m?.[2] ?? kind?.mode ?? "";
408
408
  if (m === null || kind === undefined || !Object.hasOwn(kind.modes, mode))
409
409
  throw new Error(`${where}: unknown built-in definition (expected ` +
410
- builtinKinds.flatMap((kind) => Object.keys(kind.modes).map((mode) => `"builtin:${kind.kind}-${mode}"`)).join(", ") + ")");
410
+ builtinKinds.flatMap((k) => Object.keys(k.modes).map((n) => `"builtin:${k.kind}-${n}"`)).join(", ") + ")");
411
411
  id = `builtin:${kind.kind}-${mode}`;
412
412
  loaded = definitionValues(kind.modes[mode], where, "");
413
413
  }
@@ -576,6 +576,106 @@ const resolveStates = (str, list, has, type, unclaimed) => {
576
576
  }
577
577
  return { incoming, activity, success, failure, phase, passed, verdicts, lifecycles };
578
578
  };
579
+ /* resolve the prompt of the processing type <type> through the option accessors <str>
580
+ and <has>: the skill (if any) and the parsed template of its arguments, of the prompt
581
+ text, or of the prompt file, with the declared parameters <params> */
582
+ const resolvePrompt = (str, has, type, params) => {
583
+ const fail = failOption;
584
+ const prompts = ["prompt-skill", "prompt-text", "prompt-file"].filter((key) => has(key));
585
+ if (prompts.length !== 1)
586
+ fail(prompts[1] ?? "prompt-skill", "exactly one of \"--prompt-skill\", \"--prompt-text\", or \"--prompt-file\" is required");
587
+ if (has("prompt-args") && !has("prompt-skill"))
588
+ fail("prompt-args", "allowed with \"--prompt-skill\" only");
589
+ const skill = str("prompt-skill");
590
+ if (has("prompt-skill") && !/^(?:[A-Za-z0-9_.-]+:)?[A-Za-z0-9_.-]+$/.test(skill))
591
+ fail("prompt-skill", `invalid skill name "${skill}"`);
592
+ let template;
593
+ if (has("prompt-skill"))
594
+ template = parseTemplate(str("prompt-args", type === "phase" ? "{id}" : "{id}: {prompt}"), "prompt-args", type, params);
595
+ else if (has("prompt-text"))
596
+ template = parseTemplate(str("prompt-text"), "prompt-text", type, params);
597
+ else {
598
+ let text = "";
599
+ try {
600
+ text = fs.readFileSync(path.resolve(str("prompt-file")), "utf8");
601
+ }
602
+ catch (err) {
603
+ fail("prompt-file", `cannot read prompt file: ${err instanceof Error ? err.message : String(err)}`);
604
+ }
605
+ template = parseTemplate(text, "prompt-file", type, params);
606
+ }
607
+ return { skill, template };
608
+ };
609
+ /* resolve the success criteria, the output line matches, and the accepted verdicts of the
610
+ processing type <type> through the option accessors <list> and <has>, validated against
611
+ the determination of a verdict <verdicting> and the target states per verdict <verdicts> */
612
+ const resolveSuccess = (list, has, type, unclaimed, verdicting, verdicts) => {
613
+ const fail = failOption;
614
+ const criteria = has("success-criterion") ? [...new Set(list("success-criterion"))] :
615
+ (type === "phase" ? ["exit", "clean"] : ["clean", "changed"]);
616
+ for (const criterion of criteria) {
617
+ if (!definitionCriteria.includes(criterion))
618
+ fail("success-criterion", `invalid criterion "${criterion}" ` +
619
+ `(expected ${definitionCriteria.map((c) => `"${c}"`).join(", ")})`);
620
+ else if (criterion === "changed" && type !== "request")
621
+ fail("success-criterion", "criterion \"changed\" is for \"--type request\" only");
622
+ else if (criterion === "status" && type !== "phase")
623
+ fail("success-criterion", "criterion \"status\" is for \"--type phase\" only");
624
+ else if (criterion === "status" && unclaimed)
625
+ fail("success-criterion", "criterion \"status\" is not for \"--unclaimed\" (as an unclaimed task has no phase success state)");
626
+ else if (criterion === "verdict" && !verdicting)
627
+ fail("success-criterion", "criterion \"verdict\" requires \"--verdict-label\" (or the hook \"verdict\")");
628
+ else if (criterion === "attachment" && !has("attach-expect"))
629
+ fail("success-criterion", "criterion \"attachment\" requires \"--attach-expect\"");
630
+ }
631
+ const match = list("success-match").map((spec) => {
632
+ try {
633
+ return new RegExp(spec);
634
+ }
635
+ catch (err) {
636
+ return fail("success-match", `invalid regular expression "${spec}": ${err instanceof Error ? err.message : String(err)}`);
637
+ }
638
+ });
639
+ if ((Object.keys(verdicts).length > 0 || has("verdict-allow")) && !verdicting)
640
+ fail(Object.keys(verdicts).length > 0 ? "state-verdict" : "verdict-allow", "requires \"--verdict-label\" (or the hook \"verdict\")");
641
+ const allow = has("verdict-allow") ? list("verdict-allow") : Object.keys(verdicts);
642
+ for (const verdict of allow)
643
+ if (!/^[A-Z]+$/.test(verdict))
644
+ fail("verdict-allow", `invalid verdict "${verdict}" (expected an upper-case word)`);
645
+ for (const verdict of Object.keys(verdicts))
646
+ if (!allow.includes(verdict))
647
+ fail("state-verdict", `verdict "${verdict}" is not accepted by "--verdict-allow"`);
648
+ return { criteria: criteria, match, allow };
649
+ };
650
+ /* resolve the attachments through the option accessors <str> and <has>: the expected
651
+ attachment and the attachment leaving a task unclaimed, validated together with the
652
+ kinds of the output and change set attachments */
653
+ const resolveAttach = (str, has) => {
654
+ const fail = failOption;
655
+ const kindRe = /^[A-Za-z0-9][A-Za-z0-9_.:-]*$/;
656
+ const attachSpec = (key) => {
657
+ if (!has(key))
658
+ return null;
659
+ const spec = str(key);
660
+ const i = spec.indexOf(";");
661
+ const mime = i < 0 ? "" : spec.slice(0, i).trim().toLowerCase();
662
+ const kind = i < 0 ? spec.trim() : spec.slice(i + 1).trim();
663
+ if (!kindRe.test(kind) || (i >= 0 && !/^[a-z0-9.+-]+\/[a-z0-9.+-]+$/.test(mime)))
664
+ fail(key, `invalid attachment "${spec}" (expected "[<mime-type>;]<kind>")`);
665
+ return { mime, kind };
666
+ };
667
+ if (has("attach-output") && !kindRe.test(str("attach-output")))
668
+ fail("attach-output", `invalid kind "${str("attach-output")}"`);
669
+ else if (str("attach-output") === "agent")
670
+ fail("attach-output", "kind \"agent\" is reserved for the processing log");
671
+ if (has("attach-changeset") && !kindRe.test(str("attach-changeset")))
672
+ fail("attach-changeset", `invalid kind "${str("attach-changeset")}"`);
673
+ const expect = attachSpec("attach-expect");
674
+ if (expect !== null && has("attach-output")
675
+ && attachMatches(expect, `text/plain; kind="${str("attach-output")}"`))
676
+ fail("attach-expect", `attachment "${str("attach-expect")}" is always met by the output attachment of "--attach-output"`);
677
+ return { expect, skip: attachSpec("attach-skip") };
678
+ };
579
679
  /* resolve the definition of the agent named <name> from its (camel-cased) command-line
580
680
  options <opts>: the defaults, overridden by the definition of "--definition" (if any),
581
681
  overridden by the given options, validated against the task lifecycle models */
@@ -707,88 +807,12 @@ export const resolveDefinition = async (opts, name) => {
707
807
  /* the parameters (see "resolveParams") */
708
808
  const decls = loaded.params;
709
809
  const params = resolveParams(decls, list("params"));
710
- /* the prompt */
711
- const prompts = ["prompt-skill", "prompt-text", "prompt-file"].filter((key) => has(key));
712
- if (prompts.length !== 1)
713
- fail(prompts[1] ?? "prompt-skill", "exactly one of \"--prompt-skill\", \"--prompt-text\", or \"--prompt-file\" is required");
714
- if (has("prompt-args") && !has("prompt-skill"))
715
- fail("prompt-args", "allowed with \"--prompt-skill\" only");
716
- const skill = str("prompt-skill");
717
- if (has("prompt-skill") && !/^(?:[A-Za-z0-9_.-]+:)?[A-Za-z0-9_.-]+$/.test(skill))
718
- fail("prompt-skill", `invalid skill name "${skill}"`);
719
- let template;
720
- if (has("prompt-skill"))
721
- template = parseTemplate(str("prompt-args", type === "phase" ? "{id}" : "{id}: {prompt}"), "prompt-args", type, Object.keys(decls));
722
- else if (has("prompt-text"))
723
- template = parseTemplate(str("prompt-text"), "prompt-text", type, Object.keys(decls));
724
- else {
725
- let text = "";
726
- try {
727
- text = fs.readFileSync(path.resolve(str("prompt-file")), "utf8");
728
- }
729
- catch (err) {
730
- fail("prompt-file", `cannot read prompt file: ${err instanceof Error ? err.message : String(err)}`);
731
- }
732
- template = parseTemplate(text, "prompt-file", type, Object.keys(decls));
733
- }
734
- /* the success criteria and the verdict */
735
- const criteria = has("success-criterion") ? [...new Set(list("success-criterion"))] :
736
- (type === "phase" ? ["exit", "clean"] : ["clean", "changed"]);
737
- for (const criterion of criteria) {
738
- if (!definitionCriteria.includes(criterion))
739
- fail("success-criterion", `invalid criterion "${criterion}" ` +
740
- `(expected ${definitionCriteria.map((c) => `"${c}"`).join(", ")})`);
741
- else if (criterion === "changed" && type !== "request")
742
- fail("success-criterion", "criterion \"changed\" is for \"--type request\" only");
743
- else if (criterion === "status" && type !== "phase")
744
- fail("success-criterion", "criterion \"status\" is for \"--type phase\" only");
745
- else if (criterion === "status" && unclaimed)
746
- fail("success-criterion", "criterion \"status\" is not for \"--unclaimed\" (as an unclaimed task has no phase success state)");
747
- else if (criterion === "verdict" && !verdicting)
748
- fail("success-criterion", "criterion \"verdict\" requires \"--verdict-label\" (or the hook \"verdict\")");
749
- else if (criterion === "attachment" && !has("attach-expect"))
750
- fail("success-criterion", "criterion \"attachment\" requires \"--attach-expect\"");
751
- }
752
- const match = list("success-match").map((spec) => {
753
- try {
754
- return new RegExp(spec);
755
- }
756
- catch (err) {
757
- return fail("success-match", `invalid regular expression "${spec}": ${err instanceof Error ? err.message : String(err)}`);
758
- }
759
- });
760
- if ((Object.keys(verdicts).length > 0 || has("verdict-allow")) && !verdicting)
761
- fail(Object.keys(verdicts).length > 0 ? "state-verdict" : "verdict-allow", "requires \"--verdict-label\" (or the hook \"verdict\")");
762
- const allow = has("verdict-allow") ? list("verdict-allow") : Object.keys(verdicts);
763
- for (const verdict of allow)
764
- if (!/^[A-Z]+$/.test(verdict))
765
- fail("verdict-allow", `invalid verdict "${verdict}" (expected an upper-case word)`);
766
- for (const verdict of Object.keys(verdicts))
767
- if (!allow.includes(verdict))
768
- fail("state-verdict", `verdict "${verdict}" is not accepted by "--verdict-allow"`);
769
- /* the attachments */
770
- const kindRe = /^[A-Za-z0-9][A-Za-z0-9_.:-]*$/;
771
- const attachSpec = (key) => {
772
- if (!has(key))
773
- return null;
774
- const spec = str(key);
775
- const i = spec.indexOf(";");
776
- const mime = i < 0 ? "" : spec.slice(0, i).trim().toLowerCase();
777
- const kind = i < 0 ? spec.trim() : spec.slice(i + 1).trim();
778
- if (!kindRe.test(kind) || (i >= 0 && !/^[a-z0-9.+-]+\/[a-z0-9.+-]+$/.test(mime)))
779
- fail(key, `invalid attachment "${spec}" (expected "[<mime-type>;]<kind>")`);
780
- return { mime, kind };
781
- };
782
- if (has("attach-output") && !kindRe.test(str("attach-output")))
783
- fail("attach-output", `invalid kind "${str("attach-output")}"`);
784
- else if (str("attach-output") === "agent")
785
- fail("attach-output", "kind \"agent\" is reserved for the processing log");
786
- if (has("attach-changeset") && !kindRe.test(str("attach-changeset")))
787
- fail("attach-changeset", `invalid kind "${str("attach-changeset")}"`);
788
- const attachExpect = attachSpec("attach-expect");
789
- if (attachExpect !== null && has("attach-output")
790
- && attachMatches(attachExpect, `text/plain; kind="${str("attach-output")}"`))
791
- fail("attach-expect", `attachment "${str("attach-expect")}" is always met by the output attachment of "--attach-output"`);
810
+ /* the prompt (see "resolvePrompt") */
811
+ const { skill, template } = resolvePrompt(str, has, type, Object.keys(decls));
812
+ /* the success criteria and the verdict (see "resolveSuccess") */
813
+ const { criteria, match, allow } = resolveSuccess(list, has, type, unclaimed, verdicting, verdicts);
814
+ /* the attachments (see "resolveAttach") */
815
+ const { expect: attachExpect, skip: attachSkip } = resolveAttach(str, has);
792
816
  for (const key of ["prepare-run", "finish-success", "finish-failure"])
793
817
  if ((has(key) && str(key, "-") === "") || list(key).some((cmd) => cmd.trim() === ""))
794
818
  fail(key, "empty shell command");
@@ -814,10 +838,10 @@ export const resolveDefinition = async (opts, name) => {
814
838
  request: { name: requestName, done: str("request-done", "processed"), prompt: consumes },
815
839
  prompt: { skill, template },
816
840
  prepare: list("prepare-run"),
817
- success: { criteria: criteria, match },
841
+ success: { criteria, match },
818
842
  verdict: { label: str("verdict-label"), allow },
819
843
  attach: {
820
- output: str("attach-output"), expect: attachExpect, skip: attachSpec("attach-skip"),
844
+ output: str("attach-output"), expect: attachExpect, skip: attachSkip,
821
845
  changeset: str("attach-changeset")
822
846
  },
823
847
  finish: { commit: flag("finish-commit"), success: str("finish-success"), failure: str("finish-failure") },
@@ -335,7 +335,7 @@ export default class AgentFleetCommand extends DaemonCommand {
335
335
  const { kind = name, ...options } = section(name);
336
336
  if (typeof kind !== "string" || !this.kinds.has(kind))
337
337
  throw new Error(`fleet: section "${name}": unknown agent kind "${String(kind)}" ` +
338
- `(expected ${kinds.map((name) => `"${name}"`).join(", ")})`);
338
+ `(expected ${kinds.map((k) => `"${k}"`).join(", ")})`);
339
339
  validateName(name, kind, kinds);
340
340
  /* the options of the definition of an agent can be nested by group, and the
341
341
  definition file of an ad-hoc agent (but not a built-in definition) is
@@ -51,7 +51,9 @@ export class AgentRun {
51
51
  record(run) {
52
52
  try {
53
53
  const data = JSON.parse(fs.readFileSync(this.file(run, ".json"), "utf8"));
54
- return { pid: Number(data.pid ?? 0), start: String(data.start ?? ""), claims: data.claims ?? {} };
54
+ const claims = data.claims !== null && typeof data.claims === "object" && !Array.isArray(data.claims) ?
55
+ data.claims : {};
56
+ return { pid: Number(data.pid ?? 0), start: String(data.start ?? ""), claims };
55
57
  }
56
58
  catch (_err) {
57
59
  return { pid: 0, start: "", claims: {} };
@@ -112,12 +114,12 @@ export class AgentRun {
112
114
  .filter((entry) => /\.(?:lock|json)$/.test(entry))
113
115
  .map((entry) => entry.replace(/\.(?:lock|json)$/, "")))];
114
116
  }
115
- /* the terminated runs of the agent kind for the task store, with their recorded claims */
117
+ /* the terminated runs of the agent kind for the task store */
116
118
  async dead() {
117
119
  const dead = [];
118
120
  for (const run of this.runs())
119
121
  if (!(await this.alive(run)))
120
- dead.push({ run, claims: this.record(run).claims });
122
+ dead.push(run);
121
123
  return dead;
122
124
  }
123
125
  /* seize a terminated run for its recovery, by taking over its (stale or removed) run lock,
@@ -412,7 +412,7 @@ class DefinedCore {
412
412
  };
413
413
  }
414
414
  catch (err) {
415
- await work.cleanup?.(log).catch((err2) => log("warning", err2 instanceof Error ? err2.message : String(err2)));
415
+ await work.cleanup?.(log).catch((err2) => log("warning", reason(err2)));
416
416
  throw err;
417
417
  }
418
418
  }
@@ -515,7 +515,7 @@ export class PhaseAgent extends Agent {
515
515
  is removed once all its tasks are recovered */
516
516
  async recover() {
517
517
  const { incoming, success } = this.states;
518
- for (const { run } of await this.runLock.dead()) {
518
+ for (const run of await this.runLock.dead()) {
519
519
  const claims = await this.runLock.seize(run);
520
520
  if (claims === null)
521
521
  continue;
@@ -100,7 +100,7 @@ export class RequestAgent extends Agent {
100
100
  });
101
101
  await this.write("warning", this.name, `task "${item.id}": ${this.requests.name} request of terminated ${this.role} marked as failed`);
102
102
  }
103
- for (const { run } of runs)
103
+ for (const run of runs)
104
104
  await this.runLock.purge(run);
105
105
  }
106
106
  /* select every task with a request (except one whose finalization is still retried),
@@ -840,9 +840,7 @@ export class Agent {
840
840
  const cli = toolSpecs[tool].cli;
841
841
  const cmd = job.cmd ?? "";
842
842
  const i = cmd.indexOf(" ");
843
- const args = toolArgs[tool](job.prompt ?? skillPrompt(tool, i < 0 ? cmd : cmd.slice(0, i), i < 0 ? "" : cmd.slice(i + 1)), (job.dirs ?? [])
844
- .filter((dir, i, dirs) => dir !== cwd && dirs.indexOf(dir) === i)
845
- .flatMap((dir) => ["--add-dir", dir]));
843
+ const args = toolArgs[tool](job.prompt ?? skillPrompt(tool, i < 0 ? cmd : cmd.slice(0, i), i < 0 ? "" : cmd.slice(i + 1)), [...new Set(job.dirs ?? [])].filter((dir) => dir !== cwd).flatMap((dir) => ["--add-dir", dir]));
846
844
  await log("info", `running: ${cli} ${args.map((arg) => arg.includes(" ") ? `"${arg}"` : arg).join(" ")}`);
847
845
  const started = Date.now();
848
846
  const costs = { costToken: null, costUSD: null, cread: 0, cwrite: 0, input: 0, apiMs: null };
@@ -1041,8 +1039,8 @@ export class Agent {
1041
1039
  ...Object.fromEntries(Object.entries(own).filter((entry) => entry[1] !== undefined))
1042
1040
  };
1043
1041
  this._name = opts.name ?? "";
1044
- if (this._name !== "" && !/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(this._name))
1045
- throw new Error(`agent: invalid name "${this._name}" (expected letters, digits, "_", ".", or "-")`);
1042
+ if (this._name !== "" && !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/.test(this._name))
1043
+ throw new Error(`agent: invalid name "${this._name}" (expected up to 64 letters, digits, "_", ".", or "-")`);
1046
1044
  this.relay = relay;
1047
1045
  this.tool = parseTool(opts.tool);
1048
1046
  for (const cli of [toolSpecs[this.tool].cli, ...(this.worktree ? ["git"] : [])])
@@ -149,7 +149,7 @@ export class Artifact {
149
149
  /* resolve a base-relative "filename" within a kind's "basedir" to a
150
150
  project-root-relative POSIX path; the implicit "othr" catch-all
151
151
  has no configured "basedir" and is therefore rejected */
152
- static name(log, kind, filename) {
152
+ static resolveName(log, kind, filename) {
153
153
  if (kind === "othr")
154
154
  throw new Error("artifact: kind \"othr\" has no configured basedir");
155
155
  const file = filename.replace(/\\/g, "/").replace(/^\/+/, "");
@@ -216,7 +216,7 @@ export default class ArtifactCommand {
216
216
  `(${configuredKinds.join("|")})`, "code")
217
217
  .action(async (filename, opts) => {
218
218
  const kind = Artifact.validateKind(opts.kind.trim());
219
- await writeStdout(`${Artifact.name(this.log, kind, filename)}\n`);
219
+ await writeStdout(`${Artifact.resolveName(this.log, kind, filename)}\n`);
220
220
  });
221
221
  }
222
222
  }
@@ -285,7 +285,7 @@ export class ArtifactMCP {
285
285
  }, async (args) => {
286
286
  try {
287
287
  const kind = Artifact.validateKind(args.kind ?? "code");
288
- const result = { name: Artifact.name(this.log, kind, args.filename) };
288
+ const result = { name: Artifact.resolveName(this.log, kind, args.filename) };
289
289
  return {
290
290
  structuredContent: result,
291
291
  content: [{ type: "text", text: JSON.stringify(result) }]
@@ -427,15 +427,8 @@ export class Config {
427
427
  next.setIn(prefix, next.createNode({}));
428
428
  }
429
429
  next.setIn(segments, value);
430
- const saved = td.doc;
430
+ this.validateDoc(next, td.filename, "strict");
431
431
  td.doc = next;
432
- try {
433
- this.validateDoc(td.doc, td.filename, "strict");
434
- }
435
- catch (err) {
436
- td.doc = saved;
437
- throw err;
438
- }
439
432
  }
440
433
  /* delete a value at a dotted key from the target scope */
441
434
  delete(key) {
@@ -445,15 +438,8 @@ export class Config {
445
438
  return;
446
439
  const next = td.doc.clone();
447
440
  next.deleteIn(this.resolveKey(key).split("."));
448
- const saved = td.doc;
441
+ this.validateDoc(next, td.filename, "strict");
449
442
  td.doc = next;
450
- try {
451
- this.validateDoc(td.doc, td.filename, "strict");
452
- }
453
- catch (err) {
454
- td.doc = saved;
455
- throw err;
456
- }
457
443
  }
458
444
  }
459
445
  /* access the internal "state.yaml" of a session, which, unlike the
@@ -5,9 +5,7 @@
5
5
  */
6
6
  import * as v from "valibot";
7
7
  import { foregroundColorNames } from "chalk";
8
- import { checkIdScheme } from "./ase-task-format.js";
9
- import { parseEffortScale } from "./ase-task-format.js";
10
- import { EFFORT_SCALE } from "./ase-task-format.js";
8
+ import { checkIdScheme, parseEffortScale, EFFORT_SCALE } from "./ase-task-format.js";
11
9
  /* classification taxonomy */
12
10
  export const projectClassification = {
13
11
  boxing: ["white", "grey", "black"],
package/dst/ase-config.js CHANGED
@@ -191,7 +191,10 @@ export class ConfigMCP {
191
191
  const cfg = new Config("config", configSchema, this.log, scope);
192
192
  cfg.read();
193
193
  const val = cfg.get(args.key);
194
- const text = val === undefined ? "" : JSON.stringify(val);
194
+ if (isMap(val))
195
+ throw new Error(`key "${args.key}" is not a leaf key`);
196
+ const text = val === undefined ? "" :
197
+ JSON.stringify(configSecretKeys.includes(cfg.resolveKey(args.key)) ? "***" : val);
195
198
  return { content: [{ type: "text", text }] };
196
199
  }
197
200
  catch (err) {
package/dst/ase-hook.js CHANGED
@@ -16,6 +16,8 @@ import { parseScope, userStateDir } from "./ase-config-scope.js";
16
16
  import { readStdin, writeStdout } from "./ase-lib-stdio.js";
17
17
  import { Task } from "./ase-task.js";
18
18
  import * as TaskFormat from "./ase-task-format.js";
19
+ import { parseTool } from "./ase-setup-common.js";
20
+ /* the addon MCP servers whose tools get auto-approved */
19
21
  const addonMcpServers = [
20
22
  "chat-alibaba-qwen",
21
23
  "chat-deepseek",
@@ -37,6 +39,7 @@ const addonMcpToolNamePattern = (prefix, suffix) => {
37
39
  .join("|");
38
40
  return new RegExp(`^${prefix}(?:${alternatives})${suffix}`);
39
41
  };
42
+ /* the per-tool dispatch table of the hook integrations */
40
43
  const toolSpecs = {
41
44
  "claude": {
42
45
  toolNameField: "tool_name",
@@ -88,7 +91,7 @@ export default class HookCommand {
88
91
  }
89
92
  /* validate a session id against the accepted character set */
90
93
  isValidSessionId(id) {
91
- return /^[A-Za-z0-9._-]+$/.test(id);
94
+ return /^[A-Za-z0-9._-]+$/.test(id) && id !== "." && id !== "..";
92
95
  }
93
96
  /* resolve the base directory holding all per-session state */
94
97
  sessionBaseDir() {
@@ -105,7 +108,7 @@ export default class HookCommand {
105
108
  try {
106
109
  entries = fs.readdirSync(base, { withFileTypes: true });
107
110
  }
108
- catch (_e) {
111
+ catch {
109
112
  /* best-effort: no base directory yet, or unreadable */
110
113
  return;
111
114
  }
@@ -120,7 +123,7 @@ export default class HookCommand {
120
123
  fs.rmSync(dir, { recursive: true, force: true });
121
124
  this.log.write("debug", `hook: pruned stale session directory: ${dir}`);
122
125
  }
123
- catch (_e) {
126
+ catch {
124
127
  /* best-effort: ignore vanished or undeletable directories */
125
128
  }
126
129
  }
@@ -142,7 +145,7 @@ export default class HookCommand {
142
145
  try {
143
146
  raw = JSON.parse(text);
144
147
  }
145
- catch (_e) {
148
+ catch {
146
149
  /* best-effort: return empty object on malformed JSON */
147
150
  return empty;
148
151
  }
@@ -167,7 +170,7 @@ export default class HookCommand {
167
170
  try {
168
171
  content = fs.readFileSync(abs, "utf8");
169
172
  }
170
- catch (_e) {
173
+ catch {
171
174
  return match;
172
175
  }
173
176
  const next = new Set(visited);
@@ -199,7 +202,7 @@ export default class HookCommand {
199
202
  try {
200
203
  return this.pluginRoot(tool);
201
204
  }
202
- catch (_e) {
205
+ catch {
203
206
  return "";
204
207
  }
205
208
  }
@@ -428,7 +431,7 @@ export default class HookCommand {
428
431
  ASE_HEADLESS: headless,
429
432
  ASE_AGENT_TOOL: tool
430
433
  });
431
- /* prepend ASE information to constitution markdown */
434
+ /* prepend ASE information to the constitution markdown (with its @<file> references expanded) */
432
435
  let context = `<ase-version>${version}</ase-version>\n` +
433
436
  `<ase-version-hint>${versionHint}</ase-version-hint>\n` +
434
437
  `<ase-plugin-root>${pluginRoot}</ase-plugin-root>\n` +
@@ -447,9 +450,7 @@ export default class HookCommand {
447
450
  `<ase-session-id>${sessionId}</ase-session-id>\n` +
448
451
  `<ase-headless>${headless}</ase-headless>\n` +
449
452
  `<ase-agent-tool>${tool}</ase-agent-tool>\n` +
450
- "\n" + md;
451
- /* expand all @<file> references manually */
452
- context = this.expandReferences(context, mdDir);
453
+ "\n" + this.expandReferences(md, mdDir);
453
454
  /* append the output style to the constitution markdown (GitHub
454
455
  Copilot CLI and OpenAI Codex CLI only -- Anthropic Claude Code CLI
455
456
  applies the plugin output style natively), stripping the
@@ -530,7 +531,7 @@ export default class HookCommand {
530
531
  try {
531
532
  fs.rmSync(dir, { recursive: true, force: true });
532
533
  }
533
- catch (_e) {
534
+ catch {
534
535
  /* best-effort: ignore failures */
535
536
  }
536
537
  /* detach the session from its AI usage window (if any), which closes
@@ -577,7 +578,7 @@ export default class HookCommand {
577
578
  });
578
579
  return val;
579
580
  }
580
- catch (_e) {
581
+ catch {
581
582
  return "";
582
583
  }
583
584
  }
@@ -706,12 +707,6 @@ export default class HookCommand {
706
707
  }
707
708
  return 0;
708
709
  }
709
- /* parse and validate the --tool option */
710
- parseTool(value) {
711
- if (value !== "claude" && value !== "copilot" && value !== "codex")
712
- throw new Error(`invalid --tool value: "${value}" (expected "claude", "copilot", or "codex")`);
713
- return value;
714
- }
715
710
  /* register commands */
716
711
  register(program) {
717
712
  /* default for --tool derived from ASE_TOOL environment variable
@@ -743,7 +738,7 @@ export default class HookCommand {
743
738
  .description(desc)
744
739
  .option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
745
740
  .action(async (opts) => {
746
- process.exitCode = await handler(this.parseTool(opts.tool));
741
+ process.exitCode = await handler(parseTool(opts.tool));
747
742
  });
748
743
  }
749
744
  }