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:
@@ -62,10 +62,9 @@ bash scripts/mattermost/bootstrap.sh
|
||||
The current Mattermost extractor is stdlib-only and does not require installing `requests`.
|
||||
It also supports readable channel names in `.env`, not only channel IDs.
|
||||
|
||||
If you still want to override it with another script, expose that to OpenCode with:
|
||||
If you want to override it with another script, expose that to OpenCode with:
|
||||
|
||||
- `AIW_MATTERMOST_SYNC_CMD`
|
||||
- `FIDELITY_MATTERMOST_SYNC_CMD`
|
||||
|
||||
Example:
|
||||
|
||||
@@ -79,7 +78,7 @@ Expected behavior:
|
||||
- avoid interactive prompts
|
||||
- return a non-zero exit code on failure
|
||||
|
||||
OpenCode can then use that output to refresh the active profile inbox proactively. When the local Mattermost proxy mirror is running, commands should prefer `<profile inbox>/mattermost-mirror/` through `scripts/mattermost-proxy/read-context.py --profile <profile>` and use legacy sync output only as fallback evidence.
|
||||
OpenCode can then use that output to refresh the active profile inbox proactively. When the local Mattermost proxy mirror is running, commands should prefer `<profile inbox>/mattermost-mirror/` through `scripts/mattermost-proxy/read-context.py --profile <profile>`.
|
||||
|
||||
Historical Slack exports can also be imported through:
|
||||
|
||||
|
||||
@@ -62,7 +62,14 @@ Current fields:
|
||||
}
|
||||
```
|
||||
|
||||
Use `scripts/aiw/profile.py` from new scripts instead of hardcoding root-level project memory or inbox paths.
|
||||
Use `scripts/aiw/profile.py` from new scripts instead of hardcoding project memory or inbox paths.
|
||||
|
||||
Create and validate an isolated profile:
|
||||
|
||||
```bash
|
||||
python3 scripts/aiw/profile.py create my-project --display-name "My Project"
|
||||
python3 scripts/aiw/profile.py doctor --profile my-project
|
||||
```
|
||||
|
||||
## Robustness features
|
||||
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -48,6 +48,35 @@ class ProfileTests(unittest.TestCase):
|
||||
self.assertEqual(profile.relative_to_root(root / "a" / "b", root=root), "a/b")
|
||||
self.assertEqual(profile.relative_to_root(Path("/external/path"), root=root), "/external/path")
|
||||
|
||||
def test_create_profile_writes_isolated_layout(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
result = profile.create_profile("demo-project", "Demo Project", root=root)
|
||||
|
||||
self.assertEqual(result["knowledge_dir"], "workspaces/demo-project/project-knowledge")
|
||||
self.assertTrue((root / "profiles" / "demo-project" / "workspace.json").is_file())
|
||||
self.assertTrue((root / "workspaces" / "demo-project" / "project-knowledge" / "00-start" / "start-here.md").is_file())
|
||||
self.assertTrue((root / "workspaces" / "demo-project" / "inbox" / "README.md").is_file())
|
||||
|
||||
def test_doctor_reports_clean_layout(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
profile.create_profile("demo", root=root)
|
||||
result = profile.doctor("demo", root=root)
|
||||
|
||||
self.assertTrue(result["ok"])
|
||||
self.assertTrue(result["checks"]["root_project_knowledge_absent"])
|
||||
|
||||
def test_doctor_rejects_root_level_project_data_dirs(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
profile.create_profile("demo", root=root)
|
||||
(root / "project-knowledge").mkdir()
|
||||
result = profile.doctor("demo", root=root)
|
||||
|
||||
self.assertFalse(result["ok"])
|
||||
self.assertFalse(result["checks"]["root_project_knowledge_absent"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -149,7 +149,7 @@ def mode_latest() -> None:
|
||||
records = read_jsonl(MIRROR_DIR / "latest.jsonl")
|
||||
if print_jsonl(records):
|
||||
return
|
||||
print("No proxy mirror latest context available; falling back to legacy sync artifacts.")
|
||||
print("No proxy mirror latest context available; checking configured sync artifacts.")
|
||||
fallback()
|
||||
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@ AIW_CHANNEL_PREFIX=fidelity
|
||||
# MATTERMOST_TEAM_NAME=fidelity
|
||||
# MATTERMOST_TEAM_ID=team_id_here
|
||||
# Legacy Fidelity-specific options still supported by workspace plugins:
|
||||
# FIDELITY_MATTERMOST_SYNC_CMD=bash scripts/mattermost/sync.sh
|
||||
# FIDELITY_MATTERMOST_SYNC_INTERVAL_MINUTES=15
|
||||
# FIDELITY_MANAGER_NAME=jeff
|
||||
# AIW_MATTERMOST_SYNC_CMD=bash scripts/mattermost/sync.sh
|
||||
# AIW_MATTERMOST_SYNC_INTERVAL_MINUTES=15
|
||||
# AIW_MANAGER_NAME=jeff
|
||||
# Legacy options still supported:
|
||||
# CHANNEL_NAMES=fidelity-preguntas,otro-canal
|
||||
# CHANNEL_IDS=canal_id_1,canal_id_2
|
||||
|
||||
@@ -2,10 +2,7 @@
|
||||
|
||||
This directory contains the workspace-local Mattermost extractor used by OpenCode to refresh communication context.
|
||||
|
||||
The preferred live Mattermost evidence source is now the proxy mirror under
|
||||
the active profile inbox, `<profile inbox>/mattermost-mirror/`, when it is running. This legacy extractor remains
|
||||
the fallback and explicit refresh path for commands that need a fresh pull from
|
||||
the Mattermost API.
|
||||
The preferred live Mattermost evidence source is the proxy mirror under the active profile inbox, `<profile inbox>/mattermost-mirror/`. This extractor is an explicit API refresh path for commands that need a fresh pull from the Mattermost API.
|
||||
|
||||
## Files
|
||||
|
||||
@@ -63,7 +60,7 @@ Manual run:
|
||||
bash scripts/mattermost/sync.sh
|
||||
```
|
||||
|
||||
OpenCode can use this script directly. If `AIW_MATTERMOST_SYNC_CMD` is not set, the workspace plugins will fall back to `FIDELITY_MATTERMOST_SYNC_CMD`, then to this wrapper automatically.
|
||||
OpenCode can use this script directly. If `AIW_MATTERMOST_SYNC_CMD` is not set, the workspace plugin uses this wrapper automatically.
|
||||
|
||||
Generic workspace variables are preferred for reusable projects:
|
||||
|
||||
@@ -72,7 +69,7 @@ Generic workspace variables are preferred for reusable projects:
|
||||
- `AIW_MATTERMOST_SYNC_CMD`
|
||||
- `AIW_MATTERMOST_SYNC_INTERVAL_MINUTES`
|
||||
|
||||
Profile-specific compatibility variables may exist for older project setups, but new reusable workflows should prefer the generic `AIW_*` variables.
|
||||
Reusable workflows should prefer the generic `AIW_*` variables.
|
||||
|
||||
Previous workday mode for standups:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user