skillwiki 0.2.6 → 0.3.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.
- package/dist/cli.js +106 -11
- package/package.json +1 -1
- package/skills/.claude-plugin/plugin.json +1 -1
- package/skills/package.json +1 -1
- package/skills/wiki-add-task/SKILL.md +24 -11
- package/templates/capture.md +11 -0
package/dist/cli.js
CHANGED
|
@@ -1845,6 +1845,7 @@ async function runStale(input) {
|
|
|
1845
1845
|
const incompleteWorkItems = [];
|
|
1846
1846
|
const archived = [];
|
|
1847
1847
|
const workDirs = /* @__PURE__ */ new Map();
|
|
1848
|
+
const workDirsBySlug = /* @__PURE__ */ new Map();
|
|
1848
1849
|
const projectsDir = join13(input.vault, "projects");
|
|
1849
1850
|
let projectSlugs = [];
|
|
1850
1851
|
try {
|
|
@@ -1859,6 +1860,7 @@ async function runStale(input) {
|
|
|
1859
1860
|
} catch {
|
|
1860
1861
|
continue;
|
|
1861
1862
|
}
|
|
1863
|
+
const slugDirs = /* @__PURE__ */ new Map();
|
|
1862
1864
|
for (const e of entries) {
|
|
1863
1865
|
if (!e.isDirectory()) continue;
|
|
1864
1866
|
const relDir = `projects/${slug}/work/${e.name}`;
|
|
@@ -1869,6 +1871,7 @@ async function runStale(input) {
|
|
|
1869
1871
|
files = await readdir4(absDir);
|
|
1870
1872
|
} catch {
|
|
1871
1873
|
workDirs.set(relDir, "");
|
|
1874
|
+
slugDirs.set(e.name, "");
|
|
1872
1875
|
continue;
|
|
1873
1876
|
}
|
|
1874
1877
|
for (const f of files) {
|
|
@@ -1883,16 +1886,106 @@ async function runStale(input) {
|
|
|
1883
1886
|
}
|
|
1884
1887
|
}
|
|
1885
1888
|
workDirs.set(relDir, status);
|
|
1889
|
+
slugDirs.set(e.name, status);
|
|
1886
1890
|
}
|
|
1891
|
+
workDirsBySlug.set(slug, slugDirs);
|
|
1887
1892
|
}
|
|
1893
|
+
function extractSlug(projectField) {
|
|
1894
|
+
return projectField.replace(/^\[\[/, "").replace(/\]\]$/, "").replace(/^"|"$/g, "");
|
|
1895
|
+
}
|
|
1896
|
+
const KIND_FROM_FILENAME = /^(?:\d{4}-\d{2}-\d{2})-(task|bug|idea|note|observation)-.+\.md$/;
|
|
1897
|
+
const LOOP_CYCLE_PATTERN = /loop-cycle-/;
|
|
1888
1898
|
const transcripts = scan.data.raw.filter((p) => p.relPath.startsWith("raw/transcripts/") && p.relPath.endsWith(".md"));
|
|
1899
|
+
const claimedPaths = /* @__PURE__ */ new Set();
|
|
1900
|
+
const transcriptMeta = /* @__PURE__ */ new Map();
|
|
1901
|
+
for (const t of transcripts) {
|
|
1902
|
+
try {
|
|
1903
|
+
const content = await readFile10(join13(input.vault, t.relPath), "utf8");
|
|
1904
|
+
const fm = extractFrontmatter(content);
|
|
1905
|
+
let kind = fm.ok && typeof fm.data.kind === "string" ? fm.data.kind : "";
|
|
1906
|
+
let project = fm.ok && typeof fm.data.project === "string" ? fm.data.project : "";
|
|
1907
|
+
let inferred = false;
|
|
1908
|
+
if (input.forceScan && !kind) {
|
|
1909
|
+
const basename = t.relPath.split("/").pop();
|
|
1910
|
+
if (!LOOP_CYCLE_PATTERN.test(basename)) {
|
|
1911
|
+
const m = basename.match(KIND_FROM_FILENAME);
|
|
1912
|
+
if (m) {
|
|
1913
|
+
kind = m[1];
|
|
1914
|
+
inferred = true;
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
if (input.forceScan && !project && kind) {
|
|
1919
|
+
const bodyStart = content.indexOf("---", 4);
|
|
1920
|
+
if (bodyStart > 0) {
|
|
1921
|
+
const body = content.slice(bodyStart);
|
|
1922
|
+
const wikilink2 = body.match(/\[\[([a-z0-9-]+)\]\]/);
|
|
1923
|
+
if (wikilink2) {
|
|
1924
|
+
const candidate = wikilink2[1];
|
|
1925
|
+
if (workDirsBySlug.has(candidate)) {
|
|
1926
|
+
project = `[[${candidate}]]`;
|
|
1927
|
+
inferred = true;
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
transcriptMeta.set(t.relPath, { kind, project, slug: extractSlug(project), inferred });
|
|
1933
|
+
} catch {
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1889
1936
|
for (const t of transcripts) {
|
|
1890
1937
|
const datePrefix = t.relPath.split("/").pop().slice(0, 10);
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1938
|
+
const meta = transcriptMeta.get(t.relPath);
|
|
1939
|
+
const slug = meta?.slug || "";
|
|
1940
|
+
if (slug && workDirsBySlug.has(slug)) {
|
|
1941
|
+
const slugDirs = workDirsBySlug.get(slug);
|
|
1942
|
+
const tSlug = t.relPath.split("/").pop().replace(/^\d{4}-\d{2}-\d{2}-/, "").replace(/\.md$/, "").replace(/^(task|bug|idea|note|observation)-/, "");
|
|
1943
|
+
for (const [dirName, status] of slugDirs) {
|
|
1944
|
+
if (!dirName.startsWith(datePrefix)) continue;
|
|
1945
|
+
const wSlug = dirName.replace(/^\d{4}-\d{2}-\d{2}-/, "");
|
|
1946
|
+
const tWords = new Set(tSlug.split("-").filter((w) => w.length >= 3));
|
|
1947
|
+
const wWords = wSlug.split("-").filter((w) => w.length >= 3);
|
|
1948
|
+
const overlap = wWords.filter((w) => tWords.has(w)).length;
|
|
1949
|
+
if (dirName.includes(tSlug) || tSlug.includes(wSlug) || overlap >= 1) {
|
|
1950
|
+
claimedPaths.add(t.relPath);
|
|
1951
|
+
if (status === "done" || status === "invalid") {
|
|
1952
|
+
staleTranscripts.push({ path: t.relPath, reason: `work item projects/${slug}/work/${dirName} is ${status}` });
|
|
1953
|
+
}
|
|
1954
|
+
break;
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
} else if (!slug) {
|
|
1958
|
+
for (const [dir, status] of workDirs) {
|
|
1959
|
+
if (dir.split("/").pop().startsWith(datePrefix)) {
|
|
1960
|
+
claimedPaths.add(t.relPath);
|
|
1961
|
+
if (status === "done" || status === "invalid") {
|
|
1962
|
+
staleTranscripts.push({ path: t.relPath, reason: `work item ${dir} is ${status}` });
|
|
1963
|
+
}
|
|
1964
|
+
break;
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
for (const [relDir] of workDirs) {
|
|
1970
|
+
const specPath = join13(input.vault, relDir, "spec.md");
|
|
1971
|
+
try {
|
|
1972
|
+
const specContent = await readFile10(specPath, "utf8");
|
|
1973
|
+
const specFm = extractFrontmatter(specContent);
|
|
1974
|
+
if (specFm.ok && typeof specFm.data.source === "string") {
|
|
1975
|
+
const sourcePath = specFm.data.source;
|
|
1976
|
+
if (sourcePath.startsWith("raw/transcripts/")) claimedPaths.add(sourcePath);
|
|
1895
1977
|
}
|
|
1978
|
+
} catch {
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
const unclaimedTranscripts = [];
|
|
1982
|
+
const CLAIMABLE_KINDS = /* @__PURE__ */ new Set(["task", "bug"]);
|
|
1983
|
+
for (const t of transcripts) {
|
|
1984
|
+
if (claimedPaths.has(t.relPath)) continue;
|
|
1985
|
+
const meta = transcriptMeta.get(t.relPath);
|
|
1986
|
+
if (!meta) continue;
|
|
1987
|
+
if (CLAIMABLE_KINDS.has(meta.kind) && meta.project) {
|
|
1988
|
+
unclaimedTranscripts.push({ path: t.relPath, reason: `${meta.kind} for ${meta.project} \u2014 no work item` });
|
|
1896
1989
|
}
|
|
1897
1990
|
}
|
|
1898
1991
|
const doneWorkItems = [];
|
|
@@ -1987,17 +2080,19 @@ async function runStale(input) {
|
|
|
1987
2080
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1988
2081
|
});
|
|
1989
2082
|
}
|
|
1990
|
-
const total = stale.length + staleTranscripts.length + incompleteWorkItems.length + doneWorkItems.length;
|
|
2083
|
+
const total = stale.length + staleTranscripts.length + unclaimedTranscripts.length + incompleteWorkItems.length + doneWorkItems.length;
|
|
1991
2084
|
const hintLines = [];
|
|
1992
2085
|
if (stale.length > 0) hintLines.push(`stale_pages: ${stale.length}`, ...stale.map((p) => ` ${p.page}: ${p.reason}`));
|
|
1993
2086
|
if (staleTranscripts.length > 0) hintLines.push(`stale_transcripts: ${staleTranscripts.length}`, ...staleTranscripts.map((t) => ` ${t.path}: ${t.reason}`));
|
|
2087
|
+
if (unclaimedTranscripts.length > 0) hintLines.push(`unclaimed_transcripts: ${unclaimedTranscripts.length}`, ...unclaimedTranscripts.map((t) => ` ${t.path}: ${t.reason}`));
|
|
1994
2088
|
if (incompleteWorkItems.length > 0) hintLines.push(`incomplete_work_items: ${incompleteWorkItems.length}`, ...incompleteWorkItems.map((w) => ` ${w.path}: ${w.reason}`));
|
|
1995
2089
|
if (doneWorkItems.length > 0) hintLines.push(`done_work_items: ${doneWorkItems.length}`, ...doneWorkItems.map((w) => ` ${w.path}: ${w.reason}`));
|
|
1996
2090
|
if (archived.length > 0) hintLines.push(`archived: ${archived.length}`, ...archived.map((a) => ` ${a}`));
|
|
1997
2091
|
if (hintLines.length === 0) hintLines.push("no stale transcripts or incomplete work items");
|
|
1998
2092
|
return { exitCode: total > 0 ? ExitCode.STALE_PAGE : ExitCode.OK, result: ok({
|
|
1999
|
-
stale: [...stale, ...staleTranscripts.map((t) => ({ page: t.path, reason: t.reason })), ...incompleteWorkItems.map((w) => ({ page: w.path, reason: w.reason })), ...doneWorkItems.map((w) => ({ page: w.path, reason: w.reason }))],
|
|
2093
|
+
stale: [...stale, ...staleTranscripts.map((t) => ({ page: t.path, reason: t.reason })), ...unclaimedTranscripts.map((t) => ({ page: t.path, reason: t.reason })), ...incompleteWorkItems.map((w) => ({ page: w.path, reason: w.reason })), ...doneWorkItems.map((w) => ({ page: w.path, reason: w.reason }))],
|
|
2000
2094
|
stale_transcripts: staleTranscripts,
|
|
2095
|
+
unclaimed_transcripts: unclaimedTranscripts,
|
|
2001
2096
|
incomplete_work_items: incompleteWorkItems,
|
|
2002
2097
|
done_work_items: doneWorkItems,
|
|
2003
2098
|
archived,
|
|
@@ -2268,7 +2363,7 @@ async function runLint(input) {
|
|
|
2268
2363
|
const staleResult = await runStale({ vault: input.vault, days: input.days });
|
|
2269
2364
|
if (staleResult.result.ok) {
|
|
2270
2365
|
const st = staleResult.result.data;
|
|
2271
|
-
const staleList = [...st.stale_transcripts.map((t) => t.path), ...st.incomplete_work_items.map((w) => w.path), ...(st.done_work_items ?? []).map((w) => w.path)];
|
|
2366
|
+
const staleList = [...st.stale_transcripts.map((t) => t.path), ...(st.unclaimed_transcripts ?? []).map((t) => t.path), ...st.incomplete_work_items.map((w) => w.path), ...(st.done_work_items ?? []).map((w) => w.path)];
|
|
2272
2367
|
if (staleList.length > 0) buckets.stale_page = staleList;
|
|
2273
2368
|
}
|
|
2274
2369
|
const pagesize = await runPagesize({ vault: input.vault, lines: input.lines });
|
|
@@ -4392,7 +4487,7 @@ function slugify2(text) {
|
|
|
4392
4487
|
return words || "untitled";
|
|
4393
4488
|
}
|
|
4394
4489
|
async function runObserve(input) {
|
|
4395
|
-
const kind = input.kind || "
|
|
4490
|
+
const kind = input.kind || "task";
|
|
4396
4491
|
if (!ALLOWED_KINDS.has(kind)) {
|
|
4397
4492
|
return {
|
|
4398
4493
|
exitCode: ExitCode.SCHEME_REJECTED,
|
|
@@ -5944,10 +6039,10 @@ program.command("topic-map-check [vault]").description("check whether a topic ma
|
|
|
5944
6039
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
5945
6040
|
else emit(await runTopicMapCheck({ vault: v.vault, threshold: opts.threshold }), v.vault);
|
|
5946
6041
|
});
|
|
5947
|
-
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("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
6042
|
+
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) => {
|
|
5948
6043
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
5949
6044
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
5950
|
-
else emit(await runStale({ vault: v.vault, days: opts.days, archive: !!opts.archive }), v.vault);
|
|
6045
|
+
else emit(await runStale({ vault: v.vault, days: opts.days, archive: !!opts.archive, forceScan: !!opts.forceScan }), v.vault);
|
|
5951
6046
|
});
|
|
5952
6047
|
program.command("pagesize [vault]").description("report page sizes and flag oversized pages").option("--lines <n>", "max body lines", (s) => parseInt(s, 10), 200).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
5953
6048
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
@@ -6109,7 +6204,7 @@ program.command("seed [vault]").description("populate a vault with example conte
|
|
|
6109
6204
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
6110
6205
|
else emit(await runSeed({ vault: v.vault }), v.vault);
|
|
6111
6206
|
});
|
|
6112
|
-
program.command("observe [vault]").description("create a raw transcript observation entry").requiredOption("--text <text>", "observation text").option("--kind <kind>", "observation kind (note|bug|task|idea|session-log)", "
|
|
6207
|
+
program.command("observe [vault]").description("create a raw transcript observation entry").requiredOption("--text <text>", "observation text").option("--kind <kind>", "observation kind (note|bug|task|idea|session-log)", "task").option("--project <slug>", "associated project slug (required for task/bug claim detection)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
6113
6208
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
6114
6209
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
6115
6210
|
else emit(await runObserve({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skillwiki",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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
|
@@ -29,22 +29,23 @@ Run `skillwiki lang` at the start. Entry prose and `--human` summaries use the r
|
|
|
29
29
|
0. **Resolve vault and language.** Run `skillwiki path` (fail if NO_VAULT_CONFIGURED) and `skillwiki lang`.
|
|
30
30
|
1. **Parse arguments.** Extract from the user's message:
|
|
31
31
|
- `text` — the idea/bug/task/note content (required)
|
|
32
|
-
- `type` — one of: `idea`, `bug`, `task`, `note` (default: `
|
|
33
|
-
- `project` —
|
|
32
|
+
- `type` — one of: `idea`, `bug`, `task`, `note` (default: `task`)
|
|
33
|
+
- `project` — project slug for claim detection (e.g., `llm-wiki`). Required for `task` and `bug` types to appear in `skillwiki stale` unclaimed list.
|
|
34
34
|
2. **Build filename.** Derive a slug from the first ~6 words of the text (lowercased, hyphens for spaces, non-alphanumeric stripped). The capture file is `raw/transcripts/YYYY-MM-DD-{type}-{slug}.md`. Each capture gets its own file — never append to an existing file.
|
|
35
35
|
3. **Write frontmatter.** Create the file with ad-hoc capture frontmatter:
|
|
36
36
|
```yaml
|
|
37
37
|
---
|
|
38
38
|
source_url:
|
|
39
|
-
|
|
39
|
+
created: YYYY-MM-DD
|
|
40
|
+
ingested: # filled by ingest pipeline
|
|
40
41
|
kind: {type}
|
|
41
42
|
project: "[[{slug}]]"
|
|
42
43
|
---
|
|
43
44
|
```
|
|
44
45
|
- Set `kind` to the parsed type (`idea`, `bug`, `task`, `note`).
|
|
45
|
-
-
|
|
46
|
-
- If no project, omit the `project` field entirely.
|
|
46
|
+
- Set `project: "[[slug]]"` — **required for claim detection**. Without this field, the transcript is invisible to `skillwiki stale`. If no project context exists, ask the user which project this capture belongs to.
|
|
47
47
|
- `source_url` is null (these are locally originated captures).
|
|
48
|
+
- `created` — today's date. `ingested` is left empty for the ingest pipeline.
|
|
48
49
|
- No `sha256` — ad-hoc captures are mutable working notes, not immutable sources.
|
|
49
50
|
4. **Write body.** Below the frontmatter, write:
|
|
50
51
|
```markdown
|
|
@@ -72,7 +73,8 @@ Each capture is a standalone file with ad-hoc capture frontmatter:
|
|
|
72
73
|
```yaml
|
|
73
74
|
---
|
|
74
75
|
source_url:
|
|
75
|
-
|
|
76
|
+
created: 2026-05-08
|
|
77
|
+
ingested:
|
|
76
78
|
kind: idea
|
|
77
79
|
project: "[[llm-wiki]]"
|
|
78
80
|
---
|
|
@@ -84,7 +86,7 @@ Fix the template mismatch between wiki-add-task and the vault template.
|
|
|
84
86
|
|
|
85
87
|
The `kind` field uses the capture type and must be one of: `idea`, `bug`, `task`, `note` (plus the existing `postmortem`, `session-log`, `meeting-notes`, `other` for non-capture raw sources).
|
|
86
88
|
|
|
87
|
-
The `project`
|
|
89
|
+
The `project` field is **required for claim detection**. Without it, `skillwiki stale` cannot surface the transcript as unclaimed work. The `kind` and `project` fields can be set independently — they do not require `work_item`. The `work_item` field is only used when the raw source is directly tied to a project work item (set by `proj-work`).
|
|
88
90
|
|
|
89
91
|
Ad-hoc captures omit `sha256` — they are mutable working notes, not immutable sources. The `sha256` field is reserved for ingested raw sources that require integrity verification.
|
|
90
92
|
|
|
@@ -106,14 +108,25 @@ Ad-hoc captures omit `sha256` — they are mutable working notes, not immutable
|
|
|
106
108
|
When you're not in a Claude session, drop files directly into `raw/transcripts/`:
|
|
107
109
|
|
|
108
110
|
1. Create a `.md` file in `raw/transcripts/` — name it descriptively (e.g., `2026-05-08-idea-fix-template.md`)
|
|
109
|
-
2. Use ad-hoc capture frontmatter: `source_url:`, `ingested:`, `kind:`, and
|
|
111
|
+
2. Use ad-hoc capture frontmatter: `source_url:`, `ingested:`, `kind:`, and `project:` (required for `task`/`bug`)
|
|
110
112
|
3. Write your idea/bug/task/note below the frontmatter
|
|
111
113
|
|
|
112
|
-
|
|
114
|
+
**For claim detection**, `kind` must be `task` or `bug` AND `project: "[[slug]]"` must be set. Without both fields, the transcript won't appear in `skillwiki stale` unclaimed list.
|
|
115
|
+
|
|
116
|
+
No special format required — `skillwiki stale --days 0` will detect unclaimed task/bug transcripts on the next run. Mark the type with a heading like `## idea`, `## bug`, `## task`, or just write freeform.
|
|
113
117
|
|
|
114
118
|
## Dev-loop discovery
|
|
115
119
|
|
|
116
|
-
|
|
117
|
-
-
|
|
120
|
+
`skillwiki stale` detects unclaimed transcripts automatically. A transcript is **unclaimed** when:
|
|
121
|
+
- `kind` is `task` or `bug`
|
|
122
|
+
- `project` field is set (e.g., `project: "[[llm-wiki]]"`)
|
|
123
|
+
- No work item matches it (via date-prefix or `source:` frontmatter reference in spec.md)
|
|
124
|
+
|
|
125
|
+
Run `skillwiki stale --days 0 --human` to see all unclaimed transcripts. The output includes an `unclaimed_transcripts` section listing each capture that needs a work item.
|
|
126
|
+
|
|
127
|
+
The agent then decides whether to:
|
|
128
|
+
- Create a work item via `proj-work` (for tasks and bugs) — set `source:` in spec.md to the transcript path for cross-date linking
|
|
118
129
|
- Ingest as a knowledge page via `wiki-ingest` (for ideas with sources)
|
|
119
130
|
- Leave in place (for notes that don't need action yet)
|
|
131
|
+
|
|
132
|
+
Transcripts without `kind` or `project` fields (e.g., `loop-cycle-*` session logs) are excluded from claim detection.
|