@rse/ase 0.0.8 → 0.0.9
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 +334 -84
- package/dst/ase.1 +9 -3
- package/package.json +1 -1
package/dst/ase-config.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
5
|
*/
|
|
6
6
|
import path from "node:path";
|
|
7
|
+
import os from "node:os";
|
|
7
8
|
import fs from "node:fs";
|
|
8
9
|
import readline from "node:readline/promises";
|
|
9
10
|
import { Document, parseDocument, isMap, isScalar } from "yaml";
|
|
@@ -27,6 +28,16 @@ export const projectClassification = {
|
|
|
27
28
|
target: ["prototype", "mvp", "product"]
|
|
28
29
|
}
|
|
29
30
|
};
|
|
31
|
+
/* agent classification taxonomy */
|
|
32
|
+
export const agentClassification = {
|
|
33
|
+
persona: {
|
|
34
|
+
style: ["writer", "engineer", "telegrapher", "caveman"],
|
|
35
|
+
creativity: ["none", "lite", "full"]
|
|
36
|
+
},
|
|
37
|
+
process: {
|
|
38
|
+
autonomy: ["assistant", "hotl", "agent"]
|
|
39
|
+
}
|
|
40
|
+
};
|
|
30
41
|
/* classification presets */
|
|
31
42
|
export const projectClassificationPresets = {
|
|
32
43
|
vibe: {
|
|
@@ -39,7 +50,10 @@ export const projectClassificationPresets = {
|
|
|
39
50
|
"project.process.actors": "person",
|
|
40
51
|
"project.process.control": "agent",
|
|
41
52
|
"project.process.drive": "spec",
|
|
42
|
-
"project.result.target": "prototype"
|
|
53
|
+
"project.result.target": "prototype",
|
|
54
|
+
"agent.persona.style": "writer",
|
|
55
|
+
"agent.persona.creativity": "full",
|
|
56
|
+
"agent.process.autonomy": "agent",
|
|
43
57
|
},
|
|
44
58
|
pro: {
|
|
45
59
|
"project.id": "example",
|
|
@@ -51,7 +65,25 @@ export const projectClassificationPresets = {
|
|
|
51
65
|
"project.process.actors": "person",
|
|
52
66
|
"project.process.control": "human",
|
|
53
67
|
"project.process.drive": "code",
|
|
54
|
-
"project.result.target": "product"
|
|
68
|
+
"project.result.target": "product",
|
|
69
|
+
"agent.persona.style": "engineer",
|
|
70
|
+
"agent.persona.creativity": "none",
|
|
71
|
+
"agent.process.autonomy": "assistant",
|
|
72
|
+
},
|
|
73
|
+
default: {
|
|
74
|
+
"project.id": "example",
|
|
75
|
+
"project.name": "Example Project",
|
|
76
|
+
"project.source.ambition": "artist",
|
|
77
|
+
"project.source.boxing": "white",
|
|
78
|
+
"project.source.size": "medium",
|
|
79
|
+
"project.source.structure": "framework",
|
|
80
|
+
"project.process.actors": "person",
|
|
81
|
+
"project.process.control": "human",
|
|
82
|
+
"project.process.drive": "code",
|
|
83
|
+
"project.result.target": "product",
|
|
84
|
+
"agent.persona.style": "engineer",
|
|
85
|
+
"agent.persona.creativity": "none",
|
|
86
|
+
"agent.process.autonomy": "assistant",
|
|
55
87
|
},
|
|
56
88
|
industry: {
|
|
57
89
|
"project.id": "example",
|
|
@@ -63,8 +95,77 @@ export const projectClassificationPresets = {
|
|
|
63
95
|
"project.process.actors": "crew",
|
|
64
96
|
"project.process.control": "hitl",
|
|
65
97
|
"project.process.drive": "code",
|
|
66
|
-
"project.result.target": "mvp"
|
|
98
|
+
"project.result.target": "mvp",
|
|
99
|
+
"agent.persona.style": "engineer",
|
|
100
|
+
"agent.persona.creativity": "none",
|
|
101
|
+
"agent.process.autonomy": "hotl",
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
/* canonical ordering rank of a scope kind */
|
|
105
|
+
const scopeRank = (kind) => ({ default: -1, user: 0, project: 1, task: 2, session: 3 })[kind];
|
|
106
|
+
/* parse a single scope term */
|
|
107
|
+
const parseScopeTerm = (value) => {
|
|
108
|
+
if (value === "user")
|
|
109
|
+
return { kind: "user" };
|
|
110
|
+
else if (value === "project")
|
|
111
|
+
return { kind: "project" };
|
|
112
|
+
const m = /^(session|task):([A-Za-z0-9._-]+)$/.exec(value);
|
|
113
|
+
if (m !== null)
|
|
114
|
+
return { kind: m[1], id: m[2] };
|
|
115
|
+
throw new Error(`invalid --scope term "${value}" ` +
|
|
116
|
+
"(expected: \"user\", \"project\", \"task:<id>\", or \"session:<id>\")");
|
|
117
|
+
};
|
|
118
|
+
/* detect whether a project context exists, i.e. either we are inside
|
|
119
|
+
a Git working tree or a ".ase" directory is present at or above cwd */
|
|
120
|
+
const hasProjectContext = () => {
|
|
121
|
+
try {
|
|
122
|
+
const result = execaSync("git", ["rev-parse", "--show-toplevel"], { stderr: "ignore" });
|
|
123
|
+
if (result.stdout.trim() !== "")
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
/* not inside a Git working tree */
|
|
67
128
|
}
|
|
129
|
+
let dir = fs.realpathSync(process.cwd());
|
|
130
|
+
for (;;) {
|
|
131
|
+
if (fs.existsSync(path.join(dir, ".ase")))
|
|
132
|
+
return true;
|
|
133
|
+
const parent = path.dirname(dir);
|
|
134
|
+
if (parent === dir)
|
|
135
|
+
return false;
|
|
136
|
+
dir = parent;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
/* parse a raw "--scope" option value into a canonical Scope chain;
|
|
140
|
+
accepts a comma-separated list of terms in any order. The "user"
|
|
141
|
+
term is always implicitly added at the bottom of the chain; the
|
|
142
|
+
"project" term is implicitly added only when a project context
|
|
143
|
+
exists (Git repository or ".ase" directory at or above cwd), and
|
|
144
|
+
an explicit "project" term requires that same context */
|
|
145
|
+
const parseScope = (value) => {
|
|
146
|
+
const projectActive = hasProjectContext();
|
|
147
|
+
const input = (value === undefined || value === "") ?
|
|
148
|
+
(projectActive ? "project" : "user") :
|
|
149
|
+
value.trim();
|
|
150
|
+
if (input === "")
|
|
151
|
+
throw new Error("invalid --scope: value must not be empty");
|
|
152
|
+
const terms = input.split(",").map((s) => parseScopeTerm(s.trim()));
|
|
153
|
+
const seen = new Set();
|
|
154
|
+
for (const t of terms) {
|
|
155
|
+
if (seen.has(t.kind))
|
|
156
|
+
throw new Error(`invalid --scope: duplicate term of kind "${t.kind}"`);
|
|
157
|
+
seen.add(t.kind);
|
|
158
|
+
}
|
|
159
|
+
if (seen.has("project") && !projectActive)
|
|
160
|
+
throw new Error("invalid --scope: \"project\" requires a project context " +
|
|
161
|
+
"(a Git repository or a \".ase\" directory at or above the current directory)");
|
|
162
|
+
if (!seen.has("project") && projectActive)
|
|
163
|
+
terms.unshift({ kind: "project" });
|
|
164
|
+
if (!seen.has("user"))
|
|
165
|
+
terms.unshift({ kind: "user" });
|
|
166
|
+
terms.sort((a, b) => scopeRank(a.kind) - scopeRank(b.kind));
|
|
167
|
+
terms.unshift({ kind: "default" });
|
|
168
|
+
return terms;
|
|
68
169
|
};
|
|
69
170
|
/* schema for ".ase/config.yaml" */
|
|
70
171
|
export const configSchema = v.nullish(v.strictObject({
|
|
@@ -85,25 +186,87 @@ export const configSchema = v.nullish(v.strictObject({
|
|
|
85
186
|
result: v.optional(v.strictObject({
|
|
86
187
|
target: v.optional(v.picklist(projectClassification.result.target))
|
|
87
188
|
}))
|
|
189
|
+
})),
|
|
190
|
+
agent: v.optional(v.strictObject({
|
|
191
|
+
persona: v.optional(v.strictObject({
|
|
192
|
+
style: v.optional(v.picklist(agentClassification.persona.style)),
|
|
193
|
+
creativity: v.optional(v.picklist(agentClassification.persona.creativity))
|
|
194
|
+
})),
|
|
195
|
+
process: v.optional(v.strictObject({
|
|
196
|
+
autonomy: v.optional(v.picklist(agentClassification.process.autonomy))
|
|
197
|
+
}))
|
|
88
198
|
}))
|
|
89
199
|
}));
|
|
90
|
-
/* encapsulate read/write access to a
|
|
200
|
+
/* encapsulate read/write access to a stack of "<name>.yaml" configuration files,
|
|
201
|
+
each associated with a scope term; reads cascade along user < project < task < session,
|
|
202
|
+
writes are confined to the target (strongest) scope term */
|
|
91
203
|
export class Config {
|
|
204
|
+
/* public state */
|
|
92
205
|
filename;
|
|
93
|
-
|
|
206
|
+
/* private state */
|
|
207
|
+
name;
|
|
208
|
+
scope;
|
|
94
209
|
schema;
|
|
95
210
|
log;
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
this.
|
|
104
|
-
this.doc = new Document();
|
|
211
|
+
docs;
|
|
212
|
+
target;
|
|
213
|
+
/* creation */
|
|
214
|
+
constructor(name, schema, log, scope = [{ kind: "project" }]) {
|
|
215
|
+
if (scope.length === 0)
|
|
216
|
+
throw new Error("invalid scope: chain must not be empty");
|
|
217
|
+
this.name = name;
|
|
218
|
+
this.scope = scope[0].kind === "default" ? scope : [{ kind: "default" }, ...scope];
|
|
105
219
|
this.schema = schema ?? null;
|
|
106
220
|
this.log = log;
|
|
221
|
+
const tgt = this.scope[this.scope.length - 1];
|
|
222
|
+
this.filename = this.resolveFilename(name, tgt);
|
|
223
|
+
this.docs = [{ scope: tgt, filename: this.filename, doc: new Document() }];
|
|
224
|
+
this.target = 0;
|
|
225
|
+
}
|
|
226
|
+
/* render a scope term as a short textual label */
|
|
227
|
+
static scopeLabel(term) {
|
|
228
|
+
if (term.kind === "default" || term.kind === "user" || term.kind === "project")
|
|
229
|
+
return term.kind;
|
|
230
|
+
return `${term.kind}:${term.id}`;
|
|
231
|
+
}
|
|
232
|
+
/* resolve the per-OS user-scope base directory */
|
|
233
|
+
userConfigDir() {
|
|
234
|
+
if (process.platform === "darwin")
|
|
235
|
+
/* macOS */
|
|
236
|
+
return path.join(os.homedir(), "Library", "Application Support", "ase");
|
|
237
|
+
else if (process.platform === "win32")
|
|
238
|
+
/* Windows */
|
|
239
|
+
return path.join(process.env.APPDATA ?? os.homedir(), "ase");
|
|
240
|
+
else {
|
|
241
|
+
/* Linux */
|
|
242
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
243
|
+
const base = xdg !== undefined && xdg !== "" ? xdg : path.join(os.homedir(), ".config");
|
|
244
|
+
return path.join(base, "ase");
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/* resolve the configuration filename based on the selected scope term */
|
|
248
|
+
resolveFilename(name, term) {
|
|
249
|
+
if (term.kind === "default")
|
|
250
|
+
throw new Error("internal error: \"default\" scope has no filename");
|
|
251
|
+
if (term.kind === "user")
|
|
252
|
+
return path.join(this.userConfigDir(), `${name}.yaml`);
|
|
253
|
+
else if (term.kind === "project") {
|
|
254
|
+
const rel = path.join(".ase", `${name}.yaml`);
|
|
255
|
+
const cwd = process.cwd();
|
|
256
|
+
const top = this.gitToplevel();
|
|
257
|
+
const found = top !== null ?
|
|
258
|
+
this.findUpward(cwd, top, rel) :
|
|
259
|
+
(fs.existsSync(path.join(cwd, rel)) ? path.join(cwd, rel) : null);
|
|
260
|
+
return found ?? path.join(top ?? cwd, rel);
|
|
261
|
+
}
|
|
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`);
|
|
269
|
+
}
|
|
107
270
|
}
|
|
108
271
|
/* upward-walk on filesystem for a file path relative to a start directory,
|
|
109
272
|
bounded above (inclusive) by a stop directory */
|
|
@@ -135,31 +298,67 @@ export class Config {
|
|
|
135
298
|
return null;
|
|
136
299
|
}
|
|
137
300
|
}
|
|
138
|
-
/* read
|
|
301
|
+
/* read the full scope chain into memory; the requested mode applies
|
|
302
|
+
to the target scope only, inherited scopes are always lenient */
|
|
139
303
|
read(mode = "lenient") {
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
if (
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
304
|
+
const chain = this.scope;
|
|
305
|
+
const docs = [];
|
|
306
|
+
for (let i = 0; i < chain.length; i++) {
|
|
307
|
+
const sc = chain[i];
|
|
308
|
+
if (sc.kind === "default") {
|
|
309
|
+
const doc = new Document();
|
|
310
|
+
doc.contents = doc.createNode({});
|
|
311
|
+
const preset = projectClassificationPresets.default;
|
|
312
|
+
for (const [k, val] of Object.entries(preset)) {
|
|
313
|
+
const segments = k.split(".");
|
|
314
|
+
for (let j = 1; j < segments.length; j++) {
|
|
315
|
+
const prefix = segments.slice(0, j);
|
|
316
|
+
const node = doc.getIn(prefix, true);
|
|
317
|
+
if (node === undefined)
|
|
318
|
+
doc.setIn(prefix, doc.createNode({}));
|
|
319
|
+
}
|
|
320
|
+
doc.setIn(segments, doc.createNode(val));
|
|
321
|
+
}
|
|
322
|
+
docs.push({ scope: sc, filename: "", doc });
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
const filename = this.resolveFilename(this.name, sc);
|
|
326
|
+
const isTarget = (i === chain.length - 1);
|
|
327
|
+
const perDocMode = isTarget ? mode : "lenient";
|
|
328
|
+
const text = fs.existsSync(filename) ? fs.readFileSync(filename, "utf8") : "";
|
|
329
|
+
let doc = parseDocument(text);
|
|
330
|
+
if (doc.errors.length > 0) {
|
|
331
|
+
const msg = `invalid YAML in ${filename}: ${doc.errors[0].message}`;
|
|
332
|
+
if (perDocMode === "strict")
|
|
333
|
+
throw new Error(msg);
|
|
334
|
+
this.log.write("warning", msg);
|
|
335
|
+
doc = new Document();
|
|
336
|
+
}
|
|
337
|
+
docs.push({ scope: sc, filename, doc });
|
|
338
|
+
}
|
|
339
|
+
this.docs = docs;
|
|
340
|
+
this.target = docs.length - 1;
|
|
341
|
+
for (let i = 0; i < docs.length; i++) {
|
|
342
|
+
const isTarget = (i === this.target);
|
|
343
|
+
const perDocMode = isTarget ? mode : "lenient";
|
|
344
|
+
this.validateDoc(docs[i].doc, docs[i].filename, perDocMode);
|
|
148
345
|
}
|
|
149
|
-
this.validate(mode);
|
|
150
346
|
}
|
|
151
|
-
/* write in-memory configuration back to file */
|
|
347
|
+
/* write in-memory configuration back to the target scope's file */
|
|
152
348
|
write() {
|
|
153
|
-
this.
|
|
154
|
-
|
|
155
|
-
|
|
349
|
+
const td = this.docs[this.target];
|
|
350
|
+
if (td.scope.kind === "default")
|
|
351
|
+
throw new Error("internal error: \"default\" scope is not writable");
|
|
352
|
+
this.validateDoc(td.doc, td.filename, "strict");
|
|
353
|
+
fs.mkdirSync(path.dirname(td.filename), { recursive: true });
|
|
354
|
+
fs.writeFileSync(td.filename, td.doc.toString({ indent: 4 }), "utf8");
|
|
156
355
|
}
|
|
157
|
-
/* validate
|
|
158
|
-
|
|
356
|
+
/* validate a single YAML document against the optional schema */
|
|
357
|
+
validateDoc(doc, filename, mode = "strict") {
|
|
159
358
|
if (this.schema === null)
|
|
160
359
|
return;
|
|
161
360
|
for (;;) {
|
|
162
|
-
const result = v.safeParse(this.schema,
|
|
361
|
+
const result = v.safeParse(this.schema, doc.toJS());
|
|
163
362
|
if (result.success)
|
|
164
363
|
return;
|
|
165
364
|
if (mode === "strict") {
|
|
@@ -167,15 +366,15 @@ export class Config {
|
|
|
167
366
|
const dotPath = (i.path ?? []).map((p) => String(p.key)).join(".");
|
|
168
367
|
return dotPath ? `${dotPath}: ${i.message}` : i.message;
|
|
169
368
|
}).join("; ");
|
|
170
|
-
throw new Error(`invalid configuration in ${
|
|
369
|
+
throw new Error(`invalid configuration in ${filename}: ${issues}`);
|
|
171
370
|
}
|
|
172
371
|
let progressed = false;
|
|
173
372
|
for (const i of result.issues) {
|
|
174
373
|
const segs = (i.path ?? []).map((p) => String(p.key));
|
|
175
374
|
const dotPath = segs.join(".");
|
|
176
|
-
this.log.write("warning", `invalid entry in ${
|
|
375
|
+
this.log.write("warning", `invalid entry in ${filename}: ${dotPath ? `${dotPath}: ` : ""}${i.message}`);
|
|
177
376
|
if (segs.length > 0) {
|
|
178
|
-
|
|
377
|
+
doc.deleteIn(segs);
|
|
179
378
|
progressed = true;
|
|
180
379
|
}
|
|
181
380
|
else
|
|
@@ -228,16 +427,66 @@ export class Config {
|
|
|
228
427
|
throw new Error(`ambiguous key "${key}" matches: ${matches.map((m) => m.join(".")).join(", ")}`);
|
|
229
428
|
return matches[0].join(".");
|
|
230
429
|
}
|
|
231
|
-
/* retrieve
|
|
430
|
+
/* retrieve the effective value at a dotted key (strongest scope wins),
|
|
431
|
+
or the target scope's root contents if no key is given */
|
|
232
432
|
get(key) {
|
|
233
433
|
if (key === undefined)
|
|
234
|
-
return this.doc.contents;
|
|
235
|
-
|
|
434
|
+
return this.docs[this.target].doc.contents;
|
|
435
|
+
const segs = this.resolveKey(key).split(".");
|
|
436
|
+
for (let i = this.docs.length - 1; i >= 0; i--) {
|
|
437
|
+
const v = this.docs[i].doc.getIn(segs);
|
|
438
|
+
if (v !== undefined)
|
|
439
|
+
return v;
|
|
440
|
+
}
|
|
441
|
+
return undefined;
|
|
442
|
+
}
|
|
443
|
+
/* retrieve the effective value together with the scope it came from */
|
|
444
|
+
getWithOrigin(key) {
|
|
445
|
+
const segs = this.resolveKey(key).split(".");
|
|
446
|
+
for (let i = this.docs.length - 1; i >= 0; i--) {
|
|
447
|
+
const v = this.docs[i].doc.getIn(segs);
|
|
448
|
+
if (v !== undefined)
|
|
449
|
+
return { value: v, scope: this.docs[i].scope };
|
|
450
|
+
}
|
|
451
|
+
return undefined;
|
|
452
|
+
}
|
|
453
|
+
/* enumerate the effective leaf entries across the full scope chain;
|
|
454
|
+
each returned entry identifies the originating scope */
|
|
455
|
+
entries() {
|
|
456
|
+
const keys = new Set();
|
|
457
|
+
const walk = (node, prefix) => {
|
|
458
|
+
if (isMap(node))
|
|
459
|
+
for (const item of node.items) {
|
|
460
|
+
const k = [...prefix, String(item.key)];
|
|
461
|
+
if (isMap(item.value))
|
|
462
|
+
walk(item.value, k);
|
|
463
|
+
else if (isScalar(item.value))
|
|
464
|
+
keys.add(k.join("."));
|
|
465
|
+
else
|
|
466
|
+
throw new Error(`key "${k.join(".")}" has unsupported node type`);
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
for (const d of this.docs)
|
|
470
|
+
walk(d.doc.contents, []);
|
|
471
|
+
const result = [];
|
|
472
|
+
for (const k of keys) {
|
|
473
|
+
const segs = k.split(".");
|
|
474
|
+
for (let i = this.docs.length - 1; i >= 0; i--) {
|
|
475
|
+
const v = this.docs[i].doc.getIn(segs);
|
|
476
|
+
if (v !== undefined) {
|
|
477
|
+
result.push({ key: k, value: v, scope: this.docs[i].scope });
|
|
478
|
+
break;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
result.sort((a, b) => a.key.localeCompare(b.key));
|
|
483
|
+
return result;
|
|
236
484
|
}
|
|
237
|
-
/* set a value at a dotted key, creating intermediate maps as needed */
|
|
485
|
+
/* set a value at a dotted key in the target scope, creating intermediate maps as needed */
|
|
238
486
|
set(key, value) {
|
|
239
487
|
const segments = this.resolveKey(key).split(".");
|
|
240
|
-
const
|
|
488
|
+
const td = this.docs[this.target];
|
|
489
|
+
const next = td.doc.clone();
|
|
241
490
|
for (let i = 1; i < segments.length; i++) {
|
|
242
491
|
const prefix = segments.slice(0, i);
|
|
243
492
|
const node = next.getIn(prefix, true);
|
|
@@ -247,27 +496,28 @@ export class Config {
|
|
|
247
496
|
next.setIn(prefix, next.createNode({}));
|
|
248
497
|
}
|
|
249
498
|
next.setIn(segments, value);
|
|
250
|
-
const saved =
|
|
251
|
-
|
|
499
|
+
const saved = td.doc;
|
|
500
|
+
td.doc = next;
|
|
252
501
|
try {
|
|
253
|
-
this.
|
|
502
|
+
this.validateDoc(td.doc, td.filename, "strict");
|
|
254
503
|
}
|
|
255
504
|
catch (err) {
|
|
256
|
-
|
|
505
|
+
td.doc = saved;
|
|
257
506
|
throw err;
|
|
258
507
|
}
|
|
259
508
|
}
|
|
260
|
-
/* delete a value at a dotted key */
|
|
509
|
+
/* delete a value at a dotted key from the target scope */
|
|
261
510
|
delete(key) {
|
|
262
|
-
const
|
|
511
|
+
const td = this.docs[this.target];
|
|
512
|
+
const next = td.doc.clone();
|
|
263
513
|
next.deleteIn(this.resolveKey(key).split("."));
|
|
264
|
-
const saved =
|
|
265
|
-
|
|
514
|
+
const saved = td.doc;
|
|
515
|
+
td.doc = next;
|
|
266
516
|
try {
|
|
267
|
-
this.
|
|
517
|
+
this.validateDoc(td.doc, td.filename, "strict");
|
|
268
518
|
}
|
|
269
519
|
catch (err) {
|
|
270
|
-
|
|
520
|
+
td.doc = saved;
|
|
271
521
|
throw err;
|
|
272
522
|
}
|
|
273
523
|
}
|
|
@@ -283,7 +533,11 @@ export default class ConfigCommand {
|
|
|
283
533
|
/* register CLI top-level command "ase config" */
|
|
284
534
|
const configCmd = program
|
|
285
535
|
.command("config")
|
|
286
|
-
.
|
|
536
|
+
.option("--scope <scope>", "configuration scope chain: comma-separated list of \"user\", \"project\", " +
|
|
537
|
+
"\"task:<id>\", and/or \"session:<id>\" terms (e.g. \"task:N,session:M\"); " +
|
|
538
|
+
"\"user\" is always implicitly included and \"project\" is implicitly " +
|
|
539
|
+
"included whenever a project context (Git repo or upward \".ase\" directory) exists")
|
|
540
|
+
.description("manage ASE configuration")
|
|
287
541
|
.action((_opts, cmd) => {
|
|
288
542
|
cmd.outputHelp();
|
|
289
543
|
process.exit(1);
|
|
@@ -291,13 +545,14 @@ export default class ConfigCommand {
|
|
|
291
545
|
/* register CLI sub-command "ase config init" */
|
|
292
546
|
configCmd
|
|
293
547
|
.command("init")
|
|
294
|
-
.description("
|
|
295
|
-
.argument("<type>", "Preset type (vibe|pro|industry)")
|
|
296
|
-
.action((type) => {
|
|
548
|
+
.description("initialize configuration with preset values (default|vibe|pro|industry)")
|
|
549
|
+
.argument("<type>", "Preset type (default|vibe|pro|industry)")
|
|
550
|
+
.action((type, _opts, cmd) => {
|
|
551
|
+
const scope = parseScope(cmd.optsWithGlobals().scope);
|
|
297
552
|
const preset = projectClassificationPresets[type];
|
|
298
553
|
if (preset === undefined)
|
|
299
|
-
throw new Error(`unknown preset "${type}" (expected: vibe|pro|industry)`);
|
|
300
|
-
const cfg = new Config("config", configSchema, this.log);
|
|
554
|
+
throw new Error(`unknown preset "${type}" (expected: default|vibe|pro|industry)`);
|
|
555
|
+
const cfg = new Config("config", configSchema, this.log, scope);
|
|
301
556
|
cfg.read();
|
|
302
557
|
for (const [k, val] of Object.entries(preset))
|
|
303
558
|
cfg.set(k, val);
|
|
@@ -306,37 +561,30 @@ export default class ConfigCommand {
|
|
|
306
561
|
/* register CLI sub-command "ase config list" */
|
|
307
562
|
configCmd
|
|
308
563
|
.command("list")
|
|
309
|
-
.description("
|
|
310
|
-
.action(() => {
|
|
311
|
-
const
|
|
564
|
+
.description("list all configured values as flat dotted keys")
|
|
565
|
+
.action((_opts, cmd) => {
|
|
566
|
+
const scope = parseScope(cmd.optsWithGlobals().scope);
|
|
567
|
+
const cfg = new Config("config", configSchema, this.log, scope);
|
|
312
568
|
cfg.read();
|
|
313
569
|
const table = new Table({
|
|
314
|
-
head: ["KEY", "VALUE"],
|
|
570
|
+
head: ["KEY", "VALUE", "SCOPE"],
|
|
315
571
|
chars: { "mid": "", "left-mid": "", "mid-mid": "", "right-mid": "" },
|
|
316
572
|
style: { head: ["blue"] }
|
|
317
573
|
});
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
if (isMap(item.value))
|
|
323
|
-
list(item.value, k);
|
|
324
|
-
else if (!isScalar(item.value))
|
|
325
|
-
throw new Error(`key "${k}" has unsupported node type`);
|
|
326
|
-
else
|
|
327
|
-
table.push([k, String(item.value.value)]);
|
|
328
|
-
}
|
|
329
|
-
};
|
|
330
|
-
list(cfg.get(), "");
|
|
574
|
+
for (const e of cfg.entries()) {
|
|
575
|
+
const v = isScalar(e.value) ? e.value.value : e.value;
|
|
576
|
+
table.push([e.key, String(v), Config.scopeLabel(e.scope)]);
|
|
577
|
+
}
|
|
331
578
|
process.stdout.write(`${table.toString()}\n`);
|
|
332
579
|
});
|
|
333
580
|
/* register CLI sub-command "ase config edit" */
|
|
334
581
|
configCmd
|
|
335
582
|
.command("edit")
|
|
336
|
-
.description("
|
|
337
|
-
.action(async () => {
|
|
583
|
+
.description("edit configuration file with $EDITOR")
|
|
584
|
+
.action(async (_opts, cmd) => {
|
|
585
|
+
const scope = parseScope(cmd.optsWithGlobals().scope);
|
|
338
586
|
const editor = process.env.EDITOR ?? process.env.VISUAL ?? "vi";
|
|
339
|
-
const cfg = new Config("config", configSchema, this.log);
|
|
587
|
+
const cfg = new Config("config", configSchema, this.log, scope);
|
|
340
588
|
fs.mkdirSync(path.dirname(cfg.filename), { recursive: true });
|
|
341
589
|
if (!fs.existsSync(cfg.filename))
|
|
342
590
|
fs.writeFileSync(cfg.filename, "", "utf8");
|
|
@@ -364,10 +612,11 @@ export default class ConfigCommand {
|
|
|
364
612
|
/* register CLI sub-command "ase config get" */
|
|
365
613
|
configCmd
|
|
366
614
|
.command("get")
|
|
367
|
-
.description("
|
|
368
|
-
.argument("<key>", "
|
|
369
|
-
.action((key) => {
|
|
370
|
-
const
|
|
615
|
+
.description("print the value at a dotted configuration key")
|
|
616
|
+
.argument("<key>", "configuration key (dotted path)")
|
|
617
|
+
.action((key, _opts, cmd) => {
|
|
618
|
+
const scope = parseScope(cmd.optsWithGlobals().scope);
|
|
619
|
+
const cfg = new Config("config", configSchema, this.log, scope);
|
|
371
620
|
cfg.read();
|
|
372
621
|
const val = cfg.get(key);
|
|
373
622
|
if (val === undefined)
|
|
@@ -379,11 +628,12 @@ export default class ConfigCommand {
|
|
|
379
628
|
/* register CLI sub-command "ase config set" */
|
|
380
629
|
configCmd
|
|
381
630
|
.command("set")
|
|
382
|
-
.description("
|
|
383
|
-
.argument("<key>", "
|
|
384
|
-
.argument("<value>", "
|
|
385
|
-
.action((key, value) => {
|
|
386
|
-
const
|
|
631
|
+
.description("set the value at a dotted configuration key")
|
|
632
|
+
.argument("<key>", "configuration key (dotted path)")
|
|
633
|
+
.argument("<value>", "configuration value")
|
|
634
|
+
.action((key, value, _opts, cmd) => {
|
|
635
|
+
const scope = parseScope(cmd.optsWithGlobals().scope);
|
|
636
|
+
const cfg = new Config("config", configSchema, this.log, scope);
|
|
387
637
|
cfg.read();
|
|
388
638
|
cfg.set(key, value);
|
|
389
639
|
cfg.write();
|
package/dst/ase.1
CHANGED
|
@@ -26,13 +26,13 @@ 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.
|
|
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.
|
|
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
|
|
33
33
|
\fBase config edit\fR: Open \fB.ase/config.yaml\fR in the editor defined by the \fB$EDITOR\fR or \fB$VISUAL\fR environment variable (falling back to \fBvi\fR). The file and its parent directory are created if missing. After the editor exits, the file is re-read and schema warnings are reported.
|
|
34
34
|
.IP \(bu 4
|
|
35
|
-
\fBase config list\fR: List all configured values
|
|
35
|
+
\fBase config list\fR: List all effective configured values across the scope inheritance chain, rendered as a three-column table of \fBkey\fR, \fBvalue\fR, and \fBorigin\fR. The \fBorigin\fR column identifies the scope (\fBuser\fR, \fBproject\fR, \fBtask:\fR\fIid\fR, or \fBsession:\fR\fIid\fR) that supplied each value. For overlapping keys only the value from the strongest scope is shown.
|
|
36
36
|
.IP \(bu 4
|
|
37
37
|
\fBase config get\fR \fIkey\fR: Print the value at the given dotted \fIkey\fR. Fails with an error if \fIkey\fR does not resolve to a leaf value.
|
|
38
38
|
.IP \(bu 4
|
|
@@ -57,7 +57,13 @@ The following top-level commands exist for service management:
|
|
|
57
57
|
.SH "FILES"
|
|
58
58
|
.RS 0
|
|
59
59
|
.IP \(bu 4
|
|
60
|
-
\fB.ase/config.yaml\fR: Per-project \fIASE\fR configuration. 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).
|
|
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).
|
|
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.
|
|
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.
|
|
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.
|
|
61
67
|
.IP \(bu 4
|
|
62
68
|
\fB.ase/service.yaml\fR: Per-project service state. Recognized key: \fBport\fR (integer in \fB1024\fR..\fB65535\fR).
|
|
63
69
|
.IP \(bu 4
|
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
|
+
"version": "0.0.9",
|
|
10
10
|
"license": "GPL-3.0-only",
|
|
11
11
|
"author": {
|
|
12
12
|
"name": "Dr. Ralf S. Engelschall",
|