@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.
- package/.claude-plugin/marketplace.json +12 -0
- package/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/rules/config-resolution.md +32 -1
- package/plugins/lisa/skills/atlassian-access/SKILL.md +32 -1
- package/plugins/lisa/skills/notion-access/SKILL.md +32 -1
- package/plugins/lisa/skills/setup-atlassian/SKILL.md +32 -1
- package/plugins/lisa/skills/setup-linear/SKILL.md +32 -1
- package/plugins/lisa/skills/setup-notion/SKILL.md +32 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +8 -0
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +32 -0
- package/plugins/lisa-wiki/ci/lisa-wiki-validate.yml +32 -0
- package/plugins/lisa-wiki/commands/add-ingest.md +6 -0
- package/plugins/lisa-wiki/commands/add-role.md +6 -0
- package/plugins/lisa-wiki/commands/doctor.md +6 -0
- package/plugins/lisa-wiki/commands/ingest.md +6 -0
- package/plugins/lisa-wiki/commands/lint.md +6 -0
- package/plugins/lisa-wiki/commands/migrate.md +6 -0
- package/plugins/lisa-wiki/commands/onboard-me.md +6 -0
- package/plugins/lisa-wiki/commands/query.md +6 -0
- package/plugins/lisa-wiki/commands/setup.md +6 -0
- package/plugins/lisa-wiki/schema/lisa-wiki-config.schema.json +118 -0
- package/plugins/lisa-wiki/schema/wiki-structure.schema.json +51 -0
- package/plugins/lisa-wiki/scripts/_wiki-lib.mjs +185 -0
- package/plugins/lisa-wiki/scripts/diff-guard.mjs +116 -0
- package/plugins/lisa-wiki/scripts/ingest-git.mjs +189 -0
- package/plugins/lisa-wiki/scripts/ingest-memory.mjs +130 -0
- package/plugins/lisa-wiki/scripts/ingest-roles.mjs +85 -0
- package/plugins/lisa-wiki/scripts/ingest_slack_channel.py +329 -0
- package/plugins/lisa-wiki/scripts/lint-wiki.mjs +320 -0
- package/plugins/lisa-wiki/scripts/mcp-doctor.mjs +72 -0
- package/plugins/lisa-wiki/scripts/render-contract.mjs +107 -0
- package/plugins/lisa-wiki/scripts/rewrite-refs.mjs +144 -0
- package/plugins/lisa-wiki/scripts/slack_oauth_user.py +179 -0
- package/plugins/lisa-wiki/scripts/validate-config.mjs +232 -0
- package/plugins/lisa-wiki/scripts/verify-migration.mjs +199 -0
- package/plugins/lisa-wiki/skills/lisa-wiki-add-ingest/SKILL.md +34 -0
- package/plugins/lisa-wiki/skills/lisa-wiki-add-role/SKILL.md +30 -0
- package/plugins/lisa-wiki/skills/lisa-wiki-connector-confluence/SKILL.md +25 -0
- package/plugins/lisa-wiki/skills/lisa-wiki-connector-docs/SKILL.md +30 -0
- package/plugins/lisa-wiki/skills/lisa-wiki-connector-git/SKILL.md +25 -0
- package/plugins/lisa-wiki/skills/lisa-wiki-connector-jira/SKILL.md +28 -0
- package/plugins/lisa-wiki/skills/lisa-wiki-connector-memory/SKILL.md +28 -0
- package/plugins/lisa-wiki/skills/lisa-wiki-connector-notion/SKILL.md +25 -0
- package/plugins/lisa-wiki/skills/lisa-wiki-connector-roles/SKILL.md +22 -0
- package/plugins/lisa-wiki/skills/lisa-wiki-connector-slack/SKILL.md +30 -0
- package/plugins/lisa-wiki/skills/lisa-wiki-connector-web/SKILL.md +23 -0
- package/plugins/lisa-wiki/skills/lisa-wiki-doctor/SKILL.md +47 -0
- package/plugins/lisa-wiki/skills/lisa-wiki-ingest/SKILL.md +43 -0
- package/plugins/lisa-wiki/skills/lisa-wiki-lint/SKILL.md +32 -0
- package/plugins/lisa-wiki/skills/lisa-wiki-migrate/SKILL.md +43 -0
- package/plugins/lisa-wiki/skills/lisa-wiki-onboard-me/SKILL.md +33 -0
- package/plugins/lisa-wiki/skills/lisa-wiki-query/SKILL.md +30 -0
- package/plugins/lisa-wiki/skills/lisa-wiki-setup/SKILL.md +45 -0
- package/plugins/lisa-wiki/skills/lisa-wiki-usage/SKILL.md +50 -0
- package/plugins/lisa-wiki/templates/agents/role-agent.claude.md +16 -0
- package/plugins/lisa-wiki/templates/agents/role-agent.codex.toml +15 -0
- package/plugins/lisa-wiki/templates/index.md +17 -0
- package/plugins/lisa-wiki/templates/llm-wiki-contract.md +60 -0
- package/plugins/lisa-wiki/templates/log.md +8 -0
- package/plugins/lisa-wiki/templates/page-types/architecture.md +18 -0
- package/plugins/lisa-wiki/templates/page-types/concept.md +18 -0
- package/plugins/lisa-wiki/templates/page-types/decision.md +18 -0
- package/plugins/lisa-wiki/templates/page-types/entity.md +19 -0
- package/plugins/lisa-wiki/templates/page-types/open-question.md +18 -0
- package/plugins/lisa-wiki/templates/page-types/playbook.md +18 -0
- package/plugins/lisa-wiki/templates/page-types/project.md +19 -0
- package/plugins/lisa-wiki/templates/page-types/requirement.md +19 -0
- package/plugins/lisa-wiki/templates/page-types/staff.md +26 -0
- package/plugins/lisa-wiki/templates/start-here.md +24 -0
- package/plugins/lisa-wiki/templates/state-readme.md +20 -0
- package/plugins/src/base/rules/config-resolution.md +32 -1
- package/plugins/src/base/skills/atlassian-access/SKILL.md +32 -1
- package/plugins/src/base/skills/notion-access/SKILL.md +32 -1
- package/plugins/src/base/skills/setup-atlassian/SKILL.md +32 -1
- package/plugins/src/base/skills/setup-linear/SKILL.md +32 -1
- package/plugins/src/base/skills/setup-notion/SKILL.md +32 -1
- package/plugins/src/wiki/.claude-plugin/plugin.json +6 -0
- package/plugins/src/wiki/ci/lisa-wiki-validate.yml +32 -0
- package/plugins/src/wiki/commands/add-ingest.md +6 -0
- package/plugins/src/wiki/commands/add-role.md +6 -0
- package/plugins/src/wiki/commands/doctor.md +6 -0
- package/plugins/src/wiki/commands/ingest.md +6 -0
- package/plugins/src/wiki/commands/lint.md +6 -0
- package/plugins/src/wiki/commands/migrate.md +6 -0
- package/plugins/src/wiki/commands/onboard-me.md +6 -0
- package/plugins/src/wiki/commands/query.md +6 -0
- package/plugins/src/wiki/commands/setup.md +6 -0
- package/plugins/src/wiki/schema/lisa-wiki-config.schema.json +118 -0
- package/plugins/src/wiki/schema/wiki-structure.schema.json +51 -0
- package/plugins/src/wiki/scripts/_wiki-lib.mjs +185 -0
- package/plugins/src/wiki/scripts/diff-guard.mjs +116 -0
- package/plugins/src/wiki/scripts/ingest-git.mjs +189 -0
- package/plugins/src/wiki/scripts/ingest-memory.mjs +130 -0
- package/plugins/src/wiki/scripts/ingest-roles.mjs +85 -0
- package/plugins/src/wiki/scripts/ingest_slack_channel.py +329 -0
- package/plugins/src/wiki/scripts/lint-wiki.mjs +320 -0
- package/plugins/src/wiki/scripts/mcp-doctor.mjs +72 -0
- package/plugins/src/wiki/scripts/render-contract.mjs +107 -0
- package/plugins/src/wiki/scripts/rewrite-refs.mjs +144 -0
- package/plugins/src/wiki/scripts/slack_oauth_user.py +179 -0
- package/plugins/src/wiki/scripts/validate-config.mjs +232 -0
- package/plugins/src/wiki/scripts/verify-migration.mjs +199 -0
- package/plugins/src/wiki/skills/lisa-wiki-add-ingest/SKILL.md +34 -0
- package/plugins/src/wiki/skills/lisa-wiki-add-role/SKILL.md +30 -0
- package/plugins/src/wiki/skills/lisa-wiki-connector-confluence/SKILL.md +25 -0
- package/plugins/src/wiki/skills/lisa-wiki-connector-docs/SKILL.md +30 -0
- package/plugins/src/wiki/skills/lisa-wiki-connector-git/SKILL.md +25 -0
- package/plugins/src/wiki/skills/lisa-wiki-connector-jira/SKILL.md +28 -0
- package/plugins/src/wiki/skills/lisa-wiki-connector-memory/SKILL.md +28 -0
- package/plugins/src/wiki/skills/lisa-wiki-connector-notion/SKILL.md +25 -0
- package/plugins/src/wiki/skills/lisa-wiki-connector-roles/SKILL.md +22 -0
- package/plugins/src/wiki/skills/lisa-wiki-connector-slack/SKILL.md +30 -0
- package/plugins/src/wiki/skills/lisa-wiki-connector-web/SKILL.md +23 -0
- package/plugins/src/wiki/skills/lisa-wiki-doctor/SKILL.md +47 -0
- package/plugins/src/wiki/skills/lisa-wiki-ingest/SKILL.md +43 -0
- package/plugins/src/wiki/skills/lisa-wiki-lint/SKILL.md +32 -0
- package/plugins/src/wiki/skills/lisa-wiki-migrate/SKILL.md +43 -0
- package/plugins/src/wiki/skills/lisa-wiki-onboard-me/SKILL.md +33 -0
- package/plugins/src/wiki/skills/lisa-wiki-query/SKILL.md +30 -0
- package/plugins/src/wiki/skills/lisa-wiki-setup/SKILL.md +45 -0
- package/plugins/src/wiki/skills/lisa-wiki-usage/SKILL.md +50 -0
- package/plugins/src/wiki/templates/agents/role-agent.claude.md +16 -0
- package/plugins/src/wiki/templates/agents/role-agent.codex.toml +15 -0
- package/plugins/src/wiki/templates/index.md +17 -0
- package/plugins/src/wiki/templates/llm-wiki-contract.md +60 -0
- package/plugins/src/wiki/templates/log.md +8 -0
- package/plugins/src/wiki/templates/page-types/architecture.md +18 -0
- package/plugins/src/wiki/templates/page-types/concept.md +18 -0
- package/plugins/src/wiki/templates/page-types/decision.md +18 -0
- package/plugins/src/wiki/templates/page-types/entity.md +19 -0
- package/plugins/src/wiki/templates/page-types/open-question.md +18 -0
- package/plugins/src/wiki/templates/page-types/playbook.md +18 -0
- package/plugins/src/wiki/templates/page-types/project.md +19 -0
- package/plugins/src/wiki/templates/page-types/requirement.md +19 -0
- package/plugins/src/wiki/templates/page-types/staff.md +26 -0
- package/plugins/src/wiki/templates/start-here.md +24 -0
- package/plugins/src/wiki/templates/state-readme.md +20 -0
- package/scripts/build-plugins.sh +29 -21
- package/scripts/check-plugins-sync.sh +38 -1
- package/scripts/generate-codex-plugin-artifacts.mjs +22 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Run a local Slack OAuth flow for a user token.
|
|
3
|
+
|
|
4
|
+
This helper intentionally requests user scopes, not bot scopes. It stores the
|
|
5
|
+
OAuth response in an ignored local file by default; do not commit that file.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import base64
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import secrets
|
|
15
|
+
import stat
|
|
16
|
+
import sys
|
|
17
|
+
import urllib.parse
|
|
18
|
+
import urllib.request
|
|
19
|
+
import webbrowser
|
|
20
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
DEFAULT_SCOPES = ",".join(
|
|
25
|
+
[
|
|
26
|
+
"channels:read",
|
|
27
|
+
"channels:history",
|
|
28
|
+
"groups:read",
|
|
29
|
+
"groups:history",
|
|
30
|
+
"users:read",
|
|
31
|
+
"files:read",
|
|
32
|
+
]
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class OAuthHandler(BaseHTTPRequestHandler):
|
|
37
|
+
server: "OAuthServer"
|
|
38
|
+
|
|
39
|
+
def log_message(self, fmt: str, *args: object) -> None:
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
def do_GET(self) -> None: # noqa: N802
|
|
43
|
+
parsed = urllib.parse.urlparse(self.path)
|
|
44
|
+
params = urllib.parse.parse_qs(parsed.query)
|
|
45
|
+
state = params.get("state", [""])[0]
|
|
46
|
+
code = params.get("code", [""])[0]
|
|
47
|
+
error = params.get("error", [""])[0]
|
|
48
|
+
|
|
49
|
+
if error:
|
|
50
|
+
self.server.error = error
|
|
51
|
+
self._respond(400, f"Slack OAuth failed: {error}")
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
if state != self.server.expected_state:
|
|
55
|
+
self.server.error = "state_mismatch"
|
|
56
|
+
self._respond(400, "Slack OAuth failed: state mismatch.")
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
if not code:
|
|
60
|
+
self.server.error = "missing_code"
|
|
61
|
+
self._respond(400, "Slack OAuth failed: missing code.")
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
self.server.code = code
|
|
65
|
+
self._respond(200, "Slack OAuth complete. You can return to the terminal.")
|
|
66
|
+
|
|
67
|
+
def _respond(self, status: int, body: str) -> None:
|
|
68
|
+
data = body.encode("utf-8")
|
|
69
|
+
self.send_response(status)
|
|
70
|
+
self.send_header("Content-Type", "text/plain; charset=utf-8")
|
|
71
|
+
self.send_header("Content-Length", str(len(data)))
|
|
72
|
+
self.end_headers()
|
|
73
|
+
self.wfile.write(data)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class OAuthServer(HTTPServer):
|
|
77
|
+
expected_state: str
|
|
78
|
+
code: str | None
|
|
79
|
+
error: str | None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def exchange_code(
|
|
83
|
+
*,
|
|
84
|
+
client_id: str,
|
|
85
|
+
client_secret: str,
|
|
86
|
+
code: str,
|
|
87
|
+
redirect_uri: str,
|
|
88
|
+
) -> dict:
|
|
89
|
+
body = urllib.parse.urlencode(
|
|
90
|
+
{
|
|
91
|
+
"grant_type": "authorization_code",
|
|
92
|
+
"code": code,
|
|
93
|
+
"redirect_uri": redirect_uri,
|
|
94
|
+
}
|
|
95
|
+
).encode("utf-8")
|
|
96
|
+
basic = base64.b64encode(f"{client_id}:{client_secret}".encode("utf-8")).decode("ascii")
|
|
97
|
+
request = urllib.request.Request(
|
|
98
|
+
"https://slack.com/api/oauth.v2.access",
|
|
99
|
+
data=body,
|
|
100
|
+
headers={
|
|
101
|
+
"Authorization": f"Basic {basic}",
|
|
102
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
103
|
+
"Accept": "application/json",
|
|
104
|
+
},
|
|
105
|
+
method="POST",
|
|
106
|
+
)
|
|
107
|
+
with urllib.request.urlopen(request, timeout=30) as response:
|
|
108
|
+
payload = json.loads(response.read().decode("utf-8"))
|
|
109
|
+
if not payload.get("ok"):
|
|
110
|
+
raise RuntimeError(f"Slack token exchange failed: {payload}")
|
|
111
|
+
return payload
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def main() -> int:
|
|
115
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
116
|
+
parser.add_argument("--client-id", default=os.environ.get("SLACK_CLIENT_ID"))
|
|
117
|
+
parser.add_argument("--client-secret", default=os.environ.get("SLACK_CLIENT_SECRET"))
|
|
118
|
+
parser.add_argument("--redirect-uri", default="http://localhost:8765/slack/oauth/callback")
|
|
119
|
+
parser.add_argument("--scopes", default=os.environ.get("SLACK_USER_SCOPES", DEFAULT_SCOPES))
|
|
120
|
+
parser.add_argument("--output", default=".secrets/slack-user-token.json")
|
|
121
|
+
parser.add_argument("--no-open", action="store_true", help="Print the URL without opening a browser.")
|
|
122
|
+
args = parser.parse_args()
|
|
123
|
+
|
|
124
|
+
if not args.client_id or not args.client_secret:
|
|
125
|
+
parser.error("Provide --client-id/--client-secret or SLACK_CLIENT_ID/SLACK_CLIENT_SECRET.")
|
|
126
|
+
|
|
127
|
+
redirect = urllib.parse.urlparse(args.redirect_uri)
|
|
128
|
+
if redirect.hostname not in {"localhost", "127.0.0.1"}:
|
|
129
|
+
parser.error("This helper only starts a localhost callback server.")
|
|
130
|
+
|
|
131
|
+
state = secrets.token_urlsafe(32)
|
|
132
|
+
query = urllib.parse.urlencode(
|
|
133
|
+
{
|
|
134
|
+
"client_id": args.client_id,
|
|
135
|
+
"user_scope": args.scopes,
|
|
136
|
+
"redirect_uri": args.redirect_uri,
|
|
137
|
+
"state": state,
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
authorize_url = f"https://slack.com/oauth/v2/authorize?{query}"
|
|
141
|
+
|
|
142
|
+
server = OAuthServer((redirect.hostname or "localhost", redirect.port or 80), OAuthHandler)
|
|
143
|
+
server.expected_state = state
|
|
144
|
+
server.code = None
|
|
145
|
+
server.error = None
|
|
146
|
+
|
|
147
|
+
print("Open this URL to authorize Slack user-token access:")
|
|
148
|
+
print(authorize_url)
|
|
149
|
+
if not args.no_open:
|
|
150
|
+
webbrowser.open(authorize_url)
|
|
151
|
+
|
|
152
|
+
while server.code is None and server.error is None:
|
|
153
|
+
server.handle_request()
|
|
154
|
+
|
|
155
|
+
if server.error:
|
|
156
|
+
print(f"OAuth failed: {server.error}", file=sys.stderr)
|
|
157
|
+
return 1
|
|
158
|
+
|
|
159
|
+
payload = exchange_code(
|
|
160
|
+
client_id=args.client_id,
|
|
161
|
+
client_secret=args.client_secret,
|
|
162
|
+
code=server.code or "",
|
|
163
|
+
redirect_uri=args.redirect_uri,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
output = Path(args.output)
|
|
167
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
168
|
+
output.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
|
|
169
|
+
output.chmod(stat.S_IRUSR | stat.S_IWUSR)
|
|
170
|
+
|
|
171
|
+
team = payload.get("team") or {}
|
|
172
|
+
print(f"Saved Slack OAuth response to {output}")
|
|
173
|
+
print(f"Team: {team.get('name') or team.get('id') or 'unknown'}")
|
|
174
|
+
print("Use it with: python3 scripts/ingest_slack_channel.py --token-file " + str(output) + " --channel '#channel-name'")
|
|
175
|
+
return 0
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
if __name__ == "__main__":
|
|
179
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* validate-config.mjs — dependency-free validator for wiki/lisa-wiki.config.json.
|
|
4
|
+
*
|
|
5
|
+
* Enforces the constraints described in schema/lisa-wiki-config.schema.json without
|
|
6
|
+
* requiring a JSON-Schema runtime, so it is portable to any downstream repo that
|
|
7
|
+
* installs the lisa-wiki plugin (no ajv / node_modules assumptions).
|
|
8
|
+
*
|
|
9
|
+
* Usage: node validate-config.mjs [path-to-config]
|
|
10
|
+
* default path: wiki/lisa-wiki.config.json (relative to cwd)
|
|
11
|
+
* Exit code 0 = valid, 1 = invalid or unreadable.
|
|
12
|
+
*/
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
|
|
16
|
+
const MODES = ["embedded", "wrapper", "standalone", "subdir"];
|
|
17
|
+
const SIDE_EFFECTS = ["read-only-ingest", "repo-write", "external-write"];
|
|
18
|
+
const RETENTION = [
|
|
19
|
+
"raw-ok",
|
|
20
|
+
"sanitized-note-only",
|
|
21
|
+
"metadata-only",
|
|
22
|
+
"external-pointer-only",
|
|
23
|
+
];
|
|
24
|
+
const SENSITIVITY = ["public", "internal", "confidential", "restricted"];
|
|
25
|
+
const SOURCE_LAYOUT = ["by-system", "by-category"];
|
|
26
|
+
const README_MODE = ["rich", "stub", "preserve"];
|
|
27
|
+
|
|
28
|
+
const configPath = path.resolve(
|
|
29
|
+
process.argv[2] ?? "wiki/lisa-wiki.config.json"
|
|
30
|
+
);
|
|
31
|
+
const errors = [];
|
|
32
|
+
const err = msg => errors.push(msg);
|
|
33
|
+
|
|
34
|
+
function isObject(v) {
|
|
35
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
36
|
+
}
|
|
37
|
+
function isStringArray(v) {
|
|
38
|
+
return Array.isArray(v) && v.every(x => typeof x === "string");
|
|
39
|
+
}
|
|
40
|
+
function checkEnum(value, allowed, label) {
|
|
41
|
+
if (value !== undefined && !allowed.includes(value)) {
|
|
42
|
+
err(`${label}: "${String(value)}" is not one of ${allowed.join(" | ")}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function checkType(value, type, label) {
|
|
46
|
+
if (value !== undefined && typeof value !== type) {
|
|
47
|
+
err(
|
|
48
|
+
`${label}: expected ${type}, got ${Array.isArray(value) ? "array" : typeof value}`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!fs.existsSync(configPath)) {
|
|
54
|
+
console.error(`✗ config not found: ${configPath}`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let config;
|
|
59
|
+
try {
|
|
60
|
+
config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
61
|
+
} catch (e) {
|
|
62
|
+
console.error(`✗ config is not valid JSON: ${e.message}`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!isObject(config)) {
|
|
67
|
+
console.error("✗ config must be a JSON object");
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Required
|
|
72
|
+
for (const key of ["schemaVersion", "org", "mode", "wikiRoot", "categories"]) {
|
|
73
|
+
if (config[key] === undefined) err(`missing required field: ${key}`);
|
|
74
|
+
}
|
|
75
|
+
checkType(config.schemaVersion, "string", "schemaVersion");
|
|
76
|
+
checkType(config.org, "string", "org");
|
|
77
|
+
checkType(config.displayName, "string", "displayName");
|
|
78
|
+
checkType(config.purpose, "string", "purpose");
|
|
79
|
+
checkEnum(config.mode, MODES, "mode");
|
|
80
|
+
checkType(config.wikiRoot, "string", "wikiRoot");
|
|
81
|
+
if (
|
|
82
|
+
typeof config.wikiRoot === "string" &&
|
|
83
|
+
(path.isAbsolute(config.wikiRoot) ||
|
|
84
|
+
config.wikiRoot.split(/[\\/]/).includes(".."))
|
|
85
|
+
) {
|
|
86
|
+
err(
|
|
87
|
+
'wikiRoot: must be a relative path inside the repo (no absolute paths, no ".." traversal)'
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
checkType(config.frontmatter, "boolean", "frontmatter");
|
|
91
|
+
if (
|
|
92
|
+
config.categories !== undefined &&
|
|
93
|
+
!(isStringArray(config.categories) && config.categories.length > 0)
|
|
94
|
+
) {
|
|
95
|
+
err("categories: must be a non-empty array of strings");
|
|
96
|
+
}
|
|
97
|
+
checkEnum(config.sourceRetention, RETENTION, "sourceRetention");
|
|
98
|
+
checkType(config.contaminationTerms, "object", "contaminationTerms");
|
|
99
|
+
if (
|
|
100
|
+
config.contaminationTerms !== undefined &&
|
|
101
|
+
!isStringArray(config.contaminationTerms)
|
|
102
|
+
) {
|
|
103
|
+
err("contaminationTerms: must be an array of strings");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (config.sources !== undefined) {
|
|
107
|
+
if (!isObject(config.sources)) err("sources: must be an object");
|
|
108
|
+
else checkEnum(config.sources.layout, SOURCE_LAYOUT, "sources.layout");
|
|
109
|
+
}
|
|
110
|
+
if (config.git !== undefined) {
|
|
111
|
+
if (!isObject(config.git)) err("git: must be an object");
|
|
112
|
+
else {
|
|
113
|
+
checkType(config.git.prPerIngestion, "boolean", "git.prPerIngestion");
|
|
114
|
+
checkType(config.git.autoMerge, "boolean", "git.autoMerge");
|
|
115
|
+
checkType(config.git.targetBranch, "string", "git.targetBranch");
|
|
116
|
+
checkType(config.git.branchPrefix, "string", "git.branchPrefix");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (config.readme !== undefined) {
|
|
120
|
+
if (!isObject(config.readme)) err("readme: must be an object");
|
|
121
|
+
else checkEnum(config.readme.mode, README_MODE, "readme.mode");
|
|
122
|
+
}
|
|
123
|
+
if (config.sensitivity !== undefined) {
|
|
124
|
+
if (!isObject(config.sensitivity)) err("sensitivity: must be an object");
|
|
125
|
+
else {
|
|
126
|
+
checkType(config.sensitivity.enabled, "boolean", "sensitivity.enabled");
|
|
127
|
+
checkEnum(config.sensitivity.default, SENSITIVITY, "sensitivity.default");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (config.documentation !== undefined) {
|
|
131
|
+
if (!isObject(config.documentation)) err("documentation: must be an object");
|
|
132
|
+
else {
|
|
133
|
+
checkType(config.documentation.absorb, "boolean", "documentation.absorb");
|
|
134
|
+
if (
|
|
135
|
+
config.documentation.keepInPlace !== undefined &&
|
|
136
|
+
!isStringArray(config.documentation.keepInPlace)
|
|
137
|
+
) {
|
|
138
|
+
err("documentation.keepInPlace: must be an array of strings");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (config.onboarding !== undefined) {
|
|
143
|
+
if (!isObject(config.onboarding)) err("onboarding: must be an object");
|
|
144
|
+
else
|
|
145
|
+
checkType(
|
|
146
|
+
config.onboarding.allowAudienceNote,
|
|
147
|
+
"boolean",
|
|
148
|
+
"onboarding.allowAudienceNote"
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (config.connectors !== undefined) {
|
|
153
|
+
if (!isObject(config.connectors))
|
|
154
|
+
err("connectors: must be an object (name -> connector config)");
|
|
155
|
+
else {
|
|
156
|
+
for (const [name, c] of Object.entries(config.connectors)) {
|
|
157
|
+
if (!isObject(c)) {
|
|
158
|
+
err(`connectors.${name}: must be an object`);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
checkType(c.enabled, "boolean", `connectors.${name}.enabled`);
|
|
162
|
+
if (c.sideEffects === undefined) {
|
|
163
|
+
err(
|
|
164
|
+
`connectors.${name}: missing required field "sideEffects" (every connector must declare its side-effect class so full ingest can skip external-write)`
|
|
165
|
+
);
|
|
166
|
+
} else {
|
|
167
|
+
checkEnum(
|
|
168
|
+
c.sideEffects,
|
|
169
|
+
SIDE_EFFECTS,
|
|
170
|
+
`connectors.${name}.sideEffects`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (config.customConnectors !== undefined) {
|
|
178
|
+
if (!Array.isArray(config.customConnectors))
|
|
179
|
+
err("customConnectors: must be an array");
|
|
180
|
+
else {
|
|
181
|
+
config.customConnectors.forEach((c, i) => {
|
|
182
|
+
if (!isObject(c)) {
|
|
183
|
+
err(`customConnectors[${i}]: must be an object`);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
for (const k of ["name", "skill", "sourceSystem", "sideEffects"]) {
|
|
187
|
+
if (c[k] === undefined)
|
|
188
|
+
err(`customConnectors[${i}]: missing required field "${k}"`);
|
|
189
|
+
}
|
|
190
|
+
checkEnum(
|
|
191
|
+
c.sideEffects,
|
|
192
|
+
SIDE_EFFECTS,
|
|
193
|
+
`customConnectors[${i}].sideEffects`
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (config.staff !== undefined) {
|
|
200
|
+
if (!Array.isArray(config.staff)) err("staff: must be an array");
|
|
201
|
+
else {
|
|
202
|
+
config.staff.forEach((s, i) => {
|
|
203
|
+
if (!isObject(s)) {
|
|
204
|
+
err(`staff[${i}]: must be an object`);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
for (const k of ["id", "role"]) {
|
|
208
|
+
if (s[k] === undefined)
|
|
209
|
+
err(`staff[${i}]: missing required field "${k}"`);
|
|
210
|
+
}
|
|
211
|
+
checkEnum(s.sensitivity, SENSITIVITY, `staff[${i}].sensitivity`);
|
|
212
|
+
if (s.owns !== undefined) {
|
|
213
|
+
if (!isObject(s.owns)) err(`staff[${i}].owns: must be an object`);
|
|
214
|
+
else {
|
|
215
|
+
for (const ok of ["categories", "connectors", "skills"]) {
|
|
216
|
+
if (s.owns[ok] !== undefined && !isStringArray(s.owns[ok])) {
|
|
217
|
+
err(`staff[${i}].owns.${ok}: must be an array of strings`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (errors.length > 0) {
|
|
227
|
+
console.error(`✗ ${path.relative(process.cwd(), configPath)} is invalid:`);
|
|
228
|
+
for (const e of errors) console.error(` - ${e}`);
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
console.log(`✓ ${path.relative(process.cwd(), configPath)} is valid.`);
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* verify-migration.mjs — the deterministic half of /doctor. Dependency-free.
|
|
4
|
+
*
|
|
5
|
+
* Composes validate-config + lint-wiki into a grouped report and writes
|
|
6
|
+
* <wikiRoot>/state/migration/doctor-report.json with an overall verdict. Groups D
|
|
7
|
+
* (runtime surfaces) and E (functional smoke) are SKIPPED here — the lisa-wiki-doctor
|
|
8
|
+
* SKILL performs those and merges its results.
|
|
9
|
+
*
|
|
10
|
+
* Usage: node verify-migration.mjs [--wiki <root>] [--config <path>] [--migration]
|
|
11
|
+
* Exit 0 = READY or READY_WITH_WARNINGS, 1 = NOT_READY.
|
|
12
|
+
*/
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { spawnSync } from "node:child_process";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { loadConfig } from "./_wiki-lib.mjs";
|
|
18
|
+
|
|
19
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const argv = process.argv.slice(2);
|
|
21
|
+
const opt = n => {
|
|
22
|
+
const i = argv.indexOf(n);
|
|
23
|
+
return i !== -1 ? argv[i + 1] : undefined;
|
|
24
|
+
};
|
|
25
|
+
const migration = argv.includes("--migration");
|
|
26
|
+
const configPath = opt("--config");
|
|
27
|
+
const { config } = loadConfig(configPath);
|
|
28
|
+
const wikiRoot = path.resolve(opt("--wiki") ?? config?.wikiRoot ?? "wiki");
|
|
29
|
+
|
|
30
|
+
const groups = { A: [], B: [], C: [], D: [], E: [], F: [], G: [] };
|
|
31
|
+
const add = (g, id, status, message) => groups[g].push({ id, status, message });
|
|
32
|
+
|
|
33
|
+
function runNode(script, args) {
|
|
34
|
+
const res = spawnSync("node", [path.join(scriptDir, script), ...args], {
|
|
35
|
+
encoding: "utf8",
|
|
36
|
+
});
|
|
37
|
+
return {
|
|
38
|
+
status: res.status ?? 1,
|
|
39
|
+
stdout: res.stdout ?? "",
|
|
40
|
+
stderr: (res.stderr ?? "") + (res.error ? `\n${res.error.message}` : ""),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// --- A. structure & config ------------------------------------------------
|
|
45
|
+
const vc = runNode("validate-config.mjs", [
|
|
46
|
+
configPath ?? path.join(wikiRoot, "lisa-wiki.config.json"),
|
|
47
|
+
]);
|
|
48
|
+
add(
|
|
49
|
+
"A",
|
|
50
|
+
"config-valid",
|
|
51
|
+
vc.status === 0 ? "PASS" : "FAIL",
|
|
52
|
+
vc.status === 0
|
|
53
|
+
? "config validates"
|
|
54
|
+
: `config invalid: ${(vc.stderr || vc.stdout).trim().split("\n").slice(0, 6).join("; ")}`
|
|
55
|
+
);
|
|
56
|
+
add(
|
|
57
|
+
"A",
|
|
58
|
+
"schema-version",
|
|
59
|
+
config?.schemaVersion ? "PASS" : "FAIL",
|
|
60
|
+
config?.schemaVersion
|
|
61
|
+
? `schemaVersion ${config.schemaVersion}`
|
|
62
|
+
: "schemaVersion missing"
|
|
63
|
+
);
|
|
64
|
+
add(
|
|
65
|
+
"A",
|
|
66
|
+
"readme-mode",
|
|
67
|
+
config?.readme?.mode ? "PASS" : "WARN",
|
|
68
|
+
config?.readme?.mode
|
|
69
|
+
? `readme.mode ${config.readme.mode}`
|
|
70
|
+
: "readme.mode not recorded (asked by /setup)"
|
|
71
|
+
);
|
|
72
|
+
add(
|
|
73
|
+
"A",
|
|
74
|
+
"purpose",
|
|
75
|
+
config?.purpose ? "PASS" : "WARN",
|
|
76
|
+
config?.purpose
|
|
77
|
+
? "purpose set"
|
|
78
|
+
: "purpose not set (asked by /setup; feeds onboarding + contract)"
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// --- A/B. lint (structure -> A, everything else -> B) ---------------------
|
|
82
|
+
const lint = runNode("lint-wiki.mjs", [
|
|
83
|
+
"--wiki",
|
|
84
|
+
wikiRoot,
|
|
85
|
+
...(configPath ? ["--config", configPath] : []),
|
|
86
|
+
"--json",
|
|
87
|
+
]);
|
|
88
|
+
let lintReport;
|
|
89
|
+
try {
|
|
90
|
+
lintReport = JSON.parse(lint.stdout);
|
|
91
|
+
} catch {
|
|
92
|
+
add(
|
|
93
|
+
"B",
|
|
94
|
+
"lint-run",
|
|
95
|
+
"FAIL",
|
|
96
|
+
`lint-wiki did not produce JSON: ${(lint.stderr || lint.stdout).trim().slice(0, 200)}`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
if (lintReport) {
|
|
100
|
+
const structureItems = lintReport.items.filter(i => i.group === "structure");
|
|
101
|
+
const otherItems = lintReport.items.filter(i => i.group !== "structure");
|
|
102
|
+
if (structureItems.length === 0)
|
|
103
|
+
add("A", "structure", "PASS", "structure conforms to the manifest");
|
|
104
|
+
for (const i of structureItems)
|
|
105
|
+
add(
|
|
106
|
+
"A",
|
|
107
|
+
`structure:${i.group}`,
|
|
108
|
+
i.status,
|
|
109
|
+
`${i.message}${i.file ? ` (${i.file})` : ""}`
|
|
110
|
+
);
|
|
111
|
+
if (otherItems.length === 0)
|
|
112
|
+
add("B", "integrity", "PASS", "no integrity/safety findings");
|
|
113
|
+
for (const i of otherItems)
|
|
114
|
+
add(
|
|
115
|
+
"B",
|
|
116
|
+
`${i.group}`,
|
|
117
|
+
i.status,
|
|
118
|
+
`${i.message}${i.file ? ` (${i.file})` : ""}`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// --- C/D/E/F/G: deterministic-light + delegated to the doctor skill --------
|
|
123
|
+
add(
|
|
124
|
+
"C",
|
|
125
|
+
"no-loss",
|
|
126
|
+
"SKIP",
|
|
127
|
+
"no-loss/parity needs the migration manifest from /migrate; dangling-link check is covered in B"
|
|
128
|
+
);
|
|
129
|
+
add(
|
|
130
|
+
"D",
|
|
131
|
+
"runtime",
|
|
132
|
+
"SKIP",
|
|
133
|
+
"runtime surfaces (commands/skills/subagents/MCP) verified by the lisa-wiki-doctor skill"
|
|
134
|
+
);
|
|
135
|
+
add(
|
|
136
|
+
"E",
|
|
137
|
+
"smoke",
|
|
138
|
+
"SKIP",
|
|
139
|
+
"functional smoke tests (ingest/query/lint/onboard) performed by the lisa-wiki-doctor skill"
|
|
140
|
+
);
|
|
141
|
+
add(
|
|
142
|
+
"F",
|
|
143
|
+
"mode",
|
|
144
|
+
config?.mode ? "PASS" : "FAIL",
|
|
145
|
+
config?.mode ? `mode: ${config.mode}; wikiRoot resolves` : "mode not set"
|
|
146
|
+
);
|
|
147
|
+
add(
|
|
148
|
+
"G",
|
|
149
|
+
"git-ci-dist",
|
|
150
|
+
"SKIP",
|
|
151
|
+
"git/CI/distribution checks performed by the lisa-wiki-doctor skill and CI"
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
// --- verdict --------------------------------------------------------------
|
|
155
|
+
const all = Object.values(groups).flat();
|
|
156
|
+
const isBlockingWarn = (item, group) =>
|
|
157
|
+
migration && (group === "A" || group === "B") && item.status === "WARN";
|
|
158
|
+
let verdict = "READY";
|
|
159
|
+
if (all.some(i => i.status === "FAIL")) verdict = "NOT_READY";
|
|
160
|
+
else if (
|
|
161
|
+
Object.entries(groups).some(([g, items]) =>
|
|
162
|
+
items.some(i => isBlockingWarn(i, g))
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
verdict = "NOT_READY";
|
|
166
|
+
else if (all.some(i => i.status === "WARN")) verdict = "READY_WITH_WARNINGS";
|
|
167
|
+
|
|
168
|
+
const reportObj = {
|
|
169
|
+
tool: "verify-migration",
|
|
170
|
+
deterministic: true,
|
|
171
|
+
wikiRoot: path.relative(process.cwd(), wikiRoot) || ".",
|
|
172
|
+
mode: config?.mode ?? null,
|
|
173
|
+
migration,
|
|
174
|
+
generatedAt: new Date().toISOString(),
|
|
175
|
+
verdict,
|
|
176
|
+
subset: "deterministic",
|
|
177
|
+
note: "Deterministic subset only. Groups C (no-loss/parity), D (runtime surfaces), E (functional smoke), and G (git/CI/distribution) are completed by the lisa-wiki-doctor skill, which merges its results into this report.",
|
|
178
|
+
groups,
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const outPath = path.join(wikiRoot, "state", "migration", "doctor-report.json");
|
|
182
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
183
|
+
fs.writeFileSync(outPath, `${JSON.stringify(reportObj, null, 2)}\n`);
|
|
184
|
+
|
|
185
|
+
const counts = all.reduce(
|
|
186
|
+
(acc, i) => ((acc[i.status] = (acc[i.status] ?? 0) + 1), acc),
|
|
187
|
+
{}
|
|
188
|
+
);
|
|
189
|
+
console.log(
|
|
190
|
+
`verdict: ${verdict} (${["PASS", "WARN", "FAIL", "SKIP"].map(s => `${counts[s] ?? 0} ${s}`).join(", ")})`
|
|
191
|
+
);
|
|
192
|
+
console.log(`report → ${path.relative(process.cwd(), outPath)}`);
|
|
193
|
+
console.log(
|
|
194
|
+
" (deterministic subset — C/D/E/G completed by the /doctor skill)"
|
|
195
|
+
);
|
|
196
|
+
for (const i of all.filter(i => i.status === "FAIL" || i.status === "WARN")) {
|
|
197
|
+
console.log(` ${i.status === "FAIL" ? "✗" : "⚠"} [${i.id}] ${i.message}`);
|
|
198
|
+
}
|
|
199
|
+
process.exit(verdict === "NOT_READY" ? 1 : 0);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lisa-wiki-add-ingest
|
|
3
|
+
description: Scaffold a project-specific "front-door" ingest skill that does something unique (classify a source, fetch from a special system, stamp domain frontmatter) and then chains into /ingest. Use when a project needs a bespoke ingestion path that the core connectors do not cover — instead of forking the kernel. The generated skill enriches and delegates; the kernel still owns synthesis, index, log, verify, state, and PR.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# lisa-wiki-add-ingest
|
|
7
|
+
|
|
8
|
+
Generate a thin, project-local front-door ingest skill so a project can extend ingestion **without
|
|
9
|
+
forking** the kernel. The front-door does only the unique part, then hands enriched parameters to
|
|
10
|
+
`/ingest`.
|
|
11
|
+
|
|
12
|
+
## Workflow
|
|
13
|
+
1. **Interview** the project: a short name; what the source is; which `wiki/sources/<sourceSystem>/`
|
|
14
|
+
bucket and page type/frontmatter it should produce; whether it merely *enriches/classifies* an
|
|
15
|
+
input or also *fetches* from an external system; and its **side-effect class**
|
|
16
|
+
(`read-only-ingest` | `repo-write` | `external-write`).
|
|
17
|
+
2. **Generate** the front-door skill on both runtimes —
|
|
18
|
+
`.claude/skills/lisa-wiki-local-<name>/SKILL.md` and `.agents/skills/lisa-wiki-local-<name>/SKILL.md`.
|
|
19
|
+
Its body does the unique step, then **delegates to the `lisa-wiki-ingest` skill** (Claude facade
|
|
20
|
+
`/ingest`) passing the bucket/type/metadata. If it fetches, it writes only a sanitized source note
|
|
21
|
+
(+ run metadata) and lets the kernel do synthesis/index/log/verify/state/PR.
|
|
22
|
+
3. **Register** it in `wiki/lisa-wiki.config.json` under `customConnectors`
|
|
23
|
+
(`{ name, skill, sourceSystem, stateFile, sideEffects }`). `/ingest` dispatches **only** to
|
|
24
|
+
registered names — no auto-discovery.
|
|
25
|
+
|
|
26
|
+
## Rules (the front-door contract)
|
|
27
|
+
- A generated front-door writes **only** its source note + run metadata. It must not write synthesis
|
|
28
|
+
pages, `index.md`, `log.md`, or final state — the kernel does those, in order, after it returns.
|
|
29
|
+
- `external-write` front-doors require config opt-in **and** explicit per-run intent, and their PRs
|
|
30
|
+
never auto-merge.
|
|
31
|
+
- Side effects outside the declared class are a hard failure (enforced by the touched-file guard).
|
|
32
|
+
|
|
33
|
+
## Related
|
|
34
|
+
`lisa-wiki-ingest` (what it chains into), `lisa-wiki-add-role`, `lisa-wiki-doctor`.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lisa-wiki-add-role
|
|
3
|
+
description: Scaffold a domain-expert "digital staff" role over the wiki — a dual-runtime subagent (Claude + Codex) plus a staff doc page — from a config.staff[] entry. Use when a project wants a role-scoped expert (e.g. Legal, Finance, Sales) whose knowledge is a slice of the wiki. The plugin only SETS UP the subagent; whether it is ever invoked, scheduled, or routed is out of scope.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# lisa-wiki-add-role
|
|
7
|
+
|
|
8
|
+
Turn a role definition into two generated artifacts: a documentation page in the wiki and a runnable,
|
|
9
|
+
brain-pointed subagent on both runtimes. **Running the subagent (invocation, scheduling, Telegram /
|
|
10
|
+
agent-team routing, private notebooks) is out of scope** — this skill only creates it.
|
|
11
|
+
|
|
12
|
+
## Workflow
|
|
13
|
+
1. **Resolve the role** from a `config.staff[]` entry (or interview to add one): `id`, `role`,
|
|
14
|
+
`expertise`, `owns` (categories / connectors / skills), `sensitivity`.
|
|
15
|
+
2. **Doc page:** generate `wiki/staff/<id>.md` describing the role, its owned domain, and who it
|
|
16
|
+
reports to. This page is wiki content and is itself ingestible (the `roles` connector).
|
|
17
|
+
3. **Subagents (dual-runtime), rendered from the role-agent templates:**
|
|
18
|
+
- Claude: `.claude/agents/<id>.md`.
|
|
19
|
+
- Codex: `.codex/agents/<id>.toml` (keys `name`, `description`, `developer_instructions`; optional
|
|
20
|
+
`model`, `model_reasoning_effort`, `sandbox_mode`).
|
|
21
|
+
4. **Brain-pointed, not baked:** the subagent's instructions say *"your domain is `wiki/<owned>/`;
|
|
22
|
+
`/query` it first, contribute via `/ingest`; stay in your lane."* It points at the live wiki so it
|
|
23
|
+
never goes stale. Only its one-line `description` is synthesized from the wiki at generation time.
|
|
24
|
+
|
|
25
|
+
## Rules
|
|
26
|
+
- v1 instructs lane-keeping but does not *enforce* per-role write isolation (deferred).
|
|
27
|
+
- Setup seeds the starter roster by delegating here per `config.staff[]` entry.
|
|
28
|
+
|
|
29
|
+
## Related
|
|
30
|
+
`lisa-wiki-setup` (seeds the roster), `lisa-wiki-add-ingest`, `lisa-wiki-onboard-me`.
|