feat: Implement Mattermost sync functionality and enhance workspace context management

This commit is contained in:
2026-04-09 14:46:50 -06:00
parent e92c07b8b1
commit 0173e3d376
18 changed files with 740 additions and 7 deletions

19
.gitignore vendored Normal file
View File

@@ -0,0 +1,19 @@
.DS_Store
# Python caches
__pycache__/
*.py[cod]
# Local virtual environments
.venv/
# Mattermost raw inbox artifacts
ai/inbox/mattermost-latest.md
ai/inbox/mattermost-*.md
ai/inbox/mattermost-status.json
# Workspace-local Mattermost runtime artifacts
scripts/mattermost/.env
scripts/mattermost/.venv/
scripts/mattermost/generated/*
!scripts/mattermost/generated/.gitkeep

View File

@@ -0,0 +1,28 @@
---
description: Force a Mattermost sync using the configured local script
---
Use the configured Mattermost sync command to fetch fresh communication context.
Preferred command sources:
- `FIDELITY_MATTERMOST_SYNC_CMD`
- fallback: `bash scripts/mattermost/sync.sh`
Run the command and use its output as fresh communication context:
!`if [ -n "$FIDELITY_MATTERMOST_SYNC_CMD" ]; then bash -lc "$FIDELITY_MATTERMOST_SYNC_CMD"; elif [ -f scripts/mattermost/sync.sh ]; then bash scripts/mattermost/sync.sh; else echo "No Mattermost sync command is configured."; fi`
Then:
- if the command fails, stop there and do not edit any workspace files
- if no `ai/inbox/mattermost-latest.md` exists after the sync attempt, do not update logs or stable context
- inspect `ai/inbox/mattermost-latest.md` if it exists
- decide whether the new information belongs in today's log, current state, or stable project context
- update the workspace only if the sync succeeded and durable facts were learned
Return:
1. What was synchronized
2. Which files were updated
3. What still needs human confirmation

View File

@@ -0,0 +1,120 @@
import { access, mkdir, readFile, writeFile } from "node:fs/promises"
import path from "node:path"
async function safeRead(filePath) {
try {
return await readFile(filePath, "utf8")
} catch {
return null
}
}
function nowIso() {
return new Date().toISOString()
}
async function resolveSyncCommand(directory) {
const configured = process.env.FIDELITY_MATTERMOST_SYNC_CMD?.trim()
if (configured) return configured
const fallbackScript = path.join(directory, "scripts/mattermost/sync.sh")
try {
await access(fallbackScript)
return `bash "${fallbackScript}"`
} catch {
return ""
}
}
export const MattermostInbox = async ({ $, directory, client }) => {
let lastSyncAt = 0
async function writeStatus(statusPath, data) {
await mkdir(path.dirname(statusPath), { recursive: true })
await writeFile(statusPath, `${JSON.stringify(data, null, 2)}\n`, "utf8")
}
async function sync(reason) {
const command = await resolveSyncCommand(directory)
if (!command) return
const intervalMinutes = Number.parseInt(
process.env.FIDELITY_MATTERMOST_SYNC_INTERVAL_MINUTES || "15",
10,
)
const minIntervalMs = Math.max(1, Number.isNaN(intervalMinutes) ? 15 : intervalMinutes) * 60 * 1000
const now = Date.now()
if (now - lastSyncAt < minIntervalMs) return
lastSyncAt = now
const inboxDir = path.join(directory, "ai/inbox")
const latestPath = path.join(inboxDir, "mattermost-latest.md")
const statusPath = path.join(inboxDir, "mattermost-status.json")
try {
await mkdir(inboxDir, { recursive: true })
const output = (await $`bash -lc ${command}`.text()).trim()
const previous = await safeRead(latestPath)
if (output) {
if (previous !== `${output}\n` && previous !== output) {
const stamp = nowIso().replace(/[:]/g, "-")
const snapshotPath = path.join(inboxDir, `mattermost-${stamp}.md`)
await writeFile(latestPath, `${output}\n`, "utf8")
await writeFile(snapshotPath, `${output}\n`, "utf8")
}
await writeStatus(statusPath, {
syncedAt: nowIso(),
reason,
changed: previous !== `${output}\n` && previous !== output,
commandConfigured: true,
commandSource: process.env.FIDELITY_MATTERMOST_SYNC_CMD?.trim() ? "env" : "workspace-default",
})
} else {
await writeStatus(statusPath, {
syncedAt: nowIso(),
reason,
changed: false,
commandConfigured: true,
commandSource: process.env.FIDELITY_MATTERMOST_SYNC_CMD?.trim() ? "env" : "workspace-default",
note: "Sync command returned no output.",
})
}
} catch (error) {
await writeStatus(statusPath, {
syncedAt: nowIso(),
reason,
changed: false,
commandConfigured: true,
commandSource: process.env.FIDELITY_MATTERMOST_SYNC_CMD?.trim() ? "env" : "workspace-default",
error: error instanceof Error ? error.message : String(error),
})
await client.app.log({
body: {
service: "mattermost-inbox",
level: "warn",
message: "Mattermost sync failed",
extra: {
reason,
error: error instanceof Error ? error.message : String(error),
},
},
})
}
}
return {
event: async ({ event }) => {
if (event.type === "session.created") {
await sync("session.created")
}
if (event.type === "tui.prompt.append") {
await sync("tui.prompt.append")
}
},
}
}

View File

@@ -26,6 +26,9 @@ These are also loaded through `opencode.json`.
- Assume the workspace may contain stale context until checked.
- Before answering questions that depend on current work state, inspect `ai/state/current.md` and the latest relevant log under `ai/logs/`.
- If `ai/inbox/mattermost-latest.md` exists, inspect it for fresher communication context before answering standup, status, or manager-message prompts.
- If a sync command, extraction script, or inbox refresh fails, do not update logs, state, or context files from that failed attempt.
- Treat sync failures as operational errors, not project context.
- If the user provides durable new facts, update the appropriate context files instead of leaving the new information only in chat history.
- If a previous context file is now stale or inaccurate, update that file directly.
- Prefer correcting canonical context over appending contradictory notes.

View File

@@ -157,8 +157,24 @@ Recommended usage:
Project commands live under `.opencode/commands/` and are intended to:
- load the baseline Fidelity context
- sync Mattermost context into the workspace inbox
- draft standups
- draft Jeff updates
- convert rough notes into daily log updates
This keeps AI output tied to the latest workspace state instead of relying on chat memory alone.
---
## Mattermost Memory Flow
This workspace supports a live-memory pattern for Mattermost.
Recommended setup:
1. Use the workspace-local script at `scripts/mattermost/sync.sh`, or override it with `FIDELITY_MATTERMOST_SYNC_CMD`.
2. Let OpenCode run with the project plugins enabled.
3. The Mattermost inbox plugin will periodically refresh `ai/inbox/mattermost-latest.md`.
4. Promote durable facts from the inbox into `ai/logs/`, `ai/state/`, and `ai/context/`.
Use `/mattermost-sync` when you want to force a refresh manually.

View File

@@ -62,6 +62,9 @@ When drafting messages for Jeff:
- Treat workspace files as persistent memory, not just reference notes
- Before answering prompts about current work, verify `ai/state/current.md` and the latest relevant log in `ai/logs/`
- If `ai/inbox/mattermost-latest.md` exists, check it before answering prompts about current status, standups, or supervisor communication
- If a Mattermost sync or other context-ingestion step fails, do not update `ai/logs/`, `ai/state/`, or stable context files based on that failure
- Do not record missing dependencies, failed sync attempts, or missing inbox files as project facts unless the user explicitly asks to track the operational issue
- When the user shares relevant new information, update today's log if the information belongs to the daily record
- When the user corrects or changes stable context, update the canonical file directly:
- `ai/state/current.md` for current focus or active concerns
@@ -79,7 +82,7 @@ For day-to-day prompts in this workspace:
1. Verify the latest relevant context.
2. Decide whether the prompt introduces new durable information.
3. Update the workspace when needed.
3. Update the workspace when needed, but never from a failed sync or failed tool run.
4. Then answer using the refreshed context.
---

1
ai/inbox/.gitkeep Normal file
View File

@@ -0,0 +1 @@

11
ai/inbox/README.md Normal file
View File

@@ -0,0 +1,11 @@
# Mattermost Inbox
This directory stores raw or semi-processed communication captured from Mattermost before it is promoted into the persistent workspace context.
Recommended usage:
- `mattermost-latest.md` contains the most recent capture
- timestamped snapshots can be stored here if needed
- durable facts should be promoted into `ai/logs/`, `ai/state/`, or `ai/context/`
This directory is intentionally treated as an inbox, not as the final source of truth.

View File

@@ -2,6 +2,43 @@
"$schema": "https://opencode.ai/config.json",
"default_agent": "fidelity",
"share": "manual",
"permission": {
"*": "ask",
"read": {
"*": "allow",
"*.env": "deny",
"*.env.*": "deny",
"*.env.example": "allow"
},
"edit": {
"*": "allow",
".git/*": "deny"
},
"glob": "allow",
"grep": "allow",
"list": "allow",
"lsp": "allow",
"task": "allow",
"question": "allow",
"skill": "allow",
"todoread": "allow",
"todowrite": "allow",
"bash": {
"*": "allow",
"rm *": "deny",
"sudo *": "deny",
"git reset *": "deny",
"git clean *": "deny",
"git push *": "deny",
"git commit *": "ask",
"git checkout *": "ask"
},
"webfetch": "ask",
"websearch": "ask",
"codesearch": "ask",
"external_directory": "ask",
"doom_loop": "ask"
},
"instructions": [
"./README.md",
"./ai/context/project.md",

View File

@@ -1,10 +1,44 @@
# Scripts
This directory is reserved for future helpers that automate:
This directory contains helpers that automate:
- context aggregation
- standup generation
- manager update drafting
- Mattermost-ready message formatting
No automation has been implemented yet. The current workflow is document-driven.
The default workspace Mattermost extractor now lives in:
- `scripts/mattermost/`
Recommended default command:
```bash
bash scripts/mattermost/sync.sh
```
Bootstrap command:
```bash
bash scripts/mattermost/bootstrap.sh
```
The current Mattermost extractor is stdlib-only and does not require installing `requests`.
If you still want to override it with another script, expose that to OpenCode with:
- `FIDELITY_MATTERMOST_SYNC_CMD`
Example:
```bash
export FIDELITY_MATTERMOST_SYNC_CMD="/absolute/path/to/your-mattermost-sync-script"
```
Expected behavior:
- print the latest relevant Mattermost context to stdout
- avoid interactive prompts
- return a non-zero exit code on failure
OpenCode can then use that output to refresh `ai/inbox/mattermost-latest.md` proactively.

View File

@@ -0,0 +1,8 @@
MATTERMOST_URL=https://tu-mattermost.example.com
MATTERMOST_TOKEN=tu_personal_access_token
CHANNEL_IDS=canal_id_1,canal_id_2
MESSAGE_WINDOW_HOURS=24
MAX_MESSAGES=200
MATTERMOST_CA_BUNDLE=ca.pem
MATTERMOST_SKIP_TLS_VERIFY=false
MATTERMOST_OUTPUT_FILE=generated/mattermost_context.jsonl

5
scripts/mattermost/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
.env
.venv/
generated/
__pycache__/
*.py[cod]

View File

@@ -0,0 +1,52 @@
# Mattermost Sync
This directory contains the workspace-local Mattermost extractor used by OpenCode to refresh communication context.
## Files
- `mattermost_context.py`
Extracts recent Mattermost messages as JSONL.
- `sync.sh`
Stable wrapper intended to be called by OpenCode.
- `.env.example`
Example configuration without secrets.
- `requirements.txt`
Reserved for future optional dependencies.
## Recommended Setup
1. Copy `.env.example` to `.env`.
2. Fill in your Mattermost values.
3. Create a local virtual environment if you want an isolated runtime.
Example:
```bash
cd scripts/mattermost
bash bootstrap.sh
```
## Usage
Manual run:
```bash
bash scripts/mattermost/sync.sh
```
OpenCode can use this script directly. If `FIDELITY_MATTERMOST_SYNC_CMD` is not set, the workspace plugins will fall back to this wrapper automatically.
## Bootstrap
You can initialize the local runtime with:
```bash
bash scripts/mattermost/bootstrap.sh
```
This will:
- create `.venv/` if missing
- create `.env` from `.env.example` if needed
The current extractor uses only the Python standard library, so no third-party install is required.

View File

@@ -0,0 +1,29 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VENV_DIR="$SCRIPT_DIR/.venv"
PYTHON_BIN="${PYTHON_BIN:-python3}"
if ! command -v "$PYTHON_BIN" >/dev/null 2>&1; then
echo "Python interpreter not found: $PYTHON_BIN" >&2
exit 1
fi
if [[ ! -d "$VENV_DIR" ]]; then
"$PYTHON_BIN" -m venv "$VENV_DIR"
fi
if [[ ! -f "$SCRIPT_DIR/.env" ]]; then
cp "$SCRIPT_DIR/.env.example" "$SCRIPT_DIR/.env"
echo "Created $SCRIPT_DIR/.env from template. Fill in secrets before syncing."
else
echo "Using existing $SCRIPT_DIR/.env"
fi
echo
echo "Bootstrap complete."
echo "Next steps:"
echo "1. Review $SCRIPT_DIR/.env"
echo "2. Run: bash scripts/mattermost/sync.sh"

View File

@@ -0,0 +1,349 @@
#!/usr/bin/env python3
import json
import logging
import os
import ssl
import sys
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any, Dict, List
from urllib import error, parse, request
SCRIPT_DIR = Path(__file__).resolve().parent
DEFAULT_WINDOW_HOURS = 24
DEFAULT_MAX_MESSAGES = 200
MAX_PER_PAGE = 200
DEFAULT_OUTPUT_FILE = str(SCRIPT_DIR / "generated" / "mattermost_context.jsonl")
REQUEST_TIMEOUT = 15
LOGGER = logging.getLogger("mattermost_context")
USER_CACHE: Dict[str, Dict[str, Any]] = {}
MATTERMOST_URL = ""
CHANNEL_IDS: List[str] = []
WINDOW_HOURS = DEFAULT_WINDOW_HOURS
MAX_MESSAGES = DEFAULT_MAX_MESSAGES
CUTOFF_TIMESTAMP_MS = 0
OUTPUT_FILE = DEFAULT_OUTPUT_FILE
REQUEST_HEADERS: Dict[str, str] = {}
SSL_CONTEXT: ssl.SSLContext | None = None
class MattermostAPIError(RuntimeError):
pass
def parse_bool_env(name: str, default: bool = False) -> bool:
raw_value = os.getenv(name)
if raw_value is None:
return default
return raw_value.strip().lower() in {"1", "true", "yes", "on"}
def load_dotenv_file(path: Path | None = None) -> None:
dotenv_path = path or (SCRIPT_DIR / ".env")
if not dotenv_path.exists():
return
with dotenv_path.open("r", encoding="utf-8") as file_handle:
for raw_line in file_handle:
line = raw_line.strip()
if not line or line.startswith("#"):
continue
if line.startswith("export "):
line = line[len("export ") :].strip()
if "=" not in line:
continue
key, value = line.split("=", 1)
key = key.strip()
value = value.strip()
if not key:
continue
if len(value) >= 2 and value[0] == value[-1] and value[0] in {'"', "'"}:
value = value[1:-1]
os.environ.setdefault(key, value)
def require_env(name: str) -> str:
value = os.getenv(name, "").strip()
if not value:
raise ValueError(f"Missing required environment variable: {name}")
return value
def parse_channel_ids(raw_value: str) -> List[str]:
normalized = raw_value.replace("\n", ",")
channel_ids = [item.strip() for item in normalized.split(",") if item.strip()]
if not channel_ids:
raise ValueError("CHANNEL_IDS must contain at least one channel id.")
return channel_ids
def build_ssl_context() -> ssl.SSLContext:
ca_bundle = os.getenv("MATTERMOST_CA_BUNDLE", "").strip()
skip_tls_verify = parse_bool_env("MATTERMOST_SKIP_TLS_VERIFY", default=False)
if skip_tls_verify:
LOGGER.warning("TLS certificate verification is disabled via MATTERMOST_SKIP_TLS_VERIFY.")
return ssl._create_unverified_context()
if ca_bundle:
LOGGER.info("Using custom CA bundle from MATTERMOST_CA_BUNDLE: %s", ca_bundle)
return ssl.create_default_context(cafile=ca_bundle)
return ssl.create_default_context()
def configure() -> None:
global MATTERMOST_URL, CHANNEL_IDS, WINDOW_HOURS, MAX_MESSAGES, CUTOFF_TIMESTAMP_MS, OUTPUT_FILE
global REQUEST_HEADERS, SSL_CONTEXT
load_dotenv_file()
MATTERMOST_URL = require_env("MATTERMOST_URL").rstrip("/")
token = require_env("MATTERMOST_TOKEN")
CHANNEL_IDS = parse_channel_ids(require_env("CHANNEL_IDS"))
WINDOW_HOURS = int(os.getenv("MESSAGE_WINDOW_HOURS", str(DEFAULT_WINDOW_HOURS)))
MAX_MESSAGES = int(os.getenv("MAX_MESSAGES", str(DEFAULT_MAX_MESSAGES)))
OUTPUT_FILE = os.getenv("MATTERMOST_OUTPUT_FILE", DEFAULT_OUTPUT_FILE).strip() or DEFAULT_OUTPUT_FILE
if WINDOW_HOURS <= 0:
raise ValueError("MESSAGE_WINDOW_HOURS must be greater than 0.")
if MAX_MESSAGES <= 0:
raise ValueError("MAX_MESSAGES must be greater than 0.")
cutoff = datetime.now().astimezone() - timedelta(hours=WINDOW_HOURS)
CUTOFF_TIMESTAMP_MS = int(cutoff.timestamp() * 1000)
REQUEST_HEADERS = {
"Authorization": f"Bearer {token}",
"Accept": "application/json",
"Content-Type": "application/json",
}
SSL_CONTEXT = build_ssl_context()
def api_get_json(api_path: str, params: Dict[str, Any] | None = None) -> Dict[str, Any]:
query = f"?{parse.urlencode(params)}" if params else ""
url = f"{MATTERMOST_URL}{api_path}{query}"
req = request.Request(url, headers=REQUEST_HEADERS, method="GET")
try:
with request.urlopen(req, timeout=REQUEST_TIMEOUT, context=SSL_CONTEXT) as response:
charset = response.headers.get_content_charset() or "utf-8"
payload = response.read().decode(charset)
except error.HTTPError as exc:
body = ""
try:
body = exc.read().decode("utf-8", errors="replace")
except Exception:
body = ""
raise MattermostAPIError(f"HTTP {exc.code} for {api_path}: {body or exc.reason}") from exc
except error.URLError as exc:
reason = exc.reason if hasattr(exc, "reason") else str(exc)
raise MattermostAPIError(f"Request failed for {api_path}: {reason}") from exc
try:
return json.loads(payload)
except json.JSONDecodeError as exc:
raise MattermostAPIError(f"Invalid JSON returned by {api_path}") from exc
def get_channel_posts(channel_id: str) -> List[Dict[str, Any]]:
collected: List[Dict[str, Any]] = []
page = 0
per_page = min(MAX_PER_PAGE, MAX_MESSAGES)
while len(collected) < MAX_MESSAGES:
payload = api_get_json(
f"/api/v4/channels/{channel_id}/posts",
{"page": page, "per_page": per_page},
)
order = payload.get("order", [])
posts_by_id = payload.get("posts", {})
if not order:
break
reached_cutoff = False
for post_id in order:
post = posts_by_id.get(post_id)
if not post:
continue
created_at = int(post.get("create_at", 0))
if created_at < CUTOFF_TIMESTAMP_MS:
reached_cutoff = True
continue
collected.append(post)
if len(collected) >= MAX_MESSAGES:
break
if reached_cutoff or len(order) < per_page:
break
page += 1
LOGGER.info("Fetched %s raw posts from channel %s", len(collected), channel_id)
return collected
def get_user_info(user_id: str) -> Dict[str, Any]:
if not user_id:
return {"id": "", "username": "unknown"}
if user_id in USER_CACHE:
return USER_CACHE[user_id]
try:
user_data = api_get_json(f"/api/v4/users/{user_id}")
except MattermostAPIError as exc:
LOGGER.error("Could not fetch user %s: %s", user_id, exc)
fallback = {"id": user_id, "username": user_id}
USER_CACHE[user_id] = fallback
return fallback
USER_CACHE[user_id] = user_data
return user_data
def build_user_map(messages: List[Dict[str, Any]]) -> Dict[str, str]:
user_map: Dict[str, str] = {}
user_ids = sorted({message["user_id"] for message in messages if message.get("user_id")})
for user_id in user_ids:
user_info = get_user_info(user_id)
username = (
user_info.get("username")
or user_info.get("nickname")
or user_info.get("first_name")
or user_id
)
user_map[user_id] = username
return user_map
def is_system_message(post: Dict[str, Any]) -> bool:
post_type = (post.get("type") or "").strip()
return post_type.startswith("system_")
def extract_messages() -> List[Dict[str, Any]]:
all_messages: List[Dict[str, Any]] = []
for channel_id in CHANNEL_IDS:
raw_posts = get_channel_posts(channel_id)
for post in raw_posts:
if is_system_message(post):
continue
message = (post.get("message") or "").strip()
if not message:
continue
all_messages.append(
{
"channel_id": channel_id,
"channel_ref": channel_id,
"post_id": post.get("id", ""),
"user_id": post.get("user_id", ""),
"create_at": int(post.get("create_at", 0)),
"message": message.replace("\r\n", "\n"),
"root_id": post.get("root_id", ""),
"reply_count": int(post.get("reply_count", 0)),
}
)
all_messages.sort(key=lambda item: item["create_at"])
if len(all_messages) > MAX_MESSAGES:
all_messages = all_messages[-MAX_MESSAGES:]
user_map = build_user_map(all_messages)
for message in all_messages:
message["username"] = user_map.get(message["user_id"], message["user_id"] or "unknown")
LOGGER.info("Prepared %s messages after filtering", len(all_messages))
return all_messages
def format_messages(messages: List[Dict[str, Any]]) -> str:
lines: List[str] = []
for message in messages:
timestamp = datetime.fromtimestamp(message["create_at"] / 1000).astimezone()
username = message.get("username", "unknown")
post_id = message.get("post_id", "")
root_id = message.get("root_id", "")
thread_id = root_id or post_id or "unknown"
reply_count = int(message.get("reply_count", 0))
if root_id:
message_kind = "thread_reply"
elif reply_count > 0:
message_kind = "thread_root"
else:
message_kind = "channel_post"
channel_ref = message.get("channel_ref", message.get("channel_id", "unknown"))
record = {
"source": "mattermost",
"channel": channel_ref,
"channel_id": message.get("channel_id", ""),
"post_id": post_id,
"thread_id": thread_id,
"root_id": root_id or None,
"type": message_kind,
"timestamp": timestamp.isoformat(),
"username": username,
"message": message["message"],
}
if root_id:
record["reply_to"] = root_id
if reply_count > 0:
record["reply_count"] = reply_count
lines.append(json.dumps(record, ensure_ascii=False, sort_keys=False))
return "\n".join(lines)
def save_to_file(text: str) -> None:
output_path = Path(OUTPUT_FILE).expanduser().resolve()
output_path.parent.mkdir(parents=True, exist_ok=True)
if text and not text.endswith("\n"):
text = f"{text}\n"
output_path.write_text(text, encoding="utf-8")
def main() -> int:
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
try:
configure()
messages = extract_messages()
output = format_messages(messages)
print(output)
save_to_file(output)
LOGGER.info("Saved context to %s", OUTPUT_FILE)
except ValueError as exc:
LOGGER.error("%s", exc)
return 1
except MattermostAPIError as exc:
LOGGER.error("%s", exc)
return 1
except Exception as exc:
LOGGER.exception("Unexpected error: %s", exc)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1 @@
# No third-party dependencies are currently required.

15
scripts/mattermost/sync.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [[ -x "$SCRIPT_DIR/.venv/bin/python" ]]; then
PYTHON_BIN="$SCRIPT_DIR/.venv/bin/python"
elif [[ -n "${PYTHON_BIN:-}" ]]; then
PYTHON_BIN="$PYTHON_BIN"
else
PYTHON_BIN="python3"
fi
exec "$PYTHON_BIN" "$SCRIPT_DIR/mattermost_context.py"

View File

@@ -18,10 +18,11 @@ Use OpenCode as the daily AI entry point for this workspace without losing proje
## Recommended Daily Sequence
1. Run `/fidelity-context` at the start of the day.
2. When new work happens on the main development machine, run `/sync-context ...` or `/log-note ...`.
3. When you need a supervisor update, run `/jeff-update ...`.
4. When you need polished English for Mattermost, run `/translate ...`.
5. When you need a standup, run `/standup`.
2. If Mattermost sync is configured, let the inbox refresh automatically or run `/mattermost-sync`.
3. When new work happens on the main development machine, run `/sync-context ...` or `/log-note ...`.
4. When you need a supervisor update, run `/jeff-update ...`.
5. When you need polished English for Mattermost, run `/translate ...`.
6. When you need a standup, run `/standup`.
---
@@ -30,6 +31,7 @@ Use OpenCode as the daily AI entry point for this workspace without losing proje
- Project instructions load automatically from `opencode.json`.
- Root rules also load automatically from `AGENTS.md`.
- The main context command reads the stable workspace files plus today's log.
- Mattermost context can be refreshed into `ai/inbox/` using your existing local sync script.
- Daily updates go back into the workspace, so later prompts inherit better context.
- The local compaction plugin helps preserve the most important workspace context during long sessions.
- This reduces reliance on fragile session memory.