@tomingtoming/kioq 0.8.3 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,130 @@
1
+ const HUMAN_SIGNALS = new Set([
2
+ "human_cited",
3
+ "human_discussed",
4
+ "human_applied",
5
+ "human_questioned",
6
+ "human_dismissed",
7
+ ]);
8
+ const SIGNAL_STRENGTH = {
9
+ search_hit: 1,
10
+ context_pulled: 2,
11
+ read: 3,
12
+ orient_discovered: 4,
13
+ orient_referenced: 5,
14
+ modified: 6,
15
+ human_questioned: 7,
16
+ human_cited: 8,
17
+ human_discussed: 9,
18
+ human_dismissed: 5,
19
+ human_applied: 10,
20
+ };
21
+ export class EngagementIndex {
22
+ facetsMap = new Map();
23
+ build(signals, notes) {
24
+ this.facetsMap.clear();
25
+ const notesByPermalink = new Map();
26
+ for (const note of notes) {
27
+ notesByPermalink.set(note.permalink, note);
28
+ }
29
+ // Initialize facets for all notes
30
+ for (const note of notes) {
31
+ this.facetsMap.set(note.permalink, this.emptyFacets(note.frontmatter));
32
+ }
33
+ // Process signal log
34
+ for (const signal of signals) {
35
+ const facets = this.facetsMap.get(signal.note);
36
+ if (!facets)
37
+ continue;
38
+ facets.contactCount++;
39
+ if (!facets.lastContact || signal.ts > facets.lastContact) {
40
+ facets.lastContact = signal.ts;
41
+ }
42
+ if (HUMAN_SIGNALS.has(signal.op)) {
43
+ facets.humanSignalCount++;
44
+ if (!facets.lastHumanSignal || signal.ts > facets.lastHumanSignal) {
45
+ facets.lastHumanSignal = signal.ts;
46
+ }
47
+ }
48
+ if (!facets.dominantSignal || SIGNAL_STRENGTH[signal.op] > SIGNAL_STRENGTH[facets.dominantSignal]) {
49
+ facets.dominantSignal = signal.op;
50
+ }
51
+ }
52
+ // Compute ai_reference_count and synthesis_count from note content
53
+ const wikiLinkPattern = /\[\[([^\]\n]+)\]\]/g;
54
+ for (const note of notes) {
55
+ const origin = safeString(note.frontmatter.origin);
56
+ if (origin !== "ai" && origin !== "collaborative")
57
+ continue;
58
+ const links = new Set();
59
+ let match;
60
+ while ((match = wikiLinkPattern.exec(note.body)) !== null) {
61
+ const target = match[1].split("|")[0].trim();
62
+ links.add(target);
63
+ }
64
+ wikiLinkPattern.lastIndex = 0;
65
+ for (const target of links) {
66
+ // Try to resolve the target to a permalink
67
+ const resolved = this.resolveTarget(target, notes, notesByPermalink);
68
+ if (resolved) {
69
+ const targetFacets = this.facetsMap.get(resolved);
70
+ if (targetFacets) {
71
+ targetFacets.aiReferenceCount++;
72
+ }
73
+ }
74
+ }
75
+ // Check if this note is a synthesis (has parent or source links)
76
+ const parentMatch = /^-\s*(?:親|parent):\s*\[\[([^\]]+)\]\]/im.exec(note.body);
77
+ if (parentMatch && (origin === "ai" || origin === "collaborative")) {
78
+ const parentTarget = parentMatch[1].split("|")[0].trim();
79
+ const resolved = this.resolveTarget(parentTarget, notes, notesByPermalink);
80
+ if (resolved) {
81
+ const parentFacets = this.facetsMap.get(resolved);
82
+ if (parentFacets) {
83
+ parentFacets.synthesisCount++;
84
+ }
85
+ }
86
+ }
87
+ }
88
+ }
89
+ get(permalink) {
90
+ return this.facetsMap.get(permalink);
91
+ }
92
+ getAll() {
93
+ return this.facetsMap;
94
+ }
95
+ resolveTarget(target, notes, byPermalink) {
96
+ // Direct permalink match
97
+ if (byPermalink.has(target))
98
+ return target;
99
+ // Title match (case-insensitive)
100
+ const lowerTarget = target.toLowerCase();
101
+ for (const note of notes) {
102
+ if (note.title.toLowerCase() === lowerTarget)
103
+ return note.permalink;
104
+ if (note.permalink.toLowerCase() === lowerTarget)
105
+ return note.permalink;
106
+ const basename = note.permalink.split("/").pop()?.toLowerCase();
107
+ if (basename === lowerTarget)
108
+ return note.permalink;
109
+ }
110
+ return undefined;
111
+ }
112
+ emptyFacets(frontmatter) {
113
+ return {
114
+ contactCount: 0,
115
+ lastContact: null,
116
+ humanSignalCount: 0,
117
+ lastHumanSignal: null,
118
+ dominantSignal: null,
119
+ aiReferenceCount: 0,
120
+ synthesisCount: 0,
121
+ origin: safeString(frontmatter.origin),
122
+ createdVia: safeString(frontmatter.created_via),
123
+ };
124
+ }
125
+ }
126
+ function safeString(value) {
127
+ if (typeof value === "string" && value.length > 0)
128
+ return value;
129
+ return null;
130
+ }
package/dist/src/index.js CHANGED
@@ -3,13 +3,14 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
3
3
  import { z } from "zod";
4
4
  import { lintStructureResponseModeHint, lintStructureResponsePlan, summarizeLintStructureResponse, } from "./auditResponseSummary.js";
5
5
  import { loadConfig } from "./config.js";
6
- import { memoryContractResponseModeHint, memoryContractResponsePlan, summarizeMemoryContractResponse, } from "./contractResponseSummary.js";
7
6
  import { summarizeNoConflict } from "./conflictSummary.js";
8
7
  import { LocalNoteStore } from "./noteStore.js";
9
8
  import { GitHubStorage, LocalFsStorage } from "./storage/index.js";
9
+ import { SignalLog } from "./signalLog.js";
10
+ import { EngagementIndex } from "./engagement.js";
10
11
  import { normalizeDateInput, preview } from "./normalizers.js";
11
12
  import { summarizeDeleteImpact, summarizeRenameImpact } from "./operationImpact.js";
12
- import { summarizeBacklinksResponse, summarizeContextBundleResponse, summarizeReadNoteResponse, summarizeRecentResponse, summarizeResolveLinksResponse, summarizeSearchResponse, } from "./readResponseSummary.js";
13
+ import { summarizeReadNoteResponse, summarizeSearchResponse, } from "./readResponseSummary.js";
13
14
  import { summarizeLintChangeTrigger, summarizeReadChangeTrigger } from "./changeTriggerSummary.js";
14
15
  import { RESPONSE_MODE_VALUES, normalizeResponseMode, responseModeAtLeast } from "./responseMode.js";
15
16
  import { TOOL_SCOPE_LABELS, responsibilityWarningCode } from "./toolScope.js";
@@ -117,7 +118,10 @@ function appendScopeLabel(lines, scopeLabel) {
117
118
  lines.push(`scope_label: ${scopeLabel}`);
118
119
  }
119
120
  function appendResponsibilityWarning(lines, warnings) {
120
- lines.push(`responsibility_warning: ${responsibilityWarningCode(warnings)}`);
121
+ const code = responsibilityWarningCode(warnings);
122
+ if (code === "none")
123
+ return;
124
+ lines.push(`responsibility_warning: ${code}`);
121
125
  }
122
126
  function appendTemplateHints(lines, hints) {
123
127
  lines.push(`template_hint_primary: ${hints.primary}`);
@@ -262,10 +266,9 @@ function appendAuditResponseSummary(lines, summary) {
262
266
  }
263
267
  }
264
268
  function appendChangeTriggerSummary(lines, summary) {
265
- lines.push(`change_trigger: ${summary.code}`);
266
- if (summary.code === "none") {
269
+ if (summary.code === "none")
267
270
  return;
268
- }
271
+ lines.push(`change_trigger: ${summary.code}`);
269
272
  lines.push(`trigger_confidence: ${summary.confidence}`);
270
273
  lines.push(`cascade_targets: ${summary.cascadeTargets?.join(", ") ?? "(none)"}`);
271
274
  lines.push(`noise_risk: ${summary.noiseRisk}`);
@@ -276,9 +279,7 @@ function appendOperationImpact(lines, impact) {
276
279
  lines.push(`impact_reasons: ${impact.reasons.join(", ")}`);
277
280
  }
278
281
  function appendConflictSummary(lines, summary) {
279
- lines.push(`conflict_type: ${summary.conflictType}`);
280
282
  lines.push(`server_updated: ${summary.serverUpdated}`);
281
- lines.push(`retry_hint: ${summary.retryHint}`);
282
283
  }
283
284
  const responseModeSchema = z.enum(RESPONSE_MODE_VALUES).optional().describe("default: standard");
284
285
  async function main() {
@@ -288,7 +289,13 @@ async function main() {
288
289
  : new LocalFsStorage(config.root);
289
290
  await storage.ensureRoot();
290
291
  const store = new LocalNoteStore(config, storage);
292
+ const signalLog = new SignalLog(storage);
293
+ const engagementIndex = new EngagementIndex();
294
+ let storageContextEmitted = false;
291
295
  const appendStorageContext = (lines) => {
296
+ if (storageContextEmitted)
297
+ return;
298
+ storageContextEmitted = true;
292
299
  lines.push(`storage_backend: ${config.github ? "github" : "filesystem"}`);
293
300
  lines.push(`storage_root: ${config.root}`);
294
301
  if (config.github) {
@@ -296,72 +303,99 @@ async function main() {
296
303
  lines.push(`storage_branch: ${config.github.branch}`);
297
304
  }
298
305
  };
306
+ /** Returns storage context lines only on first call, empty array after. */
307
+ const storageContextLines = () => {
308
+ if (storageContextEmitted)
309
+ return [];
310
+ storageContextEmitted = true;
311
+ return [
312
+ `storage_backend: ${config.github ? "github" : "filesystem"}`,
313
+ `storage_root: ${config.root}`,
314
+ ...(config.github
315
+ ? [`storage_repo: ${config.github.owner}/${config.github.repo}`, `storage_branch: ${config.github.branch}`]
316
+ : []),
317
+ ];
318
+ };
299
319
  const appendAutoProjectHealth = async (lines) => {
300
320
  try {
301
321
  const health = await store.lintStructure({
302
322
  limit: 3,
303
323
  });
324
+ const hasAttention = health.unresolvedWikiLinkCount > 0
325
+ || health.duplicateWarningCount > 0
326
+ || health.technicalDebtSignals.dependencyDirectionViolationCount > 0
327
+ || health.orphanNoteCount > 0;
328
+ if (!hasAttention)
329
+ return;
304
330
  appendProjectHealthSummary(lines, health);
305
331
  }
306
- catch (error) {
307
- lines.push("");
308
- lines.push(`project_health: unavailable (${normalizeError(error)})`);
332
+ catch {
333
+ // silently skip if lint fails
309
334
  }
310
335
  };
311
336
  const server = new McpServer({
312
337
  name: "kioq",
313
338
  version: "0.7.0",
314
339
  });
315
- server.tool("recent_notes", "最近更新したノートを一覧します。", {
316
- limit: z.number().int().min(1).max(50).optional().describe("最大件数 (default: 10)"),
317
- directory: z.string().min(1).optional().describe("指定時は配下ディレクトリに限定"),
318
- response_mode: responseModeSchema,
319
- }, async ({ limit, directory, response_mode }) => {
320
- const responseMode = normalizeResponseMode(response_mode);
321
- const result = await store.recentNotes({
322
- limit: limit ?? 10,
323
- directory,
324
- });
325
- const firstIndexLike = result.results.find((item) => item.indexLike);
326
- const summary = summarizeRecentResponse({
327
- resultCount: result.results.length,
328
- firstResultTitle: result.results[0]?.title,
329
- hasIndexLikeResult: Boolean(firstIndexLike),
330
- firstIndexLikeTitle: firstIndexLike?.title,
331
- indexCandidateCount: result.indexNoteCandidates.length,
332
- firstIndexCandidateTitle: result.indexNoteCandidates[0]?.title,
340
+ server.tool("orient", "kioqを通じて思考を構造化するツール。チェインバー型パイロット支援システムとして、応答を組み立てる前に現在の思考を渡すことで、3つのスロットを返します: (1) relevant — 思考に直接関連するノート(engagement重み付け)、(2) active_context — 進行中のflowと最近のアクティブなノート、(3) discovery — 未接触だが接点がありそうなノート(セレンディピティ)。各ノートのengagement(接触回数、人間の反応、AI被参照数、origin)が付与されます。observationで人間の反応を記録でき、それがengagementに蓄積されます。対話の各ターンで呼び出すことを推奨します。", {
341
+ thought: z.string().min(1).describe("今何を考えているか、何について応答しようとしているか"),
342
+ references: z.array(z.string()).optional().describe("明示的に関連するノートの identifier(あれば)"),
343
+ observation: z.object({
344
+ note: z.string().min(1).describe("観察対象のノート identifier"),
345
+ signal: z.enum(["cited", "discussed", "applied", "questioned", "dismissed"]).describe("人間の反応の種類"),
346
+ }).optional().describe("直前の対話で人間がノートの概念に反応した場合に記録"),
347
+ }, async ({ thought, references, observation }) => {
348
+ const signals = await signalLog.load();
349
+ const notes = await store.loadAllNotes();
350
+ engagementIndex.build(signals, notes);
351
+ const result = await store.orient({
352
+ thought,
353
+ references,
354
+ observation,
355
+ engagementIndex,
356
+ signalLog,
333
357
  });
358
+ await signalLog.flush();
334
359
  const lines = [];
335
- lines.push(`対象プロジェクト: ${result.project}`);
360
+ lines.push(`project: ${result.project}`);
336
361
  appendStorageContext(lines);
337
- appendScopeLabel(lines, TOOL_SCOPE_LABELS.recent_notes);
338
- lines.push(`response_mode: ${responseMode}`);
339
- appendReadResponseSummary(lines, summary);
340
- lines.push(`件数: ${result.results.length}`);
362
+ lines.push(`scope_label: orient`);
363
+ lines.push(`thought: ${thought.slice(0, 200)}`);
341
364
  lines.push("");
342
- if (result.results.length === 0) {
343
- lines.push("候補が見つかりませんでした。");
365
+ if (result.relevant.length > 0) {
366
+ lines.push("--- relevant ---");
367
+ for (const item of result.relevant) {
368
+ lines.push(`- ${item.title}`);
369
+ lines.push(` permalink: ${item.permalink}`);
370
+ lines.push(` reason: ${item.reason}`);
371
+ lines.push(` engagement: contact=${item.engagement.contactCount} human=${item.engagement.humanSignalCount} ai_ref=${item.engagement.aiReferenceCount} origin=${item.engagement.origin ?? "unknown"} created_via=${item.engagement.createdVia ?? "unknown"}`);
372
+ lines.push(` snippet: ${item.snippet.slice(0, 200)}`);
373
+ }
374
+ lines.push("");
344
375
  }
345
- else {
346
- result.results.forEach((item, index) => {
347
- lines.push(`${index + 1}. ${item.title}`);
348
- lines.push(` updated: ${item.updated}`);
349
- if (responseModeAtLeast(responseMode, "standard")) {
350
- lines.push(` permalink: ${item.permalink}`);
351
- lines.push(` file: ${item.filePath}`);
352
- lines.push(` index_like: ${item.indexLike ? "yes" : "no"}`);
353
- }
354
- if (responseModeAtLeast(responseMode, "verbose")) {
355
- lines.push(` index_like_reasons: ${item.indexLike && item.indexReasons.length > 0 ? item.indexReasons.join(", ") : "(none)"}`);
356
- }
357
- });
376
+ if (result.activeContext.length > 0) {
377
+ lines.push("--- active_context ---");
378
+ for (const item of result.activeContext) {
379
+ lines.push(`- ${item.title}`);
380
+ lines.push(` permalink: ${item.permalink}`);
381
+ lines.push(` reason: ${item.reason}`);
382
+ lines.push(` engagement: contact=${item.engagement.contactCount} human=${item.engagement.humanSignalCount} origin=${item.engagement.origin ?? "unknown"}`);
383
+ }
384
+ lines.push("");
358
385
  }
359
- if (responseModeAtLeast(responseMode, "verbose")) {
386
+ if (result.discovery.length > 0) {
387
+ lines.push("--- discovery ---");
388
+ for (const item of result.discovery) {
389
+ lines.push(`- ${item.title}`);
390
+ lines.push(` permalink: ${item.permalink}`);
391
+ lines.push(` reason: ${item.reason}`);
392
+ lines.push(` engagement: contact=${item.engagement.contactCount} origin=${item.engagement.origin ?? "unknown"} created_via=${item.engagement.createdVia ?? "unknown"}`);
393
+ lines.push(` snippet: ${item.snippet.slice(0, 160)}`);
394
+ }
360
395
  lines.push("");
361
- appendIndexNoteCandidates(lines, result.indexNoteCandidates, "recommended_indexes");
362
396
  }
363
- if (responseModeAtLeast(responseMode, "verbose")) {
364
- await appendAutoProjectHealth(lines);
397
+ if (result.relevant.length === 0 && result.discovery.length === 0) {
398
+ lines.push("関連するノートは見つかりませんでした。");
365
399
  }
366
400
  return textResult(lines.join("\n"));
367
401
  });
@@ -411,11 +445,13 @@ async function main() {
411
445
  if (responseModeAtLeast(responseMode, "standard")) {
412
446
  lines.push(` permalink: ${item.permalink}`);
413
447
  lines.push(` file: ${item.filePath}`);
414
- lines.push(` exact_identifier_match: ${item.exactIdentifierMatch ? "yes" : "no"}`);
415
- lines.push(` index_like: ${item.indexLike ? "yes" : "no"}`);
448
+ if (item.exactIdentifierMatch)
449
+ lines.push(` exact_identifier_match: yes`);
450
+ if (item.indexLike)
451
+ lines.push(` index_like: yes`);
416
452
  }
417
- if (responseModeAtLeast(responseMode, "verbose")) {
418
- lines.push(` index_like_reasons: ${item.indexLike && item.indexReasons.length > 0 ? item.indexReasons.join(", ") : "(none)"}`);
453
+ if (responseModeAtLeast(responseMode, "verbose") && item.indexLike && item.indexReasons.length > 0) {
454
+ lines.push(` index_like_reasons: ${item.indexReasons.join(", ")}`);
419
455
  }
420
456
  });
421
457
  }
@@ -454,8 +490,7 @@ async function main() {
454
490
  const lines = [
455
491
  `identifier: ${identifier}`,
456
492
  `project: ${result.project}`,
457
- `storage_backend: ${config.github ? "github" : "filesystem"}`,
458
- `storage_root: ${config.root}`,
493
+ ...storageContextLines(),
459
494
  `scope_label: ${TOOL_SCOPE_LABELS.read_note}`,
460
495
  `response_mode: ${responseMode}`,
461
496
  `primary_navigation_signal: ${summary.primaryNavigationSignal}`,
@@ -464,9 +499,6 @@ async function main() {
464
499
  `file: ${result.note.relativePath}`,
465
500
  `permalink: ${result.note.permalink}`,
466
501
  ];
467
- if (config.github) {
468
- lines.splice(4, 0, `storage_repo: ${config.github.owner}/${config.github.repo}`, `storage_branch: ${config.github.branch}`);
469
- }
470
502
  if (summary.nextRecommendedTarget) {
471
503
  insertBeforeLine(lines, "next_recommended_action:", `next_recommended_target: ${summary.nextRecommendedTarget}`);
472
504
  }
@@ -476,7 +508,8 @@ async function main() {
476
508
  if (responseModeAtLeast(responseMode, "standard")) {
477
509
  lines.push(`link_health: ${result.linkHealth.resolved}/${result.linkHealth.total} resolved`);
478
510
  lines.push(`backlinks: ${result.backlinkCount}`);
479
- lines.push(`orphan_warning: ${result.orphanWarning ? "yes" : "no"}`);
511
+ if (result.orphanWarning)
512
+ lines.push(`orphan_warning: yes`);
480
513
  appendDocumentClarity(lines, result.documentClarity, responseModeAtLeast(responseMode, "verbose"));
481
514
  appendUnresolvedWikiLinks(lines, result.unresolvedWikiLinks);
482
515
  }
@@ -516,285 +549,14 @@ async function main() {
516
549
  lines.push(` score: ${item.score}`);
517
550
  lines.push(` permalink: ${item.permalink}`);
518
551
  lines.push(` file: ${item.filePath}`);
519
- lines.push(` exact_identifier_match: ${item.exactIdentifierMatch ? "yes" : "no"}`);
552
+ if (item.exactIdentifierMatch)
553
+ lines.push(` exact_identifier_match: yes`);
520
554
  });
521
555
  }
522
556
  return textResult(lines.join("\n"));
523
557
  }
524
558
  });
525
- server.tool("resolve_links", "ノート内の WikiLink を解決し、リンク先の存在状況を返します。", {
526
- identifier: z.string().min(1),
527
- response_mode: responseModeSchema,
528
- }, async ({ identifier, response_mode }) => {
529
- const responseMode = normalizeResponseMode(response_mode);
530
- const result = await store.resolveLinks({
531
- identifier,
532
- });
533
- const unresolvedLinkCount = result.links.filter((link) => !link.resolved).length;
534
- const summary = summarizeResolveLinksResponse({
535
- indexLike: result.indexLike,
536
- unresolvedLinkCount,
537
- boundaryWarningCount: result.boundaryWarnings.length,
538
- indexCandidateTitle: result.indexNoteCandidates[0]?.title,
539
- });
540
- const lines = [];
541
- lines.push(`identifier: ${identifier}`);
542
- lines.push(`project: ${result.project}`);
543
- appendStorageContext(lines);
544
- appendScopeLabel(lines, TOOL_SCOPE_LABELS.resolve_links);
545
- lines.push(`response_mode: ${responseMode}`);
546
- appendReadResponseSummary(lines, summary);
547
- appendResponsibilityWarning(lines, result.boundaryWarnings);
548
- lines.push(`file: ${result.note.relativePath}`);
549
- lines.push(`wikilinks: ${result.links.length}`);
550
- if (responseModeAtLeast(responseMode, "verbose")) {
551
- appendIndexLike(lines, result.indexLike, result.indexReasons);
552
- }
553
- appendBoundaryWarnings(lines, result.boundaryWarnings);
554
- lines.push("");
555
- if (result.links.length === 0) {
556
- lines.push("WikiLink は見つかりませんでした。");
557
- }
558
- else {
559
- result.links.forEach((link, index) => {
560
- lines.push(`${index + 1}. ${link.raw}`);
561
- if (link.resolved) {
562
- lines.push(` resolved: yes`);
563
- lines.push(` to: ${link.resolvedTitle} (${link.resolvedFilePath})`);
564
- }
565
- else {
566
- lines.push(` resolved: no`);
567
- lines.push(` reason: ${link.reason ?? "unknown"}`);
568
- lines.push(` target: ${link.target}`);
569
- }
570
- });
571
- }
572
- if (responseModeAtLeast(responseMode, "verbose")) {
573
- lines.push("");
574
- appendIndexNoteCandidates(lines, result.indexNoteCandidates);
575
- }
576
- return textResult(lines.join("\n"));
577
- });
578
- server.tool("list_backlinks", "指定ノートへのバックリンク一覧を返します。", {
579
- identifier: z.string().min(1),
580
- limit: z.number().int().min(1).max(100).optional().describe("最大件数 (default: 20)"),
581
- response_mode: responseModeSchema,
582
- }, async ({ identifier, limit, response_mode }) => {
583
- const responseMode = normalizeResponseMode(response_mode);
584
- const result = await store.listBacklinks({
585
- identifier,
586
- limit: limit ?? 20,
587
- });
588
- const summary = summarizeBacklinksResponse({
589
- indexLike: result.indexLike,
590
- backlinkCount: result.backlinks.length,
591
- boundaryWarningCount: result.boundaryWarnings.length,
592
- firstBacklinkTitle: result.backlinks[0]?.title,
593
- indexCandidateTitle: result.indexNoteCandidates[0]?.title,
594
- });
595
- const lines = [];
596
- lines.push(`target: ${result.target.title}`);
597
- lines.push(`project: ${result.project}`);
598
- appendStorageContext(lines);
599
- appendScopeLabel(lines, TOOL_SCOPE_LABELS.list_backlinks);
600
- lines.push(`response_mode: ${responseMode}`);
601
- appendReadResponseSummary(lines, summary);
602
- appendResponsibilityWarning(lines, result.boundaryWarnings);
603
- if (responseModeAtLeast(responseMode, "verbose")) {
604
- appendIndexLike(lines, result.indexLike, result.indexReasons, "target");
605
- }
606
- appendBoundaryWarnings(lines, result.boundaryWarnings);
607
- lines.push(`backlinks: ${result.backlinks.length}`);
608
- lines.push("");
609
- if (result.backlinks.length === 0) {
610
- lines.push("バックリンクは見つかりませんでした。");
611
- }
612
- else {
613
- result.backlinks.forEach((item, index) => {
614
- lines.push(`${index + 1}. ${item.title}`);
615
- lines.push(` count: ${item.count}`);
616
- if (responseModeAtLeast(responseMode, "standard")) {
617
- lines.push(` file: ${item.filePath}`);
618
- lines.push(` updated: ${item.updated}`);
619
- }
620
- item.examples.forEach((example) => {
621
- lines.push(` example: ${example}`);
622
- });
623
- });
624
- }
625
- if (responseModeAtLeast(responseMode, "verbose")) {
626
- lines.push("");
627
- appendIndexNoteCandidates(lines, result.indexNoteCandidates);
628
- }
629
- return textResult(lines.join("\n"));
630
- });
631
- server.tool("context_bundle", "指定ノートを中心に、関連ノートを優先度付きで返します。", {
632
- identifier: z.string().min(1),
633
- limit: z.number().int().min(1).max(30).optional().describe("最大件数 (default: 8)"),
634
- response_mode: responseModeSchema,
635
- }, async ({ identifier, limit, response_mode }) => {
636
- const responseMode = normalizeResponseMode(response_mode);
637
- const result = await store.contextBundle({
638
- identifier,
639
- limit: limit ?? 8,
640
- });
641
- const summary = summarizeContextBundleResponse({
642
- sourceIndexLike: result.source.indexLike,
643
- bundleItemCount: result.items.length,
644
- boundaryWarningCount: result.source.boundaryWarnings.length,
645
- firstBundleItemTitle: result.items[0]?.title,
646
- indexCandidateTitle: result.indexNoteCandidates[0]?.title,
647
- });
648
- const lines = [];
649
- lines.push(`source: ${result.source.title}`);
650
- lines.push(`project: ${result.project}`);
651
- appendStorageContext(lines);
652
- appendScopeLabel(lines, TOOL_SCOPE_LABELS.context_bundle);
653
- lines.push(`response_mode: ${responseMode}`);
654
- appendReadResponseSummary(lines, summary);
655
- appendResponsibilityWarning(lines, result.source.boundaryWarnings);
656
- if (responseModeAtLeast(responseMode, "standard")) {
657
- lines.push(`memory_kind: ${result.source.memoryKind}`);
658
- lines.push(`file: ${result.source.filePath}`);
659
- }
660
- if (responseModeAtLeast(responseMode, "verbose")) {
661
- appendIndexLike(lines, result.source.indexLike, result.source.indexReasons, "source");
662
- }
663
- appendBoundaryWarnings(lines, result.source.boundaryWarnings);
664
- lines.push(`bundle_items: ${result.items.length}`);
665
- lines.push("");
666
- if (result.items.length === 0) {
667
- lines.push("関連ノートは見つかりませんでした。");
668
- }
669
- else {
670
- result.items.forEach((item, index) => {
671
- lines.push(`${index + 1}. ${item.title}`);
672
- lines.push(` score: ${item.score}`);
673
- if (responseModeAtLeast(responseMode, "standard")) {
674
- lines.push(` memory_kind: ${item.memoryKind}`);
675
- lines.push(` file: ${item.filePath}`);
676
- }
677
- if (responseModeAtLeast(responseMode, "verbose")) {
678
- lines.push(` reasons: ${item.reasons.join(", ")}`);
679
- }
680
- });
681
- }
682
- if (responseModeAtLeast(responseMode, "verbose")) {
683
- lines.push("");
684
- appendIndexNoteCandidates(lines, result.indexNoteCandidates);
685
- }
686
- if (responseModeAtLeast(responseMode, "standard")) {
687
- appendExplorationGuidance(lines, result.explorationGuidance);
688
- }
689
- return textResult(lines.join("\n"));
690
- });
691
- server.tool("memory_contract", "kioq の Memory Contract(構造化ルール)を返します。", {
692
- response_mode: responseModeSchema,
693
- }, async ({ response_mode }) => {
694
- const responseMode = normalizeResponseMode(response_mode);
695
- const plan = memoryContractResponsePlan(responseMode);
696
- const summary = {
697
- ...summarizeMemoryContractResponse(),
698
- ...memoryContractResponseModeHint(responseMode),
699
- };
700
- const contract = store.getMemoryContract();
701
- const lines = [];
702
- appendStorageContext(lines);
703
- appendScopeLabel(lines, TOOL_SCOPE_LABELS.memory_contract);
704
- lines.push(`response_mode: ${responseMode}`);
705
- appendAuditResponseSummary(lines, summary);
706
- lines.push(`memory_contract_version: ${contract.version}`);
707
- lines.push(`required_frontmatter: ${contract.requiredFrontmatter.join(", ")}`);
708
- lines.push(`flow_required_frontmatter: ${contract.flowRequiredFrontmatter.join(", ")}`);
709
- lines.push(`parent_markers: ${contract.parentLinkMarkers.join(", ")}`);
710
- lines.push(`source_metadata_canonical_field: ${contract.optionalSourceMetadata.canonicalField}`);
711
- lines.push(`source_metadata_auxiliary_fields: ${contract.optionalSourceMetadata.auxiliaryFields.join(", ")}`);
712
- lines.push(`index_threshold_score_at_least: ${contract.indexNavigationPolicy.thresholds.scoreAtLeast}`);
713
- lines.push(`index_threshold_index_sections_at_least: ${contract.indexNavigationPolicy.thresholds.indexSectionsAtLeast}`);
714
- lines.push(`technical_debt_threshold_stale_flow_days: ${contract.technicalDebtPolicy.thresholds.staleFlowDays}`);
715
- lines.push("sample_templates_available: stock, flow, index");
716
- if (!plan.includePolicyRules) {
717
- return textResult(lines.join("\n"));
718
- }
719
- lines.push("requirements:");
720
- lines.push(`- min_resolved_links: ${contract.requirements.minResolvedLinks}`);
721
- lines.push(`- min_backlinks: ${contract.requirements.minBacklinks}`);
722
- lines.push(`- min_parent_links: ${contract.requirements.minParentLinks}`);
723
- lines.push(`- min_related_links: ${contract.requirements.minRelatedLinks}`);
724
- lines.push(`- max_unresolved_links: ${contract.requirements.maxUnresolvedLinks}`);
725
- lines.push(`- min_tags: ${contract.requirements.minTags}`);
726
- lines.push("flow_requirements:");
727
- lines.push(`- min_resolved_links: ${contract.flowRequirements.minResolvedLinks}`);
728
- lines.push(`- min_backlinks: ${contract.flowRequirements.minBacklinks}`);
729
- lines.push(`- min_parent_links: ${contract.flowRequirements.minParentLinks}`);
730
- lines.push(`- min_related_links: ${contract.flowRequirements.minRelatedLinks}`);
731
- lines.push(`- max_unresolved_links: ${contract.flowRequirements.maxUnresolvedLinks}`);
732
- lines.push(`- min_tags: ${contract.flowRequirements.minTags}`);
733
- lines.push("optional_source_metadata:");
734
- lines.push(`- canonical_field: ${contract.optionalSourceMetadata.canonicalField}`);
735
- lines.push(`- auxiliary_fields: ${contract.optionalSourceMetadata.auxiliaryFields.join(", ")}`);
736
- contract.optionalSourceMetadata.rules.forEach((rule) => {
737
- lines.push(`- rule: ${rule}`);
738
- });
739
- lines.push("index_navigation_policy:");
740
- lines.push(`- excluded_memory_kinds: ${contract.indexNavigationPolicy.excludedMemoryKinds.join(", ")}`);
741
- lines.push(`- threshold_score_at_least: ${contract.indexNavigationPolicy.thresholds.scoreAtLeast}`);
742
- lines.push(`- threshold_index_sections_at_least: ${contract.indexNavigationPolicy.thresholds.indexSectionsAtLeast}`);
743
- lines.push(`- configurable_via_env: ${contract.indexNavigationPolicy.configurableVia.env.join(", ")}`);
744
- lines.push(`- configurable_via_cli: ${contract.indexNavigationPolicy.configurableVia.cli.join(", ")}`);
745
- if (plan.includePolicySignals) {
746
- contract.indexNavigationPolicy.signals.forEach((signal) => {
747
- lines.push(`- signal: ${signal.code} weight=${signal.weight} description=${signal.description}`);
748
- });
749
- }
750
- contract.indexNavigationPolicy.rules.forEach((rule) => {
751
- lines.push(`- rule: ${rule}`);
752
- });
753
- lines.push("technical_debt_policy:");
754
- lines.push(`- signals: ${contract.technicalDebtPolicy.signals.join(", ")}`);
755
- lines.push(`- stale_flow_states: ${contract.technicalDebtPolicy.staleFlowStates.join(", ")}`);
756
- lines.push(`- threshold_stale_flow_days: ${contract.technicalDebtPolicy.thresholds.staleFlowDays}`);
757
- lines.push(`- stale_flow_age_bucket_near_threshold: ${contract.technicalDebtPolicy.staleFlowAgeBuckets.nearThreshold}`);
758
- lines.push(`- stale_flow_age_bucket_aging: ${contract.technicalDebtPolicy.staleFlowAgeBuckets.aging}`);
759
- lines.push(`- stale_flow_age_bucket_long_stale: ${contract.technicalDebtPolicy.staleFlowAgeBuckets.longStale}`);
760
- lines.push(`- cleanup_ratio_definition: ${contract.technicalDebtPolicy.cleanupRatioDefinition}`);
761
- lines.push(`- cleanup_ready_count_definition: ${contract.technicalDebtPolicy.cleanupReadyCountDefinition}`);
762
- lines.push(`- attention_note_count_definition: ${contract.technicalDebtPolicy.attentionNoteCountDefinition}`);
763
- lines.push(`- dependency_direction_violation_count_definition: ${contract.technicalDebtPolicy.dependencyDirectionViolationCountDefinition}`);
764
- lines.push(`- single_use_tag_candidate_count_definition: ${contract.technicalDebtPolicy.singleUseTagCandidateCountDefinition}`);
765
- lines.push(`- title_body_mismatch_candidate_count_definition: ${contract.technicalDebtPolicy.titleBodyMismatchCandidateCountDefinition}`);
766
- lines.push(`- unresolved_wikilinks_delta_status: ${contract.technicalDebtPolicy.unresolvedWikilinksDeltaStatus}`);
767
- lines.push(`- configurable_via_env: ${contract.technicalDebtPolicy.configurableVia.env.join(", ")}`);
768
- lines.push(`- configurable_via_cli: ${contract.technicalDebtPolicy.configurableVia.cli.join(", ")}`);
769
- lines.push("response_mode_policy:");
770
- lines.push(`- supported_read_tools: ${contract.responseModePolicy.supportedTools.read.join(", ")}`);
771
- lines.push(`- supported_audit_tools: ${contract.responseModePolicy.supportedTools.audit.join(", ")}`);
772
- lines.push(`- minimal: ${contract.responseModePolicy.modes.minimal}`);
773
- lines.push(`- standard: ${contract.responseModePolicy.modes.standard}`);
774
- lines.push(`- verbose: ${contract.responseModePolicy.modes.verbose}`);
775
- contract.responseModePolicy.rules.forEach((rule) => {
776
- lines.push(`- rule: ${rule}`);
777
- });
778
- if (plan.includeSampleTemplates) {
779
- lines.push("");
780
- lines.push("sample_template_stock:");
781
- lines.push("```md");
782
- contract.sampleTemplates.stock.forEach((line) => lines.push(line));
783
- lines.push("```");
784
- lines.push("");
785
- lines.push("sample_template_flow:");
786
- lines.push("```md");
787
- contract.sampleTemplates.flow.forEach((line) => lines.push(line));
788
- lines.push("```");
789
- lines.push("");
790
- lines.push("sample_template_index:");
791
- lines.push("```md");
792
- contract.sampleTemplates.index.forEach((line) => lines.push(line));
793
- lines.push("```");
794
- }
795
- return textResult(lines.join("\n"));
796
- });
797
- server.tool("lint_structure", "構造化を促すために、ノート群の問題を診断して優先順位付きで返します。", {
559
+ server.tool("lint_structure", "ノート群の構造的問題を診断し、優先順位付きで返します。", {
798
560
  limit: z.number().int().min(1).max(200).optional().describe("返却する issue 最大件数 (default: 50)"),
799
561
  response_mode: responseModeSchema,
800
562
  }, async ({ limit, response_mode }) => {
@@ -914,19 +676,93 @@ async function main() {
914
676
  }
915
677
  return textResult(lines.join("\n"));
916
678
  });
917
- server.tool("write_note", "ローカル markdown ノートを作成/更新します。", {
679
+ server.tool("write_note", "ノートを作成/更新します。note_type='flow' で進行中の問いを記録、それ以外で知識(stock)を記録します。origin で誰が起点かを記録し、engagement追跡の基盤になります。", {
918
680
  title: z.string().min(1),
919
- content: z.string().min(1),
920
- directory: z.string().min(1).optional().describe("default: KIOQ_DEFAULT_DIRECTORY または Inbox"),
681
+ content: z.string().min(1).describe("note_type='flow' の場合は ## Notes セクションの本文"),
682
+ directory: z.string().min(1).optional().describe("default: stock→KIOQ_DEFAULT_DIRECTORY, flow→Flow"),
921
683
  tags: z.array(z.string()).optional(),
922
- note_type: z.string().optional().describe("例: note / decision / task"),
923
- }, async ({ title, content, directory, tags, note_type }) => {
684
+ note_type: z.string().optional().describe("stock(default) / flow / note / decision / task"),
685
+ origin: z.enum(["human", "ai", "collaborative"]).optional().describe("誰が起点か (human=体験, ai=AI生成/抽出, collaborative=対話から)"),
686
+ created_via: z.enum(["experience", "dialogue", "extraction", "curation", "capture"]).optional().describe("生成経路"),
687
+ human_role: z.enum(["thinker", "architect", "curator", "reviewer"]).optional().describe("collaborative 時の人間の役割"),
688
+ question: z.string().optional().describe("flow 時の問い (note_type='flow' で必須)"),
689
+ next_action: z.string().optional().describe("flow 時の次のアクション (note_type='flow' で必須)"),
690
+ parent_stock: z.string().optional().describe("flow 時の親 stock identifier (note_type='flow' で必須)"),
691
+ related: z.array(z.string()).optional().describe("flow 時の追加関連リンク先"),
692
+ flow_state: z.string().optional().describe("flow 時の状態: capture / active / blocked / done / promoted / dropped"),
693
+ }, async ({ title, content, directory, tags, note_type, origin, created_via, human_role, question, next_action, parent_stock, related, flow_state }) => {
694
+ // Flow note path
695
+ if (note_type === "flow") {
696
+ if (!question || !next_action || !parent_stock) {
697
+ return textResult("error: note_type='flow' requires question, next_action, parent_stock");
698
+ }
699
+ const flowResult = await store.writeFlowNote({
700
+ title,
701
+ question,
702
+ nextAction: next_action,
703
+ parentStock: parent_stock,
704
+ related,
705
+ details: content,
706
+ directory,
707
+ tags,
708
+ flowState: flow_state,
709
+ origin,
710
+ createdVia: created_via,
711
+ humanRole: human_role,
712
+ });
713
+ const flowSummary = summarizeWriteFlowResponse({
714
+ operation: flowResult.operation,
715
+ unresolvedWikiLinkCount: flowResult.unresolvedWikiLinks.length,
716
+ boundaryWarningCount: flowResult.boundaryWarnings.length,
717
+ memoryContractStatus: flowResult.memoryContract.status,
718
+ duplicateWarningCount: flowResult.duplicateWarnings.length,
719
+ orphanWarning: flowResult.orphanWarning,
720
+ title: flowResult.title,
721
+ parentStockResolved: flowResult.parentStockResolved,
722
+ parentStock: flowResult.parentStock,
723
+ });
724
+ const flowLines = [
725
+ "flow ノートを作成/更新しました。",
726
+ `project: ${flowResult.project}`,
727
+ `scope_label: ${TOOL_SCOPE_LABELS.write_flow_note}`,
728
+ `primary_navigation_signal: ${flowSummary.primaryNavigationSignal}`,
729
+ `primary_quality_signal: ${flowSummary.primaryQualitySignal}`,
730
+ `next_recommended_action: ${flowSummary.nextRecommendedAction}`,
731
+ `operation: ${flowResult.operation}`,
732
+ `file: ${flowResult.relativePath}`,
733
+ `permalink: ${flowResult.permalink}`,
734
+ `title: ${flowResult.title}`,
735
+ `flow_state: ${flowResult.flowState}`,
736
+ `parent_stock: ${flowResult.parentStock}`,
737
+ `parent_stock_resolved: ${flowResult.parentStockResolved ? "yes" : "no"}`,
738
+ `link_health: ${flowResult.linkHealth.resolved}/${flowResult.linkHealth.total} resolved`,
739
+ `backlinks: ${flowResult.backlinkCount}`,
740
+ ];
741
+ if (flowResult.orphanWarning)
742
+ flowLines.push(`orphan_warning: yes`);
743
+ flowLines.splice(2, 0, ...storageContextLines());
744
+ if (flowSummary.nextRecommendedTarget) {
745
+ insertBeforeLine(flowLines, "next_recommended_action:", `next_recommended_target: ${flowSummary.nextRecommendedTarget}`);
746
+ }
747
+ appendConflictSummary(flowLines, summarizeNoConflict({ serverUpdated: flowResult.serverUpdated }));
748
+ appendResponsibilityWarning(flowLines, flowResult.boundaryWarnings);
749
+ appendStructureScore(flowLines, flowResult.structureScore);
750
+ appendMemoryContract(flowLines, flowResult.memoryContract);
751
+ appendUnresolvedWikiLinks(flowLines, flowResult.unresolvedWikiLinks);
752
+ appendDuplicateWarnings(flowLines, flowResult.duplicateWarnings);
753
+ await appendAutoProjectHealth(flowLines);
754
+ return textResult(flowLines.join("\n"));
755
+ }
756
+ // Stock/generic note path
924
757
  const result = await store.writeNote({
925
758
  title,
926
759
  content,
927
760
  directory,
928
761
  tags,
929
762
  noteType: note_type,
763
+ origin,
764
+ createdVia: created_via,
765
+ humanRole: human_role,
930
766
  });
931
767
  const summary = summarizeWriteNoteResponse({
932
768
  operation: result.operation,
@@ -950,15 +786,8 @@ async function main() {
950
786
  `title: ${result.title}`,
951
787
  `link_health: ${result.linkHealth.resolved}/${result.linkHealth.total} resolved`,
952
788
  `backlinks: ${result.backlinkCount}`,
953
- `orphan_warning: ${result.orphanWarning ? "yes" : "no"}`,
954
789
  ];
955
- lines.splice(2, 0, ...[
956
- `storage_backend: ${config.github ? "github" : "filesystem"}`,
957
- `storage_root: ${config.root}`,
958
- ...(config.github
959
- ? [`storage_repo: ${config.github.owner}/${config.github.repo}`, `storage_branch: ${config.github.branch}`]
960
- : []),
961
- ]);
790
+ lines.splice(2, 0, ...storageContextLines());
962
791
  if (summary.nextRecommendedTarget) {
963
792
  insertBeforeLine(lines, "next_recommended_action:", `next_recommended_target: ${summary.nextRecommendedTarget}`);
964
793
  }
@@ -967,90 +796,14 @@ async function main() {
967
796
  appendStructureScore(lines, result.structureScore);
968
797
  appendMemoryContract(lines, result.memoryContract);
969
798
  appendBoundaryWarnings(lines, result.boundaryWarnings);
970
- appendTemplateHints(lines, {
971
- primary: "stock",
972
- alternatives: ["index"],
973
- reason: "通常ノートとして知識を残すときは stock template を基準にする",
974
- whenToSwitch: "再訪導線を固定したい入口ノートなら index template に切り替える",
975
- });
976
- appendUnresolvedWikiLinks(lines, result.unresolvedWikiLinks);
977
- appendUnresolvedLinkHints(lines, result.unresolvedLinkHints);
978
- appendDuplicateWarnings(lines, result.duplicateWarnings);
979
- await appendAutoProjectHealth(lines);
980
- return textResult(lines.join("\n"));
981
- });
982
- server.tool("write_flow_note", "flow ノートを構造テンプレート付きで作成/更新します。", {
983
- title: z.string().min(1),
984
- question: z.string().min(1),
985
- next_action: z.string().min(1),
986
- parent_stock: z.string().min(1),
987
- related: z.array(z.string()).optional().describe("追加の関連リンク先"),
988
- details: z.string().optional().describe("## Notes に入れる本文"),
989
- directory: z.string().min(1).optional().describe("default: Flow"),
990
- tags: z.array(z.string()).optional(),
991
- flow_state: z.string().optional().describe("capture / active / blocked / done / promoted / dropped"),
992
- }, async ({ title, question, next_action, parent_stock, related, details, directory, tags, flow_state }) => {
993
- const result = await store.writeFlowNote({
994
- title,
995
- question,
996
- nextAction: next_action,
997
- parentStock: parent_stock,
998
- related,
999
- details,
1000
- directory,
1001
- tags,
1002
- flowState: flow_state,
1003
- });
1004
- const summary = summarizeWriteFlowResponse({
1005
- operation: result.operation,
1006
- unresolvedWikiLinkCount: result.unresolvedWikiLinks.length,
1007
- boundaryWarningCount: result.boundaryWarnings.length,
1008
- memoryContractStatus: result.memoryContract.status,
1009
- duplicateWarningCount: result.duplicateWarnings.length,
1010
- orphanWarning: result.orphanWarning,
1011
- title: result.title,
1012
- parentStockResolved: result.parentStockResolved,
1013
- parentStock: result.parentStock,
1014
- });
1015
- const lines = [
1016
- "flow ノートを作成/更新しました。",
1017
- `project: ${result.project}`,
1018
- `scope_label: ${TOOL_SCOPE_LABELS.write_flow_note}`,
1019
- `primary_navigation_signal: ${summary.primaryNavigationSignal}`,
1020
- `primary_quality_signal: ${summary.primaryQualitySignal}`,
1021
- `next_recommended_action: ${summary.nextRecommendedAction}`,
1022
- `operation: ${result.operation}`,
1023
- `file: ${result.relativePath}`,
1024
- `permalink: ${result.permalink}`,
1025
- `title: ${result.title}`,
1026
- `flow_state: ${result.flowState}`,
1027
- `parent_stock: ${result.parentStock}`,
1028
- `parent_stock_resolved: ${result.parentStockResolved ? "yes" : "no"}`,
1029
- `link_health: ${result.linkHealth.resolved}/${result.linkHealth.total} resolved`,
1030
- `backlinks: ${result.backlinkCount}`,
1031
- `orphan_warning: ${result.orphanWarning ? "yes" : "no"}`,
1032
- ];
1033
- lines.splice(2, 0, ...[
1034
- `storage_backend: ${config.github ? "github" : "filesystem"}`,
1035
- `storage_root: ${config.root}`,
1036
- ...(config.github
1037
- ? [`storage_repo: ${config.github.owner}/${config.github.repo}`, `storage_branch: ${config.github.branch}`]
1038
- : []),
1039
- ]);
1040
- if (summary.nextRecommendedTarget) {
1041
- insertBeforeLine(lines, "next_recommended_action:", `next_recommended_target: ${summary.nextRecommendedTarget}`);
799
+ if (result.memoryContract.status === "fail") {
800
+ appendTemplateHints(lines, {
801
+ primary: "stock",
802
+ alternatives: ["index"],
803
+ reason: "通常ノートとして知識を残すときは stock template を基準にする",
804
+ whenToSwitch: "再訪導線を固定したい入口ノートなら index template に切り替える",
805
+ });
1042
806
  }
1043
- appendConflictSummary(lines, summarizeNoConflict({ serverUpdated: result.serverUpdated }));
1044
- appendResponsibilityWarning(lines, result.boundaryWarnings);
1045
- appendStructureScore(lines, result.structureScore);
1046
- appendMemoryContract(lines, result.memoryContract);
1047
- appendBoundaryWarnings(lines, result.boundaryWarnings);
1048
- appendTemplateHints(lines, {
1049
- primary: "flow",
1050
- alternatives: ["stock", "index"],
1051
- reason: "進行中の問いと next_action を残すときは flow template を基準にする",
1052
- whenToSwitch: "知識として固まったら stock、再訪入口を作るなら index template を使う",
1053
- });
1054
807
  appendUnresolvedWikiLinks(lines, result.unresolvedWikiLinks);
1055
808
  appendUnresolvedLinkHints(lines, result.unresolvedLinkHints);
1056
809
  appendDuplicateWarnings(lines, result.duplicateWarnings);
@@ -1090,13 +843,7 @@ async function main() {
1090
843
  `stock_permalink: ${result.stock.permalink}`,
1091
844
  `flow_state: ${result.flowState}`,
1092
845
  ];
1093
- lines.splice(2, 0, ...[
1094
- `storage_backend: ${config.github ? "github" : "filesystem"}`,
1095
- `storage_root: ${config.root}`,
1096
- ...(config.github
1097
- ? [`storage_repo: ${config.github.owner}/${config.github.repo}`, `storage_branch: ${config.github.branch}`]
1098
- : []),
1099
- ]);
846
+ lines.splice(2, 0, ...storageContextLines());
1100
847
  if (summary.nextRecommendedTarget) {
1101
848
  insertBeforeLine(lines, "next_recommended_action:", `next_recommended_target: ${summary.nextRecommendedTarget}`);
1102
849
  }
@@ -1147,15 +894,8 @@ async function main() {
1147
894
  `title: ${result.title}`,
1148
895
  `link_health: ${result.linkHealth.resolved}/${result.linkHealth.total} resolved`,
1149
896
  `backlinks: ${result.backlinkCount}`,
1150
- `orphan_warning: ${result.orphanWarning ? "yes" : "no"}`,
1151
897
  ];
1152
- lines.splice(2, 0, ...[
1153
- `storage_backend: ${config.github ? "github" : "filesystem"}`,
1154
- `storage_root: ${config.root}`,
1155
- ...(config.github
1156
- ? [`storage_repo: ${config.github.owner}/${config.github.repo}`, `storage_branch: ${config.github.branch}`]
1157
- : []),
1158
- ]);
898
+ lines.splice(2, 0, ...storageContextLines());
1159
899
  if (summary.nextRecommendedTarget) {
1160
900
  insertBeforeLine(lines, "next_recommended_action:", `next_recommended_target: ${summary.nextRecommendedTarget}`);
1161
901
  }
@@ -1164,7 +904,7 @@ async function main() {
1164
904
  appendStructureScore(lines, result.structureScore);
1165
905
  appendMemoryContract(lines, result.memoryContract);
1166
906
  appendBoundaryWarnings(lines, result.boundaryWarnings);
1167
- if (result.operation === "created") {
907
+ if (result.operation === "created" && result.memoryContract.status === "fail") {
1168
908
  appendTemplateHints(lines, {
1169
909
  primary: "stock",
1170
910
  alternatives: ["index"],
@@ -1210,13 +950,7 @@ async function main() {
1210
950
  `project_link_health_after: ${result.projectLinkHealthAfter.resolved}/${result.projectLinkHealthAfter.total}`,
1211
951
  `project_unresolved_delta: ${projectUnresolvedDelta}`,
1212
952
  ];
1213
- lines.splice(2, 0, ...[
1214
- `storage_backend: ${config.github ? "github" : "filesystem"}`,
1215
- `storage_root: ${config.root}`,
1216
- ...(config.github
1217
- ? [`storage_repo: ${config.github.owner}/${config.github.repo}`, `storage_branch: ${config.github.branch}`]
1218
- : []),
1219
- ]);
953
+ lines.splice(2, 0, ...storageContextLines());
1220
954
  if (summary.nextRecommendedTarget) {
1221
955
  insertBeforeLine(lines, "next_recommended_action:", `next_recommended_target: ${summary.nextRecommendedTarget}`);
1222
956
  }
@@ -1272,18 +1006,11 @@ async function main() {
1272
1006
  `files_updated: ${result.updatedFiles}`,
1273
1007
  `renamed_note_link_health: ${result.renamedNoteLinkHealth.resolved}/${result.renamedNoteLinkHealth.total}`,
1274
1008
  `backlinks: ${result.backlinkCount}`,
1275
- `orphan_warning: ${result.orphanWarning ? "yes" : "no"}`,
1276
1009
  `project_link_health_before: ${result.projectLinkHealthBefore.resolved}/${result.projectLinkHealthBefore.total}`,
1277
1010
  `project_link_health_after: ${result.projectLinkHealthAfter.resolved}/${result.projectLinkHealthAfter.total}`,
1278
1011
  `project_unresolved_delta: ${projectUnresolvedDelta}`,
1279
1012
  ];
1280
- lines.splice(2, 0, ...[
1281
- `storage_backend: ${config.github ? "github" : "filesystem"}`,
1282
- `storage_root: ${config.root}`,
1283
- ...(config.github
1284
- ? [`storage_repo: ${config.github.owner}/${config.github.repo}`, `storage_branch: ${config.github.branch}`]
1285
- : []),
1286
- ]);
1013
+ lines.splice(2, 0, ...storageContextLines());
1287
1014
  if (summary.nextRecommendedTarget) {
1288
1015
  insertBeforeLine(lines, "next_recommended_action:", `next_recommended_target: ${summary.nextRecommendedTarget}`);
1289
1016
  }
@@ -2048,6 +2048,9 @@ export class LocalNoteStore {
2048
2048
  unresolvedLinkHints,
2049
2049
  };
2050
2050
  }
2051
+ async loadAllNotes() {
2052
+ return this.loadProjectNotes();
2053
+ }
2051
2054
  async loadProjectNotes() {
2052
2055
  const files = await this.storage.listMarkdownFiles();
2053
2056
  return Promise.all(files.map((relativePath) => this.loadNote(relativePath)));
@@ -2814,6 +2817,143 @@ export class LocalNoteStore {
2814
2817
  explorationGuidance: this.contextExplorationGuidance(notes, sourceNote, rankedNotes),
2815
2818
  };
2816
2819
  }
2820
+ async orient(args) {
2821
+ const notes = await this.loadProjectNotes();
2822
+ const lookup = this.buildLookup(notes);
2823
+ const thought = args.thought.trim();
2824
+ // Record observation signal if present
2825
+ if (args.observation && args.signalLog) {
2826
+ const signalType = `human_${args.observation.signal}`;
2827
+ const resolved = this.resolveWikiTarget(args.observation.note, lookup);
2828
+ const noteId = resolved.status === "resolved" && resolved.note
2829
+ ? resolved.note.permalink
2830
+ : args.observation.note;
2831
+ args.signalLog.record(signalType, noteId, thought.slice(0, 120));
2832
+ }
2833
+ // Record orient references as signals
2834
+ if (args.references && args.signalLog) {
2835
+ for (const ref of args.references) {
2836
+ const resolved = this.resolveWikiTarget(ref, lookup);
2837
+ const noteId = resolved.status === "resolved" && resolved.note
2838
+ ? resolved.note.permalink
2839
+ : ref;
2840
+ args.signalLog.record("orient_referenced", noteId, thought.slice(0, 120));
2841
+ }
2842
+ }
2843
+ const toOrientItem = (note, reason) => {
2844
+ const engagement = args.engagementIndex?.get(note.permalink);
2845
+ const snippet = note.body.slice(0, this.config.previewLength).trim();
2846
+ return {
2847
+ identifier: note.title,
2848
+ title: note.title,
2849
+ permalink: note.permalink,
2850
+ snippet,
2851
+ reason,
2852
+ engagement: {
2853
+ contactCount: engagement?.contactCount ?? 0,
2854
+ lastContact: engagement?.lastContact ?? null,
2855
+ humanSignalCount: engagement?.humanSignalCount ?? 0,
2856
+ dominantSignal: engagement?.dominantSignal ?? null,
2857
+ aiReferenceCount: engagement?.aiReferenceCount ?? 0,
2858
+ origin: engagement?.origin ?? safeString(note.frontmatter.origin) ?? null,
2859
+ createdVia: engagement?.createdVia ?? safeString(note.frontmatter.created_via) ?? null,
2860
+ },
2861
+ };
2862
+ };
2863
+ // === Slot 1: relevant (search-based + engagement boost) ===
2864
+ const relevant = [];
2865
+ if (thought.length > 0) {
2866
+ for (const note of notes) {
2867
+ let score = this.noteScore(note, thought);
2868
+ if (score <= 0)
2869
+ continue;
2870
+ // Engagement boost
2871
+ const eng = args.engagementIndex?.get(note.permalink);
2872
+ if (eng) {
2873
+ if (eng.humanSignalCount > 0)
2874
+ score = Math.floor(score * 1.5);
2875
+ else if (eng.aiReferenceCount > 0)
2876
+ score = Math.floor(score * 1.2);
2877
+ else if (eng.contactCount === 0)
2878
+ score = Math.floor(score * 0.8);
2879
+ }
2880
+ relevant.push({ note, score, reason: "text_match" });
2881
+ }
2882
+ relevant.sort((a, b) => b.score - a.score);
2883
+ }
2884
+ // Record discovered notes as signals
2885
+ if (args.signalLog) {
2886
+ for (const item of relevant.slice(0, 5)) {
2887
+ args.signalLog.record("orient_discovered", item.note.permalink, thought.slice(0, 120));
2888
+ }
2889
+ }
2890
+ // === Slot 2: active_context (active flows + recent high-engagement) ===
2891
+ const activeContext = [];
2892
+ const relevantPermalinks = new Set(relevant.slice(0, 5).map((r) => r.note.permalink));
2893
+ for (const note of notes) {
2894
+ if (relevantPermalinks.has(note.permalink))
2895
+ continue;
2896
+ const flowState = safeString(note.frontmatter.flow_state);
2897
+ if (flowState === "active" || flowState === "capture") {
2898
+ activeContext.push({ note, reason: `active_flow (${flowState})` });
2899
+ continue;
2900
+ }
2901
+ const eng = args.engagementIndex?.get(note.permalink);
2902
+ if (eng && eng.contactCount >= 3 && eng.lastContact) {
2903
+ const daysSinceContact = Math.floor((Date.now() - new Date(eng.lastContact).getTime()) / (1000 * 60 * 60 * 24));
2904
+ if (daysSinceContact <= 7) {
2905
+ activeContext.push({ note, reason: `recent_high_engagement (${eng.contactCount} contacts)` });
2906
+ }
2907
+ }
2908
+ }
2909
+ // === Slot 3: discovery (untouched notes with tag/directory adjacency) ===
2910
+ const discovery = [];
2911
+ const usedPermalinks = new Set([
2912
+ ...relevantPermalinks,
2913
+ ...activeContext.map((a) => a.note.permalink),
2914
+ ]);
2915
+ // Extract tags from thought terms for tag adjacency
2916
+ const thoughtTerms = new Set(thought.toLowerCase().split(/[\s\p{P}]+/u).filter((t) => t.length >= 2));
2917
+ for (const note of notes) {
2918
+ if (usedPermalinks.has(note.permalink))
2919
+ continue;
2920
+ const eng = args.engagementIndex?.get(note.permalink);
2921
+ if (eng && eng.contactCount > 0)
2922
+ continue;
2923
+ let discoveryScore = 0;
2924
+ let reasons = [];
2925
+ // Tag adjacency
2926
+ const tags = safeStringArray(note.frontmatter.tags);
2927
+ for (const tag of tags) {
2928
+ if (thoughtTerms.has(tag.toLowerCase())) {
2929
+ discoveryScore += 10;
2930
+ reasons.push(`tag:${tag}`);
2931
+ }
2932
+ }
2933
+ // Weak text match (lower threshold than relevant)
2934
+ if (thought.length > 0) {
2935
+ const textScore = this.noteScore(note, thought);
2936
+ if (textScore > 0) {
2937
+ discoveryScore += Math.min(textScore, 20);
2938
+ reasons.push("weak_text_match");
2939
+ }
2940
+ }
2941
+ // Random serendipity factor
2942
+ discoveryScore += Math.floor(Math.random() * 5);
2943
+ if (discoveryScore > 0 || Math.random() < 0.02) {
2944
+ if (reasons.length === 0)
2945
+ reasons.push("serendipity");
2946
+ discovery.push({ note, score: discoveryScore, reason: reasons.join(", ") });
2947
+ }
2948
+ }
2949
+ discovery.sort((a, b) => b.score - a.score);
2950
+ return {
2951
+ project: this.projectName,
2952
+ relevant: relevant.slice(0, 5).map((r) => toOrientItem(r.note, r.reason)),
2953
+ activeContext: activeContext.slice(0, 5).map((a) => toOrientItem(a.note, a.reason)),
2954
+ discovery: discovery.slice(0, 3).map((d) => toOrientItem(d.note, d.reason)),
2955
+ };
2956
+ }
2817
2957
  async writeFlowNote(args) {
2818
2958
  await this.storage.ensureRoot();
2819
2959
  const flowState = (safeString(args.flowState)?.toLowerCase() ?? "capture");
@@ -2871,6 +3011,9 @@ export class LocalNoteStore {
2871
3011
  next_action: nextAction,
2872
3012
  parent_stock: parentStock,
2873
3013
  },
3014
+ origin: args.origin,
3015
+ createdVia: args.createdVia,
3016
+ humanRole: args.humanRole,
2874
3017
  });
2875
3018
  const notes = await this.loadProjectNotes();
2876
3019
  const lookup = this.buildLookup(notes);
@@ -3029,6 +3172,15 @@ export class LocalNoteStore {
3029
3172
  created: safeString(existingFrontmatter.created) ?? date,
3030
3173
  updated: date,
3031
3174
  };
3175
+ if (input.origin && !existingFrontmatter.origin) {
3176
+ frontmatter.origin = input.origin;
3177
+ }
3178
+ if (input.createdVia && !existingFrontmatter.created_via) {
3179
+ frontmatter.created_via = input.createdVia;
3180
+ }
3181
+ if (input.humanRole && !existingFrontmatter.human_role) {
3182
+ frontmatter.human_role = input.humanRole;
3183
+ }
3032
3184
  for (const [key, value] of Object.entries(patch)) {
3033
3185
  if (RESERVED_FRONTMATTER_KEYS.has(key)) {
3034
3186
  continue;
@@ -0,0 +1,62 @@
1
+ const SIGNAL_DIR = ".kioq";
2
+ const SIGNAL_FILE = `${SIGNAL_DIR}/signals.jsonl`;
3
+ export class SignalLog {
4
+ storage;
5
+ buffer = [];
6
+ flushPromise = null;
7
+ constructor(storage) {
8
+ this.storage = storage;
9
+ }
10
+ record(op, note, ctx) {
11
+ this.buffer.push({
12
+ ts: new Date().toISOString(),
13
+ op,
14
+ note,
15
+ ctx,
16
+ });
17
+ }
18
+ recordBatch(entries) {
19
+ const ts = new Date().toISOString();
20
+ for (const entry of entries) {
21
+ this.buffer.push({ ts, op: entry.op, note: entry.note, ctx: entry.ctx });
22
+ }
23
+ }
24
+ async flush() {
25
+ if (this.buffer.length === 0)
26
+ return;
27
+ if (this.flushPromise) {
28
+ await this.flushPromise;
29
+ }
30
+ const entries = this.buffer.splice(0);
31
+ const lines = entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
32
+ this.flushPromise = this.appendToLog(lines);
33
+ try {
34
+ await this.flushPromise;
35
+ }
36
+ finally {
37
+ this.flushPromise = null;
38
+ }
39
+ }
40
+ async load() {
41
+ try {
42
+ const content = await this.storage.readFile(SIGNAL_FILE);
43
+ return content
44
+ .split("\n")
45
+ .filter((line) => line.trim().length > 0)
46
+ .map((line) => JSON.parse(line));
47
+ }
48
+ catch {
49
+ return [];
50
+ }
51
+ }
52
+ async appendToLog(content) {
53
+ let existing = "";
54
+ try {
55
+ existing = await this.storage.readFile(SIGNAL_FILE);
56
+ }
57
+ catch {
58
+ // file doesn't exist yet
59
+ }
60
+ await this.storage.writeFile(SIGNAL_FILE, existing + content);
61
+ }
62
+ }
@@ -354,7 +354,7 @@ export class GitHubStorage {
354
354
  }
355
355
  async pullLocal(context) {
356
356
  try {
357
- await this.execFileAsyncFn("git", ["pull", "--ff-only"], {
357
+ await this.execFileAsyncFn("git", ["pull", "--ff-only", "origin", this.branch], {
358
358
  cwd: this.rootPath,
359
359
  timeout: 30_000,
360
360
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomingtoming/kioq",
3
- "version": "0.8.3",
3
+ "version": "0.9.1",
4
4
  "description": "Japanese-first local markdown MCP server",
5
5
  "type": "module",
6
6
  "bin": {