@tomingtoming/kioq 0.8.2 → 0.9.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.
@@ -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";
@@ -288,6 +289,8 @@ 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();
291
294
  const appendStorageContext = (lines) => {
292
295
  lines.push(`storage_backend: ${config.github ? "github" : "filesystem"}`);
293
296
  lines.push(`storage_root: ${config.root}`);
@@ -312,56 +315,65 @@ async function main() {
312
315
  name: "kioq",
313
316
  version: "0.7.0",
314
317
  });
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,
318
+ server.tool("orient", "kioqを通じて思考を構造化するツール。チェインバー型パイロット支援システムとして、応答を組み立てる前に現在の思考を渡すことで、3つのスロットを返します: (1) relevant — 思考に直接関連するノート(engagement重み付け)、(2) active_context — 進行中のflowと最近のアクティブなノート、(3) discovery — 未接触だが接点がありそうなノート(セレンディピティ)。各ノートのengagement(接触回数、人間の反応、AI被参照数、origin)が付与されます。observationで人間の反応を記録でき、それがengagementに蓄積されます。対話の各ターンで呼び出すことを推奨します。", {
319
+ thought: z.string().min(1).describe("今何を考えているか、何について応答しようとしているか"),
320
+ references: z.array(z.string()).optional().describe("明示的に関連するノートの identifier(あれば)"),
321
+ observation: z.object({
322
+ note: z.string().min(1).describe("観察対象のノート identifier"),
323
+ signal: z.enum(["cited", "discussed", "applied", "questioned", "dismissed"]).describe("人間の反応の種類"),
324
+ }).optional().describe("直前の対話で人間がノートの概念に反応した場合に記録"),
325
+ }, async ({ thought, references, observation }) => {
326
+ const signals = await signalLog.load();
327
+ const notes = await store.loadAllNotes();
328
+ engagementIndex.build(signals, notes);
329
+ const result = await store.orient({
330
+ thought,
331
+ references,
332
+ observation,
333
+ engagementIndex,
334
+ signalLog,
333
335
  });
336
+ await signalLog.flush();
334
337
  const lines = [];
335
- lines.push(`対象プロジェクト: ${result.project}`);
338
+ lines.push(`project: ${result.project}`);
336
339
  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}`);
340
+ lines.push(`scope_label: orient`);
341
+ lines.push(`thought: ${thought.slice(0, 200)}`);
341
342
  lines.push("");
342
- if (result.results.length === 0) {
343
- lines.push("候補が見つかりませんでした。");
343
+ if (result.relevant.length > 0) {
344
+ lines.push("--- relevant ---");
345
+ for (const item of result.relevant) {
346
+ lines.push(`- ${item.title}`);
347
+ lines.push(` permalink: ${item.permalink}`);
348
+ lines.push(` reason: ${item.reason}`);
349
+ 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"}`);
350
+ lines.push(` snippet: ${item.snippet.slice(0, 200)}`);
351
+ }
352
+ lines.push("");
344
353
  }
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
- });
354
+ if (result.activeContext.length > 0) {
355
+ lines.push("--- active_context ---");
356
+ for (const item of result.activeContext) {
357
+ lines.push(`- ${item.title}`);
358
+ lines.push(` permalink: ${item.permalink}`);
359
+ lines.push(` reason: ${item.reason}`);
360
+ lines.push(` engagement: contact=${item.engagement.contactCount} human=${item.engagement.humanSignalCount} origin=${item.engagement.origin ?? "unknown"}`);
361
+ }
362
+ lines.push("");
358
363
  }
359
- if (responseModeAtLeast(responseMode, "verbose")) {
364
+ if (result.discovery.length > 0) {
365
+ lines.push("--- discovery ---");
366
+ for (const item of result.discovery) {
367
+ lines.push(`- ${item.title}`);
368
+ lines.push(` permalink: ${item.permalink}`);
369
+ lines.push(` reason: ${item.reason}`);
370
+ lines.push(` engagement: contact=${item.engagement.contactCount} origin=${item.engagement.origin ?? "unknown"} created_via=${item.engagement.createdVia ?? "unknown"}`);
371
+ lines.push(` snippet: ${item.snippet.slice(0, 160)}`);
372
+ }
360
373
  lines.push("");
361
- appendIndexNoteCandidates(lines, result.indexNoteCandidates, "recommended_indexes");
362
374
  }
363
- if (responseModeAtLeast(responseMode, "verbose")) {
364
- await appendAutoProjectHealth(lines);
375
+ if (result.relevant.length === 0 && result.discovery.length === 0) {
376
+ lines.push("関連するノートは見つかりませんでした。");
365
377
  }
366
378
  return textResult(lines.join("\n"));
367
379
  });
@@ -522,279 +534,7 @@ async function main() {
522
534
  return textResult(lines.join("\n"));
523
535
  }
524
536
  });
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", "構造化を促すために、ノート群の問題を診断して優先順位付きで返します。", {
537
+ server.tool("lint_structure", "ノート群の構造的問題を診断し、優先順位付きで返します。", {
798
538
  limit: z.number().int().min(1).max(200).optional().describe("返却する issue 最大件数 (default: 50)"),
799
539
  response_mode: responseModeSchema,
800
540
  }, async ({ limit, response_mode }) => {
@@ -914,19 +654,92 @@ async function main() {
914
654
  }
915
655
  return textResult(lines.join("\n"));
916
656
  });
917
- server.tool("write_note", "ローカル markdown ノートを作成/更新します。", {
657
+ server.tool("write_note", "ノートを作成/更新します。note_type='flow' で進行中の問いを記録、それ以外で知識(stock)を記録します。origin で誰が起点かを記録し、engagement追跡の基盤になります。", {
918
658
  title: z.string().min(1),
919
- content: z.string().min(1),
920
- directory: z.string().min(1).optional().describe("default: KIOQ_DEFAULT_DIRECTORY または Inbox"),
659
+ content: z.string().min(1).describe("note_type='flow' の場合は ## Notes セクションの本文"),
660
+ directory: z.string().min(1).optional().describe("default: stock→KIOQ_DEFAULT_DIRECTORY, flow→Flow"),
921
661
  tags: z.array(z.string()).optional(),
922
- note_type: z.string().optional().describe("例: note / decision / task"),
923
- }, async ({ title, content, directory, tags, note_type }) => {
662
+ note_type: z.string().optional().describe("stock(default) / flow / note / decision / task"),
663
+ origin: z.enum(["human", "ai", "collaborative"]).optional().describe("誰が起点か (human=体験, ai=AI生成/抽出, collaborative=対話から)"),
664
+ created_via: z.enum(["experience", "dialogue", "extraction", "curation", "capture"]).optional().describe("生成経路"),
665
+ human_role: z.enum(["thinker", "architect", "curator", "reviewer"]).optional().describe("collaborative 時の人間の役割"),
666
+ question: z.string().optional().describe("flow 時の問い (note_type='flow' で必須)"),
667
+ next_action: z.string().optional().describe("flow 時の次のアクション (note_type='flow' で必須)"),
668
+ parent_stock: z.string().optional().describe("flow 時の親 stock identifier (note_type='flow' で必須)"),
669
+ related: z.array(z.string()).optional().describe("flow 時の追加関連リンク先"),
670
+ flow_state: z.string().optional().describe("flow 時の状態: capture / active / blocked / done / promoted / dropped"),
671
+ }, async ({ title, content, directory, tags, note_type, origin, created_via, human_role, question, next_action, parent_stock, related, flow_state }) => {
672
+ // Flow note path
673
+ if (note_type === "flow") {
674
+ if (!question || !next_action || !parent_stock) {
675
+ return textResult("error: note_type='flow' requires question, next_action, parent_stock");
676
+ }
677
+ const flowResult = await store.writeFlowNote({
678
+ title,
679
+ question,
680
+ nextAction: next_action,
681
+ parentStock: parent_stock,
682
+ related,
683
+ details: content,
684
+ directory,
685
+ tags,
686
+ flowState: flow_state,
687
+ origin,
688
+ createdVia: created_via,
689
+ humanRole: human_role,
690
+ });
691
+ const flowSummary = summarizeWriteFlowResponse({
692
+ operation: flowResult.operation,
693
+ unresolvedWikiLinkCount: flowResult.unresolvedWikiLinks.length,
694
+ boundaryWarningCount: flowResult.boundaryWarnings.length,
695
+ memoryContractStatus: flowResult.memoryContract.status,
696
+ duplicateWarningCount: flowResult.duplicateWarnings.length,
697
+ orphanWarning: flowResult.orphanWarning,
698
+ title: flowResult.title,
699
+ parentStockResolved: flowResult.parentStockResolved,
700
+ parentStock: flowResult.parentStock,
701
+ });
702
+ const flowLines = [
703
+ "flow ノートを作成/更新しました。",
704
+ `project: ${flowResult.project}`,
705
+ `scope_label: ${TOOL_SCOPE_LABELS.write_flow_note}`,
706
+ `primary_navigation_signal: ${flowSummary.primaryNavigationSignal}`,
707
+ `primary_quality_signal: ${flowSummary.primaryQualitySignal}`,
708
+ `next_recommended_action: ${flowSummary.nextRecommendedAction}`,
709
+ `operation: ${flowResult.operation}`,
710
+ `file: ${flowResult.relativePath}`,
711
+ `permalink: ${flowResult.permalink}`,
712
+ `title: ${flowResult.title}`,
713
+ `flow_state: ${flowResult.flowState}`,
714
+ `parent_stock: ${flowResult.parentStock}`,
715
+ `parent_stock_resolved: ${flowResult.parentStockResolved ? "yes" : "no"}`,
716
+ `link_health: ${flowResult.linkHealth.resolved}/${flowResult.linkHealth.total} resolved`,
717
+ `backlinks: ${flowResult.backlinkCount}`,
718
+ `orphan_warning: ${flowResult.orphanWarning ? "yes" : "no"}`,
719
+ ];
720
+ flowLines.splice(2, 0, ...[`storage_backend: ${config.github ? "github" : "filesystem"}`, `storage_root: ${config.root}`, ...(config.github ? [`storage_repo: ${config.github.owner}/${config.github.repo}`, `storage_branch: ${config.github.branch}`] : [])]);
721
+ if (flowSummary.nextRecommendedTarget) {
722
+ insertBeforeLine(flowLines, "next_recommended_action:", `next_recommended_target: ${flowSummary.nextRecommendedTarget}`);
723
+ }
724
+ appendConflictSummary(flowLines, summarizeNoConflict({ serverUpdated: flowResult.serverUpdated }));
725
+ appendResponsibilityWarning(flowLines, flowResult.boundaryWarnings);
726
+ appendStructureScore(flowLines, flowResult.structureScore);
727
+ appendMemoryContract(flowLines, flowResult.memoryContract);
728
+ appendUnresolvedWikiLinks(flowLines, flowResult.unresolvedWikiLinks);
729
+ appendDuplicateWarnings(flowLines, flowResult.duplicateWarnings);
730
+ await appendAutoProjectHealth(flowLines);
731
+ return textResult(flowLines.join("\n"));
732
+ }
733
+ // Stock/generic note path
924
734
  const result = await store.writeNote({
925
735
  title,
926
736
  content,
927
737
  directory,
928
738
  tags,
929
739
  noteType: note_type,
740
+ origin,
741
+ createdVia: created_via,
742
+ humanRole: human_role,
930
743
  });
931
744
  const summary = summarizeWriteNoteResponse({
932
745
  operation: result.operation,
@@ -979,84 +792,6 @@ async function main() {
979
792
  await appendAutoProjectHealth(lines);
980
793
  return textResult(lines.join("\n"));
981
794
  });
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}`);
1042
- }
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
- appendUnresolvedWikiLinks(lines, result.unresolvedWikiLinks);
1055
- appendUnresolvedLinkHints(lines, result.unresolvedLinkHints);
1056
- appendDuplicateWarnings(lines, result.duplicateWarnings);
1057
- await appendAutoProjectHealth(lines);
1058
- return textResult(lines.join("\n"));
1059
- });
1060
795
  server.tool("promote_to_stock", "flow ノートを stock ノートへ昇格させ、flow 側の状態を promoted に更新します。", {
1061
796
  flow_identifier: z.string().min(1),
1062
797
  stock_title: z.string().min(1).optional(),
@@ -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
+ }
@@ -38,6 +38,8 @@ export class GitHubStorage {
38
38
  fetchFn;
39
39
  execFileAsyncFn;
40
40
  setupValidated = false;
41
+ lastFetchMs = 0;
42
+ static FETCH_INTERVAL_MS = 10_000;
41
43
  constructor(githubConfig, rootPath, deps = {}) {
42
44
  this.githubConfig = githubConfig;
43
45
  this.rootPath = rootPath;
@@ -48,17 +50,21 @@ export class GitHubStorage {
48
50
  this.fetchFn = deps.fetchFn ?? fetch;
49
51
  this.execFileAsyncFn = deps.execFileAsyncFn ?? execFileAsync;
50
52
  }
51
- // --- Reads: delegate to local clone ---
52
- stat(relativePath) {
53
+ // --- Reads: delegate to local clone (with auto-refresh) ---
54
+ async stat(relativePath) {
55
+ await this.refreshIfStale();
53
56
  return this.local.stat(relativePath);
54
57
  }
55
- readFile(relativePath) {
58
+ async readFile(relativePath) {
59
+ await this.refreshIfStale();
56
60
  return this.local.readFile(relativePath);
57
61
  }
58
- readdir(relativePath) {
62
+ async readdir(relativePath) {
63
+ await this.refreshIfStale();
59
64
  return this.local.readdir(relativePath);
60
65
  }
61
- listMarkdownFiles() {
66
+ async listMarkdownFiles() {
67
+ await this.refreshIfStale();
62
68
  return this.local.listMarkdownFiles();
63
69
  }
64
70
  // --- Writes: GitHub Git Data API ---
@@ -352,6 +358,7 @@ export class GitHubStorage {
352
358
  cwd: this.rootPath,
353
359
  timeout: 30_000,
354
360
  });
361
+ this.lastFetchMs = Date.now();
355
362
  }
356
363
  catch (error) {
357
364
  throw new Error(`${context}: ${this.normalizeExecError(error)}`);
@@ -365,6 +372,25 @@ export class GitHubStorage {
365
372
  // best-effort only
366
373
  }
367
374
  }
375
+ async refreshIfStale() {
376
+ const now = Date.now();
377
+ if (now - this.lastFetchMs < GitHubStorage.FETCH_INTERVAL_MS) {
378
+ return;
379
+ }
380
+ try {
381
+ const { stdout: localRef } = await this.execFileAsyncFn("git", ["rev-parse", `refs/heads/${this.branch}`], { cwd: this.rootPath, timeout: 5_000 });
382
+ await this.execFileAsyncFn("git", ["fetch", "--no-tags", "origin", this.branch], { cwd: this.rootPath, timeout: 15_000 });
383
+ const { stdout: remoteRef } = await this.execFileAsyncFn("git", ["rev-parse", `refs/remotes/origin/${this.branch}`], { cwd: this.rootPath, timeout: 5_000 });
384
+ this.lastFetchMs = Date.now();
385
+ if (localRef.trim() !== remoteRef.trim()) {
386
+ await this.pullLocal("git pull --ff-only failed during auto-refresh");
387
+ }
388
+ }
389
+ catch {
390
+ // best-effort: if fetch fails, proceed with stale data
391
+ this.lastFetchMs = Date.now();
392
+ }
393
+ }
368
394
  isRetryableBatchError(error) {
369
395
  return (error instanceof GitHubApiError && error.retryable)
370
396
  || this.isTransientNetworkError(error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomingtoming/kioq",
3
- "version": "0.8.2",
3
+ "version": "0.9.0",
4
4
  "description": "Japanese-first local markdown MCP server",
5
5
  "type": "module",
6
6
  "bin": {