kushi-agents 5.0.4 → 5.1.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/README.md CHANGED
@@ -7,6 +7,8 @@
7
7
  [![host: VS Code](https://img.shields.io/badge/host-VS%20Code-007acc)](https://gim-home.github.io/kushi/)
8
8
  [![spec: agentskills.io](https://img.shields.io/badge/spec-agentskills.io-22c55e)](https://agentskills.io/skill-creation/best-practices)
9
9
 
10
+ > **v5.1.0 — Living wiki.** Build-state is now incremental: human edits outside `<!-- kushi:auto -->` fences are preserved, contradictions are flagged with Obsidian-compatible callouts (`> [!warning]`), and a new `lint-state` skill monitors wiki health. State/ is a valid [Obsidian](https://obsidian.md) vault — callout syntax, Dataview-compatible frontmatter, and `[[wikilinks]]` all work natively.
11
+
10
12
  > **v5.0.1 spec-compliance pass.** Every `plugin/skills/<name>/SKILL.md` follows the [agentskills.io best practices](https://agentskills.io/skill-creation/best-practices) and [optimizing-descriptions](https://agentskills.io/skill-creation/optimizing-descriptions) guides: ≤ 500 lines & ≤ 5000 tokens, load-on-trigger references, top-5 gotchas, checklist orchestrators, validation loops, plan-validate-execute for graph + State writers, and "USE WHEN …" descriptions. Enforced by `self-check -Deep` D30.*.
11
13
 
12
14
  > **Project lineage:** for the *why* behind each release — what built on what, what trade-offs were accepted, what external work inspired which design — see [`docs/genealogy.md`](docs/genealogy.md). Every release MUST add an entry there before tagging (enforced by `self-check` D31.genealogy).
@@ -74,6 +76,21 @@ Apply [a]ll · [s]elect · [n]one?
74
76
 
75
77
  Try it: `npx kushi-agents --clawpilot --profile preview` · How-to: [Two-way ADO update](https://gim-home.github.io/kushi/how-to/two-way-ado-update/) · Roadmap: [`docs/concepts/roadmap.md`](docs/concepts/roadmap.md).
76
78
 
79
+ ## Obsidian compatible
80
+
81
+ `State/` is a valid [Obsidian](https://obsidian.md) vault out of the box:
82
+
83
+ - **Callout syntax** — contradictions use `> [!warning]` / `> [!info]` callouts (native Obsidian rendering).
84
+ - **Dataview compatible** — every State page has YAML frontmatter (`kushi_state_page: true`, `entity_ids`, `related`, timestamps) queryable by [Dataview](https://blacksmithgu.github.io/obsidian-dataview/).
85
+ - **Wikilinks** — cross-references use `[[category/slug]]` form, resolvable by Obsidian's link resolver.
86
+ - **No binary assets required** — all content is plain Markdown. No images, no custom plugins needed.
87
+
88
+ To use: point Obsidian at `<project>/State/` as a vault. The `index.md` serves as the home page, `log.md` is the activity feed, and category folders (`people/`, `decisions/`, etc.) are navigable in the sidebar.
89
+
90
+ > **Note:** Obsidian is optional. State/ works identically without it — the conventions are designed to be tool-agnostic while being Obsidian-first for teams that use it.
91
+
92
+ <!-- Screenshot placeholder: Obsidian rendering of State/ with contradiction callouts visible. No binary screenshots committed — use text descriptions or link to docs site. -->
93
+
77
94
  ---
78
95
 
79
96
  ## Three install profiles
package/bin/cli.mjs CHANGED
@@ -15,6 +15,17 @@ if (args.length > 0 && SKILL_VERBS.has(args[0])) {
15
15
  process.exit(0);
16
16
  }
17
17
 
18
+ // ── lint verb (v5.1.0+) ──────────────────────────────────────────────────────
19
+ if (args.length > 0 && args[0] === 'lint') {
20
+ const project = args[1] || '';
21
+ if (!project) {
22
+ console.error('\n Usage: kushi lint <project>\n');
23
+ process.exit(1);
24
+ }
25
+ await dispatchLint(project);
26
+ process.exit(0);
27
+ }
28
+
18
29
  if (args.includes('--help') || args.includes('-h')) {
19
30
  console.log(`
20
31
  Usage: npx kushi-agents [options]
@@ -61,13 +72,17 @@ if (args.includes('--help') || args.includes('-h')) {
61
72
  Rewrite a skill's description per the optimizer rules.
62
73
  review-evals <skill> Render an HTML side-by-side eval-review viewer.
63
74
 
75
+ Wiki maintenance (v5.1.0+):
76
+ lint <project> Run wiki-lint checks on State/ (contradictions, stale claims, orphans).
77
+
64
78
  After install, talk to Kushi:
65
79
  bootstrap <project> First-time setup
66
80
  refresh <project> Incremental refresh + rebuild State/
67
81
  state <project> Re-render State/ from existing Evidence
68
82
  consolidate <project> Merge per-user evidence
69
83
  status <project> Show run-log
70
- ask <project> <q> Cited Q&A over Evidence/ (auto-routes)
84
+ ask <project> <q> Cited Q&A over Evidence/ (auto-routes, --file-back to save)
85
+ lint <project> Run wiki-lint checks on State/
71
86
 
72
87
  In VS Code Chat the prefix is "@Kushi". In Clawpilot just say "kushi <verb>".
73
88
  `);
@@ -210,6 +225,53 @@ async function dispatchSkillVerb(verb, rest) {
210
225
  process.exit(result.status ?? 1);
211
226
  }
212
227
 
228
+ async function dispatchLint(project) {
229
+ const { spawn } = await import('node:child_process');
230
+ const { resolve } = await import('node:path');
231
+ const { fileURLToPath } = await import('node:url');
232
+ const __dirname = fileURLToPath(new URL('.', import.meta.url));
233
+ const scriptPath = resolve(__dirname, '..', 'plugin', 'skills', 'lint-state', 'lint.ps1');
234
+
235
+ const cwd = process.cwd();
236
+ const { readdirSync, existsSync } = await import('node:fs');
237
+ let stateDir = '';
238
+
239
+ const evidenceDir = resolve(cwd, project, 'Evidence');
240
+ if (existsSync(evidenceDir)) {
241
+ const aliases = readdirSync(evidenceDir, { withFileTypes: true })
242
+ .filter(d => d.isDirectory() && !d.name.startsWith('_'));
243
+ for (const alias of aliases) {
244
+ const candidate = resolve(evidenceDir, alias.name, 'State');
245
+ if (existsSync(candidate)) { stateDir = candidate; break; }
246
+ }
247
+ }
248
+
249
+ if (!stateDir) {
250
+ const direct = resolve(cwd, project, 'State');
251
+ if (existsSync(direct)) { stateDir = direct; }
252
+ }
253
+
254
+ if (!stateDir) {
255
+ console.error(`\n Could not find State/ directory for project '${project}'.`);
256
+ console.error(` Looked in: ${evidenceDir}/*/State/ and ${resolve(cwd, project, 'State')}/`);
257
+ console.error(` Run 'kushi state ${project}' first to build State/.\n`);
258
+ process.exit(1);
259
+ }
260
+
261
+ const child = spawn('pwsh', ['-NoProfile', '-File', scriptPath, '-StateDir', stateDir], {
262
+ stdio: 'inherit',
263
+ cwd: resolve(__dirname, '..'),
264
+ });
265
+
266
+ return new Promise((res, rej) => {
267
+ child.on('close', (code) => {
268
+ if (code !== 0) rej(new Error(`lint-state exited with code ${code}`));
269
+ else res();
270
+ });
271
+ child.on('error', rej);
272
+ });
273
+ }
274
+
213
275
  function pickFlag(args, flag) {
214
276
  const idx = args.indexOf(flag);
215
277
  if (idx !== -1 && idx + 1 < args.length) return args[idx + 1];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kushi-agents",
3
- "version": "5.0.4",
3
+ "version": "5.1.0",
4
4
  "description": "Install Kushi — multi-source project evidence agent with Comprehensive Structured Capture (CSC) into weekly-only files across Email, Teams, OneNote, Loop, SharePoint, Meetings, CRM, ADO. Meetings retain a sibling verbatim/ audit folder. WorkIQ-only for M365 sources (Graph / m365_* FORBIDDEN as fallbacks; user-paste is first-class). Host-agnostic.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,8 +17,8 @@ Kushi ships in three profiles. The installed profile is recorded in `kushi-insta
17
17
  | Profile | What's installed | Verbs available |
18
18
  |---|---|---|
19
19
  | `core` | Aggregator only: `setup`, `pull-*`, `consolidate-evidence`, `aggregate-project`, `ask-project`, `project-status`, `vertex-link`, `emit-vertex`, `self-check`, `eval`, `intro` | `setup`, `aggregate`, `consolidate`, `status`, `pull`, `ask`, `vertex-link`, `emit-vertex` |
20
- | `standard` *(default)* | core + `bootstrap-project`, `refresh-project`, `fde-intake`, `fde-report`, `fde-triage` + FDE reference pack | core + `bootstrap`, `refresh`, `fde-intake`, `fde-report`, `fde-triage` |
21
- | `full` | standard + `build-state` | standard + `state` |
20
+ | `standard` *(default)* | core + `bootstrap-project`, `refresh-project`, `lint-state`, `fde-intake`, `fde-report`, `fde-triage` + FDE reference pack | core + `bootstrap`, `refresh`, `lint`, `fde-intake`, `fde-report`, `fde-triage` |
21
+ | `full` | standard + `build-state`, `link-entities`, `dashboard`, `tour` | standard + `state`, `link-entities`, `dashboard`, `tour` |
22
22
  | **`preview`** *(opt-in)* | standard + `propose-ado-update`, `apply-ado-update` | standard + `propose-ado`, `apply-ado` |
23
23
 
24
24
  The Evidence/ folder produced by `aggregate` is the **public contract** between Kushi and any downstream consumer (external rollup repo, BI tooling). See `docs/reference/evidence-contract.md`.
@@ -39,6 +39,7 @@ The Evidence/ folder produced by `aggregate` is the **public contract** between
39
39
  | `@Kushi link-entities <project>` | standard+ | n/a (read-only) | v5.0.0 — `link-entities` writes `<project>/Evidence/_graph/project-graph.json` from per-source `_index/entities.yml` + weekly CSC bodies. Deterministic by default; opt-in LLM augment via `m365Mutable.graph.llm_infer`. |
40
40
  | `@Kushi dashboard <project>` | standard+ | n/a (read-only) | v5.0.0 — `dashboard` writes `<project>/dashboard.html` (single self-contained file). Cytoscape.js v3 + Clawpilot theme. |
41
41
  | `@Kushi tour <project> [--top N]` | standard+ | n/a (read-only) | v5.0.0 — `tour` writes `<project>/State/tour.md`, an auto-generated week-in-review walkthrough scored by `recency_weight × cross_ref_count` (14-day half-life). Default N=10. |
42
+ | `@Kushi lint <project>` | standard+ | n/a (read-only) | v5.1.0 — `lint-state` runs wiki-lint checks against `State/` (contradictions, stale claims, orphans, missing cross-refs, data gaps). Writes `State/reports/lint-YYYY-MM-DD.md`. |
42
43
  | `@Kushi consolidate <project> last N days` | core+ | N days | `consolidate-evidence` only |
43
44
  | `@Kushi status <project>` | core+ | n/a | `project-status` — show run-log |
44
45
  | `@Kushi ask <project> <question>` | core+ | n/a (read-only) | `ask-project` — cited Q&A over Evidence/ (+ State/ on full) |
@@ -0,0 +1,88 @@
1
+ ---
2
+ name: "living-wiki"
3
+ description: "v5.1.0 — Incremental-maintenance + contradiction-handling pattern for State/. Build-state preserves human edits outside <!-- kushi:auto --> fences; contradictions are flagged with callouts, never silently overwritten. Adapted from Karpathy's living-wiki gist. Authored to agentskills.io spec; deltas: adds incremental fencing convention + contradiction lifecycle + auto-resolve threshold not present in source."
4
+ applies_to: "build-state, lint-state, all writers touching State/"
5
+ since: "kushi v5.1.0"
6
+ ---
7
+
8
+ # living-wiki — doctrine
9
+
10
+ > **Authored to [agentskills.io](https://agentskills.io/skill-creation/best-practices) spec.**
11
+ > Deltas from source (Karpathy's living-wiki gist): adds `<!-- kushi:auto -->` fencing convention, contradiction lifecycle (current → contradicted → superseded), auto-resolve threshold with 3 conditions, `_review-queue.md` open-contradiction tracker.
12
+
13
+ Adapted from [Karpathy's living-wiki gist](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f). The core insight: a wiki maintained by both humans and machines must have clear ownership boundaries and explicit conflict resolution — silent overwrites destroy trust.
14
+
15
+ ## Rules (HARD)
16
+
17
+ ### 1. Incremental maintenance
18
+
19
+ Every writer skill that touches `State/` MUST:
20
+
21
+ - **Preserve human edits.** Content outside `<!-- kushi:auto:start -->` / `<!-- kushi:auto:end -->` fences is NEVER modified by automation. Writers only update derived sections between those fences.
22
+ - **Never wipe the page.** If a page exists, read it first. Regenerate only the fenced regions. If the page does not exist, create it with fences around any auto-derived content.
23
+ - **Fence hygiene.** Every auto-derived block MUST be wrapped:
24
+ ```markdown
25
+ <!-- kushi:auto:start section="<section-id>" -->
26
+ ...auto-derived content...
27
+ <!-- kushi:auto:end section="<section-id>" -->
28
+ ```
29
+ The `section` attribute identifies which derivation produced this block (e.g., `section="entity-summary"`, `section="related-entities"`).
30
+
31
+ ### 2. Contradiction handling
32
+
33
+ When a new fact contradicts an existing claim (different value for the same entity property):
34
+
35
+ - **Flag both claims** with Obsidian-compatible callouts:
36
+ ```markdown
37
+ > [!warning] Contradicted by build-state run 2026-05-28
38
+ > Previous value: "MACC $500K" [source: ushak/crm/weekly/2026-05-01_crm-csc.md#acme-opp · 2026-05-01]
39
+
40
+ > [!info] New value (2026-05-28)
41
+ > Current value: "MACC $750K" [source: ushak/crm/weekly/2026-05-22_crm-csc.md#acme-opp · 2026-05-22]
42
+ ```
43
+ - **Never silently overwrite.** The old value stays visible until explicitly resolved.
44
+ - **Add to `_review-queue.md`** with entity, property, old value, new value, both sources, and date flagged.
45
+
46
+ ### 3. Contradiction lifecycle
47
+
48
+ Each contradicted claim moves through states:
49
+
50
+ | State | Meaning |
51
+ |---|---|
52
+ | `current` | Active, accepted truth. |
53
+ | `contradicted` | A newer claim disagrees. Both are visible. |
54
+ | `superseded` | Resolved — old claim is archived. The newer (or human-chosen) value wins. |
55
+
56
+ ### 4. Auto-resolve threshold
57
+
58
+ A contradicted claim MAY be automatically marked `superseded` (and removed from `_review-queue.md`) when ALL of:
59
+
60
+ 1. The new claim has **≥ 3 corroborating sources from different surfaces** (e.g., meetings + CRM + email all agree on the new value).
61
+ 2. The contradicted claim is **≥ 30 days old** (based on its source citation date).
62
+ 3. The contradicted claim has **no human override marker** (`<!-- kushi:human-override -->` above the claim blocks auto-resolve).
63
+
64
+ When auto-resolved, replace the callout pair with:
65
+ ```markdown
66
+ > [!info] Auto-resolved 2026-06-28 — superseded by 3+ corroborating sources (meetings, crm, email)
67
+ > Previous value archived: "MACC $500K" [source: ...]
68
+ ```
69
+
70
+ ### 5. `_review-queue.md`
71
+
72
+ Located at `Evidence/<alias>/State/_review-queue.md`. Lists all open (unresolved) contradictions:
73
+
74
+ ```markdown
75
+ # Review Queue
76
+
77
+ | Entity | Property | Old value | New value | Flagged | Sources |
78
+ |---|---|---|---|---|---|
79
+ | Acme Opp | MACC | $500K | $750K | 2026-05-28 | crm, meetings |
80
+ ```
81
+
82
+ Updated by `build-state` on every run. Cleared when contradictions are resolved (auto or manual).
83
+
84
+ ## References
85
+
86
+ - [Karpathy's living-wiki gist](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f)
87
+ - `karpathy-state-layout.instructions.md` (v5.0.0 base layout)
88
+ - `wiki-lint.instructions.md` (contradiction-flagged finding class)
@@ -0,0 +1,78 @@
1
+ ---
2
+ name: "log-format"
3
+ description: "v5.1.0 — Canonical log format for State/log.md. Every writer appends reverse-chronological entries with grep-friendly headings. Adapted from axoviq-ai/synthadoc log-as-history pattern. Authored to agentskills.io spec; deltas: kushi-specific ops taxonomy, index.md 'Last touched' pointer, append-only rule."
4
+ applies_to: "every writer skill that touches State/ (bootstrap-project, refresh-project, build-state, lint-state, link-entities, dashboard, tour, ask-project --file-back)"
5
+ since: "kushi v5.1.0"
6
+ ---
7
+
8
+ # log-format — doctrine
9
+
10
+ > **Authored to [agentskills.io](https://agentskills.io/skill-creation/best-practices) spec.**
11
+ > Deltas from source (axoviq-ai/synthadoc): kushi-specific op taxonomy (`bootstrap`, `refresh`, `build-state`, `lint-state`, `ask-fileback`, `link-entities`, `dashboard`, `tour`), `index.md` "Last touched" pointer, reverse-chronological append-only rule.
12
+
13
+ Adapted from [axoviq-ai/synthadoc](https://github.com/axoviq-ai/synthadoc) — the log-as-grep-friendly-history pattern.
14
+
15
+ ## Rules (HARD)
16
+
17
+ ### 1. Canonical format
18
+
19
+ Every entry in `Evidence/<alias>/State/log.md` MUST use this heading format:
20
+
21
+ ```markdown
22
+ ## [YYYY-MM-DD HH:MM] <op> | <title>
23
+
24
+ <1–3 line summary>
25
+ Sources: <comma-separated source pointers>
26
+ ```
27
+
28
+ Example:
29
+ ```markdown
30
+ ## [2026-05-28 14:30] build-state | Incremental rebuild (3 entities updated)
31
+
32
+ Re-derived people/jane-doe.md, decisions/macc-increase.md, risks/timeline-slip.md.
33
+ Contradictions flagged: 1 (Acme Opp MACC). Auto-resolved: 0.
34
+ Sources: ushak/crm/weekly/2026-05-22_crm-csc.md, ushak/meetings/weekly/2026-05-21_meetings-csc.md
35
+ ```
36
+
37
+ ### 2. Grep-friendly
38
+
39
+ The heading format is designed for grep:
40
+ ```bash
41
+ grep '^## \[2026-05-' log.md # all May 2026 entries
42
+ grep '^## \[.*\] build-state' log.md # all build-state runs
43
+ grep '^## \[.*\] lint-state' log.md # all lint runs
44
+ ```
45
+
46
+ ### 3. Ops taxonomy (closed set)
47
+
48
+ | Op | Writer skill | Meaning |
49
+ |---|---|---|
50
+ | `bootstrap` | bootstrap-project | First-time project setup |
51
+ | `refresh` | refresh-project | Incremental evidence pull + state rebuild |
52
+ | `build-state` | build-state | Pure re-render of State/ from Evidence/ |
53
+ | `lint-state` | lint-state | Wiki lint pass over State/ |
54
+ | `ask-fileback` | ask-project --file-back | Q&A answer written back to State/answers/ |
55
+ | `link-entities` | link-entities | Cross-source graph rebuild |
56
+ | `dashboard` | dashboard | Dashboard HTML regeneration |
57
+ | `tour` | tour | Guided tour regeneration |
58
+
59
+ ### 4. Append-only, reverse-chronological
60
+
61
+ - New entries are **prepended** (newest at top, after the front-matter and file header).
62
+ - Entries are NEVER deleted or rewritten.
63
+ - Readers scan top-down for recency.
64
+
65
+ ### 5. Index.md "Last touched" pointer
66
+
67
+ After appending to `log.md`, the writer MUST also update the top of `State/index.md`:
68
+
69
+ ```markdown
70
+ > Last touched: YYYY-MM-DD HH:MM by <op> ([log](log.md))
71
+ ```
72
+
73
+ This single-line pointer lives immediately after the front-matter block of `index.md`.
74
+
75
+ ## References
76
+
77
+ - [axoviq-ai/synthadoc](https://github.com/axoviq-ai/synthadoc)
78
+ - `karpathy-state-layout.instructions.md` (log.md base contract)
@@ -0,0 +1,110 @@
1
+ ---
2
+ name: "wiki-lint"
3
+ description: "v5.1.0 — Wiki lint finding classes for State/. Detects contradictions, stale claims, orphan pages, missing cross-refs, and data gaps. Each finding includes a ready-to-paste fix snippet. Adapted from sametbrr/llm-wiki-manager lint patterns. Authored to agentskills.io spec; deltas: kushi-specific finding classes (contradiction-flagged, stale-claim, orphan-page, missing-cross-ref, data-gap), fix-snippet convention."
4
+ applies_to: "lint-state skill"
5
+ since: "kushi v5.1.0"
6
+ ---
7
+
8
+ # wiki-lint — doctrine
9
+
10
+ > **Authored to [agentskills.io](https://agentskills.io/skill-creation/best-practices) spec.**
11
+ > Deltas from source (sametbrr/llm-wiki-manager): kushi-specific finding classes mapped to State/ layout, Obsidian-callout aware, fix-snippet per finding (not just a diagnostic), integration with `_review-queue.md`.
12
+
13
+ Adapted from [sametbrr/llm-wiki-manager](https://github.com/sametbrr/llm-wiki-manager) — wiki lint patterns.
14
+
15
+ ## Finding classes
16
+
17
+ ### 1. `contradiction-flagged`
18
+
19
+ **What:** Count unresolved `> [!warning] Contradicted` callouts in State/ pages.
20
+
21
+ **Detection:** Regex `^> \[!warning\] Contradicted` in any `State/**/*.md`.
22
+
23
+ **Severity:** warning (informational — contradictions are expected during active projects).
24
+
25
+ **Fix snippet:**
26
+ ```markdown
27
+ <!-- To resolve: review both values, pick the correct one, then either:
28
+ (a) Delete the callout pair and keep the winning value, OR
29
+ (b) Add <!-- kushi:human-override --> above the old value to prevent auto-resolve. -->
30
+ ```
31
+
32
+ ### 2. `stale-claim`
33
+
34
+ **What:** A claim in a State page is ≥ 60 days old (based on its `[source: ... · YYYY-MM-DD]` citation date) with no corroborating refresh since.
35
+
36
+ **Detection:** Parse inline citations `[source: ... · YYYY-MM-DD]`; flag if the newest citation for an entity property is > 60 days old AND no `build-state` or `refresh` log entry touched that entity since.
37
+
38
+ **Severity:** warning.
39
+
40
+ **Fix snippet:**
41
+ ```markdown
42
+ <!-- Stale claim (>60 days). Run '@Kushi refresh <project>' to pull fresh evidence,
43
+ then '@Kushi state <project>' to rebuild. If the claim is still valid, add a
44
+ corroborating citation from a recent source. -->
45
+ ```
46
+
47
+ ### 3. `orphan-page`
48
+
49
+ **What:** A page in `State/` (with `kushi_state_page: true`) that is NOT linked from `index.md` or any other State page.
50
+
51
+ **Detection:** Enumerate all `State/**/*.md` with `kushi_state_page: true`. For each, check if its path or `[[wikilink]]` slug appears in `index.md` or any sibling page body.
52
+
53
+ **Severity:** warning.
54
+
55
+ **Fix snippet:**
56
+ ```markdown
57
+ <!-- Orphan page: not linked from index.md or any other State page.
58
+ Fix: add a [[category/slug]] link to index.md under the correct category heading,
59
+ OR delete this page if it was generated in error. -->
60
+ ```
61
+
62
+ ### 4. `missing-cross-ref`
63
+
64
+ **What:** An entity mentioned in a page body (matching a known entity name from `Evidence/_graph/project-graph.json` nodes) but NOT listed in the page's `entity_ids` or `related` frontmatter arrays.
65
+
66
+ **Detection:** Load entity names from graph nodes. For each State page, scan body for entity-name matches not in frontmatter.
67
+
68
+ **Severity:** info.
69
+
70
+ **Fix snippet:**
71
+ ```markdown
72
+ <!-- Missing cross-ref: entity "<name>" mentioned in body but not in frontmatter.
73
+ Fix: add to `related:` array in front-matter:
74
+ related:
75
+ - category/entity-slug -->
76
+ ```
77
+
78
+ ### 5. `data-gap`
79
+
80
+ **What:** A required section header is present in a State page but its body (content between that header and the next header or EOF) is empty or contains only whitespace/placeholder text.
81
+
82
+ **Detection:** For each State page, parse `###` and `##` sections. Flag sections whose body is empty, contains only `<!-- TODO -->`, or is fewer than 2 non-blank lines.
83
+
84
+ **Severity:** info.
85
+
86
+ **Fix snippet:**
87
+ ```markdown
88
+ <!-- Data gap: section "<heading>" has no content.
89
+ Fix: run '@Kushi refresh <project>' to pull evidence that populates this section,
90
+ or remove the section header if it's not applicable to this entity. -->
91
+ ```
92
+
93
+ ## Output format
94
+
95
+ Each finding in the lint report:
96
+
97
+ ```markdown
98
+ ### <class>: <entity/page> — <short description>
99
+
100
+ **File:** `State/<path>`
101
+ **Line:** <N>
102
+ **Fix:**
103
+ <fix snippet>
104
+ ```
105
+
106
+ ## References
107
+
108
+ - [sametbrr/llm-wiki-manager](https://github.com/sametbrr/llm-wiki-manager)
109
+ - `living-wiki.instructions.md` (contradiction lifecycle)
110
+ - `karpathy-state-layout.instructions.md` (page convention)
@@ -0,0 +1,73 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Appends a canonical entry to State/log.md per log-format.instructions.md.
4
+
5
+ .DESCRIPTION
6
+ Idempotent helper called by every writer skill after completing a State/ write.
7
+ Prepends (reverse-chronological) a new entry after the file header.
8
+
9
+ .PARAMETER StateDir
10
+ Path to the State/ directory (e.g., Evidence/<alias>/State/).
11
+
12
+ .PARAMETER Op
13
+ Operation name from the closed taxonomy: bootstrap, refresh, build-state, lint-state,
14
+ ask-fileback, link-entities, dashboard, tour.
15
+
16
+ .PARAMETER Title
17
+ Short one-line title for the log entry.
18
+
19
+ .PARAMETER Summary
20
+ 1–3 line summary body.
21
+
22
+ .PARAMETER Sources
23
+ Comma-separated source pointers.
24
+ #>
25
+ [CmdletBinding()]
26
+ param(
27
+ [Parameter(Mandatory)][string]$StateDir,
28
+ [Parameter(Mandatory)][ValidateSet('bootstrap','refresh','build-state','lint-state','ask-fileback','link-entities','dashboard','tour')][string]$Op,
29
+ [Parameter(Mandatory)][string]$Title,
30
+ [string]$Summary = '',
31
+ [string]$Sources = ''
32
+ )
33
+
34
+ $ErrorActionPreference = 'Stop'
35
+
36
+ $logFile = Join-Path $StateDir 'log.md'
37
+ $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm'
38
+ $entry = "## [$timestamp] $Op | $Title`n"
39
+ if ($Summary) { $entry += "`n$Summary" }
40
+ if ($Sources) { $entry += "`nSources: $Sources" }
41
+ $entry += "`n"
42
+
43
+ if (-not (Test-Path $logFile)) {
44
+ # Create with header
45
+ $header = @"
46
+ ---
47
+ kushi_state_log: true
48
+ ---
49
+
50
+ # State Log
51
+
52
+ $entry
53
+ "@
54
+ New-Item -Path $logFile -ItemType File -Force | Out-Null
55
+ Set-Content -Path $logFile -Value $header -Encoding utf8NoBOM
56
+ } else {
57
+ $content = Get-Content -Raw $logFile
58
+ # Insert after the header block (after "# State Log" line or after front-matter)
59
+ if ($content -match '(?ms)(^---\r?\n.*?\r?\n---\r?\n\r?\n# [^\r\n]+\r?\n)(.*)$') {
60
+ $headerPart = $Matches[1]
61
+ $bodyPart = $Matches[2]
62
+ $newContent = $headerPart + "`n" + $entry + "`n" + $bodyPart
63
+ } elseif ($content -match '(?ms)(^# [^\r\n]+\r?\n)(.*)$') {
64
+ $headerPart = $Matches[1]
65
+ $bodyPart = $Matches[2]
66
+ $newContent = $headerPart + "`n" + $entry + "`n" + $bodyPart
67
+ } else {
68
+ $newContent = $entry + "`n" + $content
69
+ }
70
+ Set-Content -Path $logFile -Value $newContent -Encoding utf8NoBOM
71
+ }
72
+
73
+ Write-Verbose "Appended log entry: [$timestamp] $Op | $Title"
@@ -0,0 +1,47 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Updates the "Last touched" pointer at the top of State/index.md per log-format.instructions.md.
4
+
5
+ .DESCRIPTION
6
+ Idempotent helper. Updates or inserts the "Last touched" line immediately after
7
+ the front-matter block in index.md.
8
+
9
+ .PARAMETER StateDir
10
+ Path to the State/ directory.
11
+
12
+ .PARAMETER Op
13
+ Operation name (same taxonomy as Append-StateLog).
14
+ #>
15
+ [CmdletBinding()]
16
+ param(
17
+ [Parameter(Mandatory)][string]$StateDir,
18
+ [Parameter(Mandatory)][string]$Op
19
+ )
20
+
21
+ $ErrorActionPreference = 'Stop'
22
+
23
+ $indexFile = Join-Path $StateDir 'index.md'
24
+ $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm'
25
+ $pointer = "> Last touched: $timestamp by $Op ([log](log.md))"
26
+
27
+ if (-not (Test-Path $indexFile)) {
28
+ Write-Warning "index.md not found at $indexFile — skipping Update-StateIndex."
29
+ return
30
+ }
31
+
32
+ $content = Get-Content -Raw $indexFile
33
+ $pointerPattern = '(?m)^> Last touched:.*$'
34
+
35
+ if ($content -match $pointerPattern) {
36
+ $content = $content -replace $pointerPattern, $pointer
37
+ } else {
38
+ # Insert after front-matter closing ---
39
+ if ($content -match '(?ms)(^---\r?\n.*?\r?\n---\r?\n)(.*)$') {
40
+ $content = $Matches[1] + "`n" + $pointer + "`n" + $Matches[2]
41
+ } else {
42
+ $content = $pointer + "`n`n" + $content
43
+ }
44
+ }
45
+
46
+ Set-Content -Path $indexFile -Value $content -Encoding utf8NoBOM
47
+ Write-Verbose "Updated index.md: Last touched $timestamp by $Op"
@@ -42,6 +42,36 @@ The user does NOT need to type `/ask-project` or `@Kushi ask`. If the message:
42
42
  - **Freshness gate, not freshness auto-fix.** If the freshest source relevant to the question is older than `chat.freshness_warn_days` (default 14), warn the user and offer `@Kushi refresh <project>` — but NEVER auto-refresh.
43
43
  - **Reference packs may be consulted for domain doctrine.** When the question maps to a known reference-pack domain (currently only `fde/` — FDE stages, fitness, CRM status meanings, intake gates, risk categories, "MACC", "is this FDE-fit"), ALSO load the matching reference pack as additional grounding using the 3-layer override order (project → user → packaged). Cite reference-pack assertions with `[source: reference-packs/<pack>/<file>.md · <layer>]` where layer is `packaged` / `user-override` / `project-override`. Reference-pack content NEVER overrides project Evidence/ for facts about *this* project; it only provides definitions, gates, and rubrics.
44
44
 
45
+ ## --file-back option (v5.1.0+)
46
+
47
+ When the user passes `--file-back` (or says "file this answer back", "save this answer"):
48
+
49
+ 1. After answering normally, write the Q+A to `Evidence/<alias>/State/answers/YYYY-MM-DD_<slug>.md`:
50
+ ```markdown
51
+ ---
52
+ question: "<the user's original question>"
53
+ asked_at: <ISO-8601 timestamp>
54
+ sources: [<list of cited source paths>]
55
+ ---
56
+
57
+ ## Answer
58
+
59
+ <the answer text, with citations preserved>
60
+
61
+ ## Sources
62
+
63
+ - <source 1>
64
+ - <source 2>
65
+ ```
66
+ 2. The `<slug>` is derived from the question: lowercase, alphanumeric + hyphens, ≤ 60 chars.
67
+ 3. Append `ask-fileback` entry to `State/log.md` via `Append-StateLog.ps1`:
68
+ - Title: `Filed answer: <slug>`
69
+ - Summary: one-line description of the question.
70
+ 4. Update `State/index.md` via `Update-StateIndex.ps1 -Op ask-fileback`.
71
+ 5. If `State/answers/` does not exist, create it.
72
+
73
+ The `--file-back` flag is OPTIONAL. Without it, ask-project behaves exactly as before (read-only, no writes).
74
+
45
75
  ## Inputs
46
76
 
47
77
  - `<project>` — fuzzy-matched project name. If multiple plausible matches, ask the user.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: "build-state"
3
- version: "4.0.0"
3
+ version: "5.0.0"
4
4
  description: "USE WHEN the user says \"regenerate State for <X>\", \"rebuild State\", or \"@Kushi state <X>\" AND the project already has Evidence/ populated. DO NOT USE to pull new evidence (use refresh-project or pull-*). Capability: pure re-render — reads Evidence/_index/entities.yml + weekly CSC + legacy fallback, writes <project>/State/ in BOTH legacy 00–09 synthesis (full profile) AND Karpathy layout (index.md + log.md + per-entity pages + CLAUDE.md/AGENTS.md). Plan-validate-execute writer."
5
5
  ---
6
6
 
@@ -56,7 +56,23 @@ runs — transient working artifact, not authoritative truth.
56
56
 
57
57
  ## Steps
58
58
 
59
-
59
+
60
+ ## v5.1.0 — Incremental mode (HARD RULE; per `living-wiki.instructions.md`)
61
+
62
+ Build-state is now incremental by default:
63
+
64
+ 1. **Read existing State/ pages.** Before regenerating, load every existing `State/**/*.md`.
65
+ 2. **Preserve human edits.** Content OUTSIDE `<!-- kushi:auto:start -->` / `<!-- kushi:auto:end -->` fences is NEVER modified.
66
+ 3. **Re-derive fenced regions only.** For each `<!-- kushi:auto:start section="<id>" -->` block, re-derive content from current evidence and replace the block contents.
67
+ 4. **Contradiction detection.** When a re-derived fact contradicts an existing fact (different value for same entity property):
68
+ - Wrap both in `> [!warning] Contradicted` / `> [!info] New value` callouts per `living-wiki.instructions.md`.
69
+ - Add entity to `_review-queue.md`.
70
+ 5. **Auto-resolve.** Apply the auto-resolve threshold from `living-wiki.instructions.md` (≥3 sources + ≥30 days old + no human override).
71
+ 6. **Log + index.** Append `build-state` entry to `State/log.md` via `Append-StateLog.ps1`. Update `State/index.md` via `Update-StateIndex.ps1`.
72
+
73
+ When creating NEW pages (entity not previously in State/), wrap all auto-derived content in `<!-- kushi:auto -->` fences.
74
+
75
+
60
76
  ## Step checklist
61
77
 
62
78
  Progress-trackable view of the steps below. Each `### Step` block expands the corresponding checkbox.
@@ -0,0 +1,98 @@
1
+ ---
2
+ name: "lint-state"
3
+ version: "1.0.0"
4
+ description: "USE WHEN the user says 'lint state', 'check state health', 'wiki lint', 'kushi lint <project>', or 'find stale/orphan/contradictions in State'. DO NOT USE for evidence-level validation (use self-check) or for rebuilding State (use build-state). Capability: runs wiki-lint finding classes against Evidence/<alias>/State/, writes dated lint report, updates log.md + index.md."
5
+ ---
6
+
7
+ # Skill: lint-state
8
+
9
+ Run wiki-lint checks against a project's `State/` directory per `wiki-lint.instructions.md`. Reports contradictions, stale claims, orphan pages, missing cross-refs, and data gaps — each with a ready-to-paste fix snippet.
10
+
11
+ ## Triggers
12
+
13
+ - "lint state for `<project>`"
14
+ - "check state health"
15
+ - "wiki lint `<project>`"
16
+ - `kushi lint <project>` (CLI verb)
17
+ - "find contradictions in State"
18
+ - "any stale claims in `<project>`?"
19
+
20
+ ## Inputs
21
+
22
+ - `<project>` — engagement name (fuzzy-matched per `engagement-root-resolution.instructions.md`).
23
+
24
+ ## Step checklist
25
+
26
+ - [ ] Step 1 — Resolve project root + State/ path
27
+ - [ ] Step 2 — Run finding-class checks
28
+ - [ ] Step 3 — Generate lint report
29
+ - [ ] Step 4 — Update log.md + index.md
30
+ - [ ] Step 5 — Print summary
31
+
32
+ ### Step 1 — Resolve project root
33
+
34
+ Resolve `<engagement-root>/<project>/Evidence/<alias>/State/` using standard engagement-root resolution. Verify `State/` exists; if not, advise running `build-state` first.
35
+
36
+ ### Step 2 — Run finding-class checks
37
+
38
+ Per `wiki-lint.instructions.md`, execute each finding class:
39
+
40
+ 1. **contradiction-flagged**: scan `State/**/*.md` for `> [!warning] Contradicted` callouts.
41
+ 2. **stale-claim**: parse citations, flag entities with newest citation > 60 days old.
42
+ 3. **orphan-page**: find pages with `kushi_state_page: true` not linked from index.md or siblings.
43
+ 4. **missing-cross-ref**: cross-reference entity mentions against frontmatter `entity_ids`/`related`.
44
+ 5. **data-gap**: find section headers with empty/placeholder bodies.
45
+
46
+ ### Step 3 — Generate lint report
47
+
48
+ Write `Evidence/<alias>/State/reports/lint-YYYY-MM-DD.md`:
49
+
50
+ ```markdown
51
+ ---
52
+ generated_at: "YYYY-MM-DDTHH:MM:SSZ"
53
+ generated_by: "lint-state v1.0.0"
54
+ findings_count: N
55
+ ---
56
+
57
+ # Lint Report — YYYY-MM-DD
58
+
59
+ ## Summary
60
+
61
+ | Class | Count | Severity |
62
+ |---|---|---|
63
+ | contradiction-flagged | N | warning |
64
+ | stale-claim | N | warning |
65
+ | orphan-page | N | warning |
66
+ | missing-cross-ref | N | info |
67
+ | data-gap | N | info |
68
+
69
+ **Total findings: N**
70
+
71
+ ## Findings
72
+
73
+ ### contradiction-flagged: <entity> — <description>
74
+ ...per wiki-lint.instructions.md format...
75
+ ```
76
+
77
+ ### Step 4 — Update log.md + index.md
78
+
79
+ Call shared helpers:
80
+ - `Append-StateLog.ps1 -Op lint-state -Title "Lint pass (<N> findings)"`
81
+ - `Update-StateIndex.ps1 -Op lint-state`
82
+
83
+ ### Step 5 — Print summary
84
+
85
+ One-line console output: `lint-state: <N> findings (<breakdown by class>). Report: State/reports/lint-YYYY-MM-DD.md`
86
+
87
+ ## Validation loop
88
+
89
+ After writing outputs:
90
+ 1. Verify `State/reports/lint-YYYY-MM-DD.md` exists and has valid frontmatter.
91
+ 2. Verify `State/log.md` has the new entry at top.
92
+ 3. Verify `State/index.md` "Last touched" pointer is updated.
93
+
94
+ ## References
95
+
96
+ - `../../instructions/wiki-lint.instructions.md`
97
+ - `../../instructions/log-format.instructions.md`
98
+ - `../../instructions/living-wiki.instructions.md`
@@ -0,0 +1,34 @@
1
+ {
2
+ "skill": "lint-state",
3
+ "version": "1.0.0",
4
+ "description": "Wiki-lint checks against State/ — contradictions, stale claims, orphans, cross-refs, data gaps.",
5
+ "cases": [
6
+ {
7
+ "id": "lint-state-clean",
8
+ "name": "Lint a clean State/ with no findings",
9
+ "input": "Run lint against a well-maintained State/ directory with no contradictions, stale claims, or orphans.",
10
+ "expected_assertions": [
11
+ { "type": "regex-match", "target": "stdout", "pattern": "lint-state: 0 findings" }
12
+ ],
13
+ "grader_type": "script"
14
+ },
15
+ {
16
+ "id": "lint-state-contradiction",
17
+ "name": "Lint detects contradiction callouts",
18
+ "input": "Run lint against a State/ directory with one unresolved > [!warning] Contradicted callout.",
19
+ "expected_assertions": [
20
+ { "type": "regex-match", "target": "stdout", "pattern": "contradiction-flagged=1" }
21
+ ],
22
+ "grader_type": "script"
23
+ },
24
+ {
25
+ "id": "lint-state-orphan",
26
+ "name": "Lint detects orphan pages",
27
+ "input": "Run lint against a State/ directory with a page that has kushi_state_page: true but is not linked from index.md.",
28
+ "expected_assertions": [
29
+ { "type": "regex-match", "target": "stdout", "pattern": "orphan-page=1" }
30
+ ],
31
+ "grader_type": "script"
32
+ }
33
+ ]
34
+ }
@@ -0,0 +1,218 @@
1
+ <#
2
+ .SYNOPSIS
3
+ lint-state — runs wiki-lint checks against a project's State/ directory.
4
+
5
+ .DESCRIPTION
6
+ Implements the finding classes defined in wiki-lint.instructions.md:
7
+ - contradiction-flagged
8
+ - stale-claim
9
+ - orphan-page
10
+ - missing-cross-ref
11
+ - data-gap
12
+
13
+ Writes a dated lint report to State/reports/, appends to State/log.md, updates
14
+ State/index.md "Last touched" pointer.
15
+
16
+ .PARAMETER StateDir
17
+ Path to the State/ directory to lint.
18
+
19
+ .PARAMETER GraphFile
20
+ Optional path to project-graph.json for missing-cross-ref checks.
21
+
22
+ .PARAMETER Quiet
23
+ Suppress console output (for programmatic use).
24
+ #>
25
+ [CmdletBinding()]
26
+ param(
27
+ [Parameter(Mandatory)][string]$StateDir,
28
+ [string]$GraphFile,
29
+ [switch]$Quiet
30
+ )
31
+
32
+ $ErrorActionPreference = 'Stop'
33
+ $findings = @()
34
+
35
+ # --- Helpers ---
36
+ function Add-LintFinding {
37
+ param([string]$Class, [string]$Severity, [string]$Entity, [string]$Description, [string]$File, [int]$Line, [string]$Fix)
38
+ $script:findings += [PSCustomObject]@{
39
+ class = $Class
40
+ severity = $Severity
41
+ entity = $Entity
42
+ description = $Description
43
+ file = $File
44
+ line = $Line
45
+ fix = $Fix
46
+ }
47
+ }
48
+
49
+ # --- Check 1: contradiction-flagged ---
50
+ $stateFiles = Get-ChildItem -Path $StateDir -Recurse -Filter '*.md' -ErrorAction SilentlyContinue
51
+ foreach ($f in $stateFiles) {
52
+ $lines = Get-Content -Path $f.FullName -ErrorAction SilentlyContinue
53
+ for ($i = 0; $i -lt $lines.Count; $i++) {
54
+ if ($lines[$i] -match '^\s*>\s*\[!warning\]\s*Contradicted') {
55
+ $entity = $f.BaseName -replace '-', ' '
56
+ Add-LintFinding 'contradiction-flagged' 'warning' $entity "Unresolved contradiction callout" $f.FullName ($i + 1) "Review both values, pick the correct one. Delete callout pair and keep winner, or add <!-- kushi:human-override --> to prevent auto-resolve."
57
+ }
58
+ }
59
+ }
60
+
61
+ # --- Check 2: stale-claim ---
62
+ $staleDays = 60
63
+ $now = Get-Date
64
+ foreach ($f in $stateFiles) {
65
+ $content = Get-Content -Raw $f.FullName -ErrorAction SilentlyContinue
66
+ if (-not $content) { continue }
67
+ $citations = [regex]::Matches($content, '\[source:[^\]]*\xB7\s*(\d{4}-\d{2}-\d{2})\]')
68
+ if ($citations.Count -eq 0) { continue }
69
+ $newestDate = $null
70
+ foreach ($m in $citations) {
71
+ try {
72
+ $d = [datetime]::ParseExact($m.Groups[1].Value, 'yyyy-MM-dd', $null)
73
+ if (-not $newestDate -or $d -gt $newestDate) { $newestDate = $d }
74
+ } catch {}
75
+ }
76
+ if ($newestDate -and ($now - $newestDate).TotalDays -gt $staleDays) {
77
+ $entity = $f.BaseName -replace '-', ' '
78
+ Add-LintFinding 'stale-claim' 'warning' $entity "Newest citation is $([math]::Round(($now - $newestDate).TotalDays)) days old (threshold: $staleDays)" $f.FullName 0 "Run '@Kushi refresh <project>' then '@Kushi state <project>' to pull fresh evidence. If claim is still valid, add a corroborating citation from a recent source."
79
+ }
80
+ }
81
+
82
+ # --- Check 3: orphan-page ---
83
+ $indexFile = Join-Path $StateDir 'index.md'
84
+ $indexContent = ''
85
+ if (Test-Path $indexFile) { $indexContent = Get-Content -Raw $indexFile }
86
+ $allPageContent = ''
87
+ foreach ($f in $stateFiles) {
88
+ if ($f.FullName -ne (Resolve-Path $indexFile -ErrorAction SilentlyContinue)) {
89
+ $allPageContent += (Get-Content -Raw $f.FullName -ErrorAction SilentlyContinue) + "`n"
90
+ }
91
+ }
92
+ foreach ($f in $stateFiles) {
93
+ $content = Get-Content -Raw $f.FullName -ErrorAction SilentlyContinue
94
+ if ($content -notmatch 'kushi_state_page:\s*true') { continue }
95
+ $slug = $f.BaseName
96
+ $relPath = $f.FullName.Replace($StateDir, '').TrimStart('\', '/') -replace '\\', '/'
97
+ if ($indexContent -notmatch [regex]::Escape($slug) -and $allPageContent -notmatch [regex]::Escape($slug)) {
98
+ Add-LintFinding 'orphan-page' 'warning' $slug "Page not linked from index.md or any sibling" $f.FullName 0 "Add [[$(Split-Path $relPath -Parent)/$slug]] to index.md under the correct category heading, or delete this page if generated in error."
99
+ }
100
+ }
101
+
102
+ # --- Check 4: missing-cross-ref ---
103
+ if ($GraphFile -and (Test-Path $GraphFile)) {
104
+ try {
105
+ $graph = Get-Content -Raw $GraphFile | ConvertFrom-Json
106
+ $entityNames = @()
107
+ if ($graph.nodes) { $entityNames = $graph.nodes | ForEach-Object { $_.name } | Where-Object { $_ } }
108
+ foreach ($f in $stateFiles) {
109
+ $content = Get-Content -Raw $f.FullName -ErrorAction SilentlyContinue
110
+ if ($content -notmatch 'kushi_state_page:\s*true') { continue }
111
+ foreach ($name in $entityNames) {
112
+ if ($name.Length -lt 3) { continue }
113
+ if ($content -match [regex]::Escape($name) -and $content -notmatch "entity_ids:.*$([regex]::Escape($name))" -and $content -notmatch "related:.*$([regex]::Escape($name))") {
114
+ # Check frontmatter more carefully
115
+ if ($content -match '(?ms)^---\r?\n(.*?)\r?\n---') {
116
+ $fm = $Matches[1]
117
+ if ($fm -notmatch [regex]::Escape($name)) {
118
+ Add-LintFinding 'missing-cross-ref' 'info' ($f.BaseName -replace '-',' ') "Entity '$name' mentioned in body but not in frontmatter" $f.FullName 0 "Add to related: array in front-matter."
119
+ }
120
+ }
121
+ }
122
+ }
123
+ }
124
+ } catch {
125
+ Write-Warning "Could not parse graph file: $_"
126
+ }
127
+ }
128
+
129
+ # --- Check 5: data-gap ---
130
+ foreach ($f in $stateFiles) {
131
+ $content = Get-Content -Raw $f.FullName -ErrorAction SilentlyContinue
132
+ if ($content -notmatch 'kushi_state_page:\s*true') { continue }
133
+ $lines = Get-Content -Path $f.FullName
134
+ for ($i = 0; $i -lt $lines.Count; $i++) {
135
+ if ($lines[$i] -match '^#{2,3}\s+(.+)$') {
136
+ $heading = $Matches[1]
137
+ # Look at content until next heading or EOF
138
+ $bodyLines = @()
139
+ for ($j = $i + 1; $j -lt $lines.Count; $j++) {
140
+ if ($lines[$j] -match '^#{2,3}\s+') { break }
141
+ if ($lines[$j].Trim() -and $lines[$j].Trim() -ne '<!-- TODO -->' -and $lines[$j].Trim() -ne '<!-- TODO(retrofit) -->') {
142
+ $bodyLines += $lines[$j]
143
+ }
144
+ }
145
+ if ($bodyLines.Count -lt 2 -and $heading -notmatch '^(State Log|State Index|Review Queue)') {
146
+ Add-LintFinding 'data-gap' 'info' ($f.BaseName -replace '-',' ') "Section '$heading' has no meaningful content" $f.FullName ($i + 1) "Run '@Kushi refresh <project>' to pull evidence for this section, or remove the header if not applicable."
147
+ }
148
+ }
149
+ }
150
+ }
151
+
152
+ # --- Generate report ---
153
+ $reportsDir = Join-Path $StateDir 'reports'
154
+ if (-not (Test-Path $reportsDir)) { New-Item -Path $reportsDir -ItemType Directory -Force | Out-Null }
155
+
156
+ $dateStr = Get-Date -Format 'yyyy-MM-dd'
157
+ $reportFile = Join-Path $reportsDir "lint-$dateStr.md"
158
+ $timestamp = Get-Date -Format 'yyyy-MM-ddTHH:mm:ssZ'
159
+
160
+ $classCounts = @{}
161
+ foreach ($f2 in $findings) {
162
+ if (-not $classCounts.ContainsKey($f2.class)) { $classCounts[$f2.class] = 0 }
163
+ $classCounts[$f2.class]++
164
+ }
165
+
166
+ $report = @"
167
+ ---
168
+ generated_at: "$timestamp"
169
+ generated_by: "lint-state v1.0.0"
170
+ findings_count: $($findings.Count)
171
+ ---
172
+
173
+ # Lint Report — $dateStr
174
+
175
+ ## Summary
176
+
177
+ | Class | Count | Severity |
178
+ |---|---|---|
179
+ "@
180
+
181
+ $classOrder = @('contradiction-flagged','stale-claim','orphan-page','missing-cross-ref','data-gap')
182
+ foreach ($cls in $classOrder) {
183
+ $count = if ($classCounts.ContainsKey($cls)) { $classCounts[$cls] } else { 0 }
184
+ $sev = if ($cls -match 'contradiction|stale|orphan') { 'warning' } else { 'info' }
185
+ $report += "| $cls | $count | $sev |`n"
186
+ }
187
+
188
+ $report += "`n**Total findings: $($findings.Count)**`n`n## Findings`n"
189
+
190
+ foreach ($f2 in $findings) {
191
+ $report += "`n### $($f2.class): $($f2.entity) — $($f2.description)`n`n"
192
+ $report += "**File:** ``$($f2.file)```n"
193
+ if ($f2.line -gt 0) { $report += "**Line:** $($f2.line)`n" }
194
+ $report += "**Fix:** $($f2.fix)`n"
195
+ }
196
+
197
+ Set-Content -Path $reportFile -Value $report -Encoding utf8NoBOM
198
+
199
+ # --- Update log + index ---
200
+ $sharedDir = Join-Path $PSScriptRoot '..\_shared'
201
+ $appendLog = Join-Path $sharedDir 'Append-StateLog.ps1'
202
+ $updateIndex = Join-Path $sharedDir 'Update-StateIndex.ps1'
203
+
204
+ if (Test-Path $appendLog) {
205
+ & $appendLog -StateDir $StateDir -Op 'lint-state' -Title "Lint pass ($($findings.Count) findings)" -Summary "Classes: $(($classCounts.Keys | Sort-Object | ForEach-Object { "$_=$($classCounts[$_])" }) -join ', ')" -Sources "State/**/*.md"
206
+ }
207
+ if (Test-Path $updateIndex) {
208
+ & $updateIndex -StateDir $StateDir -Op 'lint-state'
209
+ }
210
+
211
+ # --- Console output ---
212
+ if (-not $Quiet) {
213
+ $breakdown = ($classCounts.Keys | Sort-Object | ForEach-Object { "$_=$($classCounts[$_])" }) -join ', '
214
+ Write-Host "lint-state: $($findings.Count) findings ($breakdown). Report: $reportFile"
215
+ }
216
+
217
+ # Return findings as output for programmatic use
218
+ $findings
@@ -71,6 +71,8 @@ Checks split into **core** (always run) and **deep** (opt-in).
71
71
  | D32.multi-host | Multi-host install integrity | validates `src/multi-host.mjs` exports + `bin/cli.mjs` flag handling, then performs a temp-dir dry-run install for BOTH supported hosts (Clawpilot + VS Code Chat) under a fake `$HOME` in `$env:TEMP`. Asserts SKILL.md + agent file + skills/ + prompts/ + skills-metadata.json with a kushi entry are present, then asserts a clean uninstall. NEVER touches the real `~/.copilot/` or `~/.vscode/`. See `multi-host-install.instructions.md`. |
72
72
  | D33.evals | Skill evals framework integrity | every `plugin/skills/<name>/` (except `eval`) ships `evals/evals.json` with ≥ 2 cases and ≥ 1 assertion per case; the runner (`plugin/skills/eval/run-evals.ps1`) and schema (`plugin/skills/eval/evals.schema.json`) are present; `evals/baseline.json` exists (warn-only). Six sub-checks: `D33.evals-exist`, `D33.evals-schema`, `D33.evals-min-cases`, `D33.evals-have-assertions`, `D33.eval-runner-exists`, `D33.baseline-exists`. See `skill-evals.instructions.md`. |
73
73
  | D34.creator-conformance | skill-creator + skill-checker harness integrity (v5.0.4+) | validates `scaffold.ps1` + `check-skill.ps1` ship and are parseable; every skill carrying the `.created-by-skill-creator` marker passes `check-skill --lint` clean; `check-skill --all --retrofit --dry-run` shows no non-additive gaps; the dogfood report at `docs/audits/v5.0.4-skill-creator-dogfood.md` is fresh (≤14 days). Five sub-checks: `D34.skill-creator-exists`, `D34.skill-checker-exists`, `D34.creator-output-conforms`, `D34.retrofit-clean`, `D34.dogfood-report-fresh`. See `skill-authoring.instructions.md`. |
74
+ | D35.log | State log (v5.1.0+) | `State/log.md` exists, `## [` headings match canonical format, timestamps reverse-chronological. Sub-checks: `D35.log-exists`, `D35.log-format`, `D35.log-monotonic`. |
75
+ | D36.contradictions | Contradictions (v5.1.0+) | `> [!warning] Contradicted` callouts well-formed, `_review-queue.md` fresh, `<!-- kushi:auto -->` fences balanced. Sub-checks: `D36.callout-syntax`, `D36.review-queue-fresh`, `D36.no-silent-overwrite`. |
74
76
  | **CSC weekly-layout checks (kushi v4.9.0)** | | gated on `Resolve-EngagementRoots` — no-ops on the kushi repo itself. |
75
77
  | D11.csc | CSC entity coverage + depth | every `Evidence/<alias>/<source>/weekly/*-csc.md` has ≥ 1 entity heading; per-source minimum bullet count + populated-section count (meetings 25/6, email 8/4, teams 6/3, onenote 10/4, sharepoint 8/3, crm 12/5, ado 8/4). Coverage-Notes-only blocks (low-signal escape) are exempt. |
76
78
  | D12.csc | CSC section order | every entity block's `###` section headings appear in the canonical order: Participants → Topics → Q&A → Who Said What → Decisions → Dates & Numbers → Action Items → Next Steps → Open Questions → Risks → Customer Asks → Artifacts → Coverage Notes. |
@@ -83,7 +85,7 @@ Checks split into **core** (always run) and **deep** (opt-in).
83
85
  | D19.csc | CSC legacy write guard | WARN if `snapshot/` or `stream/` contains a file modified after the v4.9.0 release date (2026-05-26). Reads of legacy layouts remain supported. |
84
86
  | **v5.0.0 cross-source / state / dashboard / tour checks** | | gated on `Resolve-EngagementRoots` — no-ops on the kushi repo itself. |
85
87
  | D20.graph | Entity-graph well-formed | `Evidence/_graph/project-graph.json`: valid JSON, has `schema/project/generated_at/nodes/edges`, every edge `kind` is in the closed taxonomy (`references`/`decides`/`action-item-tracks`/`discusses`/`produced-by`/`follow-up-of`/`same-thread`/`participant-of`), every edge endpoint resolves to a node. See `entity-graph.instructions.md`. |
86
- | D21.state | Karpathy State layout | when any `State/**/*.md` has front-matter `kushi_state_page: true`, the sibling `index.md`, `log.md`, `CLAUDE.md`, `AGENTS.md` MUST exist, `CLAUDE.md` and `AGENTS.md` MUST be byte-identical, and every v5 page MUST live under one of the closed-set category folders (`people/`, `opportunities/`, `adoworkitems/`, `decisions/`, `risks/`, `customerasks/`, `meetings/`, `artifacts/`). See `karpathy-state-layout.instructions.md`. |
88
+ | D21.state | Karpathy State layout | `kushi_state_page: true` pages require sibling `index.md`, `log.md`, `CLAUDE.md`, `AGENTS.md`; pages live under closed-set category folders. See `karpathy-state-layout.instructions.md`. |
87
89
  | D22.dashboard | Dashboard produced + substituted | when `project-graph.json` exists, `<project>/dashboard.html` MUST exist, MUST NOT still contain the literal `__GRAPH_JSON__` placeholder (would indicate embedder failure), and MUST reference Cytoscape. See `dashboard-artifact.instructions.md`. |
88
90
  | D23.tour | Tour citations resolve | every `- **Cite:** [`<path>`]` row in `<project>/State/tour.md` MUST resolve to a real file under `<project>/Evidence/`. See `guided-tour.instructions.md`. |
89
91
 
@@ -120,7 +120,7 @@ if (-not (Test-Path $pluginDir)) {
120
120
  exit 2
121
121
  }
122
122
 
123
- $skillDirs = Get-ChildItem $skillsDir -Directory | Sort-Object Name
123
+ $skillDirs = Get-ChildItem $skillsDir -Directory | Where-Object { $_.Name -notmatch '^\.' -and $_.Name -notmatch '^_' } | Sort-Object Name
124
124
  $skillNames = $skillDirs.Name
125
125
  $instructionFiles = Get-ChildItem $instructionsDir -Filter '*.instructions.md' -EA SilentlyContinue
126
126
  $promptFiles = Get-ChildItem $promptsDir -Filter '*.prompt.md' -EA SilentlyContinue
@@ -264,7 +264,7 @@ if (Test-Path $pluginJsonFile) {
264
264
  $chain += $name
265
265
  return $chain
266
266
  }
267
- $skillNames = (Get-ChildItem $skillsDir -Directory).Name
267
+ $skillNames = (Get-ChildItem $skillsDir -Directory | Where-Object { $_.Name -notmatch '^\.' -and $_.Name -notmatch '^_' }).Name
268
268
  $promptStems = (Get-ChildItem (Join-Path $pluginDir 'prompts') -File -Filter '*.prompt.md' -ErrorAction SilentlyContinue) `
269
269
  | ForEach-Object { $_.Name -replace '\.prompt\.md$','' }
270
270
  $instructionStems = (Get-ChildItem (Join-Path $pluginDir 'instructions') -File -Filter '*.instructions.md' -ErrorAction SilentlyContinue) `
@@ -1663,7 +1663,7 @@ process.stdout.write(JSON.stringify(out));
1663
1663
  }
1664
1664
 
1665
1665
  $skillsRoot = Join-Path $Root 'plugin/skills'
1666
- $skillDirs = Get-ChildItem -Path $skillsRoot -Directory | Where-Object { $_.Name -notin @('eval', 'self-check') }
1666
+ $skillDirs = Get-ChildItem -Path $skillsRoot -Directory | Where-Object { $_.Name -notin @('eval', 'self-check') -and $_.Name -notmatch '^_' }
1667
1667
  foreach ($sd in $skillDirs) {
1668
1668
  $evalsFile = Join-Path $sd.FullName 'evals/evals.json'
1669
1669
  if (-not (Test-Path $evalsFile)) {
@@ -1778,6 +1778,136 @@ process.stdout.write(JSON.stringify(out));
1778
1778
  }
1779
1779
  }
1780
1780
 
1781
+ # === D35.log — State/log.md format and presence (v5.1.0+) ===
1782
+ # Validates that writer-touched State dirs have log.md and that entries match canonical format.
1783
+ # Operates on .testtmp/ fixtures or real engagement roots.
1784
+ $logFixtureDir = Join-Path $Root '.testtmp'
1785
+ $logTestDirs = @()
1786
+ if (Test-Path $logFixtureDir) {
1787
+ $logTestDirs += Get-ChildItem -Path $logFixtureDir -Recurse -Directory -ErrorAction SilentlyContinue |
1788
+ Where-Object { $_.Name -eq 'State' -and (Test-Path (Join-Path $_.FullName 'log.md')) }
1789
+ }
1790
+ # Also check real engagement roots if available
1791
+ foreach ($engRoot in (Resolve-EngagementRoots -RepoRoot $Root)) {
1792
+ Get-ChildItem -Path $engRoot -Directory -ErrorAction SilentlyContinue | ForEach-Object {
1793
+ $stateDir = Join-Path $_.FullName 'State'
1794
+ if (Test-Path $stateDir) { $logTestDirs += Get-Item $stateDir }
1795
+ }
1796
+ }
1797
+
1798
+ foreach ($sd in $logTestDirs) {
1799
+ $logMd = Join-Path $sd.FullName 'log.md'
1800
+
1801
+ # D35.log-exists
1802
+ if (-not (Test-Path $logMd)) {
1803
+ Add-Finding 'D35.log-exists' 'State log' 'warning' "State directory '$($sd.FullName)' has no log.md" "Add log.md per log-format.instructions.md. Run build-state to auto-create it." $sd.FullName 0
1804
+ continue
1805
+ }
1806
+
1807
+ # D35.log-format — every ## [ heading matches canonical format
1808
+ $logLines = Get-Content -Path $logMd -ErrorAction SilentlyContinue
1809
+ $prevTs = $null
1810
+ $formatOk = $true
1811
+ $monotonicOk = $true
1812
+ foreach ($line in $logLines) {
1813
+ if ($line -match '^\#\#\s+\[') {
1814
+ if ($line -notmatch '^\#\#\s+\[\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}\]\s+\S+\s+\|\s+.+$') {
1815
+ Add-Finding 'D35.log-format' 'State log' 'warning' "log.md heading does not match canonical format: $line" "Heading must be: ## [YYYY-MM-DD HH:MM] <op> | <title>. See log-format.instructions.md." $logMd 0
1816
+ $formatOk = $false
1817
+ break
1818
+ }
1819
+ # D35.log-monotonic — timestamps non-decreasing (newest first = decreasing)
1820
+ if ($line -match '^\#\#\s+\[(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2})\]') {
1821
+ try {
1822
+ $ts = [datetime]::ParseExact($Matches[1], 'yyyy-MM-dd HH:mm', $null)
1823
+ if ($prevTs -and $ts -gt $prevTs) {
1824
+ Add-Finding 'D35.log-monotonic' 'State log' 'warning' "log.md timestamps not reverse-chronological: $($Matches[1]) appears after an earlier timestamp" "Entries should be prepended (newest first). Re-order or re-run build-state." $logMd 0
1825
+ $monotonicOk = $false
1826
+ break
1827
+ }
1828
+ $prevTs = $ts
1829
+ } catch {}
1830
+ }
1831
+ }
1832
+ }
1833
+ }
1834
+
1835
+ # === D36.contradictions — contradiction handling integrity (v5.1.0+) ===
1836
+ # Validates callout syntax, review-queue freshness, and build-state non-fenced preservation.
1837
+ $contradictionDirs = @()
1838
+ if (Test-Path $logFixtureDir) {
1839
+ $contradictionDirs += Get-ChildItem -Path $logFixtureDir -Recurse -Directory -ErrorAction SilentlyContinue |
1840
+ Where-Object { $_.Name -eq 'State' }
1841
+ }
1842
+ foreach ($engRoot in (Resolve-EngagementRoots -RepoRoot $Root)) {
1843
+ Get-ChildItem -Path $engRoot -Directory -ErrorAction SilentlyContinue | ForEach-Object {
1844
+ $stateDir = Join-Path $_.FullName 'State'
1845
+ if (Test-Path $stateDir) { $contradictionDirs += Get-Item $stateDir }
1846
+ }
1847
+ }
1848
+
1849
+ foreach ($sd in $contradictionDirs) {
1850
+ $mdFiles = Get-ChildItem -Path $sd.FullName -Recurse -Filter '*.md' -ErrorAction SilentlyContinue
1851
+
1852
+ # D36.callout-syntax — every > [!warning] Contradicted callout has required structure
1853
+ foreach ($mf in $mdFiles) {
1854
+ $content = Get-Content -Raw $mf.FullName -ErrorAction SilentlyContinue
1855
+ if (-not $content) { continue }
1856
+ if ($content -match '>\s*\[!warning\]\s*Contradicted') {
1857
+ # Must have 'by' clause and a following > [!info] block
1858
+ if ($content -notmatch '>\s*\[!warning\]\s*Contradicted\s+by\s+') {
1859
+ Add-Finding 'D36.callout-syntax' 'Contradictions' 'warning' "Callout in $($mf.Name) missing 'by <source>' clause" "Format: > [!warning] Contradicted by <skill> run <date>. See living-wiki.instructions.md." $mf.FullName 0
1860
+ }
1861
+ }
1862
+ }
1863
+
1864
+ # D36.review-queue-fresh — if _review-queue.md has open items, most recent log entry is within 30 days
1865
+ $reviewQueue = Join-Path $sd.FullName '_review-queue.md'
1866
+ if (Test-Path $reviewQueue) {
1867
+ $rqContent = Get-Content -Raw $reviewQueue -ErrorAction SilentlyContinue
1868
+ # Check if review-queue has data rows (not just header + separator)
1869
+ $rqLines = ($rqContent -split '\r?\n') | Where-Object { $_ -match '^\|' -and $_ -notmatch '^\|\s*-' -and $_ -notmatch '^\|\s*Entity' }
1870
+ if ($rqLines.Count -gt 0) {
1871
+ # Has table rows (open items)
1872
+ $logMd2 = Join-Path $sd.FullName 'log.md'
1873
+ if (Test-Path $logMd2) {
1874
+ $logContent2 = Get-Content -Raw $logMd2
1875
+ $recentEntry = $false
1876
+ if ($logContent2 -match '(?m)^\#\#\s+\[(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2})\]') {
1877
+ try {
1878
+ $lastTs = [datetime]::ParseExact($Matches[1], 'yyyy-MM-dd HH:mm', $null)
1879
+ if (((Get-Date) - $lastTs).TotalDays -le 30) { $recentEntry = $true }
1880
+ } catch {}
1881
+ }
1882
+ if (-not $recentEntry) {
1883
+ Add-Finding 'D36.review-queue-fresh' 'Contradictions' 'warning' "_review-queue.md has open items but most recent log entry is >30 days old" "Run 'lint-state' or 'build-state' to refresh. Open contradictions need periodic review." $reviewQueue 0
1884
+ }
1885
+ }
1886
+ }
1887
+ }
1888
+
1889
+ # D36.no-silent-overwrite — build-state preserves non-fenced regions
1890
+ # This check validates the invariant by looking for the fencing pattern.
1891
+ # If State pages exist with kushi:auto fences AND content outside fences,
1892
+ # verify the file has not been fully regenerated (check mtime consistency).
1893
+ # For the fixture-based test: run build-state twice, assert non-fenced bytes unchanged.
1894
+ # In self-check, we do a structural check: if a page has content outside fences,
1895
+ # verify it also has content INSIDE fences (indicating incremental mode is active).
1896
+ foreach ($mf in $mdFiles) {
1897
+ $content = Get-Content -Raw $mf.FullName -ErrorAction SilentlyContinue
1898
+ if (-not $content) { continue }
1899
+ if ($content -notmatch 'kushi_state_page:\s*true') { continue }
1900
+ if ($content -match '<!-- kushi:auto:start') {
1901
+ # Good — page uses fencing. Verify fence pairs are balanced.
1902
+ $starts = ([regex]::Matches($content, '<!-- kushi:auto:start')).Count
1903
+ $ends = ([regex]::Matches($content, '<!-- kushi:auto:end')).Count
1904
+ if ($starts -ne $ends) {
1905
+ Add-Finding 'D36.no-silent-overwrite' 'Contradictions' 'warning' "$($mf.Name) has mismatched kushi:auto fence pairs ($starts starts, $ends ends)" "Every <!-- kushi:auto:start --> must have a matching <!-- kushi:auto:end -->. Fix the fencing." $mf.FullName 0
1906
+ }
1907
+ }
1908
+ }
1909
+ }
1910
+
1781
1911
  # === Output ===
1782
1912
  if ($Targeted) {
1783
1913
  # Filter findings to those whose code, surface, file path, or message contain the substring.
@@ -88,7 +88,7 @@ function Get-TargetSkills {
88
88
  return @(Get-Item -LiteralPath $d)
89
89
  }
90
90
  if ($All) {
91
- return Get-ChildItem -Path $skillsRoot -Directory | Where-Object { $excludeFromAll -notcontains $_.Name }
91
+ return Get-ChildItem -Path $skillsRoot -Directory | Where-Object { $excludeFromAll -notcontains $_.Name -and $_.Name -notmatch '^_' }
92
92
  }
93
93
  throw "Specify -Skill <name> or -All."
94
94
  }
@@ -0,0 +1,7 @@
1
+ # State/answers/
2
+
3
+ This folder contains filed-back Q&A answers from `ask-project --file-back`.
4
+
5
+ Each file is named `YYYY-MM-DD_<slug>.md` and contains the question, answer, and source citations.
6
+
7
+ These are durable knowledge artifacts — they persist across refreshes and serve as a queryable FAQ for the project.
@@ -0,0 +1,12 @@
1
+ ---
2
+ kushi_state_hot: true
3
+ generated_at: "{{generated_at}}"
4
+ ---
5
+
6
+ # Hot — {{project}}
7
+
8
+ > _Auto-generated. Entities touched in the last 7 days, ranked by recency. Content between fences is regenerated on every build-state run._
9
+
10
+ <!-- kushi:auto:start section="hot-entities" -->
11
+ {{hot_entities}}
12
+ <!-- kushi:auto:end section="hot-entities" -->
@@ -0,0 +1,10 @@
1
+ ---
2
+ kushi_state_review_queue: true
3
+ ---
4
+
5
+ # Review Queue
6
+
7
+ > _Open contradictions requiring human review. Updated by build-state when contradictions are detected. Cleared when resolved (auto or manual)._
8
+
9
+ | Entity | Property | Old value | New value | Flagged | Sources |
10
+ |---|---|---|---|---|---|
@@ -35,7 +35,7 @@ test('eval-runner: evals.schema.json validates structurally', () => {
35
35
  test('eval-runner: every plugin/skills/<name>/ (except eval/) has evals/evals.json that parses', () => {
36
36
  const skillsDir = path.join(repoRoot, 'plugin/skills');
37
37
  const skills = fs.readdirSync(skillsDir, { withFileTypes: true })
38
- .filter((d) => d.isDirectory() && d.name !== 'eval')
38
+ .filter((d) => d.isDirectory() && d.name !== 'eval' && !d.name.startsWith('_'))
39
39
  .map((d) => d.name);
40
40
  const missing = [];
41
41
  for (const skill of skills) {