@multileaf/ai-workflow 0.4.1 → 0.6.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/dist/workflow.js CHANGED
@@ -7,8 +7,9 @@ import { assertSafeProjectPath, withSafeProjectWrites, writeProjectFilesAtomical
7
7
  import { parseProjectProfile, profileProject } from "./profile.js";
8
8
  import { applyOverrides } from "./confirmation.js";
9
9
  import { mergeFacts } from "./intelligence.js";
10
- import { createInterpretationRequest } from "./interpreter.js";
11
- import { recommendCapabilities, resourcesForCapabilities, serializeRecommendations, } from "./recommendations.js";
10
+ import { configureOptionalModels } from "./model-onboarding.js";
11
+ import { createInterpretationRequest, createOpenAiCompatibleProvider, createProjectInterpreter, buildInterpreterQuery, loadAiProviderConfig, selectInterpreterKeyFiles, } from "./interpreter.js";
12
+ import { recommendCapabilities, rankRecommendations, resourcesForCapabilities, serializeRecommendations, } from "./recommendations.js";
12
13
  import { selectRecommendations } from "./selection.js";
13
14
  import { createInterface } from "node:readline/promises";
14
15
  import { stdin, stdout } from "node:process";
@@ -20,9 +21,9 @@ import { validatePackageContract } from "./package-contract.js";
20
21
  import { parseLock, resolvePackage, serializeLock } from "./lockfile.js";
21
22
  import { executeVercelSkills, installVercelSkill, nodeCommandExecutor } from "./vercel-skills.js";
22
23
  import { scanProject } from "./scanner.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";
24
+ import { readFactOverrides, saveSettingOverrides, saveSettingsSection } from "./settings.js";
25
+ import { buildEvidenceTriageRequest, buildRecommendationRankingRequest, buildSemanticRerankRequest, createOllamaDecisionProvider, loadDecisionConfig, } from "./decision.js";
26
+ import { buildSemanticIndex, createOllamaEmbeddingProvider, loadSemanticConfig, parseSemanticIndex, querySemanticIndex, rerankSemanticMatches, SEMANTIC_INDEX_PATH, serializeSemanticIndex, } from "./semantic.js";
26
27
  import { assertKnownPermissions, assertPermissions, assertPackagePermissions, serializePermissionReview, } from "./package-audit.js";
27
28
  import { planPackageUpdate } from "./package-updates.js";
28
29
  import { searchRegistry, serializeRegistry } from "./registry.js";
@@ -42,7 +43,7 @@ import { evaluateQualityGate } from "./quality-gates.js";
42
43
  import { parseMigrationCheckpoint, planNeutralResourceMigration, readResourceBytes, serializeMigrationPreview, serializeMigrationCheckpoint, validateNeutralResourceDocument, writeResourceBytes, } from "./migration.js";
43
44
  import { parsePrivateRegistries, parseTeamPreset, serializePrivateRegistries, serializeTeamPreset, fetchPrivateRegistryClient, } from "./team-configuration.js";
44
45
  import { enforcePolicyGate, gitPolicyTracking } from "./policy-gate.js";
45
- import { DEFAULT_TELEMETRY_CONFIG, loadTelemetryConfig, parseTelemetryConfig, serializeTelemetryMapping, } from "./telemetry.js";
46
+ import { DEFAULT_TELEMETRY_CONFIG, loadTelemetryConfig, offerTelemetryConsent, parseTelemetryConfig, purgeTelemetryBuffer, readTelemetryBuffer, serializeTelemetryMapping, TELEMETRY_BUFFER_CAP, TELEMETRY_BUFFER_FILE, } from "./telemetry.js";
46
47
  import { createPluginScaffold, validatePluginDirectory, writePluginScaffold, } from "./plugin-sdk.js";
47
48
  import { executeOrchestrationPlan, parseOrchestrationPlan, serializeOrchestrationPreview, serializeOrchestrationResults, validateOrchestrationParallelism, } from "./orchestration.js";
48
49
  export async function runCommand(args, root, fs, services = {}) {
@@ -331,10 +332,12 @@ export async function runCommand(args, root, fs, services = {}) {
331
332
  pendingWrites.set(manifestPath, `schema: 1\nproject:\n name: ${root.split("/").pop()}\ntarget:\n active: ${target}\npolicies:\n artifact_language: en\n`);
332
333
  writeProjectFilesAtomically(fs, root, [...pendingWrites].map(([path, content]) => ({ path, content })), [...BASE_DIRECTORIES.map((directory) => join(aiw, directory)), join(root, ".context/adrs")]);
333
334
  let optionalModels = [];
335
+ let telemetryEnabled;
334
336
  if (stdin.isTTY) {
335
337
  const terminal = createInterface({ input: stdin, output: stdout });
336
338
  try {
337
339
  optionalModels = await configureOptionalModels(fs, aiw, (question) => terminal.question(question));
340
+ telemetryEnabled = await offerTelemetryConsent(fs, aiw, (question) => terminal.question(question));
338
341
  }
339
342
  finally {
340
343
  terminal.close();
@@ -345,34 +348,49 @@ export async function runCommand(args, root, fs, services = {}) {
345
348
  output: `AI Workflow initialized for target: ${target} with only the ai-init skill. Run /ai-init to scan the project and choose which resources to install.` +
346
349
  (optionalModels.length
347
350
  ? ` Optional models enabled: ${optionalModels.join(", ")}. They activate once the models are available.`
351
+ : "") +
352
+ (telemetryEnabled !== undefined
353
+ ? telemetryEnabled
354
+ ? " Telemetry enabled (command name and outcome only); disable anytime with `aiw telemetry disable`."
355
+ : " Telemetry disabled; enable anytime with `aiw telemetry enable`."
348
356
  : ""),
349
357
  };
350
358
  }
351
359
  if (command === "telemetry") {
352
360
  if (!fs.exists(join(aiw, "manifest.yml")))
353
361
  return { exitCode: 1, error: "Run `aiw install` first." };
354
- const usage = "Usage: aiw telemetry <status|enable|disable> [--commands=include|exclude] [--outcomes=include|exclude]";
362
+ const usage = "Usage: aiw telemetry <status|enable|disable> [--verbose] [--commands=include|exclude] [--outcomes=include|exclude]";
355
363
  const action = args[1];
356
364
  if (!action)
357
365
  return { exitCode: 1, error: usage };
358
366
  const telemetryConfig = loadTelemetryConfig(fs, aiw);
367
+ const actionArgs = args.slice(2).filter((arg) => arg !== "--verbose");
368
+ const verbose = args.includes("--verbose");
359
369
  if (action === "status") {
360
- if (args.length !== 2)
370
+ if (actionArgs.length !== 0)
361
371
  return { exitCode: 1, error: usage };
362
372
  const config = telemetryConfig;
373
+ const buffered = verbose ? readTelemetryBuffer(fs, aiw) : undefined;
363
374
  return {
364
375
  exitCode: 0,
365
- output: `Telemetry is ${config.enabled ? "enabled" : "disabled"}. Command collection: ${config.includeCommand ? "included" : "excluded"}. Outcome collection: ${config.includeOutcome ? "included" : "excluded"}.`,
376
+ output: `Telemetry is ${config.enabled ? "enabled" : "disabled"}. Command collection: ${config.includeCommand ? "included" : "excluded"}. Outcome collection: ${config.includeOutcome ? "included" : "excluded"}.` +
377
+ (buffered !== undefined
378
+ ? `\nLocal buffer: ${buffered.length} event${buffered.length === 1 ? "" : "s"} (cap ${TELEMETRY_BUFFER_CAP}, .aiw/${TELEMETRY_BUFFER_FILE}).` +
379
+ (buffered.length > 0
380
+ ? `\n${buffered.map((event) => `- ${JSON.stringify(event)}`).join("\n")}`
381
+ : "")
382
+ : ""),
366
383
  };
367
384
  }
368
385
  if (action === "disable") {
369
- if (args.length !== 2)
386
+ if (actionArgs.length !== 0)
370
387
  return { exitCode: 1, error: usage };
388
+ purgeTelemetryBuffer(fs, aiw);
371
389
  saveSettingsSection(fs, aiw, "telemetry", serializeTelemetryMapping(DEFAULT_TELEMETRY_CONFIG));
372
390
  return { exitCode: 0, output: "Telemetry disabled." };
373
391
  }
374
392
  if (action === "enable") {
375
- const options = args.slice(2);
393
+ const options = args.slice(2).filter((arg) => arg !== "--verbose");
376
394
  if (options.some((option) => !/^--(?:commands|outcomes)=(?:include|exclude)$/.test(option)) ||
377
395
  new Set(options.map((option) => option.slice(0, option.indexOf("=")))).size !==
378
396
  options.length)
@@ -424,10 +442,57 @@ export async function runCommand(args, root, fs, services = {}) {
424
442
  if (!fs.exists(join(aiw, "manifest.yml")))
425
443
  return { exitCode: 1, error: "Run `aiw install` first." };
426
444
  const profile = await (services.profiler?.profile(root) ?? profileProject(root));
427
- if (services.interpreter) {
445
+ let interpreter = services.interpreter;
446
+ let interpreterModel;
447
+ if (!interpreter) {
448
+ const aiConfig = loadAiProviderConfig(fs, aiw);
449
+ if (aiConfig?.enabled) {
450
+ interpreter = createProjectInterpreter(root, services.aiProvider ?? createOpenAiCompatibleProvider(aiConfig));
451
+ interpreterModel = aiConfig.model;
452
+ }
453
+ }
454
+ else
455
+ interpreterModel = "host-injected";
456
+ const modelUsage = [];
457
+ if (interpreter) {
428
458
  const scan = await (services.scanner?.scan(root) ?? scanProject(root));
429
- const inferred = await services.interpreter.interpret(createInterpretationRequest(profile, scan.files));
459
+ let scopedFiles = scan.files;
460
+ let priorityFiles;
461
+ const contextConfig = loadSemanticConfig(fs, aiw);
462
+ const contextIndexPath = join(aiw, SEMANTIC_INDEX_PATH);
463
+ if (contextConfig?.enabled && fs.exists(contextIndexPath)) {
464
+ try {
465
+ const index = parseSemanticIndex(fs.read(contextIndexPath));
466
+ if (index.model === contextConfig.model && index.chunks.length > 0) {
467
+ const embedding = services.embeddingProvider ?? createOllamaEmbeddingProvider(contextConfig);
468
+ if (await embedding.available()) {
469
+ const matches = await querySemanticIndex(index, buildInterpreterQuery(profile), embedding, contextConfig);
470
+ priorityFiles = [
471
+ ...selectInterpreterKeyFiles(scan.files),
472
+ ...matches.map((match) => match.file),
473
+ ];
474
+ const allowed = new Set(priorityFiles);
475
+ scopedFiles = scan.files.filter((file) => allowed.has(file));
476
+ modelUsage.push({
477
+ label: "embedding",
478
+ tokens: Math.ceil(buildInterpreterQuery(profile).length / 4),
479
+ detail: `${contextConfig.model} — scoped interpreter context to ${matches.length} semantic matches`,
480
+ });
481
+ }
482
+ }
483
+ }
484
+ catch {
485
+ // An unreadable index keeps the scan deterministic with alphabetical scoping.
486
+ }
487
+ }
488
+ const interpretationRequest = createInterpretationRequest(profile, scopedFiles, 4000, priorityFiles);
489
+ const inferred = await interpreter.interpret(interpretationRequest);
430
490
  profile.facts = mergeFacts(profile.facts, inferred);
491
+ modelUsage.push({
492
+ label: "interpreter",
493
+ tokens: interpretationRequest.estimatedTokens,
494
+ detail: `${interpreterModel} — ${inferred.length} facts from ${interpretationRequest.files.length} files`,
495
+ });
431
496
  }
432
497
  const overrides = readFactOverrides(fs, aiw);
433
498
  const rejectedKeys = new Set(overrides
@@ -442,7 +507,7 @@ export async function runCommand(args, root, fs, services = {}) {
442
507
  fs.write(join(aiw, "profile.yml"), `schema: 1\nstatus: scanned\nfacts:\n${evidence}\nruntime:\n languages: [${profile.runtime.languages.join(", ")}]\nframeworks: [${profile.frameworks.join(", ")}]\npackage_manager: ${profile.packageManager}\nquality:\n linter: ${profile.quality.linter?.name ?? "unknown"}\n linter_command: ${profile.quality.linter?.command ?? "unknown"}\n formatter: ${profile.quality.formatter?.name ?? "unknown"}\n formatter_command: ${profile.quality.formatter?.command ?? "unknown"}\n typecheck: ${profile.quality.typecheck?.name ?? "unknown"}\n typecheck_command: ${profile.quality.typecheck?.command ?? "unknown"}\ntesting: ${profile.testing?.name ?? "unknown"}\ntesting_command: ${profile.testing?.command ?? "unknown"}\nci: [${profile.ci.join(", ")}]\nworkspaces: [${profile.workspaces.join(", ")}]\nproject_modules: ${JSON.stringify(profile.modules ?? [])}\npolicies:\n artifact_language: en\n`);
443
508
  const semanticConfig = loadSemanticConfig(fs, aiw);
444
509
  if (semanticConfig?.enabled) {
445
- const provider = createOllamaEmbeddingProvider(semanticConfig);
510
+ const provider = services.embeddingProvider ?? createOllamaEmbeddingProvider(semanticConfig);
446
511
  if (!(await provider.available()))
447
512
  return {
448
513
  exitCode: 1,
@@ -460,8 +525,34 @@ export async function runCommand(args, root, fs, services = {}) {
460
525
  }, provider, semanticConfig);
461
526
  fs.mkdir(join(aiw, "semantic"));
462
527
  fs.write(join(aiw, SEMANTIC_INDEX_PATH), serializeSemanticIndex(index));
528
+ const indexedChars = index.chunks.reduce((total, chunk) => total + chunk.text.length, 0);
529
+ modelUsage.push({
530
+ label: "embedding",
531
+ tokens: Math.ceil(indexedChars / 4),
532
+ detail: `${semanticConfig.model} — indexed ${index.chunks.length} chunks in ${Math.ceil(index.chunks.length / 64)} batch requests`,
533
+ });
463
534
  }
464
- return { exitCode: 0, output: "Project profile generated." };
535
+ if (modelUsage.length > 0) {
536
+ const usagePath = join(aiw, "usage.yml");
537
+ const entries = fs.exists(usagePath) ? parseTokenUsage(fs.read(usagePath)) : [];
538
+ let next = entries;
539
+ for (const usage of modelUsage)
540
+ next = recordTokenUsage(next, {
541
+ stage: `scan.${usage.label}`,
542
+ budget: usage.tokens,
543
+ used: usage.tokens,
544
+ });
545
+ fs.write(usagePath, serializeTokenUsage(next));
546
+ }
547
+ return {
548
+ exitCode: 0,
549
+ output: "Project profile generated." +
550
+ (modelUsage.length > 0
551
+ ? `\nModels used:\n${modelUsage
552
+ .map((usage) => `- ${usage.label}: ${usage.detail} (~${usage.tokens.toLocaleString("en-US")} tokens)`)
553
+ .join("\n")}`
554
+ : "\nModels used: none (deterministic scan)."),
555
+ };
465
556
  }
466
557
  if (command === "decision") {
467
558
  const usage = "Usage: aiw decision triage --artifact=<path> | aiw decision status";
@@ -511,7 +602,7 @@ export async function runCommand(args, root, fs, services = {}) {
511
602
  return { exitCode: 1, error: usage };
512
603
  }
513
604
  if (command === "semantic") {
514
- const usage = "Usage: aiw semantic query --text=<query> | aiw semantic status";
605
+ const usage = "Usage: aiw semantic query --text=<query> [--rerank] | aiw semantic status";
515
606
  const action = args[1];
516
607
  if (action === "status") {
517
608
  const config = loadSemanticConfig(fs, aiw);
@@ -530,7 +621,9 @@ export async function runCommand(args, root, fs, services = {}) {
530
621
  };
531
622
  }
532
623
  if (action === "query") {
533
- const parsed = parseNamedOptions(args.slice(2), ["text"]);
624
+ const queryArgs = args.slice(2).filter((arg) => arg !== "--rerank");
625
+ const rerankRequested = args.slice(2).includes("--rerank");
626
+ const parsed = parseNamedOptions(queryArgs, ["text"]);
534
627
  if (!parsed?.text)
535
628
  return { exitCode: 1, error: usage };
536
629
  const config = loadSemanticConfig(fs, aiw);
@@ -549,13 +642,38 @@ export async function runCommand(args, root, fs, services = {}) {
549
642
  error: `Semantic model ${config.model} is not available at ${config.baseUrl}.`,
550
643
  };
551
644
  const matches = await querySemanticIndex(parseSemanticIndex(fs.read(indexPath)), parsed.text, provider, config);
645
+ let ranked = matches.map((match) => ({ ...match }));
646
+ let influence;
647
+ if (rerankRequested) {
648
+ const decisionConfig = loadDecisionConfig(fs, aiw);
649
+ if (!decisionConfig?.enabled)
650
+ return {
651
+ exitCode: 1,
652
+ error: "Decision model is disabled. Set decision.enabled=true in .aiw/settings.yml to use --rerank.",
653
+ };
654
+ const decisionProvider = createOllamaDecisionProvider(decisionConfig);
655
+ if (!(await decisionProvider.available()))
656
+ return {
657
+ exitCode: 1,
658
+ error: `Decision model ${decisionConfig.model} is not available at ${decisionConfig.baseUrl}.`,
659
+ };
660
+ const outcome = await rerankSemanticMatches(matches, parsed.text, async (query, candidates) => {
661
+ const response = await decisionProvider.decide(buildSemanticRerankRequest(query, candidates));
662
+ const scores = {};
663
+ for (const [id, answer] of Object.entries(response.answers))
664
+ scores[id] = typeof answer.score === "number" ? answer.score : undefined;
665
+ return scores;
666
+ });
667
+ ranked = outcome.matches;
668
+ influence = `rerank influence: ${outcome.changedPositions}/${outcome.matches.length} positions reordered (advisory; cosine recall unchanged)`;
669
+ }
552
670
  return {
553
671
  exitCode: 0,
554
- output: matches.length === 0
672
+ output: ranked.length === 0
555
673
  ? "No semantic matches."
556
- : matches
557
- .map((match) => `- ${match.file} (score ${match.score.toFixed(3)})\n ${match.excerpt}`)
558
- .join("\n"),
674
+ : `${ranked
675
+ .map((match) => `- ${match.file} (score ${match.score.toFixed(3)}${match.rerankScore !== undefined ? `, rerank ${match.rerankScore.toFixed(3)}` : ""})\n ${match.excerpt}`)
676
+ .join("\n")}${influence ? `\n${influence}` : ""}`,
559
677
  };
560
678
  }
561
679
  return { exitCode: 1, error: usage };
@@ -922,6 +1040,33 @@ export async function runCommand(args, root, fs, services = {}) {
922
1040
  return { exitCode: 1, error: "Run `aiw install` first." };
923
1041
  const profile = await (services.profiler?.profile(root) ?? profileProject(root));
924
1042
  const recommendations = services.recommender?.recommend(profile) ?? recommendCapabilities(profile);
1043
+ let ordered = recommendations;
1044
+ let ranked;
1045
+ const rankingConfig = loadDecisionConfig(fs, aiw);
1046
+ if (rankingConfig?.enabled) {
1047
+ const decisionProvider = services.decisionProvider ?? createOllamaDecisionProvider(rankingConfig);
1048
+ if (!(await decisionProvider.available()))
1049
+ return {
1050
+ exitCode: 1,
1051
+ error: `Decision model ${rankingConfig.model} is not available at ${rankingConfig.baseUrl}.`,
1052
+ };
1053
+ ranked = await rankRecommendations(recommendations, profile, async (scoped, recs) => {
1054
+ const response = await decisionProvider.decide(buildRecommendationRankingRequest(profileSummaryForRanking(scoped), recs));
1055
+ const answers = {};
1056
+ for (const [id, answer] of Object.entries(response.answers))
1057
+ answers[id] =
1058
+ typeof answer.score === "number"
1059
+ ? {
1060
+ relevance: answer.score,
1061
+ ...(typeof answer.confidence === "number"
1062
+ ? { confidence: answer.confidence }
1063
+ : {}),
1064
+ }
1065
+ : {};
1066
+ return answers;
1067
+ });
1068
+ ordered = ranked.items;
1069
+ }
925
1070
  const selectedOption = args.find((arg) => arg.startsWith("--select="));
926
1071
  const selectedArg = selectedOption?.slice("--select=".length) ?? "";
927
1072
  let selection;
@@ -931,15 +1076,18 @@ export async function runCommand(args, root, fs, services = {}) {
931
1076
  selection = [];
932
1077
  else {
933
1078
  const terminal = createInterface({ input: stdin, output: stdout });
934
- selection = await selectRecommendations(recommendations, (question) => terminal.question(question));
1079
+ selection = await selectRecommendations(ordered, (question) => terminal.question(question));
935
1080
  terminal.close();
936
1081
  }
937
- fs.write(join(aiw, "recommendations.yml"), serializeRecommendations(recommendations, selection));
1082
+ fs.write(join(aiw, "recommendations.yml"), serializeRecommendations(ordered, selection, ranked?.ranking));
1083
+ const rankingNote = ranked
1084
+ ? ` Advisory decision-model ranking applied (${ranked.changedPositions} positions reordered); evidence states are unchanged.`
1085
+ : "";
938
1086
  return {
939
1087
  exitCode: 0,
940
- output: selectedOption === undefined && !stdin.isTTY
941
- ? `${recommendations.length} recommendations generated without selection; review .aiw/recommendations.yml, then record user choices with --select=id,id.`
942
- : `${recommendations.length} recommendations generated.`,
1088
+ output: `${ordered.length} recommendations generated${selectedOption === undefined && !stdin.isTTY
1089
+ ? " without selection; review .aiw/recommendations.yml, then record user choices with --select=id,id."
1090
+ : "."}${rankingNote}`,
943
1091
  };
944
1092
  }
945
1093
  if (command === "skills") {
@@ -1795,39 +1943,6 @@ function approvalArtifactPath(aiw, stage) {
1795
1943
  };
1796
1944
  return resolve(aiw, paths[stage]);
1797
1945
  }
1798
- export async function configureOptionalModels(fs, aiwPath, answer) {
1799
- const enabled = [];
1800
- const settings = loadSettings(fs, aiwPath) ?? {};
1801
- if (!("semantic" in settings)) {
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] `))
1803
- .trim()
1804
- .toLowerCase();
1805
- if (response === "y" || response === "yes") {
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
- });
1813
- enabled.push(`semantic (${DEFAULT_SEMANTIC_CONFIG.model})`);
1814
- }
1815
- }
1816
- if (!("decision" in settings)) {
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] `))
1818
- .trim()
1819
- .toLowerCase();
1820
- if (response === "y" || response === "yes") {
1821
- saveSettingsSection(fs, aiwPath, "decision", {
1822
- enabled: true,
1823
- model: DEFAULT_DECISION_CONFIG.model,
1824
- baseUrl: DEFAULT_DECISION_CONFIG.baseUrl,
1825
- });
1826
- enabled.push(`decision (${DEFAULT_DECISION_CONFIG.model})`);
1827
- }
1828
- }
1829
- return enabled;
1830
- }
1831
1946
  function formatConfidence(confidence) {
1832
1947
  return confidence === undefined ? "unknown" : confidence.toFixed(3);
1833
1948
  }
@@ -2193,6 +2308,15 @@ function safeProjectSignal(value) {
2193
2308
  .replace(/\s+/g, " ")
2194
2309
  .slice(0, 180);
2195
2310
  }
2311
+ function profileSummaryForRanking(profile) {
2312
+ return [
2313
+ `languages: ${profile.runtime.languages.join(", ") || "none"}`,
2314
+ `frameworks: ${profile.frameworks.join(", ") || "none"}`,
2315
+ `package-manager: ${profile.packageManager}`,
2316
+ `testing: ${profile.testing?.name ?? "none"}`,
2317
+ `linter: ${profile.quality.linter?.name ?? "none"}`,
2318
+ ].join("; ");
2319
+ }
2196
2320
  function readSelectedRecommendations(fs, aiw) {
2197
2321
  if (!fs.exists(join(aiw, "recommendations.yml")))
2198
2322
  return [];
package/docs/README.md CHANGED
@@ -13,6 +13,11 @@ AI Workflow is a portable, spec-driven development system for AI coding agents.
13
13
  - [Quality and testing](quality.md)
14
14
  - [Token efficiency](token-efficiency.md)
15
15
  - [Project intelligence](project-intelligence.md)
16
+ - [Semantic mapping](semantic-mapping.md)
17
+ - [Decision model evaluation](decision-model-evaluation.md)
18
+ - [Telemetry and privacy](telemetry.md)
19
+ - [Multi-agent orchestration](orchestration.md)
20
+ - [Plugin authoring](plugin-authoring.md)
16
21
  - [Release process](release-process.md)
17
22
  - [Release backlog](release-backlog.md)
18
23
  - [Backlog](backlog.md)
package/docs/backlog.md CHANGED
@@ -29,8 +29,10 @@ Priority levels: **P0** blocks the product or makes it unsafe; **P1** is require
29
29
  - [x] **SCAN-006** Implement scoped AI project interpreter. _Acceptance:_ interpreter receives filtered context and produces structured insights requiring confirmation when uncertain.
30
30
  - [x] **SCAN-007** Add interpreter privacy and token guardrails. _Acceptance:_ secrets, dependencies, generated files, Git internals, and checkpoints are excluded and token usage is measured.
31
31
  - [x] **SCAN-004** Add interactive confirmation and overrides. _Acceptance:_ users can accept, reject, or edit inferred facts.
32
+ - [x] **INT-001** Ship a built-in OpenAI-compatible `AiProvider`. _Acceptance:_ an `aiw` settings section configures a local or remote OpenAI-compatible endpoint (for example Ollama `gpt-oss:20b`) so the scoped AI interpreter works without a host-injected provider; secrets redaction, token ceilings, and scoped-context validation still apply; missing configuration keeps scans deterministic and offline.
32
33
  - [x] **REC-001** Build capability recommendation engine. _Acceptance:_ recommendations include rationale, evidence, permissions, conflicts, and confidence.
33
34
  - [x] **REC-002** Add interactive capability selection. _Acceptance:_ users select packages and individual resources before installation.
35
+ - [x] **REC-003** Rank capability recommendations with a decision model. _Acceptance:_ when a decision model is enabled in settings, recommendations are ordered by decision-model relevance to the confirmed profile with confidence and rationale; ranking is advisory and never changes evidence states. See `docs/decision-model-evaluation.md`.
34
36
  - [x] **GEN-001** Generate project-specific rules, agents, hooks, and context. _Acceptance:_ generated content uses confirmed profile data and English artifacts.
35
37
  - [x] **GEN-002** Preserve manual overrides and detect drift. _Acceptance:_ regeneration never silently overwrites user changes.
36
38
 
@@ -70,6 +72,23 @@ Priority levels: **P0** blocks the product or makes it unsafe; **P1** is require
70
72
 
71
73
  - [x] **SEM-001** Add flag-gated semantic mapping to scan and retrieval. _Acceptance:_ `.aiw/semantic.yml` with `enabled: true` verifies model availability before indexing, builds a versioned chunk index during `aiw scan`, and `aiw semantic query` returns ranked matches; missing or disabled configuration keeps scans fully deterministic and offline; semantic results are retrieval aids, never confirmed facts.
72
74
  - [x] **SEM-002** Integrate a decision model (jev/clef-flash via Ollama `/v1/systemone`) for advisory gate triage. _Acceptance:_ `.aiw/decision.yml` with `enabled: true` verifies model availability before use; `aiw decision triage` returns advisory `evidence_complete` and `gate` decisions with confidence; disabled or missing configuration makes no network request; advisory output never replaces human approval. See `docs/decision-model-evaluation.md`.
75
+ - [x] **SEM-003** Embed chunks in batches. _Acceptance:_ indexing uses the batch embeddings endpoint (for example Ollama `/api/embed`) instead of one request per chunk, measurably reducing scan time on this repository (baseline: ~34s for 824 chunks) with identical index output.
76
+ - [x] **SEM-004** Exclude lockfiles and generated vendor manifests from the semantic index. _Acceptance:_ files such as `package-lock.json` (observed contributing 75 noise chunks) are skipped during indexing by default with an explicit, documented override.
77
+ - [x] **SEM-005** Re-rank semantic recall with a decision model. _Acceptance:_ when both semantic mapping and a decision model are enabled, `aiw semantic query` optionally re-ranks embedding matches with the decision model, reporting the ranking influence; recall without a decision model is unchanged.
78
+ - [x] **SEM-006** Scope AI interpreter context with semantic retrieval. _Acceptance:_ when semantic mapping is enabled and an index exists, the interpreter receives deterministic key files plus the top semantically relevant chunks for the active analysis question instead of alphabetically ordered files up to the token ceiling; measured token usage decreases and inferred-fact quality does not regress against the current baseline; scans without semantic mapping are unchanged.
79
+
80
+ ## Onboarding — P1
81
+
82
+ - [x] **TEL-001** Offer telemetry consent during installation. _Acceptance:_ interactive install asks about telemetry after the model questions with an explicit default of No, states the exact event payload (command name and outcome only, no arguments, paths, or contents), records the decision in `.aiw/settings.yml`, and remains reversible through `aiw telemetry enable/disable`; non-interactive installs skip the prompt; the prompt itself never emits an event.
83
+
84
+ ## Telemetry — P2
85
+
86
+ - [x] **TEL-002** Add an inspectable local telemetry buffer. _Acceptance:_ when telemetry is enabled and a host client is absent, events accumulate in a rotated local buffer under `.aiw/` with a documented retention cap; `aiw telemetry status --verbose` displays buffered events; `aiw telemetry disable` purges the buffer; no network transport is introduced by this item.
87
+
88
+ - [x] **ONB-001** Detect and offer real local models during installation. _Acceptance:_ interactive install probes the configured Ollama endpoint (`/api/tags`), lists the embedding and decision models actually available, and records the user's selection in `.aiw/settings.yml`; when no server or models are found, the installer reports the optional capability and continues without network assumptions; non-interactive installs are unchanged.
89
+
90
+ ## Context and cost — P2 (continued)
91
+
73
92
  - [x] **CTX-001** Build layered project context store.
74
93
  - [x] **CTX-002** Add task-scoped context retrieval and delta loading.
75
94
  - [x] **CTX-003** Add summary cache and freshness invalidation.
@@ -86,6 +105,10 @@ Priority levels: **P0** blocks the product or makes it unsafe; **P1** is require
86
105
  - [x] **ENT-006** Add web UI for project profile, recommendations, traceability, and migrations. _Acceptance:_ `aiw ui` serves a local-only, responsive and accessible dashboard from packaged project state; profile, recommendations, and requirement evidence are visible; migrations require explicit confirmation and delegate to the existing workflow.
87
106
  - [x] **ENT-007** Add multi-agent orchestration and parallel task execution. _Acceptance:_ strict versioned task graphs support deterministic preview, explicit adapter-backed execution, bounded parallelism, dependency-aware failure isolation, and immutable checkpoint evidence.
88
107
 
108
+ ## Engineering — P2
109
+
110
+ - [x] **ENG-001** Update GitHub Actions to Node 24-native versions. _Acceptance:_ `actions/checkout` and `actions/setup-node` upgraded past v4 so release workflows no longer emit Node.js 20 deprecation warnings; CI and Release workflows pass on `ubuntu-latest` before and after the October 19, 2026 Ubuntu 26 runner migration.
111
+
89
112
  ## Backlog rules
90
113
 
91
114
  - [x] **FND-009** Add self-validation workflow. _Acceptance:_ changes affecting AIW behavior execute against this repository and record generated-output evidence.
@@ -27,14 +27,14 @@ Clef Flash answers typed questions over a supplied state in a single non-autoreg
27
27
 
28
28
  The earlier blocker (no exposed endpoint in Ollama 0.40.1) is resolved by the `/v1/systemone` endpoint documented at <https://ollama.com/library/clef-flash>. AI Workflow now ships the advisory layer:
29
29
 
30
- - `src/decision.ts`: `DecisionConfig` (`.aiw/decision.yml`, flag-gated: `enabled` / `model` / `baseUrl`), a `DecisionProvider` with availability verification (`/api/tags`), typed question/response contracts, and `buildEvidenceTriageRequest` for pre-gate triage.
30
+ - `src/decision.ts`: `DecisionConfig` (`.aiw/decision.yml`, flag-gated: `enabled` / `model` / `baseUrl` / optional `apiKeyEnv` for an external OpenAI-compatible endpoint with bearer authentication), a `DecisionProvider` with availability verification (`/api/tags` for Ollama, `/v1/models` with auth for external), typed question/response contracts, and `buildEvidenceTriageRequest` for pre-gate triage.
31
31
  - `aiw decision status` and `aiw decision triage --artifact=<path>`: advisory `evidence_complete` (noul) and `gate` (`ready` / `needs-work`) decisions over any project artifact, with explicit confidence reporting.
32
32
 
33
33
  Observed behavior on real plans: an evidence-free plan scored `gate: ready` with confidence 0.044 and `evidence_complete` 0.260, while a fully evidenced plan scored confidence 0.718 and `evidence_complete` 0.924 — low confidence signals an inconclusive decision rather than approval.
34
34
 
35
35
  ## Future integration points (advisory)
36
36
 
37
- - **Recommendation ranking**: order capability recommendations by decision-model relevance to the confirmed profile.
37
+ - **Recommendation ranking**: order capability recommendations by decision-model relevance to the confirmed profile. _Status:_ implemented 2026-10-08 behind `decision.enabled` — `aiw recommend` sends one `score` question per recommendation (with the confirmed profile summary and the recommendation rationale/evidence) through `/v1/systemone`, orders the output by advisory relevance, and serializes `decision_relevance` / `decision_confidence` per item. The ranking is advisory: evidence, confidence, and selection logic are unchanged, and without a decision model the deterministic order is preserved.
38
38
  - **Semantic re-ranking**: combine embedding recall (`aiw semantic query`) with clef re-ranking for sharper context selection during interpretation.
39
39
 
40
40
  ## Hard boundary (unchanged)
@@ -58,4 +58,16 @@ Before a provider call, the interpreter:
58
58
  - redacts common credential assignments, bearer tokens, and private keys;
59
59
  - rejects inferred evidence that cites any file outside the scoped request.
60
60
 
61
- The workflow accepts an injected interpreter service. A configured interpreter runs during `aiw scan`, and its structured facts are stored in `.aiw/profile.yml` alongside deterministic facts. Facts below the confidence threshold are marked `requires_confirmation: true`. Without a configured provider, scans remain deterministic and do not make a network request.
61
+ The workflow accepts an injected interpreter service. Additionally, a built-in OpenAI-compatible provider can be configured through an `ai` section in `.aiw/settings.yml` so the interpreter works without a host-injected service:
62
+
63
+ ```yaml
64
+ ai:
65
+ enabled: true
66
+ model: gpt-oss:20b # any model served by an OpenAI-compatible endpoint
67
+ baseUrl: http://localhost:11434 # local (Ollama) or remote
68
+ apiKeyEnv: AIW_AI_KEY # optional; bearer token read from this environment variable
69
+ ```
70
+
71
+ The provider posts to `<baseUrl>/v1/chat/completions` with a JSON response format and the interpreter's token ceiling; all existing guardrails (secrets redaction, scoped-context validation, binary and sensitive-file exclusion) apply unchanged because the same interpreter pipeline is used. An injected interpreter service always takes precedence. Without an `ai` section, scans remain deterministic and do not make a network request.
72
+
73
+ A configured interpreter runs during `aiw scan`, and its structured facts are stored in `.aiw/profile.yml` alongside deterministic facts. Facts below the confidence threshold are marked `requires_confirmation: true`.
@@ -35,4 +35,4 @@ For the first publication only, create a short-lived granular npm token scoped t
35
35
 
36
36
  After `0.1.0` exists, configure organization `MultiLeaf`, repository `aiw`, workflow `release.yml`, and environment `npm` as a trusted publisher in the package settings. Publish a subsequent release through OIDC and verify its provenance on npm before deleting the GitHub bootstrap secret and revoking the granular token. A saved publisher configuration alone is insufficient evidence because npm validates it only during publication.
37
37
 
38
- The workflow uses Node 24 and npm 11.5.1 or newer, strictly validates Semantic Versioning, checks that the tag is `v<package-version>`, requires the approved license, and verifies ancestry from `origin/main`. Publish each tag only once; never reuse, move, or overwrite a published version.
38
+ The workflow uses Node 24 and npm 11.5.1 or newer, strictly validates Semantic Versioning, checks that the tag is `v<package-version>`, requires the approved license, and verifies ancestry from `origin/main`. After publishing, a `github-release` job creates the GitHub Release for the tag from the matching `CHANGELOG.md` section, so the signed Git tag, GitHub release, and npm package always agree on the immutable version. Publish each tag only once; never reuse, move, or overwrite a published version.
@@ -1,6 +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. 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.
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.
4
+
5
+ The provider can be Ollama (default, `http://localhost:11434`) or any OpenAI-compatible embeddings endpoint: set `apiKeyEnv` to the name of the environment variable holding the bearer token, and requests go to `<baseUrl>/v1/embeddings` with `Authorization: Bearer <token>` instead of Ollama's `/api/embed`. The token is read from the environment at call time and never stored in settings.
6
+
7
+ During an interactive `aiw install`, the installer walks through provider selection: it probes the configured Ollama endpoint (`GET /api/tags`), offers Ollama or an external provider (external only, when Ollama is unreachable), lists the locally available embedding models — filtering to embedding-type models — and asks permission to run `ollama pull embeddinggemma-2` when none exist. External configuration asks for the endpoint URL, model name, and an optional token environment variable. The chosen setup is validated with a live request before being recorded in `.aiw/settings.yml`; a failed validation can be retried or skipped, and the next capability (decision triage) follows the same flow. Answering `n`, or installing non-interactively, leaves the section absent.
4
8
 
5
9
  ## Configuration
6
10
 
@@ -13,6 +17,8 @@ semantic:
13
17
  baseUrl: http://localhost:11434
14
18
  chunkChars: 1200 # optional, minimum 200
15
19
  topK: 8 # optional, results per query
20
+ excludeLockfiles: true # optional, skip lockfiles and generated vendor manifests (default: true)
21
+ apiKeyEnv: AIW_EMBED_KEY # optional, external OpenAI-compatible endpoint bearer token
16
22
  ```
17
23
 
18
24
  - `enabled: false` (or a missing file) disables semantic mapping entirely; scans stay deterministic and make no network request.
@@ -22,18 +28,41 @@ semantic:
22
28
 
23
29
  When semantic mapping is enabled, `aiw scan` additionally:
24
30
 
25
- 1. Reads the scanned files with strict UTF-8 decoding (unreadable files are skipped).
31
+ 1. Reads the scanned files with strict UTF-8 decoding (unreadable files are skipped). Lockfiles and generated vendor manifests (`package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`, `go.sum`, `Cargo.lock`, `poetry.lock`, `Gemfile.lock`, `composer.lock`, `deno.lock`, `bun.lock`, `bun.lockb`, `uv.lock`, `Pipfile.lock`, `flake.lock`, `npm-shrinkwrap.json`) are skipped by default because they contribute noise chunks without retrieval value; set `excludeLockfiles: false` to index them explicitly.
26
32
  2. Splits each file into line-aligned chunks of at most `chunkChars` characters.
27
- 3. Embeds each chunk with `POST /api/embeddings`.
33
+ 3. Embeds chunks in batches of up to 64 per request with `POST /api/embed` (Ollama batch embeddings endpoint) instead of one request per chunk.
28
34
  4. Writes the versioned index to `.aiw/semantic/index.json` (`schema: 1`).
29
35
 
30
36
  The index stores the model name; queries against an index built with a different model fail explicitly.
31
37
 
38
+ ## Usage visibility
39
+
40
+ Every `aiw scan` reports the models it actually used, for example:
41
+
42
+ ```text
43
+ Project profile generated.
44
+ Models used:
45
+ - embedding: embeddinggemma-2:latest — indexed 824 chunks in 13 batch requests (~45,000 tokens)
46
+ - interpreter: gpt-oss:20b — 5 facts from 12 files (~3,800 tokens)
47
+ ```
48
+
49
+ Token estimates are also recorded per stage in `.aiw/usage.yml` (`scan.embedding`, `scan.interpreter`; the latest scan overwrites its stage entry) and can be inspected with `aiw token-usage`. Scans that use no models report `Models used: none (deterministic scan).`
50
+
32
51
  ## Queries during the flow
33
52
 
34
- `aiw semantic query --text="<natural language>"` ranks indexed chunks by cosine similarity and returns the top `topK` matches with file, score, and excerpt. `aiw semantic status` reports whether mapping is configured and whether the index is built.
53
+ `aiw semantic query --text="<natural language>" [--rerank]` ranks indexed chunks by cosine similarity and returns the top `topK` matches with file, score, and excerpt. With `--rerank`, and only when a decision model is also enabled and available, the decision model additionally scores each retrieved candidate 0–1 for query relevance and the output is reordered by that advisory score; matches without a decision score keep their cosine order. The command reports the ranking influence (how many positions were reordered) and the per-match `rerank` score; cosine recall itself is unchanged and reranking never creates confirmed facts. `aiw semantic status` reports whether mapping is configured and whether the index is built.
35
54
 
36
55
  ## Boundaries
37
56
 
38
57
  - Semantic results are retrieval aids, not evidence. They never create confirmed project facts and never bypass human approval gates.
39
58
  - The index is derived state; delete and rebuild it with `aiw scan` after structural changes.
59
+
60
+ ## Interpreter scoping
61
+
62
+ When semantic mapping is enabled and a valid index built with the active model exists, `aiw scan` scopes the AI interpreter's context semantically instead of alphabetically:
63
+
64
+ 1. A deterministic set of key files (README, contributing/architecture docs, and primary package manifests such as `package.json`, `pyproject.toml`, `go.mod`, `Cargo.toml`) is always included first, in a fixed priority order.
65
+ 2. The remaining context is the top semantically relevant files for a deterministic analysis query derived from the detected profile (languages, frameworks, package manager), ranked through the existing embedding index.
66
+ 3. Everything else is excluded, so measured token usage decreases while inferred-fact evidence still points at real scoped files.
67
+
68
+ Without semantic mapping (or when the index is missing, built with another model, or the embedding model is unavailable), the interpreter keeps the previous deterministic alphabetical scoping and scans make no network request for scoping purposes.
package/docs/telemetry.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  AI Workflow telemetry is disabled by default. No event is recorded unless a user explicitly runs `aiw telemetry enable` and a host application provides a telemetry client.
4
4
 
5
+ ## Installation consent
6
+
7
+ During an interactive `aiw install`, after the optional model questions, the installer asks whether to enable anonymous telemetry with an explicit default of **No**. The question states the exact event payload: the command name and its outcome only — never arguments, file paths, or file contents. The answer is recorded in `.aiw/settings.yml` under the `telemetry` section (declining is recorded too, so the question is never repeated), and the decision is reversible at any time with `aiw telemetry enable` / `aiw telemetry disable`. Non-interactive installs skip the prompt entirely and leave the section absent, and the prompt itself never emits a telemetry event.
8
+
5
9
  ## Controls
6
10
 
7
11
  ```bash
@@ -15,6 +19,14 @@ Preferences are stored in the local user settings file `.aiw/settings.yml` under
15
19
 
16
20
  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
21
 
22
+ ## Local buffer
23
+
24
+ When telemetry is enabled but no host application provides a telemetry client, events accumulate in a rotated local buffer at `.aiw/telemetry-buffer.jsonl` (one JSON event per line, retention cap of 200 events — the oldest events are dropped first). This introduces no network transport: the buffer is written only to the local `.aiw/` directory.
25
+
26
+ - `aiw telemetry status --verbose` displays the number of buffered events, the retention cap, and each buffered event verbatim.
27
+ - `aiw telemetry disable` purges the buffer.
28
+ - When a host client is present, events go to the client and the local buffer stays untouched.
29
+
18
30
  ## Data contract
19
31
 
20
32
  An event contains only:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@multileaf/ai-workflow",
3
- "version": "0.4.1",
3
+ "version": "0.6.0",
4
4
  "description": "Portable spec-driven development workflow for AI coding agents",
5
5
  "license": "MIT",
6
6
  "repository": {