8 Commits

Author SHA1 Message Date
0980e6695a docs: complete accountlink UIKit validation, add daily log, and document force-path testing guide 2026-05-22 16:29:45 -06:00
abab9bb9b6 chore: decommission repository and document migration to new runtime structure 2026-05-22 15:24:12 -06:00
60868b9c96 docs: add initial project architecture, concepts, and quick-start documentation 2026-05-22 09:26:53 -06:00
7594e8ebc9 docs: add AI workspace improvements document and link it in the getting started guide 2026-05-22 07:48:05 -06:00
d01ee1ac1a chore: transition work items to in-review, update validation logs, and document AI workspace improvements 2026-05-22 07:48:01 -06:00
cc716f8f7e feat: implement dynamic profile discovery and improve UI responsiveness for service management 2026-05-21 14:49:52 -06:00
e03518e507 fix: update profile paths in documentation and scripts for consistency and clarity 2026-05-21 14:17:15 -06:00
ad230e1abe 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. 2026-05-21 14:13:21 -06:00
47 changed files with 1136 additions and 174 deletions

View File

@@ -10,14 +10,13 @@ Inputs:
- `$ARGUMENTS` may contain an export path, channel names, or date filters
- if no explicit path is given, use `AIW_SLACK_EXPORT_PATH` when available
- Fidelity profile alias: `FIDELITY_SLACK_EXPORT_PATH`
- otherwise, if `archives/slack/export/` exists, use it as the default import source
- if no channels are specified, auto-detect channels using `AIW_CHANNEL_PREFIX`
- Fidelity default prefix: `fidelity`
Run the importer:
!`prefix="${AIW_CHANNEL_PREFIX:-fidelity}"; if [ -n "$ARGUMENTS" ]; then python3 scripts/slack/import_slack_export.py $ARGUMENTS; elif [ -n "$AIW_SLACK_EXPORT_PATH" ]; then python3 scripts/slack/import_slack_export.py --export-path "$AIW_SLACK_EXPORT_PATH" --channel-prefix "$prefix"; elif [ -n "$FIDELITY_SLACK_EXPORT_PATH" ]; then python3 scripts/slack/import_slack_export.py --export-path "$FIDELITY_SLACK_EXPORT_PATH" --channel-prefix "$prefix"; elif [ -d archives/slack/export ]; then python3 scripts/slack/import_slack_export.py --export-path archives/slack/export --channel-prefix "$prefix"; else echo "Provide archive import arguments, set AIW_SLACK_EXPORT_PATH, set FIDELITY_SLACK_EXPORT_PATH, or place an extracted export in archives/slack/export."; fi`
!`prefix="${AIW_CHANNEL_PREFIX:-fidelity}"; if [ -n "$ARGUMENTS" ]; then python3 scripts/slack/import_slack_export.py $ARGUMENTS; elif [ -n "$AIW_SLACK_EXPORT_PATH" ]; then python3 scripts/slack/import_slack_export.py --export-path "$AIW_SLACK_EXPORT_PATH" --channel-prefix "$prefix"; elif [ -d archives/slack/export ]; then python3 scripts/slack/import_slack_export.py --export-path archives/slack/export --channel-prefix "$prefix"; else echo "Provide archive import arguments, set AIW_SLACK_EXPORT_PATH, or place an extracted export in archives/slack/export."; fi`
Read:

View File

@@ -7,12 +7,11 @@ Use the configured live communication connector to fetch fresh evidence and main
Preferred command sources:
- `AIW_MATTERMOST_SYNC_CMD`
- Fidelity profile alias: `FIDELITY_MATTERMOST_SYNC_CMD`
- workspace fallback: `bash scripts/mattermost/sync.sh`
- Built-in wrapper: `bash scripts/mattermost/sync.sh`
Run the connector:
!`if [ -n "$AIW_MATTERMOST_SYNC_CMD" ]; then bash -lc "$AIW_MATTERMOST_SYNC_CMD"; elif [ -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 live communication sync command is configured."; fi`
!`if [ -n "$AIW_MATTERMOST_SYNC_CMD" ]; then bash -lc "$AIW_MATTERMOST_SYNC_CMD"; elif [ -f scripts/mattermost/sync.sh ]; then bash scripts/mattermost/sync.sh; else echo "No live communication sync command is configured."; fi`
Read:
@@ -31,7 +30,7 @@ Fresh communication evidence:
Instructions:
- if the sync command failed, stop and do not edit workspace memory
- prefer local proxy mirror evidence when present; legacy sync output is fallback evidence
- prefer local proxy mirror evidence when present; use the configured sync wrapper only when a fresh API pull is required
- treat connector output as evidence, not automatically as project truth
- promote only explicit, project-relevant, high-confidence facts
- default destination is `workspaces/fidelity/project-knowledge/06-daily/$(date +%F).md`

View File

@@ -2,7 +2,7 @@
description: Force-sync Mattermost and answer from the latest matching message
---
Refresh/read Mattermost first, then answer the user's question from the freshest evidence. Prefer the local proxy mirror when it is present; legacy sync output is fallback evidence.
Refresh/read Mattermost first, then answer the user's question from the freshest evidence. Prefer the local proxy mirror when it is present; use the configured sync wrapper only when a fresh API pull is required.
Use this when the user asks for:
@@ -13,7 +13,7 @@ Use this when the user asks for:
Run sync/fallback refresh:
!`start=$(date +%s); if [ -n "$AIW_MATTERMOST_SYNC_CMD" ]; then bash -lc "$AIW_MATTERMOST_SYNC_CMD"; elif [ -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; status=$?; end=$(date +%s); echo "__MATTERMOST_SYNC_SECONDS__=$((end - start))"; exit "$status"`
!`start=$(date +%s); if [ -n "$AIW_MATTERMOST_SYNC_CMD" ]; then bash -lc "$AIW_MATTERMOST_SYNC_CMD"; elif [ -f scripts/mattermost/sync.sh ]; then bash scripts/mattermost/sync.sh; else echo "No Mattermost sync command is configured."; fi; status=$?; end=$(date +%s); echo "__MATTERMOST_SYNC_SECONDS__=$((end - start))"; exit "$status"`
Read a focused slice of refreshed Mattermost context, preferring the proxy mirror:

View File

@@ -7,12 +7,11 @@ Use the configured Mattermost sync command and/or local proxy mirror evidence to
Preferred command sources:
- `AIW_MATTERMOST_SYNC_CMD`
- Fidelity profile alias: `FIDELITY_MATTERMOST_SYNC_CMD`
- fallback: `bash scripts/mattermost/sync.sh`
- Built-in wrapper: `bash scripts/mattermost/sync.sh`
Run the command and use its output as fresh communication context:
!`if [ -n "$AIW_MATTERMOST_SYNC_CMD" ]; then bash -lc "$AIW_MATTERMOST_SYNC_CMD"; elif [ -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`
!`if [ -n "$AIW_MATTERMOST_SYNC_CMD" ]; then bash -lc "$AIW_MATTERMOST_SYNC_CMD"; elif [ -f scripts/mattermost/sync.sh ]; then bash scripts/mattermost/sync.sh; else echo "No Mattermost sync command is configured."; fi`
Fresh Mattermost evidence, preferring the proxy mirror:

View File

@@ -10,7 +10,6 @@ Inputs:
- `$ARGUMENTS` may contain an export path, channel names, or date filters
- if no explicit path is given in the arguments, use `AIW_SLACK_EXPORT_PATH` when available
- Fidelity profile alias: `FIDELITY_SLACK_EXPORT_PATH`
- otherwise, if `archives/slack/export/` exists, use it as the default import source
- if no channels are specified, auto-detect channels using `AIW_CHANNEL_PREFIX`
- Fidelity default prefix: `fidelity`
@@ -20,7 +19,7 @@ Inputs:
First, run the importer:
!`prefix="${AIW_CHANNEL_PREFIX:-fidelity}"; if [ -n "$ARGUMENTS" ]; then python3 scripts/slack/import_slack_export.py $ARGUMENTS; elif [ -n "$AIW_SLACK_EXPORT_PATH" ]; then python3 scripts/slack/import_slack_export.py --export-path "$AIW_SLACK_EXPORT_PATH" --channel-prefix "$prefix"; elif [ -n "$FIDELITY_SLACK_EXPORT_PATH" ]; then python3 scripts/slack/import_slack_export.py --export-path "$FIDELITY_SLACK_EXPORT_PATH" --channel-prefix "$prefix"; elif [ -d archives/slack/export ]; then python3 scripts/slack/import_slack_export.py --export-path archives/slack/export --channel-prefix "$prefix"; else echo "Provide Slack import arguments, set AIW_SLACK_EXPORT_PATH, set FIDELITY_SLACK_EXPORT_PATH, or place an extracted export in archives/slack/export."; fi`
!`prefix="${AIW_CHANNEL_PREFIX:-fidelity}"; if [ -n "$ARGUMENTS" ]; then python3 scripts/slack/import_slack_export.py $ARGUMENTS; elif [ -n "$AIW_SLACK_EXPORT_PATH" ]; then python3 scripts/slack/import_slack_export.py --export-path "$AIW_SLACK_EXPORT_PATH" --channel-prefix "$prefix"; elif [ -d archives/slack/export ]; then python3 scripts/slack/import_slack_export.py --export-path archives/slack/export --channel-prefix "$prefix"; else echo "Provide Slack import arguments, set AIW_SLACK_EXPORT_PATH, or place an extracted export in archives/slack/export."; fi`
Read:

View File

@@ -32,7 +32,7 @@ import subprocess
from pathlib import Path
from datetime import datetime
cmd = os.environ.get("AIW_MATTERMOST_SYNC_CMD") or os.environ.get("FIDELITY_MATTERMOST_SYNC_CMD")
cmd = os.environ.get("AIW_MATTERMOST_SYNC_CMD")
today = datetime.now().astimezone().date().isoformat()
commands = []

View File

@@ -26,7 +26,7 @@ Behavior rules:
- Work item notes should preserve Jira ID/title and explicit relationships so standups, Bases, and graph navigation stay useful.
- Daily notes should include `focus`, `work-items`, and `blockers` when those values are clear.
- Before answering a prompt that depends on current state, verify the latest relevant files instead of relying only on conversation history.
- If the prompt asks for the latest Mattermost message, the last message from Jeff/current manager, or what someone just said, refresh or read the freshest Mattermost evidence before answering; the proxy mirror is the primary source when it is present, and legacy sync artifacts are fallback evidence.
- If the prompt asks for the latest Mattermost message, the last message from Jeff/current manager, or what someone just said, refresh or read the freshest Mattermost evidence before answering; the proxy mirror is the primary source when it is present, and configured sync artifacts are secondary evidence.
- Treat latest-message prompts as read-first: answer from refreshed evidence and report memory update candidates instead of editing canonical memory by default.
- For learning-style questions, answer from known context and verified facts only; explicitly label unknowns, assumptions, and inferences.
- For learning sessions, prioritize durable architecture, process, ownership, debugging strategy, release mechanics, domain concepts, and decision rules over transient ticket status.
@@ -51,7 +51,7 @@ Behavior rules:
- role-to-person mapping and recurring stakeholders go to `workspaces/fidelity/project-knowledge/04-people/`
- confirmed decisions go to `workspaces/fidelity/project-knowledge/05-decisions/`
- behavioral rules for how this workspace should respond go to the exact command, prompt, agent, skill, or `agent-memory/` file that enforces that behavior
- Use generic `AIW_*` integration variables for new tooling and keep `FIDELITY_*` only as Fidelity-profile aliases.
- Use generic `AIW_*` integration variables for workspace tooling.
- Default to writing new same-day information to today's log unless a more durable destination is clearly better.
- Write canonical memory to `workspaces/fidelity/project-knowledge/`.
- Update preexisting memory when a new prompt clarifies or corrects something already stored.

View File

@@ -20,9 +20,7 @@ function envFlag(name, defaultValue = false) {
}
async function resolveSyncCommand(directory) {
const configured =
process.env.AIW_MATTERMOST_SYNC_CMD?.trim() ||
process.env.FIDELITY_MATTERMOST_SYNC_CMD?.trim()
const configured = process.env.AIW_MATTERMOST_SYNC_CMD?.trim()
if (configured) return configured
const fallbackScript = path.join(directory, "scripts/mattermost/sync.sh")
@@ -116,7 +114,6 @@ function requiresFreshMattermost(promptText) {
process.env.AIW_COMMUNICATION_FRESH_TERMS,
process.env.AIW_MANAGER_NAME,
process.env.AIW_PRIMARY_STAKEHOLDER,
process.env.FIDELITY_MANAGER_NAME,
"jeff",
"fidelity-preguntas",
]
@@ -148,7 +145,6 @@ export const MattermostInbox = async ({ $, directory, client }) => {
const intervalMinutes = Number.parseInt(
process.env.AIW_MATTERMOST_SYNC_INTERVAL_MINUTES ||
process.env.FIDELITY_MATTERMOST_SYNC_INTERVAL_MINUTES ||
"15",
10,
)
@@ -165,9 +161,7 @@ export const MattermostInbox = async ({ $, directory, client }) => {
const commandSource = process.env.AIW_MATTERMOST_SYNC_CMD?.trim()
? "aiw-env"
: process.env.FIDELITY_MATTERMOST_SYNC_CMD?.trim()
? "fidelity-env"
: "workspace-default"
: "workspace-default"
try {
await mkdir(inboxDir, { recursive: true })

24
DECOMMISSIONED.md Normal file
View File

@@ -0,0 +1,24 @@
# Decommissioned Workspace
This repository is no longer the active AIWorkspace runtime.
As of 2026-05-22, the active split is:
- reusable core: `/Users/david/Developer/ai-workspace`
- Fidelity context pack: `/Users/david/Developer/fidelity-ai-context`
- local project registry: `~/Library/Application Support/AIWorkspace/projects.json`
- runtime state: `~/Library/Application Support/AIWorkspace/runtime/`
- logs: `~/Library/Logs/AIWorkspace/`
Do not start services from this repository unless intentionally rolling back.
Use the new core instead:
```bash
cd /Users/david/Developer/ai-workspace
python3 scripts/aiw/services.py status --project fidelity
python3 scripts/aiw/services.py doctor --project fidelity
```
The old profile-based service manager may report services as `externally running`
because the new core owns the same health ports.

View File

@@ -1,5 +1,10 @@
# AI Workspace
> **Decommissioned:** this repository is no longer the active AIWorkspace runtime.
> Active services now run from `/Users/david/Developer/ai-workspace`, and Fidelity
> context lives in `/Users/david/Developer/fidelity-ai-context`. See
> [`DECOMMISSIONED.md`](DECOMMISSIONED.md) before using this repo.
AI Workspace is a local, profile-based companion workspace for AI-assisted professional work. It keeps project memory, raw evidence, local services, and AI client integrations organized so agents can work from current, auditable context instead of chat history alone.
The first real profile in this repository is `fidelity`, but the reusable model is intentionally project-independent.
@@ -190,12 +195,22 @@ python3 scripts/iphone-photo-inbox/test_receiver.py
Recommended order for new projects:
1. Copy `profiles/example/` to a new profile.
2. Create or point to a project knowledge vault.
3. Configure only the services the project needs.
4. Keep raw evidence outside canonical memory.
5. Build the local index.
6. Connect AI clients through MCP.
7. Promote durable facts into Markdown as work progresses.
1. Create an isolated profile:
```bash
python3 scripts/aiw/profile.py create my-project --display-name "My Project"
```
2. Configure only the services the project needs.
3. Keep raw evidence outside canonical memory.
4. Build the local index.
5. Connect AI clients through MCP.
6. Promote durable facts into Markdown as work progresses.
Validate the profile layout with:
```bash
python3 scripts/aiw/profile.py doctor --profile my-project
```
The reusable core should not depend on a company name, ticket prefix, channel name, programming stack, or AI client.

View File

@@ -19,7 +19,7 @@ Mattermost is the current live communication connector.
- Primary local evidence is the Mattermost proxy mirror under `workspaces/fidelity/inbox/mattermost-mirror/` when present.
- Prefer `workspaces/fidelity/inbox/mattermost-mirror/latest.md` / `latest.jsonl` for latest-message context, `by-date/YYYY/MM/YYYY-MM-DD.jsonl` for daily/standup context, `channels/<channel>/YYYY/MM/YYYY-MM-DD.jsonl` for channel-specific context, and `threads/<root-or-post-id>.jsonl` for thread-specific context.
- Use `scripts/mattermost-proxy/read-context.py` from commands/workflows instead of reading ad hoc files; it prefers the proxy mirror and falls back to legacy sync artifacts.
- Use `scripts/mattermost-proxy/read-context.py` from commands/workflows instead of reading ad hoc files; it prefers the profile proxy mirror and can read configured sync artifacts when a mirror is unavailable.
- Legacy fresh output may still go to `workspaces/fidelity/inbox/mattermost-latest.md`.
- Legacy generated extraction artifacts stay under `scripts/mattermost/generated/`.
- Failed syncs must not update project knowledge.
@@ -29,12 +29,12 @@ Mattermost is the current live communication connector.
- Standup reads should use the focused reader mode, `scripts/mattermost-proxy/read-context.py --mode standup --today YYYY-MM-DD`, which reads date-bucketed previous-workday/today records and should use the active profile's configured `AIW_MATTERMOST_CONTEXT_CHANNELS` when available. Avoid loading broad mirror `latest.md` into standup prompts because it may include stale or unrelated channels and waste tokens. Keep project-specific channel names out of reusable connector code.
- If adding MCP support for Mattermost, treat it as a read-only query wrapper over the existing proxy mirror and `read-context.py`, not as a replacement for the capture/mirror pipeline. Keep the mirror's file layout as canonical raw evidence and expose only narrow tools such as latest, standup/date, channel, and thread reads with channel filters and limits.
- Do not build a write-capable Mattermost MCP or expose tokens, cookies, raw headers, or broad unfiltered raw dumps through MCP. MCP output should remain evidence for agent reasoning; promotion to `workspaces/fidelity/project-knowledge/` still follows normal memory rules.
- If the proxy mirror is running, treat it as fresher than legacy `mattermost-latest.md` / generated JSONL. Do not ignore mirror evidence merely because a legacy sync command also ran.
- If the proxy mirror is running, treat it as fresher than generated sync artifacts. Do not ignore mirror evidence merely because a sync command also ran.
- Do not refresh Mattermost just because a prompt mentions a manager or stakeholder.
- Treat document review, message polishing, translation, and "does this align with Jeff's expectations?" prompts as normal drafting tasks unless the user explicitly asks for the latest message or fresh Mattermost evidence.
- The OpenCode plugin syncs automatically only for explicit latest-message requests by default.
- Optional aggressive sync can be enabled with `AIW_MATTERMOST_SYNC_ON_SESSION=true` or `AIW_MATTERMOST_SYNC_ON_PROMPT=true`, but these should stay off for low-latency daily use.
- When invoking Mattermost sync from OpenCode, do not use parameter expansion that places a command with spaces into a single shell word, such as `${VAR:-bash scripts/mattermost/sync.sh}`. Run configured command strings via `bash -lc "$AIW_MATTERMOST_SYNC_CMD"` / `bash -lc "$FIDELITY_MATTERMOST_SYNC_CMD"`, and run the fallback as separate words: `bash scripts/mattermost/sync.sh`.
- When invoking Mattermost sync from OpenCode, do not use parameter expansion that places a command with spaces into a single shell word, such as `${VAR:-bash scripts/mattermost/sync.sh}`. Run configured command strings via `bash -lc "$AIW_MATTERMOST_SYNC_CMD"`, and run the built-in wrapper as separate words: `bash scripts/mattermost/sync.sh`.
---

View File

@@ -17,18 +17,14 @@ Use `scripts/memory/memory.sh` as the project-agnostic interface to project know
The primary project knowledge directory is `workspaces/fidelity/project-knowledge/`.
Environment variable precedence:
Configuration precedence:
1. `AIW_PROJECT_KNOWLEDGE_DIR`
2. `AIW_MEMORY_VAULT_DIR`
3. `AIW_OBSIDIAN_VAULT_DIR`
4. `<workspace-root>/project-knowledge`
`AIW_MEMORY_VAULT_DIR` and `AIW_OBSIDIAN_VAULT_DIR` are transition aliases only.
1. `AIW_PROJECT_PROFILE`
2. `profiles/<profile>/workspace.json`
3. optional local `AIW_PROJECT_KNOWLEDGE_DIR` override
---
## Agent Rule
Prefer the memory interface for typed note creation, search, Base queries, and health checks. Use direct Markdown edits when the adapter fails or when precise edits are simpler and safer.

View File

@@ -28,6 +28,21 @@ Install to `~/Applications/AIWorkspace.app`:
apps/mac/AIWorkspace/scripts/package-app.sh --install
```
The script defaults to the per-user install location because this workspace is a
developer-local utility and frequent rebuilds should not require `sudo`. To
install system-wide instead, pass an explicit install directory:
```bash
INSTALL_DIR=/Applications apps/mac/AIWorkspace/scripts/package-app.sh --install
APP_PATH=/Applications/AIWorkspace.app apps/mac/AIWorkspace/scripts/install-start-at-login.sh
```
Avoid keeping both `~/Applications/AIWorkspace.app` and
`/Applications/AIWorkspace.app` installed unless you are intentionally comparing
builds. They share the same bundle identifier (`com.aiworkspace.menu`) and can
confuse LaunchServices, login item registration, and which copy opens at login.
If both exist, quit any running AI Workspace instances and remove the stale copy.
## Build a DMG
```bash

View File

@@ -4,11 +4,51 @@ import ServiceManagement
import SwiftUI
private let workspaceRoot = URL(fileURLWithPath: "/Users/david/Developer/fidelity-ai-workspace", isDirectory: true)
private let defaultProfile = "fidelity"
struct ProfileConfig {
let id: String
let displayName: String
let knowledgeDir: String
static func discoverDefault() -> ProfileConfig {
let profilesRoot = workspaceRoot.appendingPathComponent("profiles", isDirectory: true)
let candidates = (try? FileManager.default.contentsOfDirectory(at: profilesRoot, includingPropertiesForKeys: nil)) ?? []
let configs = candidates.compactMap { directory -> ProfileConfig? in
let configURL = directory.appendingPathComponent("workspace.json")
guard let data = try? Data(contentsOf: configURL),
let decoded = try? JSONDecoder().decode(ProfileWorkspaceConfig.self, from: data)
else { return nil }
return ProfileConfig(
id: decoded.profile ?? directory.lastPathComponent,
displayName: decoded.displayName ?? decoded.profile ?? directory.lastPathComponent,
knowledgeDir: decoded.knowledgeDir ?? "workspaces/\(decoded.profile ?? directory.lastPathComponent)/project-knowledge"
)
}
if let fidelity = configs.first(where: { $0.id == "fidelity" }) {
return fidelity
}
if let first = configs.sorted(by: { $0.id < $1.id }).first {
return first
}
return ProfileConfig(id: "fidelity", displayName: "Fidelity", knowledgeDir: "workspaces/fidelity/project-knowledge")
}
}
private struct ProfileWorkspaceConfig: Decodable {
let profile: String?
let displayName: String?
let knowledgeDir: String?
enum CodingKeys: String, CodingKey {
case profile
case displayName = "display_name"
case knowledgeDir = "knowledge_dir"
}
}
@main
struct AIWorkspaceApp: App {
@StateObject private var model = ServiceStatusModel(profile: defaultProfile)
@StateObject private var model = ServiceStatusModel()
var body: some Scene {
MenuBarExtra {
@@ -30,11 +70,12 @@ final class ServiceStatusModel: ObservableObject {
@Published private(set) var lanIP: String?
@Published private(set) var loginItemStatus: SMAppService.Status = .notRegistered
@Published private(set) var isRefreshing = false
@Published private(set) var activeAction: String?
let profile: String
let profile: ProfileConfig
init(profile: String) {
self.profile = profile
init() {
self.profile = ProfileConfig.discoverDefault()
}
var menuBarSymbol: String {
@@ -52,7 +93,7 @@ final class ServiceStatusModel: ObservableObject {
isRefreshing = true
defer { isRefreshing = false }
do {
let data = try await ServiceManager.run(["status", "--profile", profile, "--json"])
let data = try await ServiceManager.run(["status", "--profile", profile.id, "--json"])
report = try JSONDecoder().decode(StatusReport.self, from: data)
lanIP = await NetworkInfo.primaryLANIP()
loginItemStatus = SMAppService.mainApp.status
@@ -92,25 +133,33 @@ final class ServiceStatusModel: ObservableObject {
}
func startProfile() {
runAction(["start", "--profile", profile])
runAction("Starting services…", ["start", "--profile", profile.id])
}
func stopProfile() {
runAction(["stop", "--profile", profile])
runAction("Stopping services…", ["stop", "--profile", profile.id])
}
func primaryServiceAction() {
if allManagedServicesRunning {
stopProfile()
} else {
startProfile()
}
}
func restartMCP() {
runAction(["restart", "aiw-context-mcp", "--profile", profile])
runAction("Restarting MCP…", ["restart", "aiw-context-mcp", "--profile", profile.id])
}
func runDoctor() {
runAction(["doctor", "--profile", profile])
runAction("Running doctor…", ["doctor", "--profile", profile.id])
}
func copyDoctorJSON() {
Task {
do {
let data = try await ServiceManager.run(["doctor", "--profile", profile, "--json"])
let data = try await ServiceManager.run(["doctor", "--profile", profile.id, "--json"])
copyToPasteboard(String(data: data, encoding: .utf8) ?? "")
} catch {
lastError = String(describing: error)
@@ -128,7 +177,7 @@ final class ServiceStatusModel: ObservableObject {
do {
var chunks: [String] = []
for service in report?.services ?? [] {
let data = try await ServiceManager.run(["logs", service.name, "--profile", profile, "--lines", "30"])
let data = try await ServiceManager.run(["logs", service.name, "--profile", profile.id, "--lines", "30"])
if let text = String(data: data, encoding: .utf8), !text.isEmpty {
chunks.append(text)
}
@@ -147,15 +196,33 @@ final class ServiceStatusModel: ObservableObject {
}
func openLogsFolder() {
NSWorkspace.shared.open(workspaceRoot.appendingPathComponent(".aiw/runtime/logs", isDirectory: true))
NSWorkspace.shared.open(workspaceRoot.appendingPathComponent(".aiw/runtime/logs/\(profile.id)", isDirectory: true))
}
func openProjectKnowledge() {
NSWorkspace.shared.open(workspaceRoot.appendingPathComponent("project-knowledge", isDirectory: true))
NSWorkspace.shared.open(workspaceRoot.appendingPathComponent(profile.knowledgeDir, isDirectory: true))
}
func openMattermost() {
runAction(["start", "mattermost-desktop", "--profile", profile])
runAction("Launching Mattermost…", ["start", "mattermost-desktop", "--profile", profile.id])
}
var allManagedServicesRunning: Bool {
guard let services = report?.services else { return false }
let managed = services.filter { $0.enabled && $0.kind != "app-launcher" }
return !managed.isEmpty && managed.allSatisfy { $0.status == "running" || $0.status == "externally running" }
}
var primaryActionTitle: String {
allManagedServicesRunning ? "Stop Services" : "Start Services"
}
var primaryActionSymbol: String {
allManagedServicesRunning ? "stop.fill" : "play.fill"
}
var isBusy: Bool {
isRefreshing || activeAction != nil
}
private func copyToPasteboard(_ value: String) {
@@ -163,8 +230,10 @@ final class ServiceStatusModel: ObservableObject {
NSPasteboard.general.setString(value, forType: .string)
}
private func runAction(_ arguments: [String]) {
private func runAction(_ label: String, _ arguments: [String]) {
Task {
activeAction = label
defer { activeAction = nil }
do {
_ = try await ServiceManager.run(arguments)
await refresh()
@@ -184,20 +253,31 @@ struct ServiceMenuView: View {
VStack(alignment: .leading, spacing: 3) {
Text("AI Workspace")
.font(.title3.bold())
Text("Profile: \(model.profile)")
Text("Profile: \(model.profile.displayName)")
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
if model.isBusy {
ProgressView()
.controlSize(.small)
}
Button {
Task { await model.refresh() }
} label: {
Image(systemName: "arrow.clockwise")
}
.buttonStyle(.borderless)
.disabled(model.isBusy)
.help("Refresh")
}
if let activeAction = model.activeAction {
Label(activeAction, systemImage: "hourglass")
.font(.caption)
.foregroundStyle(.secondary)
}
Divider()
if let report = model.report {
@@ -231,20 +311,26 @@ struct ServiceMenuView: View {
Divider()
Button(role: model.allManagedServicesRunning ? .destructive : nil, action: model.primaryServiceAction) {
Label(model.primaryActionTitle, systemImage: model.primaryActionSymbol)
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
.disabled(model.isBusy)
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 8) {
ActionButton(title: "Start Fidelity", systemImage: "play.fill", action: model.startProfile)
ActionButton(title: "Stop Fidelity", systemImage: "stop.fill", role: .destructive, action: model.stopProfile)
ActionButton(title: "Restart MCP", systemImage: "arrow.triangle.2.circlepath", action: model.restartMCP)
ActionButton(title: "Mattermost via Proxy", systemImage: "message.badge", action: model.openMattermost)
ActionButton(title: "Restart MCP", systemImage: "arrow.triangle.2.circlepath", disabled: model.isBusy, action: model.restartMCP)
ActionButton(title: "Mattermost via Proxy", systemImage: "message.badge", disabled: model.isBusy, action: model.openMattermost)
}
Divider()
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 8) {
ActionButton(title: "Run Doctor", systemImage: "stethoscope", action: model.runDoctor)
ActionButton(title: "Copy Doctor JSON", systemImage: "doc.on.doc", action: model.copyDoctorJSON)
ActionButton(title: "Run Doctor", systemImage: "stethoscope", disabled: model.isBusy, action: model.runDoctor)
ActionButton(title: "Copy Doctor JSON", systemImage: "doc.on.doc", disabled: model.isBusy, action: model.copyDoctorJSON)
ActionButton(title: "Copy Photo URL", systemImage: "link", action: model.copyPhotoInboxURL)
ActionButton(title: "Copy Logs", systemImage: "doc.text", action: model.copyRecentLogs)
ActionButton(title: "Copy Logs", systemImage: "doc.text", disabled: model.isBusy, action: model.copyRecentLogs)
ActionButton(title: "MCP Health", systemImage: "heart.text.square", action: model.openMCPHealth)
ActionButton(title: "Logs Folder", systemImage: "folder", action: model.openLogsFolder)
}
@@ -290,6 +376,7 @@ struct ActionButton: View {
let title: String
let systemImage: String
var role: ButtonRole?
var disabled = false
let action: () -> Void
var body: some View {
@@ -299,6 +386,7 @@ struct ActionButton: View {
}
.buttonStyle(.bordered)
.controlSize(.small)
.disabled(disabled)
}
}
@@ -369,6 +457,15 @@ enum ServiceManager {
process.currentDirectoryURL = workspaceRoot
process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
process.arguments = ["python3", "scripts/aiw/services.py"] + arguments
var environment = ProcessInfo.processInfo.environment
let guiSafePATH = "/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/local/sbin:/usr/bin:/bin:/usr/sbin:/sbin"
if let existingPATH = environment["PATH"], !existingPATH.isEmpty {
environment["PATH"] = "\(guiSafePATH):\(existingPATH)"
} else {
environment["PATH"] = guiSafePATH
}
environment["AIW_WORKSPACE_ROOT"] = workspaceRoot.path
process.environment = environment
let output = Pipe()
let error = Pipe()

View File

@@ -12,7 +12,7 @@ Obsidian is the current human-facing implementation, but the workspace should no
### Canonical Memory
The source of truth is plain Markdown under `project-knowledge/`.
The source of truth is plain Markdown under `workspaces/<profile>/project-knowledge/`.
Agents should edit canonical notes directly when precision matters because direct edits produce auditable diffs.
@@ -31,16 +31,16 @@ Use `scripts/memory/memory.sh` for project-knowledge operations:
The current adapter is Obsidian CLI through `scripts/obsidian/cli.sh`.
If Obsidian CLI is unavailable or fails, `scripts/memory/memory.sh` falls back to direct Markdown operations unless `AIW_MEMORY_BACKEND=obsidian` is explicitly set.
If Obsidian CLI is unavailable or fails, `scripts/memory/memory.sh` uses direct Markdown operations unless `AIW_MEMORY_BACKEND=obsidian` is explicitly set.
---
## Configuration
- `AIW_PROJECT_KNOWLEDGE_DIR`: canonical project knowledge directory. Defaults to `<workspace-root>/project-knowledge`.
- `AIW_MEMORY_VAULT_DIR`: transition alias for the canonical project knowledge directory.
- `AIW_PROJECT_PROFILE`: selected profile. Defaults to `fidelity` in this workspace.
- `profiles/<profile>/workspace.json`: canonical source for `knowledge_dir`, `inbox_dir`, and `index_dir`.
- `AIW_PROJECT_KNOWLEDGE_DIR`: optional local override for the canonical project knowledge directory.
- `AIW_MEMORY_BACKEND`: `auto`, `files`, or `obsidian`. Defaults to `auto`.
- `AIW_OBSIDIAN_VAULT_DIR`: Obsidian-specific transition alias, still supported by the adapter.
- `AIW_OBSIDIAN_VAULT_NAME`: Obsidian URI vault name override for URI wrappers.
Backend meanings:
@@ -55,15 +55,15 @@ Backend meanings:
The memory interface owns type-to-folder routing:
- `daily` -> `project-knowledge/06-daily/`
- `work-item` -> `project-knowledge/02-work-items/`
- `person` -> `project-knowledge/04-people/`
- `decision` -> `project-knowledge/05-decisions/`
- `system` -> `project-knowledge/03-context/systems/`
- `workstream` -> `project-knowledge/03-context/workstreams/`
- `meeting-note` -> `project-knowledge/06-daily/`
- `daily` -> `workspaces/<profile>/project-knowledge/06-daily/`
- `work-item` -> `workspaces/<profile>/project-knowledge/02-work-items/`
- `person` -> `workspaces/<profile>/project-knowledge/04-people/`
- `decision` -> `workspaces/<profile>/project-knowledge/05-decisions/`
- `system` -> `workspaces/<profile>/project-knowledge/03-context/systems/`
- `workstream` -> `workspaces/<profile>/project-knowledge/03-context/workstreams/`
- `meeting-note` -> `workspaces/<profile>/project-knowledge/06-daily/`
Templates live in `project-knowledge/09-templates/`.
Templates live in `workspaces/<profile>/project-knowledge/09-templates/`.
This gives agents an automatic destination for new notes without coupling the rule to Obsidian.
@@ -77,7 +77,7 @@ This gives agents an automatic destination for new notes without coupling the ru
- Edit Markdown directly for precise memory curation, corrections, and content updates.
- Do not treat Obsidian CLI failure as project context.
- Do not require Obsidian to be running for core workspace operation.
- Keep raw evidence outside `project-knowledge/`; promote only curated memory.
- Keep raw evidence outside `workspaces/<profile>/project-knowledge/`; promote only curated memory.
---

View File

@@ -10,10 +10,10 @@ Obsidian should not become a second memory store.
## Recommended Vault
Open the `project-knowledge/` folder as the Obsidian vault:
Open the `workspaces/<profile>/project-knowledge/` folder as the Obsidian vault:
```text
<workspace-root>/project-knowledge/
<workspace-root>/workspaces/<profile>/project-knowledge/
```
This keeps one source of truth:
@@ -30,14 +30,14 @@ This keeps one source of truth:
Canonical project knowledge lives in:
- `project-knowledge/00-start/`
- `project-knowledge/01-current/`
- `project-knowledge/02-work-items/`
- `project-knowledge/03-context/`
- `project-knowledge/04-people/`
- `project-knowledge/05-decisions/`
- `project-knowledge/06-daily/`
- `project-knowledge/07-maps/`
- `workspaces/<profile>/project-knowledge/00-start/`
- `workspaces/<profile>/project-knowledge/01-current/`
- `workspaces/<profile>/project-knowledge/02-work-items/`
- `workspaces/<profile>/project-knowledge/03-context/`
- `workspaces/<profile>/project-knowledge/04-people/`
- `workspaces/<profile>/project-knowledge/05-decisions/`
- `workspaces/<profile>/project-knowledge/06-daily/`
- `workspaces/<profile>/project-knowledge/07-maps/`
Technical runtime remains outside the vault:
@@ -58,21 +58,21 @@ Communication evidence may exist under `workspaces/<profile>/inbox/` or connecto
Version portable Obsidian configuration only when it improves the workspace for every clone:
- `project-knowledge/.obsidian/app.json`
- `project-knowledge/.obsidian/core-plugins.json`
- `project-knowledge/.obsidian/graph.json`
- `project-knowledge/.obsidian/appearance.json`
- `project-knowledge/.obsidian/daily-notes.json`
- `project-knowledge/.obsidian/templates.json`
- `project-knowledge/.obsidian/bookmarks.json`
- `workspaces/<profile>/project-knowledge/.obsidian/app.json`
- `workspaces/<profile>/project-knowledge/.obsidian/core-plugins.json`
- `workspaces/<profile>/project-knowledge/.obsidian/graph.json`
- `workspaces/<profile>/project-knowledge/.obsidian/appearance.json`
- `workspaces/<profile>/project-knowledge/.obsidian/daily-notes.json`
- `workspaces/<profile>/project-knowledge/.obsidian/templates.json`
- `workspaces/<profile>/project-knowledge/.obsidian/bookmarks.json`
Do not version local runtime state:
- `project-knowledge/.obsidian/workspace*.json`
- `project-knowledge/.obsidian/workspace-mobile*.json`
- `project-knowledge/.obsidian/plugins/`
- `project-knowledge/.obsidian/snippets/`
- `project-knowledge/.obsidian/cache/`
- `workspaces/<profile>/project-knowledge/.obsidian/workspace*.json`
- `workspaces/<profile>/project-knowledge/.obsidian/workspace-mobile*.json`
- `workspaces/<profile>/project-knowledge/.obsidian/plugins/`
- `workspaces/<profile>/project-knowledge/.obsidian/snippets/`
- `workspaces/<profile>/project-knowledge/.obsidian/cache/`
Recommended graph and search exclusions:
@@ -107,7 +107,7 @@ The agent should not treat Obsidian runtime layout changes as project context.
If Obsidian metadata or properties are added, use them selectively for high-value notes such as work items, decisions, and index pages. Do not mass-convert existing files just to add metadata.
Use map notes under `project-knowledge/07-maps/` as graph hubs. This keeps the graph navigable without forcing every file into Obsidian-specific wiki-link syntax.
Use map notes under `workspaces/<profile>/project-knowledge/07-maps/` as graph hubs. This keeps the graph navigable without forcing every file into Obsidian-specific wiki-link syntax.
---

View File

@@ -87,7 +87,7 @@ core/**/*.md
scripts/**/*.md
```
Raw evidence stays outside `project-knowledge/`; promoted memory is written to `project-knowledge/`.
Raw evidence stays outside `workspaces/<profile>/project-knowledge/`; promoted memory is written to `workspaces/<profile>/project-knowledge/`.
---
@@ -178,7 +178,7 @@ The agent should also identify recurring quality gaps proactively. If an answer
For technical topics, recurring gaps often belong in:
- domain-specific guidance under `project-knowledge/03-context/`
- domain-specific guidance under `workspaces/<profile>/project-knowledge/03-context/`
- reusable skills under `.agents/skills/`
- agent behavior rules under `.opencode/agents/`, `AGENTS.md`, or `agent-memory/`
- source anchors for official/current documentation

View File

@@ -48,7 +48,7 @@ project-knowledge/09-templates/
Keep project-specific facts out of `core/`.
Treat `project-knowledge/` as canonical project memory. Keep connector inboxes and generated evidence outside the project knowledge vault.
Treat `workspaces/<profile>/project-knowledge/` as canonical project memory. Keep connector inboxes and generated evidence outside the project knowledge vault.
---
@@ -102,15 +102,15 @@ Before using the workspace for real work:
## 6. Obsidian Vault
Open `project-knowledge/` as the Obsidian vault.
Open `workspaces/<profile>/project-knowledge/` as the Obsidian vault.
Use `scripts/memory/` as the project-agnostic memory interface. Obsidian is the default visual and CLI-backed adapter, but profile logic should not depend on Obsidian.
Recommended rules:
- keep `project-knowledge/` as the clean canonical project knowledge
- keep runtime evidence, scripts, profiles, and generated files outside `project-knowledge/`
- version only portable `project-knowledge/.obsidian` configuration
- keep `workspaces/<profile>/project-knowledge/` as the clean canonical project knowledge
- keep runtime evidence, scripts, profiles, and generated files outside `workspaces/<profile>/project-knowledge/`
- version only portable `workspaces/<profile>/project-knowledge/.obsidian` configuration
- ignore local Obsidian workspace state and plugin runtime files
- create or update map notes under `project-knowledge/07-maps/` for human navigation
- create Bases under `project-knowledge/08-bases/` using simple `type` properties
- create or update map notes under `workspaces/<profile>/project-knowledge/07-maps/` for human navigation
- create Bases under `workspaces/<profile>/project-knowledge/08-bases/` using simple `type` properties

View File

@@ -12,7 +12,7 @@ tags:
## Goal
Add retrieval over canonical workspace memory without replacing the human-readable `project-knowledge/` vault.
Add retrieval over canonical workspace memory without replacing the human-readable profile project knowledge vault.
The local index is derived and disposable. If the index disagrees with Markdown, the Markdown wins.
@@ -29,7 +29,7 @@ scripts/aiw/indexer.py
It reads:
```text
project-knowledge/**/*.md
workspaces/<profile>/project-knowledge/**/*.md
```
and writes:
@@ -42,7 +42,7 @@ and writes:
It skips:
```text
project-knowledge/09-templates/
workspaces/<profile>/project-knowledge/09-templates/
```
so Obsidian templates do not appear as real memory.

View File

@@ -7,7 +7,7 @@ The AI Workspace should unify local service lifecycle without collapsing service
- Service manager: starts, stops, checks, and logs local services.
- Context MCP: exposes bounded read-only context to AI clients.
- Capture services: produce local evidence such as Mattermost mirror records or photo inbox files.
- Canonical memory remains under `project-knowledge/` and is updated by the agent using memory rules.
- Canonical memory remains under `workspaces/<profile>/project-knowledge/` and is updated by the agent using memory rules.
## Service Types

31
docs/README.md Normal file
View File

@@ -0,0 +1,31 @@
# AIWorkspace Documentation
AIWorkspace is a local context platform for developers. It connects project evidence, canonical human-readable memory, local services, and AI clients without making any single AI tool the source of truth.
This documentation describes the target production architecture: reusable AIWorkspace core separated from project-specific AI context packs.
## Start Here
- [Concepts: Overview](concepts/overview.md)
- [Concepts: Workspace vs Context Pack](concepts/workspace-vs-context-pack.md)
- [Architecture: Storage Model](architecture/storage-model.md)
- [Guide: Quick Start](guides/quick-start.md)
- [Reference: Project Manifest](reference/project-manifest.md)
## Documentation Map
```text
docs/
concepts/ Product concepts and user-facing mental model
architecture/ System boundaries, storage, services, and security
guides/ Task-oriented setup and usage guides
reference/ File formats, CLI, manifests, and environment variables
```
## Core Principle
```text
AIWorkspace provides context. Project context packs own project memory.
```
The core product should remain reusable and shareable. Project-specific facts, connector settings, people, channels, raw evidence, and prompts belong in separate context packs.

View File

@@ -0,0 +1,98 @@
# Storage Model
AIWorkspace uses different storage locations for different kinds of data. This keeps the product shareable, the project context auditable, and local runtime state disposable.
## Storage Responsibilities
| Data | Target Location | Git? | Notes |
|---|---|---:|---|
| Core product code | `~/Developer/ai-workspace` | yes | Shared reusable product. |
| Project manifest | `<context-pack>/aiw.project.json` | yes | Project identity and relative paths. |
| Canonical memory | `<context-pack>/knowledge/` | usually yes | Human-readable Markdown. |
| Raw evidence | `<context-pack>/inbox/` | usually no/private | Captured messages, screenshots, exports. |
| Connector config | `<context-pack>/connectors/` | depends | Safe config can be tracked; secrets must not be tracked. |
| Project prompts/agents | `<context-pack>/prompts/`, `<context-pack>/agents/` | depends | Track if safe for the team. |
| User registry | `~/Library/Application Support/AIWorkspace/` | no | Local project registration and preferences. |
| Logs | `~/Library/Logs/AIWorkspace/` | no | Service and app logs. |
| Caches/indexes | `~/Library/Caches/AIWorkspace/` | no | Rebuildable derived data. |
| Runtime/PIDs | `~/Library/Application Support/AIWorkspace/runtime/` | no | Local service state. |
## Canonical Memory
Canonical memory is the durable project source of truth for humans and AI.
Target structure:
```text
knowledge/
current/
work-items/
systems/
workstreams/
people/
decisions/
daily/
templates/
```
Canonical memory should be:
- readable without AIWorkspace;
- reviewable in Git when shared;
- explicit about uncertainty;
- updated by humans or agents using small auditable changes.
## Raw Evidence
Raw evidence is input, not truth.
Examples:
```text
inbox/
mattermost/
slack/
photos/
documents/
screenshots/
```
Raw evidence should not be promoted automatically into durable memory unless a workflow explicitly classifies it as high-confidence and project-relevant.
## Derived Indexes
Indexes are rebuildable.
They should live outside the context pack by default:
```text
~/Library/Caches/AIWorkspace/indexes/<project>/
```
This prevents large derived artifacts from polluting project memory repositories.
## Runtime State
Service logs, PID files, and health snapshots should be local machine state.
Target locations:
```text
~/Library/Logs/AIWorkspace/
~/Library/Application Support/AIWorkspace/runtime/
```
Runtime state should not be used as project memory.
## Secrets
Secrets should not be stored in context packs unless they are encrypted and explicitly supported.
Preferred approaches:
- macOS Keychain references;
- ignored local `.env` files;
- environment variables;
- enterprise-approved credential stores.
Documentation, manifests, and generated configs should refer to secret names, not secret values.

65
docs/concepts/overview.md Normal file
View File

@@ -0,0 +1,65 @@
# AIWorkspace Overview
AIWorkspace helps developers give AI tools the right project context at the right time.
It is designed for teams and individuals who use multiple AI clients—OpenCode, GitHub Copilot, Claude Code, Cursor, VS Code, or others—but do not want project knowledge scattered across chat histories, prompt files, and product repositories.
## What AIWorkspace Does
AIWorkspace provides a local platform to:
- connect communication and evidence sources;
- keep canonical project memory in human-readable Markdown;
- index that memory for fast retrieval;
- expose bounded context through MCP;
- generate AI client configuration when needed;
- run local services such as context servers, capture proxies, and inbox receivers;
- keep raw evidence separate from curated project knowledge.
## What AIWorkspace Is Not
AIWorkspace is not:
- a replacement for a product repository;
- a replacement for project documentation;
- a cloud memory system;
- a single-agent framework that forces one AI workflow;
- a place to store secrets in Git;
- a dumping ground for raw chat exports.
## Target User Flow
```text
Developer installs AIWorkspace
Registers or creates a project context pack
Connects evidence sources and local repos
AIWorkspace captures evidence and maintains indexes
AI clients ask AIWorkspace MCP for project context
Humans and agents update canonical project memory
```
## Key Concepts
| Concept | Meaning |
|---|---|
| AIWorkspace Core | The reusable app, CLI, scripts, MCP server, connectors, templates, and documentation. |
| User Registry | Local app configuration that remembers registered projects, paths, preferences, and service state. |
| Project Context Pack | A project-specific folder or repo containing canonical memory, connector config, prompts, and optional raw evidence. |
| Canonical Memory | Human-readable Markdown that represents current durable project knowledge. |
| Raw Evidence | Captured messages, screenshots, exports, logs, or documents before curation. |
| AI Client | Any tool that consumes context: OpenCode, Copilot, Claude Code, Cursor, VS Code, etc. |
## Design Goal
AIWorkspace should be plug-and-play for a new developer:
1. install the app and CLI;
2. register an existing context pack or create a new one;
3. connect sources such as Mattermost, Slack, tickets, email, calendar, or local folders;
4. let AIWorkspace detect repositories and services;
5. use any AI client with project-aware context through MCP.

View File

@@ -0,0 +1,127 @@
# Workspace vs Context Pack
AIWorkspace separates the installed product, user-local settings, and project memory.
This distinction keeps the system simple, shareable, and safe for unrelated projects such as `client-mobile` and `it-support`.
## Three-Layer Model
```text
AIWorkspace Core
reusable installed product
User/App Registry
local machine settings and registered context packs
Project Context Packs
one isolated context package per project, client, or workstream
```
## AIWorkspace Core
The core is the reusable product. It can be shared with a team.
Typical location during development:
```text
~/Developer/ai-workspace
```
It contains:
```text
apps/
scripts/
connectors/
templates/
docs/
examples/
```
It should not contain real project memory, captured messages, customer names, ticket details, or private prompts.
## User/App Registry
The registry is local to one developer machine. It records which context packs are available and which project is active.
On macOS, the target location is:
```text
~/Library/Application Support/AIWorkspace/
```
Example registry:
```json
{
"schema": "aiworkspace.projects.v1",
"default_project": "client-mobile",
"projects": [
{
"id": "client-mobile",
"context_path": "/Users/david/Developer/client-mobile-ai-context"
},
{
"id": "it-support",
"context_path": "/Users/david/Developer/it-support-ai-context"
}
]
}
```
The registry is not intended for Git.
## Project Context Pack
A context pack is the portable project-specific unit. It may be a private repo, a team-shared repo, or a local folder.
Example:
```text
client-mobile-ai-context/
aiw.project.json
knowledge/
inbox/
connectors/
agents/
prompts/
```
Each context pack owns its own:
- canonical memory;
- raw evidence;
- connector definitions;
- people/channel/system maps;
- prompts and agent instructions specific to that project.
## Why Not Store Everything In One Workspace Repo?
One developer can work on unrelated projects. A single monolithic workspace can accidentally mix:
- channels from different organizations;
- people maps from unrelated clients;
- raw evidence with different privacy rules;
- prompts that only make sense for one domain;
- AI behavior learned from one project but harmful in another.
Context packs prevent this by making project boundaries explicit.
## Optional Product Repository Binding
Product repositories should stay clean by default. If useful, a repo can contain a small optional binding file:
```text
.aiworkspace.json
```
Example:
```json
{
"project": "client-mobile",
"context_path": "/Users/david/Developer/client-mobile-ai-context"
}
```
This file should point to context. It should not contain memory, secrets, or raw evidence.

View File

@@ -30,6 +30,13 @@ python3 scripts/aiw/indexer.py build --profile fidelity
The `fidelity` profile is the first real project profile in this repo. New projects should follow the same shape but keep their own profile configuration and project memory isolated.
Create a new isolated profile with:
```bash
python3 scripts/aiw/profile.py create my-project --display-name "My Project"
python3 scripts/aiw/profile.py doctor --profile my-project
```
## Daily Use
1. Open the project knowledge vault in Obsidian or your Markdown editor.

121
docs/guides/quick-start.md Normal file
View File

@@ -0,0 +1,121 @@
# Quick Start
This guide describes the target production flow for a new developer using AIWorkspace.
The current repository still contains an active project-specific implementation. During refactor, these commands may be implemented incrementally.
## 1. Install AIWorkspace
Install the app, CLI, and local services from the reusable core repo:
```bash
git clone <team-ai-workspace-repo> ~/Developer/ai-workspace
cd ~/Developer/ai-workspace
```
The target CLI entry point is:
```bash
aiw
```
During development, commands may still be run through Python scripts.
## 2. Register Or Create A Project Context Pack
Register an existing context pack:
```bash
aiw project register ~/Developer/client-mobile-ai-context
```
Or create a new one:
```bash
aiw project create it-support --display-name "IT Support" --path ~/Developer/it-support-ai-context
```
The context pack contains project memory and project-specific connector configuration.
## 3. Connect Evidence Sources
Add only the connectors this project needs:
```bash
aiw connector add mattermost --project client-mobile
aiw connector add photos --project client-mobile
```
For another project, the connector set may be completely different:
```bash
aiw connector add tickets --project it-support
aiw connector add calendar --project it-support
```
## 4. Detect Local Repositories
AIWorkspace should help detect local repos and associate them with projects:
```bash
aiw repo scan ~/Developer --project it-support
```
For projects whose source code lives on another machine, the context pack should say so explicitly in `aiw.project.json`.
## 5. Start Context Services
Start services for the active project:
```bash
aiw services start --project client-mobile
aiw services status --project client-mobile
```
The service manager should start only capabilities configured for that project.
## 6. Configure AI Clients
Generate AI client integration files on demand:
```bash
aiw agent configure opencode --project client-mobile
aiw agent configure copilot --project client-mobile
aiw mcp config --client vscode
```
The preferred integration path is MCP, so AI clients can request current context without copying memory into product repositories.
## 7. Use AIWorkspace From Any AI Tool
The AI client should call MCP with a project id:
```json
{
"project": "client-mobile"
}
```
Example tool calls:
```text
project_current_context(project="client-mobile")
project_search_memory(project="it-support", query="printer onboarding")
communication_latest(project="client-mobile")
```
## 8. Keep Canonical Memory Clean
Project memory lives in the context pack:
```text
<context-pack>/knowledge/
```
Raw evidence lives separately:
```text
<context-pack>/inbox/
```
Do not treat raw captures, indexes, logs, or AI chat history as canonical memory.

View File

@@ -77,13 +77,23 @@ Source filters for profile-bounded reads. For example, a Mattermost profile can
## Adding A New Project
1. Copy `profiles/example/` to `profiles/<new-project>/`.
2. Create or point to a project knowledge vault.
3. Define services only for integrations the project actually uses.
4. Put connector secrets in ignored `.env` files.
1. Create the isolated profile and workspace:
```bash
python3 scripts/aiw/profile.py create my-project --display-name "My Project"
```
2. Define services only for integrations the project actually uses.
3. Put connector secrets in ignored `.env` files.
4. Validate the layout:
```bash
python3 scripts/aiw/profile.py doctor --profile my-project
```
5. Build the local index.
6. Connect AI clients through the MCP server.
## Migration Rule
Reusable code should accept a `--profile` argument and resolve paths through profile configuration. Avoid adding new hardcoded references to Fidelity, channel names, ticket prefixes, or company-specific folders in generic scripts.
Reusable code should accept a `--profile` argument and resolve paths through profile configuration. Avoid adding hardcoded references to Fidelity, channel names, ticket prefixes, or company-specific folders in generic scripts.

View File

@@ -0,0 +1,125 @@
# Project Manifest Reference
Each project context pack is described by an `aiw.project.json` file at the root of the context pack.
The manifest defines project identity, storage paths, connectors, code locations, and AI client preferences. Paths are relative to the context pack unless explicitly absolute.
## Minimal Manifest
```json
{
"schema": "aiworkspace.project.v1",
"id": "my-project",
"display_name": "My Project",
"knowledge_dir": "knowledge",
"inbox_dir": "inbox"
}
```
## Full Example
```json
{
"schema": "aiworkspace.project.v1",
"id": "client-mobile",
"display_name": "Client Mobile",
"description": "Project context pack for a mobile application team.",
"knowledge_dir": "knowledge",
"inbox_dir": "inbox",
"connectors_dir": "connectors",
"prompts_dir": "prompts",
"agents_dir": "agents",
"connectors": {
"mattermost": {
"enabled": true,
"config": "connectors/mattermost.json"
},
"photos": {
"enabled": true,
"config": "connectors/photos.json"
}
},
"code_locations": [
{
"name": "Development machine",
"availability": "remote-machine"
}
],
"ai_clients": {
"opencode": {
"enabled": true
},
"copilot": {
"enabled": true
}
}
}
```
## Fields
| Field | Required | Description |
|---|---:|---|
| `schema` | yes | Manifest schema version. Current target: `aiworkspace.project.v1`. |
| `id` | yes | Stable lowercase project id used by CLI and MCP. |
| `display_name` | yes | Human-friendly project name. |
| `description` | no | Short project description. |
| `knowledge_dir` | yes | Canonical Markdown memory directory. |
| `inbox_dir` | yes | Raw evidence directory. |
| `connectors_dir` | no | Directory for connector configs. Defaults to `connectors`. |
| `prompts_dir` | no | Directory for project prompts. Defaults to `prompts`. |
| `agents_dir` | no | Directory for project agent config. Defaults to `agents`. |
| `connectors` | no | Connector registry for this project. |
| `code_locations` | no | Local or remote code locations associated with this project. |
| `ai_clients` | no | AI client integration preferences. |
## Code Locations
Code may be local, remote, or unavailable on the current machine.
```json
{
"name": "iOS App",
"path": "/Users/dev/Developer/app-ios",
"role": "consumer-app",
"availability": "local"
}
```
Supported target availability values:
- `local`
- `remote-machine`
- `not-configured`
- `unknown`
## Connector Configs
Connector definitions should describe source behavior without storing secrets.
Example `connectors/mattermost.json`:
```json
{
"schema": "aiworkspace.connector.mattermost.v1",
"enabled": true,
"context_channels": [
"team-standup",
"team-code-review"
],
"secret_refs": {
"session": "keychain:aiworkspace/mattermost/session"
}
}
```
## Validation Rules
A production-ready manifest validator should check:
- required fields exist;
- project id is stable and path-safe;
- configured directories exist or can be created;
- connector config files exist when enabled;
- secrets are referenced, not embedded;
- code locations are explicit about local vs remote availability.

View File

@@ -38,9 +38,9 @@ Non-sensitive example profile showing how a new project can reuse the workspace
## Context
Project-specific durable context should live under `project-knowledge/03-context/`.
Project-specific durable context should live under `workspaces/example/project-knowledge/03-context/`.
People, decisions, daily notes, maps, and templates should live under the corresponding `project-knowledge/` folders.
People, decisions, daily notes, maps, and templates should live under the corresponding `workspaces/example/project-knowledge/` folders.
Do not place company secrets, raw exports, credentials, or private communication transcripts in the profile.

View File

@@ -26,13 +26,7 @@ It keeps Fidelity-specific context, integrations, commands, and skills separate
- Current high-signal channel: `fidelity-preguntas`
- Focused Mattermost context for standups/latest project reads should come from configured profile/environment channels, not hardcoded connector defaults. For this profile, the useful context-channel set is currently `fidelity-preguntas`, `fidelity-standup`, `fidelity-code-review`, `fidelity-interface-meetings-on-calendar-outlook-team-etc`, and `dm-david--jeff`; keep that list in local `.env` as `AIW_MATTERMOST_CONTEXT_CHANNELS` or an equivalent profile setup when using the reusable Mattermost reader.
Compatibility environment variables:
- `FIDELITY_MATTERMOST_SYNC_CMD`
- `FIDELITY_MATTERMOST_SYNC_INTERVAL_MINUTES`
- `FIDELITY_SLACK_EXPORT_PATH`
Generic variables should be preferred for new setup:
Generic workspace variables:
- `AIW_PROJECT_KNOWLEDGE_DIR`
- `AIW_MATTERMOST_SYNC_CMD`

View File

@@ -8,7 +8,7 @@ Generate a standup update for the active project profile.
## Required refresh
- At the start of the day, fetch or read refreshed Mattermost evidence before drafting. Prefer the local proxy mirror through `scripts/mattermost-proxy/read-context.py` when it exists; legacy sync output is fallback evidence.
- At the start of the day, fetch or read refreshed Mattermost evidence before drafting. Prefer the local proxy mirror through `scripts/mattermost-proxy/read-context.py` when it exists; use configured sync output only when a fresh API pull is required.
- Fetch focused standup evidence with `python3 scripts/mattermost-proxy/read-context.py --mode standup --today YYYY-MM-DD`; this reads previous-workday and today records from date-bucketed mirror files and should filter through the active profile's configured `AIW_MATTERMOST_CONTEXT_CHANNELS` when available. Do not read broad `latest.md` for standups unless the focused date-bucketed view is unavailable and you explicitly label the fallback as broad/noisy.
- If Mattermost refresh fails, say so internally and use only saved workspace memory with clear caution; do not invent fresher context.
- Do not skip communication refresh for standup just to reduce latency, because stale standups cost more time to correct later.

View File

@@ -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:

View File

@@ -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

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,74 @@ 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(),
}
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 +170,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 +187,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)),

View File

@@ -31,6 +31,16 @@ STATE_DIR = RUNTIME_DIR / "state"
DEFAULT_LOG_MAX_BYTES = 5 * 1024 * 1024
DEFAULT_LOG_BACKUPS = 3
DEFAULT_STOP_TIMEOUT_SECONDS = 5.0
DEFAULT_SERVICE_PATHS = [
"/opt/homebrew/bin",
"/opt/homebrew/sbin",
"/usr/local/bin",
"/usr/local/sbin",
"/usr/bin",
"/bin",
"/usr/sbin",
"/sbin",
]
@dataclass(frozen=True)
@@ -126,14 +136,35 @@ def resolve_workspace_path(raw: str) -> Path:
return path if path.is_absolute() else ROOT / path
def command_exists(command: str) -> bool:
def service_env(profile: str | None = None) -> dict[str, str]:
"""Return a robust environment for services launched from shells or GUI apps.
macOS login items do not reliably inherit the user's interactive shell PATH.
Homebrew-installed tools such as mitmdump commonly live under /opt/homebrew/bin
or /usr/local/bin, so make those paths available to both direct service
commands and nested scripts.
"""
env = os.environ.copy()
existing = [part for part in env.get("PATH", "").split(os.pathsep) if part]
merged: list[str] = []
for part in [*existing, *DEFAULT_SERVICE_PATHS]:
if part not in merged:
merged.append(part)
env["PATH"] = os.pathsep.join(merged)
env.setdefault("AIW_WORKSPACE_ROOT", str(ROOT))
if profile:
env.setdefault("AIW_PROJECT_PROFILE", profile)
return env
def command_exists(command: str, env: dict[str, str] | None = None) -> bool:
if not command:
return False
path = Path(command)
if path.is_absolute() or "/" in command:
resolved = resolve_workspace_path(command)
return resolved.exists() and os.access(resolved, os.X_OK)
return shutil_which(command) is not None
return shutil_which(command, env=env) is not None
def rotate_log_if_needed(path: Path, max_bytes: int = DEFAULT_LOG_MAX_BYTES, backups: int = DEFAULT_LOG_BACKUPS) -> None:
@@ -273,9 +304,10 @@ def start_service(profile: str, ref: ServiceRef, manifest: dict[str, Any], start
kind = ref.config.get("kind", "process")
command = ref.config.get("command") or []
env = service_env(profile)
if not command:
raise SystemExit(f"{ref.name} has no command")
if not command_exists(str(command[0])):
if not command_exists(str(command[0]), env=env):
raise SystemExit(f"{ref.name} command is not executable or not found: {command[0]}")
if kind != "app-launcher":
@@ -298,11 +330,11 @@ def start_service(profile: str, ref: ServiceRef, manifest: dict[str, Any], start
with path.open("ab") as log_file:
log_file.write(f"\n--- start {time.strftime('%Y-%m-%d %H:%M:%S')} ---\n".encode("utf-8"))
if kind == "app-launcher":
result = subprocess.run(command, cwd=ROOT, stdout=log_file, stderr=subprocess.STDOUT, check=False)
result = subprocess.run(command, cwd=ROOT, env=env, stdout=log_file, stderr=subprocess.STDOUT, check=False)
write_state(profile, ref.name, {"last_launch_exit": result.returncode, "launched_at": time.time()})
print(f"{ref.name}: launched (exit {result.returncode})")
else:
process = subprocess.Popen(command, cwd=ROOT, stdout=log_file, stderr=subprocess.STDOUT, start_new_session=True)
process = subprocess.Popen(command, cwd=ROOT, env=env, stdout=log_file, stderr=subprocess.STDOUT, start_new_session=True)
pid_file = pid_path(profile, ref.name)
pid_file.parent.mkdir(parents=True, exist_ok=True)
pid_file.write_text(str(process.pid) + "\n", encoding="utf-8")
@@ -385,6 +417,7 @@ def tail_log(profile: str, service: str, lines: int) -> None:
def doctor_report(profile: str, manifest: dict[str, Any]) -> dict[str, Any]:
errors = validate_manifest(manifest)
env = service_env(profile)
service_reports = []
for ref in service_items(manifest, include_disabled=True):
command = ref.config.get("command") or []
@@ -392,15 +425,15 @@ def doctor_report(profile: str, manifest: dict[str, Any]) -> dict[str, Any]:
doctor = ref.config.get("doctor") or {}
checks = []
for command_name in doctor.get("required_commands") or []:
checks.append({"type": "required_command", "name": command_name, "ok": command_exists(command_name)})
checks.append({"type": "required_command", "name": command_name, "ok": command_exists(command_name, env=env)})
for command_name in doctor.get("optional_commands") or []:
checks.append({"type": "optional_command", "name": command_name, "ok": command_exists(command_name)})
checks.append({"type": "optional_command", "name": command_name, "ok": command_exists(command_name, env=env)})
for raw_path in doctor.get("required_paths") or []:
checks.append({"type": "required_path", "name": str(raw_path), "ok": resolve_workspace_path(str(raw_path)).exists()})
for raw_path in doctor.get("optional_paths") or []:
checks.append({"type": "optional_path", "name": str(raw_path), "ok": resolve_workspace_path(str(raw_path)).exists()})
status = service_status(profile, ref)
status["command_ok"] = command_exists(first) if first else False
status["command_ok"] = command_exists(first, env=env) if first else False
status["checks"] = checks
service_reports.append(status)
return {
@@ -443,8 +476,8 @@ def run_doctor(profile: str, manifest: dict[str, Any], json_output: bool = False
print(f" {label} {check['name']}: {'ok' if check['ok'] else 'missing'}")
def shutil_which(command: str) -> str | None:
paths = os.environ.get("PATH", "").split(os.pathsep)
def shutil_which(command: str, env: dict[str, str] | None = None) -> str | None:
paths = (env or os.environ).get("PATH", "").split(os.pathsep)
for directory in paths:
candidate = Path(directory) / command
if candidate.exists() and os.access(candidate, os.X_OK):

View File

@@ -48,6 +48,25 @@ 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.assertEqual(result["paths"]["knowledge_dir"], "workspaces/demo/project-knowledge")
if __name__ == "__main__":
unittest.main()

View File

@@ -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()

View File

@@ -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

View File

@@ -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:

View File

@@ -82,6 +82,12 @@ Use the latest dated note for recent evidence, but promote durable facts into `0
---
## General Tools & Improvements
- [AI Workspace Improvements](../ai-workspace-improvements.md)
---
## Evidence Boundary
Inbox and generated files are evidence, not durable memory by default.
@@ -91,3 +97,4 @@ Inbox and generated files are evidence, not durable memory by default.
- `scripts/slack/generated/`
Promote only high-confidence, project-relevant facts into this vault.

View File

@@ -2,7 +2,7 @@
type: current
project: fidelity
status: active
updated: 2026-05-14
updated: 2026-05-21
tags:
- current-work
- fidelity
@@ -15,17 +15,17 @@ tags:
- Track REST migration findings
- Debug Discourse and AO issues
- Prepare better updates for the current manager or stakeholder through Mattermost
- Follow up on active tickets through `workspaces/fidelity/project-knowledge/02-work-items/`, especially branch maintenance for `PDIAP-15838` and implementation planning for `PDIAP-15836` / `PDIAP-12284`
- Follow up on active tickets through `workspaces/fidelity/project-knowledge/02-work-items/`, especially branch maintenance for `PDIAP-15838` and validation/review for `PDIAP-15836` / `PDIAP-12284`
- `PDIAP-15765` is done and `PDIAP-14859` is also done
- `PDIAP-15838` is Done from a Jira/status perspective after external review feedback was addressed, but its draft PR must remain unmerged and kept current with `main` until REST backend production readiness and the required REST-toggle consumer validation window allow merge
- `PDIAP-15836` has moved to In Progress. David found a possible minimal dismissal fix path that would not require removing `UIHostingController`, but Jeff directed David to do the `PDIAP-15836` dismissal/lifecycle work and `PDIAP-12284` UIKit-removal work in the same branch because both are disruptive enough to require consumer testing.
- `PDIAP-12284` remains paired with `PDIAP-15836`; plan the branch as combined UIKit-removal / SwiftUI lifecycle work rather than splitting the dismissal sequencing into an independently merged path unless direction changes.
- `PDIAP-15836` moved to In Review on May 21 after completing Bloom, Brokerage, and Youth validation. Previously it moved to In Progress on May 7.
- `PDIAP-12284` remains paired with `PDIAP-15836`; both moved to In Review on May 21. Plan the branch as combined UIKit-removal / SwiftUI lifecycle work.
- Current `PDIAP-12284` implementation direction is to explore XFlowViewMaker-owned global host-mode resolution rather than Fid4-owned per-flow host-mode mapping: SwiftUI host should remain the default, missing/unknown feature configuration should also default to SwiftUI, and `UIHostingController` should be selected only through an explicit temporary fallback flag.
- Keep host-mode resolution decoupled from XFlowSDK and app-specific LaunchDarkly/Flagship clients; XFlowSDK should consume a resolved host-mode decision, while Copilot/code inspection should confirm whether XFlowViewMaker can use existing `FeatureEnabling` / `Featuring` abstractions or needs a small dependency-injection path.
- May 12 implementation pass for `PDIAP-12284` / `PDIAP-15836` indicates the host-mode API is branch-local/new, keeps two enum types to preserve the XFlowViewMaker/XFlowSDK boundary, uses neutral `swiftUIHost` / `uiKitHost` cases, sets the feature flag key to `iOS-XflowUIKitHostEnabled`, defaults missing/false/unavailable values to SwiftUI, and removes `AnyView` from `buildFlow` internals while leaving it only at the existing public `FlowViewMaker` boundary.
- Current compile validation blocker for that branch appears to be dependency alignment: XFlowViewMaker's podspec resolves `XFlowSDK 2.8.48`, which does not expose the new host-mode API; XFlowSDK SwiftPM validation is also blocked in the current environment by missing `fidelity-src` registry configuration.
- After manual dependency/build handling, Fid4 now compiles with the latest `FlowViewBuilder` shape. Next validation should be runtime log evidence: confirm a representative flow selects the default SwiftUI host path, then simulate/force `iOS-XflowUIKitHostEnabled == true` to confirm the temporary UIKit host path and dismissal-completion behavior.
- `PDIAP-12284` moved to In Progress on May 12. Quy confirmed on May 13 that `PDIAP-12284` and `PDIAP-15836` can both remain In Progress together, so no immediate Jira restructuring is required.
- `PDIAP-12284` moved to In Review on May 21 after validation progress. Previously it moved to In Progress on May 12. Quy confirmed on May 13 that `PDIAP-12284` and `PDIAP-15836` can both remain in progress/review together.
- Latest `PDIAP-12284` / `PDIAP-15836` simulator log review suggests the SwiftUI-host dismissal sequence is firing in the intended order for the tested run: host disappearance is confirmed before `fireEndActivityDelegates`, delegate callbacks, and `activitySession` cleanup. Treat this as a promising validation run, not final story closure.
- May 13 AccountLink runtime validation looks good in both host modes: SwiftUI-default and forced UIKit-host paths selected the expected host branch and preserved dismissal sequencing, with delegate/session teardown after confirmed dismissal. Before push, clean temporary debug prints and consider moving dismissal-specific helper types out of `XFlowManager.swift` into a dedicated dismissal file.
- Follow-up SampleApp validation found a likely branch-introduced dismissal regression caused by host-mode mismatch: SampleApp uses `initialViewController(...)` / UIKit presentation, but the branch defaulted manager host mode to SwiftUI, so `endActivity` can take the SwiftUI subject path without a SwiftUI dismissal host subscriber. Fix direction should preserve existing behavior by keeping `initialViewController(...)` on the UIKit dismiss-completion path and `makeInitialFlowView(...)` on the SwiftUI lifecycle path.
@@ -37,9 +37,9 @@ tags:
- `HybridBrokerageAccountOpening`: Fully validated on both UIKit and SwiftUI hosts with full lifecycle/dismissal confirmed.
- `HybridYouthAccountOpening`: Fully validated on both UIKit and SwiftUI hosts with full lifecycle/dismissal confirmed, resolving the previous dismissal coverage gap.
- Remaining validation gaps on the combined PDIAP-12284 / PDIAP-15836 branch include:
- `accountlink` (P2P Transfer Flow): UIKit host validation is pending (SwiftUI host and dismissal are validated).
- `AODeeplinkLaunchView` (AO Deep-Link with Native/Web Toggle): untested on both branches (Native ON/OFF) and dismissal.
- Fid4 surface attribution for launch wrappers and common-launch flows.
- Session 017 on May 22 completed `accountlink` (P2P Transfer Flow) validation on UIKit host (confirming dismissal/cleanup) using a forced-path setup, and Copilot updated the validation checklist with a local testing force-path guide for FTP2PTransfer and FTTransfer.
- Draft PRs were opened for XFlowSDK and XFlowViewMaker on May 20 while validation continues.
- Keep the separate `HybridBrokerageAccountOpening` / `JointIdentityCheck` scenario out of `PDIAP-15765` scope unless later evidence proves it belongs there
- Include feature-flag planning for the broader UIKit-removal spike, including dismissal sequencing changes that affect consumers

View File

@@ -2,7 +2,7 @@
type: current-work-items
project: fidelity
status: active
updated: 2026-05-14
updated: 2026-05-21
tags:
- current-work
- work-item
@@ -24,11 +24,11 @@ Update the per-ticket files first when scope, status, sequencing, or communicati
- `PDIAP-15836` - Modernize dismissal delegate lifecycle sequencing for pure SwiftUI environment
Detail: `workspaces/fidelity/project-knowledge/02-work-items/pdiap-15836.md`
Current note: moved to In Progress on May 7. David found a possible minimal dismissal fix path that would not require removing `UIHostingController`, but Jeff directed David to do the dismissal/lifecycle work and `PDIAP-12284` UIKit-removal work in the same branch because both changes require consumer testing. A May 11 simulator log review suggests the tested SwiftUI-host path fires delegate/session-clear callbacks only after host-disappearance confirmation, but broader branch and consumer validation are still needed.
Current note: moved to In Review on May 21 after completing validation for Bloom, Brokerage, and Youth flows. Previously moved to In Progress on May 7. David found a possible minimal dismissal fix path that would not require removing `UIHostingController`, but Jeff directed David to do the dismissal/lifecycle work and `PDIAP-12284` UIKit-removal work in the same branch because both changes require consumer testing. A May 11 simulator log review suggests the tested SwiftUI-host path fires delegate/session-clear callbacks only after host-disappearance confirmation, but broader branch and consumer validation are still needed.
- `PDIAP-12284` - Remove UIKit wrapping from XFlow
Detail: `workspaces/fidelity/project-knowledge/02-work-items/pdiap-12284.md`
Current note: moved to In Progress on May 12 and should be handled with `PDIAP-15836` in the same branch. Quy confirmed both stories can remain In Progress together, so no immediate Jira restructuring is required. Current implementation direction is to avoid Fid4 per-flow host-mode mapping and instead evaluate XFlowViewMaker-owned global host-mode resolution, with SwiftUI as default and `UIHostingController` only as an explicit temporary fallback. May 12 implementation pass resolved earlier shape concerns with neutral enum names, `iOS-XflowUIKitHostEnabled`, SwiftUI default semantics, and `AnyView` removed from builder internals. Fid4 now compiles after manual dependency/build handling. May 13 AccountLink runtime validation looks good for both SwiftUI-default and forced UIKit-host paths. May 14 SampleApp work added explicit UIKit-host vs SwiftUI-host validation paths. May 20-21 status: sessions 004, 005, 008, and 009 confirmed full validation for HybridBloomAccountOpening (both hosts, full lifecycle/dismissal confirmed), HybridBrokerageAccountOpening (both hosts, full lifecycle confirmed), and HybridYouthAccountOpening (both hosts, full lifecycle/dismissal confirmed, resolving the dismissal gap). Remaining gaps are accountlink UIKit host, AODeeplinkLaunchView (both toggle branches and dismissal), and launch wrappers/common-launch flows. Draft PRs were opened for XFlowSDK and XFlowViewMaker on May 20 while validation continues.
Current note: moved to In Review on May 21 after completing validation for Bloom, Brokerage, and Youth flows. Previously moved to In Progress on May 12 and handled with `PDIAP-15836` in the same branch. Quy confirmed both stories can remain In Progress together, so no immediate Jira restructuring is required. Current implementation direction is to avoid Fid4 per-flow host-mode mapping and instead evaluate XFlowViewMaker-owned global host-mode resolution, with SwiftUI as default and `UIHostingController` only as an explicit temporary fallback. May 12 implementation pass resolved earlier shape concerns with neutral enum names, `iOS-XflowUIKitHostEnabled`, SwiftUI default semantics, and `AnyView` removed from builder internals. Fid4 now compiles after manual dependency/build handling. May 13 AccountLink runtime validation looks good for both SwiftUI-default and forced UIKit-host paths. May 14 SampleApp work added explicit UIKit-host vs SwiftUI-host validation paths. May 20-21 status: sessions 004, 005, 008, and 009 confirmed full validation for HybridBloomAccountOpening (both hosts, full lifecycle/dismissal confirmed), HybridBrokerageAccountOpening (both hosts, full lifecycle confirmed), and HybridYouthAccountOpening (both hosts, full lifecycle/dismissal confirmed, resolving the dismissal gap). Remaining gaps are accountlink UIKit host, AODeeplinkLaunchView (both toggle branches and dismissal), and launch wrappers/common-launch flows. Draft PRs were opened for XFlowSDK and XFlowViewMaker on May 20 while validation continues.
## Backlog / Future Reference

View File

@@ -1,14 +1,14 @@
---
type: work-item
project: fidelity
status: in-progress
status: in-review
ticket: PDIAP-12284
title: "Remove UIKit wrapping from XFlow"
systems: [xflowsdk, xflowviewmaker]
workstreams: [xflow-swiftui-migration, consumer-integration]
people: [jeff-dewitte]
related: [pdiap-15836, pdiap-15838]
updated: 2026-05-14
updated: 2026-05-21
tags:
- work-item
- fidelity
@@ -20,6 +20,7 @@ tags:
## Status
- Moved to In Review on May 21 after completing Bloom, Brokerage, and Youth validation.
- Reopened after rollback and moved to In Progress on May 12.
- Jeff directed David to do this UIKit-removal work and `PDIAP-15836` dismissal/lifecycle work in the same branch because both are disruptive enough to require consumer testing.
- Current implementation direction is to avoid Fid4-owned per-flow host-mode mapping and evaluate XFlowViewMaker-owned global host-mode resolution.
@@ -40,7 +41,7 @@ tags:
- `HybridBloomAccountOpening` (Affiliation-Gated AO Modal): Fully validated in both UIKit host (Session 008) and SwiftUI host (Session 009) with full dismissal/cleanup confirmed.
- `HybridBrokerageAccountOpening`: Fully validated on both UIKit and SwiftUI hosts with full lifecycle/dismissal confirmed.
- `HybridYouthAccountOpening`: Fully validated on both UIKit and SwiftUI hosts with full lifecycle/dismissal confirmed, resolving the previous dismissal coverage gap.
- Remaining gaps: `accountlink` UIKit host, `AODeeplinkLaunchView` (both toggle branches and dismissal), and launch wrappers/common-launch flows. Draft PRs were opened for XFlowSDK and XFlowViewMaker on May 20 while validation continues.
- Remaining gaps: `AODeeplinkLaunchView` (both toggle branches and dismissal) and launch wrappers/common-launch flows. `accountlink` UIKit host validation was completed on May 22 (Session 017) using a forced-path setup, and Copilot updated the entry-point validation checklist with a local testing force-path guide. Draft PRs were opened for XFlowSDK and XFlowViewMaker on May 20 while validation continues.
---

View File

@@ -1,14 +1,14 @@
---
type: work-item
project: fidelity
status: in-progress
status: in-review
ticket: PDIAP-15836
title: "Modernize dismissal delegate lifecycle sequencing for pure SwiftUI environment"
systems: [xflowsdk, xflowviewmaker, ftframeworks]
workstreams: [xflow-swiftui-migration, consumer-integration]
people: [jeff-dewitte]
related: [pdiap-14859, pdiap-12284, pdiap-15838]
updated: 2026-05-14
updated: 2026-05-21
tags:
- work-item
- fidelity
@@ -19,6 +19,7 @@ tags:
## Status
- Moved to In Review on May 21 after completing Bloom, Brokerage, and Youth validation.
- Moved to In Progress on May 7.
- David found a possible minimal dismissal fix path that would not require removing `UIHostingController`, and was validating it.
- Jeff directed David to do this dismissal/lifecycle work together with `PDIAP-12284` in the same branch because both changes are disruptive enough to require consumer testing.

View File

@@ -21,11 +21,18 @@ tags:
- Screenshot evidence from the XFlow entry-point validation checklist indicates `HybridBloomAccountOpening` has both UIKit-host and SwiftUI-host validation marked complete, with full dismissal/cleanup confirmed for both host modes.
- `HybridBrokerageAccountOpening` and `HybridYouthAccountOpening` are also marked as validated for SwiftUI host, UIKit host, and full lifecycle/dismissal coverage in the checklist.
- The `accountLink` P2P transfer flow is marked validated for SwiftUI host and dismissal on SwiftUI host, but UIKit-host validation is still pending; the checklist notes Fid4 surface attribution is unclear and may go through `XFlowCommonLaunchView` or direct FTTransfer builder.
- `ADDeepLinkLaunchView` remains untested in the checklist for native-on, native-off, and dismissal paths.
- The `accountLink` P2P transfer flow uses `fidelity://p2ptransfer?id=testAlias` (via `FTP2PDeepLinks.swift`). Note: authenticate-then-custom (user must be logged in).
- Common-launch routes use `XFlowCommonLaunchView`:
- `cd` (Certificate of Deposit): `fidelity://XFlowHost?flowId=cd` (defaults to cd without `flowId`) or `https://www.fidelity.com/u/account/feature/cd`.
- `psta` (Penny Stock Agreement): `fidelity://XFlowHost?flowId=pst` or `fidelity://PstFeature`.
- `AODeepLinkLaunchView` remains untested in the checklist for native-on, native-off, and dismissal paths.
## Validation To Run
- Complete UIKit-host validation for `accountLink` / P2P transfer flow.
- Validate `ADDeepLinkLaunchView` for both native toggle branches and dismissal behavior.
- Complete UIKit-host validation for `accountLink` / P2P transfer flow using `fidelity://p2ptransfer?id=testAlias`.
- Validate common-launch routes:
- `cd`: `fidelity://XFlowHost?flowId=cd&workitem=smoke_cd_01`
- `psta`: `fidelity://XFlowHost?flowId=pst&workitem=smoke_pst_01`
- Validate `AODeepLinkLaunchView` for both native toggle branches and dismissal behavior.
- Keep using the checklist as a general entry-point guide rather than a session-by-session log.

View File

@@ -0,0 +1,29 @@
---
type: daily
project: fidelity
date: 2026-05-22
updated: 2026-05-22
focus:
- PDIAP-12284
- PDIAP-15836
work-items:
- PDIAP-12284
- PDIAP-15836
blockers: []
tags:
- daily
- fidelity
---
# 2026-05-22
## Findings
- Session 017 validated the deep-link-driven `accountlink` lifecycle on the UIKit host using debug force-path environment variables (`XFLOW_SMOKE_FORCE_LINK_RECIPIENT=1`, `XFLOW_SMOKE_FORCE_P2P_XFLOW=1`) and `iOS_Native_Link` OFF.
- Verified that the dismissal completion sequence operates correctly on the UIKit host mode, ensuring cleanup and callback execution via `activitySessionCleared`.
- Copilot updated the `xflow-entrypoint-validation-checklist.md` (adding 39 lines starting at line 317) with a dedicated "Consumer Framework Force-Path Guide" detailing code modifications and debug overrides required in `FTP2PTransfer` (`PaymentsRootViewModel.swift`, `PaymentsExperienceViewModel+ErrorView.swift`, `P2PTransferNetworkClient.swift`) and `FTTransfer` (`BankInformationViewModel.swift`).
- Reviewed secret scanning alerts: The 1 Critical finding in SecureTrak with a 72-hour deadline belongs to the backend `ap141020-xflow-model-state` repository, not iOS. The iOS repository ([pr100660-xflow-for-ios](file:///Users/david/Developer/fidelity-ai-workspace/workspaces/fidelity/project-knowledge/03-context/systems/xflowsdk.md)) alerts are just the two Google API Key findings in `MockPageWithHiddenToogle` on an orphan branch (backend-provided token flagged as public leak by GitHub) and carry no immediate urgency.
## Validation To Run
- Continue validating remaining untested paths (e.g., `AODeepLinkLaunchView` native branches, common-launch routes like `cd` and `psta`) as planned.

View File

@@ -0,0 +1,45 @@
# AI Workspace Improvements
This note tracks general improvements, integrations, and architectural upgrades for the AI Workspace (AIW) tools and proxies, independent of specific project domains.
---
## 1. Mattermost Integration Enhancements
### A. Keyless Authentication via Local Cookies (macOS Keychain)
* **Goal:** Eliminate the need to manually configure and rotate `MATTERMOST_TOKEN` in local `.env` files.
* **Mechanism:**
* Read the sandboxed Chrome/Electron cookies database directly:
`~/Library/Containers/Mattermost.Desktop/Data/Library/Application Support/Mattermost/Cookies`
* Extract the `encrypted_value` for the `MMAUTHTOKEN` cookie.
* Retrieve the encryption key from the macOS Keychain under the service `"Mattermost Safe Storage"` and account `"Mattermost"`.
* Decrypt using PBKDF2 (salt: `b"saltysalt"`, iterations: `1003`) and AES-128-CBC.
* **Database Version 24+ Compatibility:** Strip the 32-byte domain SHA-256 prefix from the decrypted block and remove PKCS7 padding to reveal the raw 26-character token.
* **Benefits:** Zero-configuration authentication, inherits SSO/MFA validation from the desktop app, runs in under 50ms, and requires no background proxy daemon.
### B. Interactive Session Cache (Alternative)
* **Goal:** Dynamically obtain the session token without saving the master password on disk for environments using basic username/password authentication.
* **Mechanism:**
* Request the password interactively in the terminal using Python's `getpass` library or fetch it from the macOS Keychain.
* Call `POST /api/v4/users/login` to authenticate.
* Save **only the returned session token** to a local temporary cache file (e.g., `.mattermost_session`).
* Check the cache validity on startup before prompting the user again.
---
## 2. Desktop Automation & Control (AI Message Drafting)
### A. Native AppleScript Automation (macOS)
* **Goal:** Allow the AI agent to draft messages directly in the official Mattermost Desktop UI for final user review before sending.
* **Mechanism:**
* Execute AppleScript commands from the terminal to focus the Mattermost Desktop application.
* Copy the AI-generated draft to the clipboard.
* Simulate keyboard shortcuts (`Cmd+V`) to paste the draft into the active chat input field.
* **Benefits:** No API keys required, completely safe (user retains final send control), and works natively on macOS without external dependencies.
### B. Chromium CDP Attachment (Playwright / Puppeteer)
* **Goal:** Programmatic UI interaction without launching a new headless browser.
* **Mechanism:**
* Launch Mattermost Desktop with the `--remote-debugging-port=9222` flag.
* Attach Playwright to the active session using `connect_over_cdp("http://localhost:9222")`.
* Execute Javascript inside the page context (e.g., `page.evaluate(...)` or `page.eval_on_selector(...)`) to input text or trigger actions even when the window is minimized (bypassing Chromium CPU/timer throttling).