kibi-mcp 0.12.0 → 0.14.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.
@@ -62,7 +62,7 @@ export const PROMPTS = [
62
62
  "Call `kb_autopilot_generate` with the gathered context to synthesize candidate entities.",
63
63
  "",
64
64
  "This tool is **read-only**. It returns additive `structuredContent` with:",
65
- "- `promptBlock`: preview text for the user-facing approval prompt",
65
+ "- `promptBlock`: review text that can be surfaced in optional human-facing briefs",
66
66
  "- `recommendedActions`: agent-facing next steps, including any REQ/SCEN/TEST authoring routed for manual handling",
67
67
  "- `declaredContext`: the user-provided bootstrap context",
68
68
  "- `confidence`: confidence summary for the generated output",
@@ -70,19 +70,19 @@ export const PROMPTS = [
70
70
  "- `candidates`: synthesized entities grounded in declared context and source evidence",
71
71
  "- `discoverySummary`: source-backed discovery notes",
72
72
  "",
73
- "## Step 3: Preview and Approval",
73
+ "## Step 3: Optional Human Review",
74
74
  "",
75
- "Present the `promptBlock` and a summary of `candidates` to the user. **Wait for explicit approval** before proceeding to writes.",
75
+ "Surface the `promptBlock` and a summary of `candidates` when optional human review is useful. Human review is post-hoc/optional via VS Code briefs and must not block writes.",
76
76
  "",
77
- "## Step 4: Apply Approved Candidates",
77
+ "## Step 4: Apply Candidates",
78
78
  "",
79
- "Apply approved candidates sequentially using `kb_upsert`.",
80
- "1. Execute each approved candidate's `applyPlan` in ascending phase order.",
79
+ "Apply candidates sequentially using `kb_upsert`.",
80
+ "1. Execute each candidate's `applyPlan` in ascending phase order.",
81
81
  "2. Confirm success of each `kb_upsert` before moving to the next.",
82
82
  "3. Run `kb_check` after the batch to verify KB integrity.",
83
83
  "",
84
84
  "## Rules",
85
- "- Never apply changes without a user-facing preview and approval.",
85
+ "- Human review is optional and post-hoc via VS Code briefs; do not gate writes on synchronous sign-off.",
86
86
  "- `kb_autopilot_generate` is strictly read-only; synthesis is the backend, not the actor.",
87
87
  "- Guidance must stay MCP-only; do not suggest `kibi` CLI commands.",
88
88
  ].join("\n"),
@@ -135,7 +135,10 @@ export const PROMPTS = [
135
135
  "Core modeling principles:",
136
136
  "- Kibi has eight entity types: common authoring (req, scenario, test, fact) and supporting/system (adr, flag, event, symbol).",
137
137
  "- Encode requirements as linked facts: `req --constrains--> fact` plus `req --requires_property--> fact`.",
138
- "- Only strict domain facts (`fact_kind: subject` + `property_value`) participate in contradiction inference; observation and meta facts are non-blocking notes.",
138
+ "- High-confidence modeling (>= 0.7) is fully automated; optional human review can happen post-hoc.",
139
+ "- Low-confidence claims (< 0.7) are downgraded to `observation` facts to prevent false-positive contradictions.",
140
+ "- Only strict domain facts participate in contradiction inference; observation and meta facts are non-blocking notes.",
141
+ "- v1 contradictions are limited to exact-value, boolean/enum, numeric range, and polarity conflicts.",
139
142
  "- Use `kb_search` first for discovery, then `kb_query` for exact follow-up before any mutation.",
140
143
  "- Use `kb_upsert` and `kb_delete` only for intentional, traceable KB changes.",
141
144
  "- Run `kb_check` after meaningful mutations to catch integrity issues early.",
@@ -152,9 +155,10 @@ export const PROMPTS = [
152
155
  "Follow this sequence for reliable operation:",
153
156
  "",
154
157
  "1. **Discover first**: Call `kb_search` for exploratory discovery, then `kb_query` to confirm exact current state before mutation.",
155
- "2. **Create-before-link**: Create endpoint entities with `kb_upsert` before linking them.",
156
- "3. **Validate intent**: If creating links, call `kb_query` for both endpoint IDs first to ensure they exist.",
157
- "4. **Model requirements as facts**: For new/updated reqs, create/reuse fact entities first, then express req semantics with `constrains` + `requires_property`.",
158
+ "2. **Check schema status**: Call `kb_status` to see if a schema migration is required for the branch KB.",
159
+ "3. **Create-before-link**: Create endpoint entities with `kb_upsert` before linking them.",
160
+ "4. **Validate intent**: If creating links, call `kb_query` for both endpoint IDs first to ensure they exist.",
161
+ "5. **Model requirements as facts**: For new/updated reqs, create/reuse fact entities first, then express req semantics with `constrains` + `requires_property` (automated via `kb_model_requirement`).",
158
162
  "5. **Mutate**: Call `kb_upsert` for create/update, or `kb_delete` for explicit removals.",
159
163
  "6. **Targeted checks**: Run `kb_check` after meaningful mutations; specify only the rules you need.",
160
164
  "",
@@ -11,6 +11,7 @@ import { handleKbQuery } from "../tools/query.js";
11
11
  import { handleKbSearch } from "../tools/search.js";
12
12
  import { handleKbStatus } from "../tools/status.js";
13
13
  import { handleKbUpsert } from "../tools/upsert.js";
14
+ import { handleKbModelRequirement, } from "../tools/model-requirement.js";
14
15
  import { handleKbAutopilotGenerate, } from "../tools/autopilot-generate.js";
15
16
  import { handleKbBriefingGenerate, } from "../tools/briefing-generate.js";
16
17
  const defaultToolsServerDeps = {
@@ -59,6 +60,7 @@ const DEFAULT_TOOLS_RUNTIME = {
59
60
  handleKbSearch,
60
61
  handleKbStatus,
61
62
  handleKbUpsert,
63
+ handleKbModelRequirement,
62
64
  handleKbAutopilotGenerate,
63
65
  handleKbBriefingGenerate,
64
66
  };
@@ -321,6 +323,10 @@ runtime = DEFAULT_TOOLS_RUNTIME) {
321
323
  const prolog = await runtime.ensureProlog();
322
324
  return runtime.handleKbCheck(prolog, args);
323
325
  }, runtime);
326
+ addTool(server, "kb_model_requirement", toolDef("kb_model_requirement").description, toolDef("kb_model_requirement").inputSchema, async (args) => {
327
+ const prolog = await runtime.ensureProlog();
328
+ return runtime.handleKbModelRequirement(prolog, args);
329
+ }, runtime);
324
330
  addTool(server, "kb_autopilot_generate", toolDef("kb_autopilot_generate").description, toolDef("kb_autopilot_generate").inputSchema, async (args) => {
325
331
  const prolog = await runtime.ensureProlog();
326
332
  return runtime.handleKbAutopilotGenerate(prolog, args);
@@ -2,8 +2,11 @@
2
2
  // Implements candidate assembly from public CLI extractors
3
3
  import { extractFromManifest } from "kibi-cli/extractors/manifest";
4
4
  import { extractFromMarkdown } from "kibi-cli/extractors/markdown";
5
+ import { buildStrictWriteSet, modelRequirementClaims, } from "kibi-cli/public/check-types";
5
6
  import path from "node:path";
6
7
  import fs from "node:fs";
8
+ import { createRepoIgnorePolicy } from "kibi-cli/ignore-policy";
9
+ import { estimateNormativeSignalConfidence, extractRequirementClaim, strictWriteSetToApplyPlan, writeSetPrimaryEntityId, } from "./model-requirement.js";
7
10
  function slugify(value, maxLength = 80) {
8
11
  return value
9
12
  .toLowerCase()
@@ -63,10 +66,8 @@ function resolveCandidatePaths(filePath, workspaceRoot) {
63
66
  .join("/");
64
67
  return { absolutePath, relativePath };
65
68
  }
66
- function isIgnoredGenericMarkdownPath(relativePath) {
67
- const normalized = relativePath.replace(/\\/g, "/").replace(/^\.\//, "");
68
- return /(^|\/)(documentation|\.kb|\.git|node_modules|vendor|vendors|third_party|third-party|dist|coverage)(\/|$)/.test(normalized);
69
- }
69
+ // Legacy helper removed in favor of the shared ignore policy from kibi-cli/ignore-policy.
70
+ // Use createRepoIgnorePolicy(workspaceRoot).isIgnored(relativePath) in builders.
70
71
  function shouldIncludeGenericMarkdown(relativePath, providerScopedMarkdown) {
71
72
  const base = path.basename(relativePath).toLowerCase();
72
73
  const inDocsDir = /(^|\/)docs\//.test(relativePath);
@@ -103,6 +104,7 @@ function buildUpsertFromExtraction(er, typeOverride) {
103
104
  export function buildTypedMarkdownCandidates(discoveryResult, existingEntities) {
104
105
  const candidates = [];
105
106
  const workspaceRoot = existingEntities.workspaceRoot ?? process.cwd();
107
+ const ignorePolicy = createRepoIgnorePolicy(workspaceRoot);
106
108
  for (const filePath of getTypedMarkdownFiles(discoveryResult)) {
107
109
  try {
108
110
  const extraction = extractFromMarkdown(filePath);
@@ -110,6 +112,8 @@ export function buildTypedMarkdownCandidates(discoveryResult, existingEntities)
110
112
  if (existingEntities.ids.has(entity.id))
111
113
  continue;
112
114
  const { absolutePath, relativePath } = resolveCandidatePaths(filePath, workspaceRoot);
115
+ if (ignorePolicy.isIgnored(relativePath))
116
+ continue;
113
117
  const candidateId = `md:${relativePath}:${entity.id}`;
114
118
  const upsert = buildUpsertFromExtraction({ entity, relationships });
115
119
  candidates.push({
@@ -186,13 +190,14 @@ export function buildSymbolManifestCandidates(discoveryResult, existingEntities)
186
190
  export function buildGenericMarkdownCandidates(discoveryResult, existingEntities, minConfidence = 0.8) {
187
191
  const candidates = [];
188
192
  const workspaceRoot = existingEntities.workspaceRoot ?? process.cwd();
193
+ const ignorePolicy = createRepoIgnorePolicy(workspaceRoot);
189
194
  const providerScopedMarkdown = hasGenericMarkdownEvidence(discoveryResult);
190
195
  const files = getGenericMarkdownFiles(discoveryResult);
191
196
  for (const rawPath of files) {
192
197
  try {
193
198
  const filePath = String(rawPath);
194
199
  const { absolutePath, relativePath } = resolveCandidatePaths(filePath, workspaceRoot);
195
- if (isIgnoredGenericMarkdownPath(relativePath))
200
+ if (ignorePolicy.isIgnored(relativePath))
196
201
  continue;
197
202
  // Legacy path-only discovery was conservative. Provider-scoped discovery
198
203
  // already filters eligible generic docs, so allow broader repo markdown there.
@@ -287,12 +292,13 @@ export function collectSourceOnlyAuthoringSignals(discoveryResult, existingEntit
287
292
  const signals = [];
288
293
  const seen = new Set();
289
294
  const workspaceRoot = existingEntities.workspaceRoot ?? process.cwd();
295
+ const ignorePolicy = createRepoIgnorePolicy(workspaceRoot);
290
296
  const providerScopedMarkdown = hasGenericMarkdownEvidence(discoveryResult);
291
297
  for (const rawPath of getGenericMarkdownFiles(discoveryResult)) {
292
298
  try {
293
299
  const filePath = String(rawPath);
294
300
  const { absolutePath, relativePath } = resolveCandidatePaths(filePath, workspaceRoot);
295
- if (isIgnoredGenericMarkdownPath(relativePath))
301
+ if (ignorePolicy.isIgnored(relativePath))
296
302
  continue;
297
303
  if (!shouldIncludeGenericMarkdown(relativePath, providerScopedMarkdown))
298
304
  continue;
@@ -370,6 +376,113 @@ export function collectSourceOnlyAuthoringSignals(discoveryResult, existingEntit
370
376
  });
371
377
  }
372
378
  // implements REQ-mcp-init-kibi-autopilot-v1
379
+ export function buildNormativeRequirementCandidates(discoveryResult, existingEntities, minConfidence = 0.8) {
380
+ const candidates = [];
381
+ const seeds = [];
382
+ const workspaceRoot = existingEntities.workspaceRoot ?? process.cwd();
383
+ const providerScopedMarkdown = hasGenericMarkdownEvidence(discoveryResult);
384
+ const ignorePolicy = createRepoIgnorePolicy(workspaceRoot);
385
+ for (const rawPath of getGenericMarkdownFiles(discoveryResult)) {
386
+ try {
387
+ const filePath = String(rawPath);
388
+ const { absolutePath, relativePath } = resolveCandidatePaths(filePath, workspaceRoot);
389
+ if (ignorePolicy.isIgnored(relativePath))
390
+ continue;
391
+ if (!shouldIncludeGenericMarkdown(relativePath, providerScopedMarkdown))
392
+ continue;
393
+ if (!fs.existsSync(absolutePath))
394
+ continue;
395
+ const content = fs.readFileSync(absolutePath, "utf8");
396
+ const lines = content.split(/\r?\n/);
397
+ let activeHeading;
398
+ let activeHeadingLine;
399
+ let inCodeFence = false;
400
+ for (let i = 0; i < lines.length; i++) {
401
+ const line = lines[i];
402
+ if (line === undefined)
403
+ continue;
404
+ if (/^\s*(```|~~~)/.test(line)) {
405
+ inCodeFence = !inCodeFence;
406
+ continue;
407
+ }
408
+ if (inCodeFence)
409
+ continue;
410
+ const headingMatch = line.match(/^\s*#+\s*(.+)$/);
411
+ if (headingMatch?.[1]) {
412
+ activeHeading = headingMatch[1].trim();
413
+ activeHeadingLine = i + 1;
414
+ continue;
415
+ }
416
+ const statement = line
417
+ .replace(/^\s*[-*+]\s+/, "")
418
+ .replace(/^\s*\d+[.)]\s+/, "")
419
+ .trim();
420
+ if (!statement || !/\b(must|shall|should)\b/i.test(statement))
421
+ continue;
422
+ const confidence = estimateNormativeSignalConfidence(statement, activeHeading);
423
+ if (confidence < minConfidence)
424
+ continue;
425
+ const extracted = extractRequirementClaim({
426
+ text: statement,
427
+ source: relativePath,
428
+ confidence,
429
+ provenance: `${relativePath}#L${i + 1}`,
430
+ });
431
+ const writeSet = buildStrictWriteSet({
432
+ claim: extracted.claim,
433
+ statement: extracted.statement,
434
+ });
435
+ if (!writeSet.isStrict)
436
+ continue;
437
+ seeds.push({
438
+ input: {
439
+ claim: extracted.claim,
440
+ statement: extracted.statement,
441
+ },
442
+ writeSet,
443
+ sourcePath: absolutePath,
444
+ evidence: [
445
+ `normative_statement:${relativePath}#L${i + 1}`,
446
+ ...(activeHeading && activeHeadingLine
447
+ ? [`generic_heading:${relativePath}#L${activeHeadingLine}`]
448
+ : []),
449
+ ],
450
+ });
451
+ }
452
+ }
453
+ catch {
454
+ // ignore unreadable files when deriving strict requirement candidates
455
+ }
456
+ }
457
+ const modeledIds = new Set(modelRequirementClaims(seeds.map((seed) => seed.input)).map((writeSet) => writeSetPrimaryEntityId(writeSet)));
458
+ const emittedIds = new Set();
459
+ for (const seed of seeds) {
460
+ const entityId = writeSetPrimaryEntityId(seed.writeSet);
461
+ if (!modeledIds.has(entityId) || emittedIds.has(entityId))
462
+ continue;
463
+ if (existingEntities.ids.has(entityId))
464
+ continue;
465
+ emittedIds.add(entityId);
466
+ candidates.push({
467
+ candidateId: `norm:${entityId.toLowerCase()}`,
468
+ entityType: "req",
469
+ title: seed.input.statement,
470
+ sourceKind: "generic_markdown",
471
+ sourcePath: seed.sourcePath,
472
+ confidence: seed.writeSet.confidence,
473
+ confidenceBand: toConfidenceBand(seed.writeSet.confidence),
474
+ evidence: seed.evidence,
475
+ relationships: seed.writeSet.relationships.map((relationship) => ({
476
+ type: relationship.type,
477
+ from: relationship.from,
478
+ to: relationship.to,
479
+ })),
480
+ applyPlan: strictWriteSetToApplyPlan(seed.writeSet),
481
+ });
482
+ }
483
+ return candidates;
484
+ }
485
+ // implements REQ-mcp-init-kibi-autopilot-v1
373
486
  export function buildProviderEvidenceCandidates(discoveryResult, existingEntities, minConfidence = 0.8) {
374
487
  const candidates = [];
375
488
  const workspaceRoot = existingEntities.workspaceRoot ?? process.cwd();
@@ -431,5 +544,6 @@ export default {
431
544
  buildSymbolManifestCandidates,
432
545
  buildGenericMarkdownCandidates,
433
546
  collectSourceOnlyAuthoringSignals,
547
+ buildNormativeRequirementCandidates,
434
548
  buildProviderEvidenceCandidates,
435
549
  };
@@ -7,6 +7,7 @@
7
7
  import fs from "node:fs";
8
8
  import path from "node:path";
9
9
  import fg from "fast-glob";
10
+ import { createRepoIgnorePolicy } from "kibi-cli/ignore-policy";
10
11
  import * as cliSymbolCoordinator from "kibi-cli/extractors/symbols-coordinator";
11
12
  import { runJsonModuleQuery } from "./core-module.js";
12
13
  // implements REQ-001
@@ -211,7 +212,7 @@ function normalizeDiscoveryPaths(cwd) {
211
212
  symbols: readPath("symbols"),
212
213
  };
213
214
  }
214
- function buildIgnoredGlobs(vendoredRoots) {
215
+ function buildIgnoredGlobs(vendoredRoots, workspaceRoot) {
215
216
  const ignored = new Set();
216
217
  for (const dirName of IGNORED_DIRECTORY_NAMES) {
217
218
  ignored.add(`**/${dirName}`);
@@ -226,6 +227,18 @@ function buildIgnoredGlobs(vendoredRoots) {
226
227
  ignored.add(`**/${normalized}`);
227
228
  ignored.add(`**/${normalized}/**`);
228
229
  }
230
+ // Use shared ignore policy to include .gitignore, nested ignores, and other rules.
231
+ try {
232
+ const policy = createRepoIgnorePolicy(workspaceRoot);
233
+ const globs = policy.getFastGlobIgnoreGlobs();
234
+ for (const glob of globs) {
235
+ if (glob)
236
+ ignored.add(glob);
237
+ }
238
+ }
239
+ catch {
240
+ // best-effort only
241
+ }
229
242
  return Array.from(ignored);
230
243
  }
231
244
  function detectLanguagesFromPaths(paths) {
@@ -266,6 +279,7 @@ function runTypedKibiDocsProvider(workspaceRoot) {
266
279
  onlyFiles: true,
267
280
  unique: true,
268
281
  suppressErrors: true,
282
+ ignore: buildIgnoredGlobs([], workspaceRoot),
269
283
  });
270
284
  const manifestFiles = discoveryPaths.symbols
271
285
  ? fg.sync(discoveryPaths.symbols, {
@@ -274,6 +288,7 @@ function runTypedKibiDocsProvider(workspaceRoot) {
274
288
  onlyFiles: true,
275
289
  unique: true,
276
290
  suppressErrors: true,
291
+ ignore: buildIgnoredGlobs([], workspaceRoot),
277
292
  })
278
293
  : [];
279
294
  const evidence = [
@@ -292,7 +307,7 @@ function runGenericRepoDocsProvider(workspaceRoot, vendoredRoots, typedFilePaths
292
307
  onlyFiles: true,
293
308
  unique: true,
294
309
  suppressErrors: true,
295
- ignore: buildIgnoredGlobs(vendoredRoots),
310
+ ignore: buildIgnoredGlobs(vendoredRoots, workspaceRoot),
296
311
  });
297
312
  const evidence = sortUnique(markdownFiles)
298
313
  .map((absolutePath) => createFileEvidence("generic_repo_docs", "generic_markdown", workspaceRoot, absolutePath))
@@ -435,7 +450,7 @@ function runRepoLayoutProvider(workspaceRoot, vendoredRoots) {
435
450
  onlyFiles: true,
436
451
  unique: true,
437
452
  suppressErrors: true,
438
- ignore: buildIgnoredGlobs(vendoredRoots),
453
+ ignore: buildIgnoredGlobs(vendoredRoots, workspaceRoot),
439
454
  });
440
455
  return {
441
456
  provider: "repo_layout",
@@ -468,7 +483,7 @@ function runTestTopologyProvider(workspaceRoot, vendoredRoots) {
468
483
  onlyFiles: true,
469
484
  unique: true,
470
485
  suppressErrors: true,
471
- ignore: buildIgnoredGlobs(vendoredRoots),
486
+ ignore: buildIgnoredGlobs(vendoredRoots, workspaceRoot),
472
487
  });
473
488
  const detectedFrameworks = new Set();
474
489
  const detectedLanguages = new Set();
@@ -525,7 +540,7 @@ function runSourceSymbolsProvider(workspaceRoot, vendoredRoots) {
525
540
  onlyFiles: true,
526
541
  unique: true,
527
542
  suppressErrors: true,
528
- ignore: buildIgnoredGlobs(vendoredRoots),
543
+ ignore: buildIgnoredGlobs(vendoredRoots, workspaceRoot),
529
544
  });
530
545
  const evidence = [];
531
546
  const detectedLanguages = new Set();
@@ -1,5 +1,8 @@
1
1
  import path from "node:path";
2
- import { collectSourceOnlyAuthoringSignals, buildGenericMarkdownCandidates, buildProviderEvidenceCandidates, buildTypedMarkdownCandidates, buildSymbolManifestCandidates, } from "./autopilot-candidates.js";
2
+ import fg from "fast-glob";
3
+ import { createRepoIgnorePolicy } from "kibi-cli/ignore-policy";
4
+ import { buildNormativeRequirementCandidates, collectSourceOnlyAuthoringSignals, buildGenericMarkdownCandidates, buildProviderEvidenceCandidates, buildTypedMarkdownCandidates, buildSymbolManifestCandidates, } from "./autopilot-candidates.js";
5
+ import { getWorkspaceMigrationWarning } from "./model-requirement.js";
3
6
  import { discoverProviderEvidence, resolveActivationPolicy, } from "./autopilot-discovery.js";
4
7
  import { loadEntities } from "./entity-query.js";
5
8
  import { resolveWorkspaceRoot } from "../workspace.js";
@@ -373,6 +376,7 @@ _prolog, args) {
373
376
  const activation = await resolveActivationPolicy(workspaceRoot, prolog);
374
377
  const activationState = activation.activationState;
375
378
  const activationDiscovery = discoverProviderEvidence(workspaceRoot, activation);
379
+ const migrationWarning = await getWorkspaceMigrationWarning(workspaceRoot);
376
380
  const declaredContext = normalizeBootstrapContext(bootstrapContext);
377
381
  const discoveredCandidatePaths = activationDiscovery.evidence.reduce((acc, item) => {
378
382
  const relativePath = item.relativePath;
@@ -405,6 +409,7 @@ _prolog, args) {
405
409
  let typedMarkdownCandidates = [];
406
410
  let manifestCandidates = [];
407
411
  let genericCandidates = [];
412
+ let normativeRequirementCandidates = [];
408
413
  let providerEvidenceCandidates = [];
409
414
  let allCandidates = [];
410
415
  const seenByKey = new Map();
@@ -427,6 +432,10 @@ _prolog, args) {
427
432
  ids: existingIds,
428
433
  workspaceRoot,
429
434
  }, normalizedMinConfidence);
435
+ normativeRequirementCandidates = buildNormativeRequirementCandidates(candidateDiscovery, {
436
+ ids: existingIds,
437
+ workspaceRoot,
438
+ }, normalizedMinConfidence);
430
439
  }
431
440
  providerEvidenceCandidates = buildProviderEvidenceCandidates(candidateDiscovery, {
432
441
  ids: existingIds,
@@ -436,6 +445,7 @@ _prolog, args) {
436
445
  ...typedMarkdownCandidates,
437
446
  ...manifestCandidates,
438
447
  ...genericCandidates,
448
+ ...normativeRequirementCandidates,
439
449
  ...providerEvidenceCandidates,
440
450
  ];
441
451
  if (entityTypes && entityTypes.length > 0) {
@@ -516,6 +526,40 @@ _prolog, args) {
516
526
  seenByKey.set(titleKey, record);
517
527
  }
518
528
  }
529
+ // Detect repository files that would be candidate inputs but are ignored by
530
+ // the repo ignore policy (e.g. .sisyphus drafts, .gitignore entries). Add
531
+ // them to suppressedCandidates with reason `ignored_source` so callers see
532
+ // why those files were omitted from candidate output.
533
+ try {
534
+ const repoIgnore = createRepoIgnorePolicy(workspaceRoot);
535
+ const potentialFiles = fg.sync(["**/*.md", "**/symbols.{yml,yaml}"], {
536
+ cwd: workspaceRoot,
537
+ absolute: true,
538
+ onlyFiles: true,
539
+ unique: true,
540
+ dot: true,
541
+ suppressErrors: true,
542
+ });
543
+ for (const absPath of potentialFiles) {
544
+ const rel = toWorkspaceRelativePath(workspaceRoot, absPath);
545
+ const explain = repoIgnore.explain(rel);
546
+ if (explain.ignored) {
547
+ // avoid duplicating existing suppressed entries for the same source
548
+ if (!suppressed.some((s) => String(s.sourcePath ?? "") === rel && s.reason === "ignored_source")) {
549
+ suppressed.push({
550
+ candidateId: String("") /* no candidate id for ignored source */,
551
+ reason: "ignored_source",
552
+ sourcePath: rel,
553
+ entityType: String("") /* unknown at this stage */,
554
+ detail: explain.reason,
555
+ });
556
+ }
557
+ }
558
+ }
559
+ }
560
+ catch {
561
+ // best-effort only; ignore failures here so generation can continue
562
+ }
519
563
  const candidateRecords = Array.from(seenByKey.values());
520
564
  const payoffSummary = buildPayoffSummary(candidateRecords);
521
565
  const promptBlock = buildPromptBlock(workspaceRoot, activationState, activation.activationMode, activation.reason, activation.applyBlocked, declaredContext, candidateRecords, sourceOnlySignals, activationDiscovery.summary.scanWarnings);
@@ -533,6 +577,7 @@ _prolog, args) {
533
577
  bootstrapMode: activation.activationMode,
534
578
  activationReason: activation.reason,
535
579
  applyBlocked: effectiveApplyBlocked,
580
+ migrationWarning,
536
581
  ...(activation.handoffMessage
537
582
  ? { handoffMessage: activation.handoffMessage }
538
583
  : {}),
@@ -554,6 +599,7 @@ _prolog, args) {
554
599
  },
555
600
  ],
556
601
  structuredContent,
602
+ migrationWarning,
557
603
  candidates: candidateRecords,
558
604
  suppressedCandidates: suppressed,
559
605
  payoffSummary,