skillwiki 0.5.0 → 0.5.3
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/dist/cli.js +202 -44
- package/package.json +1 -1
- package/skills/.claude-plugin/plugin.json +1 -1
- package/skills/.codex-plugin/plugin.json +1 -1
- package/skills/package.json +1 -2
- package/skills/using-skillwiki/SKILL.md +1 -1
- package/skills/wiki-crystallize/SKILL.md +1 -1
- package/skills/wiki-ingest/SKILL.md +1 -1
package/dist/cli.js
CHANGED
|
@@ -59,7 +59,8 @@ var ExitCode = {
|
|
|
59
59
|
SYNC_PUSH_FAILED: 42,
|
|
60
60
|
SYNC_PULL_FAILED: 43,
|
|
61
61
|
BACKUP_SYNC_FAILED: 44,
|
|
62
|
-
BACKUP_RESTORE_CONFLICTS: 45
|
|
62
|
+
BACKUP_RESTORE_CONFLICTS: 45,
|
|
63
|
+
USAGE: 46
|
|
63
64
|
};
|
|
64
65
|
|
|
65
66
|
// ../shared/src/json-output.ts
|
|
@@ -91,7 +92,8 @@ var TypedKnowledgeSchema = z.object({
|
|
|
91
92
|
contradictions: z.array(z.string()).optional(),
|
|
92
93
|
provenance: z.enum(["research", "project", "mixed"]).optional(),
|
|
93
94
|
provenance_projects: z.array(wikilink).optional(),
|
|
94
|
-
work_items: z.array(wikilink).optional()
|
|
95
|
+
work_items: z.array(wikilink).optional(),
|
|
96
|
+
stale_ttl: z.number().int().positive().optional()
|
|
95
97
|
}).superRefine((v, ctx) => {
|
|
96
98
|
if (v.provenance && v.provenance !== "research" && (!v.provenance_projects || v.provenance_projects.length === 0)) {
|
|
97
99
|
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["provenance_projects"], message: "required when provenance != research" });
|
|
@@ -1004,8 +1006,10 @@ function isLegacyCitationStyle(body) {
|
|
|
1004
1006
|
return false;
|
|
1005
1007
|
}
|
|
1006
1008
|
function hasOrphanedCitations(body) {
|
|
1007
|
-
const
|
|
1009
|
+
const noFm = body.replace(FRONTMATTER, "");
|
|
1010
|
+
const stripped = stripFences(noFm);
|
|
1008
1011
|
const lines = stripped.split("\n");
|
|
1012
|
+
const rawLines = noFm.split("\n");
|
|
1009
1013
|
let inSources = false;
|
|
1010
1014
|
let sourcesEnded = false;
|
|
1011
1015
|
let sourcesStartLine = -1;
|
|
@@ -1025,10 +1029,13 @@ function hasOrphanedCitations(body) {
|
|
|
1025
1029
|
}
|
|
1026
1030
|
continue;
|
|
1027
1031
|
}
|
|
1028
|
-
const isListItem = /^\s*[-*]\s+/.test(line);
|
|
1032
|
+
const isListItem = /^\s*(?:[-*]|\d+\.)\s+/.test(line);
|
|
1029
1033
|
const hasMarker = /\^\[raw\//.test(line);
|
|
1034
|
+
const hasBacktickRawPath = /`raw\/[^`]+`/.test(rawLines[i]);
|
|
1030
1035
|
if (isListItem && hasMarker) {
|
|
1031
1036
|
lastNonBlankInSources = i;
|
|
1037
|
+
} else if (isListItem && hasBacktickRawPath) {
|
|
1038
|
+
lastNonBlankInSources = i;
|
|
1032
1039
|
} else if (hasMarker && !isListItem) {
|
|
1033
1040
|
return true;
|
|
1034
1041
|
} else {
|
|
@@ -1835,6 +1842,42 @@ async function runIndexCheck(input) {
|
|
|
1835
1842
|
// src/commands/stale.ts
|
|
1836
1843
|
import { readdir as readdir4, rename as rename2, mkdir as mkdir6, readFile as readFile10 } from "fs/promises";
|
|
1837
1844
|
import { join as join13 } from "path";
|
|
1845
|
+
|
|
1846
|
+
// src/parsers/expiry-annotations.ts
|
|
1847
|
+
var HEADING_RE = /^#{1,6}\s+(.+)$/;
|
|
1848
|
+
var ANNOTATION_RE = /^<!--\s*expires:\s*(\d{4}-\d{2}-\d{2})(?:\s+refresh:\s*(weekly|monthly|quarterly))?(?:\s+source:\s*(\S+))?\s*-->$/;
|
|
1849
|
+
var VALID_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
1850
|
+
function isValidDate(s) {
|
|
1851
|
+
if (!VALID_DATE_RE.test(s)) return false;
|
|
1852
|
+
const d = /* @__PURE__ */ new Date(s + "T00:00:00Z");
|
|
1853
|
+
return !Number.isNaN(d.getTime()) && s === d.toISOString().slice(0, 10);
|
|
1854
|
+
}
|
|
1855
|
+
function parseExpiryAnnotations(content, pagePath) {
|
|
1856
|
+
const lines = content.split("\n");
|
|
1857
|
+
const annotations = [];
|
|
1858
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1859
|
+
const match = lines[i].match(ANNOTATION_RE);
|
|
1860
|
+
if (!match) continue;
|
|
1861
|
+
const expires = match[1];
|
|
1862
|
+
if (!isValidDate(expires)) continue;
|
|
1863
|
+
if (i === 0) continue;
|
|
1864
|
+
const prevLine = lines[i - 1];
|
|
1865
|
+
const headingMatch = prevLine.match(HEADING_RE);
|
|
1866
|
+
if (!headingMatch) continue;
|
|
1867
|
+
annotations.push({
|
|
1868
|
+
page: pagePath,
|
|
1869
|
+
heading: headingMatch[1].trim(),
|
|
1870
|
+
line: i + 1,
|
|
1871
|
+
// 1-indexed
|
|
1872
|
+
expires,
|
|
1873
|
+
refresh: match[2],
|
|
1874
|
+
source: match[3]
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
return annotations;
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
// src/commands/stale.ts
|
|
1838
1881
|
function daysSince(isoDate2) {
|
|
1839
1882
|
return Math.floor((Date.now() - Date.parse(isoDate2)) / 864e5);
|
|
1840
1883
|
}
|
|
@@ -1852,6 +1895,12 @@ async function runStale(input) {
|
|
|
1852
1895
|
projectSlugs = (await readdir4(projectsDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
1853
1896
|
} catch {
|
|
1854
1897
|
}
|
|
1898
|
+
if (input.project) {
|
|
1899
|
+
if (!projectSlugs.includes(input.project)) {
|
|
1900
|
+
return { exitCode: ExitCode.USAGE, result: { ok: false, error: "UNKNOWN_PROJECT", detail: `Project "${input.project}" not found. Available: ${projectSlugs.join(", ") || "(none)"}` } };
|
|
1901
|
+
}
|
|
1902
|
+
projectSlugs = [input.project];
|
|
1903
|
+
}
|
|
1855
1904
|
for (const slug of projectSlugs) {
|
|
1856
1905
|
const workPath = join13(projectsDir, slug, "work");
|
|
1857
1906
|
let entries;
|
|
@@ -1893,9 +1942,10 @@ async function runStale(input) {
|
|
|
1893
1942
|
function extractSlug(projectField) {
|
|
1894
1943
|
return projectField.replace(/^\[\[/, "").replace(/\]\]$/, "").replace(/^"|"$/g, "");
|
|
1895
1944
|
}
|
|
1945
|
+
const TERMINAL_STATUSES = /* @__PURE__ */ new Set(["completed", "abandoned", "done", "invalid"]);
|
|
1896
1946
|
const KIND_FROM_FILENAME = /^(?:\d{4}-\d{2}-\d{2})-(task|bug|idea|note|observation)-.+\.md$/;
|
|
1897
1947
|
const LOOP_CYCLE_PATTERN = /loop-cycle-/;
|
|
1898
|
-
|
|
1948
|
+
let transcripts = scan.data.raw.filter((p) => p.relPath.startsWith("raw/transcripts/") && p.relPath.endsWith(".md"));
|
|
1899
1949
|
const claimedPaths = /* @__PURE__ */ new Set();
|
|
1900
1950
|
const transcriptMeta = /* @__PURE__ */ new Map();
|
|
1901
1951
|
for (const t of transcripts) {
|
|
@@ -1904,6 +1954,7 @@ async function runStale(input) {
|
|
|
1904
1954
|
const fm = extractFrontmatter(content);
|
|
1905
1955
|
let kind = fm.ok && typeof fm.data.kind === "string" ? fm.data.kind : "";
|
|
1906
1956
|
let project = fm.ok && typeof fm.data.project === "string" ? fm.data.project : "";
|
|
1957
|
+
if (input.project && !project.includes(input.project)) continue;
|
|
1907
1958
|
let inferred = false;
|
|
1908
1959
|
if (input.forceScan && !kind) {
|
|
1909
1960
|
const basename = t.relPath.split("/").pop();
|
|
@@ -1948,7 +1999,7 @@ async function runStale(input) {
|
|
|
1948
1999
|
const overlap = wWords.filter((w) => tWords.has(w)).length;
|
|
1949
2000
|
if (dirName.includes(tSlug) || tSlug.includes(wSlug) || overlap >= 1) {
|
|
1950
2001
|
claimedPaths.add(t.relPath);
|
|
1951
|
-
if (status
|
|
2002
|
+
if (TERMINAL_STATUSES.has(status)) {
|
|
1952
2003
|
staleTranscripts.push({ path: t.relPath, reason: `work item projects/${slug}/work/${dirName} is ${status}` });
|
|
1953
2004
|
}
|
|
1954
2005
|
break;
|
|
@@ -1958,7 +2009,7 @@ async function runStale(input) {
|
|
|
1958
2009
|
for (const [dir, status] of workDirs) {
|
|
1959
2010
|
if (dir.split("/").pop().startsWith(datePrefix)) {
|
|
1960
2011
|
claimedPaths.add(t.relPath);
|
|
1961
|
-
if (status
|
|
2012
|
+
if (TERMINAL_STATUSES.has(status)) {
|
|
1962
2013
|
staleTranscripts.push({ path: t.relPath, reason: `work item ${dir} is ${status}` });
|
|
1963
2014
|
}
|
|
1964
2015
|
break;
|
|
@@ -2003,10 +2054,8 @@ async function runStale(input) {
|
|
|
2003
2054
|
continue;
|
|
2004
2055
|
}
|
|
2005
2056
|
const hasSpec = files.includes("spec.md"), hasPlan = files.includes("plan.md"), hasWI = files.includes("work-item.md");
|
|
2006
|
-
if (status
|
|
2007
|
-
doneWorkItems.push({ path: relDir, reason: "completed \u2014 should be archived
|
|
2008
|
-
} else if (status === "invalid") {
|
|
2009
|
-
doneWorkItems.push({ path: relDir, reason: "invalid \u2014 should be archived" });
|
|
2057
|
+
if (TERMINAL_STATUSES.has(status)) {
|
|
2058
|
+
doneWorkItems.push({ path: relDir, reason: `${status || "completed"} \u2014 should be archived` });
|
|
2010
2059
|
} else if (hasSpec && !hasPlan) {
|
|
2011
2060
|
incompleteWorkItems.push({ path: relDir, reason: "has spec but no plan" });
|
|
2012
2061
|
} else if (hasWI && !hasSpec && !hasPlan) {
|
|
@@ -2019,16 +2068,53 @@ async function runStale(input) {
|
|
|
2019
2068
|
const text = await readFile10(join13(input.vault, page.relPath), "utf8");
|
|
2020
2069
|
const fm = extractFrontmatter(text);
|
|
2021
2070
|
if (fm.ok && typeof fm.data.updated === "string") {
|
|
2071
|
+
if (input.project) {
|
|
2072
|
+
const pp = fm.data.provenance_projects;
|
|
2073
|
+
const linked = Array.isArray(pp) && pp.some((p) => String(p).includes(input.project));
|
|
2074
|
+
if (!linked) continue;
|
|
2075
|
+
}
|
|
2022
2076
|
const age = daysSince(fm.data.updated);
|
|
2023
|
-
|
|
2024
|
-
|
|
2077
|
+
const threshold = typeof fm.data.stale_ttl === "number" && fm.data.stale_ttl > 0 ? fm.data.stale_ttl : input.days;
|
|
2078
|
+
if (age >= threshold) {
|
|
2079
|
+
stale.push({ page: page.relPath, reason: `updated ${age} days ago (threshold: ${threshold})` });
|
|
2025
2080
|
}
|
|
2026
2081
|
}
|
|
2027
2082
|
} catch {
|
|
2028
2083
|
}
|
|
2029
2084
|
}
|
|
2085
|
+
const staleSections = [];
|
|
2086
|
+
for (const page of scan.data.typedKnowledge) {
|
|
2087
|
+
try {
|
|
2088
|
+
const text = await readFile10(join13(input.vault, page.relPath), "utf8");
|
|
2089
|
+
const projectFilter = input.project;
|
|
2090
|
+
if (projectFilter) {
|
|
2091
|
+
const fm = extractFrontmatter(text);
|
|
2092
|
+
if (fm.ok) {
|
|
2093
|
+
const pp = fm.data.provenance_projects;
|
|
2094
|
+
const linked = Array.isArray(pp) && pp.some((p) => String(p).includes(projectFilter));
|
|
2095
|
+
if (!linked) continue;
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
const annotations = parseExpiryAnnotations(text, page.relPath);
|
|
2099
|
+
for (const ann of annotations) {
|
|
2100
|
+
if (daysSince(ann.expires) >= 0) {
|
|
2101
|
+
staleSections.push({
|
|
2102
|
+
page: ann.page,
|
|
2103
|
+
heading: ann.heading,
|
|
2104
|
+
line: ann.line,
|
|
2105
|
+
expires: ann.expires,
|
|
2106
|
+
refresh: ann.refresh,
|
|
2107
|
+
source: ann.source,
|
|
2108
|
+
reason: `section "${ann.heading}" expired on ${ann.expires}`
|
|
2109
|
+
});
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
} catch {
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2030
2116
|
if (input.archive) {
|
|
2031
|
-
const archiveDir = join13(input.vault, "_archive",
|
|
2117
|
+
const archiveDir = join13(input.vault, "_archive", today);
|
|
2032
2118
|
await mkdir6(archiveDir, { recursive: true });
|
|
2033
2119
|
const citedRawPaths = /* @__PURE__ */ new Set();
|
|
2034
2120
|
for (const page of scan.data.typedKnowledge) {
|
|
@@ -2082,7 +2168,7 @@ async function runStale(input) {
|
|
|
2082
2168
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2083
2169
|
});
|
|
2084
2170
|
}
|
|
2085
|
-
const total = stale.length + staleTranscripts.length + unclaimedTranscripts.length + incompleteWorkItems.length + doneWorkItems.length;
|
|
2171
|
+
const total = stale.length + staleTranscripts.length + unclaimedTranscripts.length + incompleteWorkItems.length + doneWorkItems.length + staleSections.length;
|
|
2086
2172
|
const hintLines = [];
|
|
2087
2173
|
if (stale.length > 0) hintLines.push(`stale_pages: ${stale.length}`, ...stale.map((p) => ` ${p.page}: ${p.reason}`));
|
|
2088
2174
|
if (staleTranscripts.length > 0) hintLines.push(`stale_transcripts: ${staleTranscripts.length}`, ...staleTranscripts.map((t) => ` ${t.path}: ${t.reason}`));
|
|
@@ -2090,6 +2176,7 @@ async function runStale(input) {
|
|
|
2090
2176
|
hint: ${t.hint}` : ""}`));
|
|
2091
2177
|
if (incompleteWorkItems.length > 0) hintLines.push(`incomplete_work_items: ${incompleteWorkItems.length}`, ...incompleteWorkItems.map((w) => ` ${w.path}: ${w.reason}`));
|
|
2092
2178
|
if (doneWorkItems.length > 0) hintLines.push(`done_work_items: ${doneWorkItems.length}`, ...doneWorkItems.map((w) => ` ${w.path}: ${w.reason}`));
|
|
2179
|
+
if (staleSections.length > 0) hintLines.push(`stale_sections: ${staleSections.length}`, ...staleSections.map((s) => ` ${s.page}#${s.heading}: ${s.reason}`));
|
|
2093
2180
|
if (archived.length > 0) hintLines.push(`archived: ${archived.length}`, ...archived.map((a) => ` ${a}`));
|
|
2094
2181
|
if (hintLines.length === 0) hintLines.push("no stale transcripts or incomplete work items");
|
|
2095
2182
|
return { exitCode: total > 0 ? ExitCode.STALE_PAGE : ExitCode.OK, result: ok({
|
|
@@ -2098,6 +2185,7 @@ async function runStale(input) {
|
|
|
2098
2185
|
unclaimed_transcripts: unclaimedTranscripts,
|
|
2099
2186
|
incomplete_work_items: incompleteWorkItems,
|
|
2100
2187
|
done_work_items: doneWorkItems,
|
|
2188
|
+
stale_sections: staleSections,
|
|
2101
2189
|
archived,
|
|
2102
2190
|
humanHint: hintLines.join("\n")
|
|
2103
2191
|
}) };
|
|
@@ -2466,11 +2554,11 @@ function buildCliSurface() {
|
|
|
2466
2554
|
program2.command("index-check").option("--wiki <name>");
|
|
2467
2555
|
program2.command("index-link-format").option("--wiki <name>");
|
|
2468
2556
|
program2.command("topic-map-check").option("--threshold <n>").option("--wiki <name>");
|
|
2469
|
-
program2.command("stale").option("--archive").option("--days <n>").option("--force-scan").option("--wiki <name>");
|
|
2557
|
+
program2.command("stale").option("--archive").option("--days <n>").option("--force-scan").option("--project <slug>").option("--refresh").option("--wiki <name>");
|
|
2470
2558
|
program2.command("claim").option("--project <slug>").option("--slug <slug>").option("--wiki <name>");
|
|
2471
2559
|
program2.command("pagesize").option("--lines <n>").option("--wiki <name>");
|
|
2472
2560
|
program2.command("log-rotate").option("--threshold <n>").option("--apply").option("--wiki <name>");
|
|
2473
|
-
program2.command("lint").option("--days <n>").option("--lines <n>").option("--log-threshold <n>").option("--fix").option("--wiki <name>");
|
|
2561
|
+
program2.command("lint").option("--days <n>").option("--lines <n>").option("--log-threshold <n>").option("--fix").option("--only <bucket>").option("--wiki <name>");
|
|
2474
2562
|
program2.command("config");
|
|
2475
2563
|
program2.command("doctor");
|
|
2476
2564
|
program2.command("status").option("--wiki <name>");
|
|
@@ -2615,8 +2703,8 @@ function extractSourceEntries(rawFm) {
|
|
|
2615
2703
|
return entries;
|
|
2616
2704
|
}
|
|
2617
2705
|
var ERROR_ORDER = ["broken_wikilinks", "invalid_frontmatter", "raw_dedup", "broken_sources", "tag_not_in_taxonomy"];
|
|
2618
|
-
var WARNING_ORDER = ["raw_body_duplicate", "raw_subdirectory_duplicate", "index_incomplete", "index_link_format", "stale_page", "page_too_large", "log_rotate_needed", "orphans", "compound_refs", "legacy_citation_style", "orphaned_citations", "duplicate_frontmatter", "work_item_health", "orphaned_project_pages", "missing_overview"];
|
|
2619
|
-
var INFO_ORDER = ["bridges", "page_structure", "topic_map_recommended", "frontmatter_wikilink", "wikilink_citation", "missing_tldr", "
|
|
2706
|
+
var WARNING_ORDER = ["raw_body_duplicate", "raw_subdirectory_duplicate", "index_incomplete", "index_link_format", "stale_page", "page_too_large", "log_rotate_needed", "orphans", "compound_refs", "legacy_citation_style", "orphaned_citations", "duplicate_frontmatter", "work_item_health", "orphaned_project_pages", "missing_overview", "missing_diagram"];
|
|
2707
|
+
var INFO_ORDER = ["bridges", "page_structure", "topic_map_recommended", "frontmatter_wikilink", "wikilink_citation", "missing_tldr", "stale_sections", "cli_refs"];
|
|
2620
2708
|
async function runLint(input) {
|
|
2621
2709
|
const buckets = {};
|
|
2622
2710
|
const fixed = [];
|
|
@@ -2741,7 +2829,7 @@ async function runLint(input) {
|
|
|
2741
2829
|
const hasOverview = /^## Overview/m.test(body);
|
|
2742
2830
|
if (!hasOverview) noOverview.push(page.relPath);
|
|
2743
2831
|
const bodyFirst15 = body.split("\n").slice(0, 15).join("\n");
|
|
2744
|
-
if (!/^##\s+TL;\s*DR/m.test(bodyFirst15)) missingTldrFlags.push(page.relPath);
|
|
2832
|
+
if (!/^>\s*\*\*TL;DR:?\*\*/m.test(bodyFirst15) && !/^##\s+TL;\s*DR/m.test(bodyFirst15)) missingTldrFlags.push(page.relPath);
|
|
2745
2833
|
const fmData = extractFrontmatter(text);
|
|
2746
2834
|
const pageTags = fmData.ok && Array.isArray(fmData.data.tags) ? fmData.data.tags : [];
|
|
2747
2835
|
if (pageTags.includes("architecture") && !body.includes("```mermaid")) {
|
|
@@ -2785,7 +2873,7 @@ async function runLint(input) {
|
|
|
2785
2873
|
const text = await readPage(specPage);
|
|
2786
2874
|
const fm = extractFrontmatter(text);
|
|
2787
2875
|
if (fm.ok) {
|
|
2788
|
-
specStatus = fm.data.status;
|
|
2876
|
+
specStatus = typeof fm.data.status === "string" ? fm.data.status : void 0;
|
|
2789
2877
|
specStarted = fm.data.started;
|
|
2790
2878
|
}
|
|
2791
2879
|
}
|
|
@@ -2840,6 +2928,27 @@ async function runLint(input) {
|
|
|
2840
2928
|
}
|
|
2841
2929
|
}
|
|
2842
2930
|
if (cliRefFlags.length > 0) buckets.cli_refs = cliRefFlags;
|
|
2931
|
+
const staleSectionFlags = [];
|
|
2932
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2933
|
+
const approachingThreshold = 7;
|
|
2934
|
+
for (const page of scan.data.typedKnowledge) {
|
|
2935
|
+
try {
|
|
2936
|
+
const text = await readPage(page);
|
|
2937
|
+
const annotations = parseExpiryAnnotations(text, page.relPath);
|
|
2938
|
+
for (const ann of annotations) {
|
|
2939
|
+
if (ann.expires < today) {
|
|
2940
|
+
staleSectionFlags.push(`${page.relPath}: section "${ann.heading}" expired on ${ann.expires}`);
|
|
2941
|
+
} else {
|
|
2942
|
+
const daysUntilExpiry = Math.floor((Date.parse(ann.expires) - Date.now()) / 864e5);
|
|
2943
|
+
if (daysUntilExpiry <= approachingThreshold) {
|
|
2944
|
+
staleSectionFlags.push(`${page.relPath}: section "${ann.heading}" expires in ${daysUntilExpiry} day(s) (${ann.expires})`);
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
} catch {
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
if (staleSectionFlags.length > 0) buckets.stale_sections = staleSectionFlags;
|
|
2843
2952
|
if (input.fix && legacyPages.length > 0) {
|
|
2844
2953
|
const FENCE_RE2 = /```[\s\S]*?```/g;
|
|
2845
2954
|
const INLINE_MARKER = /\^\[raw\/[^\]]+\]/g;
|
|
@@ -2986,16 +3095,26 @@ ${trimmedBody}`;
|
|
|
2986
3095
|
}
|
|
2987
3096
|
const body = split.data.body;
|
|
2988
3097
|
const rawFm = split.data.rawFrontmatter;
|
|
2989
|
-
const
|
|
3098
|
+
const lines = body.split("\n");
|
|
3099
|
+
let insertIndex = 0;
|
|
3100
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3101
|
+
if (/^# /.test(lines[i])) {
|
|
3102
|
+
insertIndex = i + 1;
|
|
3103
|
+
while (insertIndex < lines.length && lines[insertIndex].trim() === "") {
|
|
3104
|
+
insertIndex++;
|
|
3105
|
+
}
|
|
3106
|
+
break;
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
if (insertIndex === 0) {
|
|
3110
|
+
lines.splice(0, 0, "", "> **TL;DR:** ");
|
|
3111
|
+
} else {
|
|
3112
|
+
lines.splice(insertIndex, 0, "> **TL;DR:** ");
|
|
3113
|
+
}
|
|
3114
|
+
const trimmedFm = rawFm.endsWith("\n") ? rawFm : rawFm + "\n";
|
|
2990
3115
|
const newContent = `---
|
|
2991
|
-
${
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
## TL;DR
|
|
2995
|
-
|
|
2996
|
-
- Pending summary.
|
|
2997
|
-
|
|
2998
|
-
${trimmedBody}`;
|
|
3116
|
+
${trimmedFm}---
|
|
3117
|
+
${lines.join("\n")}`;
|
|
2999
3118
|
await writeFile8(absPath, newContent, "utf8");
|
|
3000
3119
|
fixed.push(relPath);
|
|
3001
3120
|
} catch {
|
|
@@ -3096,6 +3215,38 @@ ${newBody}`;
|
|
|
3096
3215
|
const errorOut = ERROR_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
|
|
3097
3216
|
const warningOut = WARNING_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
|
|
3098
3217
|
const infoOut = INFO_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
|
|
3218
|
+
if (input.only) {
|
|
3219
|
+
const allKnown = [...ERROR_ORDER, ...WARNING_ORDER, ...INFO_ORDER];
|
|
3220
|
+
if (!allKnown.includes(input.only)) {
|
|
3221
|
+
return {
|
|
3222
|
+
exitCode: ExitCode.USAGE,
|
|
3223
|
+
result: { ok: false, error: "UNKNOWN_BUCKET", detail: `Unknown bucket "${input.only}". Valid: ${allKnown.join(", ")}` }
|
|
3224
|
+
};
|
|
3225
|
+
}
|
|
3226
|
+
const match = [...errorOut, ...warningOut, ...infoOut].filter((b) => b.kind === input.only);
|
|
3227
|
+
const severity = ERROR_ORDER.includes(input.only) ? "error" : WARNING_ORDER.includes(input.only) ? "warning" : "info";
|
|
3228
|
+
const filtered = severity === "error" ? { error: match, warning: [], info: [] } : severity === "warning" ? { error: [], warning: match, info: [] } : { error: [], warning: [], info: match };
|
|
3229
|
+
const fSummary = {
|
|
3230
|
+
errors: filtered.error.reduce((n, b) => n + b.items.length, 0),
|
|
3231
|
+
warnings: filtered.warning.reduce((n, b) => n + b.items.length, 0),
|
|
3232
|
+
info: filtered.info.reduce((n, b) => n + b.items.length, 0)
|
|
3233
|
+
};
|
|
3234
|
+
let fExit = ExitCode.OK;
|
|
3235
|
+
if (fSummary.errors > 0) fExit = ExitCode.LINT_HAS_ERRORS;
|
|
3236
|
+
else if (fSummary.warnings > 0 || fSummary.info > 0) fExit = ExitCode.LINT_HAS_WARNINGS;
|
|
3237
|
+
return {
|
|
3238
|
+
exitCode: fExit,
|
|
3239
|
+
result: ok({
|
|
3240
|
+
vault: { path: input.vault, source: input.source ?? "resolved" },
|
|
3241
|
+
summary: fSummary,
|
|
3242
|
+
by_severity: filtered,
|
|
3243
|
+
fixed,
|
|
3244
|
+
unresolved,
|
|
3245
|
+
humanHint: `--only ${input.only}
|
|
3246
|
+
${match.length === 0 ? "0 violations" : match.map((b) => ` ${b.kind}: ${b.items.length}`).join("\n")}`
|
|
3247
|
+
})
|
|
3248
|
+
};
|
|
3249
|
+
}
|
|
3099
3250
|
const summary = {
|
|
3100
3251
|
errors: errorOut.reduce((n, b) => n + b.items.length, 0),
|
|
3101
3252
|
warnings: warningOut.reduce((n, b) => n + b.items.length, 0),
|
|
@@ -3203,7 +3354,7 @@ import { platform } from "os";
|
|
|
3203
3354
|
|
|
3204
3355
|
// src/utils/auto-update.ts
|
|
3205
3356
|
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
|
|
3206
|
-
import { join as join20, dirname as
|
|
3357
|
+
import { join as join20, dirname as dirname7 } from "path";
|
|
3207
3358
|
import { spawn } from "child_process";
|
|
3208
3359
|
|
|
3209
3360
|
// src/utils/update-consts.ts
|
|
@@ -3233,7 +3384,7 @@ function readCache(home) {
|
|
|
3233
3384
|
}
|
|
3234
3385
|
function writeCache(home, cache) {
|
|
3235
3386
|
const p = cachePath(home);
|
|
3236
|
-
mkdirSync2(
|
|
3387
|
+
mkdirSync2(dirname7(p), { recursive: true });
|
|
3237
3388
|
writeFileSync3(p, JSON.stringify(cache, null, 2));
|
|
3238
3389
|
}
|
|
3239
3390
|
function latestFromCache(home, currentVersion) {
|
|
@@ -3782,7 +3933,7 @@ async function runDoctor(input) {
|
|
|
3782
3933
|
|
|
3783
3934
|
// src/commands/archive.ts
|
|
3784
3935
|
import { rename as rename4, mkdir as mkdir8, readFile as readFile16, writeFile as writeFile9 } from "fs/promises";
|
|
3785
|
-
import { join as join23, dirname as
|
|
3936
|
+
import { join as join23, dirname as dirname8 } from "path";
|
|
3786
3937
|
async function runArchive(input) {
|
|
3787
3938
|
const scan = await scanVault(input.vault);
|
|
3788
3939
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
@@ -3799,7 +3950,7 @@ async function runArchive(input) {
|
|
|
3799
3950
|
if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
|
|
3800
3951
|
if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
|
|
3801
3952
|
const archivePath = join23("_archive", relPath).replace(/\\/g, "/");
|
|
3802
|
-
await mkdir8(
|
|
3953
|
+
await mkdir8(dirname8(join23(input.vault, archivePath)), { recursive: true });
|
|
3803
3954
|
let indexUpdated = false;
|
|
3804
3955
|
if (!isRaw) {
|
|
3805
3956
|
const indexPath = join23(input.vault, "index.md");
|
|
@@ -4461,7 +4612,7 @@ async function runTranscripts(input) {
|
|
|
4461
4612
|
|
|
4462
4613
|
// src/commands/project-index.ts
|
|
4463
4614
|
import { readdir as readdir6, readFile as readFile18, writeFile as writeFile13, mkdir as mkdir9 } from "fs/promises";
|
|
4464
|
-
import { join as join27, dirname as
|
|
4615
|
+
import { join as join27, dirname as dirname9 } from "path";
|
|
4465
4616
|
var LAYER2_DIRS = ["entities", "concepts", "comparisons", "queries", "meta"];
|
|
4466
4617
|
async function runProjectIndex(input) {
|
|
4467
4618
|
const slug = input.slug;
|
|
@@ -4575,7 +4726,7 @@ Autogenerated by \`skillwiki project-index\` on ${today}.
|
|
|
4575
4726
|
}
|
|
4576
4727
|
if (input.apply) {
|
|
4577
4728
|
try {
|
|
4578
|
-
await mkdir9(
|
|
4729
|
+
await mkdir9(dirname9(indexPath), { recursive: true });
|
|
4579
4730
|
await writeFile13(indexPath, body, "utf8");
|
|
4580
4731
|
} catch (e) {
|
|
4581
4732
|
return {
|
|
@@ -5630,7 +5781,7 @@ async function runSyncPull(input) {
|
|
|
5630
5781
|
|
|
5631
5782
|
// src/commands/backup.ts
|
|
5632
5783
|
import { statSync as statSync4, readdirSync as readdirSync2, readFileSync as readFileSync9, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
5633
|
-
import { join as join32, relative as relative3, dirname as
|
|
5784
|
+
import { join as join32, relative as relative3, dirname as dirname10 } from "path";
|
|
5634
5785
|
import { PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, GetObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
|
|
5635
5786
|
|
|
5636
5787
|
// src/utils/s3-client.ts
|
|
@@ -5766,7 +5917,7 @@ async function runBackupRestore(input) {
|
|
|
5766
5917
|
const resp = await client.send(new GetObjectCommand({ Bucket: input.bucket, Key: obj.Key }));
|
|
5767
5918
|
const body = await resp.Body?.transformToByteArray();
|
|
5768
5919
|
if (body) {
|
|
5769
|
-
mkdirSync3(
|
|
5920
|
+
mkdirSync3(dirname10(localPath), { recursive: true });
|
|
5770
5921
|
writeFileSync4(localPath, Buffer.from(body));
|
|
5771
5922
|
downloaded++;
|
|
5772
5923
|
}
|
|
@@ -6451,10 +6602,10 @@ program.command("topic-map-check [vault]").description("check whether a topic ma
|
|
|
6451
6602
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
6452
6603
|
else emit(await runTopicMapCheck({ vault: v.vault, threshold: opts.threshold }), v.vault);
|
|
6453
6604
|
});
|
|
6454
|
-
program.command("stale [vault]").description("identify stale transcripts and incomplete work items").option("--archive", "move stale items to _archive/", false).option("--days <n>", "staleness threshold in days", (s) => parseInt(s, 10), 3).option("--force-scan", "infer kind/project from filename and content when frontmatter is missing", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
6605
|
+
program.command("stale [vault]").description("identify stale transcripts and incomplete work items").option("--archive", "move stale items to _archive/", false).option("--days <n>", "staleness threshold in days", (s) => parseInt(s, 10), 3).option("--force-scan", "infer kind/project from filename and content when frontmatter is missing", false).option("--project <slug>", "scope to a single project").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
6455
6606
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
6456
6607
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
6457
|
-
else emit(await runStale({ vault: v.vault, days: opts.days, archive: !!opts.archive, forceScan: !!opts.forceScan }), v.vault);
|
|
6608
|
+
else emit(await runStale({ vault: v.vault, days: opts.days, archive: !!opts.archive, forceScan: !!opts.forceScan, project: opts.project }), v.vault);
|
|
6458
6609
|
});
|
|
6459
6610
|
program.command("claim <transcript> [vault]").description("claim an unclaimed transcript by creating a work item with source: link").option("--project <slug>", "project slug (overrides transcript frontmatter)").option("--slug <slug>", "work-item slug (defaults to transcript filename without date/kind prefix)").option("--wiki <name>", "wiki profile name").action(async (transcript, vault, opts) => {
|
|
6460
6611
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
@@ -6471,7 +6622,7 @@ program.command("log-rotate [vault]").description("rotate or trim the vault log
|
|
|
6471
6622
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
6472
6623
|
else emit(await runLogRotate({ vault: v.vault, threshold: opts.threshold, apply: !!opts.apply }), v.vault);
|
|
6473
6624
|
});
|
|
6474
|
-
program.command("lint [vault]").description("run all vault health checks").option("--days <n>", "stale threshold", (s) => parseInt(s, 10), 90).option("--lines <n>", "pagesize threshold", (s) => parseInt(s, 10), 200).option("--log-threshold <n>", "log rotation threshold", (s) => parseInt(s, 10), 500).option("--fix", "auto-fix legacy_citation_style violations").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
6625
|
+
program.command("lint [vault]").description("run all vault health checks").option("--days <n>", "stale threshold", (s) => parseInt(s, 10), 90).option("--lines <n>", "pagesize threshold", (s) => parseInt(s, 10), 200).option("--log-threshold <n>", "log rotation threshold", (s) => parseInt(s, 10), 500).option("--fix", "auto-fix legacy_citation_style violations").option("--only <bucket>", "run only the specified lint bucket").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
6475
6626
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
6476
6627
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
6477
6628
|
else emit(await runLint({
|
|
@@ -6480,7 +6631,8 @@ program.command("lint [vault]").description("run all vault health checks").optio
|
|
|
6480
6631
|
days: opts.days,
|
|
6481
6632
|
lines: opts.lines,
|
|
6482
6633
|
logThreshold: opts.logThreshold,
|
|
6483
|
-
fix: opts.fix ?? false
|
|
6634
|
+
fix: opts.fix ?? false,
|
|
6635
|
+
only: opts.only
|
|
6484
6636
|
}), v.vault);
|
|
6485
6637
|
});
|
|
6486
6638
|
var configCmd = program.command("config").description("manage skillwiki configuration");
|
|
@@ -6587,7 +6739,10 @@ syncCmd.command("pull [vault]").description("pull remote vault changes and lint"
|
|
|
6587
6739
|
var backupCmd = program.command("backup").description("manage S3-compatible remote backup");
|
|
6588
6740
|
backupCmd.command("sync [vault]").description("sync vault to S3-compatible remote backup").option("--dry-run", "list actions without executing").option("--bucket <name>", "S3 bucket name").option("--endpoint <url>", "S3 endpoint URL").option("--region <region>", "S3 region", "us-east-1").option("--prune", "delete orphaned S3 objects not in vault", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
6589
6741
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
6590
|
-
if (!v.ok)
|
|
6742
|
+
if (!v.ok) {
|
|
6743
|
+
emit({ exitCode: v.exitCode, result: v.payload });
|
|
6744
|
+
return;
|
|
6745
|
+
}
|
|
6591
6746
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
|
|
6592
6747
|
const dotenv = await parseDotenvFile(configPath(home));
|
|
6593
6748
|
emit(await runBackupSync({
|
|
@@ -6603,7 +6758,10 @@ backupCmd.command("sync [vault]").description("sync vault to S3-compatible remot
|
|
|
6603
6758
|
});
|
|
6604
6759
|
backupCmd.command("restore [vault]").description("restore vault from S3-compatible remote backup").option("--bucket <name>", "S3 bucket name").option("--endpoint <url>", "S3 endpoint URL").option("--region <region>", "S3 region", "us-east-1").option("--target <dir>", "restore target directory (defaults to vault)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
6605
6760
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
6606
|
-
if (!v.ok)
|
|
6761
|
+
if (!v.ok) {
|
|
6762
|
+
emit({ exitCode: v.exitCode, result: v.payload });
|
|
6763
|
+
return;
|
|
6764
|
+
}
|
|
6607
6765
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
|
|
6608
6766
|
const dotenv = await parseDotenvFile(configPath(home));
|
|
6609
6767
|
emit(await runBackupRestore({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skillwiki",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"skills": "./",
|
|
5
5
|
"description": "Project-aware Karpathy-style knowledge base for Claude Code: 18 prompt-only skills (wiki-*, proj-*, using-skillwiki) backed by the deterministic `skillwiki` CLI.",
|
|
6
6
|
"author": {
|
package/skills/package.json
CHANGED
|
@@ -71,7 +71,7 @@ sha256: # computed by skillwiki hash over body bytes after closing ---
|
|
|
71
71
|
| `wiki-canvas` | Generate Obsidian Canvas visualization from vault graph data |
|
|
72
72
|
| `proj-decide` | Write an Architectural Decision Record (ADR) |
|
|
73
73
|
| `wiki-gate-plan-mode` | Toggle EnterPlanMode gating — force superpowers planning instead of built-in plan mode |
|
|
74
|
-
| `dev-loop
|
|
74
|
+
| `dev-loop:research` | Research agent for dev-loop IDLE — scans repo + vault health, outputs prioritized work-item recommendations (formerly `/dev-loop-research`) |
|
|
75
75
|
## CLI Backbone
|
|
76
76
|
All skills are backed by the `skillwiki` CLI — a deterministic tool with no LLM calls. It handles path resolution, config management, validation, and linting. Skills invoke it via Bash for the mechanical parts and use Claude for the creative parts.
|
|
77
77
|
Key CLI subcommands: `init`, `lint`, `config`, `doctor`, `path`, `lang`, `install`, `graph build`, `archive`, `drift`, `compound`, `tag-sync`, `sync status`, `seed`, `stale`, `observe`, `canvas generate`.
|
|
@@ -16,7 +16,7 @@ Standard four reads. If cwd is inside `projects/{slug}/`, also read project READ
|
|
|
16
16
|
1. Identify type: entity / concept / comparison / query / summary.
|
|
17
17
|
2. Set `provenance:`. Default `research`. If in project context: `project` with `provenance_projects: ["[[slug]]"]`.
|
|
18
18
|
3. Compose the page with citations pre-attached. Reuse existing `raw/` sources where possible. Every page MUST include:
|
|
19
|
-
-
|
|
19
|
+
- `> **TL;DR:**` blockquote as the first content after the title heading — a one-sentence summary of the page's key takeaway (under 200 chars). See SCHEMA.md `## TL;DR Convention`.
|
|
20
20
|
- For pages tagged `architecture` or explaining workflows/systems: include a Mermaid diagram (`graph TB` or `sequenceDiagram`) in the body. Follow Obsidian-compatible Mermaid rules (see SCHEMA.md `## Mermaid Diagrams`).
|
|
21
21
|
4. `skillwiki validate <page>`. If non-zero, STOP.
|
|
22
22
|
5. Apply writes: page → `index.md` → `log.md`.
|
|
@@ -21,7 +21,7 @@ Run `skillwiki lang` at the start. Generate page-body prose, narrative sections,
|
|
|
21
21
|
2. **Fetch.** Use `web_fetch` (or read local file) under Layer 2 controls (the CLI Layer 2 fetcher applies in tests; in skill runtime use `web_fetch` directly and treat any error as STOP).
|
|
22
22
|
3. **Hash.** Write the raw file (frontmatter + body). Run `skillwiki hash <raw-file>` and embed the result in raw frontmatter `sha256:`.
|
|
23
23
|
4. **Generate page(s).** Compose typed-knowledge page(s) with citations pre-attached (`^[raw/...]` markers). Every page MUST include:
|
|
24
|
-
-
|
|
24
|
+
- `> **TL;DR:**` blockquote as the first content after the title heading — a one-sentence summary of the page's key takeaway (under 200 chars). See SCHEMA.md `## TL;DR Convention`.
|
|
25
25
|
- For pages tagged `architecture` or explaining workflows/systems: include a Mermaid diagram (`graph TB` or `sequenceDiagram`) in the body. Follow Obsidian-compatible Mermaid rules (see SCHEMA.md `## Mermaid Diagrams`).
|
|
26
26
|
5. **Validate.** For each generated page: run `skillwiki validate <page>`. If exit ≠ 0, STOP — do not write index/log.
|
|
27
27
|
6. **Apply writes in order.** raw → page(s) → `index.md` → `log.md`.
|