Compare commits

..

7 Commits

22 changed files with 886 additions and 67 deletions

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.

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

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.

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

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

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

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

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

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).