@rse/ase 1.0.7 → 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 +228 -111
- 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",
|
|
@@ -88,12 +92,13 @@ export default class HookCommand {
|
|
|
88
92
|
}
|
|
89
93
|
/* resolve the base directory holding all per-session state */
|
|
90
94
|
sessionBaseDir() {
|
|
91
|
-
return path.join(
|
|
95
|
+
return path.join(userStateDir(), "session");
|
|
92
96
|
}
|
|
93
97
|
/* garbage-collect orphaned session directories left behind by agents
|
|
94
98
|
which died before their session-end hook could run; a live session
|
|
95
|
-
|
|
96
|
-
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 */
|
|
97
102
|
pruneStaleSessions(currentSessionId) {
|
|
98
103
|
const base = this.sessionBaseDir();
|
|
99
104
|
let entries;
|
|
@@ -198,42 +203,20 @@ export default class HookCommand {
|
|
|
198
203
|
return "";
|
|
199
204
|
}
|
|
200
205
|
}
|
|
201
|
-
/*
|
|
202
|
-
|
|
203
|
-
/* determine plugin root (env var name differs per tool) */
|
|
204
|
-
const pluginRoot = this.pluginRoot(tool);
|
|
205
|
-
/* determine path to external files */
|
|
206
|
-
const filePkg = path.join(pluginRoot, ".claude-plugin", "plugin.json");
|
|
207
|
-
const fileMd = path.join(pluginRoot, "meta", "ase-constitution.md");
|
|
208
|
-
const fileStyle = path.join(pluginRoot, "output-styles", "ase-terse.md");
|
|
209
|
-
/* read external files */
|
|
210
|
-
let pkg;
|
|
211
|
-
let md;
|
|
212
|
-
let style;
|
|
206
|
+
/* read a plugin file, throwing a descriptive error on failure */
|
|
207
|
+
readPluginFile(file, what) {
|
|
213
208
|
try {
|
|
214
|
-
|
|
209
|
+
return fs.readFileSync(file, "utf8");
|
|
215
210
|
}
|
|
216
211
|
catch (err) {
|
|
217
|
-
throw new Error(`failed to read
|
|
218
|
-
}
|
|
219
|
-
try {
|
|
220
|
-
md = fs.readFileSync(fileMd, "utf8");
|
|
212
|
+
throw new Error(`failed to read ${what}: ${file}`, { cause: err });
|
|
221
213
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
style = fs.readFileSync(fileStyle, "utf8");
|
|
227
|
-
}
|
|
228
|
-
catch (err) {
|
|
229
|
-
throw new Error(`failed to read output style file: ${fileStyle}`, { cause: err });
|
|
230
|
-
}
|
|
231
|
-
/* determine own version */
|
|
232
|
-
const pkgObj = this.parseJSON(pkg, v.object({ version: v.optional(v.string()) }));
|
|
233
|
-
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) {
|
|
234
218
|
const versionCurrentTool = Version.current();
|
|
235
219
|
const versionLatestTool = await Version.latest();
|
|
236
|
-
/* sanity check situation */
|
|
237
220
|
const versionHints = [];
|
|
238
221
|
if (versionCurrentPlugin !== versionCurrentTool)
|
|
239
222
|
versionHints.push("**WARNING:** version *mismatch*: " +
|
|
@@ -242,7 +225,81 @@ export default class HookCommand {
|
|
|
242
225
|
versionHints.push(`**NOTICE:** *latest* version: **${versionLatestTool}**, please update!`);
|
|
243
226
|
if (process.env.ASE_SETUP_DEV !== undefined)
|
|
244
227
|
versionHints.push("**NOTICE:** *development* setup");
|
|
245
|
-
|
|
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);
|
|
246
303
|
/* read session information (Anthropic Claude Code CLI uses snake_case fields,
|
|
247
304
|
GitHub Copilot CLI uses camelCase fields) */
|
|
248
305
|
const stdin = await readStdin().catch(() => "");
|
|
@@ -253,36 +310,48 @@ export default class HookCommand {
|
|
|
253
310
|
}));
|
|
254
311
|
/* determine session id */
|
|
255
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);
|
|
256
319
|
/* garbage-collect orphaned session directories of previous agent runs */
|
|
257
320
|
this.pruneStaleSessions(sessionId);
|
|
258
321
|
/* establish config context (session-scoped only if a valid sessionId is present) */
|
|
259
322
|
const hasSession = this.isValidSessionId(sessionId);
|
|
260
323
|
const cfg = new Config("config", configSchema, this.log, hasSession ? parseScope(`session:${sessionId}`) : parseScope(undefined));
|
|
261
|
-
/* 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 */
|
|
262
340
|
const taskId = process.env.ASE_TASK_ID ?? "default";
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
+
}
|
|
270
351
|
/* initialize agent activity status */
|
|
271
352
|
this.writeAgentStatus("ready");
|
|
272
353
|
/* determine project id */
|
|
273
|
-
const
|
|
274
|
-
let projectDir = cwd;
|
|
275
|
-
try {
|
|
276
|
-
const result = execaSync("git", ["rev-parse", "--show-toplevel"], {
|
|
277
|
-
stderr: "ignore", cwd
|
|
278
|
-
});
|
|
279
|
-
if (result.stdout.trim() !== "")
|
|
280
|
-
projectDir = result.stdout.trim();
|
|
281
|
-
}
|
|
282
|
-
catch {
|
|
283
|
-
/* not inside a Git working tree */
|
|
284
|
-
}
|
|
285
|
-
const projectId = path.basename(projectDir);
|
|
354
|
+
const projectId = this.determineProjectId(projectDir, cfg);
|
|
286
355
|
/* determine user id */
|
|
287
356
|
const userId = process.env.USER ?? process.env.LOGNAME ?? "unknown";
|
|
288
357
|
/* helper function: determine a setting from the explicitly configured
|
|
@@ -293,64 +362,100 @@ export default class HookCommand {
|
|
|
293
362
|
const val = cfg.getExplicit(key);
|
|
294
363
|
return typeof val === "string" ? val : (process.env[envVar] ?? dflt);
|
|
295
364
|
};
|
|
296
|
-
/* determine agent persona style, agent guidance level,
|
|
365
|
+
/* determine agent persona style, agent guidance level, project
|
|
366
|
+
boxing transparency, and project task lifecycle */
|
|
297
367
|
const persona = setting("agent.persona", "ASE_PERSONA_STYLE", "engineer");
|
|
298
368
|
const guidance = setting("agent.guidance", "ASE_GUIDANCE_LEVEL", "normal");
|
|
299
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");
|
|
300
375
|
/* determine the specification base directory and the whitespace-separated
|
|
301
376
|
list of SpecBook schema configurations (empty: the bundled standard schema) */
|
|
302
377
|
const specBasedir = String(cfg.get("project.artifact.spec.basedir") ?? "");
|
|
303
378
|
const specSchema = String(cfg.get("project.artifact.spec.schema") ?? "");
|
|
304
|
-
/*
|
|
305
|
-
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;
|
|
306
412
|
/* provide ASE information to Anthropic Claude Code CLI shell commands
|
|
307
413
|
(Anthropic Claude Code CLI only -- GitHub Copilot CLI has no equivalent mechanism) */
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
326
|
-
this.log.write("warning", `hook: failed to write environment file: ${message}`);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
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
|
+
});
|
|
329
431
|
/* prepend ASE information to constitution markdown */
|
|
330
|
-
|
|
331
|
-
`<ase-version>${
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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;
|
|
346
451
|
/* expand all @<file> references manually */
|
|
347
|
-
|
|
452
|
+
context = this.expandReferences(context, mdDir);
|
|
348
453
|
/* append the output style to the constitution markdown (GitHub
|
|
349
454
|
Copilot CLI and OpenAI Codex CLI only -- Anthropic Claude Code CLI
|
|
350
455
|
applies the plugin output style natively), stripping the
|
|
351
456
|
YAML frontmatter which only Anthropic Claude Code CLI understands */
|
|
352
457
|
if (tool !== "claude")
|
|
353
|
-
|
|
458
|
+
context += "\n" + style.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "");
|
|
354
459
|
/* build the deterministic ASE banner (rendered directly by the
|
|
355
460
|
agent harness, independent of any model decision, so it is
|
|
356
461
|
guaranteed to appear once in every non-headless session;
|
|
@@ -358,7 +463,7 @@ export default class HookCommand {
|
|
|
358
463
|
"systemMessage" field for this -- GitHub Copilot CLI has no equivalent);
|
|
359
464
|
the trailing help hint is emitted only if the guidance level asks for it */
|
|
360
465
|
const banner = "\n" +
|
|
361
|
-
`\n⧉ ASE: ⎈ version: ${
|
|
466
|
+
`\n⧉ ASE: ⎈ version: ${version}${versionHint !== "" ? " " + versionHint.replace(/\*/g, "") : ""}` +
|
|
362
467
|
`\n⧉ ASE: ※ user: ${userId}, ⚑ project: ${projectId}` +
|
|
363
468
|
`\n⧉ ASE: ◉ task: ${taskId}, ⏻ session: ${sessionId}` +
|
|
364
469
|
`\n⧉ ASE: ☯ persona: ${persona}, ▶ guidance: ${guidance}, ▢ boxing: ${boxing}` +
|
|
@@ -373,10 +478,10 @@ export default class HookCommand {
|
|
|
373
478
|
const payload = tool !== "copilot" ? {
|
|
374
479
|
"hookSpecificOutput": {
|
|
375
480
|
"hookEventName": "SessionStart",
|
|
376
|
-
"additionalContext":
|
|
481
|
+
"additionalContext": context
|
|
377
482
|
}
|
|
378
483
|
} : {
|
|
379
|
-
"additionalContext":
|
|
484
|
+
"additionalContext": context
|
|
380
485
|
};
|
|
381
486
|
/* attach the deterministic banner as a top-level "systemMessage"
|
|
382
487
|
(only for the harnesses that support it and only when not
|
|
@@ -385,8 +490,7 @@ export default class HookCommand {
|
|
|
385
490
|
by letting the model emit the banner itself) */
|
|
386
491
|
if ((tool === "claude" || tool === "codex") && headless !== "true" && guidance !== "none")
|
|
387
492
|
payload.systemMessage = banner;
|
|
388
|
-
|
|
389
|
-
return 0;
|
|
493
|
+
return payload;
|
|
390
494
|
}
|
|
391
495
|
/* publish the agent activity marker to tmux as a per-pane user
|
|
392
496
|
option, so tmux can render the live state via
|
|
@@ -420,7 +524,7 @@ export default class HookCommand {
|
|
|
420
524
|
async doSessionEnd(_tool) {
|
|
421
525
|
/* determine session id */
|
|
422
526
|
const sessionId = await this.readSessionIdFromStdin();
|
|
423
|
-
/* remove the session directory
|
|
527
|
+
/* remove the session directory <state-dir>/session/<id> (only for a valid sessionId) */
|
|
424
528
|
if (this.isValidSessionId(sessionId)) {
|
|
425
529
|
const dir = path.join(this.sessionBaseDir(), sessionId);
|
|
426
530
|
try {
|
|
@@ -429,6 +533,18 @@ export default class HookCommand {
|
|
|
429
533
|
catch (_e) {
|
|
430
534
|
/* best-effort: ignore failures */
|
|
431
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
|
+
}
|
|
432
548
|
}
|
|
433
549
|
return 0;
|
|
434
550
|
}
|
|
@@ -446,16 +562,16 @@ export default class HookCommand {
|
|
|
446
562
|
}));
|
|
447
563
|
return this.pickSessionId(input);
|
|
448
564
|
}
|
|
449
|
-
/* read the
|
|
565
|
+
/* read the "skill" value of the internal session state */
|
|
450
566
|
readActiveSkill(sessionId) {
|
|
451
567
|
if (!this.isValidSessionId(sessionId))
|
|
452
568
|
return "";
|
|
453
569
|
try {
|
|
454
|
-
const
|
|
570
|
+
const state = sessionState(this.log, sessionId);
|
|
455
571
|
let val = "";
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
const skill =
|
|
572
|
+
state.lock(() => {
|
|
573
|
+
state.read();
|
|
574
|
+
const skill = state.get("skill");
|
|
459
575
|
if (typeof skill === "string")
|
|
460
576
|
val = skill;
|
|
461
577
|
});
|
|
@@ -614,6 +730,7 @@ export default class HookCommand {
|
|
|
614
730
|
/* register CLI sub-commands "ase hook <event>" */
|
|
615
731
|
const subCmds = [
|
|
616
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) },
|
|
617
734
|
{ name: "session-end", desc: "handle SessionEnd hook event", handler: (tool) => this.doSessionEnd(tool) },
|
|
618
735
|
{ name: "pre-tool-use", desc: "handle tool PreToolUse hook event", handler: (tool) => this.doPreToolUse(tool) },
|
|
619
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
|
+
};
|