@zosmaai/pi-llm-wiki 0.9.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.de.md +2 -2
- package/README.es.md +2 -2
- package/README.fr.md +2 -2
- package/README.hi.md +2 -2
- package/README.ja.md +2 -2
- package/README.ko.md +2 -2
- package/README.md +25 -2
- package/README.pt.md +2 -2
- package/README.ru.md +2 -2
- package/README.zh.md +2 -2
- package/docs/api.md +123 -10
- package/docs/architecture.md +38 -1
- package/docs/commands.md +21 -1
- package/extensions/llm-wiki/index.ts +80 -17
- package/extensions/llm-wiki/lib/metadata.ts +34 -1
- package/extensions/llm-wiki/lib/observation.ts +50 -10
- package/extensions/llm-wiki/lib/recall.ts +107 -8
- package/extensions/llm-wiki/lib/runtime.ts +49 -1
- package/extensions/llm-wiki/lib/task-config.ts +107 -13
- package/extensions/llm-wiki/lib/tools.ts +338 -183
- package/extensions/llm-wiki/lib/trajectories-command.ts +67 -0
- package/extensions/llm-wiki/lib/trajectory.ts +613 -0
- package/extensions/llm-wiki/lib/utils.ts +20 -3
- package/extensions/llm-wiki/lib/visible-status.ts +51 -0
- package/package.json +1 -1
- package/prompts/wiki-record.md +36 -0
- package/prompts/wiki-run.md +4 -2
- package/prompts/wiki-skills.md +26 -0
- package/skills/llm-wiki/SKILL.md +70 -4
|
@@ -44,6 +44,55 @@ function requireVault(paths: VaultPaths): { ok: true } | { ok: false; reason: st
|
|
|
44
44
|
return { ok: true };
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
type WikiToolResult = {
|
|
48
|
+
content: { type: "text"; text: string }[];
|
|
49
|
+
details: Record<string, unknown>;
|
|
50
|
+
isError?: boolean;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
type ToolCtx = {
|
|
54
|
+
cwd?: string;
|
|
55
|
+
hasUI: boolean;
|
|
56
|
+
ui?: { notify: (message: string, type?: string) => void };
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Dispatch a heavy mutating action to the background runtime and report its
|
|
61
|
+
* result (issue #77). The agent turn is never blocked: `work` runs off-thread
|
|
62
|
+
* and the returned one-line summary is surfaced to the user via
|
|
63
|
+
* `runtime.report()`. Returns an immediate, non-blocking tool result.
|
|
64
|
+
*
|
|
65
|
+
* When no runtime is available (unit tests / degraded mode), `work` runs
|
|
66
|
+
* synchronously and its summary is returned inline, preserving prior behavior.
|
|
67
|
+
* Retrieval tools (search/read/recall/status) never use this — the model needs
|
|
68
|
+
* their output inline.
|
|
69
|
+
*/
|
|
70
|
+
async function dispatchReported(
|
|
71
|
+
runtime: Runtime | undefined,
|
|
72
|
+
ctx: ToolCtx,
|
|
73
|
+
opts: {
|
|
74
|
+
label: string;
|
|
75
|
+
/** Immediate, non-blocking acknowledgement shown while work runs. */
|
|
76
|
+
started: string;
|
|
77
|
+
/** Off-thread work; resolves to the human-readable completion summary. */
|
|
78
|
+
work: () => Promise<string>;
|
|
79
|
+
details?: Record<string, unknown>;
|
|
80
|
+
},
|
|
81
|
+
): Promise<WikiToolResult> {
|
|
82
|
+
if (!runtime) {
|
|
83
|
+
const summary = await opts.work();
|
|
84
|
+
return {
|
|
85
|
+
content: [{ type: "text", text: summary }],
|
|
86
|
+
details: { background: false, ...opts.details },
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
runtime.launchReported({ hasUI: ctx.hasUI, ui: ctx.ui }, opts.label, opts.work);
|
|
90
|
+
return {
|
|
91
|
+
content: [{ type: "text", text: opts.started }],
|
|
92
|
+
details: { background: true, ...opts.details },
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
47
96
|
// ─── 1. wiki_bootstrap ──────────────────────────────────
|
|
48
97
|
|
|
49
98
|
export function registerWikiBootstrap(pi: ExtensionAPI): void {
|
|
@@ -381,14 +430,15 @@ export function registerWikiIngest(pi: ExtensionAPI, runtime?: Runtime): void {
|
|
|
381
430
|
];
|
|
382
431
|
launchEmbedPages(runtime, launchCtx, paths, pageIds, `embed:ingest:${s.id}`);
|
|
383
432
|
}
|
|
433
|
+
const summary = committed
|
|
434
|
+
? `LLM Wiki: ingested ${s.id} → ${committed.entitiesCreated.length} entit${committed.entitiesCreated.length === 1 ? "y" : "ies"}, ${committed.conceptsCreated.length} concept${committed.conceptsCreated.length === 1 ? "" : "s"}`
|
|
435
|
+
: `LLM Wiki: ${s.id} produced no synthesis`;
|
|
384
436
|
if (ctx.hasUI) {
|
|
385
|
-
ctx.ui.notify(
|
|
386
|
-
committed
|
|
387
|
-
? `LLM Wiki: ingested ${s.id} → ${committed.entitiesCreated.length} entit${committed.entitiesCreated.length === 1 ? "y" : "ies"}, ${committed.conceptsCreated.length} concept${committed.conceptsCreated.length === 1 ? "" : "s"}`
|
|
388
|
-
: `LLM Wiki: ${s.id} produced no synthesis`,
|
|
389
|
-
committed ? "info" : "warning",
|
|
390
|
-
);
|
|
437
|
+
ctx.ui.notify(summary, committed ? "info" : "warning");
|
|
391
438
|
}
|
|
439
|
+
// Persistent, user-visible completion report (issue #77) in
|
|
440
|
+
// addition to the transient toast above. Notices-gated.
|
|
441
|
+
runtime.report(committed ? `✅ ${summary}` : `⚠️ ${summary}`);
|
|
392
442
|
});
|
|
393
443
|
}
|
|
394
444
|
return {
|
|
@@ -463,7 +513,8 @@ export function registerWikiEnsurePage(pi: ExtensionAPI, runtime?: Runtime): voi
|
|
|
463
513
|
],
|
|
464
514
|
parameters: Type.Object({
|
|
465
515
|
type: Type.String({
|
|
466
|
-
description:
|
|
516
|
+
description:
|
|
517
|
+
"Page type: entity | concept | synthesis | analysis | requirement | skill | case",
|
|
467
518
|
}),
|
|
468
519
|
title: Type.String({ description: "Page title" }),
|
|
469
520
|
content: Type.Optional(
|
|
@@ -481,7 +532,14 @@ export function registerWikiEnsurePage(pi: ExtensionAPI, runtime?: Runtime): voi
|
|
|
481
532
|
};
|
|
482
533
|
}
|
|
483
534
|
|
|
484
|
-
const type = params.type as
|
|
535
|
+
const type = params.type as
|
|
536
|
+
| "entity"
|
|
537
|
+
| "concept"
|
|
538
|
+
| "synthesis"
|
|
539
|
+
| "analysis"
|
|
540
|
+
| "requirement"
|
|
541
|
+
| "skill"
|
|
542
|
+
| "case";
|
|
485
543
|
const slug = params.title
|
|
486
544
|
.toLowerCase()
|
|
487
545
|
.replace(/[^a-z0-9\s-]/g, "")
|
|
@@ -495,6 +553,8 @@ export function registerWikiEnsurePage(pi: ExtensionAPI, runtime?: Runtime): voi
|
|
|
495
553
|
synthesis: "syntheses",
|
|
496
554
|
analysis: "analyses",
|
|
497
555
|
requirement: "requirements",
|
|
556
|
+
skill: "skills",
|
|
557
|
+
case: "cases",
|
|
498
558
|
};
|
|
499
559
|
const folder = folderMap[type] || "concepts";
|
|
500
560
|
const pagePath = join(paths.wiki, folder, `${slug}.md`);
|
|
@@ -572,6 +632,76 @@ function buildPageTemplate(
|
|
|
572
632
|
"Durable answer from a query.\n\n## Question\n\n[Original question]",
|
|
573
633
|
);
|
|
574
634
|
}
|
|
635
|
+
if (type === "skill") {
|
|
636
|
+
return [
|
|
637
|
+
"---",
|
|
638
|
+
"type: skill",
|
|
639
|
+
`created: ${date}`,
|
|
640
|
+
`updated: ${date}`,
|
|
641
|
+
"status: draft",
|
|
642
|
+
"trajectories: []",
|
|
643
|
+
"tags: []",
|
|
644
|
+
"---",
|
|
645
|
+
"",
|
|
646
|
+
`# ${title}`,
|
|
647
|
+
"",
|
|
648
|
+
"_One-line summary of the reusable pattern this skill captures._",
|
|
649
|
+
"",
|
|
650
|
+
"## When to Use",
|
|
651
|
+
"",
|
|
652
|
+
"[Trigger conditions — when this pattern applies]",
|
|
653
|
+
"",
|
|
654
|
+
"## Procedure",
|
|
655
|
+
"",
|
|
656
|
+
"1. [Step 1]",
|
|
657
|
+
"2. [Step 2]",
|
|
658
|
+
"",
|
|
659
|
+
"## Pitfalls",
|
|
660
|
+
"",
|
|
661
|
+
"- [Known failure mode or caveat]",
|
|
662
|
+
"",
|
|
663
|
+
"## Distilled From",
|
|
664
|
+
"",
|
|
665
|
+
"_Trajectories this skill was generalized from._",
|
|
666
|
+
"",
|
|
667
|
+
"- [[trajectories/TRJ-...]]",
|
|
668
|
+
"",
|
|
669
|
+
].join("\n");
|
|
670
|
+
}
|
|
671
|
+
if (type === "case") {
|
|
672
|
+
return [
|
|
673
|
+
"---",
|
|
674
|
+
"type: case",
|
|
675
|
+
`created: ${date}`,
|
|
676
|
+
`updated: ${date}`,
|
|
677
|
+
"status: draft",
|
|
678
|
+
"outcome: success",
|
|
679
|
+
"trajectory_id: ",
|
|
680
|
+
"tags: []",
|
|
681
|
+
"---",
|
|
682
|
+
"",
|
|
683
|
+
`# ${title}`,
|
|
684
|
+
"",
|
|
685
|
+
"_One-line summary of the specific task this case records._",
|
|
686
|
+
"",
|
|
687
|
+
"## Task",
|
|
688
|
+
"",
|
|
689
|
+
"[What was requested]",
|
|
690
|
+
"",
|
|
691
|
+
"## Approach",
|
|
692
|
+
"",
|
|
693
|
+
"[How the agent solved it — key steps and decisions]",
|
|
694
|
+
"",
|
|
695
|
+
"## Outcome",
|
|
696
|
+
"",
|
|
697
|
+
"[Result, and anything worth reusing or avoiding next time]",
|
|
698
|
+
"",
|
|
699
|
+
"## Trajectory",
|
|
700
|
+
"",
|
|
701
|
+
"- [[trajectories/TRJ-...]] — captured tool-call run",
|
|
702
|
+
"",
|
|
703
|
+
].join("\n");
|
|
704
|
+
}
|
|
575
705
|
if (type === "requirement") {
|
|
576
706
|
return [
|
|
577
707
|
"---",
|
|
@@ -672,7 +802,7 @@ export function registerWikiSearch(pi: ExtensionAPI): void {
|
|
|
672
802
|
|
|
673
803
|
// ─── 6. wiki_lint ───────────────────────────────────────
|
|
674
804
|
|
|
675
|
-
export function registerWikiLint(pi: ExtensionAPI): void {
|
|
805
|
+
export function registerWikiLint(pi: ExtensionAPI, runtime?: Runtime): void {
|
|
676
806
|
pi.registerTool({
|
|
677
807
|
name: "wiki_lint",
|
|
678
808
|
label: "Wiki Lint",
|
|
@@ -699,138 +829,143 @@ export function registerWikiLint(pi: ExtensionAPI): void {
|
|
|
699
829
|
};
|
|
700
830
|
}
|
|
701
831
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
const linkCounts: Record<string, number> = {};
|
|
714
|
-
|
|
715
|
-
for (const page of pages) {
|
|
716
|
-
const links = extractWikilinks(page.content);
|
|
717
|
-
for (const link of links) {
|
|
718
|
-
if (!allPageIds.has(link)) {
|
|
719
|
-
missingPages++;
|
|
720
|
-
findings.push(`Missing page: [[${link}]] (in [[${page.relative}]])`);
|
|
721
|
-
const existing = gaps.find((g) => g.topic === link);
|
|
722
|
-
if (existing) {
|
|
723
|
-
if (!existing.mentionedBy.includes(page.relative))
|
|
724
|
-
existing.mentionedBy.push(page.relative);
|
|
725
|
-
} else {
|
|
726
|
-
gaps.push({ topic: link, mentionedBy: [page.relative] });
|
|
727
|
-
}
|
|
728
|
-
} else {
|
|
729
|
-
linkCounts[link] = (linkCounts[link] || 0) + 1;
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
}
|
|
832
|
+
// Full-vault scan (+ optional auto-fix writes + reindex) is O(pages):
|
|
833
|
+
// run it in the background and report the health summary (issue #77).
|
|
834
|
+
return dispatchReported(runtime, ctx as ToolCtx, {
|
|
835
|
+
label: `lint:${paths.root}`,
|
|
836
|
+
started:
|
|
837
|
+
"\u{1F9F9} LLM Wiki: lint started in the background — the health report will be posted when it completes.",
|
|
838
|
+
work: async () => runWikiLint(paths, params.auto_fix === true),
|
|
839
|
+
});
|
|
840
|
+
},
|
|
841
|
+
});
|
|
842
|
+
}
|
|
733
843
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
844
|
+
/**
|
|
845
|
+
* Run the wiki health scan (issue #77 extracted it from the tool body so it can
|
|
846
|
+
* run off-thread via `dispatchReported`). Returns the human-readable summary.
|
|
847
|
+
*/
|
|
848
|
+
function runWikiLint(paths: VaultPaths, autoFix: boolean): string {
|
|
849
|
+
const pages = findWikiPages(paths.wiki);
|
|
850
|
+
const registry = buildRegistry(paths);
|
|
851
|
+
buildBacklinks(paths, registry); // ensures backlinks.json is current
|
|
852
|
+
|
|
853
|
+
const findings: string[] = [];
|
|
854
|
+
let orphans = 0;
|
|
855
|
+
let missingPages = 0;
|
|
856
|
+
let contradictions = 0;
|
|
857
|
+
const gaps: Array<{ topic: string; mentionedBy: string[] }> = [];
|
|
858
|
+
|
|
859
|
+
const allPageIds = new Set(pages.map((p) => p.relative));
|
|
860
|
+
const linkCounts: Record<string, number> = {};
|
|
861
|
+
|
|
862
|
+
for (const page of pages) {
|
|
863
|
+
const links = extractWikilinks(page.content);
|
|
864
|
+
for (const link of links) {
|
|
865
|
+
if (!allPageIds.has(link)) {
|
|
866
|
+
missingPages++;
|
|
867
|
+
findings.push(`Missing page: [[${link}]] (in [[${page.relative}]])`);
|
|
868
|
+
const existing = gaps.find((g) => g.topic === link);
|
|
869
|
+
if (existing) {
|
|
870
|
+
if (!existing.mentionedBy.includes(page.relative))
|
|
871
|
+
existing.mentionedBy.push(page.relative);
|
|
872
|
+
} else {
|
|
873
|
+
gaps.push({ topic: link, mentionedBy: [page.relative] });
|
|
738
874
|
}
|
|
875
|
+
} else {
|
|
876
|
+
linkCounts[link] = (linkCounts[link] || 0) + 1;
|
|
739
877
|
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
740
880
|
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
881
|
+
for (const page of pages) {
|
|
882
|
+
if (!linkCounts[page.relative] || linkCounts[page.relative] === 0) {
|
|
883
|
+
orphans++;
|
|
884
|
+
findings.push(`Orphan: [[${page.relative}]] has no inbound links`);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
747
887
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
888
|
+
for (const page of pages) {
|
|
889
|
+
if (page.content.includes("⚠️ **Contradiction")) {
|
|
890
|
+
contradictions++;
|
|
891
|
+
findings.push(`Contradiction flagged in [[${page.relative}]]`);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
let fixesApplied = 0;
|
|
896
|
+
if (autoFix) {
|
|
897
|
+
for (const gap of gaps) {
|
|
898
|
+
if (gap.mentionedBy.length >= 2) {
|
|
899
|
+
const folder = gap.topic.includes("/") ? gap.topic.split("/")[0] : "concepts";
|
|
900
|
+
const name = gap.topic.includes("/") ? gap.topic.split("/").pop()! : gap.topic;
|
|
901
|
+
const pagePath = join(paths.wiki, folder, `${name}.md`);
|
|
902
|
+
mkdirSync(join(paths.wiki, folder), { recursive: true });
|
|
903
|
+
try {
|
|
904
|
+
// Atomic create-if-absent: the `wx` flag fails with EEXIST instead of
|
|
905
|
+
// overwriting, avoiding the existsSync→write TOCTOU race (CodeQL).
|
|
906
|
+
writeFileSync(
|
|
907
|
+
pagePath,
|
|
908
|
+
`---\ntype: concept\ncreated: ${fmtDate()}\nupdated: ${fmtDate()}\nsources: []\nstatus: stub\n---\n\n# ${name.replace(/-/g, " ")}\n\n_Stub auto-created by lint. Expand with content from: ${gap.mentionedBy.map((r) => `[[${r}]]`).join(", ")}_\n`,
|
|
909
|
+
{ encoding: "utf-8", flag: "wx" },
|
|
910
|
+
);
|
|
911
|
+
fixesApplied++;
|
|
912
|
+
} catch (err) {
|
|
913
|
+
// Page already exists — nothing to fix. Re-throw anything else.
|
|
914
|
+
if ((err as NodeJS.ErrnoException).code !== "EEXIST") throw err;
|
|
765
915
|
}
|
|
766
916
|
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
767
919
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
const reportLines = [
|
|
774
|
-
"# Wiki Lint Report",
|
|
775
|
-
`Generated: ${fmtDate()}`,
|
|
776
|
-
"",
|
|
777
|
-
"## Summary",
|
|
778
|
-
`- Total pages: ${pages.length}`,
|
|
779
|
-
`- Orphans: ${orphans}`,
|
|
780
|
-
`- Missing pages: ${missingPages}`,
|
|
781
|
-
`- Contradictions: ${contradictions}`,
|
|
782
|
-
params.auto_fix ? `- Fixes applied: ${fixesApplied}` : "",
|
|
783
|
-
"",
|
|
784
|
-
"## Findings",
|
|
785
|
-
findings.length > 0 ? findings.map((f) => `- ${f}`).join("\n") : "✅ No issues found!",
|
|
786
|
-
"",
|
|
787
|
-
].filter(Boolean);
|
|
788
|
-
|
|
789
|
-
const reportPath = join(paths.outputs, `lint-${fmtDate()}.md`);
|
|
790
|
-
mkdirSync(paths.outputs, { recursive: true });
|
|
791
|
-
writeFileSync(reportPath, `${reportLines.join("\n")}\n`, "utf-8");
|
|
792
|
-
|
|
793
|
-
appendEvent(paths, {
|
|
794
|
-
kind: "lint",
|
|
795
|
-
orphans,
|
|
796
|
-
missing_pages: missingPages,
|
|
797
|
-
contradictions,
|
|
798
|
-
auto_fix: params.auto_fix ?? false,
|
|
799
|
-
});
|
|
800
|
-
|
|
801
|
-
rebuildMetadataLight(paths);
|
|
920
|
+
writeJson(join(paths.discoveries, "gaps.json"), {
|
|
921
|
+
gaps,
|
|
922
|
+
generated: new Date().toISOString(),
|
|
923
|
+
});
|
|
802
924
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
gaps: gaps.length,
|
|
830
|
-
} as Record<string, unknown>,
|
|
831
|
-
};
|
|
832
|
-
},
|
|
925
|
+
const reportLines = [
|
|
926
|
+
"# Wiki Lint Report",
|
|
927
|
+
`Generated: ${fmtDate()}`,
|
|
928
|
+
"",
|
|
929
|
+
"## Summary",
|
|
930
|
+
`- Total pages: ${pages.length}`,
|
|
931
|
+
`- Orphans: ${orphans}`,
|
|
932
|
+
`- Missing pages: ${missingPages}`,
|
|
933
|
+
`- Contradictions: ${contradictions}`,
|
|
934
|
+
autoFix ? `- Fixes applied: ${fixesApplied}` : "",
|
|
935
|
+
"",
|
|
936
|
+
"## Findings",
|
|
937
|
+
findings.length > 0 ? findings.map((f) => `- ${f}`).join("\n") : "✅ No issues found!",
|
|
938
|
+
"",
|
|
939
|
+
].filter(Boolean);
|
|
940
|
+
|
|
941
|
+
const reportPath = join(paths.outputs, `lint-${fmtDate()}.md`);
|
|
942
|
+
mkdirSync(paths.outputs, { recursive: true });
|
|
943
|
+
writeFileSync(reportPath, `${reportLines.join("\n")}\n`, "utf-8");
|
|
944
|
+
|
|
945
|
+
appendEvent(paths, {
|
|
946
|
+
kind: "lint",
|
|
947
|
+
orphans,
|
|
948
|
+
missing_pages: missingPages,
|
|
949
|
+
contradictions,
|
|
950
|
+
auto_fix: autoFix,
|
|
833
951
|
});
|
|
952
|
+
|
|
953
|
+
rebuildMetadataLight(paths);
|
|
954
|
+
|
|
955
|
+
return [
|
|
956
|
+
"🧹 **LLM Wiki lint complete**",
|
|
957
|
+
"",
|
|
958
|
+
`- Pages: ${pages.length}`,
|
|
959
|
+
`- Orphans: ${orphans}`,
|
|
960
|
+
`- Missing: ${missingPages}`,
|
|
961
|
+
`- Contradictions: ${contradictions}`,
|
|
962
|
+
autoFix ? `- Auto-fixes: ${fixesApplied}` : "",
|
|
963
|
+
"",
|
|
964
|
+
`📄 Report: \`${reportPath}\``,
|
|
965
|
+
gaps.length > 0 ? `💡 ${gaps.length} knowledge gap(s) tracked` : "",
|
|
966
|
+
]
|
|
967
|
+
.filter(Boolean)
|
|
968
|
+
.join("\n");
|
|
834
969
|
}
|
|
835
970
|
|
|
836
971
|
// ─── 7. wiki_status ─────────────────────────────────────
|
|
@@ -912,7 +1047,7 @@ export function registerWikiStatus(pi: ExtensionAPI): void {
|
|
|
912
1047
|
|
|
913
1048
|
// ─── 8. wiki_rebuild_meta ───────────────────────────────
|
|
914
1049
|
|
|
915
|
-
export function registerWikiRebuildMeta(pi: ExtensionAPI): void {
|
|
1050
|
+
export function registerWikiRebuildMeta(pi: ExtensionAPI, runtime?: Runtime): void {
|
|
916
1051
|
pi.registerTool({
|
|
917
1052
|
name: "wiki_rebuild_meta",
|
|
918
1053
|
label: "Wiki Rebuild Meta",
|
|
@@ -931,24 +1066,23 @@ export function registerWikiRebuildMeta(pi: ExtensionAPI): void {
|
|
|
931
1066
|
};
|
|
932
1067
|
}
|
|
933
1068
|
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
1069
|
+
// Heavy O(pages) rebuild — dispatch off the agent's critical path and
|
|
1070
|
+
// report on completion (issue #77).
|
|
1071
|
+
return dispatchReported(runtime, ctx as ToolCtx, {
|
|
1072
|
+
label: `rebuild_meta:${paths.root}`,
|
|
1073
|
+
started:
|
|
1074
|
+
"\u{1F9E0} LLM Wiki: metadata rebuild started in the background — the result will be reported when it completes.",
|
|
1075
|
+
work: async () => {
|
|
1076
|
+
rebuildMetadata(paths);
|
|
1077
|
+
appendEvent(paths, { kind: "rebuild_meta" });
|
|
1078
|
+
const registry = readJson<Registry>(join(paths.meta, "registry.json"), {
|
|
1079
|
+
version: "1.0",
|
|
1080
|
+
last_updated: "",
|
|
1081
|
+
pages: {},
|
|
1082
|
+
});
|
|
1083
|
+
return `✅ LLM Wiki: metadata rebuilt — ${Object.keys(registry.pages).length} pages indexed.`;
|
|
1084
|
+
},
|
|
941
1085
|
});
|
|
942
|
-
|
|
943
|
-
return {
|
|
944
|
-
content: [
|
|
945
|
-
{
|
|
946
|
-
type: "text",
|
|
947
|
-
text: `✅ Metadata rebuilt. ${Object.keys(registry.pages).length} pages indexed.`,
|
|
948
|
-
},
|
|
949
|
-
],
|
|
950
|
-
details: { pageCount: Object.keys(registry.pages).length } as Record<string, unknown>,
|
|
951
|
-
};
|
|
952
1086
|
},
|
|
953
1087
|
});
|
|
954
1088
|
}
|
|
@@ -998,28 +1132,24 @@ export function registerWikiReindexEmbeddings(pi: ExtensionAPI, runtime?: Runtim
|
|
|
998
1132
|
};
|
|
999
1133
|
}
|
|
1000
1134
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1135
|
+
// Embedding is network-bound and O(pages) — run it in the background and
|
|
1136
|
+
// report the stats on completion (issue #77).
|
|
1137
|
+
return dispatchReported(runtime, ctx as ToolCtx, {
|
|
1138
|
+
label: `reindex_embeddings:${paths.root}`,
|
|
1139
|
+
started: `\u{1F9E0} LLM Wiki: embedding reindex started in the background (${embedder.model}) — stats will be reported when it completes.`,
|
|
1140
|
+
details: { enabled: true, model: embedder.model },
|
|
1141
|
+
work: async () => {
|
|
1142
|
+
const stats = await reindexEmbeddings(paths, embedder, { force: params.force === true });
|
|
1143
|
+
appendEvent(paths, {
|
|
1144
|
+
kind: "reindex_embeddings",
|
|
1145
|
+
embedded: stats.embedded,
|
|
1146
|
+
skipped: stats.skipped,
|
|
1147
|
+
pruned: stats.pruned,
|
|
1148
|
+
model: embedder.model,
|
|
1149
|
+
});
|
|
1150
|
+
return `✅ LLM Wiki: embeddings reindexed (${embedder.model}) — ${stats.embedded} embedded, ${stats.skipped} fresh, ${stats.pruned} pruned.`;
|
|
1151
|
+
},
|
|
1008
1152
|
});
|
|
1009
|
-
|
|
1010
|
-
return {
|
|
1011
|
-
content: [
|
|
1012
|
-
{
|
|
1013
|
-
type: "text",
|
|
1014
|
-
text: `✅ Embeddings reindexed (${embedder.model}): ${stats.embedded} embedded, ${stats.skipped} fresh, ${stats.pruned} pruned.`,
|
|
1015
|
-
},
|
|
1016
|
-
],
|
|
1017
|
-
details: {
|
|
1018
|
-
enabled: true,
|
|
1019
|
-
...stats,
|
|
1020
|
-
model: embedder.model,
|
|
1021
|
-
} as Record<string, unknown>,
|
|
1022
|
-
};
|
|
1023
1153
|
},
|
|
1024
1154
|
});
|
|
1025
1155
|
}
|
|
@@ -1067,10 +1197,12 @@ export function registerWikiWatch(pi: ExtensionAPI): void {
|
|
|
1067
1197
|
pi.registerTool({
|
|
1068
1198
|
name: "wiki_watch",
|
|
1069
1199
|
label: "Wiki Watch",
|
|
1070
|
-
description:
|
|
1200
|
+
description:
|
|
1201
|
+
"Print a ready-to-paste crontab line for scheduling automatic wiki updates (discover → ingest → lint). Does NOT schedule anything itself — it returns the command for the user to install.",
|
|
1071
1202
|
promptSnippet: "Schedule auto-updates for the wiki",
|
|
1072
1203
|
promptGuidelines: [
|
|
1073
1204
|
"Use wiki_watch when the user wants the wiki to stay current automatically.",
|
|
1205
|
+
"wiki_watch only PRINTS a cron line — surface the output to the user verbatim so they can install it. Do not claim the schedule is active.",
|
|
1074
1206
|
],
|
|
1075
1207
|
parameters: Type.Object({
|
|
1076
1208
|
interval: Type.String({ description: "daily, weekly, hourly, or stop" }),
|
|
@@ -1082,15 +1214,16 @@ export function registerWikiWatch(pi: ExtensionAPI): void {
|
|
|
1082
1214
|
{
|
|
1083
1215
|
type: "text",
|
|
1084
1216
|
text: [
|
|
1085
|
-
"🛑 To stop wiki auto-updates:",
|
|
1217
|
+
"🛑 To stop wiki auto-updates, remove the cron line you installed earlier:",
|
|
1086
1218
|
"",
|
|
1219
|
+
"```bash",
|
|
1220
|
+
"crontab -e # then delete the line tagged '# llm-wiki-autoupdate'",
|
|
1087
1221
|
"```",
|
|
1088
|
-
"schedule_prompt action=list",
|
|
1089
|
-
"```",
|
|
1090
|
-
"Find the wiki job IDs, then:",
|
|
1091
1222
|
"",
|
|
1092
|
-
"
|
|
1093
|
-
"
|
|
1223
|
+
"Or list current jobs to confirm:",
|
|
1224
|
+
"",
|
|
1225
|
+
"```bash",
|
|
1226
|
+
"crontab -l | grep llm-wiki-autoupdate",
|
|
1094
1227
|
"```",
|
|
1095
1228
|
].join("\n"),
|
|
1096
1229
|
},
|
|
@@ -1099,10 +1232,11 @@ export function registerWikiWatch(pi: ExtensionAPI): void {
|
|
|
1099
1232
|
};
|
|
1100
1233
|
}
|
|
1101
1234
|
|
|
1235
|
+
// 5-field POSIX crontab expressions (min hour dom month dow).
|
|
1102
1236
|
const intervals: Record<string, { cron: string; label: string }> = {
|
|
1103
|
-
daily: { cron: "0
|
|
1104
|
-
weekly: { cron: "0
|
|
1105
|
-
hourly: { cron: "0
|
|
1237
|
+
daily: { cron: "0 8 * * *", label: "Daily at 8:00 AM" },
|
|
1238
|
+
weekly: { cron: "0 9 * * 1", label: "Weekly on Monday at 9:00 AM" },
|
|
1239
|
+
hourly: { cron: "0 * * * *", label: "Every hour" },
|
|
1106
1240
|
};
|
|
1107
1241
|
|
|
1108
1242
|
const config = intervals[params.interval];
|
|
@@ -1119,18 +1253,37 @@ export function registerWikiWatch(pi: ExtensionAPI): void {
|
|
|
1119
1253
|
};
|
|
1120
1254
|
}
|
|
1121
1255
|
|
|
1256
|
+
// Robustness for global crontab environments:
|
|
1257
|
+
// * `/bin/bash -lc` runs a LOGIN shell that sources /etc/profile +
|
|
1258
|
+
// ~/.profile / ~/.bash_profile, so npm-global / bun / nvm PATH
|
|
1259
|
+
// additions are imported — cron's default PATH is only
|
|
1260
|
+
// `/usr/bin:/bin` and would not find `pi`.
|
|
1261
|
+
// * `mkdir -p` makes the log dir self-healing for users with only
|
|
1262
|
+
// a project vault (no `~/.llm-wiki/` yet).
|
|
1263
|
+
// * All `$HOME` references are double-quoted to survive paths with spaces.
|
|
1264
|
+
// * `# llm-wiki-autoupdate` tags the line so the user can find and
|
|
1265
|
+
// remove it via `crontab -e` later (see `interval=stop`).
|
|
1266
|
+
const cronLine = `${config.cron} /bin/bash -lc 'mkdir -p "$HOME/.llm-wiki" && pi -p "/wiki-run" >> "$HOME/.llm-wiki/cron.log" 2>&1' # llm-wiki-autoupdate`;
|
|
1267
|
+
|
|
1122
1268
|
return {
|
|
1123
1269
|
content: [
|
|
1124
1270
|
{
|
|
1125
1271
|
type: "text",
|
|
1126
1272
|
text: [
|
|
1127
|
-
`⏰ To set up ${config.label} wiki updates,
|
|
1273
|
+
`⏰ To set up ${config.label} wiki updates, add this line to your crontab.`,
|
|
1274
|
+
"**This tool only prints the line — it does not install it.**",
|
|
1128
1275
|
"",
|
|
1276
|
+
"```bash",
|
|
1277
|
+
"crontab -e",
|
|
1129
1278
|
"```",
|
|
1130
|
-
|
|
1279
|
+
"",
|
|
1280
|
+
"Then append:",
|
|
1281
|
+
"",
|
|
1282
|
+
"```cron",
|
|
1283
|
+
cronLine,
|
|
1131
1284
|
"```",
|
|
1132
1285
|
"",
|
|
1133
|
-
|
|
1286
|
+
`The line uses \`/bin/bash -lc\` so your shell profile (and the \`pi\` binary on npm-global / bun PATH) is loaded. Output goes to \`~/.llm-wiki/cron.log\`. If your system has no \`/bin/bash\`, replace with \`/bin/sh -c\` and ensure \`pi\` is in cron's PATH yourself.`,
|
|
1134
1287
|
].join("\n"),
|
|
1135
1288
|
},
|
|
1136
1289
|
],
|
|
@@ -1138,6 +1291,8 @@ export function registerWikiWatch(pi: ExtensionAPI): void {
|
|
|
1138
1291
|
interval: params.interval,
|
|
1139
1292
|
cronSchedule: config.cron,
|
|
1140
1293
|
label: config.label,
|
|
1294
|
+
cronLine,
|
|
1295
|
+
installed: false,
|
|
1141
1296
|
} as Record<string, unknown>,
|
|
1142
1297
|
};
|
|
1143
1298
|
},
|