@rse/ase 1.0.6 → 2.0.0
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.
- package/dst/ase-agent-definition-api.d.ts +184 -0
- package/dst/ase-agent-definition-api.js +6 -0
- package/dst/ase-agent-definition-builtin/condenser.js +42 -0
- package/dst/ase-agent-definition-builtin/editor.js +45 -0
- package/dst/ase-agent-definition-builtin/estimator.js +98 -0
- package/dst/ase-agent-definition-builtin/implementer.js +121 -0
- package/dst/ase-agent-definition-builtin/integrator.js +56 -0
- package/dst/ase-agent-definition-builtin/qualifier.js +61 -0
- package/dst/ase-agent-definition-builtin/rebooter.js +41 -0
- package/dst/ase-agent-definition-builtin/researcher.js +77 -0
- package/dst/ase-agent-definition-core.js +827 -0
- package/dst/ase-agent-fleet.js +772 -0
- package/dst/ase-agent-process.js +232 -0
- package/dst/ase-agent-relay-client.js +509 -0
- package/dst/ase-agent-relay-core.js +213 -0
- package/dst/ase-agent-relay-server-bind.js +242 -0
- package/dst/ase-agent-relay-server-cli.js +50 -0
- package/dst/ase-agent-runtime-claim.js +189 -0
- package/dst/ase-agent-runtime-engine.js +1100 -0
- package/dst/ase-agent-runtime-exec.js +275 -0
- package/dst/ase-agent-runtime-phase.js +581 -0
- package/dst/ase-agent-runtime-request.js +272 -0
- package/dst/ase-agent-runtime-tool.js +207 -0
- package/dst/ase-agent-runtime.js +1316 -0
- package/dst/ase-agent.js +65 -0
- package/dst/ase-artifact.js +6 -5
- package/dst/ase-config-core.js +465 -0
- package/dst/ase-config-schema.js +351 -0
- package/dst/ase-config-scope.js +172 -0
- package/dst/ase-config.js +25 -585
- package/dst/ase-hook.js +230 -116
- package/dst/ase-lib-daemon-cli.js +130 -0
- package/dst/ase-lib-hash.js +20 -0
- package/dst/{ase-log.js → ase-lib-log.js} +40 -7
- package/dst/ase-lib-server-bind.js +115 -0
- package/dst/ase-lib-server-cli.js +474 -0
- package/dst/{ase-table.js → ase-lib-table.js} +1 -1
- package/dst/{ase-version.js → ase-lib-version.js} +3 -0
- package/dst/ase-man.js +96 -0
- package/dst/ase-mcp.js +166 -13
- package/dst/{ase-getopt.js → ase-service-getopt.js} +82 -80
- package/dst/{ase-markdown.js → ase-service-markdown.js} +18 -11
- package/dst/ase-service-otel.js +1246 -0
- package/dst/{ase-skills.js → ase-service-skills.js} +29 -64
- package/dst/ase-service-state.js +101 -0
- package/dst/ase-service.js +251 -76
- package/dst/ase-setup-common.js +141 -0
- package/dst/ase-setup-mcp.js +359 -0
- package/dst/ase-setup-settings.js +837 -0
- package/dst/ase-setup.js +113 -869
- package/dst/ase-spec.js +31 -19
- package/dst/ase-statusline.js +108 -43
- package/dst/ase-task-board-core.js +739 -0
- package/dst/ase-task-board-filter.js +127 -0
- package/dst/ase-task-board-flow.js +237 -0
- package/dst/ase-task-board-graph.js +497 -0
- package/dst/ase-task-board-stats.js +263 -0
- package/dst/ase-task-board-tui-control.js +775 -0
- package/dst/ase-task-board-tui-model.js +1008 -0
- package/dst/ase-task-board-tui-popup.js +626 -0
- package/dst/ase-task-board-tui-style.js +106 -0
- package/dst/ase-task-board-tui-view.js +847 -0
- package/dst/ase-task-board-tui.js +189 -0
- package/dst/ase-task-board-web-client.css +1 -0
- package/dst/ase-task-board-web-client.html +20 -0
- package/dst/ase-task-board-web-client.js +81 -0
- package/dst/ase-task-board-web-server.js +823 -0
- package/dst/ase-task-board.js +134 -0
- package/dst/ase-task-format.js +757 -0
- package/dst/ase-task-request.js +208 -0
- package/dst/ase-task-store-client.js +495 -0
- package/dst/ase-task-store-core.js +804 -0
- package/dst/ase-task-store-issues.js +237 -0
- package/dst/ase-task-store-plugin-api.js +6 -0
- package/dst/ase-task-store-plugin-ase.js +484 -0
- package/dst/ase-task-store-plugin-delegate.js +137 -0
- package/dst/ase-task-store-plugin-gitea.js +666 -0
- package/dst/ase-task-store-plugin-github.js +696 -0
- package/dst/ase-task-store-plugin-gitlab.js +668 -0
- package/dst/ase-task-store-server-bind.js +538 -0
- package/dst/ase-task-store-server-cli.js +154 -0
- package/dst/ase-task.js +1335 -357
- package/dst/{ase-compat.js → ase-util-compat.js} +2 -2
- package/dst/{ase-diagram.js → ase-util-diagram.js} +107 -32
- package/dst/{ase-meta.js → ase-util-meta.js} +2 -2
- package/dst/{ase-metric.js → ase-util-metric.js} +2 -2
- package/dst/{ase-mint.js → ase-util-mint.js} +19 -12
- package/dst/{ase-worktree.js → ase-util-worktree.js} +30 -19
- package/dst/ase-util.js +20 -0
- package/dst/ase.js +108 -32
- package/dst/man/ase-agent-adhoc.1 +515 -0
- package/dst/man/ase-agent-condenser.1 +193 -0
- package/dst/man/ase-agent-editor.1 +212 -0
- package/dst/man/ase-agent-estimator.1 +214 -0
- package/dst/man/ase-agent-fleet.1 +185 -0
- package/dst/man/ase-agent-implementer.1 +233 -0
- package/dst/man/ase-agent-integrator.1 +213 -0
- package/dst/man/ase-agent-qualifier.1 +214 -0
- package/dst/man/ase-agent-rebooter.1 +193 -0
- package/dst/man/ase-agent-relay.1 +128 -0
- package/dst/man/ase-agent-researcher.1 +217 -0
- package/dst/man/ase-agent.1 +264 -0
- package/dst/man/ase-artifact-list.1 +38 -0
- package/dst/man/ase-artifact-name.1 +39 -0
- package/dst/man/ase-artifact.1 +56 -0
- package/dst/man/ase-config-delete.1 +45 -0
- package/dst/man/ase-config-edit.1 +48 -0
- package/dst/man/ase-config-get.1 +46 -0
- package/dst/man/ase-config-init.1 +60 -0
- package/dst/man/ase-config-list.1 +42 -0
- package/dst/man/ase-config-set.1 +52 -0
- package/dst/man/ase-config.1 +225 -0
- package/dst/man/ase-hook-permission-request.1 +36 -0
- package/dst/man/ase-hook-pre-tool-use.1 +59 -0
- package/dst/man/ase-hook-session-end.1 +47 -0
- package/dst/man/ase-hook-session-persona.1 +37 -0
- package/dst/man/ase-hook-session-start.1 +81 -0
- package/dst/man/ase-hook-stop.1 +34 -0
- package/dst/man/ase-hook-user-prompt-submit.1 +34 -0
- package/dst/man/ase-hook.1 +51 -0
- package/dst/man/ase-man.1 +67 -0
- package/dst/man/ase-mcp.1 +61 -0
- package/dst/man/ase-service-log.1 +43 -0
- package/dst/man/ase-service-send.1 +47 -0
- package/dst/man/ase-service-start.1 +54 -0
- package/dst/man/ase-service-status.1 +46 -0
- package/dst/man/ase-service-stop.1 +37 -0
- package/dst/man/ase-service.1 +85 -0
- package/dst/man/ase-setup-disable.1 +44 -0
- package/dst/man/ase-setup-enable.1 +45 -0
- package/dst/man/ase-setup-install.1 +73 -0
- package/dst/man/ase-setup-mcp.1 +124 -0
- package/dst/man/ase-setup-status.1 +60 -0
- package/dst/man/ase-setup-statusline.1 +92 -0
- package/dst/man/ase-setup-telemetry.1 +92 -0
- package/dst/man/ase-setup-uninstall.1 +59 -0
- package/dst/man/ase-setup-update.1 +64 -0
- package/dst/man/ase-setup.1 +93 -0
- package/dst/man/ase-spec-export.1 +62 -0
- package/dst/man/ase-spec-lint.1 +40 -0
- package/dst/man/ase-spec-preview.1 +40 -0
- package/dst/man/ase-spec.1 +46 -0
- package/dst/man/ase-statusline.1 +189 -0
- package/dst/man/ase-task-assign.1 +71 -0
- package/dst/man/ase-task-board.1 +384 -0
- package/dst/man/ase-task-delete.1 +35 -0
- package/dst/man/ase-task-edit.1 +46 -0
- package/dst/man/ase-task-idscheme.1 +65 -0
- package/dst/man/ase-task-lifecycle.1 +57 -0
- package/dst/man/ase-task-list.1 +47 -0
- package/dst/man/ase-task-load.1 +36 -0
- package/dst/man/ase-task-newid.1 +61 -0
- package/dst/man/ase-task-purge.1 +37 -0
- package/dst/man/ase-task-rename.1 +44 -0
- package/dst/man/ase-task-request.1 +61 -0
- package/dst/man/ase-task-save.1 +48 -0
- package/dst/man/ase-task-status.1 +55 -0
- package/dst/man/ase-task-store.1 +194 -0
- package/dst/man/ase-task-view.1 +40 -0
- package/dst/man/ase-task.1 +160 -0
- package/dst/man/ase-util-compat.1 +25 -0
- package/dst/man/ase-util-diagram.1 +76 -0
- package/dst/man/ase-util-meta.1 +45 -0
- package/dst/man/ase-util-metric.1 +54 -0
- package/dst/man/ase-util-mint.1 +59 -0
- package/dst/man/ase-util-worktree.1 +72 -0
- package/dst/man/ase-util.1 +50 -0
- package/dst/man/ase.1 +124 -0
- package/package.json +79 -21
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.github/plugin/plugin.json +1 -1
- package/plugin/agents/ase-meta-diagram.md +12 -8
- package/plugin/agents/ase-repo-review.md +322 -0
- package/plugin/hooks/hooks-codex.json +5 -0
- package/plugin/hooks/hooks-copilot.json +5 -0
- package/plugin/hooks/hooks.json +8 -0
- package/plugin/meta/ase-common-approach.md +117 -0
- package/plugin/meta/ase-common-changeset.md +256 -0
- package/plugin/meta/ase-common-code.md +10 -173
- package/plugin/meta/ase-common-grill.md +317 -0
- package/plugin/meta/ase-common-resolve.md +305 -0
- package/plugin/meta/ase-common-task.md +83 -4
- package/plugin/meta/ase-constitution.md +0 -2
- package/plugin/meta/ase-format-specbook.md +208 -25
- package/plugin/meta/ase-format-specbook.yaml +242 -851
- package/plugin/meta/ase-format-task.md +669 -125
- package/plugin/meta/ase-persona.md +3 -4
- package/plugin/meta/ase-skill.md +9 -0
- package/plugin/mods/ase-mods.ts +409 -0
- package/plugin/mods/claude-code.d.ts +13417 -0
- package/plugin/skills/ase-arch-analyze/SKILL.md +1 -1
- package/plugin/skills/ase-arch-analyze/help.md +2 -2
- package/plugin/skills/ase-code-analyze/SKILL.md +59 -8
- package/plugin/skills/ase-code-analyze/help.md +29 -4
- package/plugin/skills/ase-code-edit/SKILL.md +107 -181
- package/plugin/skills/ase-code-edit/help.md +118 -31
- package/plugin/skills/ase-code-lint/SKILL.md +64 -13
- package/plugin/skills/ase-code-lint/help.md +32 -2
- package/plugin/skills/ase-docs-proofread/SKILL.md +4 -4
- package/plugin/skills/ase-docs-proofread/help.md +1 -1
- package/plugin/skills/ase-docs-refine/SKILL.md +4 -4
- package/plugin/skills/ase-docs-shorten/SKILL.md +4 -4
- package/plugin/skills/ase-help-intent/data.md +2164 -1087
- package/plugin/skills/ase-help-intent/help.md +1 -1
- package/plugin/skills/ase-help-skill/catalog.md +12 -9
- package/plugin/skills/ase-meta-brainstorm/SKILL.md +1 -1
- package/plugin/skills/ase-meta-config/SKILL.md +3 -4
- package/plugin/skills/ase-meta-config/help.md +31 -10
- package/plugin/skills/ase-meta-mint/SKILL.md +5 -5
- package/plugin/skills/ase-meta-mint/help.md +10 -5
- package/plugin/skills/ase-meta-why/help.md +1 -1
- package/plugin/skills/ase-meta-workflow/SKILL.md +7 -7
- package/plugin/skills/ase-meta-workflow/help.md +4 -4
- package/plugin/skills/ase-meta-workflow/sample.md +3 -3
- package/plugin/skills/ase-meta-workflow/workflow.txt +22 -18
- package/plugin/skills/{ase-meta-changelog → ase-repo-changelog}/SKILL.md +4 -3
- package/plugin/skills/{ase-meta-changelog → ase-repo-changelog}/help.md +5 -5
- package/plugin/skills/{ase-meta-commit → ase-repo-commit}/SKILL.md +6 -6
- package/plugin/skills/{ase-meta-commit → ase-repo-commit}/help.md +5 -5
- package/plugin/skills/{ase-meta-diff → ase-repo-diff}/SKILL.md +6 -6
- package/plugin/skills/{ase-meta-diff → ase-repo-diff}/help.md +8 -8
- package/plugin/skills/{ase-code-dissect → ase-repo-dissect}/SKILL.md +19 -19
- package/plugin/skills/{ase-code-dissect → ase-repo-dissect}/help.md +10 -12
- package/plugin/skills/ase-repo-merge/SKILL.md +346 -0
- package/plugin/skills/ase-repo-merge/help.md +107 -0
- package/plugin/skills/ase-repo-resolve/SKILL.md +158 -0
- package/plugin/skills/ase-repo-resolve/help.md +102 -0
- package/plugin/skills/ase-repo-review/SKILL.md +406 -0
- package/plugin/skills/ase-repo-review/help.md +181 -0
- package/plugin/skills/ase-spec-edit/SKILL.md +117 -183
- package/plugin/skills/ase-spec-edit/help.md +107 -26
- package/plugin/skills/ase-sync-reconcile/help.md +1 -1
- package/plugin/skills/ase-task-assign/SKILL.md +111 -0
- package/plugin/skills/ase-task-assign/help.md +119 -0
- package/plugin/skills/ase-task-condense/SKILL.md +21 -21
- package/plugin/skills/ase-task-condense/help.md +12 -10
- package/plugin/skills/ase-task-delete/SKILL.md +1 -1
- package/plugin/skills/ase-task-dissect/SKILL.md +71 -48
- package/plugin/skills/ase-task-dissect/help.md +10 -9
- package/plugin/skills/ase-task-edit/SKILL.md +379 -94
- package/plugin/skills/ase-task-edit/help.md +95 -22
- package/plugin/skills/ase-task-estimate/SKILL.md +492 -0
- package/plugin/skills/ase-task-estimate/help.md +158 -0
- package/plugin/skills/ase-task-grill/SKILL.md +246 -138
- package/plugin/skills/ase-task-grill/help.md +129 -26
- package/plugin/skills/ase-task-id/SKILL.md +8 -1
- package/plugin/skills/ase-task-id/help.md +6 -3
- package/plugin/skills/ase-task-implement/SKILL.md +535 -145
- package/plugin/skills/ase-task-implement/help.md +138 -47
- package/plugin/skills/ase-task-integrate/SKILL.md +549 -0
- package/plugin/skills/ase-task-integrate/help.md +187 -0
- package/plugin/skills/ase-task-list/SKILL.md +55 -23
- package/plugin/skills/ase-task-list/help.md +45 -21
- package/plugin/skills/ase-task-reboot/SKILL.md +84 -43
- package/plugin/skills/ase-task-reboot/help.md +28 -8
- package/plugin/skills/ase-task-rename/SKILL.md +10 -2
- package/plugin/skills/ase-task-research/SKILL.md +386 -0
- package/plugin/skills/ase-task-research/help.md +102 -0
- package/plugin/skills/ase-task-status/SKILL.md +137 -0
- package/plugin/skills/ase-task-status/help.md +80 -0
- package/plugin/skills/ase-task-view/SKILL.md +49 -24
- package/plugin/skills/ase-task-view/help.md +19 -9
- package/plugin/agents/ase-meta-review.md +0 -187
- package/plugin/skills/ase-code-craft/SKILL.md +0 -264
- package/plugin/skills/ase-code-craft/help.md +0 -122
- package/plugin/skills/ase-code-refactor/SKILL.md +0 -232
- package/plugin/skills/ase-code-refactor/help.md +0 -109
- package/plugin/skills/ase-code-resolve/SKILL.md +0 -289
- package/plugin/skills/ase-code-resolve/help.md +0 -116
- package/plugin/skills/ase-meta-review/SKILL.md +0 -210
- package/plugin/skills/ase-meta-review/help.md +0 -96
- package/plugin/skills/ase-task-preflight/SKILL.md +0 -166
- package/plugin/skills/ase-task-preflight/help.md +0 -82
- /package/dst/{ase-ignore.js → ase-lib-ignore.js} +0 -0
- /package/dst/{ase-stdio.js → ase-lib-stdio.js} +0 -0
- /package/dst/{ase-kv.js → ase-service-kv.js} +0 -0
- /package/dst/{ase-sleep.js → ase-service-sleep.js} +0 -0
- /package/dst/{ase-timestamp.js → ase-service-timestamp.js} +0 -0
package/dst/ase-hook.js
CHANGED
|
@@ -9,9 +9,13 @@ import os from "node:os";
|
|
|
9
9
|
import { execaSync } from "execa";
|
|
10
10
|
import { quote } from "shell-quote";
|
|
11
11
|
import * as v from "valibot";
|
|
12
|
-
import Version from "./ase-version.js";
|
|
13
|
-
import { Config,
|
|
14
|
-
import {
|
|
12
|
+
import Version from "./ase-lib-version.js";
|
|
13
|
+
import { Config, sessionState } from "./ase-config-core.js";
|
|
14
|
+
import { configSchema } from "./ase-config-schema.js";
|
|
15
|
+
import { parseScope, userStateDir } from "./ase-config-scope.js";
|
|
16
|
+
import { readStdin, writeStdout } from "./ase-lib-stdio.js";
|
|
17
|
+
import { Task } from "./ase-task.js";
|
|
18
|
+
import * as TaskFormat from "./ase-task-format.js";
|
|
15
19
|
const addonMcpServers = [
|
|
16
20
|
"chat-alibaba-qwen",
|
|
17
21
|
"chat-deepseek",
|
|
@@ -37,7 +41,6 @@ const toolSpecs = {
|
|
|
37
41
|
"claude": {
|
|
38
42
|
toolNameField: "tool_name",
|
|
39
43
|
toolInputField: "tool_input",
|
|
40
|
-
toolInputIsString: false,
|
|
41
44
|
bashToolName: "Bash",
|
|
42
45
|
mcpToolNamePattern: /^mcp__plugin_ase_ase__.+/,
|
|
43
46
|
addonMcpToolNamePattern: addonMcpToolNamePattern("mcp__", "__.+"),
|
|
@@ -48,7 +51,6 @@ const toolSpecs = {
|
|
|
48
51
|
"copilot": {
|
|
49
52
|
toolNameField: "toolName",
|
|
50
53
|
toolInputField: "toolArgs",
|
|
51
|
-
toolInputIsString: true,
|
|
52
54
|
bashToolName: "bash",
|
|
53
55
|
mcpToolNamePattern: /^ase-.+/,
|
|
54
56
|
addonMcpToolNamePattern: addonMcpToolNamePattern("", "-.+"),
|
|
@@ -59,7 +61,6 @@ const toolSpecs = {
|
|
|
59
61
|
"codex": {
|
|
60
62
|
toolNameField: "tool_name",
|
|
61
63
|
toolInputField: "tool_input",
|
|
62
|
-
toolInputIsString: false,
|
|
63
64
|
bashToolName: "Bash",
|
|
64
65
|
mcpToolNamePattern: /^mcp__ase__.+/,
|
|
65
66
|
addonMcpToolNamePattern: addonMcpToolNamePattern("mcp__", "__.+"),
|
|
@@ -91,12 +92,13 @@ export default class HookCommand {
|
|
|
91
92
|
}
|
|
92
93
|
/* resolve the base directory holding all per-session state */
|
|
93
94
|
sessionBaseDir() {
|
|
94
|
-
return path.join(
|
|
95
|
+
return path.join(userStateDir(), "session");
|
|
95
96
|
}
|
|
96
97
|
/* garbage-collect orphaned session directories left behind by agents
|
|
97
98
|
which died before their session-end hook could run; a live session
|
|
98
|
-
|
|
99
|
-
lock
|
|
99
|
+
usually refreshes its directory's mtime, as its locked configuration
|
|
100
|
+
accesses create lock files inside it, so plain age is a sufficient
|
|
101
|
+
liveness heuristic */
|
|
100
102
|
pruneStaleSessions(currentSessionId) {
|
|
101
103
|
const base = this.sessionBaseDir();
|
|
102
104
|
let entries;
|
|
@@ -201,42 +203,20 @@ export default class HookCommand {
|
|
|
201
203
|
return "";
|
|
202
204
|
}
|
|
203
205
|
}
|
|
204
|
-
/*
|
|
205
|
-
|
|
206
|
-
/* determine plugin root (env var name differs per tool) */
|
|
207
|
-
const pluginRoot = this.pluginRoot(tool);
|
|
208
|
-
/* determine path to external files */
|
|
209
|
-
const filePkg = path.join(pluginRoot, ".claude-plugin", "plugin.json");
|
|
210
|
-
const fileMd = path.join(pluginRoot, "meta", "ase-constitution.md");
|
|
211
|
-
const fileStyle = path.join(pluginRoot, "output-styles", "ase-terse.md");
|
|
212
|
-
/* read external files */
|
|
213
|
-
let pkg;
|
|
214
|
-
let md;
|
|
215
|
-
let style;
|
|
206
|
+
/* read a plugin file, throwing a descriptive error on failure */
|
|
207
|
+
readPluginFile(file, what) {
|
|
216
208
|
try {
|
|
217
|
-
|
|
209
|
+
return fs.readFileSync(file, "utf8");
|
|
218
210
|
}
|
|
219
211
|
catch (err) {
|
|
220
|
-
throw new Error(`failed to read
|
|
221
|
-
}
|
|
222
|
-
try {
|
|
223
|
-
md = fs.readFileSync(fileMd, "utf8");
|
|
212
|
+
throw new Error(`failed to read ${what}: ${file}`, { cause: err });
|
|
224
213
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
style = fs.readFileSync(fileStyle, "utf8");
|
|
230
|
-
}
|
|
231
|
-
catch (err) {
|
|
232
|
-
throw new Error(`failed to read output style file: ${fileStyle}`, { cause: err });
|
|
233
|
-
}
|
|
234
|
-
/* determine own version */
|
|
235
|
-
const pkgObj = this.parseJSON(pkg, v.object({ version: v.optional(v.string()) }));
|
|
236
|
-
const versionCurrentPlugin = pkgObj.version ?? "";
|
|
214
|
+
}
|
|
215
|
+
/* determine the version hint on a tool/plugin version mismatch,
|
|
216
|
+
an available tool update, and a development setup */
|
|
217
|
+
async versionHint(versionCurrentPlugin) {
|
|
237
218
|
const versionCurrentTool = Version.current();
|
|
238
219
|
const versionLatestTool = await Version.latest();
|
|
239
|
-
/* sanity check situation */
|
|
240
220
|
const versionHints = [];
|
|
241
221
|
if (versionCurrentPlugin !== versionCurrentTool)
|
|
242
222
|
versionHints.push("**WARNING:** version *mismatch*: " +
|
|
@@ -245,7 +225,81 @@ export default class HookCommand {
|
|
|
245
225
|
versionHints.push(`**NOTICE:** *latest* version: **${versionLatestTool}**, please update!`);
|
|
246
226
|
if (process.env.ASE_SETUP_DEV !== undefined)
|
|
247
227
|
versionHints.push("**NOTICE:** *development* setup");
|
|
248
|
-
|
|
228
|
+
return versionHints.length > 0 ? "(" + versionHints.join(", ") + ")" : "";
|
|
229
|
+
}
|
|
230
|
+
/* switch to the project directory announced in the event payload: GitHub
|
|
231
|
+
Copilot CLI runs its hooks in the plugin installation directory, so
|
|
232
|
+
every working-directory-relative lookup -- most notably the layered
|
|
233
|
+
configuration files -- would otherwise miss the project entirely */
|
|
234
|
+
switchDir(dir) {
|
|
235
|
+
if (dir === undefined || dir === "")
|
|
236
|
+
return;
|
|
237
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
|
|
238
|
+
this.log.write("warning", `hook: ignoring invalid project directory: ${dir}`);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
process.chdir(dir);
|
|
242
|
+
}
|
|
243
|
+
/* determine the project directory: the Git top-level directory of "cwd",
|
|
244
|
+
or "cwd" itself when outside a Git working tree */
|
|
245
|
+
determineProjectDir(cwd) {
|
|
246
|
+
let projectDir = cwd;
|
|
247
|
+
try {
|
|
248
|
+
const result = execaSync("git", ["rev-parse", "--show-toplevel"], {
|
|
249
|
+
stderr: "ignore", cwd
|
|
250
|
+
});
|
|
251
|
+
if (result.stdout.trim() !== "")
|
|
252
|
+
projectDir = result.stdout.trim();
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
/* not inside a Git working tree */
|
|
256
|
+
}
|
|
257
|
+
return projectDir;
|
|
258
|
+
}
|
|
259
|
+
/* determine the project id: the valid configured "project.id", or else
|
|
260
|
+
the id derived from the already resolved project directory */
|
|
261
|
+
determineProjectId(projectDir, cfg) {
|
|
262
|
+
let configuredId = String(cfg.get("project.id") ?? "");
|
|
263
|
+
if (configuredId !== "" && !TaskFormat.ID_RE.test(configuredId)) {
|
|
264
|
+
this.log.write("warning", `hook: ignoring invalid configured "project.id" "${configuredId}" ` +
|
|
265
|
+
"(must match [A-Za-z0-9_-]+)");
|
|
266
|
+
configuredId = "";
|
|
267
|
+
}
|
|
268
|
+
return configuredId || Task.projectIdOf(projectDir);
|
|
269
|
+
}
|
|
270
|
+
/* append environment variable exports to the environment file of
|
|
271
|
+
Anthropic Claude Code CLI (the other tools have no equivalent mechanism) */
|
|
272
|
+
writeEnvFile(tool, vars) {
|
|
273
|
+
const envFile = tool === "claude" ? (process.env.CLAUDE_ENV_FILE ?? "") : "";
|
|
274
|
+
if (envFile === "")
|
|
275
|
+
return;
|
|
276
|
+
const script = Object.entries(vars)
|
|
277
|
+
.map(([name, value]) => `export ${name}=${quote([value])}\n`)
|
|
278
|
+
.join("");
|
|
279
|
+
try {
|
|
280
|
+
fs.appendFileSync(envFile, script, "utf8");
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
284
|
+
this.log.write("warning", `hook: failed to write environment file: ${message}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/* handler for "ase hook session-start" (all tools) */
|
|
288
|
+
async doSessionStart(tool) {
|
|
289
|
+
/* determine plugin root (env var name differs per tool) */
|
|
290
|
+
const pluginRoot = this.pluginRoot(tool);
|
|
291
|
+
/* determine path to external files */
|
|
292
|
+
const filePkg = path.join(pluginRoot, ".claude-plugin", "plugin.json");
|
|
293
|
+
const fileMd = path.join(pluginRoot, "meta", "ase-constitution.md");
|
|
294
|
+
const fileStyle = path.join(pluginRoot, "output-styles", "ase-terse.md");
|
|
295
|
+
/* read external files */
|
|
296
|
+
const pkg = this.readPluginFile(filePkg, "plugin manifest");
|
|
297
|
+
const md = this.readPluginFile(fileMd, "constitution file");
|
|
298
|
+
const style = this.readPluginFile(fileStyle, "output style file");
|
|
299
|
+
/* determine own version */
|
|
300
|
+
const pkgObj = this.parseJSON(pkg, v.object({ version: v.optional(v.string()) }));
|
|
301
|
+
const versionCurrentPlugin = pkgObj.version ?? "";
|
|
302
|
+
const versionHint = await this.versionHint(versionCurrentPlugin);
|
|
249
303
|
/* read session information (Anthropic Claude Code CLI uses snake_case fields,
|
|
250
304
|
GitHub Copilot CLI uses camelCase fields) */
|
|
251
305
|
const stdin = await readStdin().catch(() => "");
|
|
@@ -256,36 +310,48 @@ export default class HookCommand {
|
|
|
256
310
|
}));
|
|
257
311
|
/* determine session id */
|
|
258
312
|
const sessionId = this.pickSessionId(input);
|
|
313
|
+
/* adopt the project directory before any context is derived from the
|
|
314
|
+
working directory, especially the layered configuration below
|
|
315
|
+
(GitHub Copilot CLI only -- the other tools already run their
|
|
316
|
+
hooks inside the project directory) */
|
|
317
|
+
if (tool === "copilot")
|
|
318
|
+
this.switchDir(input.cwd);
|
|
259
319
|
/* garbage-collect orphaned session directories of previous agent runs */
|
|
260
320
|
this.pruneStaleSessions(sessionId);
|
|
261
321
|
/* establish config context (session-scoped only if a valid sessionId is present) */
|
|
262
322
|
const hasSession = this.isValidSessionId(sessionId);
|
|
263
323
|
const cfg = new Config("config", configSchema, this.log, hasSession ? parseScope(`session:${sessionId}`) : parseScope(undefined));
|
|
264
|
-
/* determine
|
|
324
|
+
/* determine the project directory, persisted below, because for GitHub Copilot
|
|
325
|
+
CLI it is the single piece of context this agent tool withholds from the MCP
|
|
326
|
+
server: it starts the MCP server in the plugin installation directory, long
|
|
327
|
+
before this hook runs, so the server can only learn it from here (and for all
|
|
328
|
+
agent tools, the project service determines the sessions of its project by it,
|
|
329
|
+
in order to attach them to the usage windows of their tasks assigned elsewhere) */
|
|
330
|
+
const projectDir = this.determineProjectDir(input.cwd ?? process.cwd());
|
|
331
|
+
cfg.read();
|
|
332
|
+
/* determine headless mode */
|
|
333
|
+
const headless = process.env.ASE_HEADLESS === "true" ? "true" : "false";
|
|
334
|
+
/* determine task id and persist it, together with the project directory
|
|
335
|
+
(see above) and the headless mode (as the project service never attaches
|
|
336
|
+
the headless sessions of the agents, which book their usage themselves, to
|
|
337
|
+
usage windows), into the internal session state (only when scoped to a real
|
|
338
|
+
session), normalizing the path separators of the project directory, as Git
|
|
339
|
+
reports the top-level directory with forward slashes even on Windows */
|
|
265
340
|
const taskId = process.env.ASE_TASK_ID ?? "default";
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
341
|
+
if (hasSession) {
|
|
342
|
+
const state = sessionState(this.log, sessionId);
|
|
343
|
+
state.lock(() => {
|
|
344
|
+
state.read();
|
|
345
|
+
state.set("task", taskId);
|
|
346
|
+
state.set("basedir", path.resolve(projectDir));
|
|
347
|
+
state.set("headless", headless);
|
|
348
|
+
state.write();
|
|
349
|
+
});
|
|
350
|
+
}
|
|
273
351
|
/* initialize agent activity status */
|
|
274
352
|
this.writeAgentStatus("ready");
|
|
275
353
|
/* determine project id */
|
|
276
|
-
const
|
|
277
|
-
let projectDir = cwd;
|
|
278
|
-
try {
|
|
279
|
-
const result = execaSync("git", ["rev-parse", "--show-toplevel"], {
|
|
280
|
-
stderr: "ignore", cwd
|
|
281
|
-
});
|
|
282
|
-
if (result.stdout.trim() !== "")
|
|
283
|
-
projectDir = result.stdout.trim();
|
|
284
|
-
}
|
|
285
|
-
catch {
|
|
286
|
-
/* not inside a Git working tree */
|
|
287
|
-
}
|
|
288
|
-
const projectId = path.basename(projectDir);
|
|
354
|
+
const projectId = this.determineProjectId(projectDir, cfg);
|
|
289
355
|
/* determine user id */
|
|
290
356
|
const userId = process.env.USER ?? process.env.LOGNAME ?? "unknown";
|
|
291
357
|
/* helper function: determine a setting from the explicitly configured
|
|
@@ -296,64 +362,100 @@ export default class HookCommand {
|
|
|
296
362
|
const val = cfg.getExplicit(key);
|
|
297
363
|
return typeof val === "string" ? val : (process.env[envVar] ?? dflt);
|
|
298
364
|
};
|
|
299
|
-
/* determine agent persona style, agent guidance level,
|
|
365
|
+
/* determine agent persona style, agent guidance level, project
|
|
366
|
+
boxing transparency, and project task lifecycle */
|
|
300
367
|
const persona = setting("agent.persona", "ASE_PERSONA_STYLE", "engineer");
|
|
301
368
|
const guidance = setting("agent.guidance", "ASE_GUIDANCE_LEVEL", "normal");
|
|
302
369
|
const boxing = setting("project.boxing", "ASE_PROJECT_BOXING", "white");
|
|
370
|
+
const lifecycle = setting("project.task.lifecycle", "ASE_PROJECT_TASK_LIFECYCLE", "solo");
|
|
371
|
+
/* determine the defaults of the change set flow keys of the task plans */
|
|
372
|
+
const defaultSource = setting("project.task.default.source", "ASE_PROJECT_TASK_DEFAULT_SOURCE", "worktree");
|
|
373
|
+
const defaultChangeset = setting("project.task.default.changeset", "ASE_PROJECT_TASK_DEFAULT_CHANGESET", "worktree");
|
|
374
|
+
const defaultTarget = setting("project.task.default.target", "ASE_PROJECT_TASK_DEFAULT_TARGET", "source");
|
|
303
375
|
/* determine the specification base directory and the whitespace-separated
|
|
304
376
|
list of SpecBook schema configurations (empty: the bundled standard schema) */
|
|
305
377
|
const specBasedir = String(cfg.get("project.artifact.spec.basedir") ?? "");
|
|
306
378
|
const specSchema = String(cfg.get("project.artifact.spec.schema") ?? "");
|
|
307
|
-
/*
|
|
308
|
-
const
|
|
379
|
+
/* publish the ASE session information and emit it as the hook output payload */
|
|
380
|
+
const payload = this.publishSessionInfo(tool, {
|
|
381
|
+
version: versionCurrentPlugin, versionHint, pluginRoot, persona, guidance, userId, projectId,
|
|
382
|
+
boxing, lifecycle, defaultSource, defaultChangeset, defaultTarget,
|
|
383
|
+
specBasedir, specSchema, taskId, sessionId, headless
|
|
384
|
+
}, md, path.dirname(fileMd), style);
|
|
385
|
+
await writeStdout(JSON.stringify(payload));
|
|
386
|
+
return 0;
|
|
387
|
+
}
|
|
388
|
+
/* handler for "ase hook session-persona" (all tools): inject the persona rules
|
|
389
|
+
as a separate hook output, as agent tools like Anthropic Claude Code CLI
|
|
390
|
+
persist any hook output beyond 10,000 characters to a file and only show a preview */
|
|
391
|
+
async doSessionPersona(tool) {
|
|
392
|
+
/* read persona file */
|
|
393
|
+
const pluginRoot = this.pluginRoot(tool);
|
|
394
|
+
const persona = this.readPluginFile(path.join(pluginRoot, "meta", "ase-persona.md"), "persona file");
|
|
395
|
+
/* inject markdown into session context (same nesting as for "session-start") */
|
|
396
|
+
const payload = tool !== "copilot" ? {
|
|
397
|
+
"hookSpecificOutput": {
|
|
398
|
+
"hookEventName": "SessionStart",
|
|
399
|
+
"additionalContext": persona
|
|
400
|
+
}
|
|
401
|
+
} : {
|
|
402
|
+
"additionalContext": persona
|
|
403
|
+
};
|
|
404
|
+
await writeStdout(JSON.stringify(payload));
|
|
405
|
+
return 0;
|
|
406
|
+
}
|
|
407
|
+
/* publish the ASE session information: export it to the shell commands and
|
|
408
|
+
render it, together with the constitution markdown, the output style, and
|
|
409
|
+
the deterministic ASE banner, into the "session-start" hook output payload */
|
|
410
|
+
publishSessionInfo(tool, info, md, mdDir, style) {
|
|
411
|
+
const { version, versionHint, pluginRoot, persona, guidance, userId, projectId, boxing, lifecycle, defaultSource, defaultChangeset, defaultTarget, specBasedir, specSchema, taskId, sessionId, headless } = info;
|
|
309
412
|
/* provide ASE information to Anthropic Claude Code CLI shell commands
|
|
310
413
|
(Anthropic Claude Code CLI only -- GitHub Copilot CLI has no equivalent mechanism) */
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
329
|
-
this.log.write("warning", `hook: failed to write environment file: ${message}`);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
414
|
+
this.writeEnvFile(tool, {
|
|
415
|
+
ASE_VERSION: version,
|
|
416
|
+
ASE_PLUGIN_ROOT: pluginRoot,
|
|
417
|
+
ASE_USER_ID: userId,
|
|
418
|
+
ASE_PROJECT_ID: projectId,
|
|
419
|
+
ASE_PROJECT_BOXING: boxing,
|
|
420
|
+
ASE_PROJECT_TASK_LIFECYCLE: lifecycle,
|
|
421
|
+
ASE_PROJECT_TASK_DEFAULT_SOURCE: defaultSource,
|
|
422
|
+
ASE_PROJECT_TASK_DEFAULT_CHANGESET: defaultChangeset,
|
|
423
|
+
ASE_PROJECT_TASK_DEFAULT_TARGET: defaultTarget,
|
|
424
|
+
ASE_SPEC_BASEDIR: specBasedir,
|
|
425
|
+
ASE_SPEC_SCHEMA: specSchema,
|
|
426
|
+
ASE_TASK_ID: taskId,
|
|
427
|
+
ASE_SESSION_ID: sessionId,
|
|
428
|
+
ASE_HEADLESS: headless,
|
|
429
|
+
ASE_AGENT_TOOL: tool
|
|
430
|
+
});
|
|
332
431
|
/* prepend ASE information to constitution markdown */
|
|
333
|
-
|
|
334
|
-
`<ase-version>${
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
432
|
+
let context = `<ase-version>${version}</ase-version>\n` +
|
|
433
|
+
`<ase-version-hint>${versionHint}</ase-version-hint>\n` +
|
|
434
|
+
`<ase-plugin-root>${pluginRoot}</ase-plugin-root>\n` +
|
|
435
|
+
`<ase-persona-style>${persona}</ase-persona-style>\n` +
|
|
436
|
+
`<ase-guidance-level>${guidance}</ase-guidance-level>\n` +
|
|
437
|
+
`<ase-user-id>${userId}</ase-user-id>\n` +
|
|
438
|
+
`<ase-project-id>${projectId}</ase-project-id>\n` +
|
|
439
|
+
`<ase-project-boxing>${boxing}</ase-project-boxing>\n` +
|
|
440
|
+
`<ase-project-task-lifecycle>${lifecycle}</ase-project-task-lifecycle>\n` +
|
|
441
|
+
`<ase-project-task-default-source>${defaultSource}</ase-project-task-default-source>\n` +
|
|
442
|
+
`<ase-project-task-default-changeset>${defaultChangeset}</ase-project-task-default-changeset>\n` +
|
|
443
|
+
`<ase-project-task-default-target>${defaultTarget}</ase-project-task-default-target>\n` +
|
|
444
|
+
`<ase-spec-basedir>${specBasedir}</ase-spec-basedir>\n` +
|
|
445
|
+
`<ase-spec-schema>${specSchema}</ase-spec-schema>\n` +
|
|
446
|
+
`<ase-task-id>${taskId}</ase-task-id>\n` +
|
|
447
|
+
`<ase-session-id>${sessionId}</ase-session-id>\n` +
|
|
448
|
+
`<ase-headless>${headless}</ase-headless>\n` +
|
|
449
|
+
`<ase-agent-tool>${tool}</ase-agent-tool>\n` +
|
|
450
|
+
"\n" + md;
|
|
349
451
|
/* expand all @<file> references manually */
|
|
350
|
-
|
|
452
|
+
context = this.expandReferences(context, mdDir);
|
|
351
453
|
/* append the output style to the constitution markdown (GitHub
|
|
352
454
|
Copilot CLI and OpenAI Codex CLI only -- Anthropic Claude Code CLI
|
|
353
455
|
applies the plugin output style natively), stripping the
|
|
354
456
|
YAML frontmatter which only Anthropic Claude Code CLI understands */
|
|
355
457
|
if (tool !== "claude")
|
|
356
|
-
|
|
458
|
+
context += "\n" + style.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "");
|
|
357
459
|
/* build the deterministic ASE banner (rendered directly by the
|
|
358
460
|
agent harness, independent of any model decision, so it is
|
|
359
461
|
guaranteed to appear once in every non-headless session;
|
|
@@ -361,7 +463,7 @@ export default class HookCommand {
|
|
|
361
463
|
"systemMessage" field for this -- GitHub Copilot CLI has no equivalent);
|
|
362
464
|
the trailing help hint is emitted only if the guidance level asks for it */
|
|
363
465
|
const banner = "\n" +
|
|
364
|
-
`\n⧉ ASE: ⎈ version: ${
|
|
466
|
+
`\n⧉ ASE: ⎈ version: ${version}${versionHint !== "" ? " " + versionHint.replace(/\*/g, "") : ""}` +
|
|
365
467
|
`\n⧉ ASE: ※ user: ${userId}, ⚑ project: ${projectId}` +
|
|
366
468
|
`\n⧉ ASE: ◉ task: ${taskId}, ⏻ session: ${sessionId}` +
|
|
367
469
|
`\n⧉ ASE: ☯ persona: ${persona}, ▶ guidance: ${guidance}, ▢ boxing: ${boxing}` +
|
|
@@ -376,10 +478,10 @@ export default class HookCommand {
|
|
|
376
478
|
const payload = tool !== "copilot" ? {
|
|
377
479
|
"hookSpecificOutput": {
|
|
378
480
|
"hookEventName": "SessionStart",
|
|
379
|
-
"additionalContext":
|
|
481
|
+
"additionalContext": context
|
|
380
482
|
}
|
|
381
483
|
} : {
|
|
382
|
-
"additionalContext":
|
|
484
|
+
"additionalContext": context
|
|
383
485
|
};
|
|
384
486
|
/* attach the deterministic banner as a top-level "systemMessage"
|
|
385
487
|
(only for the harnesses that support it and only when not
|
|
@@ -388,8 +490,7 @@ export default class HookCommand {
|
|
|
388
490
|
by letting the model emit the banner itself) */
|
|
389
491
|
if ((tool === "claude" || tool === "codex") && headless !== "true" && guidance !== "none")
|
|
390
492
|
payload.systemMessage = banner;
|
|
391
|
-
|
|
392
|
-
return 0;
|
|
493
|
+
return payload;
|
|
393
494
|
}
|
|
394
495
|
/* publish the agent activity marker to tmux as a per-pane user
|
|
395
496
|
option, so tmux can render the live state via
|
|
@@ -423,7 +524,7 @@ export default class HookCommand {
|
|
|
423
524
|
async doSessionEnd(_tool) {
|
|
424
525
|
/* determine session id */
|
|
425
526
|
const sessionId = await this.readSessionIdFromStdin();
|
|
426
|
-
/* remove the session directory
|
|
527
|
+
/* remove the session directory <state-dir>/session/<id> (only for a valid sessionId) */
|
|
427
528
|
if (this.isValidSessionId(sessionId)) {
|
|
428
529
|
const dir = path.join(this.sessionBaseDir(), sessionId);
|
|
429
530
|
try {
|
|
@@ -432,6 +533,18 @@ export default class HookCommand {
|
|
|
432
533
|
catch (_e) {
|
|
433
534
|
/* best-effort: ignore failures */
|
|
434
535
|
}
|
|
536
|
+
/* detach the session from its AI usage window (if any), which closes
|
|
537
|
+
and books the window once no other session remains attached to it
|
|
538
|
+
(noting the session end and starting the owning project service in the
|
|
539
|
+
background, if it is not reachable in time, and imported dynamically
|
|
540
|
+
to keep it out of all other hooks) */
|
|
541
|
+
try {
|
|
542
|
+
const { UsageTracker } = await import("./ase-service-otel.js");
|
|
543
|
+
await UsageTracker.detachRemote(sessionId);
|
|
544
|
+
}
|
|
545
|
+
catch (err) {
|
|
546
|
+
this.log.write("warning", `hook: failed to detach session from its usage window: ${err instanceof Error ? err.message : String(err)}`);
|
|
547
|
+
}
|
|
435
548
|
}
|
|
436
549
|
return 0;
|
|
437
550
|
}
|
|
@@ -449,16 +562,16 @@ export default class HookCommand {
|
|
|
449
562
|
}));
|
|
450
563
|
return this.pickSessionId(input);
|
|
451
564
|
}
|
|
452
|
-
/* read the
|
|
565
|
+
/* read the "skill" value of the internal session state */
|
|
453
566
|
readActiveSkill(sessionId) {
|
|
454
567
|
if (!this.isValidSessionId(sessionId))
|
|
455
568
|
return "";
|
|
456
569
|
try {
|
|
457
|
-
const
|
|
570
|
+
const state = sessionState(this.log, sessionId);
|
|
458
571
|
let val = "";
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
const skill =
|
|
572
|
+
state.lock(() => {
|
|
573
|
+
state.read();
|
|
574
|
+
const skill = state.get("skill");
|
|
462
575
|
if (typeof skill === "string")
|
|
463
576
|
val = skill;
|
|
464
577
|
});
|
|
@@ -498,9 +611,9 @@ export default class HookCommand {
|
|
|
498
611
|
const rawInput = input[spec.toolInputField];
|
|
499
612
|
const toolName = typeof rawName === "string" ? rawName : "";
|
|
500
613
|
let toolInput = {};
|
|
501
|
-
if (
|
|
614
|
+
if (typeof rawInput === "string")
|
|
502
615
|
toolInput = this.parseJSON(rawInput, toolInputSchema);
|
|
503
|
-
else if (
|
|
616
|
+
else if (typeof rawInput === "object" && rawInput !== null) {
|
|
504
617
|
const result = v.safeParse(toolInputSchema, rawInput);
|
|
505
618
|
if (result.success)
|
|
506
619
|
toolInput = result.output;
|
|
@@ -617,6 +730,7 @@ export default class HookCommand {
|
|
|
617
730
|
/* register CLI sub-commands "ase hook <event>" */
|
|
618
731
|
const subCmds = [
|
|
619
732
|
{ name: "session-start", desc: "handle SessionStart hook event", handler: (tool) => this.doSessionStart(tool) },
|
|
733
|
+
{ name: "session-persona", desc: "handle SessionStart hook event (persona)", handler: (tool) => this.doSessionPersona(tool) },
|
|
620
734
|
{ name: "session-end", desc: "handle SessionEnd hook event", handler: (tool) => this.doSessionEnd(tool) },
|
|
621
735
|
{ name: "pre-tool-use", desc: "handle tool PreToolUse hook event", handler: (tool) => this.doPreToolUse(tool) },
|
|
622
736
|
{ name: "permission-request", desc: "handle tool PermissionRequest hook event", handler: (tool) => this.doPermissionRequest(tool) },
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** Agentic Software Engineering (ASE)
|
|
3
|
+
** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
|
+
*/
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { spawn } from "node:child_process";
|
|
10
|
+
import { Service } from "./ase-service.js";
|
|
11
|
+
/* the CLI command group managing a long-running process (a server, an agent, or a fleet
|
|
12
|
+
of them) in the background or in the foreground: it registers the command group, spawns
|
|
13
|
+
the process detached into its log file, awaits its startup, checks whether it is alive,
|
|
14
|
+
shows its log file, and stops it, where the concrete command decides on its configuration and runtime state */
|
|
15
|
+
export class DaemonCommand {
|
|
16
|
+
log;
|
|
17
|
+
constructor(log) {
|
|
18
|
+
this.log = log;
|
|
19
|
+
}
|
|
20
|
+
/* register the CLI sub-command group below the CLI command <parent>,
|
|
21
|
+
which shows its help on its own (as it requires a sub-command) */
|
|
22
|
+
group(parent) {
|
|
23
|
+
const group = parent
|
|
24
|
+
.command(this.words.split(" ").at(-1))
|
|
25
|
+
.description(this.description)
|
|
26
|
+
.action(() => {
|
|
27
|
+
group.outputHelp();
|
|
28
|
+
process.exit(1);
|
|
29
|
+
});
|
|
30
|
+
return group;
|
|
31
|
+
}
|
|
32
|
+
/* register the CLI sub-command "log" below the CLI sub-command group <group>, showing the
|
|
33
|
+
log file (resolved by <logFile> from the options of the sub-command) of the process run
|
|
34
|
+
in the background, where <setup> adds further options to the sub-command and each shown
|
|
35
|
+
line is passed through <transform> (e.g. to colorize parts of it) */
|
|
36
|
+
registerLog(group, logFile, setup = () => { }, transform) {
|
|
37
|
+
const cmd = group
|
|
38
|
+
.command("log")
|
|
39
|
+
.description(`Show the log of the ${this.label} run in the background`)
|
|
40
|
+
.option("-f, --follow", "keep following the log for newly appended lines")
|
|
41
|
+
.option("-n, --lines <n>", "show the last <n> lines only (default: all, or 10 with --follow)");
|
|
42
|
+
setup(cmd);
|
|
43
|
+
cmd.action(async (opts) => {
|
|
44
|
+
const file = logFile(opts);
|
|
45
|
+
if (!fs.existsSync(file)) {
|
|
46
|
+
this.log.write("info", `${this.label}: no log file "${file}" ` +
|
|
47
|
+
"(not yet run in the background, as one run in the foreground logs to the terminal)");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
process.exitCode = await Service.showLog(file, opts, transform);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/* spawn the CLI with the arguments <args> detached, with the additional
|
|
54
|
+
environment <env>, logging into the (trimmed) log file <logFile> */
|
|
55
|
+
spawnDetached(args, env, logFile) {
|
|
56
|
+
fs.mkdirSync(path.dirname(logFile), { recursive: true });
|
|
57
|
+
Service.trimLog(logFile);
|
|
58
|
+
const fd = fs.openSync(logFile, "a");
|
|
59
|
+
const entry = fileURLToPath(new URL("./ase.js", import.meta.url));
|
|
60
|
+
const child = spawn(process.execPath, [entry, ...args], {
|
|
61
|
+
detached: true,
|
|
62
|
+
env: { ...process.env, ...env },
|
|
63
|
+
stdio: ["ignore", fd, fd]
|
|
64
|
+
});
|
|
65
|
+
fs.closeSync(fd);
|
|
66
|
+
return child;
|
|
67
|
+
}
|
|
68
|
+
/* await the readiness of a child process (checked every 100ms up to <timeout> milliseconds),
|
|
69
|
+
resolving to null once ready, else to the reason of its failure, like "exited during
|
|
70
|
+
startup (code 1)" (where a child process not ready within the timeout is terminated) */
|
|
71
|
+
async awaitChild(child, ready, timeout) {
|
|
72
|
+
let exited = false;
|
|
73
|
+
let exitCode = null;
|
|
74
|
+
child.once("exit", (code) => {
|
|
75
|
+
exited = true;
|
|
76
|
+
exitCode = code;
|
|
77
|
+
});
|
|
78
|
+
for (let waited = 0; waited < timeout; waited += 100) {
|
|
79
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
80
|
+
if (exited)
|
|
81
|
+
break;
|
|
82
|
+
if (ready())
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
if (!exited)
|
|
86
|
+
child.kill("SIGTERM");
|
|
87
|
+
return exited ?
|
|
88
|
+
`exited during startup (code ${exitCode})` :
|
|
89
|
+
"failed to start within timeout";
|
|
90
|
+
}
|
|
91
|
+
/* await the readiness of the detached process, else fail with the tail of its log file */
|
|
92
|
+
async awaitDetached(child, logFile, ready, timeout) {
|
|
93
|
+
const failure = await this.awaitChild(child, ready, timeout);
|
|
94
|
+
child.unref();
|
|
95
|
+
if (failure === null)
|
|
96
|
+
return;
|
|
97
|
+
const reason = `${this.label}: ${failure}`;
|
|
98
|
+
const tail = Service.readLogTail(logFile, 20);
|
|
99
|
+
throw new Error(tail.length > 0 ? `${reason}\n---- ${logFile} (tail) ----\n${tail}` : reason);
|
|
100
|
+
}
|
|
101
|
+
/* check whether a process is still alive */
|
|
102
|
+
isAlive(pid) {
|
|
103
|
+
try {
|
|
104
|
+
process.kill(pid, 0);
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
return err.code === "EPERM";
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/* terminate a process and wait (up to <timeout> milliseconds) until it is gone,
|
|
112
|
+
resolving to false if it was already gone */
|
|
113
|
+
async terminate(pid, timeout) {
|
|
114
|
+
try {
|
|
115
|
+
process.kill(pid, "SIGTERM");
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
const e = err;
|
|
119
|
+
if (e.code !== "ESRCH")
|
|
120
|
+
throw err;
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
for (let waited = 0; waited < timeout; waited += 100) {
|
|
124
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
125
|
+
if (!this.isAlive(pid))
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
throw new Error(`${this.label}: process ${pid} did not stop within timeout`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** Agentic Software Engineering (ASE)
|
|
3
|
+
** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
|
+
*/
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
/* the alphabet and length of the NanoId-style hashes */
|
|
8
|
+
const BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
9
|
+
const NANOID = 10;
|
|
10
|
+
/* derive the short NanoId-style hash of a hint: the SHA-1 digest of
|
|
11
|
+
the hint, base62-encoded and truncated to its lowest digits */
|
|
12
|
+
export const nanoid = (hint) => {
|
|
13
|
+
let n = BigInt(`0x${createHash("sha1").update(hint).digest("hex")}`);
|
|
14
|
+
let id = "";
|
|
15
|
+
for (let i = 0; i < NANOID; i++) {
|
|
16
|
+
id += BASE62[Number(n % 62n)];
|
|
17
|
+
n /= 62n;
|
|
18
|
+
}
|
|
19
|
+
return id;
|
|
20
|
+
};
|