Refactor Mattermost and Slack integration workflows to remove legacy Fidelity variables, streamline command execution, and enhance documentation for project profiles. Update scripts and README files to reflect changes in directory structure and configuration precedence, ensuring a consistent approach to project knowledge management across profiles. Improve error handling and validation in profile creation and doctor commands, and enhance test coverage for profile-related functionalities.

This commit is contained in:
2026-05-21 14:13:21 -06:00
parent 1ad707373a
commit ad230e1abe
28 changed files with 263 additions and 120 deletions

View File

@@ -2,13 +2,14 @@
"""Profile path resolution for AI Workspace scripts.
Profiles own their configuration. Reusable scripts should call this module
instead of hardcoding root-level project paths.
instead of hardcoding project paths.
"""
from __future__ import annotations
import json
import argparse
import re
from pathlib import Path
from typing import Any
@@ -22,6 +23,23 @@ DEFAULT_WORKSPACE = {
"index_dir": ".aiw/indexes/{profile}",
}
PROFILE_RE = re.compile(r"^[a-z0-9][a-z0-9_-]*$")
KNOWLEDGE_DIRS = [
"00-start",
"01-current",
"02-work-items",
"03-context/process",
"03-context/systems",
"03-context/workstreams",
"04-people",
"05-decisions",
"06-daily",
"07-maps",
"08-bases",
"09-templates",
"attachments",
]
def workspace_config_path(profile: str, root: Path | None = None) -> Path:
base = root or ROOT
@@ -73,6 +91,76 @@ def relative_to_root(path: Path, root: Path | None = None) -> str:
return str(path)
def validate_profile_name(profile: str) -> None:
if not PROFILE_RE.match(profile):
raise SystemExit("profile must be lowercase letters, numbers, dashes, or underscores")
def write_file_once(path: Path, text: str) -> None:
if path.exists():
return
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text, encoding="utf-8")
def create_profile(profile: str, display_name: str | None = None, root: Path | None = None) -> dict[str, Any]:
validate_profile_name(profile)
base = root or ROOT
display = display_name or profile.replace("-", " ").replace("_", " ").title()
profile_dir = base / "profiles" / profile
knowledge = knowledge_dir(profile, root=base)
inbox = inbox_dir(profile, root=base)
index = index_dir(profile, root=base)
if profile_dir.exists() or knowledge.exists() or inbox.exists():
raise SystemExit(f"profile already exists or has data directories: {profile}")
profile_dir.mkdir(parents=True)
write_file_once(profile_dir / "profile.md", f"# {display} Profile\n\n## Purpose\n\nDescribe how this project uses AI Workspace.\n\n## Project\n\n- Name: {display}\n- Workspace role: companion AI workspace\n\n## Communication Sources\n\n- Live communication: configure if needed\n- Historical archive: optional\n\n## Work System\n\n- Use the profile project knowledge vault for work items and current state.\n")
write_file_once(profile_dir / "workspace.json", json.dumps({
"profile": profile,
"display_name": display,
"description": f"AI Workspace profile for {display}.",
"knowledge_dir": relative_to_root(knowledge, root=base),
"inbox_dir": relative_to_root(inbox, root=base),
"index_dir": relative_to_root(index, root=base),
}, ensure_ascii=False, indent=2, sort_keys=True) + "\n")
write_file_once(profile_dir / "services.json", json.dumps({"profile": profile, "description": f"Local services for {display}.", "services": {}}, ensure_ascii=False, indent=2, sort_keys=True) + "\n")
write_file_once(profile_dir / "context-sources.json", json.dumps({"communication_sources": {}}, ensure_ascii=False, indent=2, sort_keys=True) + "\n")
for folder in KNOWLEDGE_DIRS:
(knowledge / folder).mkdir(parents=True, exist_ok=True)
inbox.mkdir(parents=True, exist_ok=True)
write_file_once(knowledge / "00-start" / "start-here.md", f"# {display} Start Here\n\nUse this note as the entry point for the {display} project knowledge vault.\n")
write_file_once(knowledge / "01-current" / "current-work.md", "# Current Work\n\n## Focus\n\n- TBD\n")
write_file_once(knowledge / "01-current" / "work-items.md", "# Work Items\n\nNo active work items recorded yet.\n")
write_file_once(knowledge / "03-context" / "project.md", f"# {display} Project Context\n\nDurable project context goes here.\n")
write_file_once(knowledge / "04-people" / "index.md", "# People\n\nProject people and role mappings go here.\n")
write_file_once(knowledge / "07-maps" / "index.md", "# Maps\n\nNavigation maps go here.\n")
write_file_once(knowledge / "09-templates" / "work-item.md", "---\ntype: work-item\ntitle: {{title}}\nupdated: {{date}}\n---\n\n# {{title}}\n")
write_file_once(knowledge / "09-templates" / "daily.md", "---\ntype: daily\ndate: {{date}}\nupdated: {{date}}\n---\n\n# {{date}}\n")
write_file_once(inbox / "README.md", f"# {display} Inbox\n\nRaw evidence for this profile. Promote durable facts into the project knowledge vault.\n")
return {"profile": profile, "profile_dir": relative_to_root(profile_dir, root=base), "knowledge_dir": relative_to_root(knowledge, root=base), "inbox_dir": relative_to_root(inbox, root=base)}
def doctor(profile: str, root: Path | None = None) -> dict[str, Any]:
base = root or ROOT
config_path = workspace_config_path(profile, root=base)
knowledge = knowledge_dir(profile, root=base)
inbox = inbox_dir(profile, root=base)
checks = {
"profile_config_exists": config_path.is_file(),
"knowledge_dir_exists": knowledge.is_dir(),
"inbox_dir_exists": inbox.is_dir(),
"start_here_exists": (knowledge / "00-start" / "start-here.md").is_file(),
"current_work_exists": (knowledge / "01-current" / "current-work.md").is_file(),
"work_items_exists": (knowledge / "01-current" / "work-items.md").is_file(),
"root_project_knowledge_absent": not (base / "project-knowledge").exists(),
"root_ai_inbox_absent": not (base / "ai" / "inbox").exists(),
}
return {"profile": profile, "ok": all(checks.values()), "checks": checks, "paths": {"knowledge_dir": relative_to_root(knowledge, root=base), "inbox_dir": relative_to_root(inbox, root=base), "index_dir": relative_to_root(index_dir(profile, root=base), root=base)}}
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
subparsers = parser.add_subparsers(dest="command", required=True)
@@ -84,6 +172,13 @@ def main() -> None:
config_parser = subparsers.add_parser("config", help="Print resolved workspace configuration as JSON.")
config_parser.add_argument("--profile", default="fidelity")
create_parser = subparsers.add_parser("create", help="Create a new isolated project profile.")
create_parser.add_argument("profile")
create_parser.add_argument("--display-name", default="")
doctor_parser = subparsers.add_parser("doctor", help="Validate profile workspace layout.")
doctor_parser.add_argument("--profile", default="fidelity")
args = parser.parse_args()
if args.command == "path":
if args.kind == "knowledge":
@@ -94,6 +189,15 @@ def main() -> None:
print(index_dir(args.profile))
return
if args.command == "create":
print(json.dumps(create_profile(args.profile, args.display_name or None), ensure_ascii=False, indent=2, sort_keys=True))
return
if args.command == "doctor":
payload = doctor(args.profile)
print(json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True))
raise SystemExit(0 if payload["ok"] else 1)
config = load_workspace_config(args.profile)
config["resolved"] = {
"knowledge_dir": str(knowledge_dir(args.profile)),