@multileaf/ai-workflow 0.3.1 → 0.4.1

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/CHANGELOG.md CHANGED
@@ -4,6 +4,19 @@ All notable changes to this project are documented in this file. The format foll
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.4.1] - 2026-10-08
8
+
9
+ ### Fixed
10
+
11
+ - Approving an SDD stage that never passed its quality gate now reports an actionable `Run aiw gate <stage> before approving` error instead of a misleading stale-approval message.
12
+ - `aiw semantic status` and `aiw decision status` now point to the `.aiw/settings.yml` sections instead of the legacy standalone files.
13
+
14
+ ## [0.4.0] - 2026-10-08
15
+
16
+ ### Changed
17
+
18
+ - Unified user-local settings into a single `.aiw/settings.yml` with `semantic`, `decision`, `telemetry`, and `overrides` sections. The file is local user state, not committed project state. Interactive onboarding and the `telemetry`/`confirm` commands write to it, and legacy `.aiw/semantic.yml`, `.aiw/decision.yml`, `.aiw/telemetry.yml`, and `.aiw/overrides.yml` files are still read for backward compatibility. `uninstall` removes `settings.yml` and all four legacy files.
19
+
7
20
  ## [0.3.1] - 2026-10-08
8
21
 
9
22
  ### Added
package/dist/cli.js CHANGED
@@ -17,7 +17,7 @@ async function runWithTelemetry(args, root, services) {
17
17
  const result = await runCommand(args, root, nodeFileSystem, dependencies);
18
18
  if (args[0] === "ui" || (args[0] === "telemetry" && result.exitCode !== 0))
19
19
  return result;
20
- await recordConfiguredTelemetry(nodeFileSystem, join(root, ".aiw/telemetry.yml"), services.telemetry, args[0], result);
20
+ await recordConfiguredTelemetry(nodeFileSystem, join(root, ".aiw"), services.telemetry, args[0], result);
21
21
  return result;
22
22
  }
23
23
  if (process.argv[1]?.endsWith("cli.js") || process.argv[1]?.endsWith("/aiw")) {
@@ -42,8 +42,9 @@ export type DecisionProvider = {
42
42
  };
43
43
  export declare const DECISION_CONFIG_PATH = "decision.yml";
44
44
  export declare const DEFAULT_DECISION_CONFIG: DecisionConfig;
45
- export declare function parseDecisionConfig(text: string): DecisionConfig;
45
+ export declare function parseDecisionMapping(raw: unknown): DecisionConfig;
46
46
  export declare function serializeDecisionConfig(config: DecisionConfig): string;
47
+ export declare function parseDecisionConfig(text: string): DecisionConfig;
47
48
  export declare function loadDecisionConfig(fs: FileSystem, aiwPath: string): DecisionConfig | undefined;
48
49
  export declare function parseDecisionResponse(text: string): DecisionResponse;
49
50
  export declare function createOllamaDecisionProvider(config: DecisionConfig): DecisionProvider;
package/dist/decision.js CHANGED
@@ -1,19 +1,18 @@
1
1
  import { parseDocument } from "yaml";
2
+ import { loadSettings } from "./settings.js";
2
3
  export const DECISION_CONFIG_PATH = "decision.yml";
3
4
  export const DEFAULT_DECISION_CONFIG = {
4
5
  enabled: false,
5
6
  model: "clef-flash:latest",
6
7
  baseUrl: "http://localhost:11434",
7
8
  };
8
- export function parseDecisionConfig(text) {
9
- const document = parseDocument(text);
10
- const node = document.get("decision");
11
- if (document.errors.length > 0 || !node || typeof node !== "object")
12
- throw new Error("Decision configuration must be a YAML object with a `decision` mapping");
13
- const raw = node.toJSON();
14
- const enabled = raw.enabled;
15
- const model = raw.model;
16
- const baseUrl = raw.baseUrl;
9
+ export function parseDecisionMapping(raw) {
10
+ if (!raw || typeof raw !== "object")
11
+ throw new Error("decision must be a YAML mapping");
12
+ const record = raw;
13
+ const enabled = record.enabled;
14
+ const model = record.model;
15
+ const baseUrl = record.baseUrl;
17
16
  if (typeof enabled !== "boolean")
18
17
  throw new Error("decision.enabled must be a boolean");
19
18
  if (!enabled)
@@ -27,7 +26,17 @@ export function parseDecisionConfig(text) {
27
26
  export function serializeDecisionConfig(config) {
28
27
  return `decision:\n enabled: ${config.enabled}\n model: ${config.model}\n baseUrl: ${config.baseUrl}\n`;
29
28
  }
29
+ export function parseDecisionConfig(text) {
30
+ const document = parseDocument(text);
31
+ const node = document.get("decision");
32
+ if (document.errors.length > 0 || !node || typeof node !== "object")
33
+ throw new Error("Decision configuration must be a YAML object with a `decision` mapping");
34
+ return parseDecisionMapping(node.toJSON());
35
+ }
30
36
  export function loadDecisionConfig(fs, aiwPath) {
37
+ const settings = loadSettings(fs, aiwPath);
38
+ if (settings !== undefined)
39
+ return "decision" in settings ? parseDecisionMapping(settings.decision) : undefined;
31
40
  const configPath = `${aiwPath}/${DECISION_CONFIG_PATH}`;
32
41
  if (!fs.exists(configPath))
33
42
  return undefined;
@@ -28,9 +28,10 @@ export type EmbeddingProvider = {
28
28
  export declare const SEMANTIC_CONFIG_PATH = "semantic.yml";
29
29
  export declare const SEMANTIC_INDEX_PATH = "semantic/index.json";
30
30
  export declare const DEFAULT_SEMANTIC_CONFIG: SemanticConfig;
31
- export declare function parseSemanticConfig(text: string): SemanticConfig;
31
+ export declare function parseSemanticMapping(raw: unknown): SemanticConfig;
32
32
  export declare function serializeSemanticConfig(config: SemanticConfig): string;
33
33
  export declare function loadSemanticConfig(fs: FileSystem, aiwPath: string): SemanticConfig | undefined;
34
+ export declare function parseSemanticConfig(text: string): SemanticConfig;
34
35
  export declare function createOllamaEmbeddingProvider(config: SemanticConfig): EmbeddingProvider;
35
36
  export declare function chunkText(content: string, chunkChars: number): string[];
36
37
  export declare function cosineSimilarity(a: number[], b: number[]): number;
package/dist/semantic.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { parseDocument } from "yaml";
2
+ import { loadSettings } from "./settings.js";
2
3
  export const SEMANTIC_CONFIG_PATH = "semantic.yml";
3
4
  export const SEMANTIC_INDEX_PATH = "semantic/index.json";
4
5
  export const DEFAULT_SEMANTIC_CONFIG = {
@@ -8,17 +9,15 @@ export const DEFAULT_SEMANTIC_CONFIG = {
8
9
  chunkChars: 1200,
9
10
  topK: 8,
10
11
  };
11
- export function parseSemanticConfig(text) {
12
- const document = parseDocument(text);
13
- const node = document.get("semantic");
14
- if (document.errors.length > 0 || !node || typeof node !== "object")
15
- throw new Error("Semantic configuration must be a YAML object with a `semantic` mapping");
16
- const raw = node.toJSON();
17
- const enabled = raw.enabled;
18
- const model = raw.model;
19
- const baseUrl = raw.baseUrl;
20
- const chunkChars = raw.chunkChars;
21
- const topK = raw.topK;
12
+ export function parseSemanticMapping(raw) {
13
+ if (!raw || typeof raw !== "object")
14
+ throw new Error("semantic must be a YAML mapping");
15
+ const raw2 = raw;
16
+ const enabled = raw2.enabled;
17
+ const model = raw2.model;
18
+ const baseUrl = raw2.baseUrl;
19
+ const chunkChars = raw2.chunkChars;
20
+ const topK = raw2.topK;
22
21
  if (typeof enabled !== "boolean")
23
22
  throw new Error("semantic.enabled must be a boolean");
24
23
  if (!enabled)
@@ -39,11 +38,24 @@ export function serializeSemanticConfig(config) {
39
38
  return `semantic:\n enabled: ${config.enabled}\n model: ${config.model}\n baseUrl: ${config.baseUrl}\n chunkChars: ${config.chunkChars}\n topK: ${config.topK}\n`;
40
39
  }
41
40
  export function loadSemanticConfig(fs, aiwPath) {
41
+ const settings = loadSettings(fs, aiwPath);
42
+ if (settings !== undefined)
43
+ return "semantic" in settings ? parseSemanticMapping(settings.semantic) : undefined;
42
44
  const configPath = `${aiwPath}/${SEMANTIC_CONFIG_PATH}`;
43
45
  if (!fs.exists(configPath))
44
46
  return undefined;
45
47
  return parseSemanticConfig(fs.read(configPath));
46
48
  }
49
+ function parseSemanticDocument(text) {
50
+ const document = parseDocument(text);
51
+ const node = document.get("semantic");
52
+ if (document.errors.length > 0 || !node || typeof node !== "object")
53
+ throw new Error("Semantic configuration must be a YAML object with a `semantic` mapping");
54
+ return parseSemanticMapping(node.toJSON());
55
+ }
56
+ export function parseSemanticConfig(text) {
57
+ return parseSemanticDocument(text);
58
+ }
47
59
  export function createOllamaEmbeddingProvider(config) {
48
60
  const fetchImpl = (url, init) => fetch(url, init);
49
61
  return {
@@ -0,0 +1,11 @@
1
+ import type { FileSystem } from "./types.js";
2
+ import type { FactOverride } from "./confirmation.js";
3
+ export declare const SETTINGS_PATH = "settings.yml";
4
+ export declare const LEGACY_SETTINGS_PATHS: readonly ["semantic.yml", "decision.yml", "telemetry.yml", "overrides.yml"];
5
+ export type SettingsDocument = Record<string, unknown>;
6
+ export declare function loadSettings(fs: FileSystem, aiwPath: string): SettingsDocument | undefined;
7
+ export declare function saveSettingsSection(fs: FileSystem, aiwPath: string, section: string, value: unknown): void;
8
+ export declare function serializeSettingOverrides(overrides: FactOverride[]): unknown;
9
+ export declare function readFactOverrides(fs: FileSystem, aiwPath: string): FactOverride[];
10
+ export declare function saveSettingOverrides(fs: FileSystem, aiwPath: string, overrides: FactOverride[]): void;
11
+ export declare function parseSettingOverrides(raw: unknown): FactOverride[];
@@ -0,0 +1,70 @@
1
+ import { parseDocument, stringify } from "yaml";
2
+ import { parseOverrides } from "./confirmation.js";
3
+ export const SETTINGS_PATH = "settings.yml";
4
+ export const LEGACY_SETTINGS_PATHS = [
5
+ "semantic.yml",
6
+ "decision.yml",
7
+ "telemetry.yml",
8
+ "overrides.yml",
9
+ ];
10
+ export function loadSettings(fs, aiwPath) {
11
+ const path = `${aiwPath}/${SETTINGS_PATH}`;
12
+ if (!fs.exists(path))
13
+ return undefined;
14
+ const document = parseDocument(fs.read(path));
15
+ if (document.errors.length > 0)
16
+ throw new Error(`Settings file ${path} is not valid YAML`);
17
+ const root = document.toJSON();
18
+ if (root === null)
19
+ return {};
20
+ if (typeof root !== "object" || Array.isArray(root))
21
+ throw new Error(`Settings file ${path} must contain a YAML mapping`);
22
+ return root;
23
+ }
24
+ export function saveSettingsSection(fs, aiwPath, section, value) {
25
+ const path = `${aiwPath}/${SETTINGS_PATH}`;
26
+ const existing = loadSettings(fs, aiwPath) ?? {};
27
+ existing[section] = value;
28
+ const document = parseDocument(stringify(existing));
29
+ document.set("schema", 1);
30
+ fs.write(path, document.toString());
31
+ }
32
+ export function serializeSettingOverrides(overrides) {
33
+ return overrides.map((item) => ({
34
+ key: item.key,
35
+ action: item.action,
36
+ ...(item.value ? { value: item.value } : {}),
37
+ }));
38
+ }
39
+ export function readFactOverrides(fs, aiwPath) {
40
+ const settings = loadSettings(fs, aiwPath);
41
+ if (settings !== undefined)
42
+ return "overrides" in settings ? parseSettingOverrides(settings.overrides) : [];
43
+ const legacyPath = `${aiwPath}/overrides.yml`;
44
+ if (!fs.exists(legacyPath))
45
+ return [];
46
+ return parseOverrides(fs.read(legacyPath));
47
+ }
48
+ export function saveSettingOverrides(fs, aiwPath, overrides) {
49
+ saveSettingsSection(fs, aiwPath, "overrides", serializeSettingOverrides(overrides));
50
+ }
51
+ export function parseSettingOverrides(raw) {
52
+ if (!Array.isArray(raw))
53
+ throw new Error("Settings overrides section must be a list");
54
+ return raw.map((item) => {
55
+ if (!item || typeof item !== "object")
56
+ throw new Error("Invalid settings override entry");
57
+ const entry = item;
58
+ if (typeof entry.key !== "string" || entry.key.trim() === "")
59
+ throw new Error("Settings override key must be a non-empty string");
60
+ if (entry.action !== "accept" && entry.action !== "reject" && entry.action !== "edit")
61
+ throw new Error(`Invalid settings override action for key ${entry.key}`);
62
+ if (entry.action === "edit" && (typeof entry.value !== "string" || entry.value === ""))
63
+ throw new Error(`Settings override ${entry.key} requires a value for an edit action`);
64
+ return {
65
+ key: entry.key,
66
+ action: entry.action,
67
+ ...(typeof entry.value === "string" && entry.value !== "" ? { value: entry.value } : {}),
68
+ };
69
+ });
70
+ }
@@ -14,7 +14,10 @@ export interface TelemetryClient {
14
14
  record(event: TelemetryEvent): Promise<void>;
15
15
  }
16
16
  export declare const DEFAULT_TELEMETRY_CONFIG: TelemetryConfig;
17
+ export declare function parseTelemetryMapping(raw: unknown): TelemetryConfig;
18
+ export declare function serializeTelemetryMapping(config: TelemetryConfig): unknown;
19
+ export declare function loadTelemetryConfig(fs: FileSystem, aiwPath: string): TelemetryConfig;
17
20
  export declare function parseTelemetryConfig(content: string): TelemetryConfig;
18
21
  export declare function serializeTelemetryConfig(config: TelemetryConfig): string;
19
22
  export declare function createTelemetryEvent(command: string | undefined, result: CommandResult, config: TelemetryConfig): TelemetryEvent | undefined;
20
- export declare function recordConfiguredTelemetry(fs: FileSystem, configPath: string, client: TelemetryClient | undefined, command: string | undefined, result: CommandResult): Promise<void>;
23
+ export declare function recordConfiguredTelemetry(fs: FileSystem, aiwPath: string, client: TelemetryClient | undefined, command: string | undefined, result: CommandResult): Promise<void>;
package/dist/telemetry.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { loadSettings } from "./settings.js";
1
2
  export const DEFAULT_TELEMETRY_CONFIG = {
2
3
  schema: 1,
3
4
  enabled: false,
@@ -45,6 +46,41 @@ const TELEMETRY_COMMANDS = new Set([
45
46
  "verify",
46
47
  "verify-package",
47
48
  ]);
49
+ export function parseTelemetryMapping(raw) {
50
+ if (!raw || typeof raw !== "object")
51
+ throw new Error("telemetry settings must be a mapping");
52
+ const record = raw;
53
+ if (typeof record.enabled !== "boolean")
54
+ throw new Error("telemetry.enabled must be a boolean");
55
+ if (typeof record.include_command !== "boolean")
56
+ throw new Error("telemetry.include_command must be a boolean");
57
+ if (typeof record.include_outcome !== "boolean")
58
+ throw new Error("telemetry.include_outcome must be a boolean");
59
+ return {
60
+ schema: 1,
61
+ enabled: record.enabled,
62
+ includeCommand: record.include_command,
63
+ includeOutcome: record.include_outcome,
64
+ };
65
+ }
66
+ export function serializeTelemetryMapping(config) {
67
+ return {
68
+ enabled: config.enabled,
69
+ include_command: config.includeCommand,
70
+ include_outcome: config.includeOutcome,
71
+ };
72
+ }
73
+ export function loadTelemetryConfig(fs, aiwPath) {
74
+ const settings = loadSettings(fs, aiwPath);
75
+ if (settings !== undefined)
76
+ return "telemetry" in settings
77
+ ? parseTelemetryMapping(settings.telemetry)
78
+ : DEFAULT_TELEMETRY_CONFIG;
79
+ const legacyPath = `${aiwPath}/telemetry.yml`;
80
+ if (!fs.exists(legacyPath))
81
+ return DEFAULT_TELEMETRY_CONFIG;
82
+ return parseTelemetryConfig(fs.read(legacyPath));
83
+ }
48
84
  export function parseTelemetryConfig(content) {
49
85
  const lines = content.split(/\r?\n/);
50
86
  if (lines.at(-1) === "")
@@ -74,11 +110,16 @@ export function createTelemetryEvent(command, result, config) {
74
110
  ...(config.includeOutcome ? { outcome: result.exitCode === 0 ? "success" : "failure" } : {}),
75
111
  };
76
112
  }
77
- export async function recordConfiguredTelemetry(fs, configPath, client, command, result) {
78
- if (!client || !fs.exists(configPath))
113
+ export async function recordConfiguredTelemetry(fs, aiwPath, client, command, result) {
114
+ if (!client)
79
115
  return;
80
116
  try {
81
- const event = createTelemetryEvent(command, result, parseTelemetryConfig(fs.read(configPath)));
117
+ const config = loadTelemetryConfig(fs, aiwPath);
118
+ const settings = loadSettings(fs, aiwPath);
119
+ const configured = (settings !== undefined && "telemetry" in settings) || fs.exists(`${aiwPath}/telemetry.yml`);
120
+ if (!configured)
121
+ return;
122
+ const event = createTelemetryEvent(command, result, config);
82
123
  if (event)
83
124
  await client.record(event);
84
125
  }
package/dist/workflow.js CHANGED
@@ -5,8 +5,7 @@ import { detectTarget, isTarget, renderAiInit, renderAiInitPath, renderResourceP
5
5
  import { loadBundledResources } from "./bundled-resources.js";
6
6
  import { assertSafeProjectPath, withSafeProjectWrites, writeProjectFilesAtomically, } from "./files.js";
7
7
  import { parseProjectProfile, profileProject } from "./profile.js";
8
- import { serializeOverrides } from "./confirmation.js";
9
- import { applyOverrides, parseOverrides } from "./confirmation.js";
8
+ import { applyOverrides } from "./confirmation.js";
10
9
  import { mergeFacts } from "./intelligence.js";
11
10
  import { createInterpretationRequest } from "./interpreter.js";
12
11
  import { recommendCapabilities, resourcesForCapabilities, serializeRecommendations, } from "./recommendations.js";
@@ -21,8 +20,9 @@ import { validatePackageContract } from "./package-contract.js";
21
20
  import { parseLock, resolvePackage, serializeLock } from "./lockfile.js";
22
21
  import { executeVercelSkills, installVercelSkill, nodeCommandExecutor } from "./vercel-skills.js";
23
22
  import { scanProject } from "./scanner.js";
24
- import { buildEvidenceTriageRequest, createOllamaDecisionProvider, DEFAULT_DECISION_CONFIG, DECISION_CONFIG_PATH, loadDecisionConfig, serializeDecisionConfig, } from "./decision.js";
25
- import { buildSemanticIndex, createOllamaEmbeddingProvider, DEFAULT_SEMANTIC_CONFIG, loadSemanticConfig, parseSemanticIndex, querySemanticIndex, SEMANTIC_CONFIG_PATH, SEMANTIC_INDEX_PATH, serializeSemanticConfig, serializeSemanticIndex, } from "./semantic.js";
23
+ import { readFactOverrides, saveSettingOverrides, saveSettingsSection, loadSettings, } from "./settings.js";
24
+ import { buildEvidenceTriageRequest, createOllamaDecisionProvider, DEFAULT_DECISION_CONFIG, loadDecisionConfig, } from "./decision.js";
25
+ import { buildSemanticIndex, createOllamaEmbeddingProvider, DEFAULT_SEMANTIC_CONFIG, loadSemanticConfig, parseSemanticIndex, querySemanticIndex, SEMANTIC_INDEX_PATH, serializeSemanticIndex, } from "./semantic.js";
26
26
  import { assertKnownPermissions, assertPermissions, assertPackagePermissions, serializePermissionReview, } from "./package-audit.js";
27
27
  import { planPackageUpdate } from "./package-updates.js";
28
28
  import { searchRegistry, serializeRegistry } from "./registry.js";
@@ -42,7 +42,7 @@ import { evaluateQualityGate } from "./quality-gates.js";
42
42
  import { parseMigrationCheckpoint, planNeutralResourceMigration, readResourceBytes, serializeMigrationPreview, serializeMigrationCheckpoint, validateNeutralResourceDocument, writeResourceBytes, } from "./migration.js";
43
43
  import { parsePrivateRegistries, parseTeamPreset, serializePrivateRegistries, serializeTeamPreset, fetchPrivateRegistryClient, } from "./team-configuration.js";
44
44
  import { enforcePolicyGate, gitPolicyTracking } from "./policy-gate.js";
45
- import { DEFAULT_TELEMETRY_CONFIG, parseTelemetryConfig, serializeTelemetryConfig, } from "./telemetry.js";
45
+ import { DEFAULT_TELEMETRY_CONFIG, loadTelemetryConfig, parseTelemetryConfig, serializeTelemetryMapping, } from "./telemetry.js";
46
46
  import { createPluginScaffold, validatePluginDirectory, writePluginScaffold, } from "./plugin-sdk.js";
47
47
  import { executeOrchestrationPlan, parseOrchestrationPlan, serializeOrchestrationPreview, serializeOrchestrationResults, validateOrchestrationParallelism, } from "./orchestration.js";
48
48
  export async function runCommand(args, root, fs, services = {}) {
@@ -161,9 +161,12 @@ export async function runCommand(args, root, fs, services = {}) {
161
161
  if (ledger.stages[workflowStage]?.artifactHash !== artifactHash) {
162
162
  const invalidated = invalidateStageAndDescendants(ledger, workflowStage, new Date().toISOString());
163
163
  fs.write(join(aiw, "approvals.yml"), serializeApprovalLedger(invalidated));
164
+ const existing = ledger.stages[workflowStage];
164
165
  return {
165
166
  exitCode: 1,
166
- error: `The ${workflowStage} artifact changed after review; its approval is stale. Rerun the quality gate and request approval again.`,
167
+ error: existing
168
+ ? `The ${workflowStage} artifact changed after review; its approval is stale. Rerun the quality gate and request approval again.`
169
+ : `No passing quality gate recorded for ${workflowStage}. Run aiw gate ${workflowStage} before approving.`,
167
170
  };
168
171
  }
169
172
  const updated = approveStage(ledger, workflowStage, {
@@ -352,13 +355,11 @@ export async function runCommand(args, root, fs, services = {}) {
352
355
  const action = args[1];
353
356
  if (!action)
354
357
  return { exitCode: 1, error: usage };
355
- const path = join(aiw, "telemetry.yml");
358
+ const telemetryConfig = loadTelemetryConfig(fs, aiw);
356
359
  if (action === "status") {
357
360
  if (args.length !== 2)
358
361
  return { exitCode: 1, error: usage };
359
- const config = fs.exists(path)
360
- ? parseTelemetryConfig(fs.read(path))
361
- : DEFAULT_TELEMETRY_CONFIG;
362
+ const config = telemetryConfig;
362
363
  return {
363
364
  exitCode: 0,
364
365
  output: `Telemetry is ${config.enabled ? "enabled" : "disabled"}. Command collection: ${config.includeCommand ? "included" : "excluded"}. Outcome collection: ${config.includeOutcome ? "included" : "excluded"}.`,
@@ -367,7 +368,7 @@ export async function runCommand(args, root, fs, services = {}) {
367
368
  if (action === "disable") {
368
369
  if (args.length !== 2)
369
370
  return { exitCode: 1, error: usage };
370
- fs.write(path, serializeTelemetryConfig({ ...DEFAULT_TELEMETRY_CONFIG }));
371
+ saveSettingsSection(fs, aiw, "telemetry", serializeTelemetryMapping(DEFAULT_TELEMETRY_CONFIG));
371
372
  return { exitCode: 0, output: "Telemetry disabled." };
372
373
  }
373
374
  if (action === "enable") {
@@ -377,7 +378,7 @@ export async function runCommand(args, root, fs, services = {}) {
377
378
  options.length)
378
379
  return { exitCode: 1, error: usage };
379
380
  const privacyValue = (name) => !options.includes(`--${name}=exclude`);
380
- fs.write(path, serializeTelemetryConfig({
381
+ saveSettingsSection(fs, aiw, "telemetry", serializeTelemetryMapping({
381
382
  schema: 1,
382
383
  enabled: true,
383
384
  includeCommand: privacyValue("commands"),
@@ -428,9 +429,7 @@ export async function runCommand(args, root, fs, services = {}) {
428
429
  const inferred = await services.interpreter.interpret(createInterpretationRequest(profile, scan.files));
429
430
  profile.facts = mergeFacts(profile.facts, inferred);
430
431
  }
431
- const overrides = fs.exists(join(aiw, "overrides.yml"))
432
- ? parseOverrides(fs.read(join(aiw, "overrides.yml")))
433
- : [];
432
+ const overrides = readFactOverrides(fs, aiw);
434
433
  const rejectedKeys = new Set(overrides
435
434
  .filter((override) => override.action === "reject")
436
435
  .map((override) => override.key));
@@ -472,7 +471,7 @@ export async function runCommand(args, root, fs, services = {}) {
472
471
  if (!config)
473
472
  return {
474
473
  exitCode: 0,
475
- output: `Decision model is not configured. Create .aiw/${DECISION_CONFIG_PATH} to enable it.`,
474
+ output: `Decision model is not configured. Add a decision section to .aiw/settings.yml to enable it.`,
476
475
  };
477
476
  return {
478
477
  exitCode: 0,
@@ -519,7 +518,7 @@ export async function runCommand(args, root, fs, services = {}) {
519
518
  if (!config)
520
519
  return {
521
520
  exitCode: 0,
522
- output: `Semantic mapping is not configured. Create .aiw/${"semantic.yml"} to enable it.`,
521
+ output: `Semantic mapping is not configured. Add a semantic section to .aiw/settings.yml to enable it.`,
523
522
  };
524
523
  const indexPath = join(aiw, SEMANTIC_INDEX_PATH);
525
524
  const built = fs.exists(indexPath)
@@ -915,7 +914,7 @@ export async function runCommand(args, root, fs, services = {}) {
915
914
  exitCode: 1,
916
915
  error: "Usage: aiw confirm --accept key | --reject key | --edit key=value",
917
916
  };
918
- fs.write(join(aiw, "overrides.yml"), serializeOverrides(overrides));
917
+ saveSettingOverrides(fs, aiw, overrides);
919
918
  return { exitCode: 0, output: "Fact overrides saved." };
920
919
  }
921
920
  if (command === "recommend") {
@@ -1129,6 +1128,7 @@ export async function runCommand(args, root, fs, services = {}) {
1129
1128
  parsePrivateRegistries(fs.read(join(aiw, "registries.yml")));
1130
1129
  if (fs.exists(join(aiw, "telemetry.yml")))
1131
1130
  parseTelemetryConfig(fs.read(join(aiw, "telemetry.yml")));
1131
+ loadTelemetryConfig(fs, aiw);
1132
1132
  }
1133
1133
  catch (error) {
1134
1134
  return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
@@ -1163,6 +1163,7 @@ export async function runCommand(args, root, fs, services = {}) {
1163
1163
  "profile.yml",
1164
1164
  "lock.yml",
1165
1165
  "overrides.yml",
1166
+ "settings.yml",
1166
1167
  "recommendations.yml",
1167
1168
  "team-preset.yml",
1168
1169
  "registries.yml",
@@ -1796,23 +1797,32 @@ function approvalArtifactPath(aiw, stage) {
1796
1797
  }
1797
1798
  export async function configureOptionalModels(fs, aiwPath, answer) {
1798
1799
  const enabled = [];
1799
- const semanticPath = join(aiwPath, SEMANTIC_CONFIG_PATH);
1800
- if (!fs.exists(semanticPath)) {
1800
+ const settings = loadSettings(fs, aiwPath) ?? {};
1801
+ if (!("semantic" in settings)) {
1801
1802
  const response = (await answer(`Enable semantic mapping with a local embedding model (default: ${DEFAULT_SEMANTIC_CONFIG.model} at ${DEFAULT_SEMANTIC_CONFIG.baseUrl})? [y/N] `))
1802
1803
  .trim()
1803
1804
  .toLowerCase();
1804
1805
  if (response === "y" || response === "yes") {
1805
- fs.write(semanticPath, serializeSemanticConfig({ ...DEFAULT_SEMANTIC_CONFIG, enabled: true }));
1806
+ saveSettingsSection(fs, aiwPath, "semantic", {
1807
+ enabled: true,
1808
+ model: DEFAULT_SEMANTIC_CONFIG.model,
1809
+ baseUrl: DEFAULT_SEMANTIC_CONFIG.baseUrl,
1810
+ chunkChars: DEFAULT_SEMANTIC_CONFIG.chunkChars,
1811
+ topK: DEFAULT_SEMANTIC_CONFIG.topK,
1812
+ });
1806
1813
  enabled.push(`semantic (${DEFAULT_SEMANTIC_CONFIG.model})`);
1807
1814
  }
1808
1815
  }
1809
- const decisionPath = join(aiwPath, DECISION_CONFIG_PATH);
1810
- if (!fs.exists(decisionPath)) {
1816
+ if (!("decision" in settings)) {
1811
1817
  const response = (await answer(`Enable advisory decision triage with a local decision model (default: ${DEFAULT_DECISION_CONFIG.model} at ${DEFAULT_DECISION_CONFIG.baseUrl})? [y/N] `))
1812
1818
  .trim()
1813
1819
  .toLowerCase();
1814
1820
  if (response === "y" || response === "yes") {
1815
- fs.write(decisionPath, serializeDecisionConfig({ ...DEFAULT_DECISION_CONFIG, enabled: true }));
1821
+ saveSettingsSection(fs, aiwPath, "decision", {
1822
+ enabled: true,
1823
+ model: DEFAULT_DECISION_CONFIG.model,
1824
+ baseUrl: DEFAULT_DECISION_CONFIG.baseUrl,
1825
+ });
1816
1826
  enabled.push(`decision (${DEFAULT_DECISION_CONFIG.model})`);
1817
1827
  }
1818
1828
  }
@@ -1,10 +1,10 @@
1
1
  # Semantic Mapping
2
2
 
3
- AI Workflow can build a semantic index of the scanned project with a local embedding model such as `embeddinggemma-2` served by Ollama. Semantic mapping is opt-in and flag-gated: it activates only when a valid configuration exists, and every scan and flow query re-reads the flag. During an interactive `aiw install`, the installer offers to create this configuration with defaults; answering `y` writes it before `/ai-init` runs. Declining, or installing non-interactively, leaves the file absent.
3
+ AI Workflow can build a semantic index of the scanned project with a local embedding model such as `embeddinggemma-2` served by Ollama. Semantic mapping is opt-in and flag-gated: it activates only when a valid configuration exists, and every scan and flow query re-reads the flag. Configuration lives in the local user settings file `.aiw/settings.yml` under the `semantic` section; the legacy `.aiw/semantic.yml` file is still read when `settings.yml` does not exist. During an interactive `aiw install`, the installer offers to create this configuration with defaults; answering `y` writes it before `/ai-init` runs. Declining, or installing non-interactively, leaves the section absent.
4
4
 
5
5
  ## Configuration
6
6
 
7
- Create `.aiw/semantic.yml`:
7
+ Add a `semantic` section to `.aiw/settings.yml`:
8
8
 
9
9
  ```yaml
10
10
  semantic:
package/docs/telemetry.md CHANGED
@@ -11,7 +11,7 @@ aiw telemetry enable --commands=exclude --outcomes=include
11
11
  aiw telemetry disable
12
12
  ```
13
13
 
14
- Preferences are stored in `.aiw/telemetry.yml`. Command and outcome collection can be enabled or excluded independently. Disabling telemetry takes effect before the next event can be recorded.
14
+ Preferences are stored in the local user settings file `.aiw/settings.yml` under the `telemetry` section (the legacy `.aiw/telemetry.yml` file is still read when `settings.yml` does not exist). Command and outcome collection can be enabled or excluded independently. Disabling telemetry takes effect before the next event can be recorded.
15
15
 
16
16
  Rejected telemetry administration commands never produce telemetry events, even when a previous valid configuration enabled collection. This keeps malformed consent or privacy changes outside the telemetry stream.
17
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@multileaf/ai-workflow",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "Portable spec-driven development workflow for AI coding agents",
5
5
  "license": "MIT",
6
6
  "repository": {