@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
|
@@ -0,0 +1,351 @@
|
|
|
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 * as v from "valibot";
|
|
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";
|
|
11
|
+
/* classification taxonomy */
|
|
12
|
+
export const projectClassification = {
|
|
13
|
+
boxing: ["white", "grey", "black"],
|
|
14
|
+
lifecycle: ["solo", "team", "enterprise"]
|
|
15
|
+
};
|
|
16
|
+
/* agent classification taxonomy */
|
|
17
|
+
export const agentClassification = {
|
|
18
|
+
persona: ["writer", "engineer", "journalist", "telegrapher", "caveman"],
|
|
19
|
+
guidance: ["none", "minimal", "normal", "verbose"]
|
|
20
|
+
};
|
|
21
|
+
/* the default colors of the four color roles of the terminal board
|
|
22
|
+
("default" is the foreground color of the terminal) */
|
|
23
|
+
export const tuiColorDefaults = {
|
|
24
|
+
dim: "grey",
|
|
25
|
+
normal: "default",
|
|
26
|
+
accent: "blue",
|
|
27
|
+
signal: "red"
|
|
28
|
+
};
|
|
29
|
+
/* the default base colors of the four color roles of the web board */
|
|
30
|
+
export const webColorDefaults = {
|
|
31
|
+
dim: "grey",
|
|
32
|
+
normal: "black",
|
|
33
|
+
accent: "blue",
|
|
34
|
+
signal: "orange"
|
|
35
|
+
};
|
|
36
|
+
/* classification presets */
|
|
37
|
+
export const projectClassificationPresets = {
|
|
38
|
+
vibe: {
|
|
39
|
+
"agent.persona": "writer",
|
|
40
|
+
"project.name": "Example Project",
|
|
41
|
+
"project.boxing": "black"
|
|
42
|
+
},
|
|
43
|
+
pro: {
|
|
44
|
+
"agent.persona": "engineer",
|
|
45
|
+
"project.name": "Example Project",
|
|
46
|
+
"project.boxing": "white"
|
|
47
|
+
},
|
|
48
|
+
default: {
|
|
49
|
+
"agent.persona": "engineer",
|
|
50
|
+
"agent.guidance": "normal",
|
|
51
|
+
"project.name": "Example Project",
|
|
52
|
+
"project.boxing": "white",
|
|
53
|
+
"project.users": "",
|
|
54
|
+
"project.task.lifecycle": "solo",
|
|
55
|
+
"project.task.idscheme": "slug",
|
|
56
|
+
"project.task.store.url": "ase:./.ase/task",
|
|
57
|
+
"project.task.default.source": "worktree",
|
|
58
|
+
"project.task.default.changeset": "worktree",
|
|
59
|
+
"project.task.default.target": "source",
|
|
60
|
+
"project.task.effort.scale": EFFORT_SCALE,
|
|
61
|
+
"project.task.effort.factor": "1",
|
|
62
|
+
"project.task.effort.file": "1",
|
|
63
|
+
"project.task.effort.line": "0.025",
|
|
64
|
+
"project.task.effort.iteration": "2.5",
|
|
65
|
+
"project.artifact.spec.basedir": "docs/specbook",
|
|
66
|
+
"project.artifact.spec.files": "*.{md,txt,svg,png,jpg}",
|
|
67
|
+
"project.artifact.spec.schema": "",
|
|
68
|
+
"project.artifact.code.basedir": "src",
|
|
69
|
+
"project.artifact.code.files": "** !**/etc/** !**/{.gitignore,.npmignore,package.json}",
|
|
70
|
+
"project.artifact.docs.basedir": "doc",
|
|
71
|
+
"project.artifact.docs.files": "** **/{README,LICENSE,CHANGELOG}.{md,txt} !{spec,arch}/**",
|
|
72
|
+
"project.artifact.infr.basedir": "",
|
|
73
|
+
"project.artifact.infr.files": "**/{.github,.claude*,etc}/** **/{AGENTS.md,{package,tsconfig*}.json,.{git,npm}ignore}",
|
|
74
|
+
"board.tui.color.dim": tuiColorDefaults.dim,
|
|
75
|
+
"board.tui.color.normal": tuiColorDefaults.normal,
|
|
76
|
+
"board.tui.color.accent": tuiColorDefaults.accent,
|
|
77
|
+
"board.tui.color.signal": tuiColorDefaults.signal,
|
|
78
|
+
"board.web.color.dim": webColorDefaults.dim,
|
|
79
|
+
"board.web.color.normal": webColorDefaults.normal,
|
|
80
|
+
"board.web.color.accent": webColorDefaults.accent,
|
|
81
|
+
"board.web.color.signal": webColorDefaults.signal,
|
|
82
|
+
"board.web.editor.keymap": "default",
|
|
83
|
+
"telemetry.enabled": "false",
|
|
84
|
+
"telemetry.implicit": "true",
|
|
85
|
+
"telemetry.prices.ai": "*=4.00/20.00/0.20/5.00",
|
|
86
|
+
"telemetry.prices.hr": "*=110"
|
|
87
|
+
},
|
|
88
|
+
industry: {
|
|
89
|
+
"agent.persona": "engineer",
|
|
90
|
+
"project.name": "Example Project",
|
|
91
|
+
"project.boxing": "grey"
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
/* hard-coded map: which scope kinds each variable may be SET on
|
|
95
|
+
(reads always cascade through the full chain; this restricts writes only);
|
|
96
|
+
keys absent from this map default to all non-"default" scope kinds */
|
|
97
|
+
export const configWritableScopes = {
|
|
98
|
+
"project.users": ["user", "project"],
|
|
99
|
+
"project.task.store.url": ["user", "project"],
|
|
100
|
+
"project.task.store.token": ["user"],
|
|
101
|
+
"project.task.store.crt": ["user", "project"],
|
|
102
|
+
"project.task.store.key": ["user", "project"],
|
|
103
|
+
"project.task.lifecycle": ["user", "project"],
|
|
104
|
+
"project.task.idscheme": ["user", "project"],
|
|
105
|
+
"project.task.default.source": ["user", "project"],
|
|
106
|
+
"project.task.default.changeset": ["user", "project"],
|
|
107
|
+
"project.task.default.target": ["user", "project"],
|
|
108
|
+
"project.task.effort.scale": ["user", "project"],
|
|
109
|
+
"project.task.effort.factor": ["user", "project"],
|
|
110
|
+
"project.task.effort.file": ["user", "project"],
|
|
111
|
+
"project.task.effort.line": ["user", "project"],
|
|
112
|
+
"project.task.effort.iteration": ["user", "project"],
|
|
113
|
+
"project.relay.url": ["user", "project"],
|
|
114
|
+
"project.relay.token": ["user"],
|
|
115
|
+
"project.relay.crt": ["user", "project"],
|
|
116
|
+
"project.relay.key": ["user", "project"],
|
|
117
|
+
"project.artifact.spec.basedir": ["user", "project"],
|
|
118
|
+
"project.artifact.spec.files": ["user", "project"],
|
|
119
|
+
"project.artifact.spec.schema": ["user", "project"],
|
|
120
|
+
"project.artifact.code.basedir": ["user", "project"],
|
|
121
|
+
"project.artifact.code.files": ["user", "project"],
|
|
122
|
+
"project.artifact.docs.basedir": ["user", "project"],
|
|
123
|
+
"project.artifact.docs.files": ["user", "project"],
|
|
124
|
+
"project.artifact.infr.basedir": ["user", "project"],
|
|
125
|
+
"project.artifact.infr.files": ["user", "project"],
|
|
126
|
+
"board.tui.color.dim": ["user", "project"],
|
|
127
|
+
"board.tui.color.normal": ["user", "project"],
|
|
128
|
+
"board.tui.color.accent": ["user", "project"],
|
|
129
|
+
"board.tui.color.signal": ["user", "project"],
|
|
130
|
+
"board.web.color.dim": ["user", "project"],
|
|
131
|
+
"board.web.color.normal": ["user", "project"],
|
|
132
|
+
"board.web.color.accent": ["user", "project"],
|
|
133
|
+
"board.web.color.signal": ["user", "project"],
|
|
134
|
+
"board.web.editor.keymap": ["user", "project"],
|
|
135
|
+
"telemetry.enabled": ["user", "project"],
|
|
136
|
+
"telemetry.implicit": ["user", "project"],
|
|
137
|
+
"telemetry.service.port": ["user"],
|
|
138
|
+
"telemetry.service.token": ["user"],
|
|
139
|
+
"telemetry.service.interval": ["user", "project"],
|
|
140
|
+
"telemetry.service.idle": ["user", "project"],
|
|
141
|
+
"telemetry.prices.ai": ["user"],
|
|
142
|
+
"telemetry.prices.hr": ["user", "project"]
|
|
143
|
+
};
|
|
144
|
+
/* default set of scope kinds writable for any unrestricted key */
|
|
145
|
+
export const configWritableScopesDefault = ["user", "project", "task", "session"];
|
|
146
|
+
/* keys carrying secrets, whose values are masked in listings */
|
|
147
|
+
export const configSecretKeys = ["project.task.store.token", "project.relay.token", "telemetry.service.token"];
|
|
148
|
+
/* parse the per-model price table "<model>=<input>/<output>[/<cread>/<cwrite>][ ...]"
|
|
149
|
+
of the AI usage accounting (USD per million input, output, cache read, and cache
|
|
150
|
+
write tokens, where missing cache prices fall back to the input price), keyed by model */
|
|
151
|
+
export const parsePrices = (spec) => {
|
|
152
|
+
const prices = new Map();
|
|
153
|
+
for (const entry of spec.split(/\s+/).filter((e) => e !== "")) {
|
|
154
|
+
const m = /^([^=\s]+)=(\d+(?:\.\d+)?)\/(\d+(?:\.\d+)?)(?:\/(\d+(?:\.\d+)?)\/(\d+(?:\.\d+)?))?$/.exec(entry);
|
|
155
|
+
if (m === null)
|
|
156
|
+
throw new Error(`invalid price entry "${entry}"`);
|
|
157
|
+
prices.set(m[1], {
|
|
158
|
+
input: Number(m[2]),
|
|
159
|
+
output: Number(m[3]),
|
|
160
|
+
cread: Number(m[4] ?? m[2]),
|
|
161
|
+
cwrite: Number(m[5] ?? m[2])
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return prices;
|
|
165
|
+
};
|
|
166
|
+
/* the price of a model (or tool): its own one, else the one of "*", else none */
|
|
167
|
+
export const modelPrice = (prices, model) => prices.get(model) ?? prices.get("*");
|
|
168
|
+
/* the cost (in USD) of token counts at a price (zero without a price) */
|
|
169
|
+
export const tokenCost = (price, tokens) => price === undefined ? 0 : (tokens.cread * price.cread + tokens.cwrite * price.cwrite +
|
|
170
|
+
tokens.input * price.input + tokens.output * price.output) / 1e6;
|
|
171
|
+
/* check whether a specification is accepted by its parser */
|
|
172
|
+
const parsable = (parse, spec) => {
|
|
173
|
+
try {
|
|
174
|
+
parse(spec);
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
/* parse the per-user hourly rate table "<user-id>=<usd-per-hour>[,...]" of the
|
|
182
|
+
human time of the AI usage accounting, keyed by user id (with "*" for all others) */
|
|
183
|
+
export const parseRates = (spec) => {
|
|
184
|
+
const rates = new Map();
|
|
185
|
+
for (const entry of spec.split(",").map((e) => e.trim()).filter((e) => e !== "")) {
|
|
186
|
+
const m = /^([^=,\s]+)=(\d+(?:\.\d+)?)$/.exec(entry);
|
|
187
|
+
if (m === null)
|
|
188
|
+
throw new Error(`invalid hourly rate entry "${entry}"`);
|
|
189
|
+
rates.set(m[1], Number(m[2]));
|
|
190
|
+
}
|
|
191
|
+
return rates;
|
|
192
|
+
};
|
|
193
|
+
/* the hourly rate (in USD) of a user: its own one, else the one of "*", else zero */
|
|
194
|
+
export const userRate = (rates, user) => rates.get(user) ?? rates.get("*") ?? 0;
|
|
195
|
+
/* schema for a single artifact kind's "basedir"/"files" specification */
|
|
196
|
+
const artifactSchema = v.optional(v.strictObject({
|
|
197
|
+
basedir: v.optional(v.string()),
|
|
198
|
+
files: v.optional(v.string())
|
|
199
|
+
}));
|
|
200
|
+
/* schema for the "spec" artifact kind, additionally carrying the
|
|
201
|
+
whitespace-separated list of SpecBook YAML "schema" configuration
|
|
202
|
+
files, with "std" naming the bundled standard one (empty: "std") */
|
|
203
|
+
const artifactSpecSchema = v.optional(v.strictObject({
|
|
204
|
+
basedir: v.optional(v.string()),
|
|
205
|
+
files: v.optional(v.string()),
|
|
206
|
+
schema: v.optional(v.string())
|
|
207
|
+
}));
|
|
208
|
+
/* schema for a terminal color: "default" (the foreground color of the
|
|
209
|
+
terminal), a color name, "#rgb", "#rrggbb", "ansi256(<n>)", or "rgb(<r>,<g>,<b>)" */
|
|
210
|
+
const colorSchema = v.optional(v.pipe(v.string(), v.check((s) => s === "default"
|
|
211
|
+
|| foregroundColorNames.includes(s)
|
|
212
|
+
|| /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(s)
|
|
213
|
+
|| /^ansi256\(\s?\d+\s?\)$/.test(s)
|
|
214
|
+
|| /^rgb\(\s?\d+,\s?\d+,\s?\d+\s?\)$/.test(s), "expected \"default\", a color name, \"#rrggbb\", \"ansi256(<n>)\", or \"rgb(<r>,<g>,<b>)\"")));
|
|
215
|
+
/* the color names accepted for the web board, mapped onto pleasant base colors */
|
|
216
|
+
export const webColorNames = {
|
|
217
|
+
black: "#1a1a1a",
|
|
218
|
+
grey: "#999999",
|
|
219
|
+
gray: "#999999",
|
|
220
|
+
brown: "#8a5a3c",
|
|
221
|
+
red: "#b03a30",
|
|
222
|
+
orange: "#b06820",
|
|
223
|
+
yellow: "#b09020",
|
|
224
|
+
green: "#3d8a3d",
|
|
225
|
+
teal: "#2a8a7a",
|
|
226
|
+
cyan: "#2a8aa0",
|
|
227
|
+
blue: "#336699",
|
|
228
|
+
purple: "#6a4a99",
|
|
229
|
+
magenta: "#993d80"
|
|
230
|
+
};
|
|
231
|
+
/* schema for a web color: "default" (the default base color of the role),
|
|
232
|
+
a color name, "#rgb", or "#rrggbb" */
|
|
233
|
+
const webColorSchema = v.optional(v.pipe(v.string(), v.check((s) => s === "default"
|
|
234
|
+
|| Object.hasOwn(webColorNames, s)
|
|
235
|
+
|| /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(s), `expected "default", a color name (${Object.keys(webColorNames).join(", ")}), "#rgb", or "#rrggbb"`)));
|
|
236
|
+
/* schema for a default value of the "Source", "Changeset", or "Target" task
|
|
237
|
+
frontmatter key, restricted to the permitted forms "worktree", "source",
|
|
238
|
+
"branch:<name>" (a safe Git branch name), and "attachment:<name>", where a
|
|
239
|
+
<name> can carry the placeholder "{id}" of the task id (checked with a sample id) */
|
|
240
|
+
const flowSchema = (forms) => v.optional(v.pipe(v.string(), v.check((value) => {
|
|
241
|
+
const s = value.replaceAll("{id}", "id");
|
|
242
|
+
const i = s.indexOf(":");
|
|
243
|
+
const type = i < 0 ? s : s.slice(0, i);
|
|
244
|
+
const name = i < 0 ? "" : s.slice(i + 1);
|
|
245
|
+
if (!forms.includes(type))
|
|
246
|
+
return false;
|
|
247
|
+
else if (type === "branch")
|
|
248
|
+
return /^[A-Za-z0-9_][A-Za-z0-9_./-]*$/.test(name) && !/(?:\/|\.|\.lock)$/.test(name) && !/\.\.|\/\//.test(name);
|
|
249
|
+
else if (type === "attachment")
|
|
250
|
+
return /^[A-Za-z0-9_-]+$/.test(name);
|
|
251
|
+
else
|
|
252
|
+
return i < 0;
|
|
253
|
+
}, `expected ${forms.map((form) => form === "branch" || form === "attachment" ? `"${form}:<name>"` : `"${form}"`).join(", ")}`)));
|
|
254
|
+
/* schema for a positive number of human minutes of the effort estimation */
|
|
255
|
+
const minutesSchema = v.optional(v.pipe(v.union([v.number(), v.string()]), v.check((s) => /^\d+(?:\.\d+)?$/.test(String(s)) && Number(s) > 0, "expected a positive number of minutes")));
|
|
256
|
+
/* schema for a multiplier of the effort estimation */
|
|
257
|
+
const multiplierSchema = v.optional(v.pipe(v.union([v.number(), v.string()]), v.check((s) => /^\d+(?:\.\d+)?$/.test(String(s)) && Number(s) >= 1, "expected a multiplier of at least 1")));
|
|
258
|
+
/* schema for ".ase/config.yaml" */
|
|
259
|
+
export const configSchema = v.nullish(v.strictObject({
|
|
260
|
+
project: v.optional(v.strictObject({
|
|
261
|
+
id: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
262
|
+
name: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
263
|
+
boxing: v.optional(v.picklist(projectClassification.boxing)),
|
|
264
|
+
users: v.optional(v.string()),
|
|
265
|
+
task: v.optional(v.strictObject({
|
|
266
|
+
lifecycle: v.optional(v.picklist(projectClassification.lifecycle)),
|
|
267
|
+
store: v.optional(v.strictObject({
|
|
268
|
+
url: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
269
|
+
token: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
270
|
+
crt: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
271
|
+
key: v.optional(v.pipe(v.string(), v.minLength(1)))
|
|
272
|
+
})),
|
|
273
|
+
idscheme: v.optional(v.pipe(v.string(), v.check((s) => checkIdScheme(s) === "", "expected \"slug[:<words>]\", \"seq[:<template>]\", or \"any\""))),
|
|
274
|
+
default: v.optional(v.strictObject({
|
|
275
|
+
source: flowSchema(["worktree", "branch"]),
|
|
276
|
+
changeset: flowSchema(["worktree", "branch", "attachment"]),
|
|
277
|
+
target: flowSchema(["worktree", "branch", "source"])
|
|
278
|
+
})),
|
|
279
|
+
effort: v.optional(v.strictObject({
|
|
280
|
+
scale: v.optional(v.pipe(v.string(), v.check((s) => parsable(parseEffortScale, s), "expected \"<xs>,<s>,<m>,<l>\" (strictly ascending upper band bounds in minutes)"))),
|
|
281
|
+
factor: minutesSchema,
|
|
282
|
+
file: minutesSchema,
|
|
283
|
+
line: minutesSchema,
|
|
284
|
+
iteration: multiplierSchema
|
|
285
|
+
}))
|
|
286
|
+
})),
|
|
287
|
+
relay: v.optional(v.strictObject({
|
|
288
|
+
url: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
289
|
+
token: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
290
|
+
crt: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
291
|
+
key: v.optional(v.pipe(v.string(), v.minLength(1)))
|
|
292
|
+
})),
|
|
293
|
+
artifact: v.optional(v.strictObject({
|
|
294
|
+
spec: artifactSpecSchema,
|
|
295
|
+
code: artifactSchema,
|
|
296
|
+
docs: artifactSchema,
|
|
297
|
+
infr: artifactSchema
|
|
298
|
+
}))
|
|
299
|
+
})),
|
|
300
|
+
agent: v.optional(v.strictObject({
|
|
301
|
+
persona: v.optional(v.picklist(agentClassification.persona)),
|
|
302
|
+
guidance: v.optional(v.picklist(agentClassification.guidance))
|
|
303
|
+
})),
|
|
304
|
+
telemetry: v.optional(v.strictObject({
|
|
305
|
+
enabled: v.optional(v.union([v.boolean(), v.picklist(["true", "false"])])),
|
|
306
|
+
implicit: v.optional(v.union([v.boolean(), v.picklist(["true", "false"])])),
|
|
307
|
+
service: v.optional(v.strictObject({
|
|
308
|
+
port: v.optional(v.pipe(v.union([v.number(), v.string()]), v.check((s) => /^\d+$/.test(String(s)) && Number(s) >= 1024 && Number(s) <= 65535, "expected a port number (1024-65535)"))),
|
|
309
|
+
token: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
310
|
+
interval: v.optional(v.pipe(v.union([v.number(), v.string()]), v.check((s) => /^\d+$/.test(String(s)), "expected a number of minutes (0 disables)"))),
|
|
311
|
+
idle: v.optional(v.pipe(v.union([v.number(), v.string()]), v.check((s) => /^\d+$/.test(String(s)), "expected a number of minutes (0 disables)")))
|
|
312
|
+
})),
|
|
313
|
+
prices: v.optional(v.strictObject({
|
|
314
|
+
ai: v.optional(v.pipe(v.string(), v.check((s) => parsable(parsePrices, s), "expected \"<model>=<input>/<output>[/<cread>/<cwrite>][ ...]\" (USD per million tokens, with \"*\" as the model of all others)"))),
|
|
315
|
+
hr: v.optional(v.pipe(v.string(), v.check((s) => parsable(parseRates, s), "expected \"<user-id>=<usd-per-hour>[,...]\" (with \"*\" as the user id of all others)")))
|
|
316
|
+
}))
|
|
317
|
+
})),
|
|
318
|
+
board: v.optional(v.strictObject({
|
|
319
|
+
tui: v.optional(v.strictObject({
|
|
320
|
+
color: v.optional(v.strictObject({
|
|
321
|
+
dim: colorSchema,
|
|
322
|
+
normal: colorSchema,
|
|
323
|
+
accent: colorSchema,
|
|
324
|
+
signal: colorSchema
|
|
325
|
+
}))
|
|
326
|
+
})),
|
|
327
|
+
web: v.optional(v.strictObject({
|
|
328
|
+
color: v.optional(v.strictObject({
|
|
329
|
+
dim: webColorSchema,
|
|
330
|
+
normal: webColorSchema,
|
|
331
|
+
accent: webColorSchema,
|
|
332
|
+
signal: webColorSchema
|
|
333
|
+
})),
|
|
334
|
+
editor: v.optional(v.strictObject({
|
|
335
|
+
keymap: v.optional(v.picklist(["default", "vim", "emacs"]))
|
|
336
|
+
}))
|
|
337
|
+
}))
|
|
338
|
+
}))
|
|
339
|
+
}));
|
|
340
|
+
/* schema for the session-scoped internal "state.yaml", carrying the machine-local
|
|
341
|
+
runtime state shared between the hooks, the MCP server, and the skills, which is
|
|
342
|
+
intentionally not part of the user-facing configuration: "basedir" is the project
|
|
343
|
+
directory, "task" is the id of the active
|
|
344
|
+
task, "skill" is the id of the active edit-capable skill, and "headless"
|
|
345
|
+
whether the session is a headless one of an agent */
|
|
346
|
+
export const sessionStateSchema = v.nullish(v.strictObject({
|
|
347
|
+
basedir: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
348
|
+
task: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
349
|
+
skill: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
350
|
+
headless: v.optional(v.picklist(["true", "false"]))
|
|
351
|
+
}));
|
|
@@ -0,0 +1,172 @@
|
|
|
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 os from "node:os";
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import { execaSync } from "execa";
|
|
10
|
+
import { LRUCache } from "lru-cache";
|
|
11
|
+
/* canonical ordering rank of a scope kind */
|
|
12
|
+
const scopeRank = (kind) => ({ default: -1, user: 0, project: 1, task: 2, session: 3 })[kind];
|
|
13
|
+
/* parse a single scope term */
|
|
14
|
+
const parseScopeTerm = (value) => {
|
|
15
|
+
if (value === "user")
|
|
16
|
+
return { kind: "user" };
|
|
17
|
+
else if (value === "project")
|
|
18
|
+
return { kind: "project" };
|
|
19
|
+
const m = /^(session|task):([A-Za-z0-9._-]+)$/.exec(value);
|
|
20
|
+
if (m !== null) {
|
|
21
|
+
if (m[2] === "." || m[2] === "..")
|
|
22
|
+
throw new Error(`invalid --scope term "${value}": id must not be "." or ".."`);
|
|
23
|
+
return { kind: m[1], id: m[2] };
|
|
24
|
+
}
|
|
25
|
+
throw new Error(`invalid --scope term "${value}" ` +
|
|
26
|
+
"(expected: \"user\", \"project\", \"task:<id>\", or \"session:<id>\")");
|
|
27
|
+
};
|
|
28
|
+
/* determine the Git top-level directory, if inside a Git repository;
|
|
29
|
+
cached per working directory ("" ≡ not inside a Git working tree), as
|
|
30
|
+
each determination spawns a Git subprocess and the Git context can
|
|
31
|
+
change over the lifetime of the long-running ASE service */
|
|
32
|
+
const gitToplevelCache = new LRUCache({ max: 4, ttl: 2 * 1000 });
|
|
33
|
+
const gitToplevel = () => {
|
|
34
|
+
const cwd = process.cwd();
|
|
35
|
+
const cached = gitToplevelCache.get(cwd);
|
|
36
|
+
if (cached !== undefined)
|
|
37
|
+
return cached === "" ? null : cached;
|
|
38
|
+
let top = "";
|
|
39
|
+
try {
|
|
40
|
+
top = execaSync("git", ["rev-parse", "--show-toplevel"], { stderr: "ignore" }).stdout.trim();
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
/* not inside a Git working tree */
|
|
44
|
+
}
|
|
45
|
+
gitToplevelCache.set(cwd, top);
|
|
46
|
+
return top === "" ? null : top;
|
|
47
|
+
};
|
|
48
|
+
/* resolve the per-OS user-scope configuration directory */
|
|
49
|
+
export const userConfigDir = () => {
|
|
50
|
+
if (process.platform === "darwin")
|
|
51
|
+
/* macOS */
|
|
52
|
+
return path.join(os.homedir(), "Library", "Application Support", "ase");
|
|
53
|
+
else if (process.platform === "win32")
|
|
54
|
+
/* Windows (roaming) */
|
|
55
|
+
return path.join(process.env.APPDATA ?? os.homedir(), "ase");
|
|
56
|
+
else {
|
|
57
|
+
/* Linux */
|
|
58
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
59
|
+
const base = xdg !== undefined && xdg !== "" ? xdg : path.join(os.homedir(), ".config");
|
|
60
|
+
return path.join(base, "ase");
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
/* resolve the per-OS user-scope state directory (per-session state and
|
|
64
|
+
other machine-local runtime information, never roamed or versioned) */
|
|
65
|
+
export const userStateDir = () => {
|
|
66
|
+
if (process.platform === "darwin")
|
|
67
|
+
/* macOS */
|
|
68
|
+
return path.join(os.homedir(), "Library", "Application Support", "ase");
|
|
69
|
+
else if (process.platform === "win32")
|
|
70
|
+
/* Windows (local) */
|
|
71
|
+
return path.join(process.env.LOCALAPPDATA ?? process.env.APPDATA ?? os.homedir(), "ase");
|
|
72
|
+
else {
|
|
73
|
+
/* Linux */
|
|
74
|
+
const xdg = process.env.XDG_STATE_HOME;
|
|
75
|
+
const base = xdg !== undefined && xdg !== "" ? xdg : path.join(os.homedir(), ".local", "state");
|
|
76
|
+
return path.join(base, "ase");
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
/* ensure a self-ignoring ".ase/.gitignore" covering the machine-local runtime
|
|
80
|
+
files, so they never show up as untracked files in any project; an existing
|
|
81
|
+
file is left untouched, as the project may maintain its own rules */
|
|
82
|
+
export const ensureAseGitignore = (aseDir) => {
|
|
83
|
+
const file = path.join(aseDir, ".gitignore");
|
|
84
|
+
if (fs.existsSync(file))
|
|
85
|
+
return;
|
|
86
|
+
const rules = [".gitignore", "*.lock", "agent/", "board.yaml", "service.log", "service.yaml", "otel-capture/", "worktree/"];
|
|
87
|
+
try {
|
|
88
|
+
fs.mkdirSync(aseDir, { recursive: true });
|
|
89
|
+
fs.writeFileSync(file, `# ASE machine-local runtime files (generated)\n${rules.join("\n")}\n`, { encoding: "utf8", flag: "wx" });
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
/* intentionally ignore errors (e.g. concurrent creation or read-only
|
|
93
|
+
project), as a missing ignore file must never block the operation */
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
/* determine the project root directory, i.e. either the top-level
|
|
97
|
+
directory of the Git working tree or the nearest directory at or
|
|
98
|
+
above cwd which carries a ".ase" directory (excluding the home
|
|
99
|
+
directory, where a stale "~/.ase" of older ASE versions may exist) */
|
|
100
|
+
export const projectRoot = () => {
|
|
101
|
+
const top = gitToplevel();
|
|
102
|
+
if (top !== null)
|
|
103
|
+
return top;
|
|
104
|
+
let dir = fs.realpathSync(process.cwd());
|
|
105
|
+
const home = fs.realpathSync(os.homedir());
|
|
106
|
+
for (;;) {
|
|
107
|
+
if (dir !== home && fs.existsSync(path.join(dir, ".ase")))
|
|
108
|
+
return dir;
|
|
109
|
+
const parent = path.dirname(dir);
|
|
110
|
+
if (parent === dir)
|
|
111
|
+
return null;
|
|
112
|
+
dir = parent;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
/* derive the fallback project id from the sanitized basename of a
|
|
116
|
+
project root (shared by task store, hook, service, and statusline);
|
|
117
|
+
for a multi-worktree setup "<basedir>/<branch>/" with an "active" symlink
|
|
118
|
+
in "<basedir>/" (pointing to one of the worktrees) the id is
|
|
119
|
+
"<basedir>-<branch>", as the bare branch name (e.g. "master") is not unique */
|
|
120
|
+
export const projectIdOf = (root) => {
|
|
121
|
+
let name = path.basename(root);
|
|
122
|
+
const parent = path.dirname(root);
|
|
123
|
+
if (name !== "" && parent !== root) {
|
|
124
|
+
try {
|
|
125
|
+
if (fs.lstatSync(path.join(parent, "active")).isSymbolicLink())
|
|
126
|
+
name = `${path.basename(parent)}-${name}`;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
/* no "active" symlink, so no multi-worktree setup */
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return name.replace(/[^A-Za-z0-9_-]/g, "_") || "project";
|
|
133
|
+
};
|
|
134
|
+
/* detect whether a project context exists, i.e. either we are inside
|
|
135
|
+
a Git working tree or a ".ase" directory is present at or above cwd */
|
|
136
|
+
const hasProjectContext = () => projectRoot() !== null;
|
|
137
|
+
/* parse a raw "--scope" option value into a canonical Scope chain;
|
|
138
|
+
accepts a comma-separated list of terms in any order. The "user"
|
|
139
|
+
term is always implicitly added at the bottom of the chain; the
|
|
140
|
+
"project" term is implicitly added only when a project context
|
|
141
|
+
exists (Git repository or ".ase" directory at or above cwd) and it
|
|
142
|
+
stays weaker than the strongest explicitly requested term, and an
|
|
143
|
+
explicit "project" term requires that same context */
|
|
144
|
+
export const parseScope = (value) => {
|
|
145
|
+
const projectActive = hasProjectContext();
|
|
146
|
+
const input = (value === undefined || value === "") ?
|
|
147
|
+
(projectActive ? "project" : "user") :
|
|
148
|
+
value.trim();
|
|
149
|
+
if (input === "")
|
|
150
|
+
throw new Error("invalid --scope: value must not be empty");
|
|
151
|
+
const terms = input.split(",").map((s) => parseScopeTerm(s.trim()));
|
|
152
|
+
const seen = new Set();
|
|
153
|
+
for (const t of terms) {
|
|
154
|
+
if (seen.has(t.kind))
|
|
155
|
+
throw new Error(`invalid --scope: duplicate term of kind "${t.kind}"`);
|
|
156
|
+
seen.add(t.kind);
|
|
157
|
+
}
|
|
158
|
+
if (seen.has("project") && !projectActive)
|
|
159
|
+
throw new Error("invalid --scope: \"project\" requires a project context " +
|
|
160
|
+
"(a Git repository or a \".ase\" directory at or above the current directory)");
|
|
161
|
+
/* the strongest term of the chain is the write target, so an implicitly
|
|
162
|
+
added "project" term must never outrank the strongest explicitly
|
|
163
|
+
requested term, as this would silently retarget the caller's request */
|
|
164
|
+
const rankMax = Math.max(...terms.map((t) => scopeRank(t.kind)));
|
|
165
|
+
if (!seen.has("project") && projectActive && rankMax > scopeRank("project"))
|
|
166
|
+
terms.unshift({ kind: "project" });
|
|
167
|
+
if (!seen.has("user"))
|
|
168
|
+
terms.unshift({ kind: "user" });
|
|
169
|
+
terms.sort((a, b) => scopeRank(a.kind) - scopeRank(b.kind));
|
|
170
|
+
terms.unshift({ kind: "default" });
|
|
171
|
+
return terms;
|
|
172
|
+
};
|