Compare commits
7 Commits
ad230e1abe
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 0980e6695a | |||
| abab9bb9b6 | |||
| 60868b9c96 | |||
| 7594e8ebc9 | |||
| d01ee1ac1a | |||
| cc716f8f7e | |||
| e03518e507 |
24
DECOMMISSIONED.md
Normal file
24
DECOMMISSIONED.md
Normal 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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
31
docs/README.md
Normal file
31
docs/README.md
Normal 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.
|
||||
98
docs/architecture/storage-model.md
Normal file
98
docs/architecture/storage-model.md
Normal 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
65
docs/concepts/overview.md
Normal 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.
|
||||
127
docs/concepts/workspace-vs-context-pack.md
Normal file
127
docs/concepts/workspace-vs-context-pack.md
Normal 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.
|
||||
121
docs/guides/quick-start.md
Normal file
121
docs/guides/quick-start.md
Normal 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.
|
||||
125
docs/reference/project-manifest.md
Normal file
125
docs/reference/project-manifest.md
Normal 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.
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -155,8 +155,6 @@ def doctor(profile: str, root: Path | None = None) -> dict[str, Any]:
|
||||
"start_here_exists": (knowledge / "00-start" / "start-here.md").is_file(),
|
||||
"current_work_exists": (knowledge / "01-current" / "current-work.md").is_file(),
|
||||
"work_items_exists": (knowledge / "01-current" / "work-items.md").is_file(),
|
||||
"root_project_knowledge_absent": not (base / "project-knowledge").exists(),
|
||||
"root_ai_inbox_absent": not (base / "ai" / "inbox").exists(),
|
||||
}
|
||||
return {"profile": profile, "ok": all(checks.values()), "checks": checks, "paths": {"knowledge_dir": relative_to_root(knowledge, root=base), "inbox_dir": relative_to_root(inbox, root=base), "index_dir": relative_to_root(index_dir(profile, root=base), root=base)}}
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -65,17 +65,7 @@ class ProfileTests(unittest.TestCase):
|
||||
result = profile.doctor("demo", root=root)
|
||||
|
||||
self.assertTrue(result["ok"])
|
||||
self.assertTrue(result["checks"]["root_project_knowledge_absent"])
|
||||
|
||||
def test_doctor_rejects_root_level_project_data_dirs(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
profile.create_profile("demo", root=root)
|
||||
(root / "project-knowledge").mkdir()
|
||||
result = profile.doctor("demo", root=root)
|
||||
|
||||
self.assertFalse(result["ok"])
|
||||
self.assertFalse(result["checks"]["root_project_knowledge_absent"])
|
||||
self.assertEqual(result["paths"]["knowledge_dir"], "workspaces/demo/project-knowledge")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
29
workspaces/fidelity/project-knowledge/06-daily/2026-05-22.md
Normal file
29
workspaces/fidelity/project-knowledge/06-daily/2026-05-22.md
Normal 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.
|
||||
@@ -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).
|
||||
Reference in New Issue
Block a user