@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/CHANGELOG.md +29 -0
- package/README.md +20 -3
- package/dist/decision.d.ts +15 -1
- package/dist/decision.js +43 -6
- package/dist/interpreter.d.ts +17 -1
- package/dist/interpreter.js +116 -6
- package/dist/model-onboarding.d.ts +22 -0
- package/dist/model-onboarding.js +200 -0
- package/dist/recommendations.d.ts +16 -1
- package/dist/recommendations.js +33 -2
- package/dist/semantic.d.ts +19 -1
- package/dist/semantic.js +111 -11
- package/dist/services.d.ts +6 -1
- package/dist/telemetry.d.ts +8 -0
- package/dist/telemetry.js +62 -2
- package/dist/workflow.d.ts +0 -2
- package/dist/workflow.js +183 -59
- package/docs/README.md +5 -0
- package/docs/backlog.md +23 -0
- package/docs/decision-model-evaluation.md +2 -2
- package/docs/project-intelligence.md +13 -1
- package/docs/release-process.md +1 -1
- package/docs/semantic-mapping.md +33 -4
- package/docs/telemetry.md +12 -0
- package/package.json +1 -1
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 {
|
|
11
|
-
import {
|
|
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
|
|
24
|
-
import { buildEvidenceTriageRequest,
|
|
25
|
-
import { buildSemanticIndex, createOllamaEmbeddingProvider,
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
672
|
+
output: ranked.length === 0
|
|
555
673
|
? "No semantic matches."
|
|
556
|
-
:
|
|
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(
|
|
1079
|
+
selection = await selectRecommendations(ordered, (question) => terminal.question(question));
|
|
935
1080
|
terminal.close();
|
|
936
1081
|
}
|
|
937
|
-
fs.write(join(aiw, "recommendations.yml"), serializeRecommendations(
|
|
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
|
-
?
|
|
942
|
-
:
|
|
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.
|
|
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`.
|
package/docs/release-process.md
CHANGED
|
@@ -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.
|
package/docs/semantic-mapping.md
CHANGED
|
@@ -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.
|
|
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
|
|
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:
|