@codyswann/lisa 2.24.0 → 2.25.1

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.
Files changed (154) hide show
  1. package/.claude-plugin/marketplace.json +12 -0
  2. package/package.json +1 -1
  3. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  4. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  5. package/plugins/lisa/rules/config-resolution.md +32 -1
  6. package/plugins/lisa/skills/atlassian-access/SKILL.md +32 -1
  7. package/plugins/lisa/skills/notion-access/SKILL.md +32 -1
  8. package/plugins/lisa/skills/setup-atlassian/SKILL.md +32 -1
  9. package/plugins/lisa/skills/setup-linear/SKILL.md +32 -1
  10. package/plugins/lisa/skills/setup-notion/SKILL.md +32 -1
  11. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  12. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  13. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  14. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  15. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  16. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  17. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  18. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  19. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  20. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  21. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  23. package/plugins/lisa-wiki/.claude-plugin/plugin.json +8 -0
  24. package/plugins/lisa-wiki/.codex-plugin/plugin.json +32 -0
  25. package/plugins/lisa-wiki/ci/lisa-wiki-validate.yml +32 -0
  26. package/plugins/lisa-wiki/commands/add-ingest.md +6 -0
  27. package/plugins/lisa-wiki/commands/add-role.md +6 -0
  28. package/plugins/lisa-wiki/commands/doctor.md +6 -0
  29. package/plugins/lisa-wiki/commands/ingest.md +6 -0
  30. package/plugins/lisa-wiki/commands/lint.md +6 -0
  31. package/plugins/lisa-wiki/commands/migrate.md +6 -0
  32. package/plugins/lisa-wiki/commands/onboard-me.md +6 -0
  33. package/plugins/lisa-wiki/commands/query.md +6 -0
  34. package/plugins/lisa-wiki/commands/setup.md +6 -0
  35. package/plugins/lisa-wiki/schema/lisa-wiki-config.schema.json +118 -0
  36. package/plugins/lisa-wiki/schema/wiki-structure.schema.json +51 -0
  37. package/plugins/lisa-wiki/scripts/_wiki-lib.mjs +185 -0
  38. package/plugins/lisa-wiki/scripts/diff-guard.mjs +116 -0
  39. package/plugins/lisa-wiki/scripts/ingest-git.mjs +189 -0
  40. package/plugins/lisa-wiki/scripts/ingest-memory.mjs +130 -0
  41. package/plugins/lisa-wiki/scripts/ingest-roles.mjs +85 -0
  42. package/plugins/lisa-wiki/scripts/ingest_slack_channel.py +329 -0
  43. package/plugins/lisa-wiki/scripts/lint-wiki.mjs +320 -0
  44. package/plugins/lisa-wiki/scripts/mcp-doctor.mjs +72 -0
  45. package/plugins/lisa-wiki/scripts/render-contract.mjs +107 -0
  46. package/plugins/lisa-wiki/scripts/rewrite-refs.mjs +144 -0
  47. package/plugins/lisa-wiki/scripts/slack_oauth_user.py +179 -0
  48. package/plugins/lisa-wiki/scripts/validate-config.mjs +232 -0
  49. package/plugins/lisa-wiki/scripts/verify-migration.mjs +199 -0
  50. package/plugins/lisa-wiki/skills/lisa-wiki-add-ingest/SKILL.md +34 -0
  51. package/plugins/lisa-wiki/skills/lisa-wiki-add-role/SKILL.md +30 -0
  52. package/plugins/lisa-wiki/skills/lisa-wiki-connector-confluence/SKILL.md +25 -0
  53. package/plugins/lisa-wiki/skills/lisa-wiki-connector-docs/SKILL.md +30 -0
  54. package/plugins/lisa-wiki/skills/lisa-wiki-connector-git/SKILL.md +25 -0
  55. package/plugins/lisa-wiki/skills/lisa-wiki-connector-jira/SKILL.md +28 -0
  56. package/plugins/lisa-wiki/skills/lisa-wiki-connector-memory/SKILL.md +28 -0
  57. package/plugins/lisa-wiki/skills/lisa-wiki-connector-notion/SKILL.md +25 -0
  58. package/plugins/lisa-wiki/skills/lisa-wiki-connector-roles/SKILL.md +22 -0
  59. package/plugins/lisa-wiki/skills/lisa-wiki-connector-slack/SKILL.md +30 -0
  60. package/plugins/lisa-wiki/skills/lisa-wiki-connector-web/SKILL.md +23 -0
  61. package/plugins/lisa-wiki/skills/lisa-wiki-doctor/SKILL.md +47 -0
  62. package/plugins/lisa-wiki/skills/lisa-wiki-ingest/SKILL.md +43 -0
  63. package/plugins/lisa-wiki/skills/lisa-wiki-lint/SKILL.md +32 -0
  64. package/plugins/lisa-wiki/skills/lisa-wiki-migrate/SKILL.md +43 -0
  65. package/plugins/lisa-wiki/skills/lisa-wiki-onboard-me/SKILL.md +33 -0
  66. package/plugins/lisa-wiki/skills/lisa-wiki-query/SKILL.md +30 -0
  67. package/plugins/lisa-wiki/skills/lisa-wiki-setup/SKILL.md +45 -0
  68. package/plugins/lisa-wiki/skills/lisa-wiki-usage/SKILL.md +50 -0
  69. package/plugins/lisa-wiki/templates/agents/role-agent.claude.md +16 -0
  70. package/plugins/lisa-wiki/templates/agents/role-agent.codex.toml +15 -0
  71. package/plugins/lisa-wiki/templates/index.md +17 -0
  72. package/plugins/lisa-wiki/templates/llm-wiki-contract.md +60 -0
  73. package/plugins/lisa-wiki/templates/log.md +8 -0
  74. package/plugins/lisa-wiki/templates/page-types/architecture.md +18 -0
  75. package/plugins/lisa-wiki/templates/page-types/concept.md +18 -0
  76. package/plugins/lisa-wiki/templates/page-types/decision.md +18 -0
  77. package/plugins/lisa-wiki/templates/page-types/entity.md +19 -0
  78. package/plugins/lisa-wiki/templates/page-types/open-question.md +18 -0
  79. package/plugins/lisa-wiki/templates/page-types/playbook.md +18 -0
  80. package/plugins/lisa-wiki/templates/page-types/project.md +19 -0
  81. package/plugins/lisa-wiki/templates/page-types/requirement.md +19 -0
  82. package/plugins/lisa-wiki/templates/page-types/staff.md +26 -0
  83. package/plugins/lisa-wiki/templates/start-here.md +24 -0
  84. package/plugins/lisa-wiki/templates/state-readme.md +20 -0
  85. package/plugins/src/base/rules/config-resolution.md +32 -1
  86. package/plugins/src/base/skills/atlassian-access/SKILL.md +32 -1
  87. package/plugins/src/base/skills/notion-access/SKILL.md +32 -1
  88. package/plugins/src/base/skills/setup-atlassian/SKILL.md +32 -1
  89. package/plugins/src/base/skills/setup-linear/SKILL.md +32 -1
  90. package/plugins/src/base/skills/setup-notion/SKILL.md +32 -1
  91. package/plugins/src/wiki/.claude-plugin/plugin.json +6 -0
  92. package/plugins/src/wiki/ci/lisa-wiki-validate.yml +32 -0
  93. package/plugins/src/wiki/commands/add-ingest.md +6 -0
  94. package/plugins/src/wiki/commands/add-role.md +6 -0
  95. package/plugins/src/wiki/commands/doctor.md +6 -0
  96. package/plugins/src/wiki/commands/ingest.md +6 -0
  97. package/plugins/src/wiki/commands/lint.md +6 -0
  98. package/plugins/src/wiki/commands/migrate.md +6 -0
  99. package/plugins/src/wiki/commands/onboard-me.md +6 -0
  100. package/plugins/src/wiki/commands/query.md +6 -0
  101. package/plugins/src/wiki/commands/setup.md +6 -0
  102. package/plugins/src/wiki/schema/lisa-wiki-config.schema.json +118 -0
  103. package/plugins/src/wiki/schema/wiki-structure.schema.json +51 -0
  104. package/plugins/src/wiki/scripts/_wiki-lib.mjs +185 -0
  105. package/plugins/src/wiki/scripts/diff-guard.mjs +116 -0
  106. package/plugins/src/wiki/scripts/ingest-git.mjs +189 -0
  107. package/plugins/src/wiki/scripts/ingest-memory.mjs +130 -0
  108. package/plugins/src/wiki/scripts/ingest-roles.mjs +85 -0
  109. package/plugins/src/wiki/scripts/ingest_slack_channel.py +329 -0
  110. package/plugins/src/wiki/scripts/lint-wiki.mjs +320 -0
  111. package/plugins/src/wiki/scripts/mcp-doctor.mjs +72 -0
  112. package/plugins/src/wiki/scripts/render-contract.mjs +107 -0
  113. package/plugins/src/wiki/scripts/rewrite-refs.mjs +144 -0
  114. package/plugins/src/wiki/scripts/slack_oauth_user.py +179 -0
  115. package/plugins/src/wiki/scripts/validate-config.mjs +232 -0
  116. package/plugins/src/wiki/scripts/verify-migration.mjs +199 -0
  117. package/plugins/src/wiki/skills/lisa-wiki-add-ingest/SKILL.md +34 -0
  118. package/plugins/src/wiki/skills/lisa-wiki-add-role/SKILL.md +30 -0
  119. package/plugins/src/wiki/skills/lisa-wiki-connector-confluence/SKILL.md +25 -0
  120. package/plugins/src/wiki/skills/lisa-wiki-connector-docs/SKILL.md +30 -0
  121. package/plugins/src/wiki/skills/lisa-wiki-connector-git/SKILL.md +25 -0
  122. package/plugins/src/wiki/skills/lisa-wiki-connector-jira/SKILL.md +28 -0
  123. package/plugins/src/wiki/skills/lisa-wiki-connector-memory/SKILL.md +28 -0
  124. package/plugins/src/wiki/skills/lisa-wiki-connector-notion/SKILL.md +25 -0
  125. package/plugins/src/wiki/skills/lisa-wiki-connector-roles/SKILL.md +22 -0
  126. package/plugins/src/wiki/skills/lisa-wiki-connector-slack/SKILL.md +30 -0
  127. package/plugins/src/wiki/skills/lisa-wiki-connector-web/SKILL.md +23 -0
  128. package/plugins/src/wiki/skills/lisa-wiki-doctor/SKILL.md +47 -0
  129. package/plugins/src/wiki/skills/lisa-wiki-ingest/SKILL.md +43 -0
  130. package/plugins/src/wiki/skills/lisa-wiki-lint/SKILL.md +32 -0
  131. package/plugins/src/wiki/skills/lisa-wiki-migrate/SKILL.md +43 -0
  132. package/plugins/src/wiki/skills/lisa-wiki-onboard-me/SKILL.md +33 -0
  133. package/plugins/src/wiki/skills/lisa-wiki-query/SKILL.md +30 -0
  134. package/plugins/src/wiki/skills/lisa-wiki-setup/SKILL.md +45 -0
  135. package/plugins/src/wiki/skills/lisa-wiki-usage/SKILL.md +50 -0
  136. package/plugins/src/wiki/templates/agents/role-agent.claude.md +16 -0
  137. package/plugins/src/wiki/templates/agents/role-agent.codex.toml +15 -0
  138. package/plugins/src/wiki/templates/index.md +17 -0
  139. package/plugins/src/wiki/templates/llm-wiki-contract.md +60 -0
  140. package/plugins/src/wiki/templates/log.md +8 -0
  141. package/plugins/src/wiki/templates/page-types/architecture.md +18 -0
  142. package/plugins/src/wiki/templates/page-types/concept.md +18 -0
  143. package/plugins/src/wiki/templates/page-types/decision.md +18 -0
  144. package/plugins/src/wiki/templates/page-types/entity.md +19 -0
  145. package/plugins/src/wiki/templates/page-types/open-question.md +18 -0
  146. package/plugins/src/wiki/templates/page-types/playbook.md +18 -0
  147. package/plugins/src/wiki/templates/page-types/project.md +19 -0
  148. package/plugins/src/wiki/templates/page-types/requirement.md +19 -0
  149. package/plugins/src/wiki/templates/page-types/staff.md +26 -0
  150. package/plugins/src/wiki/templates/start-here.md +24 -0
  151. package/plugins/src/wiki/templates/state-readme.md +20 -0
  152. package/scripts/build-plugins.sh +29 -21
  153. package/scripts/check-plugins-sync.sh +38 -1
  154. package/scripts/generate-codex-plugin-artifacts.mjs +22 -0
@@ -0,0 +1,118 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://codyswann.github.io/lisa/schema/lisa-wiki-config.schema.json",
4
+ "title": "lisa-wiki project config",
5
+ "description": "Per-project behavioral config for the lisa-wiki plugin. Lives at wiki/lisa-wiki.config.json. The plugin's validate-config script enforces these constraints dependency-free; this schema is the authoritative spec and powers editor tooling.",
6
+ "type": "object",
7
+ "additionalProperties": true,
8
+ "required": ["schemaVersion", "org", "mode", "wikiRoot", "categories"],
9
+ "properties": {
10
+ "schemaVersion": { "type": "string", "description": "Config schema version (semver)." },
11
+ "org": { "type": "string", "description": "Owning organization / project name." },
12
+ "displayName": { "type": "string", "description": "Human-facing wiki name." },
13
+ "purpose": { "type": "string", "description": "One paragraph: what this wiki is for. Asked by /setup; rendered into the contract + start-here; feeds /onboard-me." },
14
+ "mode": { "enum": ["embedded", "wrapper", "standalone", "subdir"], "description": "Repository model." },
15
+ "wikiRoot": { "type": "string", "default": "wiki", "description": "Path to the wiki root, relative to repo root." },
16
+ "frontmatter": { "type": "boolean", "default": true, "description": "Whether new/touched pages require YAML frontmatter." },
17
+ "categories": { "type": "array", "items": { "type": "string" }, "minItems": 1, "description": "Synthesis category directories." },
18
+ "sources": {
19
+ "type": "object",
20
+ "additionalProperties": true,
21
+ "properties": {
22
+ "layout": { "enum": ["by-system", "by-category"], "default": "by-system" },
23
+ "buckets": { "type": "array", "items": { "type": "string" } }
24
+ }
25
+ },
26
+ "git": {
27
+ "type": "object",
28
+ "additionalProperties": true,
29
+ "properties": {
30
+ "prPerIngestion": { "type": "boolean" },
31
+ "autoMerge": { "type": "boolean" },
32
+ "targetBranch": { "type": "string" },
33
+ "branchPrefix": { "type": "string" }
34
+ }
35
+ },
36
+ "sensitivity": {
37
+ "type": "object",
38
+ "additionalProperties": true,
39
+ "properties": {
40
+ "enabled": { "type": "boolean" },
41
+ "default": { "enum": ["public", "internal", "confidential", "restricted"] }
42
+ }
43
+ },
44
+ "sourceRetention": { "enum": ["raw-ok", "sanitized-note-only", "metadata-only", "external-pointer-only"] },
45
+ "readme": {
46
+ "type": "object",
47
+ "additionalProperties": false,
48
+ "properties": { "mode": { "enum": ["rich", "stub", "preserve"], "default": "rich" } }
49
+ },
50
+ "documentation": {
51
+ "type": "object",
52
+ "additionalProperties": true,
53
+ "properties": {
54
+ "absorb": { "type": "boolean", "default": true },
55
+ "keepInPlace": { "type": "array", "items": { "type": "string" } }
56
+ }
57
+ },
58
+ "onboarding": {
59
+ "type": "object",
60
+ "additionalProperties": true,
61
+ "properties": { "allowAudienceNote": { "type": "boolean", "default": false } }
62
+ },
63
+ "connectors": {
64
+ "type": "object",
65
+ "description": "Map of connector name -> connector config.",
66
+ "additionalProperties": {
67
+ "type": "object",
68
+ "additionalProperties": true,
69
+ "required": ["sideEffects"],
70
+ "properties": {
71
+ "enabled": { "type": "boolean" },
72
+ "sideEffects": { "enum": ["read-only-ingest", "repo-write", "external-write"] }
73
+ }
74
+ }
75
+ },
76
+ "customConnectors": {
77
+ "type": "array",
78
+ "description": "Explicit allowlist of project-authored front-door ingest skills (generated by /add-ingest). No auto-discovery.",
79
+ "items": {
80
+ "type": "object",
81
+ "additionalProperties": true,
82
+ "required": ["name", "skill", "sourceSystem", "sideEffects"],
83
+ "properties": {
84
+ "name": { "type": "string" },
85
+ "skill": { "type": "string" },
86
+ "sourceSystem": { "type": "string" },
87
+ "stateFile": { "type": "string" },
88
+ "sideEffects": { "enum": ["read-only-ingest", "repo-write", "external-write"] }
89
+ }
90
+ }
91
+ },
92
+ "staff": {
93
+ "type": "array",
94
+ "description": "Digital-staff roles. Each generates a wiki/staff/<id>.md doc page + dual-runtime subagents.",
95
+ "items": {
96
+ "type": "object",
97
+ "additionalProperties": true,
98
+ "required": ["id", "role"],
99
+ "properties": {
100
+ "id": { "type": "string" },
101
+ "role": { "type": "string" },
102
+ "expertise": { "type": "string" },
103
+ "owns": {
104
+ "type": "object",
105
+ "additionalProperties": true,
106
+ "properties": {
107
+ "categories": { "type": "array", "items": { "type": "string" } },
108
+ "connectors": { "type": "array", "items": { "type": "string" } },
109
+ "skills": { "type": "array", "items": { "type": "string" } }
110
+ }
111
+ },
112
+ "sensitivity": { "enum": ["public", "internal", "confidential", "restricted"] }
113
+ }
114
+ }
115
+ },
116
+ "contaminationTerms": { "type": "array", "items": { "type": "string" }, "description": "Cross-tenant terms scanned for before every commit." }
117
+ }
118
+ }
@@ -0,0 +1,51 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://codyswann.github.io/lisa/schema/wiki-structure.schema.json",
4
+ "title": "lisa-wiki canonical structure manifest",
5
+ "description": "The canonical folder structure of a lisa-wiki. This manifest is the intended single source of truth for structure conformance, consumed by lint-wiki and validate-config as of M1 (structure enforcement) and by render-contract; in M0 it is the published spec. All paths are relative to config.wikiRoot unless noted. It is a descriptive manifest (not a JSON-Schema validation of a document); validators interpret these fields.",
6
+ "manifestVersion": "1.0.0",
7
+ "requiredFiles": [
8
+ "lisa-wiki.config.json",
9
+ "index.md",
10
+ "log.md",
11
+ "start-here.md",
12
+ "schema/llm-wiki-contract.md"
13
+ ],
14
+ "requiredDirs": ["schema", "sources", "state"],
15
+ "optionalDirs": ["staff", "documentation"],
16
+ "categoryDirs": {
17
+ "default": [
18
+ "concepts",
19
+ "entities",
20
+ "decisions",
21
+ "architecture",
22
+ "requirements",
23
+ "playbooks",
24
+ "open-questions",
25
+ "projects"
26
+ ],
27
+ "note": "A project may declare additional categories in config.categories (e.g. claims, comparisons, finance, sales)."
28
+ },
29
+ "sourcesLayout": {
30
+ "by-system": "sources/<system>/ (e.g. sources/jira/, sources/git/, sources/slack/)",
31
+ "by-category": "sources/<bucket>/ (e.g. sources/primary/, sources/secondary/)"
32
+ },
33
+ "stateLayout": "state/<system>/*.json",
34
+ "reportPath": "state/migration/doctor-report.json",
35
+ "generatedArtifacts": {
36
+ "claudeRoleAgents": ".claude/agents/<id>.md (repo root, not under wikiRoot)",
37
+ "codexRoleAgents": ".codex/agents/<id>.toml (repo root, not under wikiRoot)",
38
+ "frontDoorSkills": ".claude|.agents/skills/lisa-wiki-local-*"
39
+ },
40
+ "keepInPlaceDefault": [
41
+ "LICENSE*",
42
+ "SECURITY.md",
43
+ "CODE_OF_CONDUCT.md",
44
+ "CONTRIBUTING.md",
45
+ "CHANGELOG.md",
46
+ "NOTICE*",
47
+ "SUPPORT.md",
48
+ "GOVERNANCE.md",
49
+ ".github/**"
50
+ ]
51
+ }
@@ -0,0 +1,185 @@
1
+ /**
2
+ * _wiki-lib.mjs — shared, dependency-free helpers for the lisa-wiki validators
3
+ * (lint-wiki, diff-guard, rewrite-refs, verify-migration). Node built-ins only,
4
+ * so the scripts stay portable to any downstream repo that installs the plugin.
5
+ */
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+
9
+ /** Read and parse a JSON file, or return undefined if missing/invalid. */
10
+ export function readJsonSafe(file) {
11
+ try {
12
+ return JSON.parse(fs.readFileSync(file, "utf8"));
13
+ } catch {
14
+ return undefined;
15
+ }
16
+ }
17
+
18
+ /** Resolve the plugin root from a script in scripts/ (parent of scripts/). */
19
+ export function pluginRootFrom(scriptDir) {
20
+ return path.dirname(scriptDir);
21
+ }
22
+
23
+ /** Load the project config (wiki/lisa-wiki.config.json by default). */
24
+ export function loadConfig(configPath) {
25
+ const resolved = path.resolve(configPath ?? "wiki/lisa-wiki.config.json");
26
+ const config = readJsonSafe(resolved);
27
+ return { config, configPath: resolved };
28
+ }
29
+
30
+ /** Load the canonical structure manifest shipped with the plugin. */
31
+ export function loadStructure(pluginRoot) {
32
+ return readJsonSafe(
33
+ path.join(pluginRoot, "schema", "wiki-structure.schema.json")
34
+ );
35
+ }
36
+
37
+ /** Recursively list files under dir matching an optional extension filter. */
38
+ export function walkFiles(dir, { ext } = {}) {
39
+ const out = [];
40
+ const stack = [dir];
41
+ while (stack.length > 0) {
42
+ const current = stack.pop();
43
+ let entries = [];
44
+ try {
45
+ entries = fs.readdirSync(current, { withFileTypes: true });
46
+ } catch {
47
+ continue;
48
+ }
49
+ for (const entry of entries) {
50
+ const full = path.join(current, entry.name);
51
+ if (entry.isDirectory()) {
52
+ if (entry.name === ".git" || entry.name === "node_modules") continue;
53
+ stack.push(full);
54
+ } else if (entry.isFile()) {
55
+ if (!ext || full.endsWith(ext)) out.push(full);
56
+ }
57
+ }
58
+ }
59
+ return out.sort();
60
+ }
61
+
62
+ /**
63
+ * Minimal frontmatter detector. Returns whether a leading `--- ... ---` block
64
+ * exists and the top-level keys it declares (enough to check required fields;
65
+ * NOT a full YAML parser).
66
+ */
67
+ export function parseFrontmatter(text) {
68
+ const lines = text.split("\n");
69
+ if (lines[0].trim() !== "---") return { has: false, keys: [], body: text };
70
+ let closeIdx = -1;
71
+ for (let i = 1; i < lines.length; i += 1) {
72
+ if (lines[i].trim() === "---") {
73
+ closeIdx = i;
74
+ break;
75
+ }
76
+ }
77
+ if (closeIdx === -1) return { has: false, keys: [], body: text };
78
+ const keys = [];
79
+ for (let i = 1; i < closeIdx; i += 1) {
80
+ const m = lines[i].match(/^([A-Za-z][\w-]*):/);
81
+ if (m) keys.push(m[1]);
82
+ }
83
+ const body = lines.slice(closeIdx + 1).join("\n");
84
+ return { has: true, keys, body };
85
+ }
86
+
87
+ /**
88
+ * Extract internal markdown link targets ([txt](target)) — excludes external
89
+ * (http/https/mailto), anchors, and template tokens. Anchors are stripped.
90
+ */
91
+ export function extractMarkdownLinks(text) {
92
+ const targets = [];
93
+ const re = /\[[^\]]*\]\(([^)]+)\)/g;
94
+ let m;
95
+ while ((m = re.exec(text)) !== null) {
96
+ let target = m[1].trim();
97
+ if (!target || target.startsWith("#")) continue;
98
+ if (target.includes("{{") || target.includes("<") || target.includes(">"))
99
+ continue; // template token / placeholder / angle-bracket autolink
100
+ target = target.split(/\s+/)[0]; // drop optional link title: (url "title")
101
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(target) || target.startsWith("mailto:"))
102
+ continue;
103
+ target = target.split("#")[0].trim();
104
+ if (target) targets.push(target);
105
+ }
106
+ return targets;
107
+ }
108
+
109
+ /** Extract plain-text `Source: <path>.md` citations that look like wiki paths. */
110
+ export function extractCitations(text) {
111
+ const cites = [];
112
+ const re = /Source:\s*([^\s)]+\.md)/g;
113
+ let m;
114
+ while ((m = re.exec(text)) !== null) {
115
+ const c = m[1];
116
+ if (c.includes("{{") || c.includes("<") || c.includes(">")) continue; // template token / placeholder example
117
+ cites.push(c);
118
+ }
119
+ return cites;
120
+ }
121
+
122
+ /** Convert a tiny glob (supporting ** and *) to a RegExp anchored full-match. */
123
+ export function globToRegExp(glob) {
124
+ let re = "^";
125
+ for (let i = 0; i < glob.length; i += 1) {
126
+ const c = glob[i];
127
+ if (c === "*") {
128
+ if (glob[i + 1] === "*") {
129
+ re += ".*";
130
+ i += 1;
131
+ if (glob[i + 1] === "/") i += 1; // consume trailing slash of **/
132
+ } else {
133
+ re += "[^/]*";
134
+ }
135
+ } else if ("\\^$+?.()|[]{}".includes(c)) {
136
+ re += `\\${c}`;
137
+ } else {
138
+ re += c;
139
+ }
140
+ }
141
+ re += "$";
142
+ return new RegExp(re);
143
+ }
144
+
145
+ /** Secret-detection patterns (kept minimal + high-signal). */
146
+ export const SECRET_PATTERNS = [
147
+ { name: "Slack token", re: /xox[pbar]-[A-Za-z0-9-]{10,}/ },
148
+ { name: "AWS access key", re: /AKIA[0-9A-Z]{16}/ },
149
+ {
150
+ name: "private key header",
151
+ re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/,
152
+ },
153
+ { name: "bearer token", re: /bearer\s+[A-Za-z0-9._-]{20,}/i },
154
+ {
155
+ name: "client secret assignment",
156
+ re: /client_secret["'\s:=]+[A-Za-z0-9._-]{16,}/i,
157
+ },
158
+ ];
159
+
160
+ /** Text-ish file extensions allowed inside a wiki (others flagged as binaries). */
161
+ export const TEXT_EXTS = new Set([
162
+ ".md",
163
+ ".mdx",
164
+ ".json",
165
+ ".jsonl",
166
+ ".txt",
167
+ ".yml",
168
+ ".yaml",
169
+ ".toml",
170
+ ".csv",
171
+ ".tsv",
172
+ ".svg",
173
+ ".gitkeep",
174
+ ]);
175
+
176
+ /** Severity-tagged result accumulator. */
177
+ export function makeReport() {
178
+ const items = [];
179
+ return {
180
+ add(group, id, status, message, file) {
181
+ items.push({ group, id, status, message, ...(file ? { file } : {}) });
182
+ },
183
+ items,
184
+ };
185
+ }
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * diff-guard.mjs — enforce a connector's touched-file boundary. Dependency-free
4
+ * (git + Node built-ins).
5
+ *
6
+ * Run AFTER a connector returns and BEFORE the kernel synthesizes: a connector may
7
+ * write ONLY its declared source-note path(s) and run-metadata. Any repo change
8
+ * outside the allowed globs is a hard failure (this is what makes "source-note only"
9
+ * enforceable, not aspirational). Writes outside the repo (e.g. external-write to a
10
+ * remote system) are not git-visible and so are not policed here.
11
+ *
12
+ * The change set ALWAYS includes untracked files (connectors create new files), so a
13
+ * stray new file can never bypass the guard. `--base <ref>` additionally unions a
14
+ * committed range. Paths are read NUL-safe to handle renames/quoting.
15
+ *
16
+ * Usage: node diff-guard.mjs --allow <glob> [--allow <glob>...] [--base <ref>]
17
+ * Exit 0 = all changes within allowed globs, 1 = out-of-bounds change (or git error).
18
+ */
19
+ import { execFileSync } from "node:child_process";
20
+ import { globToRegExp } from "./_wiki-lib.mjs";
21
+
22
+ function fail(msg) {
23
+ console.error(`✗ ${msg}`);
24
+ process.exit(1);
25
+ }
26
+
27
+ const argv = process.argv.slice(2);
28
+ const allows = [];
29
+ let base;
30
+ for (let i = 0; i < argv.length; i += 1) {
31
+ const a = argv[i];
32
+ if (a === "--allow") {
33
+ const v = argv[i + 1];
34
+ if (!v || v.startsWith("--")) fail("--allow requires a glob argument");
35
+ allows.push(v);
36
+ i += 1;
37
+ } else if (a === "--base") {
38
+ const v = argv[i + 1];
39
+ if (!v || v.startsWith("--")) fail("--base requires a ref argument");
40
+ base = v;
41
+ i += 1;
42
+ } else {
43
+ fail(`unknown argument: ${a}`);
44
+ }
45
+ }
46
+
47
+ if (allows.length === 0) {
48
+ fail(
49
+ "diff-guard requires at least one --allow <glob> (the connector's permitted paths)"
50
+ );
51
+ }
52
+
53
+ function git(args) {
54
+ return execFileSync("git", args, { encoding: "utf8" });
55
+ }
56
+
57
+ let repoRoot;
58
+ try {
59
+ repoRoot = git(["rev-parse", "--show-toplevel"]).trim();
60
+ } catch {
61
+ fail("not inside a git repository");
62
+ }
63
+ const inRepo = args => ["-C", repoRoot, ...args];
64
+
65
+ // Working-tree change set, NUL-safe, including untracked files. Renames/copies
66
+ // (status starts with R/C) emit the destination then the source as separate NUL
67
+ // records — include both so neither side can slip outside the allow-list.
68
+ function workingTreePaths() {
69
+ const out = git(
70
+ inRepo(["status", "--porcelain=v1", "-z", "--untracked-files=all"])
71
+ );
72
+ const records = out.split("\0");
73
+ const paths = [];
74
+ for (let i = 0; i < records.length; i += 1) {
75
+ const rec = records[i];
76
+ if (!rec) continue;
77
+ const status = rec.slice(0, 2);
78
+ const p = rec.slice(3);
79
+ if (p) paths.push(p);
80
+ if (status[0] === "R" || status[0] === "C") {
81
+ i += 1;
82
+ if (records[i]) paths.push(records[i]);
83
+ }
84
+ }
85
+ return paths;
86
+ }
87
+
88
+ let changed = [];
89
+ try {
90
+ changed = workingTreePaths();
91
+ if (base) {
92
+ changed = changed.concat(
93
+ git(inRepo(["diff", "--name-only", "-z", base])).split("\0")
94
+ );
95
+ }
96
+ } catch (e) {
97
+ fail(`git failed: ${e.message}`);
98
+ }
99
+
100
+ changed = [...new Set(changed.map(s => s.trim()).filter(Boolean))];
101
+ const matchers = allows.map(globToRegExp);
102
+ const isAllowed = p => matchers.some(re => re.test(p));
103
+ const violations = changed.filter(p => !isAllowed(p));
104
+
105
+ if (violations.length > 0) {
106
+ console.error(
107
+ `✗ diff-guard: ${violations.length} change(s) outside the allowed connector paths:`
108
+ );
109
+ for (const v of violations) console.error(` - ${v}`);
110
+ console.error(` allowed: ${allows.join(", ")}`);
111
+ process.exit(1);
112
+ }
113
+
114
+ console.log(
115
+ `✓ diff-guard: ${changed.length} change(s) all within allowed paths (${allows.join(", ")}).`
116
+ );
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ingest-git.mjs — git/PR-history connector. Dependency-free (git + optional gh).
4
+ * Read-only: it never checks out, fetches, or mutates the target repo.
5
+ *
6
+ * Writes a sanitized, dated source note under <source-dir> summarizing commits (and
7
+ * merged PRs, if `gh` is available) since the cursor, and emits a PROPOSED next cursor
8
+ * to --emit-meta. It does NOT advance final state — the kernel does that after
9
+ * verification (per the §7 connector contract).
10
+ *
11
+ * Usage:
12
+ * node ingest-git.mjs --repo <path> [--slug <name>] [--config <p>]
13
+ * [--source-dir <dir>] [--state <file>] [--emit-meta <file>]
14
+ */
15
+ import fs from "node:fs";
16
+ import path from "node:path";
17
+ import { execFileSync } from "node:child_process";
18
+ import { readJsonSafe, SECRET_PATTERNS } from "./_wiki-lib.mjs";
19
+
20
+ const argv = process.argv.slice(2);
21
+ const opt = (n, d) => {
22
+ const i = argv.indexOf(n);
23
+ return i !== -1 ? argv[i + 1] : d;
24
+ };
25
+ const repo = path.resolve(opt("--repo", "."));
26
+ const slug = opt("--slug", path.basename(repo));
27
+ const githubRepo = opt("--github-repo"); // owner/repo for PR history (else inferred via gh)
28
+ const fileSlug = slug.replace(/[^A-Za-z0-9_.-]+/g, "-"); // safe for filenames
29
+ const sourceDir = path.resolve(opt("--source-dir", "wiki/sources/git"));
30
+ const statePath = opt("--state");
31
+ const emitMeta = opt("--emit-meta");
32
+
33
+ const fail = m => {
34
+ console.error(`✗ ${m}`);
35
+ process.exit(1);
36
+ };
37
+ const git = args =>
38
+ execFileSync("git", ["-C", repo, ...args], { encoding: "utf8" }).trim();
39
+ const tryGit = args => {
40
+ try {
41
+ return git(args);
42
+ } catch {
43
+ return "";
44
+ }
45
+ };
46
+ const commitExists = c => {
47
+ try {
48
+ execFileSync("git", ["-C", repo, "cat-file", "-e", `${c}^{commit}`], {
49
+ stdio: "ignore",
50
+ });
51
+ return true;
52
+ } catch {
53
+ return false;
54
+ }
55
+ };
56
+ const redact = t =>
57
+ SECRET_PATTERNS.reduce(
58
+ (acc, { re }) => acc.replace(new RegExp(re, "g"), "[REDACTED]"),
59
+ t
60
+ );
61
+
62
+ if (
63
+ !fs.existsSync(path.join(repo, ".git")) &&
64
+ !tryGit(["rev-parse", "--is-inside-work-tree"])
65
+ ) {
66
+ fail(`not a git repository: ${repo}`);
67
+ }
68
+
69
+ const cursor = statePath ? (readJsonSafe(statePath)?.cursor ?? {}) : {};
70
+ const lastCommit = cursor.lastCommit;
71
+ const head = tryGit(["rev-parse", "HEAD"]);
72
+ if (!head) fail("could not resolve HEAD (empty repo?)");
73
+ if (lastCommit && !commitExists(lastCommit)) {
74
+ fail(
75
+ `cursor commit ${lastCommit} not found in ${repo}; refusing to silently re-window from HEAD`
76
+ );
77
+ }
78
+
79
+ const range = lastCommit ? `${lastCommit}..HEAD` : "HEAD";
80
+ const logLines = tryGit([
81
+ "log",
82
+ range,
83
+ "--pretty=format:%h\t%ad\t%s",
84
+ "--date=short",
85
+ ])
86
+ .split("\n")
87
+ .filter(Boolean);
88
+ const totalCommits = Number(tryGit(["rev-list", "--count", "HEAD"]) || "0");
89
+
90
+ // merged PRs via gh (optional, read-only); resolve the GitHub repo explicitly
91
+ let lastPr = cursor.lastPr ?? null;
92
+ let prSummary = "(no GitHub repo resolved — PR history skipped)";
93
+ let ghRepo = githubRepo;
94
+ if (!ghRepo) {
95
+ try {
96
+ ghRepo = execFileSync(
97
+ "gh",
98
+ ["repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"],
99
+ { cwd: repo, encoding: "utf8" }
100
+ ).trim();
101
+ } catch {
102
+ ghRepo = "";
103
+ }
104
+ }
105
+ if (ghRepo) {
106
+ try {
107
+ const prs = JSON.parse(
108
+ execFileSync(
109
+ "gh",
110
+ [
111
+ "pr",
112
+ "list",
113
+ "--repo",
114
+ ghRepo,
115
+ "--state",
116
+ "merged",
117
+ "--limit",
118
+ "20",
119
+ "--json",
120
+ "number,title,mergedAt",
121
+ ],
122
+ { cwd: repo, encoding: "utf8" }
123
+ )
124
+ );
125
+ if (prs.length) {
126
+ lastPr = prs[0].number;
127
+ prSummary = `${prs.length} recent merged PR(s) in ${ghRepo}; latest #${prs[0].number} "${prs[0].title}"`;
128
+ } else {
129
+ prSummary = `no merged PRs found in ${ghRepo}`;
130
+ }
131
+ } catch {
132
+ prSummary = `(gh pr list failed for ${ghRepo} — PR history skipped)`;
133
+ }
134
+ }
135
+
136
+ const date = new Date().toISOString().slice(0, 10);
137
+ const newCommits = logLines.length;
138
+ const notePath = path.join(sourceDir, `${date}-${fileSlug}-git.md`);
139
+ const note = `---
140
+ type: source
141
+ created: ${date}
142
+ updated: ${date}
143
+ related: []
144
+ sources: []
145
+ source_system: git
146
+ project: ${slug}
147
+ ---
148
+
149
+ # git history — ${slug} (${date})
150
+
151
+ - Repo: \`${repo}\`
152
+ - HEAD: \`${head}\`
153
+ - Total commits on HEAD: ${totalCommits}
154
+ - New commits since last ingest${lastCommit ? ` (\`${lastCommit}\`)` : " (first run)"}: ${newCommits}
155
+ - Merged PRs: ${prSummary}
156
+
157
+ ## New commits
158
+ ${
159
+ newCommits
160
+ ? logLines
161
+ .slice(0, 200)
162
+ .map(l => `- ${l.replace(/\t/g, " · ")}`)
163
+ .join("\n")
164
+ : "_(none)_"
165
+ }
166
+ `;
167
+
168
+ fs.mkdirSync(sourceDir, { recursive: true });
169
+ fs.writeFileSync(notePath, redact(note));
170
+
171
+ const meta = {
172
+ connector: "git",
173
+ profile: slug,
174
+ ranAt: new Date().toISOString(),
175
+ proposedCursor: { lastCommit: head, lastPr },
176
+ sourceNotes: [path.relative(process.cwd(), notePath)],
177
+ };
178
+ if (emitMeta) {
179
+ fs.mkdirSync(path.dirname(emitMeta), { recursive: true });
180
+ fs.writeFileSync(emitMeta, `${JSON.stringify(meta, null, 2)}\n`);
181
+ }
182
+
183
+ console.log(
184
+ `✓ git connector: ${newCommits} new commit(s) → ${path.relative(process.cwd(), notePath)}`
185
+ );
186
+ if (emitMeta)
187
+ console.log(
188
+ ` proposed cursor → ${path.relative(process.cwd(), emitMeta)} (kernel advances final state after verification)`
189
+ );