@rse/ase 0.0.9 → 0.0.10

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-config.js CHANGED
@@ -21,7 +21,6 @@ export const projectClassification = {
21
21
  },
22
22
  process: {
23
23
  actors: ["person", "team", "crew"],
24
- control: ["human", "hitl", "agent"],
25
24
  drive: ["spec", "code", "test"]
26
25
  },
27
26
  result: {
@@ -48,12 +47,11 @@ export const projectClassificationPresets = {
48
47
  "project.source.size": "small",
49
48
  "project.source.structure": "bare",
50
49
  "project.process.actors": "person",
51
- "project.process.control": "agent",
52
50
  "project.process.drive": "spec",
53
51
  "project.result.target": "prototype",
54
52
  "agent.persona.style": "writer",
55
53
  "agent.persona.creativity": "full",
56
- "agent.process.autonomy": "agent",
54
+ "agent.process.autonomy": "agent"
57
55
  },
58
56
  pro: {
59
57
  "project.id": "example",
@@ -63,12 +61,11 @@ export const projectClassificationPresets = {
63
61
  "project.source.size": "medium",
64
62
  "project.source.structure": "framework",
65
63
  "project.process.actors": "person",
66
- "project.process.control": "human",
67
64
  "project.process.drive": "code",
68
65
  "project.result.target": "product",
69
66
  "agent.persona.style": "engineer",
70
67
  "agent.persona.creativity": "none",
71
- "agent.process.autonomy": "assistant",
68
+ "agent.process.autonomy": "assistant"
72
69
  },
73
70
  default: {
74
71
  "project.id": "example",
@@ -78,12 +75,17 @@ export const projectClassificationPresets = {
78
75
  "project.source.size": "medium",
79
76
  "project.source.structure": "framework",
80
77
  "project.process.actors": "person",
81
- "project.process.control": "human",
82
78
  "project.process.drive": "code",
83
79
  "project.result.target": "product",
80
+ "project.artifact.build": "{etc/**,README.md,AGENTS.md,LICENSE.txt,package.json}",
81
+ "project.artifact.code": "src/**/*",
82
+ "project.artifact.docs": "doc/user/**/*.md",
83
+ "project.artifact.spec": "doc/spec/**/*.md",
84
+ "project.artifact.arch": "doc/arch/**/*.md",
84
85
  "agent.persona.style": "engineer",
85
86
  "agent.persona.creativity": "none",
86
87
  "agent.process.autonomy": "assistant",
88
+ "task.id": "default"
87
89
  },
88
90
  industry: {
89
91
  "project.id": "example",
@@ -93,14 +95,21 @@ export const projectClassificationPresets = {
93
95
  "project.source.size": "large",
94
96
  "project.source.structure": "framework",
95
97
  "project.process.actors": "crew",
96
- "project.process.control": "hitl",
97
98
  "project.process.drive": "code",
98
99
  "project.result.target": "mvp",
99
100
  "agent.persona.style": "engineer",
100
101
  "agent.persona.creativity": "none",
101
- "agent.process.autonomy": "hotl",
102
+ "agent.process.autonomy": "hotl"
102
103
  }
103
104
  };
105
+ /* hard-coded map: which scope kinds each variable may be SET on
106
+ (reads always cascade through the full chain, this restricts writes only);
107
+ keys absent from this map default to all non-"default" scope kinds */
108
+ export const configWritableScopes = {
109
+ "task.id": ["session"]
110
+ };
111
+ /* default set of scope kinds writable for any unrestricted key */
112
+ const configWritableScopesDefault = ["user", "project", "task", "session"];
104
113
  /* canonical ordering rank of a scope kind */
105
114
  const scopeRank = (kind) => ({ default: -1, user: 0, project: 1, task: 2, session: 3 })[kind];
106
115
  /* parse a single scope term */
@@ -180,11 +189,17 @@ export const configSchema = v.nullish(v.strictObject({
180
189
  })),
181
190
  process: v.optional(v.strictObject({
182
191
  actors: v.optional(v.picklist(projectClassification.process.actors)),
183
- control: v.optional(v.picklist(projectClassification.process.control)),
184
192
  drive: v.optional(v.picklist(projectClassification.process.drive))
185
193
  })),
186
194
  result: v.optional(v.strictObject({
187
195
  target: v.optional(v.picklist(projectClassification.result.target))
196
+ })),
197
+ artifact: v.optional(v.strictObject({
198
+ build: v.optional(v.pipe(v.string(), v.minLength(1))),
199
+ code: v.optional(v.pipe(v.string(), v.minLength(1))),
200
+ docs: v.optional(v.pipe(v.string(), v.minLength(1))),
201
+ spec: v.optional(v.pipe(v.string(), v.minLength(1))),
202
+ arch: v.optional(v.pipe(v.string(), v.minLength(1)))
188
203
  }))
189
204
  })),
190
205
  agent: v.optional(v.strictObject({
@@ -195,6 +210,9 @@ export const configSchema = v.nullish(v.strictObject({
195
210
  process: v.optional(v.strictObject({
196
211
  autonomy: v.optional(v.picklist(agentClassification.process.autonomy))
197
212
  }))
213
+ })),
214
+ task: v.optional(v.strictObject({
215
+ id: v.optional(v.pipe(v.string(), v.minLength(1)))
198
216
  }))
199
217
  }));
200
218
  /* encapsulate read/write access to a stack of "<name>.yaml" configuration files,
@@ -259,14 +277,12 @@ export class Config {
259
277
  (fs.existsSync(path.join(cwd, rel)) ? path.join(cwd, rel) : null);
260
278
  return found ?? path.join(top ?? cwd, rel);
261
279
  }
262
- else {
263
- const sub = term.kind === "session" ? "sessions" : "tasks";
264
- const top = this.gitToplevel();
265
- if (top !== null)
266
- return path.join(top, ".ase", sub, term.id, `${name}.yaml`);
267
- else
268
- return path.join(this.userConfigDir(), sub, term.id, `${name}.yaml`);
280
+ else if (term.kind === "task") {
281
+ const top = this.gitToplevel() ?? process.cwd();
282
+ return path.join(top, ".ase", "task", term.id, `${name}.yaml`);
269
283
  }
284
+ else
285
+ return path.join(os.homedir(), ".ase", "session", term.id, `${name}.yaml`);
270
286
  }
271
287
  /* upward-walk on filesystem for a file path relative to a start directory,
272
288
  bounded above (inclusive) by a stop directory */
@@ -482,8 +498,26 @@ export class Config {
482
498
  result.sort((a, b) => a.key.localeCompare(b.key));
483
499
  return result;
484
500
  }
501
+ /* determine whether a key is writable on a given scope kind */
502
+ isWritableOn(key, kind) {
503
+ if (kind === "default")
504
+ return false;
505
+ const resolved = this.resolveKey(key);
506
+ const allowed = configWritableScopes[resolved] ?? configWritableScopesDefault;
507
+ return allowed.includes(kind);
508
+ }
509
+ /* enforce write-scope policy for the current target scope */
510
+ assertWritable(key) {
511
+ const td = this.docs[this.target];
512
+ const resolved = this.resolveKey(key);
513
+ const allowed = configWritableScopes[resolved] ?? configWritableScopesDefault;
514
+ if (!allowed.includes(td.scope.kind))
515
+ throw new Error(`cannot set "${resolved}" on scope "${Config.scopeLabel(td.scope)}": ` +
516
+ `this key is only writable on scope(s): ${allowed.join(", ")}`);
517
+ }
485
518
  /* set a value at a dotted key in the target scope, creating intermediate maps as needed */
486
519
  set(key, value) {
520
+ this.assertWritable(key);
487
521
  const segments = this.resolveKey(key).split(".");
488
522
  const td = this.docs[this.target];
489
523
  const next = td.doc.clone();
@@ -508,6 +542,7 @@ export class Config {
508
542
  }
509
543
  /* delete a value at a dotted key from the target scope */
510
544
  delete(key) {
545
+ this.assertWritable(key);
511
546
  const td = this.docs[this.target];
512
547
  const next = td.doc.clone();
513
548
  next.deleteIn(this.resolveKey(key).split("."));
@@ -554,8 +589,12 @@ export default class ConfigCommand {
554
589
  throw new Error(`unknown preset "${type}" (expected: default|vibe|pro|industry)`);
555
590
  const cfg = new Config("config", configSchema, this.log, scope);
556
591
  cfg.read();
557
- for (const [k, val] of Object.entries(preset))
592
+ const targetKind = scope[scope.length - 1].kind;
593
+ for (const [k, val] of Object.entries(preset)) {
594
+ if (!cfg.isWritableOn(k, targetKind))
595
+ continue;
558
596
  cfg.set(k, val);
597
+ }
559
598
  cfg.write();
560
599
  });
561
600
  /* register CLI sub-command "ase config list" */
@@ -0,0 +1,83 @@
1
+ /*
2
+ ** Agentic Software Engineering (ASE)
3
+ ** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
+ ** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
5
+ */
6
+ import path from "node:path";
7
+ import fs from "node:fs";
8
+ import { execaSync } from "execa";
9
+ /* CLI command "ase hook" */
10
+ export default class HookCommand {
11
+ log;
12
+ constructor(log) {
13
+ this.log = log;
14
+ }
15
+ /* handler for "ase hook session-start" */
16
+ doSessionStart() {
17
+ /* determine plugin root */
18
+ const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT ?? "";
19
+ if (pluginRoot === "")
20
+ throw new Error("CLAUDE_PLUGIN_ROOT environment variable is not set");
21
+ /* determine path to external files */
22
+ const filePkg = path.join(pluginRoot, ".claude-plugin", "plugin.json");
23
+ const fileMd = path.join(pluginRoot, "meta", "ase-constitution.md");
24
+ /* read external files */
25
+ const pkg = fs.readFileSync(filePkg, "utf8");
26
+ let md = fs.readFileSync(fileMd, "utf8");
27
+ /* determine own version */
28
+ const version = JSON.parse(pkg).version ?? "";
29
+ /* read session information */
30
+ const stdin = fs.readFileSync(0, "utf8");
31
+ const input = stdin.trim() !== "" ? JSON.parse(stdin) : {};
32
+ /* determine session id */
33
+ const sessionId = input.session_id ?? "";
34
+ /* determine task id */
35
+ const taskId = process.env.ASE_TASK_ID ?? "default";
36
+ try {
37
+ execaSync("ase", ["config", `--scope=session:${sessionId}`, "set", "task.id", taskId], { stdio: ["ignore", "ignore", "ignore"] });
38
+ }
39
+ catch (_e) {
40
+ /* best-effort: ignore failures */
41
+ }
42
+ /* provide session and task id to Claude Code shell commands */
43
+ const envFile = process.env.CLAUDE_ENV_FILE ?? "";
44
+ if (envFile !== "") {
45
+ const script = `export ASE_VERSION="${version}"\n` +
46
+ `export ASE_SESSION_ID="${sessionId}"\n` +
47
+ `export ASE_TASK_ID="${taskId}"\n`;
48
+ fs.appendFileSync(envFile, script, "utf8");
49
+ }
50
+ /* prepend ASE information to constitution markdown */
51
+ md =
52
+ `<ase-version>${version}</ase-version>\n` +
53
+ `<ase-task-id>${taskId}</ase-task-id>\n` +
54
+ `<ase-session-id>${sessionId}</ase-session-id>\n` +
55
+ "\n" + md;
56
+ /* inject markdown into session context */
57
+ process.stdout.write(JSON.stringify({
58
+ "hookSpecificOutput": {
59
+ "hookEventName": "SessionStart",
60
+ "additionalContext": md
61
+ }
62
+ }));
63
+ return 0;
64
+ }
65
+ /* register commands */
66
+ register(program) {
67
+ /* register CLI top-level command "ase hook" */
68
+ const hookCmd = program
69
+ .command("hook")
70
+ .description("Claude Code hook entry points")
71
+ .action(() => {
72
+ hookCmd.outputHelp();
73
+ process.exit(1);
74
+ });
75
+ /* register CLI sub-command "ase hook session-start" */
76
+ hookCmd
77
+ .command("session-start")
78
+ .description("handle Claude Code SessionStart hook event")
79
+ .action(() => {
80
+ process.exit(this.doSessionStart());
81
+ });
82
+ }
83
+ }
package/dst/ase.1 CHANGED
@@ -26,7 +26,7 @@ The following top-level command-line options exist:
26
26
  The following top-level commands exist for configuration handling:
27
27
  .RS 0
28
28
  .IP \(bu 4
29
- \fBase config\fR: Manage \fIASE\fR configuration stored in \fB.ase/config.yaml\fR. Without a subcommand, prints usage information. The file is validated against a schema: on read, unknown or invalid entries are warned about and silently dropped from the in-memory view; on set/write, they cause a fatal error. All \fBase config\fR subcommands accept a \fB--scope\fR \fIscope\fR option that selects the scope chain. The \fIscope\fR value is a comma-separated list of scope terms, in any order; each term is one of \fBuser\fR, \fBproject\fR, \fBtask:\fR\fIid\fR, or \fBsession:\fR\fIid\fR (where \fIid\fR matches \fB\[lB]A-Za-z0-9._-\[rB]+\fR). At most one term per kind is allowed. The chain is canonicalized into the fixed inheritance order \fBuser\fR < \fBproject\fR < \fBtask\fR < \fBsession\fR. \fBuser\fR is always implicitly added at the bottom of the chain. \fBproject\fR is implicitly added only when a \fIproject context\fR exists -- i.e. when the current working directory is inside a Git repository, or a \fB.ase\fR directory is found at or above it. Specifying \fBproject\fR explicitly without a project context is an error. Without an explicit \fB--scope\fR, the target defaults to \fBproject\fR when a project context exists, otherwise to \fBuser\fR. Reads cascade from the strongest (rightmost) scope down to the weakest and return the first value that is defined. Writes (\fBset\fR, \fBdelete\fR, \fBedit\fR, \fBinit\fR) are always confined to the strongest (target) scope's own file -- intermediate and weaker scopes are never modified. See \fIFILES\fR below for the resulting paths. Example: \fB--scope task:T1,session:S1\fR yields the chain \fBuser\fR -> \fBproject\fR -> \fBtask:T1\fR -> \fBsession:S1\fR, with \fBsession:S1\fR as the write target.
29
+ \fBase config\fR: Manage \fIASE\fR configuration stored in \fB.ase/config.yaml\fR. Without a subcommand, prints usage information. The file is validated against a schema: on read, unknown or invalid entries are warned about and silently dropped from the in-memory view; on set/write, they cause a fatal error. Recognized keys are grouped under three top-level sections: \fBproject.*\fR (project identity, classification, and artifact globs), \fBagent.*\fR (agent persona and process), and \fBtask.*\fR (currently \fBtask.id\fR, the active task identifier). All \fBase config\fR subcommands accept a \fB--scope\fR \fIscope\fR option that selects the scope chain. The \fIscope\fR value is a comma-separated list of scope terms, in any order; each term is one of \fBuser\fR, \fBproject\fR, \fBtask:\fR\fIid\fR, or \fBsession:\fR\fIid\fR (where \fIid\fR matches \fB\[lB]A-Za-z0-9._-\[rB]+\fR). At most one term per kind is allowed. The chain is canonicalized into the fixed inheritance order \fBuser\fR < \fBproject\fR < \fBtask\fR < \fBsession\fR. \fBuser\fR is always implicitly added at the bottom of the chain. \fBproject\fR is implicitly added only when a \fIproject context\fR exists -- i.e. when the current working directory is inside a Git repository, or a \fB.ase\fR directory is found at or above it. Specifying \fBproject\fR explicitly without a project context is an error. Without an explicit \fB--scope\fR, the target defaults to \fBproject\fR when a project context exists, otherwise to \fBuser\fR. Reads cascade from the strongest (rightmost) scope down to the weakest and return the first value that is defined. Writes (\fBset\fR, \fBdelete\fR, \fBedit\fR, \fBinit\fR) are always confined to the strongest (target) scope's own file -- intermediate and weaker scopes are never modified. See \fIFILES\fR below for the resulting paths. Example: \fB--scope task:T1,session:S1\fR yields the chain \fBuser\fR -> \fBproject\fR -> \fBtask:T1\fR -> \fBsession:S1\fR, with \fBsession:S1\fR as the write target.
30
30
  .IP \(bu 4
31
31
  \fBase config init\fR \fItype\fR: Initialize \fB.ase/config.yaml\fR with preset values for all recognized keys. The \fItype\fR argument selects the preset: \fBvibe\fR (solo rookie: small black-box prototype, bare code, fully agent-driven, spec-driven, engineer ambition), \fBpro\fR (solo expert: medium white-box product, framework-based, human-controlled, code-driven, artist ambition), or \fBindustry\fR (team crew: large grey-box MVP, framework-based, human-in-the-loop, code-driven, craftsman ambition).
32
32
  .IP \(bu 4
@@ -54,18 +54,22 @@ The following top-level commands exist for service management:
54
54
  \fBase service stop\fR: Stop the background service via HTTP \fBGET /stop\fR. Exits silently with status 0 on successful stop. If no port is configured or the port is not responding, prints an informational message and exits with status 0.
55
55
  .RE 0
56
56
 
57
- .SH "FILES"
57
+ .SH "CONFIGURATION FILES"
58
58
  .RS 0
59
59
  .IP \(bu 4
60
- \fB.ase/config.yaml\fR: Per-project \fIASE\fR configuration (scope \fBproject\fR). Read upward from the current working directory. Recognized keys: \fBproject.id\fR (non-empty string, uniqued project id), \fBproject.name\fR (non-empty string, descriptive project name), \fBproject.source.ambition\fR (\fBartist\fR|\fBcraftsman\fR|\fBengineer\fR), \fBproject.source.boxing\fR (\fBwhite\fR|\fBgrey\fR|\fBblack\fR), \fBproject.source.size\fR (\fBsmall\fR|\fBmedium\fR|\fBlarge\fR), \fBproject.source.structure\fR (\fBbare\fR|\fBlibrary\fR|\fBframework\fR), \fBproject.process.actors\fR (\fBperson\fR|\fBteam\fR|\fBcrew\fR), \fBproject.process.control\fR (\fBhuman\fR|\fBhitl\fR|\fBagent\fR), \fBproject.process.drive\fR (\fBspec\fR|\fBcode\fR|\fBtest\fR), and \fBproject.result.target\fR (\fBprototype\fR|\fBmvp\fR|\fBproduct\fR). Agent classification keys: \fBagent.persona.style\fR (\fBwriter\fR|\fBengineer\fR|\fBtelegrapher\fR|\fBcaveman\fR), \fBagent.persona.creativity\fR (\fBnone\fR|\fBlite\fR|\fBfull\fR), and \fBagent.process.autonomy\fR (\fBassistant\fR|\fBhotl\fR|\fBagent\fR).
60
+ \fBuser\fR: \fIper-user configuration directory\fR\fB/config.yaml\fR: Per-user \fIASE\fR configuration (scope \fBuser\fR). The per-user configuration directory is \fB~/Library/Application Support/ase\fR on macOS, \fB%APPDATA%\[rs]ase\fR on Windows, and \fB$XDG_CONFIG_HOME/ase\fR (falling back to \fB~/.config/ase\fR) on Linux and other Unix systems.
61
61
  .IP \(bu 4
62
- \fB.ase/sessions/\fR\fIid\fR\fB/config.yaml\fR: Per-session \fIASE\fR configuration (scope \fBsession:\fR\fIid\fR), located relative to the Git top-level directory. Outside a Git repository, the file is placed under the per-user configuration directory at \fBsessions/\fR\fIid\fR\fB/config.yaml\fR.
62
+ \fBproject\fR: \fB.ase/config.yaml\fR: Per-project \fIASE\fR configuration (scope \fBproject\fR). Read upward from the current working directory.
63
63
  .IP \(bu 4
64
- \fB.ase/tasks/\fR\fIid\fR\fB/config.yaml\fR: Per-task \fIASE\fR configuration (scope \fBtask:\fR\fIid\fR), located relative to the Git top-level directory. Outside a Git repository, the file is placed under the per-user configuration directory at \fBtasks/\fR\fIid\fR\fB/config.yaml\fR.
64
+ \fBtask\fR: \fB.ase/task/\fR\fIid\fR\fB/config.yaml\fR: Per-task \fIASE\fR configuration (scope \fBtask:\fR\fIid\fR), located relative to the Git top-level directory. Outside a Git repository, the file is placed relative to the current working directory.
65
65
  .IP \(bu 4
66
- \fIper-user configuration directory\fR\fB/config.yaml\fR: Per-user \fIASE\fR configuration (scope \fBuser\fR). The per-user configuration directory is \fB~/Library/Application Support/ase\fR on macOS, \fB%APPDATA%\[rs]ase\fR on Windows, and \fB$XDG_CONFIG_HOME/ase\fR (falling back to \fB~/.config/ase\fR) on Linux and other Unix systems.
66
+ \fBsession\fR: \fB~/.ase/session/\fR\fIid\fR\fB/config.yaml\fR: Per-session \fIASE\fR configuration (scope \fBsession:\fR\fIid\fR), located under the user's home directory (independent of any project context).
67
+ .RE 0
68
+
69
+ .SH "STATE FILES"
70
+ .RS 0
67
71
  .IP \(bu 4
68
- \fB.ase/service.yaml\fR: Per-project service state. Recognized key: \fBport\fR (integer in \fB1024\fR..\fB65535\fR).
72
+ \fB.ase/service.yaml\fR: Per-project service state.
69
73
  .IP \(bu 4
70
74
  \fB.ase/service.log\fR: Stdout/stderr log of the detached background service.
71
75
  .RE 0
package/dst/ase.js CHANGED
@@ -8,6 +8,7 @@ import { Command, CommanderError, Option } from "commander";
8
8
  import Log from "./ase-log.js";
9
9
  import ConfigCommand from "./ase-config.js";
10
10
  import ServiceCommand from "./ase-service.js";
11
+ import HookCommand from "./ase-hook.js";
11
12
  import pkg from "../package.json" with { type: "json" };
12
13
  /* globally initialize logger */
13
14
  const log = new Log("ase", "warning", "-");
@@ -38,6 +39,7 @@ const main = async () => {
38
39
  /* register top-level commands */
39
40
  new ConfigCommand(log).register(program);
40
41
  new ServiceCommand(log).register(program);
42
+ new HookCommand(log).register(program);
41
43
  /* parse program arguments */
42
44
  await program.parseAsync(process.argv);
43
45
  /* gracefully terminate */
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "homepage": "http://github.com/rse/ase",
7
7
  "repository": { "url": "git+https://github.com/rse/ase.git", "type": "git" },
8
8
  "bugs": { "url": "http://github.com/rse/ase/issues" },
9
- "version": "0.0.9",
9
+ "version": "0.0.10",
10
10
  "license": "GPL-3.0-only",
11
11
  "author": {
12
12
  "name": "Dr. Ralf S. Engelschall",