@rse/ase 2.0.2 → 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 (135) 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 +2 -0
  12. package/dst/ase-hook.js +12 -12
  13. package/dst/ase-lib-daemon-cli.js +117 -4
  14. package/dst/ase-lib-server-cli.js +4 -5
  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-flow.js +7 -6
  25. package/dst/ase-task-board-graph.js +5 -4
  26. package/dst/ase-task-board-tui-control.js +41 -35
  27. package/dst/ase-task-board-tui-model.js +185 -167
  28. package/dst/ase-task-board-tui-popup.js +2 -3
  29. package/dst/ase-task-board-tui-view.js +74 -70
  30. package/dst/ase-task-board-web-client.js +2 -2
  31. package/dst/ase-task-board-web-server.js +2 -2
  32. package/dst/ase-task-board.js +1 -1
  33. package/dst/ase-task-format.js +1 -1
  34. package/dst/ase-task-store-client.js +17 -5
  35. package/dst/ase-task-store-core.js +5 -7
  36. package/dst/ase-task-store-issues.js +16 -1
  37. package/dst/ase-task-store-plugin-ase.js +7 -3
  38. package/dst/ase-task-store-plugin-gitea.js +9 -32
  39. package/dst/ase-task-store-plugin-github.js +3 -12
  40. package/dst/ase-task-store-plugin-gitlab.js +2 -18
  41. package/dst/ase-task.js +51 -23
  42. package/dst/ase-util-compat.js +1 -1
  43. package/dst/ase-util-diagram.js +13 -37
  44. package/dst/ase.js +5 -5
  45. package/dst/man/ase-agent-adhoc.1 +1 -1
  46. package/dst/man/ase-agent-condenser.1 +1 -1
  47. package/dst/man/ase-agent-editor.1 +1 -1
  48. package/dst/man/ase-agent-estimator.1 +1 -1
  49. package/dst/man/ase-agent-fleet.1 +1 -1
  50. package/dst/man/ase-agent-implementer.1 +1 -1
  51. package/dst/man/ase-agent-integrator.1 +1 -1
  52. package/dst/man/ase-agent-qualifier.1 +1 -1
  53. package/dst/man/ase-agent-rebooter.1 +1 -1
  54. package/dst/man/ase-agent-relay.1 +1 -1
  55. package/dst/man/ase-agent-researcher.1 +1 -1
  56. package/dst/man/ase-agent.1 +1 -1
  57. package/dst/man/ase-artifact-list.1 +1 -1
  58. package/dst/man/ase-artifact-name.1 +1 -1
  59. package/dst/man/ase-artifact.1 +1 -1
  60. package/dst/man/ase-config-delete.1 +1 -1
  61. package/dst/man/ase-config-edit.1 +1 -1
  62. package/dst/man/ase-config-get.1 +1 -1
  63. package/dst/man/ase-config-init.1 +1 -1
  64. package/dst/man/ase-config-list.1 +1 -1
  65. package/dst/man/ase-config-set.1 +1 -1
  66. package/dst/man/ase-config.1 +1 -1
  67. package/dst/man/ase-hook-permission-request.1 +1 -1
  68. package/dst/man/ase-hook-pre-tool-use.1 +1 -1
  69. package/dst/man/ase-hook-session-end.1 +1 -1
  70. package/dst/man/ase-hook-session-persona.1 +1 -1
  71. package/dst/man/ase-hook-session-start.1 +1 -1
  72. package/dst/man/ase-hook-stop.1 +1 -1
  73. package/dst/man/ase-hook-user-prompt-submit.1 +1 -1
  74. package/dst/man/ase-hook.1 +1 -1
  75. package/dst/man/ase-man.1 +1 -1
  76. package/dst/man/ase-mcp.1 +1 -1
  77. package/dst/man/ase-service-log.1 +1 -1
  78. package/dst/man/ase-service-send.1 +1 -1
  79. package/dst/man/ase-service-start.1 +1 -1
  80. package/dst/man/ase-service-status.1 +1 -1
  81. package/dst/man/ase-service-stop.1 +1 -1
  82. package/dst/man/ase-service.1 +1 -1
  83. package/dst/man/ase-setup-disable.1 +1 -1
  84. package/dst/man/ase-setup-enable.1 +1 -1
  85. package/dst/man/ase-setup-install.1 +1 -1
  86. package/dst/man/ase-setup-mcp.1 +1 -1
  87. package/dst/man/ase-setup-status.1 +1 -1
  88. package/dst/man/ase-setup-statusline.1 +1 -1
  89. package/dst/man/ase-setup-telemetry.1 +1 -1
  90. package/dst/man/ase-setup-uninstall.1 +1 -1
  91. package/dst/man/ase-setup-update.1 +1 -1
  92. package/dst/man/ase-setup.1 +1 -1
  93. package/dst/man/ase-spec-export.1 +1 -1
  94. package/dst/man/ase-spec-lint.1 +1 -1
  95. package/dst/man/ase-spec-preview.1 +1 -1
  96. package/dst/man/ase-spec.1 +1 -1
  97. package/dst/man/ase-statusline.1 +1 -1
  98. package/dst/man/ase-task-assign.1 +1 -1
  99. package/dst/man/ase-task-board.1 +1 -1
  100. package/dst/man/ase-task-delete.1 +1 -1
  101. package/dst/man/ase-task-edit.1 +1 -1
  102. package/dst/man/ase-task-idscheme.1 +1 -1
  103. package/dst/man/ase-task-lifecycle.1 +1 -1
  104. package/dst/man/ase-task-list.1 +1 -1
  105. package/dst/man/ase-task-load.1 +1 -1
  106. package/dst/man/ase-task-newid.1 +1 -1
  107. package/dst/man/ase-task-purge.1 +1 -1
  108. package/dst/man/ase-task-rename.1 +1 -1
  109. package/dst/man/ase-task-request.1 +1 -1
  110. package/dst/man/ase-task-save.1 +1 -1
  111. package/dst/man/ase-task-status.1 +1 -1
  112. package/dst/man/ase-task-store.1 +1 -1
  113. package/dst/man/ase-task-view.1 +1 -1
  114. package/dst/man/ase-task.1 +1 -1
  115. package/dst/man/ase-util-compat.1 +1 -1
  116. package/dst/man/ase-util-diagram.1 +1 -1
  117. package/dst/man/ase-util-meta.1 +1 -1
  118. package/dst/man/ase-util-metric.1 +1 -1
  119. package/dst/man/ase-util-mint.1 +1 -1
  120. package/dst/man/ase-util-worktree.1 +1 -1
  121. package/dst/man/ase-util.1 +1 -1
  122. package/dst/man/ase.1 +1 -1
  123. package/package.json +1 -1
  124. package/plugin/.claude-plugin/plugin.json +1 -1
  125. package/plugin/.codex-plugin/plugin.json +1 -1
  126. package/plugin/.github/plugin/plugin.json +1 -1
  127. package/plugin/meta/ase-skill.md +7 -0
  128. package/plugin/mods/ase-mods.ts +10 -7
  129. package/plugin/skills/ase-code-analyze/SKILL.md +18 -10
  130. package/plugin/skills/ase-code-analyze/help.md +11 -6
  131. package/plugin/skills/ase-code-lint/SKILL.md +37 -23
  132. package/plugin/skills/ase-code-lint/help.md +10 -5
  133. package/plugin/skills/ase-help-intent/data.md +24 -12
  134. package/plugin/skills/ase-repo-review/SKILL.md +5 -3
  135. package/plugin/skills/ase-repo-review/help.md +3 -1
@@ -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,6 +191,8 @@ 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
+ if (isMap(val))
195
+ throw new Error(`key "${args.key}" is not a leaf key`);
194
196
  const text = val === undefined ? "" :
195
197
  JSON.stringify(configSecretKeys.includes(cfg.resolveKey(args.key)) ? "***" : val);
196
198
  return { content: [{ type: "text", text }] };
package/dst/ase-hook.js CHANGED
@@ -17,6 +17,7 @@ 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
19
  import { parseTool } from "./ase-setup-common.js";
20
+ /* the addon MCP servers whose tools get auto-approved */
20
21
  const addonMcpServers = [
21
22
  "chat-alibaba-qwen",
22
23
  "chat-deepseek",
@@ -38,6 +39,7 @@ const addonMcpToolNamePattern = (prefix, suffix) => {
38
39
  .join("|");
39
40
  return new RegExp(`^${prefix}(?:${alternatives})${suffix}`);
40
41
  };
42
+ /* the per-tool dispatch table of the hook integrations */
41
43
  const toolSpecs = {
42
44
  "claude": {
43
45
  toolNameField: "tool_name",
@@ -89,7 +91,7 @@ export default class HookCommand {
89
91
  }
90
92
  /* validate a session id against the accepted character set */
91
93
  isValidSessionId(id) {
92
- return /^[A-Za-z0-9._-]+$/.test(id);
94
+ return /^[A-Za-z0-9._-]+$/.test(id) && id !== "." && id !== "..";
93
95
  }
94
96
  /* resolve the base directory holding all per-session state */
95
97
  sessionBaseDir() {
@@ -106,7 +108,7 @@ export default class HookCommand {
106
108
  try {
107
109
  entries = fs.readdirSync(base, { withFileTypes: true });
108
110
  }
109
- catch (_e) {
111
+ catch {
110
112
  /* best-effort: no base directory yet, or unreadable */
111
113
  return;
112
114
  }
@@ -121,7 +123,7 @@ export default class HookCommand {
121
123
  fs.rmSync(dir, { recursive: true, force: true });
122
124
  this.log.write("debug", `hook: pruned stale session directory: ${dir}`);
123
125
  }
124
- catch (_e) {
126
+ catch {
125
127
  /* best-effort: ignore vanished or undeletable directories */
126
128
  }
127
129
  }
@@ -143,7 +145,7 @@ export default class HookCommand {
143
145
  try {
144
146
  raw = JSON.parse(text);
145
147
  }
146
- catch (_e) {
148
+ catch {
147
149
  /* best-effort: return empty object on malformed JSON */
148
150
  return empty;
149
151
  }
@@ -168,7 +170,7 @@ export default class HookCommand {
168
170
  try {
169
171
  content = fs.readFileSync(abs, "utf8");
170
172
  }
171
- catch (_e) {
173
+ catch {
172
174
  return match;
173
175
  }
174
176
  const next = new Set(visited);
@@ -200,7 +202,7 @@ export default class HookCommand {
200
202
  try {
201
203
  return this.pluginRoot(tool);
202
204
  }
203
- catch (_e) {
205
+ catch {
204
206
  return "";
205
207
  }
206
208
  }
@@ -429,7 +431,7 @@ export default class HookCommand {
429
431
  ASE_HEADLESS: headless,
430
432
  ASE_AGENT_TOOL: tool
431
433
  });
432
- /* prepend ASE information to constitution markdown */
434
+ /* prepend ASE information to the constitution markdown (with its @<file> references expanded) */
433
435
  let context = `<ase-version>${version}</ase-version>\n` +
434
436
  `<ase-version-hint>${versionHint}</ase-version-hint>\n` +
435
437
  `<ase-plugin-root>${pluginRoot}</ase-plugin-root>\n` +
@@ -448,9 +450,7 @@ export default class HookCommand {
448
450
  `<ase-session-id>${sessionId}</ase-session-id>\n` +
449
451
  `<ase-headless>${headless}</ase-headless>\n` +
450
452
  `<ase-agent-tool>${tool}</ase-agent-tool>\n` +
451
- "\n" + md;
452
- /* expand all @<file> references manually */
453
- context = this.expandReferences(context, mdDir);
453
+ "\n" + this.expandReferences(md, mdDir);
454
454
  /* append the output style to the constitution markdown (GitHub
455
455
  Copilot CLI and OpenAI Codex CLI only -- Anthropic Claude Code CLI
456
456
  applies the plugin output style natively), stripping the
@@ -531,7 +531,7 @@ export default class HookCommand {
531
531
  try {
532
532
  fs.rmSync(dir, { recursive: true, force: true });
533
533
  }
534
- catch (_e) {
534
+ catch {
535
535
  /* best-effort: ignore failures */
536
536
  }
537
537
  /* detach the session from its AI usage window (if any), which closes
@@ -578,7 +578,7 @@ export default class HookCommand {
578
578
  });
579
579
  return val;
580
580
  }
581
- catch (_e) {
581
+ catch {
582
582
  return "";
583
583
  }
584
584
  }