Compare commits

...

30 Commits

Author SHA1 Message Date
1ad707373a Add daily logs and templates for project fidelity
- Created daily log entries for May 13, 14, 18, 19, 20, and 21, capturing work done, findings, and next steps.
- Established a daily logs index for easy navigation of daily notes.
- Developed templates for daily logs, decisions, meeting notes, people, systems, and work items to standardize documentation.
- Introduced base files for filtering and displaying various types of project knowledge, including daily notes, decisions, people, systems, work items, and workstreams.
- Added maps for current work, fidelity apps, and fidelity domain to enhance project navigation and context.
2026-05-21 12:28:07 -06:00
7cbb49134a feat: update profile path resolution and enhance scripts for improved project adaptability 2026-05-21 10:43:44 -06:00
f0d3cd4ce9 feat: enhance profile path resolution and add example profiles for better project adaptability 2026-05-21 10:21:52 -06:00
fb8a6ba2d9 feat: add comprehensive documentation for AI Workspace, including architecture, memory model, profiles, services, and security guidelines 2026-05-21 09:32:09 -06:00
e0069fd8c6 feat: implement local indexer for project-knowledge and add memory hybrid search functionality 2026-05-21 09:13:07 -06:00
fc2abda588 feat: update validation notes and open draft PRs for PDIAP-12284 and PDIAP-15836 2026-05-21 08:39:33 -06:00
7da22da168 feat: add DMG build script and enhance README with installation instructions and Start at Login feature 2026-05-20 16:41:18 -06:00
b7ce929c50 feat: add one-step installer script and enhance README with installation instructions 2026-05-20 16:24:47 -06:00
4000747641 feat: enhance AI Workspace Menu Bar App with packaging scripts, login management, and service status improvements 2026-05-20 16:11:45 -06:00
ab36e4b465 feat: add initial implementation of AI Workspace macOS Menu Bar App with service management functionality 2026-05-20 15:52:58 -06:00
8c58210c0c feat: enhance service status reporting with JSON output and add related tests 2026-05-20 15:29:21 -06:00
b21889c4ab feat: add AI Workspace Menu Bar App design and enhance MCP server with resource definitions and read functionality 2026-05-20 15:22:37 -06:00
cfd61bdee3 feat: add AI Workspace context MCP configuration and enhance communication channel filtering in server 2026-05-20 15:16:41 -06:00
d3e909d39e feat: implement AI Workspace context MCP server with read-only access and add related tests 2026-05-20 14:57:54 -06:00
9f8d3b975f feat: enhance service manager with manifest validation, logging rotation, and command existence checks 2026-05-20 14:51:09 -06:00
1121433db8 feat: implement AI Workspace service manager with lifecycle control for local services 2026-05-20 14:43:52 -06:00
eb11bb9442 feat: update Mattermost integration documentation with MCP support guidelines and focused reader mode instructions 2026-05-20 14:30:43 -06:00
fdbf52f811 feat: update validation notes and add daily summary for ongoing PDIAP-12284 and PDIAP-15836 work 2026-05-20 08:03:33 -06:00
ee35f70c3e feat: enhance Mattermost integration with focused context for standups and improved channel filtering 2026-05-20 07:47:19 -06:00
3d4da1919a feat: update Mattermost integration to prefer local proxy mirror evidence and enhance context retrieval methods 2026-05-20 07:11:06 -06:00
e081360a84 feat: enhance Mattermost mirror with direct-message and group DM labeling for improved clarity 2026-05-19 16:35:24 -06:00
d318701899 feat: enhance Mattermost mirror structure with channels, by-date, and thread directories for improved organization and analysis 2026-05-19 16:32:05 -06:00
3816487bec feat: update Mattermost proxy configuration and documentation for improved clarity and default behavior 2026-05-19 16:15:48 -06:00
b886c61afd feat: enhance Mattermost proxy with improved post ID deduplication and file tracking 2026-05-19 16:03:16 -06:00
9dd731f758 feat: implement Mattermost proxy mirror with configuration and scripts for capturing traffic 2026-05-19 15:57:09 -06:00
73166b585f feat: add tooling and documentation for archiving Discourse content via Charles Proxy .chlsx sessions. 2026-05-19 14:28:45 -06:00
f726814811 feat: reorganize IA improvements documentation for clarity and structure 2026-05-18 07:39:16 -06:00
8950cfcdf0 feat: update iPhone photo inbox scripts with environment file support and refactor batch handling 2026-05-15 08:43:19 -06:00
456a4c3381 feat: add clipboard file handling for Mattermost and implement batch debounce functionality 2026-05-15 07:54:01 -06:00
7fc4320f46 feat: expand validation scope for PDIAP-12284 and PDIAP-15836, including broader entry and dismissal testing in both UIKit and SwiftUI modes 2026-05-14 16:13:48 -06:00
256 changed files with 6620 additions and 1038 deletions

8
.agents/mcp_config.json Normal file
View File

@@ -0,0 +1,8 @@
{
"mcpServers": {
"aiw-context-mcp": {
"url": "http://127.0.0.1:8765/mcp",
"serverUrl": "http://127.0.0.1:8765/mcp"
}
}
}

View File

@@ -19,10 +19,10 @@ Use this skill when the user wants a prompt for another AI assistant, GitHub Cop
- test strategy - test strategy
- story/PR drafting - story/PR drafting
2. Pull only the relevant context: 2. Pull only the relevant context:
- `project-knowledge/02-work-items/` for ticket-specific context - `workspaces/fidelity/project-knowledge/02-work-items/` for ticket-specific context
- `project-knowledge/03-context/systems/` for component context - `workspaces/fidelity/project-knowledge/03-context/systems/` for component context
- `project-knowledge/03-context/workstreams/` for recurring constraints - `workspaces/fidelity/project-knowledge/03-context/workstreams/` for recurring constraints
- `project-knowledge/03-context/ios/` for Swift/iOS guidance - `workspaces/fidelity/project-knowledge/03-context/ios/` for Swift/iOS guidance
3. Make the prompt self-contained. 3. Make the prompt self-contained.
4. Tell the target AI what to inspect before acting. 4. Tell the target AI what to inspect before acting.
5. State constraints, non-goals, and validation expectations. 5. State constraints, non-goals, and validation expectations.

View File

@@ -12,8 +12,8 @@ Use this skill for Swift, SwiftUI, iOS architecture, concurrency, testing, or de
## Workflow ## Workflow
1. Identify whether the question is general Swift/iOS or Fidelity-specific. 1. Identify whether the question is general Swift/iOS or Fidelity-specific.
2. Read `project-knowledge/03-context/ios/current-practices.md` for currentness rules. 2. Read `workspaces/fidelity/project-knowledge/03-context/ios/current-practices.md` for currentness rules.
3. Read `project-knowledge/03-context/ios/project-swift-guidance.md` when the answer may affect XFlow, Fid4, XFlowViewMaker, FTFrameworks, feature flags, or consumer validation. 3. Read `workspaces/fidelity/project-knowledge/03-context/ios/project-swift-guidance.md` when the answer may affect XFlow, Fid4, XFlowViewMaker, FTFrameworks, feature flags, or consumer validation.
4. If the answer depends on current Apple APIs, Xcode versions, dependency tooling, package-manager behavior, testing frameworks, or migration guidance, verify with official/primary documentation before making strong claims. 4. If the answer depends on current Apple APIs, Xcode versions, dependency tooling, package-manager behavior, testing frameworks, or migration guidance, verify with official/primary documentation before making strong claims.
5. Separate: 5. Separate:
- current best practice - current best practice

View File

@@ -8,9 +8,9 @@ Read:
@core/README.md @core/README.md
@core/memory/operational-memory.md @core/memory/operational-memory.md
@project-knowledge/01-current/current-work.md @workspaces/fidelity/project-knowledge/01-current/current-work.md
@project-knowledge/01-current/work-items.md @workspaces/fidelity/project-knowledge/01-current/work-items.md
@project-knowledge/03-context/project.md @workspaces/fidelity/project-knowledge/03-context/project.md
@agent-memory/workflows/ai-to-ai-prompting.md @agent-memory/workflows/ai-to-ai-prompting.md
Read active profile, if present: Read active profile, if present:
@@ -23,13 +23,13 @@ Relevant active work item files, if available:
import re import re
from pathlib import Path from pathlib import Path
summary = Path("project-knowledge/01-current/work-items.md") summary = Path("workspaces/fidelity/project-knowledge/01-current/work-items.md")
if not summary.is_file(): if not summary.is_file():
print("No work item files available.") print("No work item files available.")
raise SystemExit(0) raise SystemExit(0)
text = summary.read_text() text = summary.read_text()
paths = re.findall(r"Detail: `(project-knowledge/02-work-items/[^`]+)`", text) paths = re.findall(r"Detail: `(workspaces/fidelity/project-knowledge/02-work-items/[^`]+)`", text)
if not paths: if not paths:
print("No work item files available.") print("No work item files available.")
raise SystemExit(0) raise SystemExit(0)

View File

@@ -24,12 +24,12 @@ Read:
@core/README.md @core/README.md
@core/memory/operational-memory.md @core/memory/operational-memory.md
@core/integrations/communication-model.md @core/integrations/communication-model.md
@project-knowledge/01-current/current-work.md @workspaces/fidelity/project-knowledge/01-current/current-work.md
@project-knowledge/01-current/work-items.md @workspaces/fidelity/project-knowledge/01-current/work-items.md
@project-knowledge/03-context/project.md @workspaces/fidelity/project-knowledge/03-context/project.md
@agent-memory/memory/context-maintenance.md @agent-memory/memory/context-maintenance.md
@project-knowledge/04-people/index.md @workspaces/fidelity/project-knowledge/04-people/index.md
@project-knowledge/04-people/manager.md @workspaces/fidelity/project-knowledge/04-people/manager.md
Imported summary, if present: Imported summary, if present:
@@ -44,11 +44,11 @@ Instructions:
- treat the archive as historical evidence - treat the archive as historical evidence
- promote durable project-relevant context automatically when confidence is high - promote durable project-relevant context automatically when confidence is high
- prefer durable role/person associations, recurring architecture patterns, repeated work-item references, approval/scope history, and process lessons - prefer durable role/person associations, recurring architecture patterns, repeated work-item references, approval/scope history, and process lessons
- create or update `project-knowledge/04-people/*.md` when a human repeatedly affects project flow - create or update `workspaces/fidelity/project-knowledge/04-people/*.md` when a human repeatedly affects project flow
- avoid promoting outdated daily status unless it changes current understanding - avoid promoting outdated daily status unless it changes current understanding
- update existing memory when the archive clarifies or corrects it - update existing memory when the archive clarifies or corrects it
- keep ambiguous or likely outdated facts as archive-only context - keep ambiguous or likely outdated facts as archive-only context
- write promoted memory to `project-knowledge/` - write promoted memory to `workspaces/fidelity/project-knowledge/`
Return: Return:

View File

@@ -19,24 +19,25 @@ Read:
@core/README.md @core/README.md
@core/memory/operational-memory.md @core/memory/operational-memory.md
@core/integrations/communication-model.md @core/integrations/communication-model.md
@project-knowledge/01-current/current-work.md @workspaces/fidelity/project-knowledge/01-current/current-work.md
@project-knowledge/01-current/work-items.md @workspaces/fidelity/project-knowledge/01-current/work-items.md
@project-knowledge/03-context/project.md @workspaces/fidelity/project-knowledge/03-context/project.md
@agent-memory/memory/context-maintenance.md @agent-memory/memory/context-maintenance.md
Fresh communication evidence: Fresh communication evidence:
!`if [ -s ai/inbox/mattermost-latest.md ]; then cat ai/inbox/mattermost-latest.md; elif [ -s scripts/mattermost/generated/mattermost_context.jsonl ]; then cat scripts/mattermost/generated/mattermost_context.jsonl; else echo "No communication evidence available."; fi` !`python3 scripts/mattermost-proxy/read-context.py --mode latest`
Instructions: Instructions:
- if the sync command failed, stop and do not edit workspace memory - if the sync command failed, stop and do not edit workspace memory
- prefer local proxy mirror evidence when present; legacy sync output is fallback evidence
- treat connector output as evidence, not automatically as project truth - treat connector output as evidence, not automatically as project truth
- promote only explicit, project-relevant, high-confidence facts - promote only explicit, project-relevant, high-confidence facts
- default destination is `project-knowledge/06-daily/$(date +%F).md` - default destination is `workspaces/fidelity/project-knowledge/06-daily/$(date +%F).md`
- update `project-knowledge/01-current/current-work.md` only for facts that materially change the active work window - update `workspaces/fidelity/project-knowledge/01-current/current-work.md` only for facts that materially change the active work window
- update `project-knowledge/02-work-items/*.md` for explicit work-item IDs, approved titles, points, scope, and status notes - update `workspaces/fidelity/project-knowledge/02-work-items/*.md` for explicit work-item IDs, approved titles, points, scope, and status notes
- keep `project-knowledge/01-current/work-items.md` aligned as the compact summary of active work items - keep `workspaces/fidelity/project-knowledge/01-current/work-items.md` aligned as the compact summary of active work items
- do not write tooling noise, sync status, or generic chat chatter into project memory - do not write tooling noise, sync status, or generic chat chatter into project memory
- if a fact is ambiguous, skip it rather than asking what to do - if a fact is ambiguous, skip it rather than asking what to do

View File

@@ -9,14 +9,14 @@ Read:
@core/README.md @core/README.md
@core/memory/operational-memory.md @core/memory/operational-memory.md
@prompts/copilot-prompt.md @prompts/copilot-prompt.md
@project-knowledge/01-current/current-work.md @workspaces/fidelity/project-knowledge/01-current/current-work.md
@project-knowledge/01-current/work-items.md @workspaces/fidelity/project-knowledge/01-current/work-items.md
@project-knowledge/03-context/project.md @workspaces/fidelity/project-knowledge/03-context/project.md
@agent-memory/workflows/ai-to-ai-prompting.md @agent-memory/workflows/ai-to-ai-prompting.md
@project-knowledge/03-context/ios/index.md @workspaces/fidelity/project-knowledge/03-context/ios/index.md
@project-knowledge/03-context/ios/project-swift-guidance.md @workspaces/fidelity/project-knowledge/03-context/ios/project-swift-guidance.md
@project-knowledge/03-context/systems/index.md @workspaces/fidelity/project-knowledge/03-context/systems/index.md
@project-knowledge/03-context/workstreams/index.md @workspaces/fidelity/project-knowledge/03-context/workstreams/index.md
Active profile, if present: Active profile, if present:
@@ -28,13 +28,13 @@ Detailed active work item files, if available:
import re import re
from pathlib import Path from pathlib import Path
summary = Path("project-knowledge/01-current/work-items.md") summary = Path("workspaces/fidelity/project-knowledge/01-current/work-items.md")
if not summary.is_file(): if not summary.is_file():
print("No work item files available.") print("No work item files available.")
raise SystemExit(0) raise SystemExit(0)
text = summary.read_text() text = summary.read_text()
paths = re.findall(r"Detail: `(project-knowledge/02-work-items/[^`]+)`", text) paths = re.findall(r"Detail: `(workspaces/fidelity/project-knowledge/02-work-items/[^`]+)`", text)
if not paths: if not paths:
print("No work item files available.") print("No work item files available.")
raise SystemExit(0) raise SystemExit(0)

View File

@@ -11,14 +11,14 @@ Use these files as the baseline context:
@agent-memory/behavior/learning-sessions.md @agent-memory/behavior/learning-sessions.md
@agent-memory/memory/promotion-rules.md @agent-memory/memory/promotion-rules.md
@agent-memory/integrations/technical-verification.md @agent-memory/integrations/technical-verification.md
@project-knowledge/00-start/start-here.md @workspaces/fidelity/project-knowledge/00-start/start-here.md
@project-knowledge/01-current/current-work.md @workspaces/fidelity/project-knowledge/01-current/current-work.md
@project-knowledge/01-current/work-items.md @workspaces/fidelity/project-knowledge/01-current/work-items.md
@project-knowledge/03-context/project.md @workspaces/fidelity/project-knowledge/03-context/project.md
@project-knowledge/03-context/process/communication.md @workspaces/fidelity/project-knowledge/03-context/process/communication.md
@project-knowledge/03-context/ios/index.md @workspaces/fidelity/project-knowledge/03-context/ios/index.md
@project-knowledge/04-people/manager.md @workspaces/fidelity/project-knowledge/04-people/manager.md
@project-knowledge/04-people/index.md @workspaces/fidelity/project-knowledge/04-people/index.md
Today's date: Today's date:
@@ -26,11 +26,11 @@ Today's date:
Today's canonical daily note, if present: Today's canonical daily note, if present:
!`if [ -f project-knowledge/06-daily/$(date +%F).md ]; then cat project-knowledge/06-daily/$(date +%F).md; else echo "No daily note exists for today yet."; fi` !`if [ -f workspaces/fidelity/project-knowledge/06-daily/$(date +%F).md ]; then cat workspaces/fidelity/project-knowledge/06-daily/$(date +%F).md; else echo "No daily note exists for today yet."; fi`
Latest daily notes available: Latest daily notes available:
!`if [ -d project-knowledge/06-daily ]; then ls -1 project-knowledge/06-daily 2>/dev/null | sort | tail -n 3; else echo "No daily notes directory available."; fi` !`if [ -d workspaces/fidelity/project-knowledge/06-daily ]; then ls -1 workspaces/fidelity/project-knowledge/06-daily 2>/dev/null | sort | tail -n 3; else echo "No daily notes directory available."; fi`
Detailed active work item files, if available: Detailed active work item files, if available:
@@ -38,13 +38,13 @@ Detailed active work item files, if available:
import re import re
from pathlib import Path from pathlib import Path
summary = Path("project-knowledge/01-current/work-items.md") summary = Path("workspaces/fidelity/project-knowledge/01-current/work-items.md")
if not summary.is_file(): if not summary.is_file():
print("No work item files available.") print("No work item files available.")
raise SystemExit(0) raise SystemExit(0)
text = summary.read_text() text = summary.read_text()
paths = re.findall(r"Detail: `(project-knowledge/02-work-items/[^`]+)`", text) paths = re.findall(r"Detail: `(workspaces/fidelity/project-knowledge/02-work-items/[^`]+)`", text)
if not paths: if not paths:
print("No work item files available.") print("No work item files available.")
raise SystemExit(0) raise SystemExit(0)
@@ -59,7 +59,7 @@ PY`
Latest Mattermost context, if available: Latest Mattermost context, if available:
!`if [ -s ai/inbox/mattermost-latest.md ]; then cat ai/inbox/mattermost-latest.md; elif [ -s scripts/mattermost/generated/mattermost_context.jsonl ]; then cat scripts/mattermost/generated/mattermost_context.jsonl; else echo "No Mattermost context available."; fi` !`python3 scripts/mattermost-proxy/read-context.py --mode latest`
Respond with: Respond with:

View File

@@ -2,7 +2,7 @@
description: Force-sync Mattermost and answer from the latest matching message description: Force-sync Mattermost and answer from the latest matching message
--- ---
Force-refresh Mattermost first, then answer the user's question from the refreshed inbox. Refresh/read Mattermost first, then answer the user's question from the freshest evidence. Prefer the local proxy mirror when it is present; legacy sync output is fallback evidence.
Use this when the user asks for: Use this when the user asks for:
@@ -11,46 +11,13 @@ Use this when the user asks for:
- the latest Mattermost update - the latest Mattermost update
- the latest message in `fidelity-preguntas` - the latest message in `fidelity-preguntas`
Run sync: Run sync/fallback refresh:
!`start=$(date +%s); if [ -n "$AIW_MATTERMOST_SYNC_CMD" ]; then bash -lc "$AIW_MATTERMOST_SYNC_CMD"; elif [ -n "$FIDELITY_MATTERMOST_SYNC_CMD" ]; then bash -lc "$FIDELITY_MATTERMOST_SYNC_CMD"; elif [ -f scripts/mattermost/sync.sh ]; then bash scripts/mattermost/sync.sh; else echo "No Mattermost sync command is configured."; fi; status=$?; end=$(date +%s); echo "__MATTERMOST_SYNC_SECONDS__=$((end - start))"; exit "$status"` !`start=$(date +%s); if [ -n "$AIW_MATTERMOST_SYNC_CMD" ]; then bash -lc "$AIW_MATTERMOST_SYNC_CMD"; elif [ -n "$FIDELITY_MATTERMOST_SYNC_CMD" ]; then bash -lc "$FIDELITY_MATTERMOST_SYNC_CMD"; elif [ -f scripts/mattermost/sync.sh ]; then bash scripts/mattermost/sync.sh; else echo "No Mattermost sync command is configured."; fi; status=$?; end=$(date +%s); echo "__MATTERMOST_SYNC_SECONDS__=$((end - start))"; exit "$status"`
Read a focused slice of refreshed Mattermost context: Read a focused slice of refreshed Mattermost context, preferring the proxy mirror:
!`python3 - <<'PY' !`python3 scripts/mattermost-proxy/read-context.py --mode focused`
import json
from pathlib import Path
paths = [
Path("ai/inbox/mattermost-latest.md"),
Path("scripts/mattermost/generated/mattermost_context.jsonl"),
]
source = next((path for path in paths if path.is_file() and path.stat().st_size > 0), None)
if not source:
print("No Mattermost context available after sync.")
raise SystemExit(0)
records = []
for line in source.read_text().splitlines():
line = line.strip()
if not line:
continue
try:
records.append(json.loads(line))
except json.JSONDecodeError:
continue
manager_names = {"jeff", "jeff.dewitte"}
manager_records = [
record for record in records
if str(record.get("username", "")).lower() in manager_names
]
focused = manager_records[-10:] if manager_records else records[-15:]
for record in focused:
print(json.dumps(record, ensure_ascii=False))
PY`
User request: User request:

View File

@@ -9,15 +9,15 @@ Read:
@README.md @README.md
@core/README.md @core/README.md
@core/memory/operational-memory.md @core/memory/operational-memory.md
@project-knowledge/01-current/current-work.md @workspaces/fidelity/project-knowledge/01-current/current-work.md
@project-knowledge/01-current/work-items.md @workspaces/fidelity/project-knowledge/01-current/work-items.md
@project-knowledge/03-context/project.md @workspaces/fidelity/project-knowledge/03-context/project.md
@project-knowledge/03-context/workstreams/index.md @workspaces/fidelity/project-knowledge/03-context/workstreams/index.md
@agent-memory/memory/context-maintenance.md @agent-memory/memory/context-maintenance.md
Today's existing log, if present: Today's existing log, if present:
!`if [ -f project-knowledge/06-daily/$(date +%F).md ]; then cat project-knowledge/06-daily/$(date +%F).md; else echo "No daily note exists for today yet."; fi` !`if [ -f workspaces/fidelity/project-knowledge/06-daily/$(date +%F).md ]; then cat workspaces/fidelity/project-knowledge/06-daily/$(date +%F).md; else echo "No daily note exists for today yet."; fi`
Incorporate these new rough notes into today's log: Incorporate these new rough notes into today's log:
@@ -26,7 +26,7 @@ $ARGUMENTS
Instructions: Instructions:
- use `workspace-memory-curation` when available - use `workspace-memory-curation` when available
- Update or create `project-knowledge/06-daily/$(date +%F).md` - Update or create `workspaces/fidelity/project-knowledge/06-daily/$(date +%F).md`
- Maintain daily-note frontmatter for Obsidian Bases: `date`, `focus`, `work-items`, `blockers`, and `updated` - Maintain daily-note frontmatter for Obsidian Bases: `date`, `focus`, `work-items`, `blockers`, and `updated`
- Preserve concrete technical meaning - Preserve concrete technical meaning
- Capture both technical findings and communication context when relevant - Capture both technical findings and communication context when relevant

View File

@@ -9,20 +9,20 @@ Use `professional-communication` when available.
Read: Read:
@prompts/manager-update.md @prompts/manager-update.md
@project-knowledge/01-current/current-work.md @workspaces/fidelity/project-knowledge/01-current/current-work.md
@project-knowledge/01-current/work-items.md @workspaces/fidelity/project-knowledge/01-current/work-items.md
@project-knowledge/03-context/project.md @workspaces/fidelity/project-knowledge/03-context/project.md
@project-knowledge/03-context/process/communication.md @workspaces/fidelity/project-knowledge/03-context/process/communication.md
@project-knowledge/04-people/manager.md @workspaces/fidelity/project-knowledge/04-people/manager.md
@project-knowledge/04-people/index.md @workspaces/fidelity/project-knowledge/04-people/index.md
Today's log, if present: Today's log, if present:
!`if [ -f project-knowledge/06-daily/$(date +%F).md ]; then cat project-knowledge/06-daily/$(date +%F).md; else echo "No daily note exists for today yet."; fi` !`if [ -f workspaces/fidelity/project-knowledge/06-daily/$(date +%F).md ]; then cat workspaces/fidelity/project-knowledge/06-daily/$(date +%F).md; else echo "No daily note exists for today yet."; fi`
Latest Mattermost context, if available: Latest Mattermost context, if available:
!`if [ -s ai/inbox/mattermost-latest.md ]; then cat ai/inbox/mattermost-latest.md; elif [ -s scripts/mattermost/generated/mattermost_context.jsonl ]; then cat scripts/mattermost/generated/mattermost_context.jsonl; else echo "No Mattermost context available."; fi` !`python3 scripts/mattermost-proxy/read-context.py --mode latest`
Detailed active work item files, if available: Detailed active work item files, if available:
@@ -30,13 +30,13 @@ Detailed active work item files, if available:
import re import re
from pathlib import Path from pathlib import Path
summary = Path("project-knowledge/01-current/work-items.md") summary = Path("workspaces/fidelity/project-knowledge/01-current/work-items.md")
if not summary.is_file(): if not summary.is_file():
print("No work item files available.") print("No work item files available.")
raise SystemExit(0) raise SystemExit(0)
text = summary.read_text() text = summary.read_text()
paths = re.findall(r"Detail: `(project-knowledge/02-work-items/[^`]+)`", text) paths = re.findall(r"Detail: `(workspaces/fidelity/project-knowledge/02-work-items/[^`]+)`", text)
if not paths: if not paths:
print("No work item files available.") print("No work item files available.")
raise SystemExit(0) raise SystemExit(0)

View File

@@ -2,7 +2,7 @@
description: Sync Mattermost context and automatically promote high-confidence project memory description: Sync Mattermost context and automatically promote high-confidence project memory
--- ---
// turbo-all // turbo-all
Use the configured Mattermost sync command to fetch fresh communication context and maintain workspace memory automatically. Use the configured Mattermost sync command and/or local proxy mirror evidence to fetch fresh communication context and maintain workspace memory automatically.
Preferred command sources: Preferred command sources:
@@ -14,20 +14,25 @@ Run the command and use its output as fresh communication context:
!`if [ -n "$AIW_MATTERMOST_SYNC_CMD" ]; then bash -lc "$AIW_MATTERMOST_SYNC_CMD"; elif [ -n "$FIDELITY_MATTERMOST_SYNC_CMD" ]; then bash -lc "$FIDELITY_MATTERMOST_SYNC_CMD"; elif [ -f scripts/mattermost/sync.sh ]; then bash scripts/mattermost/sync.sh; else echo "No Mattermost sync command is configured."; fi` !`if [ -n "$AIW_MATTERMOST_SYNC_CMD" ]; then bash -lc "$AIW_MATTERMOST_SYNC_CMD"; elif [ -n "$FIDELITY_MATTERMOST_SYNC_CMD" ]; then bash -lc "$FIDELITY_MATTERMOST_SYNC_CMD"; elif [ -f scripts/mattermost/sync.sh ]; then bash scripts/mattermost/sync.sh; else echo "No Mattermost sync command is configured."; fi`
Fresh Mattermost evidence, preferring the proxy mirror:
!`python3 scripts/mattermost-proxy/read-context.py --mode latest`
Use this command implicitly when the user asks for the latest or last Mattermost message, especially messages from Jeff or the current manager. Use this command implicitly when the user asks for the latest or last Mattermost message, especially messages from Jeff or the current manager.
Then: Then:
- if the command fails, stop there and do not edit any workspace files - if the command fails, stop there and do not edit any workspace files
- use `ai/inbox/mattermost-latest.md` if it exists and is non-empty - prefer the local proxy mirror via `scripts/mattermost-proxy/read-context.py --mode latest` when it has evidence
- otherwise use `workspaces/fidelity/inbox/mattermost-latest.md` if it exists and is non-empty
- otherwise use `scripts/mattermost/generated/mattermost_context.jsonl` if it exists and is non-empty - otherwise use `scripts/mattermost/generated/mattermost_context.jsonl` if it exists and is non-empty
- apply the memory promotion rules from `agent-memory/memory/promotion-rules.md` - apply the memory promotion rules from `agent-memory/memory/promotion-rules.md`
- treat Mattermost output as live communication evidence; the agent decides what becomes promoted memory - treat Mattermost output as live communication evidence; the agent decides what becomes promoted memory
- automatically promote explicit, project-relevant, high-confidence facts - automatically promote explicit, project-relevant, high-confidence facts
- default destination is `project-knowledge/06-daily/$(date +%F).md` - default destination is `workspaces/fidelity/project-knowledge/06-daily/$(date +%F).md`
- update `project-knowledge/01-current/current-work.md` only for facts that materially change the current work window - update `workspaces/fidelity/project-knowledge/01-current/current-work.md` only for facts that materially change the current work window
- update `project-knowledge/02-work-items/*.md` for explicit Jira IDs, approved titles, points, scope, and status notes - update `workspaces/fidelity/project-knowledge/02-work-items/*.md` for explicit Jira IDs, approved titles, points, scope, and status notes
- keep `project-knowledge/01-current/work-items.md` aligned as the compact summary of active ticket files - keep `workspaces/fidelity/project-knowledge/01-current/work-items.md` aligned as the compact summary of active ticket files
- do not write tooling noise, sync status, or generic chat chatter into project memory - do not write tooling noise, sync status, or generic chat chatter into project memory
- if a fact is ambiguous, skip it rather than asking the user what to do - if a fact is ambiguous, skip it rather than asking the user what to do

View File

@@ -22,11 +22,11 @@ Read:
@core/integrations/memory-vault-model.md @core/integrations/memory-vault-model.md
@agent-memory/workflows/workspace-architecture.md @agent-memory/workflows/workspace-architecture.md
@project-knowledge/09-templates/work-item.md @workspaces/fidelity/project-knowledge/09-templates/work-item.md
@project-knowledge/09-templates/person.md @workspaces/fidelity/project-knowledge/09-templates/person.md
@project-knowledge/09-templates/decision.md @workspaces/fidelity/project-knowledge/09-templates/decision.md
@project-knowledge/09-templates/system.md @workspaces/fidelity/project-knowledge/09-templates/system.md
@project-knowledge/09-templates/workstream.md @workspaces/fidelity/project-knowledge/09-templates/workstream.md
Instructions: Instructions:

View File

@@ -8,25 +8,25 @@ Read:
@core/README.md @core/README.md
@core/memory/operational-memory.md @core/memory/operational-memory.md
@project-knowledge/01-current/current-work.md @workspaces/fidelity/project-knowledge/01-current/current-work.md
@project-knowledge/01-current/work-items.md @workspaces/fidelity/project-knowledge/01-current/work-items.md
@project-knowledge/03-context/project.md @workspaces/fidelity/project-knowledge/03-context/project.md
@project-knowledge/03-context/systems/index.md @workspaces/fidelity/project-knowledge/03-context/systems/index.md
@project-knowledge/03-context/workstreams/index.md @workspaces/fidelity/project-knowledge/03-context/workstreams/index.md
@project-knowledge/03-context/workstreams/flow-page-references.md @workspaces/fidelity/project-knowledge/03-context/workstreams/flow-page-references.md
@project-knowledge/03-context/process/index.md @workspaces/fidelity/project-knowledge/03-context/process/index.md
Today's log, if present: Today's log, if present:
!`if [ -f project-knowledge/06-daily/$(date +%F).md ]; then cat project-knowledge/06-daily/$(date +%F).md; else echo "No daily note exists for today yet."; fi` !`if [ -f workspaces/fidelity/project-knowledge/06-daily/$(date +%F).md ]; then cat workspaces/fidelity/project-knowledge/06-daily/$(date +%F).md; else echo "No daily note exists for today yet."; fi`
Current Mattermost inbox, if present: Current Mattermost inbox, if present:
!`if [ -f ai/inbox/mattermost-latest.md ]; then cat ai/inbox/mattermost-latest.md; else echo "No Mattermost inbox file is available."; fi` !`python3 scripts/mattermost-proxy/read-context.py --mode latest`
Generated Mattermost context, if present: Generated Mattermost context, if present:
!`if [ -s scripts/mattermost/generated/mattermost_context.jsonl ]; then cat scripts/mattermost/generated/mattermost_context.jsonl; else echo "No generated Mattermost context is available."; fi` !`if [ -s scripts/mattermost/generated/mattermost_context.jsonl ]; then cat scripts/mattermost/generated/mattermost_context.jsonl; else echo "No legacy generated Mattermost context is available."; fi`
User direction or facts to promote: User direction or facts to promote:
@@ -37,20 +37,20 @@ Instructions:
- Promote only confirmed project-relevant facts - Promote only confirmed project-relevant facts
- Ignore tooling noise and sync status - Ignore tooling noise and sync status
- Update the smallest correct set of files among: - Update the smallest correct set of files among:
- `project-knowledge/06-daily/$(date +%F).md` - `workspaces/fidelity/project-knowledge/06-daily/$(date +%F).md`
- `project-knowledge/01-current/current-work.md` - `workspaces/fidelity/project-knowledge/01-current/current-work.md`
- `project-knowledge/02-work-items/*.md` - `workspaces/fidelity/project-knowledge/02-work-items/*.md`
- `project-knowledge/01-current/work-items.md` - `workspaces/fidelity/project-knowledge/01-current/work-items.md`
- `project-knowledge/03-context/project.md` - `workspaces/fidelity/project-knowledge/03-context/project.md`
- `project-knowledge/03-context/systems/*.md` - `workspaces/fidelity/project-knowledge/03-context/systems/*.md`
- `project-knowledge/03-context/workstreams/*.md` - `workspaces/fidelity/project-knowledge/03-context/workstreams/*.md`
- `project-knowledge/03-context/process/*.md` - `workspaces/fidelity/project-knowledge/03-context/process/*.md`
- `project-knowledge/05-decisions/*.md` - `workspaces/fidelity/project-knowledge/05-decisions/*.md`
- `.opencode/commands/*.md` when command behavior needs to change - `.opencode/commands/*.md` when command behavior needs to change
- `prompts/*.md` when a reusable output template needs to change - `prompts/*.md` when a reusable output template needs to change
- `.opencode/agents/*.md` or `AGENTS.md` when default agent behavior needs to change - `.opencode/agents/*.md` or `AGENTS.md` when default agent behavior needs to change
- `.agents/skills/*/SKILL.md` when a specialized workflow needs to change - `.agents/skills/*/SKILL.md` when a specialized workflow needs to change
- `project-knowledge/00-start/*.md` when durable project onboarding guidance needs to change - `workspaces/fidelity/project-knowledge/00-start/*.md` when durable project onboarding guidance needs to change
- `agent-memory/**/*.md` when agent learning, promotion, verification, or self-maintenance behavior needs to change - `agent-memory/**/*.md` when agent learning, promotion, verification, or self-maintenance behavior needs to change
- Prefer concrete project updates over broad summaries - Prefer concrete project updates over broad summaries
- If a fact is still ambiguous, do not promote it - If a fact is still ambiguous, do not promote it

View File

@@ -27,14 +27,14 @@ Read:
@core/README.md @core/README.md
@core/memory/operational-memory.md @core/memory/operational-memory.md
@core/integrations/communication-model.md @core/integrations/communication-model.md
@project-knowledge/01-current/current-work.md @workspaces/fidelity/project-knowledge/01-current/current-work.md
@project-knowledge/01-current/work-items.md @workspaces/fidelity/project-knowledge/01-current/work-items.md
@project-knowledge/03-context/project.md @workspaces/fidelity/project-knowledge/03-context/project.md
@project-knowledge/03-context/systems/index.md @workspaces/fidelity/project-knowledge/03-context/systems/index.md
@project-knowledge/03-context/workstreams/index.md @workspaces/fidelity/project-knowledge/03-context/workstreams/index.md
@project-knowledge/03-context/process/index.md @workspaces/fidelity/project-knowledge/03-context/process/index.md
@project-knowledge/04-people/index.md @workspaces/fidelity/project-knowledge/04-people/index.md
@project-knowledge/04-people/manager.md @workspaces/fidelity/project-knowledge/04-people/manager.md
Imported summary, if present: Imported summary, if present:
@@ -55,7 +55,7 @@ Instructions:
- durable role/person associations - durable role/person associations
- recurring architecture or debugging patterns - recurring architecture or debugging patterns
- past approvals or decisions that still matter - past approvals or decisions that still matter
- create or update `project-knowledge/04-people/*.md` when the archive shows a human repeatedly contributing across channels, years, or high-signal technical/process discussions - create or update `workspaces/fidelity/project-knowledge/04-people/*.md` when the archive shows a human repeatedly contributing across channels, years, or high-signal technical/process discussions
- store people conservatively: - store people conservatively:
- exact role only when explicitly supported by the archive - exact role only when explicitly supported by the archive
- otherwise store collaboration pattern, communication style, and project relationship - otherwise store collaboration pattern, communication style, and project relationship
@@ -70,7 +70,7 @@ Instructions:
- avoid promoting outdated daily status unless it changes current understanding - avoid promoting outdated daily status unless it changes current understanding
- update existing memory when the archive clarifies or corrects it - update existing memory when the archive clarifies or corrects it
- if historical facts are ambiguous or likely outdated, summarize them as archived context instead of promoting them - if historical facts are ambiguous or likely outdated, summarize them as archived context instead of promoting them
- write promoted memory to `project-knowledge/` - write promoted memory to `workspaces/fidelity/project-knowledge/`
Return: Return:

View File

@@ -66,20 +66,20 @@ PY`
Read: Read:
@prompts/standup.md @prompts/standup.md
@project-knowledge/01-current/current-work.md @workspaces/fidelity/project-knowledge/01-current/current-work.md
@project-knowledge/01-current/work-items.md @workspaces/fidelity/project-knowledge/01-current/work-items.md
@project-knowledge/03-context/project.md @workspaces/fidelity/project-knowledge/03-context/project.md
@project-knowledge/03-context/workstreams/flow-page-references.md @workspaces/fidelity/project-knowledge/03-context/workstreams/flow-page-references.md
@project-knowledge/03-context/process/communication.md @workspaces/fidelity/project-knowledge/03-context/process/communication.md
@project-knowledge/04-people/manager.md @workspaces/fidelity/project-knowledge/04-people/manager.md
Today's log, if present: Today's log, if present:
!`if [ -f project-knowledge/06-daily/$(date +%F).md ]; then cat project-knowledge/06-daily/$(date +%F).md; else echo "No daily note exists for today yet."; fi` !`if [ -f workspaces/fidelity/project-knowledge/06-daily/$(date +%F).md ]; then cat workspaces/fidelity/project-knowledge/06-daily/$(date +%F).md; else echo "No daily note exists for today yet."; fi`
Latest refreshed Mattermost context, if present: Latest refreshed Mattermost context, if present:
!`if [ -s ai/inbox/mattermost-latest.md ]; then cat ai/inbox/mattermost-latest.md; elif [ -s scripts/mattermost/generated/mattermost_context.jsonl ]; then cat scripts/mattermost/generated/mattermost_context.jsonl; else echo "No refreshed Mattermost context available."; fi` !`python3 scripts/mattermost-proxy/read-context.py --mode standup --today $(date +%F)`
Detailed active work item files, if available: Detailed active work item files, if available:
@@ -87,13 +87,13 @@ Detailed active work item files, if available:
import re import re
from pathlib import Path from pathlib import Path
summary = Path("project-knowledge/01-current/work-items.md") summary = Path("workspaces/fidelity/project-knowledge/01-current/work-items.md")
if not summary.is_file(): if not summary.is_file():
print("No work item files available.") print("No work item files available.")
raise SystemExit(0) raise SystemExit(0)
text = summary.read_text() text = summary.read_text()
paths = re.findall(r"Detail: `(project-knowledge/02-work-items/[^`]+)`", text) paths = re.findall(r"Detail: `(workspaces/fidelity/project-knowledge/02-work-items/[^`]+)`", text)
if not paths: if not paths:
print("No work item files available.") print("No work item files available.")
raise SystemExit(0) raise SystemExit(0)
@@ -118,11 +118,11 @@ Before drafting:
- if documentation or root cause updates directly support a story, roll them into that story's update instead of listing them separately - if documentation or root cause updates directly support a story, roll them into that story's update instead of listing them separately
- exclude items that are not directly tied to a story unless they are true blockers - exclude items that are not directly tied to a story unless they are true blockers
- when one Jira item has multiple concrete updates, group them under one top-level `JIRA-ID - Title` bullet with indented markdown sub-bullets instead of repeating the same Jira line - when one Jira item has multiple concrete updates, group them under one top-level `JIRA-ID - Title` bullet with indented markdown sub-bullets instead of repeating the same Jira line
- use `project-knowledge/03-context/workstreams/flow-page-references.md` to preserve real flow/page identifiers when shorthand appears in logs or messages - use `workspaces/fidelity/project-knowledge/03-context/workstreams/flow-page-references.md` to preserve real flow/page identifiers when shorthand appears in logs or messages
- for standups that will also be sent to Teams, prefer plain language over internal implementation jargon; avoid unexplained terms like `fallback` - for standups that will also be sent to Teams, prefer plain language over internal implementation jargon; avoid unexplained terms like `fallback`
- if work is in release-process waiting state, show the parallel story work explicitly instead of implying idle waiting - if work is in release-process waiting state, show the parallel story work explicitly instead of implying idle waiting
- if Mattermost sync failed, acknowledge that internally and rely on the latest saved workspace context instead of inventing fresher communication - if Mattermost sync failed, acknowledge that internally and rely on the latest saved workspace context instead of inventing fresher communication
- prefer only the detailed work-item files referenced by `project-knowledge/01-current/work-items.md`; do not mine unrelated or completed ticket files unless they are explicitly active in current memory - prefer only the detailed work-item files referenced by `workspaces/fidelity/project-knowledge/01-current/work-items.md`; do not mine unrelated or completed ticket files unless they are explicitly active in current memory
Return a standup that is: Return a standup that is:

View File

@@ -21,22 +21,22 @@ Read:
@core/README.md @core/README.md
@core/memory/operational-memory.md @core/memory/operational-memory.md
@prompts/story-draft.md @prompts/story-draft.md
@project-knowledge/01-current/current-work.md @workspaces/fidelity/project-knowledge/01-current/current-work.md
@project-knowledge/01-current/work-items.md @workspaces/fidelity/project-knowledge/01-current/work-items.md
@project-knowledge/03-context/project.md @workspaces/fidelity/project-knowledge/03-context/project.md
@project-knowledge/03-context/workstreams/index.md @workspaces/fidelity/project-knowledge/03-context/workstreams/index.md
@project-knowledge/03-context/process/communication.md @workspaces/fidelity/project-knowledge/03-context/process/communication.md
@project-knowledge/03-context/process/jira-story-rules.md @workspaces/fidelity/project-knowledge/03-context/process/jira-story-rules.md
@project-knowledge/04-people/manager.md @workspaces/fidelity/project-knowledge/04-people/manager.md
@project-knowledge/04-people/index.md @workspaces/fidelity/project-knowledge/04-people/index.md
Today's log, if present: Today's log, if present:
!`if [ -f project-knowledge/06-daily/$(date +%F).md ]; then cat project-knowledge/06-daily/$(date +%F).md; else echo "No daily note exists for today yet."; fi` !`if [ -f workspaces/fidelity/project-knowledge/06-daily/$(date +%F).md ]; then cat workspaces/fidelity/project-knowledge/06-daily/$(date +%F).md; else echo "No daily note exists for today yet."; fi`
Latest Mattermost context, if available: Latest Mattermost context, if available:
!`if [ -s ai/inbox/mattermost-latest.md ]; then cat ai/inbox/mattermost-latest.md; elif [ -s scripts/mattermost/generated/mattermost_context.jsonl ]; then cat scripts/mattermost/generated/mattermost_context.jsonl; else echo "No Mattermost context available."; fi` !`python3 scripts/mattermost-proxy/read-context.py --mode latest`
Detailed active work item files, if available: Detailed active work item files, if available:
@@ -44,13 +44,13 @@ Detailed active work item files, if available:
import re import re
from pathlib import Path from pathlib import Path
summary = Path("project-knowledge/01-current/work-items.md") summary = Path("workspaces/fidelity/project-knowledge/01-current/work-items.md")
if not summary.is_file(): if not summary.is_file():
print("No work item files available.") print("No work item files available.")
raise SystemExit(0) raise SystemExit(0)
text = summary.read_text() text = summary.read_text()
paths = re.findall(r"Detail: `(project-knowledge/02-work-items/[^`]+)`", text) paths = re.findall(r"Detail: `(workspaces/fidelity/project-knowledge/02-work-items/[^`]+)`", text)
if not paths: if not paths:
print("No work item files available.") print("No work item files available.")
raise SystemExit(0) raise SystemExit(0)

View File

@@ -6,14 +6,14 @@ Answer the user's Swift/iOS programming question using current iOS practices and
Read: Read:
@project-knowledge/03-context/ios/index.md @workspaces/fidelity/project-knowledge/03-context/ios/index.md
@project-knowledge/03-context/ios/current-practices.md @workspaces/fidelity/project-knowledge/03-context/ios/current-practices.md
@project-knowledge/03-context/ios/project-swift-guidance.md @workspaces/fidelity/project-knowledge/03-context/ios/project-swift-guidance.md
@agent-memory/integrations/technical-verification.md @agent-memory/integrations/technical-verification.md
@project-knowledge/03-context/project.md @workspaces/fidelity/project-knowledge/03-context/project.md
@project-knowledge/03-context/systems/index.md @workspaces/fidelity/project-knowledge/03-context/systems/index.md
@project-knowledge/03-context/workstreams/index.md @workspaces/fidelity/project-knowledge/03-context/workstreams/index.md
@project-knowledge/01-current/work-items.md @workspaces/fidelity/project-knowledge/01-current/work-items.md
User question: User question:

View File

@@ -7,20 +7,20 @@ Use this command when new information should become part of the persistent works
Read: Read:
@README.md @README.md
@project-knowledge/00-start/start-here.md @workspaces/fidelity/project-knowledge/00-start/start-here.md
@project-knowledge/01-current/current-work.md @workspaces/fidelity/project-knowledge/01-current/current-work.md
@project-knowledge/01-current/work-items.md @workspaces/fidelity/project-knowledge/01-current/work-items.md
@project-knowledge/03-context/project.md @workspaces/fidelity/project-knowledge/03-context/project.md
@project-knowledge/03-context/systems/index.md @workspaces/fidelity/project-knowledge/03-context/systems/index.md
@project-knowledge/03-context/workstreams/index.md @workspaces/fidelity/project-knowledge/03-context/workstreams/index.md
@project-knowledge/03-context/workstreams/flow-page-references.md @workspaces/fidelity/project-knowledge/03-context/workstreams/flow-page-references.md
@agent-memory/memory/context-maintenance.md @agent-memory/memory/context-maintenance.md
@project-knowledge/04-people/manager.md @workspaces/fidelity/project-knowledge/04-people/manager.md
@project-knowledge/04-people/index.md @workspaces/fidelity/project-knowledge/04-people/index.md
Today's existing log, if present: Today's existing log, if present:
!`if [ -f project-knowledge/06-daily/$(date +%F).md ]; then cat project-knowledge/06-daily/$(date +%F).md; else echo "No daily note exists for today yet."; fi` !`if [ -f workspaces/fidelity/project-knowledge/06-daily/$(date +%F).md ]; then cat workspaces/fidelity/project-knowledge/06-daily/$(date +%F).md; else echo "No daily note exists for today yet."; fi`
New information to incorporate: New information to incorporate:
@@ -31,21 +31,21 @@ Instructions:
- use `workspace-memory-curation` when available - use `workspace-memory-curation` when available
- Treat direct user corrections and learning-session updates as memory sources. - Treat direct user corrections and learning-session updates as memory sources.
- For learning-session updates, prefer durable project understanding over transient ticket status. - For learning-session updates, prefer durable project understanding over transient ticket status.
- Route durable learning to architecture/process/system/workstream notes; route only truly current operational state to `project-knowledge/01-current/` or `project-knowledge/06-daily/`. - Route durable learning to architecture/process/system/workstream notes; route only truly current operational state to `workspaces/fidelity/project-knowledge/01-current/` or `workspaces/fidelity/project-knowledge/06-daily/`.
- If the new information corrects the agent's uncertainty handling or recurring behavior, update the command, prompt, agent, skill, or process rule that controls that behavior. - If the new information corrects the agent's uncertainty handling or recurring behavior, update the command, prompt, agent, skill, or process rule that controls that behavior.
- Decide whether the new information belongs in: - Decide whether the new information belongs in:
- today's daily note: `project-knowledge/06-daily/$(date +%F).md` - today's daily note: `workspaces/fidelity/project-knowledge/06-daily/$(date +%F).md`
- current work: `project-knowledge/01-current/current-work.md` - current work: `workspaces/fidelity/project-knowledge/01-current/current-work.md`
- work items: `project-knowledge/02-work-items/*.md` - work items: `workspaces/fidelity/project-knowledge/02-work-items/*.md`
- active work summary: `project-knowledge/01-current/work-items.md` - active work summary: `workspaces/fidelity/project-knowledge/01-current/work-items.md`
- project overview: `project-knowledge/03-context/project.md` - project overview: `workspaces/fidelity/project-knowledge/03-context/project.md`
- systems: `project-knowledge/03-context/systems/` - systems: `workspaces/fidelity/project-knowledge/03-context/systems/`
- workstreams: `project-knowledge/03-context/workstreams/` - workstreams: `workspaces/fidelity/project-knowledge/03-context/workstreams/`
- process: `project-knowledge/03-context/process/` - process: `workspaces/fidelity/project-knowledge/03-context/process/`
- manager mapping: `project-knowledge/04-people/manager.md` - manager mapping: `workspaces/fidelity/project-knowledge/04-people/manager.md`
- people roster: `project-knowledge/04-people/index.md` - people roster: `workspaces/fidelity/project-knowledge/04-people/index.md`
- people notes: `project-knowledge/04-people/*.md` - people notes: `workspaces/fidelity/project-knowledge/04-people/*.md`
- decisions: `project-knowledge/05-decisions/` - decisions: `workspaces/fidelity/project-knowledge/05-decisions/`
- `.agents/workflows/*.md` - `.agents/workflows/*.md`
- `.agents/rules/*.md` - `.agents/rules/*.md`
- `prompts/*.md` - `prompts/*.md`

View File

@@ -16,14 +16,14 @@ $ARGUMENTS
Read: Read:
@prompts/mattermost-translation.md @prompts/mattermost-translation.md
@project-knowledge/03-context/project.md @workspaces/fidelity/project-knowledge/03-context/project.md
@project-knowledge/03-context/process/communication.md @workspaces/fidelity/project-knowledge/03-context/process/communication.md
@project-knowledge/04-people/manager.md @workspaces/fidelity/project-knowledge/04-people/manager.md
@project-knowledge/04-people/index.md @workspaces/fidelity/project-knowledge/04-people/index.md
If relevant, use today's log for context: If relevant, use today's log for context:
!`if [ -f project-knowledge/06-daily/$(date +%F).md ]; then cat project-knowledge/06-daily/$(date +%F).md; else echo "No daily note exists for today yet."; fi` !`if [ -f workspaces/fidelity/project-knowledge/06-daily/$(date +%F).md ]; then cat workspaces/fidelity/project-knowledge/06-daily/$(date +%F).md; else echo "No daily note exists for today yet."; fi`
Return: Return:

View File

@@ -14,14 +14,14 @@ Read core:
Read active workspace memory: Read active workspace memory:
@project-knowledge/00-start/start-here.md @workspaces/fidelity/project-knowledge/00-start/start-here.md
@project-knowledge/01-current/current-work.md @workspaces/fidelity/project-knowledge/01-current/current-work.md
@project-knowledge/01-current/work-items.md @workspaces/fidelity/project-knowledge/01-current/work-items.md
@project-knowledge/03-context/project.md @workspaces/fidelity/project-knowledge/03-context/project.md
@project-knowledge/03-context/process/communication.md @workspaces/fidelity/project-knowledge/03-context/process/communication.md
@project-knowledge/03-context/ios/index.md @workspaces/fidelity/project-knowledge/03-context/ios/index.md
@project-knowledge/04-people/manager.md @workspaces/fidelity/project-knowledge/04-people/manager.md
@project-knowledge/04-people/index.md @workspaces/fidelity/project-knowledge/04-people/index.md
Read active profile, preferring the configured profile and falling back to Fidelity: Read active profile, preferring the configured profile and falling back to Fidelity:
@@ -33,11 +33,11 @@ Today's date:
Today's canonical daily note, if present: Today's canonical daily note, if present:
!`if [ -f project-knowledge/06-daily/$(date +%F).md ]; then cat project-knowledge/06-daily/$(date +%F).md; else echo "No daily note exists for today yet."; fi` !`if [ -f workspaces/fidelity/project-knowledge/06-daily/$(date +%F).md ]; then cat workspaces/fidelity/project-knowledge/06-daily/$(date +%F).md; else echo "No daily note exists for today yet."; fi`
Latest daily notes available: Latest daily notes available:
!`if [ -d project-knowledge/06-daily ]; then ls -1 project-knowledge/06-daily 2>/dev/null | sort | tail -n 3; else echo "No daily notes directory available."; fi` !`if [ -d workspaces/fidelity/project-knowledge/06-daily ]; then ls -1 workspaces/fidelity/project-knowledge/06-daily 2>/dev/null | sort | tail -n 3; else echo "No daily notes directory available."; fi`
Detailed active work item files, if available: Detailed active work item files, if available:
@@ -45,13 +45,13 @@ Detailed active work item files, if available:
import re import re
from pathlib import Path from pathlib import Path
summary = Path("project-knowledge/01-current/work-items.md") summary = Path("workspaces/fidelity/project-knowledge/01-current/work-items.md")
if not summary.is_file(): if not summary.is_file():
print("No work item files available.") print("No work item files available.")
raise SystemExit(0) raise SystemExit(0)
text = summary.read_text() text = summary.read_text()
paths = re.findall(r"Detail: `(project-knowledge/02-work-items/[^`]+)`", text) paths = re.findall(r"Detail: `(workspaces/fidelity/project-knowledge/02-work-items/[^`]+)`", text)
if not paths: if not paths:
print("No work item files available.") print("No work item files available.")
raise SystemExit(0) raise SystemExit(0)
@@ -66,7 +66,7 @@ PY`
Latest communication inbox, if available: Latest communication inbox, if available:
!`if [ -s ai/inbox/mattermost-latest.md ]; then cat ai/inbox/mattermost-latest.md; elif [ -s scripts/mattermost/generated/mattermost_context.jsonl ]; then cat scripts/mattermost/generated/mattermost_context.jsonl; else echo "No live communication context available."; fi` !`python3 scripts/mattermost-proxy/read-context.py --mode latest`
Respond with: Respond with:

40
.gitignore vendored
View File

@@ -8,26 +8,40 @@ __pycache__/
.venv/ .venv/
# Mattermost raw inbox artifacts # Mattermost raw inbox artifacts
ai/inbox/mattermost-latest.md workspaces/*/inbox/mattermost-latest.md
ai/inbox/mattermost-*.md workspaces/*/inbox/mattermost-*.md
ai/inbox/mattermost-status.json workspaces/*/inbox/mattermost-status.json
ai/inbox/photos/* workspaces/*/inbox/mattermost-mirror/*
!ai/inbox/photos/.gitkeep !workspaces/*/inbox/mattermost-mirror/.gitkeep
workspaces/*/inbox/photos/*
!workspaces/*/inbox/photos/.gitkeep
# Local build artifact for the iPhone photo inbox pasteboard helper
scripts/iphone-photo-inbox/.env
scripts/iphone-photo-inbox/copy_files_to_clipboard
# Workspace-local Mattermost runtime artifacts # Workspace-local Mattermost runtime artifacts
scripts/mattermost/.env scripts/mattermost/.env
scripts/mattermost/.venv/ scripts/mattermost/.venv/
scripts/mattermost/generated/* scripts/mattermost/generated/*
!scripts/mattermost/generated/.gitkeep !scripts/mattermost/generated/.gitkeep
scripts/mattermost-proxy/.env
# Obsidian local runtime state # Obsidian local runtime state
/.obsidian/ /.obsidian/
project-knowledge/.obsidian/workspace*.json workspaces/*/project-knowledge/.obsidian/workspace*.json
project-knowledge/.obsidian/workspace-mobile*.json workspaces/*/project-knowledge/.obsidian/workspace-mobile*.json
project-knowledge/.obsidian/graph.json workspaces/*/project-knowledge/.obsidian/graph.json
project-knowledge/.obsidian/hotkeys.json workspaces/*/project-knowledge/.obsidian/hotkeys.json
project-knowledge/.obsidian/community-plugins.json workspaces/*/project-knowledge/.obsidian/community-plugins.json
project-knowledge/.obsidian/plugins/ workspaces/*/project-knowledge/.obsidian/plugins/
project-knowledge/.obsidian/snippets/ workspaces/*/project-knowledge/.obsidian/snippets/
project-knowledge/.obsidian/cache/ workspaces/*/project-knowledge/.obsidian/cache/
.trash/ .trash/
# Antigravity CLI local workspace configuration
.antigravitycli/
# AI Workspace local service runtime
.aiw/runtime/
.aiw/indexes/

View File

@@ -12,20 +12,21 @@ Behavior rules:
- Treat `core/` as the reusable project-independent operating model. - Treat `core/` as the reusable project-independent operating model.
- Treat `profiles/fidelity/profile.md` as the active Fidelity project profile. - Treat `profiles/fidelity/profile.md` as the active Fidelity project profile.
- Treat `project-knowledge/` as the canonical clean project knowledge base for humans and AI. - Treat `workspaces/fidelity/project-knowledge/` as the canonical clean project knowledge base for humans and AI.
- Treat `agent-memory/` as the operating memory for agent behavior, learning, promotion, verification, and self-maintenance rules. - Treat `agent-memory/` as the operating memory for agent behavior, learning, promotion, verification, and self-maintenance rules.
- Treat `scripts/memory/` as the project-agnostic access layer for note creation, project-knowledge search, Base queries, and health checks. - Treat `scripts/memory/` as the project-agnostic access layer for note creation, project-knowledge search, Base queries, and health checks.
- Treat `scripts/obsidian/` as the current Obsidian adapter. Do not couple durable memory rules to Obsidian-specific behavior. - Treat `scripts/obsidian/` as the current Obsidian adapter. Do not couple durable memory rules to Obsidian-specific behavior.
- Treat `ai/inbox/` and generated connector files as raw evidence only, not promoted memory. - Treat `workspaces/fidelity/inbox/` and generated connector files as raw evidence only, not promoted memory.
- Keep Obsidian Bases clean: do not let templates in `project-knowledge/09-templates/` appear as real daily notes, work items, people, decisions, systems, or workstreams. - For Mattermost context, prefer the local proxy mirror in `workspaces/fidelity/inbox/mattermost-mirror/` when present. Use `scripts/mattermost-proxy/read-context.py` or the mirror views (`latest.*`, `by-date/`, `channels/`, `threads/`) before falling back to legacy `workspaces/fidelity/inbox/mattermost-latest.md` or `scripts/mattermost/generated/` artifacts.
- Role mapping notes such as `project-knowledge/04-people/manager.md` are `type: role-map`; actual people profiles are `type: person`. - Keep Obsidian Bases clean: do not let templates in `workspaces/fidelity/project-knowledge/09-templates/` appear as real daily notes, work items, people, decisions, systems, or workstreams.
- Role mapping notes such as `workspaces/fidelity/project-knowledge/04-people/manager.md` are `type: role-map`; actual people profiles are `type: person`.
- When editing canonical project notes, update useful metadata at the same time: `updated`, `systems`, `workstreams`, `people`, `related`, `focus`, `work-items`, and `blockers` when applicable. - When editing canonical project notes, update useful metadata at the same time: `updated`, `systems`, `workstreams`, `people`, `related`, `focus`, `work-items`, and `blockers` when applicable.
- When creating a new typed note, prefer `bash scripts/memory/memory.sh create <type> <slug> [title]`, then inspect and refine the generated Markdown. - When creating a new typed note, prefer `bash scripts/memory/memory.sh create <type> <slug> [title]`, then inspect and refine the generated Markdown.
- When checking project knowledge quality, use `bash scripts/memory/memory.sh health` and direct file inspection. - When checking project knowledge quality, use `bash scripts/memory/memory.sh health` and direct file inspection.
- Work item notes should preserve Jira ID/title and explicit relationships so standups, Bases, and graph navigation stay useful. - Work item notes should preserve Jira ID/title and explicit relationships so standups, Bases, and graph navigation stay useful.
- Daily notes should include `focus`, `work-items`, and `blockers` when those values are clear. - Daily notes should include `focus`, `work-items`, and `blockers` when those values are clear.
- Before answering a prompt that depends on current state, verify the latest relevant files instead of relying only on conversation history. - Before answering a prompt that depends on current state, verify the latest relevant files instead of relying only on conversation history.
- If the prompt asks for the latest Mattermost message, the last message from Jeff/current manager, or what someone just said, force a Mattermost refresh before answering and do not rely on stale inbox context. - If the prompt asks for the latest Mattermost message, the last message from Jeff/current manager, or what someone just said, refresh or read the freshest Mattermost evidence before answering; the proxy mirror is the primary source when it is present, and legacy sync artifacts are fallback evidence.
- Treat latest-message prompts as read-first: answer from refreshed evidence and report memory update candidates instead of editing canonical memory by default. - Treat latest-message prompts as read-first: answer from refreshed evidence and report memory update candidates instead of editing canonical memory by default.
- For learning-style questions, answer from known context and verified facts only; explicitly label unknowns, assumptions, and inferences. - For learning-style questions, answer from known context and verified facts only; explicitly label unknowns, assumptions, and inferences.
- For learning sessions, prioritize durable architecture, process, ownership, debugging strategy, release mechanics, domain concepts, and decision rules over transient ticket status. - For learning sessions, prioritize durable architecture, process, ownership, debugging strategy, release mechanics, domain concepts, and decision rules over transient ticket status.
@@ -38,21 +39,21 @@ Behavior rules:
- When the user corrects how the workspace should behave, update the linked operational surface too: commands in `.opencode/commands/`, prompt templates in `prompts/`, agent rules in `AGENTS.md` or `.opencode/agents/`, skills in `.agents/skills/`, and agent operating rules in `agent-memory/` when those files control the behavior. - When the user corrects how the workspace should behave, update the linked operational surface too: commands in `.opencode/commands/`, prompt templates in `prompts/`, agent rules in `AGENTS.md` or `.opencode/agents/`, skills in `.agents/skills/`, and agent operating rules in `agent-memory/` when those files control the behavior.
- If existing context is stale, correct it directly instead of leaving conflicting versions. - If existing context is stale, correct it directly instead of leaving conflicting versions.
- Promote information carefully: - Promote information carefully:
- daily facts go to `project-knowledge/06-daily/YYYY-MM-DD.md` - daily facts go to `workspaces/fidelity/project-knowledge/06-daily/YYYY-MM-DD.md`
- current priorities go to `project-knowledge/01-current/current-work.md` - current priorities go to `workspaces/fidelity/project-knowledge/01-current/current-work.md`
- active Jira-linked work goes to `project-knowledge/02-work-items/*.md` - active Jira-linked work goes to `workspaces/fidelity/project-knowledge/02-work-items/*.md`
- the active-work summary goes to `project-knowledge/01-current/work-items.md` - the active-work summary goes to `workspaces/fidelity/project-knowledge/01-current/work-items.md`
- durable project knowledge overview goes to `project-knowledge/03-context/project.md` - durable project knowledge overview goes to `workspaces/fidelity/project-knowledge/03-context/project.md`
- system-specific durable knowledge goes to `project-knowledge/03-context/systems/` - system-specific durable knowledge goes to `workspaces/fidelity/project-knowledge/03-context/systems/`
- workstream-specific durable knowledge goes to `project-knowledge/03-context/workstreams/` - workstream-specific durable knowledge goes to `workspaces/fidelity/project-knowledge/03-context/workstreams/`
- project-facing process knowledge goes to `project-knowledge/03-context/process/` - project-facing process knowledge goes to `workspaces/fidelity/project-knowledge/03-context/process/`
- confirmed team or manager communication preferences go to `project-knowledge/04-people/manager.md` - confirmed team or manager communication preferences go to `workspaces/fidelity/project-knowledge/04-people/manager.md`
- role-to-person mapping and recurring stakeholders go to `project-knowledge/04-people/` - role-to-person mapping and recurring stakeholders go to `workspaces/fidelity/project-knowledge/04-people/`
- confirmed decisions go to `project-knowledge/05-decisions/` - confirmed decisions go to `workspaces/fidelity/project-knowledge/05-decisions/`
- behavioral rules for how this workspace should respond go to the exact command, prompt, agent, skill, or `agent-memory/` file that enforces that behavior - behavioral rules for how this workspace should respond go to the exact command, prompt, agent, skill, or `agent-memory/` file that enforces that behavior
- Use generic `AIW_*` integration variables for new tooling and keep `FIDELITY_*` only as Fidelity-profile aliases. - Use generic `AIW_*` integration variables for new tooling and keep `FIDELITY_*` only as Fidelity-profile aliases.
- Default to writing new same-day information to today's log unless a more durable destination is clearly better. - Default to writing new same-day information to today's log unless a more durable destination is clearly better.
- Write canonical memory to `project-knowledge/`. - Write canonical memory to `workspaces/fidelity/project-knowledge/`.
- Update preexisting memory when a new prompt clarifies or corrects something already stored. - Update preexisting memory when a new prompt clarifies or corrects something already stored.
- Do not wait for a dedicated sync command if the correct memory update is already obvious. - Do not wait for a dedicated sync command if the correct memory update is already obvious.
- For analysis, drafting, review, or translation prompts, answer first and persist second unless saving the fact is required to produce the answer safely. - For analysis, drafting, review, or translation prompts, answer first and persist second unless saving the fact is required to produce the answer safely.
@@ -62,7 +63,7 @@ Behavior rules:
- If the memory interface or Obsidian adapter fails, continue with direct Markdown operations when safe and do not promote the failure as project memory. - If the memory interface or Obsidian adapter fails, continue with direct Markdown operations when safe and do not promote the failure as project memory.
- Do not over-promote uncertain information. Keep uncertain items in the daily log. - Do not over-promote uncertain information. Keep uncertain items in the daily log.
- When drafting communication, preserve technical meaning and improve clarity in natural US English. - When drafting communication, preserve technical meaning and improve clarity in natural US English.
- When answering Swift/iOS programming questions, use the project-local iOS skills and `project-knowledge/03-context/ios/`. - When answering Swift/iOS programming questions, use the project-local iOS skills and `workspaces/fidelity/project-knowledge/03-context/ios/`.
- When answering programming, dependency-management, package-manager, CI/build, testing, or architecture-practice questions, verify with primary/current documentation when the topic may be outdated, disputed, version-sensitive, or project-critical. - When answering programming, dependency-management, package-manager, CI/build, testing, or architecture-practice questions, verify with primary/current documentation when the topic may be outdated, disputed, version-sensitive, or project-critical.
- For CocoaPods, podspecs, private spec repos, trunk/CDN behavior, SPM, Xcode, Swift, and Apple frameworks, do not rely only on model memory before giving strong advice. - For CocoaPods, podspecs, private spec repos, trunk/CDN behavior, SPM, Xcode, Swift, and Apple frameworks, do not rely only on model memory before giving strong advice.
- When generating prompts for GitHub Copilot or another AI, use `agent-memory/workflows/ai-to-ai-prompting.md` and the `copilot-prompt-engineering` skill. - When generating prompts for GitHub Copilot or another AI, use `agent-memory/workflows/ai-to-ai-prompting.md` and the `copilot-prompt-engineering` skill.

View File

@@ -12,11 +12,12 @@ Behavior rules:
- Load `core/` first for project-independent operating rules. - Load `core/` first for project-independent operating rules.
- Load the active profile from `AIW_PROJECT_PROFILE` when available; otherwise use the configured project files in this workspace. - Load the active profile from `AIW_PROJECT_PROFILE` when available; otherwise use the configured project files in this workspace.
- Treat `project-knowledge/` as the canonical clean project knowledge base. - Treat `workspaces/fidelity/project-knowledge/` as the canonical clean project knowledge base.
- Treat `agent-memory/` as the operating memory for agent behavior, learning, promotion, verification, and self-maintenance rules. - Treat `agent-memory/` as the operating memory for agent behavior, learning, promotion, verification, and self-maintenance rules.
- Treat `scripts/memory/` as the stable memory access layer. - Treat `scripts/memory/` as the stable memory access layer.
- Treat tool-specific integrations such as Obsidian as replaceable adapters. - Treat tool-specific integrations such as Obsidian as replaceable adapters.
- Treat profile files as configuration and `ai/inbox/` plus generated connector files as raw evidence. - Treat profile files as configuration and `workspaces/fidelity/inbox/` plus generated connector files as raw evidence.
- For live communication context, prefer project-local mirror evidence under `workspaces/fidelity/inbox/*-mirror/` through its reader script when available, then fall back to legacy inbox/generated connector artifacts.
- Keep Obsidian Bases clean by excluding templates and typing role maps separately from people. - Keep Obsidian Bases clean by excluding templates and typing role maps separately from people.
- When updating canonical project notes, maintain relationship metadata and `updated` fields so project knowledge remains useful to both humans and agents. - When updating canonical project notes, maintain relationship metadata and `updated` fields so project knowledge remains useful to both humans and agents.
- Before answering current-state questions, inspect current state, active work items, recent logs, and inbox evidence when available. - Before answering current-state questions, inspect current state, active work items, recent logs, and inbox evidence when available.
@@ -38,11 +39,11 @@ Behavior rules:
Memory destinations: Memory destinations:
- daily facts -> `project-knowledge/06-daily/YYYY-MM-DD.md` - daily facts -> `workspaces/fidelity/project-knowledge/06-daily/YYYY-MM-DD.md`
- current priorities -> `project-knowledge/01-current/current-work.md` - current priorities -> `workspaces/fidelity/project-knowledge/01-current/current-work.md`
- active work items -> `project-knowledge/02-work-items/*.md` - active work items -> `workspaces/fidelity/project-knowledge/02-work-items/*.md`
- active-work summary -> `project-knowledge/01-current/work-items.md` - active-work summary -> `workspaces/fidelity/project-knowledge/01-current/work-items.md`
- durable project knowledge -> `project-knowledge/03-context/` - durable project knowledge -> `workspaces/fidelity/project-knowledge/03-context/`
- people and roles -> `project-knowledge/04-people/` - people and roles -> `workspaces/fidelity/project-knowledge/04-people/`
- confirmed decisions -> `project-knowledge/05-decisions/` - confirmed decisions -> `workspaces/fidelity/project-knowledge/05-decisions/`
- reusable behavior -> `.opencode/commands/`, `prompts/`, `.opencode/agents/`, `.agents/skills/`, `agent-memory/`, `core/`, or `scripts/` - reusable behavior -> `.opencode/commands/`, `prompts/`, `.opencode/agents/`, `.agents/skills/`, `agent-memory/`, `core/`, or `scripts/`

View File

@@ -43,6 +43,21 @@ async function resolveSyncContent(directory, stdoutText) {
return (generated || "").trim() return (generated || "").trim()
} }
async function resolveProfileInbox(directory) {
const profile = process.env.AIW_PROJECT_PROFILE?.trim() || "fidelity"
const configured = process.env.AIW_PROJECT_INBOX_DIR?.trim()
if (configured) return path.isAbsolute(configured) ? configured : path.join(directory, configured)
const workspaceConfigPath = path.join(directory, "profiles", profile, "workspace.json")
try {
const config = JSON.parse(await readFile(workspaceConfigPath, "utf8"))
const inboxDir = config?.inbox_dir || `workspaces/${profile}/inbox`
return path.isAbsolute(inboxDir) ? inboxDir : path.join(directory, inboxDir)
} catch {
return path.join(directory, "workspaces", profile, "inbox")
}
}
function extractPromptText(event) { function extractPromptText(event) {
const candidates = [ const candidates = [
event?.properties?.text, event?.properties?.text,
@@ -144,7 +159,7 @@ export const MattermostInbox = async ({ $, directory, client }) => {
lastSyncAt = now lastSyncAt = now
const syncStartedAt = Date.now() const syncStartedAt = Date.now()
const inboxDir = path.join(directory, "ai/inbox") const inboxDir = await resolveProfileInbox(directory)
const latestPath = path.join(inboxDir, "mattermost-latest.md") const latestPath = path.join(inboxDir, "mattermost-latest.md")
const statusPath = path.join(inboxDir, "mattermost-status.json") const statusPath = path.join(inboxDir, "mattermost-status.json")

View File

@@ -19,15 +19,15 @@ Keep the always-loaded context small. The hot set is:
- `agent-memory/memory/promotion-rules.md` - `agent-memory/memory/promotion-rules.md`
- `agent-memory/integrations/technical-verification.md` - `agent-memory/integrations/technical-verification.md`
- `agent-memory/workflows/ai-to-ai-prompting.md` - `agent-memory/workflows/ai-to-ai-prompting.md`
- `project-knowledge/00-start/start-here.md` - `workspaces/fidelity/project-knowledge/00-start/start-here.md`
- `project-knowledge/01-current/current-work.md` - `workspaces/fidelity/project-knowledge/01-current/current-work.md`
- `project-knowledge/01-current/work-items.md` - `workspaces/fidelity/project-knowledge/01-current/work-items.md`
- `project-knowledge/03-context/project.md` - `workspaces/fidelity/project-knowledge/03-context/project.md`
- `project-knowledge/03-context/process/communication.md` - `workspaces/fidelity/project-knowledge/03-context/process/communication.md`
- `project-knowledge/03-context/ios/index.md` - `workspaces/fidelity/project-knowledge/03-context/ios/index.md`
- `project-knowledge/03-context/ios/project-swift-guidance.md` - `workspaces/fidelity/project-knowledge/03-context/ios/project-swift-guidance.md`
- `project-knowledge/04-people/manager.md` - `workspaces/fidelity/project-knowledge/04-people/manager.md`
- `project-knowledge/04-people/index.md` - `workspaces/fidelity/project-knowledge/04-people/index.md`
Load everything else lazily when the task actually needs it. Load everything else lazily when the task actually needs it.
@@ -36,16 +36,16 @@ Do not preemptively load broad context sets, all work-item files, or all process
## Required Behavior ## Required Behavior
- Assume the workspace may contain stale context until checked. - Assume the workspace may contain stale context until checked.
- Treat `project-knowledge/` as the canonical clean project memory for humans and AI. Treat `agent-memory/` as agent operating memory. Treat `ai/inbox/` as raw evidence only. - Treat `workspaces/fidelity/project-knowledge/` as the canonical clean project memory for humans and AI. Treat `agent-memory/` as agent operating memory. Treat `workspaces/fidelity/inbox/` as raw evidence only.
- Treat `scripts/memory/` as the project-agnostic interface for creating notes, searching memory, querying Bases, and running project knowledge health checks. - Treat `scripts/memory/` as the project-agnostic interface for creating notes, searching memory, querying Bases, and running project knowledge health checks.
- Treat `scripts/obsidian/` as the current Obsidian adapter, not as the core memory abstraction. - Treat `scripts/obsidian/` as the current Obsidian adapter, not as the core memory abstraction.
- Keep Obsidian Bases clean: templates in `project-knowledge/09-templates/` must not be treated as real notes, and role mapping files such as `project-knowledge/04-people/manager.md` must not be typed as people. - Keep Obsidian Bases clean: templates in `workspaces/fidelity/project-knowledge/09-templates/` must not be treated as real notes, and role mapping files such as `workspaces/fidelity/project-knowledge/04-people/manager.md` must not be typed as people.
- Maintain useful project-note properties when editing canonical notes, especially work-item relationships (`systems`, `workstreams`, `people`, `related`) and daily note fields (`focus`, `work-items`, `blockers`). - Maintain useful project-note properties when editing canonical notes, especially work-item relationships (`systems`, `workstreams`, `people`, `related`) and daily note fields (`focus`, `work-items`, `blockers`).
- Before answering questions that depend on current work state, inspect `project-knowledge/01-current/current-work.md` and the latest relevant daily note under `project-knowledge/06-daily/`. - Before answering questions that depend on current work state, inspect `workspaces/fidelity/project-knowledge/01-current/current-work.md` and the latest relevant daily note under `workspaces/fidelity/project-knowledge/06-daily/`.
- Prefer lazy loading over eager loading. Pull in only the smallest relevant files for the active task. - Prefer lazy loading over eager loading. Pull in only the smallest relevant files for the active task.
- If `ai/inbox/mattermost-latest.md` exists, inspect it for fresher communication context before answering standup, status, or manager-message prompts. - For Mattermost evidence, prefer the local proxy mirror under `workspaces/fidelity/inbox/mattermost-mirror/` when present. Use `scripts/mattermost-proxy/read-context.py` or mirror views (`latest.*`, `by-date/`, `channels/`, `threads/`) before falling back to legacy `workspaces/fidelity/inbox/mattermost-latest.md` or `scripts/mattermost/generated/` artifacts.
- If the user asks for the latest/last/recent Mattermost message, the latest message from Jeff/current manager, or what someone just said, synchronize Mattermost first instead of relying on existing inbox context. - If the user asks for the latest/last/recent Mattermost message, the latest message from Jeff/current manager, or what someone just said, use the explicit latest-message flow and answer from the freshest refreshed evidence; the proxy mirror is primary when available.
- If automatic refresh is uncertain, use the explicit latest-message flow: run the Mattermost sync command, then answer from the refreshed inbox only. - If automatic refresh is uncertain and the proxy mirror is not available, run the Mattermost sync command, then answer from the refreshed inbox only.
- Treat latest-message flows as read-first. Report memory update candidates, but do not edit canonical memory from that command unless the user explicitly asks to promote the fact. - Treat latest-message flows as read-first. Report memory update candidates, but do not edit canonical memory from that command unless the user explicitly asks to promote the fact.
- For learning-style questions, answer from known context and verified facts only; label unknowns, assumptions, and inferences instead of inventing missing details. - For learning-style questions, answer from known context and verified facts only; label unknowns, assumptions, and inferences instead of inventing missing details.
- For learning sessions, prioritize durable architecture, process, ownership, debugging strategy, release mechanics, domain concepts, and decision rules over transient ticket status. - For learning sessions, prioritize durable architecture, process, ownership, debugging strategy, release mechanics, domain concepts, and decision rules over transient ticket status.
@@ -60,12 +60,12 @@ Do not preemptively load broad context sets, all work-item files, or all process
- Treat sync failures as operational errors, not project context. - Treat sync failures as operational errors, not project context.
- If a patch or edit verification fails while making a non-essential memory update, stop retrying and return the user-facing answer with the failed target noted. - If a patch or edit verification fails while making a non-essential memory update, stop retrying and return the user-facing answer with the failed target noted.
- `mattermost-sync` should automatically promote high-confidence project facts without asking what to promote. - `mattermost-sync` should automatically promote high-confidence project facts without asking what to promote.
- Prefer `project-knowledge/06-daily/` as the default destination for new Mattermost-derived facts. - Prefer `workspaces/fidelity/project-knowledge/06-daily/` as the default destination for new Mattermost-derived facts.
- Promote to `project-knowledge/01-current/current-work.md` only when the fact materially changes active work over the next few days. - Promote to `workspaces/fidelity/project-knowledge/01-current/current-work.md` only when the fact materially changes active work over the next few days.
- Keep explicit Jira IDs and approved titles visible in `project-knowledge/02-work-items/` and summarize active items in `project-knowledge/01-current/work-items.md` when they are useful for future standups or manager updates. - Keep explicit Jira IDs and approved titles visible in `workspaces/fidelity/project-knowledge/02-work-items/` and summarize active items in `workspaces/fidelity/project-knowledge/01-current/work-items.md` when they are useful for future standups or manager updates.
- Promote to `project-knowledge/03-context/project.md` only when the fact changes durable project understanding. - Promote to `workspaces/fidelity/project-knowledge/03-context/project.md` only when the fact changes durable project understanding.
- When a repeatedly mentioned person becomes relevant to project flow, create or update a file under `project-knowledge/04-people/`. - When a repeatedly mentioned person becomes relevant to project flow, create or update a file under `workspaces/fidelity/project-knowledge/04-people/`.
- Keep role-to-person mapping explicit in `project-knowledge/04-people/manager.md` and the roster in `project-knowledge/04-people/index.md`. - Keep role-to-person mapping explicit in `workspaces/fidelity/project-knowledge/04-people/manager.md` and the roster in `workspaces/fidelity/project-knowledge/04-people/index.md`.
- Never promote tooling chatter, sync status, or generic conversation noise. - Never promote tooling chatter, sync status, or generic conversation noise.
- Direct user prompts are also memory sources. Do not limit memory updates to explicit sync commands. - Direct user prompts are also memory sources. Do not limit memory updates to explicit sync commands.
- If a new prompt corrects prior understanding, update the canonical file directly instead of keeping both versions alive. - If a new prompt corrects prior understanding, update the canonical file directly instead of keeping both versions alive.
@@ -77,8 +77,8 @@ Do not preemptively load broad context sets, all work-item files, or all process
- If a previous context file is now stale or inaccurate, update that file directly. - If a previous context file is now stale or inaccurate, update that file directly.
- Prefer correcting canonical context over appending contradictory notes. - Prefer correcting canonical context over appending contradictory notes.
- Keep changes concise and auditable. - Keep changes concise and auditable.
- When the topic is architectural or historical, prefer updating the relevant file under `project-knowledge/03-context/systems/`, `project-knowledge/03-context/workstreams/`, or project-facing `project-knowledge/03-context/process/` instead of overloading `project-knowledge/03-context/project.md`. - When the topic is architectural or historical, prefer updating the relevant file under `workspaces/fidelity/project-knowledge/03-context/systems/`, `workspaces/fidelity/project-knowledge/03-context/workstreams/`, or project-facing `workspaces/fidelity/project-knowledge/03-context/process/` instead of overloading `workspaces/fidelity/project-knowledge/03-context/project.md`.
- When the user asks Swift, SwiftUI, iOS architecture, testing, or debugging questions, use `project-knowledge/03-context/ios/` and the local OpenCode iOS skills before answering. - When the user asks Swift, SwiftUI, iOS architecture, testing, or debugging questions, use `workspaces/fidelity/project-knowledge/03-context/ios/` and the local OpenCode iOS skills before answering.
- When the user asks about programming concepts, dependency tooling, package managers, CI/build tooling, testing frameworks, or practices that may be outdated or opinion-sensitive, verify against primary/current documentation before making strong claims. - When the user asks about programming concepts, dependency tooling, package managers, CI/build tooling, testing frameworks, or practices that may be outdated or opinion-sensitive, verify against primary/current documentation before making strong claims.
- For CocoaPods, podspecs, private specs repos, trunk/CDN behavior, Swift Package Manager, Xcode, Swift, Apple frameworks, and similar project-linked tooling, do not rely only on memory. - For CocoaPods, podspecs, private specs repos, trunk/CDN behavior, Swift Package Manager, Xcode, Swift, Apple frameworks, and similar project-linked tooling, do not rely only on memory.
- When the user asks for a prompt for another AI, GitHub Copilot, or the Fidelity development machine, use `agent-memory/workflows/ai-to-ai-prompting.md` and generate a self-contained prompt. - When the user asks for a prompt for another AI, GitHub Copilot, or the Fidelity development machine, use `agent-memory/workflows/ai-to-ai-prompting.md` and generate a self-contained prompt.

View File

@@ -7,9 +7,9 @@ Shared rules and context already live in the normal workspace files. Do not dupl
Read these first: Read these first:
@./AGENTS.md @./AGENTS.md
@./project-knowledge/00-start/start-here.md @./workspaces/fidelity/project-knowledge/00-start/start-here.md
@./project-knowledge/01-current/current-work.md @./workspaces/fidelity/project-knowledge/01-current/current-work.md
@./project-knowledge/01-current/work-items.md @./workspaces/fidelity/project-knowledge/01-current/work-items.md
## Tool Surface ## Tool Surface
@@ -17,7 +17,7 @@ Read these first:
- `.agents/` is the canonical shared cross-platform surface for workflows and reusable rule/skill content. - `.agents/` is the canonical shared cross-platform surface for workflows and reusable rule/skill content.
- `.opencode/` is the OpenCode compatibility/runtime surface. - `.opencode/` is the OpenCode compatibility/runtime surface.
- `.agent/` is an alias for tools that expect the singular directory naming. - `.agent/` is an alias for tools that expect the singular directory naming.
- `project-knowledge/` is canonical project memory. - `workspaces/fidelity/project-knowledge/` is canonical project memory.
- `agent-memory/` is agent operating memory. - `agent-memory/` is agent operating memory.
## Gemini-Specific Operating Notes ## Gemini-Specific Operating Notes
@@ -26,4 +26,4 @@ Read these first:
- Follow `AGENTS.md` as the shared source of truth for answer-first behavior, memory promotion, and lazy loading. - Follow `AGENTS.md` as the shared source of truth for answer-first behavior, memory promotion, and lazy loading.
- For analysis, review, translation, or drafting prompts, answer first and persist second unless saving the fact is clearly required to produce a safe answer. - For analysis, review, translation, or drafting prompts, answer first and persist second unless saving the fact is clearly required to produce a safe answer.
- Do not create new canonical notes in the critical path of a simple answer unless the user explicitly asked to save the information or the destination is obvious and non-blocking. - Do not create new canonical notes in the critical path of a simple answer unless the user explicitly asked to save the information or the destination is obvious and non-blocking.
- When memory should be updated, prefer the smallest correct change to `project-knowledge/` and avoid duplicating stale and corrected versions. - When memory should be updated, prefer the smallest correct change to `workspaces/fidelity/project-knowledge/` and avoid duplicating stale and corrected versions.

455
README.md
View File

@@ -1,366 +1,201 @@
# AI Workspace - Fidelity Profile # AI Workspace
Reusable AI-native companion workspace with Fidelity configured as the first real project profile. 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.
This repository is not the product codebase. It is an operational context layer used to keep project state current, capture communication evidence, prepare standups, draft clear stakeholder updates, and generate self-contained prompts for another AI that has access to the implementation codebase. The first real profile in this repository is `fidelity`, but the reusable model is intentionally project-independent.
The reusable logic lives in `core/`. The clean project second brain lives in `project-knowledge/`. Agent operating memory lives in `agent-memory/`. Fidelity-specific setup lives in the active profile under `profiles/fidelity/`. ## What This Repo Is
Shared cross-platform workflow/rule content lives in `.agents/`. OpenCode reads that content through `.opencode/` compatibility paths, and Gemini-compatible entry context starts at `GEMINI.md`. Use this repository beside your real implementation work to:
--- - maintain human-readable project memory;
- capture communication or screenshot evidence before curation;
- generate standups, stakeholder updates, and AI-to-AI prompts;
- expose bounded local context to AI clients through MCP;
- manage local services such as capture tools, context servers, and inbox helpers;
- support long-running AI workflows with durable state artifacts.
## Purpose This repository is not the product codebase. It is a context and workflow layer.
- Provide a reusable file-based AI workspace pattern ## Architecture At A Glance
- Keep Fidelity context current outside the main development machine
- Turn fragmented daily work into reusable AI-ready context
- Support standups, manager updates, Jira notes, and debugging summaries
- Generate high-quality prompts for GitHub Copilot or another AI on the Fidelity development machine
- Improve communication quality without losing technical accuracy
--- ```text
Communication / photos / archives / manual notes
## Operating Model
Raw profile inbox evidence
You work on Fidelity from a different machine.
Human or agent curation
This workspace is used to:
Canonical Markdown project knowledge
- record what happened during implementation and debugging
- sync relevant communication from Mattermost Derived local index
- preserve current project context between sessions
- draft messages for the current manager or stakeholder with the right tone and scope Read-only MCP context server
- translate rough notes into concise native-sounding English
- generate self-contained prompts for another AI that does have access to the product codebase OpenCode / Claude Code / Copilot / Antigravity / other AI clients
```
Core principle: Core principle:
Context must be updated before asking AI to write. ```text
Markdown project knowledge is canonical. Inboxes, indexes, chat memory, and cloud memory are supporting layers.
```
Reusable principle: ## Main Folders
Integrations extract evidence. The agent promotes memory. | Path | Purpose |
|---|---|
| `docs/` | Simple project-independent documentation for developers adopting the workspace |
| `core/` | Reusable operating model and architecture notes |
| `profiles/` | Project-specific configuration and assumptions |
| `workspaces/<profile>/project-knowledge/` | Profile-owned canonical Markdown vault |
| `agent-memory/` | Agent behavior, promotion, verification, and workflow memory |
| `workspaces/<profile>/inbox/` | Profile-owned raw evidence before promotion into canonical memory |
| `scripts/aiw/` | Service manager and local indexer |
| `scripts/mcp/` | MCP servers exposing bounded local context |
| `scripts/memory/` | Project-agnostic interface for canonical memory operations |
| `scripts/obsidian/` | Current Obsidian adapter |
| `scripts/mattermost-proxy/` | Mattermost proxy mirror connector for local evidence capture |
| `scripts/iphone-photo-inbox/` | Local photo inbox receiver |
| `apps/mac/AIWorkspace/` | macOS menu bar app for service visibility and control |
--- ## Quick Start
## Reusable Architecture Run basic checks for the active profile:
### /project-knowledge ```bash
python3 scripts/aiw/services.py doctor --profile fidelity
python3 scripts/aiw/services.py status --profile fidelity
python3 scripts/aiw/indexer.py build --profile fidelity
```
Canonical Obsidian second brain and transferable project knowledge. Start the read-only context MCP server:
- `00-start/` -> onboarding, glossary, and start-here ```bash
- `01-current/` -> current work and active work-item summary python3 scripts/aiw/services.py start aiw-context-mcp --profile fidelity
- `02-work-items/` -> ticket/task notes with metadata properties ```
- `03-context/` -> systems, workstreams, project-facing process, and iOS context
- `04-people/` -> people, roles, and collaboration context
- `05-decisions/` -> durable decisions
- `06-daily/` -> daily notes
- `07-maps/` -> graph hubs and semantic maps
- `08-bases/` -> Obsidian Bases
- `09-templates/` -> Obsidian templates
- `attachments/` -> vault assets
### /agent-memory HTTP endpoint:
Agent operating memory. ```text
http://127.0.0.1:8765/mcp
```
- `behavior/` -> agent learning and self-maintenance rules Health endpoint:
- `memory/` -> promotion, correction, and context-maintenance rules
- `integrations/` -> memory interface, Obsidian adapter, communication sources, and technical verification
- `workflows/` -> AI-to-AI prompting and workspace behavior model
- `maps/` -> agent-side navigation maps
### /.agents ```text
http://127.0.0.1:8765/health
```
Shared cross-platform workflow and rule source. ## Documentation
- `workflows/` -> reusable workflow definitions used by OpenCode-compatible commands and Antigravity-compatible workflow views Start here:
- `rules/` -> reusable skill/rule content
- `skills/` -> skill-path compatibility layer pointing at `rules/`
### /.opencode - [Getting Started](docs/getting-started.md)
- [Architecture](docs/architecture.md)
- [Profiles](docs/profiles.md)
- [Memory Model](docs/memory-model.md)
- [MCP](docs/mcp.md)
- [Services](docs/services.md)
- [Local RAG Index](docs/local-rag-index.md)
- [Security and Privacy](docs/security-and-privacy.md)
OpenCode runtime surface. Profile-specific project knowledge starts at:
- `commands/` -> OpenCode slash commands, currently linked to `.agents/workflows/` - `workspaces/fidelity/project-knowledge/00-start/start-here.md` for the current Fidelity vault
- `skills/` -> OpenCode skills, currently linked to shared `.agents/` content - `profiles/fidelity/profile.md` for the Fidelity profile declaration
- `agents/` -> OpenCode agent definitions - `profiles/example/profile.md` for a sanitized reusable profile example
- `plugins/` -> OpenCode-specific plugins
### /core ## Profiles
Project-independent workspace logic. A profile represents one project, client, team, or workflow. It declares project assumptions, context sources, local services, and workflow defaults.
- `README.md` -> reusable operating model Current profiles:
- `memory/operational-memory.md` -> reusable memory classes, promotion rules, correction rules, self-maintenance rules
- `integrations/communication-model.md` -> live communication and historical archive connector contract
- `profiles/create-project-profile.md` -> checklist for adapting the workspace to another project
### /profiles ```text
profiles/fidelity/
profiles/example/
```
Project-specific configuration. Each profile resolves memory and inbox paths from `profiles/<profile>/workspace.json`. Fidelity data now lives under `workspaces/fidelity/`.
- `profiles/fidelity/` -> active Fidelity implementation profile ## Memory Model
- `profiles/example/` -> non-sensitive example profile for new projects
Profiles declare project assumptions, communication sources, work-item system, stakeholders, enabled commands, and enabled skills. The workspace separates memory by responsibility:
--- - `workspaces/<profile>/project-knowledge/`: canonical project facts for humans and AI;
- `workspaces/<profile>/inbox/`: raw evidence;
- `agent-memory/`: rules for how agents behave;
- `.aiw/indexes/`: derived local search indexes;
- external systems such as mem9: optional agent recall, not project truth.
## Project Scope Do not treat generated connector output or vector indexes as authoritative memory. Promote durable facts into the smallest correct Markdown file.
Fidelity iOS ecosystem: ## MCP Model
- Fid4 `aiw-context-mcp` exposes profile-bounded, read-only context through MCP tools and resources. It does not capture traffic, send messages, or promote memory.
- XFlowSDK
- FTFrameworks
- REST migration replacing GraphQL over time
- Discourse and AO issues that require careful classification
--- Current examples:
## Runtime Structure - `project_current_context`
- `project_search_memory`
- `memory_hybrid_search`
- `communication_latest`
- `communication_standup_context`
- `photos_latest`
### /ai ## Service Manager
Runtime inbox for communication evidence. The service manager provides a single local lifecycle surface:
- `inbox/` -> communication evidence and transient inbox files before promotion into `project-knowledge/` ```bash
python3 scripts/aiw/services.py start --profile fidelity
python3 scripts/aiw/services.py stop --profile fidelity
python3 scripts/aiw/services.py status --profile fidelity --json
python3 scripts/aiw/services.py logs aiw-context-mcp --profile fidelity
```
### /prompts Runtime logs, PID files, and state live under `.aiw/runtime/` and are ignored.
Reusable prompts for: ## Local Index
- standups Build a derived search index over canonical Markdown:
- Mattermost updates
- manager communication
- issue clarification
### /workflows ```bash
python3 scripts/aiw/indexer.py build --profile fidelity
python3 scripts/aiw/indexer.py search "dismissal lifecycle" --profile fidelity
```
Repeatable working guides for: Indexes live under `.aiw/indexes/` and are ignored because they are rebuildable local artifacts.
- daily context sync ## Security Defaults
- flow debugging
- external issue analysis
### /scripts - Keep secrets in ignored `.env` files.
- Do not commit raw tokens, cookies, session IDs, or captured headers.
- Keep MCP read-only by default.
- Treat inboxes and generated indexes as sensitive local artifacts.
- Use cloud memory systems only with an explicit data policy.
Helpers for automation around memory access, context generation, communication drafting, and imports. ## Tests
- `scripts/memory/` -> project-agnostic interface for canonical memory ```bash
- `scripts/obsidian/` -> current Obsidian adapter and URI helpers python3 scripts/aiw/test_services.py
- `scripts/mattermost/` -> live communication connector python3 scripts/aiw/test_profile.py
- `scripts/slack/` -> historical archive importer python3 scripts/aiw/test_indexer.py
python3 scripts/mcp/aiw-context-mcp/test_server.py
python3 scripts/iphone-photo-inbox/test_receiver.py
```
### /project-knowledge/.obsidian ## Adoption Strategy
Optional Obsidian vault configuration. Recommended order for new projects:
Open `project-knowledge/` as the Obsidian vault. Do not open the repository root as the vault. 1. Copy `profiles/example/` to a new profile.
2. Create or point to a project knowledge vault.
3. Configure only the services the project needs.
4. Keep raw evidence outside canonical memory.
5. Build the local index.
6. Connect AI clients through MCP.
7. Promote durable facts into Markdown as work progresses.
Portable vault configuration can be versioned, while local layout and plugin runtime files are ignored. The reusable core should not depend on a company name, ticket prefix, channel name, programming stack, or AI client.
Obsidian is the current visual interface over canonical memory. The reusable memory access layer is `scripts/memory/`, so the workspace can later swap Obsidian for another Markdown knowledge tool without changing the memory model.
---
## Daily Usage
### Start of day
Read:
- `project-knowledge/00-start/start-here.md`
- `project-knowledge/01-current/current-work.md`
- `project-knowledge/01-current/work-items.md`
- `project-knowledge/03-context/project.md`
- `project-knowledge/04-people/manager.md`
- `project-knowledge/04-people/index.md`
- latest file under `project-knowledge/06-daily/`
### During the day
Capture:
- implementation progress from the main development machine
- Mattermost conversations that change scope or priorities
- debugging findings
- open questions, blockers, and follow-ups
Write updates in:
- `project-knowledge/06-daily/YYYY-MM-DD.md`
### Before sending a message
Confirm:
- context
- current observation
- requested or implied action
- whether the issue is external behavior or regression
- whether the flow is authenticated or non-authenticated
### End of day
Update:
- today log
- current state
- next communication needs for the current manager or stakeholder
---
## Expected Output
This workspace should help produce:
- high-signal standups
- clearer Mattermost updates
- concise supervisor communication
- better issue framing
- more reliable AI-generated English
---
## OpenCode Entry Point
This workspace is designed to work well with the OpenCode VS Code extension.
Recommended usage:
1. Open this repository as its own VS Code workspace.
2. Start OpenCode from the integrated terminal at the repository root.
3. Begin each session with `/workspace-context` or the Fidelity alias `/fidelity-context`.
4. As new information appears during the day, update context before asking for drafting help.
Project commands live under `.opencode/commands/` and are intended to:
- load the reusable core plus active project profile
- sync live communication context into the workspace inbox
- draft standups
- draft manager updates
- draft prompts for another AI on the implementation machine
- force-refresh and inspect latest communication messages
- convert rough notes into daily log updates
This keeps AI output tied to the latest workspace state instead of relying on chat memory alone.
Cross-platform note:
- `.agents/` is the shared source of truth for reusable workflows/rules
- `.opencode/` is the OpenCode execution layer
- `.agent/` may exist as a naming alias for tools that look for singular agent directories
- `GEMINI.md` is the Gemini CLI entrypoint for shared workspace context
---
## Generic Commands
- `/workspace-context` -> load core plus active profile
- `/memory-health` -> check canonical memory and adapter health
- `/memory-create` -> create a typed canonical note through the memory interface
- `/communication-sync` -> sync live communication evidence and promote high-confidence memory
- `/archive-import` -> import historical archive evidence
- `/ai-prompt` -> generate a self-contained prompt for another AI
- `/standup` -> generate a work-item-aware daily standup
- `/manager-update` -> draft stakeholder-ready status
- `/translate` -> rewrite rough notes into professional English
- `/sync-context` -> incorporate new facts or corrections into memory
Compatibility aliases remain available for the Fidelity profile:
- `/fidelity-context`
- `/mattermost-sync`
- `/slack-import`
- `/copilot-prompt`
- `/swift-help`
---
## Communication Memory Flow
This workspace supports a live-memory pattern for communication sources such as Mattermost.
Recommended setup:
1. Use the workspace-local script at `scripts/mattermost/sync.sh`, or override it with `AIW_MATTERMOST_SYNC_CMD`.
2. Let OpenCode run with the project plugins enabled.
3. The Mattermost inbox plugin will periodically refresh `ai/inbox/mattermost-latest.md`.
4. Promote durable facts from the inbox into `project-knowledge/06-daily/`, `project-knowledge/01-current/`, `project-knowledge/02-work-items/`, and `project-knowledge/03-context/`.
Use `/communication-sync` or `/mattermost-sync` when you want to force a refresh manually.
Generic environment variables:
- `AIW_PROJECT_PROFILE`
- `AIW_CHANNEL_PREFIX`
- `AIW_MATTERMOST_SYNC_CMD`
- `AIW_MATTERMOST_SYNC_INTERVAL_MINUTES`
- `AIW_SLACK_EXPORT_PATH`
The `FIDELITY_*` variables remain supported as Fidelity-profile aliases, but new reusable setup should prefer `AIW_*`.
---
## Project Knowledge Vault
Open `project-knowledge/` as the Obsidian vault. The repository root remains the technical workspace for OpenCode, scripts, profiles, runtime inboxes, and generated evidence.
Recommended entry point:
- `project-knowledge/00-start/start-here.md`
- `project-knowledge/00-start/onboarding.md` for new members
- `project-knowledge/00-start/glossary.md` for terminology
- `project-knowledge/07-maps/` for graph hubs
Use Obsidian for:
- visual navigation
- graph/backlink review
- manual review of work items, people, decisions, and logs
- lightweight editing of the same Markdown memory files
Do not use Obsidian as a second memory database. The source of truth remains the versioned Markdown files under `project-knowledge/`.
Runtime/evidence stays outside the vault:
- `.opencode/`
- `scripts/`
- `core/`
- `profiles/`
- `ai/inbox/`
- `scripts/*/generated/`
Ignored Obsidian runtime files include workspace layout, plugin cache, snippets, and local plugin installs.
Project-agnostic memory helpers live under `scripts/memory/`:
- `scripts/memory/memory.sh create <type> <slug> [title]`
- `scripts/memory/memory.sh search <query> [folder]`
- `scripts/memory/memory.sh base-query <base-name> [format]`
- `scripts/memory/memory.sh health`
Obsidian adapter helpers live under `scripts/obsidian/`:
- `scripts/obsidian/cli.sh <obsidian-cli-command>`
- `scripts/obsidian/open.sh <vault-relative-path>`
- `scripts/obsidian/daily.sh`
- `scripts/obsidian/search.sh <query>`
- `scripts/obsidian/uri.sh <action> key=value`
---
## Creating Another Project
1. Copy `profiles/example/` to `profiles/<project>/`.
2. Fill in `profiles/<project>/profile.md`.
3. Set `AIW_PROJECT_PROFILE=<project>`.
4. Configure any communication connector with `AIW_*` variables.
5. Create or adapt `project-knowledge/` for the project-specific canonical memory.
6. Use `/workspace-context` to load the core plus active profile.
See `core/profiles/create-project-profile.md` for the full checklist.

View File

@@ -11,7 +11,7 @@ tags:
This folder contains the operating memory for the AI agent. This folder contains the operating memory for the AI agent.
It is not Fidelity project documentation. Do not transfer this folder as project knowledge for a new engineer. Transfer `project-knowledge/` for the project second brain, and use this folder to explain how the agent should maintain that knowledge. It is not Fidelity project documentation. Do not transfer this folder as project knowledge for a new engineer. Transfer `workspaces/fidelity/project-knowledge/` for the project second brain, and use this folder to explain how the agent should maintain that knowledge.
--- ---
@@ -28,8 +28,8 @@ It is not Fidelity project documentation. Do not transfer this folder as project
## Relationship To Project Knowledge ## Relationship To Project Knowledge
- `project-knowledge/` is the clean Obsidian vault and project second brain. - `workspaces/fidelity/project-knowledge/` is the clean Obsidian vault and project second brain.
- `agent-memory/` is the agent operating manual. - `agent-memory/` is the agent operating manual.
- `ai/inbox/` and connector `generated/` folders are evidence, not canonical memory. - `workspaces/fidelity/inbox/` and connector `generated/` folders are evidence, not canonical memory.
- `scripts/memory/` is the project-agnostic interface for reading, searching, creating, querying, and validating project knowledge. - `scripts/memory/` is the project-agnostic interface for reading, searching, creating, querying, and validating project knowledge.
- `scripts/obsidian/` is the current Obsidian adapter and must not become the core abstraction. - `scripts/obsidian/` is the current Obsidian adapter and must not become the core abstraction.

View File

@@ -27,10 +27,10 @@ This applies to:
- debugging discussions - debugging discussions
- corrections to previous understanding - corrections to previous understanding
The agent must not wait for a separate promotion command. The agent should proactively update `project-knowledge/` (including 01-current, 02-work-items, 03-context, 04-people, 06-daily) within the same conversational turn when new information is clear, durable, and the destination is obvious. The agent must not wait for a separate promotion command. The agent should proactively update `workspaces/fidelity/project-knowledge/` (including 01-current, 02-work-items, 03-context, 04-people, 06-daily) within the same conversational turn when new information is clear, durable, and the destination is obvious.
- When editing `project-knowledge/`, write as a human engineer maintaining shared project documentation. - When editing `workspaces/fidelity/project-knowledge/`, write as a human engineer maintaining shared project documentation.
- Keep agent-operating logic out of `project-knowledge/`; store that logic in prompts, commands, skills, agents, or `agent-memory/`. - Keep agent-operating logic out of `workspaces/fidelity/project-knowledge/`; store that logic in prompts, commands, skills, agents, or `agent-memory/`.
- Answer-first rule: when the user's main goal is analysis, review, translation, or drafting, answer first unless persistence is required to avoid losing a clear durable fact. - Answer-first rule: when the user's main goal is analysis, review, translation, or drafting, answer first unless persistence is required to avoid losing a clear durable fact.
- Do not create a brand-new canonical note during the critical path of the answer unless the user explicitly asked to save the fact or the new note is the smallest correct update. - Do not create a brand-new canonical note during the critical path of the answer unless the user explicitly asked to save the fact or the new note is the smallest correct update.
- Prefer updating an existing canonical note over creating a new file when both are valid. - Prefer updating an existing canonical note over creating a new file when both are valid.
@@ -71,7 +71,7 @@ Avoid low-value learning-session questions such as:
- whether a specific PR was approved today - whether a specific PR was approved today
- whether a ticket moved columns today - whether a ticket moved columns today
- what the next standup line should be - what the next standup line should be
- temporary sequencing questions that belong in `project-knowledge/01-current/` or `project-knowledge/06-daily/` - temporary sequencing questions that belong in `workspaces/fidelity/project-knowledge/01-current/` or `workspaces/fidelity/project-knowledge/06-daily/`
Prefer high-value questions such as: Prefer high-value questions such as:
@@ -130,7 +130,7 @@ Examples:
- A standup formatting correction should update `prompts/standup.md` and the shared workflow in `.agents/workflows/standup.md`. - A standup formatting correction should update `prompts/standup.md` and the shared workflow in `.agents/workflows/standup.md`.
- A Mattermost freshness correction should update the Mattermost command/plugin instructions. - A Mattermost freshness correction should update the Mattermost command/plugin instructions.
- A Copilot prompt-structure correction should update `prompts/copilot-prompt.md`, `.agents/workflows/copilot-prompt.md`, or the shared skill/rule source. - A Copilot prompt-structure correction should update `prompts/copilot-prompt.md`, `.agents/workflows/copilot-prompt.md`, or the shared skill/rule source.
- A Swift answer-quality correction should update the relevant iOS skill or `project-knowledge/03-context/ios/` guidance. - A Swift answer-quality correction should update the relevant iOS skill or `workspaces/fidelity/project-knowledge/03-context/ios/` guidance.
Keep the daily log as evidence of what happened, but make the reusable behavior live in the file that controls that behavior. Keep the daily log as evidence of what happened, but make the reusable behavior live in the file that controls that behavior.
@@ -138,7 +138,7 @@ Keep the daily log as evidence of what happened, but make the reusable behavior
## File Selection ## File Selection
### `project-knowledge/06-daily/YYYY-MM-DD.md` ### `workspaces/fidelity/project-knowledge/06-daily/YYYY-MM-DD.md`
Default destination for: Default destination for:
@@ -148,7 +148,7 @@ Default destination for:
- story and approval movement - story and approval movement
- context that is important now but may evolve later - context that is important now but may evolve later
### `project-knowledge/01-current/current-work.md` ### `workspaces/fidelity/project-knowledge/01-current/current-work.md`
Use when the fact changes the active work window, including: Use when the fact changes the active work window, including:
@@ -157,28 +157,28 @@ Use when the fact changes the active work window, including:
- current blockers or debugging constraints - current blockers or debugging constraints
- manager direction that changes the next few days of work - manager direction that changes the next few days of work
### `project-knowledge/02-work-items/*.md` and `project-knowledge/01-current/work-items.md` ### `workspaces/fidelity/project-knowledge/02-work-items/*.md` and `workspaces/fidelity/project-knowledge/01-current/work-items.md`
Use `project-knowledge/02-work-items/*.md` as the canonical memory for current Jira-linked work that should remain easy to reference across sessions, especially: Use `workspaces/fidelity/project-knowledge/02-work-items/*.md` as the canonical memory for current Jira-linked work that should remain easy to reference across sessions, especially:
- Jira IDs - Jira IDs
- approved or explicit titles - approved or explicit titles
- currently relevant status notes - currently relevant status notes
- current points or scope notes - current points or scope notes
Use `project-knowledge/01-current/work-items.md` as the summary view of what is active now. Use `workspaces/fidelity/project-knowledge/01-current/work-items.md` as the summary view of what is active now.
These files should help standups and manager updates mention work items precisely. These files should help standups and manager updates mention work items precisely.
### `project-knowledge/03-context/project.md` ### `workspaces/fidelity/project-knowledge/03-context/project.md`
Use for durable project knowledge that should survive beyond the current work window. Use for durable project knowledge that should survive beyond the current work window.
### `project-knowledge/04-people/manager.md` ### `workspaces/fidelity/project-knowledge/04-people/manager.md`
Use only when a communication preference or manager expectation becomes stable enough to reuse repeatedly. Use only when a communication preference or manager expectation becomes stable enough to reuse repeatedly.
### `project-knowledge/04-people/index.md` and `project-knowledge/04-people/*.md` ### `workspaces/fidelity/project-knowledge/04-people/index.md` and `workspaces/fidelity/project-knowledge/04-people/*.md`
Use these files for: Use these files for:
@@ -192,7 +192,7 @@ When the role is not explicit, store:
- what kinds of topics they influence - what kinds of topics they influence
- how they affect approvals, scope, debugging, or communication - how they affect approvals, scope, debugging, or communication
### `project-knowledge/05-decisions/*.md` ### `workspaces/fidelity/project-knowledge/05-decisions/*.md`
Use for explicit confirmed decisions with ongoing impact. Use for explicit confirmed decisions with ongoing impact.

View File

@@ -21,7 +21,7 @@ The agent must keep the workspace behavior aligned with recurring user correctio
- Update `.opencode/agents/` and `AGENTS.md` for default agent behavior. - Update `.opencode/agents/` and `AGENTS.md` for default agent behavior.
- Update `.agents/skills/` for specialized workflows. - Update `.agents/skills/` for specialized workflows.
- Update `agent-memory/` for reusable agent operating rules. - Update `agent-memory/` for reusable agent operating rules.
- Update `project-knowledge/` only when the correction changes project knowledge or project-facing process documentation. - Update `workspaces/fidelity/project-knowledge/` only when the correction changes project knowledge or project-facing process documentation.
--- ---

View File

@@ -1,7 +1,7 @@
--- ---
type: agent-integration type: agent-integration
status: active status: active
updated: 2026-05-08 updated: 2026-05-20
tags: tags:
- communication - communication
- evidence - evidence
@@ -17,12 +17,19 @@ Communication connectors extract evidence. The agent decides what to promote.
Mattermost is the current live communication connector. Mattermost is the current live communication connector.
- Fresh output goes to `ai/inbox/mattermost-latest.md`. - Primary local evidence is the Mattermost proxy mirror under `workspaces/fidelity/inbox/mattermost-mirror/` when present.
- Generated extraction artifacts stay under `scripts/mattermost/generated/`. - Prefer `workspaces/fidelity/inbox/mattermost-mirror/latest.md` / `latest.jsonl` for latest-message context, `by-date/YYYY/MM/YYYY-MM-DD.jsonl` for daily/standup context, `channels/<channel>/YYYY/MM/YYYY-MM-DD.jsonl` for channel-specific context, and `threads/<root-or-post-id>.jsonl` for thread-specific context.
- Use `scripts/mattermost-proxy/read-context.py` from commands/workflows instead of reading ad hoc files; it prefers the proxy mirror and falls back to legacy sync artifacts.
- Legacy fresh output may still go to `workspaces/fidelity/inbox/mattermost-latest.md`.
- Legacy generated extraction artifacts stay under `scripts/mattermost/generated/`.
- Failed syncs must not update project knowledge. - Failed syncs must not update project knowledge.
- Latest-message requests must refresh Mattermost before answering. - Latest-message requests must refresh Mattermost before answering.
- Latest-message requests are read-first. The agent may identify a memory update candidate, but should not edit `project-knowledge/` from the latest-message command unless the user explicitly asks to promote the fact. - Latest-message requests are read-first. The agent may identify a memory update candidate, but should not edit `workspaces/fidelity/project-knowledge/` from the latest-message command unless the user explicitly asks to promote the fact.
- Standup generation is a separate required-refresh flow: it must fetch Mattermost before drafting, even though general prompts should not sync automatically. - Standup generation is a separate required-refresh flow: it must fetch Mattermost before drafting, even though general prompts should not sync automatically.
- Standup reads should use the focused reader mode, `scripts/mattermost-proxy/read-context.py --mode standup --today YYYY-MM-DD`, which reads date-bucketed previous-workday/today records and should use the active profile's configured `AIW_MATTERMOST_CONTEXT_CHANNELS` when available. Avoid loading broad mirror `latest.md` into standup prompts because it may include stale or unrelated channels and waste tokens. Keep project-specific channel names out of reusable connector code.
- If adding MCP support for Mattermost, treat it as a read-only query wrapper over the existing proxy mirror and `read-context.py`, not as a replacement for the capture/mirror pipeline. Keep the mirror's file layout as canonical raw evidence and expose only narrow tools such as latest, standup/date, channel, and thread reads with channel filters and limits.
- Do not build a write-capable Mattermost MCP or expose tokens, cookies, raw headers, or broad unfiltered raw dumps through MCP. MCP output should remain evidence for agent reasoning; promotion to `workspaces/fidelity/project-knowledge/` still follows normal memory rules.
- If the proxy mirror is running, treat it as fresher than legacy `mattermost-latest.md` / generated JSONL. Do not ignore mirror evidence merely because a legacy sync command also ran.
- Do not refresh Mattermost just because a prompt mentions a manager or stakeholder. - Do not refresh Mattermost just because a prompt mentions a manager or stakeholder.
- Treat document review, message polishing, translation, and "does this align with Jeff's expectations?" prompts as normal drafting tasks unless the user explicitly asks for the latest message or fresh Mattermost evidence. - Treat document review, message polishing, translation, and "does this align with Jeff's expectations?" prompts as normal drafting tasks unless the user explicitly asks for the latest message or fresh Mattermost evidence.
- The OpenCode plugin syncs automatically only for explicit latest-message requests by default. - The OpenCode plugin syncs automatically only for explicit latest-message requests by default.
@@ -35,6 +42,6 @@ Mattermost is the current live communication connector.
Slack export import is the current historical archive connector. Slack export import is the current historical archive connector.
- Archive evidence stays outside `project-knowledge/`. - Archive evidence stays outside `workspaces/fidelity/project-knowledge/`.
- Promote only durable project facts, people context, process rules, or historical architecture lessons. - Promote only durable project facts, people context, process rules, or historical architecture lessons.
- Do not promote dated status details unless they explain current context. - Do not promote dated status details unless they explain current context.

View File

@@ -15,7 +15,7 @@ Use mem9 as a cross-session, cross-agent recall layer without replacing the work
## Source Of Truth ## Source Of Truth
`project-knowledge/` remains the canonical human-readable Fidelity project memory. `workspaces/fidelity/project-knowledge/` remains the canonical human-readable Fidelity project memory.
`agent-memory/` remains the canonical operating memory for agent behavior, workflows, promotion rules, and integration guidance. `agent-memory/` remains the canonical operating memory for agent behavior, workflows, promotion rules, and integration guidance.
@@ -46,7 +46,7 @@ Do not bulk-import raw inboxes, generated sync output, full chat transcripts, or
1. Recall mem9 early for user preferences and relevant workspace history. 1. Recall mem9 early for user preferences and relevant workspace history.
2. Load hot Markdown context from `AGENTS.md` / `opencode.json` instructions. 2. Load hot Markdown context from `AGENTS.md` / `opencode.json` instructions.
3. For current-work questions, still inspect `project-knowledge/01-current/current-work.md` and the latest relevant daily note. 3. For current-work questions, still inspect `workspaces/fidelity/project-knowledge/01-current/current-work.md` and the latest relevant daily note.
4. Answer or act from verified context. 4. Answer or act from verified context.
5. When the interaction adds durable knowledge, update canonical Markdown first; store a compact mem9 memory only if it improves future recall. 5. When the interaction adds durable knowledge, update canonical Markdown first; store a compact mem9 memory only if it improves future recall.

View File

@@ -15,7 +15,7 @@ Use `scripts/memory/memory.sh` as the project-agnostic interface to project know
## Default Root ## Default Root
The primary project knowledge directory is `project-knowledge/`. The primary project knowledge directory is `workspaces/fidelity/project-knowledge/`.
Environment variable precedence: Environment variable precedence:

View File

@@ -16,13 +16,13 @@ Obsidian is the current interface over canonical Markdown memory. The workspace-
## Recommended Start ## Recommended Start
Open the `project-knowledge/` folder as the Obsidian vault, not the repository root and not `agent-memory/`. Open the `workspaces/fidelity/project-knowledge/` folder as the Obsidian vault, not the repository root and not `agent-memory/`.
Open: Open:
- [Start Here](../../project-knowledge/00-start/start-here.md) - [Start Here](../../workspaces/fidelity/project-knowledge/00-start/start-here.md)
- [New Member Onboarding](../../project-knowledge/00-start/onboarding.md) - [New Member Onboarding](../../workspaces/fidelity/project-knowledge/00-start/onboarding.md)
- [Knowledge Maps](../../project-knowledge/07-maps/index.md) - [Knowledge Maps](../../workspaces/fidelity/project-knowledge/07-maps/index.md)
--- ---
@@ -32,26 +32,26 @@ Open:
Start with: Start with:
- [New Member Onboarding](../../project-knowledge/00-start/onboarding.md) - [New Member Onboarding](../../workspaces/fidelity/project-knowledge/00-start/onboarding.md)
- [Glossary](../../project-knowledge/00-start/glossary.md) - [Glossary](../../workspaces/fidelity/project-knowledge/00-start/glossary.md)
- [Current Work Map](../../project-knowledge/07-maps/current-work.md) - [Current Work Map](../../workspaces/fidelity/project-knowledge/07-maps/current-work.md)
- [Fidelity Domain Map](../../project-knowledge/07-maps/fidelity-domain.md) - [Fidelity Domain Map](../../workspaces/fidelity/project-knowledge/07-maps/fidelity-domain.md)
### Daily Work View ### Daily Work View
Start with: Start with:
- [Current Work Map](../../project-knowledge/07-maps/current-work.md) - [Current Work Map](../../workspaces/fidelity/project-knowledge/07-maps/current-work.md)
- [Work Items Map](../../project-knowledge/07-maps/work-items.md) - [Work Items Map](../../workspaces/fidelity/project-knowledge/07-maps/work-items.md)
- [Daily Notes Index](../../project-knowledge/06-daily/index.md) - [Daily Notes Index](../../workspaces/fidelity/project-knowledge/06-daily/index.md)
### System Understanding View ### System Understanding View
Start with: Start with:
- [Fidelity Domain Map](../../project-knowledge/07-maps/fidelity-domain.md) - [Fidelity Domain Map](../../workspaces/fidelity/project-knowledge/07-maps/fidelity-domain.md)
- [Fidelity Apps Map](../../project-knowledge/07-maps/fidelity-apps.md) - [Fidelity Apps Map](../../workspaces/fidelity/project-knowledge/07-maps/fidelity-apps.md)
- [People Map](../../project-knowledge/07-maps/people.md) - [People Map](../../workspaces/fidelity/project-knowledge/07-maps/people.md)
--- ---

View File

@@ -50,4 +50,4 @@ Reusable workspace logic and project-independent operating rules.
## Project Profile ## Project Profile
- Active project profile: `profiles/fidelity/profile.md` - Active project profile: `profiles/fidelity/profile.md`
- Project knowledge vault: `project-knowledge/` - Project knowledge vault: `workspaces/fidelity/project-knowledge/`

View File

@@ -75,5 +75,5 @@ Commands, prompts, skills, workflows, and automation surfaces that make the work
- Project-agnostic memory scripts: `scripts/memory/` - Project-agnostic memory scripts: `scripts/memory/`
- Current Obsidian adapter: `scripts/obsidian/` - Current Obsidian adapter: `scripts/obsidian/`
- Obsidian MCP migration plan: `agent-memory/workflows/obsidian-mcp-migration.md` - Obsidian MCP migration plan: `agent-memory/workflows/obsidian-mcp-migration.md`
- Project knowledge vault: `project-knowledge/` - Project knowledge vault: `workspaces/fidelity/project-knowledge/`
- Agent operating memory: `agent-memory/` - Agent operating memory: `agent-memory/`

View File

@@ -18,22 +18,22 @@ Keep this workspace useful as living memory instead of a pile of disconnected no
- Update canonical context when a durable fact changes. - Update canonical context when a durable fact changes.
- Prefer correcting stale context over appending contradictory notes. - Prefer correcting stale context over appending contradictory notes.
- Keep `project-knowledge/` human-facing and project-facing: write it as an engineer maintaining shared project notes, not as an AI maintaining its own operating instructions. - Keep `workspaces/fidelity/project-knowledge/` human-facing and project-facing: write it as an engineer maintaining shared project notes, not as an AI maintaining its own operating instructions.
- Do not place agent-only logic, output contracts, prompting tactics, evaluation heuristics, or command-behavior rules in `project-knowledge/` unless a human engineer on the project would reasonably want that exact guidance there. - Do not place agent-only logic, output contracts, prompting tactics, evaluation heuristics, or command-behavior rules in `workspaces/fidelity/project-knowledge/` unless a human engineer on the project would reasonably want that exact guidance there.
- Put agent behavior, prompt logic, formatting contracts, and slash-command rules in `agent-memory/`, `.opencode/commands/`, `prompts/`, `.opencode/agents/`, or `.agents/skills/` instead of `project-knowledge/`. - Put agent behavior, prompt logic, formatting contracts, and slash-command rules in `agent-memory/`, `.opencode/commands/`, `prompts/`, `.opencode/agents/`, or `.agents/skills/` instead of `workspaces/fidelity/project-knowledge/`.
- If a canonical note appears in an Obsidian Base, update its frontmatter properties together with the prose content. - If a canonical note appears in an Obsidian Base, update its frontmatter properties together with the prose content.
- When changing frontmatter properties on existing canonical notes, prefer Obsidian CLI property operations through `scripts/obsidian/cli.sh` when available so YAML spacing and property formatting stay clean; fall back to direct Markdown edits only when the CLI is unavailable or the operation is unsupported. - When changing frontmatter properties on existing canonical notes, prefer Obsidian CLI property operations through `scripts/obsidian/cli.sh` when available so YAML spacing and property formatting stay clean; fall back to direct Markdown edits only when the CLI is unavailable or the operation is unsupported.
- Keep templates under `project-knowledge/09-templates/` out of real-note Bases by filtering the template folder. - Keep templates under `workspaces/fidelity/project-knowledge/09-templates/` out of real-note Bases by filtering the template folder.
- Role mapping files should not use `type: person`; reserve `type: person` for actual people profiles. - Role mapping files should not use `type: person`; reserve `type: person` for actual people profiles.
- Work-item notes should keep known `systems`, `workstreams`, `people`, and `related` properties current. - Work-item notes should keep known `systems`, `workstreams`, `people`, and `related` properties current.
- Daily notes should keep `focus`, `work-items`, and `blockers` properties current when the values are clear. - Daily notes should keep `focus`, `work-items`, and `blockers` properties current when the values are clear.
- Use the smallest correct destination: - Use the smallest correct destination:
- `project-knowledge/06-daily/YYYY-MM-DD.md` for daily progress and evolving findings - `workspaces/fidelity/project-knowledge/06-daily/YYYY-MM-DD.md` for daily progress and evolving findings
- `project-knowledge/01-current/current-work.md` for near-term active work - `workspaces/fidelity/project-knowledge/01-current/current-work.md` for near-term active work
- `project-knowledge/02-work-items/*.md` for canonical Jira-linked active work - `workspaces/fidelity/project-knowledge/02-work-items/*.md` for canonical Jira-linked active work
- `project-knowledge/01-current/work-items.md` for the compact active-work summary - `workspaces/fidelity/project-knowledge/01-current/work-items.md` for the compact active-work summary
- `project-knowledge/03-context/` for durable project knowledge - `workspaces/fidelity/project-knowledge/03-context/` for durable project knowledge
- `project-knowledge/04-people/` for named person context - `workspaces/fidelity/project-knowledge/04-people/` for named person context
- `.opencode/commands/`, `prompts/`, `.opencode/agents/`, `.agents/skills/`, `agent-memory/`, `core/`, or `scripts/` for reusable behavior rules that control how the workspace responds - `.opencode/commands/`, `prompts/`, `.opencode/agents/`, `.agents/skills/`, `agent-memory/`, `core/`, or `scripts/` for reusable behavior rules that control how the workspace responds
--- ---

View File

@@ -17,12 +17,12 @@ The reusable model lives in `core/memory/operational-memory.md`. This file recor
## Memory Classes ## Memory Classes
- `daily`: dated progress and evidence in `project-knowledge/06-daily/`. - `daily`: dated progress and evidence in `workspaces/fidelity/project-knowledge/06-daily/`.
- `state`: near-term active work in `project-knowledge/01-current/`. - `state`: near-term active work in `workspaces/fidelity/project-knowledge/01-current/`.
- `work-items`: ticket-scoped memory in `project-knowledge/02-work-items/`. - `work-items`: ticket-scoped memory in `workspaces/fidelity/project-knowledge/02-work-items/`.
- `stable-context`: durable systems, workstreams, process, and iOS context in `project-knowledge/03-context/`. - `stable-context`: durable systems, workstreams, process, and iOS context in `workspaces/fidelity/project-knowledge/03-context/`.
- `people`: collaborators, stakeholders, and role mappings in `project-knowledge/04-people/`. - `people`: collaborators, stakeholders, and role mappings in `workspaces/fidelity/project-knowledge/04-people/`.
- `decisions`: accepted durable decisions in `project-knowledge/05-decisions/`. - `decisions`: accepted durable decisions in `workspaces/fidelity/project-knowledge/05-decisions/`.
- `tooling-behavior`: agent and workspace behavior in `agent-memory/`, `.opencode/`, `prompts/`, `scripts/`, and `core/`. - `tooling-behavior`: agent and workspace behavior in `agent-memory/`, `.opencode/`, `prompts/`, `scripts/`, and `core/`.
--- ---
@@ -31,9 +31,9 @@ The reusable model lives in `core/memory/operational-memory.md`. This file recor
Raw evidence stays outside project knowledge: Raw evidence stays outside project knowledge:
- `ai/inbox/` - `workspaces/fidelity/inbox/`
- `scripts/mattermost/generated/` - `scripts/mattermost/generated/`
- `scripts/slack/generated/` - `scripts/slack/generated/`
Only curated, high-confidence project facts should be promoted into `project-knowledge/`. Only curated, high-confidence project facts should be promoted into `workspaces/fidelity/project-knowledge/`.

View File

@@ -25,7 +25,7 @@ If a fact is ambiguous, skip it or keep it only in the daily log with appropriat
## File Selection ## File Selection
### Promote to `project-knowledge/06-daily/YYYY-MM-DD.md` ### Promote to `workspaces/fidelity/project-knowledge/06-daily/YYYY-MM-DD.md`
Use the daily log for: Use the daily log for:
@@ -37,7 +37,7 @@ Use the daily log for:
Daily logs are the default destination for most promoted Mattermost facts. Daily logs are the default destination for most promoted Mattermost facts.
### Promote to `project-knowledge/01-current/current-work.md` ### Promote to `workspaces/fidelity/project-knowledge/01-current/current-work.md`
Use current state only for facts that materially affect active work over the next few days, such as: Use current state only for facts that materially affect active work over the next few days, such as:
@@ -48,9 +48,9 @@ Use current state only for facts that materially affect active work over the nex
Do not copy every daily update into current state. Do not copy every daily update into current state.
### Promote to `project-knowledge/02-work-items/*.md` and `project-knowledge/01-current/work-items.md` ### Promote to `workspaces/fidelity/project-knowledge/02-work-items/*.md` and `workspaces/fidelity/project-knowledge/01-current/work-items.md`
Use `project-knowledge/02-work-items/*.md` for: Use `workspaces/fidelity/project-knowledge/02-work-items/*.md` for:
- explicit Jira IDs - explicit Jira IDs
- approved or explicit story titles - approved or explicit story titles
@@ -60,9 +60,9 @@ Use `project-knowledge/02-work-items/*.md` for:
If a Jira item is likely to appear again in standups or manager updates, it belongs here. If a Jira item is likely to appear again in standups or manager updates, it belongs here.
Use `project-knowledge/01-current/work-items.md` as the compact summary of which ticket files are active. Use `workspaces/fidelity/project-knowledge/01-current/work-items.md` as the compact summary of which ticket files are active.
### Promote to `project-knowledge/03-context/project.md` ### Promote to `workspaces/fidelity/project-knowledge/03-context/project.md`
Use project context only for durable project knowledge that should survive beyond the current work window, such as: Use project context only for durable project knowledge that should survive beyond the current work window, such as:
@@ -73,11 +73,11 @@ Use project context only for durable project knowledge that should survive beyon
Do not promote story-specific daily movement into project context unless it changes durable project understanding. Do not promote story-specific daily movement into project context unless it changes durable project understanding.
### Promote to `project-knowledge/05-decisions/*.md` ### Promote to `workspaces/fidelity/project-knowledge/05-decisions/*.md`
Use decisions only for explicit confirmed decisions with medium or long-term impact. Use decisions only for explicit confirmed decisions with medium or long-term impact.
### Promote to `project-knowledge/04-people/index.md` and `project-knowledge/04-people/*.md` ### Promote to `workspaces/fidelity/project-knowledge/04-people/index.md` and `workspaces/fidelity/project-knowledge/04-people/*.md`
Use these files when: Use these files when:
@@ -141,5 +141,5 @@ Given Mattermost updates like:
Automatic behavior should be: Automatic behavior should be:
- add all of them to today's log if they are relevant to today's work - add all of them to today's log if they are relevant to today's work
- promote only the currently actionable subset to `project-knowledge/01-current/current-work.md` - promote only the currently actionable subset to `workspaces/fidelity/project-knowledge/01-current/current-work.md`
- keep story-specific details out of `project-knowledge/03-context/project.md` unless they reveal a durable project rule - keep story-specific details out of `workspaces/fidelity/project-knowledge/03-context/project.md` unless they reveal a durable project rule

View File

@@ -61,6 +61,9 @@ Use this structure by default:
- For VS Code multi-root Copilot workflows, preserve repo-provided customizations such as `.github/prompts`, `.github/instructions`, `.github/agents`, `.github/skills`, and `AGENTS.md`. Shared `fidelity-ai-copilot` customizations should supplement these repo files, while repo-specific instructions should be treated as the practical authority when they conflict. - For VS Code multi-root Copilot workflows, preserve repo-provided customizations such as `.github/prompts`, `.github/instructions`, `.github/agents`, `.github/skills`, and `AGENTS.md`. Shared `fidelity-ai-copilot` customizations should supplement these repo files, while repo-specific instructions should be treated as the practical authority when they conflict.
- For Fidelity Jira/Confluence access from GitHub Copilot CLI or VS Code, do not assume the approved access method. First have the target AI read the current Fidelity-provided human instructions from Confluence or local exported docs, then configure the smallest matching workflow. If those instructions require terminal `curl` with environment variables such as `COPILOT_JIRA_URL` and `COPILOT_JIRA_TOKEN`, enforce that path; otherwise follow the documented Fidelity-approved method. Never print, persist, or hardcode tokens. - For Fidelity Jira/Confluence access from GitHub Copilot CLI or VS Code, do not assume the approved access method. First have the target AI read the current Fidelity-provided human instructions from Confluence or local exported docs, then configure the smallest matching workflow. If those instructions require terminal `curl` with environment variables such as `COPILOT_JIRA_URL` and `COPILOT_JIRA_TOKEN`, enforce that path; otherwise follow the documented Fidelity-approved method. Never print, persist, or hardcode tokens.
- Treat `fidelity-ai-copilot` as a self-improving AI harness rather than a static prompt dump: the target AI should notice recurring useful workflows, newly discovered internal instructions, and tool changes, then propose small auditable updates to instructions, skills, prompts, agents, specs, or validation checklists. It should ask before making broad changes and keep product repos clean. - Treat `fidelity-ai-copilot` as a self-improving AI harness rather than a static prompt dump: the target AI should notice recurring useful workflows, newly discovered internal instructions, and tool changes, then propose small auditable updates to instructions, skills, prompts, agents, specs, or validation checklists. It should ask before making broad changes and keep product repos clean.
- For corporate-tool captures in `fidelity-ai-copilot`, prefer a single raw Charles Mirror source such as `archive/charles-mirror/` and treat it as read-only evidence organized by hostname. Generated Copilot outputs should be written to separate per-platform folders only when useful, with prompts requiring source inspection, narrow scope, local-only processing, and explicit evidence paths.
- When advising on `fidelity-ai-copilot` customization, use this routing: keep global safety and repo role in `AGENTS.md` / `.github/copilot-instructions.md`; use `.github/instructions/*.instructions.md` for path-scoped rules such as `archive/**`; use `.github/prompts/*.prompt.md` for repeatable slash-command tasks; use `.github/agents/*.agent.md` for persistent personas with tool restrictions and handoffs; use `.github/skills/*/SKILL.md` for reusable multi-step capabilities with scripts, examples, or resources. Prefer small, composable artifacts over one large instruction file.
- For read-only evidence prompts such as Discourse/Charles Mirror search, explicitly prevent the target AI from editing the prompt/configuration files while running the workflow. If Copilot changes `.github/prompts/*.prompt.md` during an evidence query, treat that as a workflow bug unless the user specifically asked to update the prompt.
- When the user says they will handle dependency alignment, registry configuration, or compile/test execution manually on the development machine, generated Copilot follow-ups should not ask Copilot to solve those dependency/tooling issues or run broad builds. Instead, ask Copilot for the smallest source-level fix for the specific compiler error the user provides, state that the user will rerun validation manually, and request a concise summary of changed files and expected validation impact. - When the user says they will handle dependency alignment, registry configuration, or compile/test execution manually on the development machine, generated Copilot follow-ups should not ask Copilot to solve those dependency/tooling issues or run broad builds. Instead, ask Copilot for the smallest source-level fix for the specific compiler error the user provides, state that the user will rerun validation manually, and request a concise summary of changed files and expected validation impact.
--- ---

View File

@@ -11,7 +11,7 @@ tags: [obsidian, mcp, migration, memory]
Replace the current Obsidian CLI-backed adapter path with an Obsidian MCP-first integration while preserving the workspace memory model: Replace the current Obsidian CLI-backed adapter path with an Obsidian MCP-first integration while preserving the workspace memory model:
- `project-knowledge/` remains the canonical source of truth - `workspaces/fidelity/project-knowledge/` remains the canonical source of truth
- `scripts/memory/` remains the stable workspace memory contract - `scripts/memory/` remains the stable workspace memory contract
- direct Markdown edits remain the safe fallback for precise curation - direct Markdown edits remain the safe fallback for precise curation
- Obsidian stays an adapter/navigation layer, not a second memory store - Obsidian stays an adapter/navigation layer, not a second memory store
@@ -66,7 +66,7 @@ The migration should change the adapter, not the memory model.
- Do not move canonical memory out of Markdown - Do not move canonical memory out of Markdown
- Do not encode promotion rules into the MCP layer - Do not encode promotion rules into the MCP layer
- Do not make `project-knowledge/` depend on Obsidian-only storage - Do not make `workspaces/fidelity/project-knowledge/` depend on Obsidian-only storage
- Do not remove direct Markdown editing as a fallback - Do not remove direct Markdown editing as a fallback
- Do not delete the current scripts until MCP coverage is validated in real use - Do not delete the current scripts until MCP coverage is validated in real use
@@ -116,7 +116,7 @@ Current Phase 1 checklist:
- [x] Add OpenCode MCP config using `OBSIDIAN_API_KEY` environment substitution. - [x] Add OpenCode MCP config using `OBSIDIAN_API_KEY` environment substitution.
- [x] Export `OBSIDIAN_API_KEY` in the shell that launches OpenCode. - [x] Export `OBSIDIAN_API_KEY` in the shell that launches OpenCode.
- [x] Restart OpenCode and confirm `opencode mcp list` shows `obsidian` connected. - [x] Restart OpenCode and confirm `opencode mcp list` shows `obsidian` connected.
- [x] Validate read/search tools against `project-knowledge/` notes. - [x] Validate read/search tools against `workspaces/fidelity/project-knowledge/` notes.
- [x] Validate write/append/delete behavior on a disposable test note before touching canonical memory. - [x] Validate write/append/delete behavior on a disposable test note before touching canonical memory.
- [ ] Determine whether Bases/properties/backlinks/template behavior needs to remain on the existing CLI/direct-Markdown path. - [ ] Determine whether Bases/properties/backlinks/template behavior needs to remain on the existing CLI/direct-Markdown path.
@@ -208,9 +208,9 @@ Delete later only if truly unused:
### Keep ### Keep
- `project-knowledge/.obsidian/` - `workspaces/fidelity/project-knowledge/.obsidian/`
- `project-knowledge/08-bases/*.base` - `workspaces/fidelity/project-knowledge/08-bases/*.base`
- `project-knowledge/09-templates/` - `workspaces/fidelity/project-knowledge/09-templates/`
- `scripts/memory/` - `scripts/memory/`
### Deprecate then remove ### Deprecate then remove

View File

@@ -51,4 +51,4 @@ Capture reusable prompting lessons so the agent does not need to re-research the
- Treat prompt improvement as eval-driven iteration: identify the exact bad output pattern, add the smallest correction, and check whether it fixes the failure without bloating the prompt. - Treat prompt improvement as eval-driven iteration: identify the exact bad output pattern, add the smallest correction, and check whether it fixes the failure without bloating the prompt.
- When a correction is about workspace behavior, update the controlling prompt or command immediately so the next run benefits. - When a correction is about workspace behavior, update the controlling prompt or command immediately so the next run benefits.
- Keep project memory clean while improving prompt quality; do not store agent heuristics in `project-knowledge/`. - Keep project memory clean while improving prompt quality; do not store agent heuristics in `workspaces/fidelity/project-knowledge/`.

View File

@@ -11,7 +11,7 @@ related:
This repository separates project knowledge from agent operation: This repository separates project knowledge from agent operation:
- `project-knowledge/` is the clean Obsidian second brain and transferable project memory. - `workspaces/fidelity/project-knowledge/` is the clean Obsidian second brain and transferable project memory.
- `agent-memory/` is the operating manual for the AI agent. - `agent-memory/` is the operating manual for the AI agent.
- Everything else is technical runtime, reusable core, profile configuration, scripts, commands, prompts, or raw evidence. - Everything else is technical runtime, reusable core, profile configuration, scripts, commands, prompts, or raw evidence.
@@ -42,10 +42,10 @@ These folders are intentionally outside Obsidian memory:
- `core/` stores reusable workspace logic. - `core/` stores reusable workspace logic.
- `profiles/` stores project-specific profile declarations. - `profiles/` stores project-specific profile declarations.
- `scripts/` stores connectors, importers, and wrappers. - `scripts/` stores connectors, importers, and wrappers.
- `ai/inbox/` stores live evidence from communication sync. - `workspaces/fidelity/inbox/` stores live evidence from communication sync.
- `scripts/*/generated/` stores extracted evidence. - `scripts/*/generated/` stores extracted evidence.
Runtime and generated files can be used as evidence, but durable project facts should be promoted into `project-knowledge/`. Runtime and generated files can be used as evidence, but durable project facts should be promoted into `workspaces/fidelity/project-knowledge/`.
Agent behavior rules belong in `agent-memory/`, not in the project vault. Agent behavior rules belong in `agent-memory/`, not in the project vault.
@@ -55,7 +55,7 @@ Agent behavior rules belong in `agent-memory/`, not in the project vault.
Use `scripts/memory/` as the platform-agnostic interface to canonical memory. Use `scripts/memory/` as the platform-agnostic interface to canonical memory.
Obsidian is the current visual and CLI-backed adapter, but the source of truth remains Markdown under `project-knowledge/`. Obsidian is the current visual and CLI-backed adapter, but the source of truth remains Markdown under `workspaces/fidelity/project-knowledge/`.
- Agents use `scripts/memory/memory.sh create` when a new typed note is needed. - Agents use `scripts/memory/memory.sh create` when a new typed note is needed.
- Agents use `scripts/memory/memory.sh search` or direct Markdown reads for context lookup. - Agents use `scripts/memory/memory.sh search` or direct Markdown reads for context lookup.
@@ -66,6 +66,6 @@ Obsidian is the current visual and CLI-backed adapter, but the source of truth r
## Memory Rule ## Memory Rule
Promoted memory lives in `project-knowledge/`. Promoted memory lives in `workspaces/fidelity/project-knowledge/`.
Raw inbox and generated connector evidence stays outside the vault until the agent promotes durable facts. Raw inbox and generated connector evidence stays outside the vault until the agent promotes durable facts.

View File

@@ -15,7 +15,7 @@ It separates reusable tooling, project knowledge, and agent operating memory:
- `core/` contains reusable project-independent operating rules - `core/` contains reusable project-independent operating rules
- `profiles/<project>/` contains project-specific configuration and assumptions - `profiles/<project>/` contains project-specific configuration and assumptions
- `project-knowledge/` contains transferable project documentation and current work memory - `workspaces/fidelity/project-knowledge/` contains transferable project documentation and current work memory
- `agent-memory/` contains agent behavior, learning, promotion, integration, and verification rules - `agent-memory/` contains agent behavior, learning, promotion, integration, and verification rules
--- ---
@@ -48,14 +48,14 @@ When the user corrects a recurring behavior, the workspace should update the fil
- `core/` for reusable project-independent behavior - `core/` for reusable project-independent behavior
- `profiles/<project>/` for project-specific assumptions - `profiles/<project>/` for project-specific assumptions
- `project-knowledge/.obsidian/` only for portable Obsidian configuration, not project memory content - `workspaces/fidelity/project-knowledge/.obsidian/` only for portable Obsidian configuration, not project memory content
- `scripts/memory/` for project-agnostic memory access, creation, search, Base queries, and health checks - `scripts/memory/` for project-agnostic memory access, creation, search, Base queries, and health checks
- `scripts/obsidian/` for the current Obsidian adapter, not for core memory semantics - `scripts/obsidian/` for the current Obsidian adapter, not for core memory semantics
- `.agents/workflows/` for slash commands (with `.opencode/commands/` for compatibility) - `.agents/workflows/` for slash commands (with `.opencode/commands/` for compatibility)
- `prompts/` for reusable drafting templates - `prompts/` for reusable drafting templates
- `.agents/rules/` and `AGENTS.md` for default agent behavior - `.agents/rules/` and `AGENTS.md` for default agent behavior
- `.agents/skills/` for specialized workflows. Do not mirror into `.opencode/skills/`; OpenCode discovers `.agents/skills/` directly. - `.agents/skills/` for specialized workflows. Do not mirror into `.opencode/skills/`; OpenCode discovers `.agents/skills/` directly.
- `project-knowledge/00-start/` and `project-knowledge/03-context/process/` for project-facing onboarding and process rules - `workspaces/fidelity/project-knowledge/00-start/` and `workspaces/fidelity/project-knowledge/03-context/process/` for project-facing onboarding and process rules
- `agent-memory/` for agent-specific behavior, learning, promotion, verification, and self-maintenance rules - `agent-memory/` for agent-specific behavior, learning, promotion, verification, and self-maintenance rules
Daily logs can preserve evidence, but they should not be the only place where a reusable behavior rule lives. Daily logs can preserve evidence, but they should not be the only place where a reusable behavior rule lives.

View File

@@ -1,11 +0,0 @@
# Mattermost Inbox
This directory stores raw or semi-processed communication captured from Mattermost before it is promoted into the persistent workspace context.
Recommended usage:
- `mattermost-latest.md` contains the most recent capture
- timestamped snapshots can be stored here if needed
- durable facts should be promoted into `vault/06-daily/`, `vault/01-current/`, `vault/02-work-items/`, `vault/03-context/`, `vault/04-people/`, or `vault/05-decisions/`
This directory is intentionally treated as an inbox, not as the final source of truth.

9
apps/mac/AIWorkspace/.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
.build/
.swiftpm/
dist/
DerivedData/
*.xcodeproj
*.xcworkspace
xcuserdata/
*.xcuserstate
.DS_Store

View File

@@ -0,0 +1,19 @@
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "AIWorkspace",
platforms: [
.macOS(.v13)
],
products: [
.executable(name: "AIWorkspace", targets: ["AIWorkspace"])
],
targets: [
.executableTarget(
name: "AIWorkspace",
path: "Sources"
)
]
)

View File

@@ -0,0 +1,94 @@
# AI Workspace macOS Menu Bar App
Minimal SwiftUI `MenuBarExtra` app for controlling local AI Workspace services.
The app is intentionally a thin UI over the service manager. It reads live status from:
```bash
python3 scripts/aiw/services.py status --profile fidelity --json
```
and sends lifecycle actions through `scripts/aiw/services.py`.
## Build
```bash
swift build --package-path apps/mac/AIWorkspace
```
## Package as `.app`
```bash
apps/mac/AIWorkspace/scripts/package-app.sh
```
Install to `~/Applications/AIWorkspace.app`:
```bash
apps/mac/AIWorkspace/scripts/package-app.sh --install
```
## Build a DMG
```bash
apps/mac/AIWorkspace/scripts/build-dmg.sh
```
This creates:
```text
apps/mac/AIWorkspace/dist/AIWorkspace.dmg
```
The DMG contains `AIWorkspace.app` and an `Applications` shortcut for drag-and-drop installation.
One-step local install, optionally enabling start at login and opening the app:
```bash
apps/mac/AIWorkspace/scripts/install.sh --start-at-login --open
```
## Start at login
Preferred: open the installed app and use the **Start at Login** toggle in the UI. The app uses macOS `SMAppService` for login item registration.
Development fallback after installing the app bundle:
```bash
apps/mac/AIWorkspace/scripts/install-start-at-login.sh
```
To remove the login item:
```bash
apps/mac/AIWorkspace/scripts/uninstall-start-at-login.sh
```
## Run during development
```bash
swift run --package-path apps/mac/AIWorkspace AIWorkspace
```
## Current actions
- Refresh status
- Start Fidelity services
- Stop Fidelity services
- Restart Context MCP
- Open Mattermost through the service manager
- Open Mattermost through the local proxy-managed launcher
- Run Doctor
- Copy Doctor JSON
- Copy Photo Inbox URL
- Copy recent logs
- Open MCP Health
- Open logs folder
- Open project knowledge
## Notes
- This is not yet packaged as a signed `.app` bundle.
- Start at login should be implemented later through a LaunchAgent or app login item.
- Start at Login is available in the app UI through `SMAppService`. The LaunchAgent scripts are retained as development fallback utilities.
- The app should remain a UI layer; service lifecycle remains in `scripts/aiw/services.py`.

View File

@@ -0,0 +1,502 @@
import AppKit
import Foundation
import ServiceManagement
import SwiftUI
private let workspaceRoot = URL(fileURLWithPath: "/Users/david/Developer/fidelity-ai-workspace", isDirectory: true)
private let defaultProfile = "fidelity"
@main
struct AIWorkspaceApp: App {
@StateObject private var model = ServiceStatusModel(profile: defaultProfile)
var body: some Scene {
MenuBarExtra {
ServiceMenuView(model: model)
.task {
await model.refresh()
}
} label: {
Label("AI Workspace", systemImage: model.menuBarSymbol)
}
.menuBarExtraStyle(.window)
}
}
@MainActor
final class ServiceStatusModel: ObservableObject {
@Published private(set) var report: StatusReport?
@Published private(set) var lastError: String?
@Published private(set) var lanIP: String?
@Published private(set) var loginItemStatus: SMAppService.Status = .notRegistered
@Published private(set) var isRefreshing = false
let profile: String
init(profile: String) {
self.profile = profile
}
var menuBarSymbol: String {
guard let report else { return "circle.dashed" }
if report.services.contains(where: { $0.status == "unhealthy" || ($0.enabled && $0.status == "stopped") }) {
return "exclamationmark.triangle"
}
if report.services.contains(where: { $0.status == "running" }) {
return "checkmark.circle"
}
return "circle"
}
func refresh() async {
isRefreshing = true
defer { isRefreshing = false }
do {
let data = try await ServiceManager.run(["status", "--profile", profile, "--json"])
report = try JSONDecoder().decode(StatusReport.self, from: data)
lanIP = await NetworkInfo.primaryLANIP()
loginItemStatus = SMAppService.mainApp.status
lastError = nil
} catch {
lastError = String(describing: error)
}
}
var startAtLoginEnabled: Bool {
loginItemStatus == .enabled
}
var startAtLoginStatusText: String {
switch loginItemStatus {
case .enabled: "enabled"
case .notRegistered: "off"
case .notFound: "not found"
case .requiresApproval: "requires approval"
@unknown default: "unknown"
}
}
func setStartAtLogin(_ enabled: Bool) {
do {
if enabled {
try SMAppService.mainApp.register()
} else {
try SMAppService.mainApp.unregister()
}
loginItemStatus = SMAppService.mainApp.status
lastError = nil
} catch {
loginItemStatus = SMAppService.mainApp.status
lastError = "Start at Login: \(error.localizedDescription)"
}
}
func startProfile() {
runAction(["start", "--profile", profile])
}
func stopProfile() {
runAction(["stop", "--profile", profile])
}
func restartMCP() {
runAction(["restart", "aiw-context-mcp", "--profile", profile])
}
func runDoctor() {
runAction(["doctor", "--profile", profile])
}
func copyDoctorJSON() {
Task {
do {
let data = try await ServiceManager.run(["doctor", "--profile", profile, "--json"])
copyToPasteboard(String(data: data, encoding: .utf8) ?? "")
} catch {
lastError = String(describing: error)
}
}
}
func copyPhotoInboxURL() {
let host = lanIP ?? "127.0.0.1"
copyToPasteboard("http://\(host):8787/upload")
}
func copyRecentLogs() {
Task {
do {
var chunks: [String] = []
for service in report?.services ?? [] {
let data = try await ServiceManager.run(["logs", service.name, "--profile", profile, "--lines", "30"])
if let text = String(data: data, encoding: .utf8), !text.isEmpty {
chunks.append(text)
}
}
copyToPasteboard(chunks.joined(separator: "\n\n"))
} catch {
lastError = String(describing: error)
}
}
}
func openMCPHealth() {
if let url = URL(string: "http://127.0.0.1:8765/health") {
NSWorkspace.shared.open(url)
}
}
func openLogsFolder() {
NSWorkspace.shared.open(workspaceRoot.appendingPathComponent(".aiw/runtime/logs", isDirectory: true))
}
func openProjectKnowledge() {
NSWorkspace.shared.open(workspaceRoot.appendingPathComponent("project-knowledge", isDirectory: true))
}
func openMattermost() {
runAction(["start", "mattermost-desktop", "--profile", profile])
}
private func copyToPasteboard(_ value: String) {
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(value, forType: .string)
}
private func runAction(_ arguments: [String]) {
Task {
do {
_ = try await ServiceManager.run(arguments)
await refresh()
} catch {
lastError = String(describing: error)
}
}
}
}
struct ServiceMenuView: View {
@ObservedObject var model: ServiceStatusModel
var body: some View {
VStack(alignment: .leading, spacing: 14) {
HStack(alignment: .firstTextBaseline) {
VStack(alignment: .leading, spacing: 3) {
Text("AI Workspace")
.font(.title3.bold())
Text("Profile: \(model.profile)")
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
Button {
Task { await model.refresh() }
} label: {
Image(systemName: "arrow.clockwise")
}
.buttonStyle(.borderless)
.help("Refresh")
}
Divider()
if let report = model.report {
VStack(spacing: 8) {
ForEach(report.services) { service in
ServiceRow(service: service)
}
}
} else if let error = model.lastError {
Label("Status unavailable", systemImage: "exclamationmark.triangle")
Text(error)
.font(.caption2)
.foregroundStyle(.secondary)
.lineLimit(3)
} else {
Label("Loading status...", systemImage: "hourglass")
}
if let lanIP = model.lanIP {
HStack(spacing: 8) {
Image(systemName: "wifi")
.foregroundStyle(.secondary)
Text("Photo Inbox LAN")
Spacer()
Text("http://\(lanIP):8787/upload")
.font(.caption.monospaced())
.foregroundStyle(.secondary)
}
.textSelection(.enabled)
}
Divider()
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)
}
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: "Copy Photo URL", systemImage: "link", action: model.copyPhotoInboxURL)
ActionButton(title: "Copy Logs", systemImage: "doc.text", action: model.copyRecentLogs)
ActionButton(title: "MCP Health", systemImage: "heart.text.square", action: model.openMCPHealth)
ActionButton(title: "Logs Folder", systemImage: "folder", action: model.openLogsFolder)
}
ActionButton(title: "Open Project Knowledge", systemImage: "books.vertical", action: model.openProjectKnowledge)
Divider()
VStack(alignment: .leading, spacing: 6) {
Toggle(isOn: Binding(
get: { model.startAtLoginEnabled },
set: { model.setStartAtLogin($0) }
)) {
Label("Start at Login", systemImage: "poweron")
}
.toggleStyle(.switch)
Text("Login item: \(model.startAtLoginStatusText)")
.font(.caption2)
.foregroundStyle(.secondary)
}
Divider()
HStack {
if let error = model.lastError {
Label(error, systemImage: "exclamationmark.triangle")
.font(.caption)
.foregroundStyle(.orange)
.lineLimit(2)
}
Spacer()
Button("Quit") {
NSApplication.shared.terminate(nil)
}
.keyboardShortcut("q")
}
}
.padding(18)
.frame(width: 430)
}
}
struct ActionButton: View {
let title: String
let systemImage: String
var role: ButtonRole?
let action: () -> Void
var body: some View {
Button(role: role, action: action) {
Label(title, systemImage: systemImage)
.frame(maxWidth: .infinity, alignment: .leading)
}
.buttonStyle(.bordered)
.controlSize(.small)
}
}
struct ServiceRow: View {
let service: ServiceStatus
var body: some View {
HStack(spacing: 10) {
Image(systemName: symbol)
.foregroundStyle(color)
.frame(width: 16, alignment: .center)
VStack(alignment: .leading, spacing: 2) {
Text(service.displayName)
.font(.body.weight(.medium))
Text(service.detail)
.font(.caption2)
.foregroundStyle(.secondary)
.lineLimit(1)
}
Spacer(minLength: 12)
StatusBadge(text: service.compactStatus, color: color)
}
.padding(.vertical, 7)
.padding(.horizontal, 10)
.background(.quaternary.opacity(0.6), in: RoundedRectangle(cornerRadius: 10, style: .continuous))
.help(service.detail)
}
private var symbol: String {
switch service.status {
case "running": "checkmark.circle"
case "launcher": "arrow.up.forward.app"
case "externally running": "link.circle"
case "unhealthy": "exclamationmark.triangle"
case "disabled": "minus.circle"
default: "xmark.circle"
}
}
private var color: Color {
switch service.status {
case "running", "launcher", "externally running": .green
case "unhealthy": .orange
case "disabled": .secondary
default: .red
}
}
}
struct StatusBadge: View {
let text: String
let color: Color
var body: some View {
Text(text)
.font(.caption.weight(.semibold))
.foregroundStyle(color)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(color.opacity(0.14), in: Capsule())
}
}
enum ServiceManager {
static func run(_ arguments: [String]) async throws -> Data {
try await Task.detached(priority: .userInitiated) {
let process = Process()
process.currentDirectoryURL = workspaceRoot
process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
process.arguments = ["python3", "scripts/aiw/services.py"] + arguments
let output = Pipe()
let error = Pipe()
process.standardOutput = output
process.standardError = error
try process.run()
process.waitUntilExit()
let data = output.fileHandleForReading.readDataToEndOfFile()
let errorData = error.fileHandleForReading.readDataToEndOfFile()
guard process.terminationStatus == 0 else {
let message = String(data: errorData.isEmpty ? data : errorData, encoding: .utf8) ?? "service command failed"
throw ServiceManagerError.commandFailed(message.trimmingCharacters(in: .whitespacesAndNewlines))
}
return data
}.value
}
}
enum NetworkInfo {
static func primaryLANIP() async -> String? {
for interface in ["en0", "en1"] {
if let value = try? await runIPConfig(interface), !value.isEmpty {
return value
}
}
return nil
}
private static func runIPConfig(_ interface: String) async throws -> String {
try await Task.detached(priority: .utility) {
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/sbin/ipconfig")
process.arguments = ["getifaddr", interface]
let output = Pipe()
process.standardOutput = output
process.standardError = Pipe()
try process.run()
process.waitUntilExit()
let data = output.fileHandleForReading.readDataToEndOfFile()
guard process.terminationStatus == 0 else { return "" }
return String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
}.value
}
}
enum ServiceManagerError: LocalizedError {
case commandFailed(String)
var errorDescription: String? {
switch self {
case .commandFailed(let message): message
}
}
}
struct StatusReport: Decodable {
let profile: String
let workspace: String
let runtime: String
let services: [ServiceStatus]
}
struct ServiceStatus: Decodable, Identifiable {
var id: String { name }
let name: String
let enabled: Bool
let kind: String
let status: String
let pid: Int?
let command: [String]
let health: Health
let state: [String: JSONValue]
var displayName: String {
switch name {
case "aiw-context-mcp": "Context MCP"
case "mattermost-proxy": "Mattermost Proxy"
case "mattermost-desktop": "Mattermost Desktop via Proxy"
case "photo-inbox": "Photo Inbox"
default: name
}
}
var detail: String {
if name == "mattermost-desktop" {
return "Launches Mattermost through local proxy 127.0.0.1:8080"
}
return health.detail
}
var compactStatus: String {
switch status {
case "externally running": "external"
default: status
}
}
}
struct Health: Decodable {
let ok: Bool?
let detail: String
}
enum JSONValue: Decodable {
case string(String)
case number(Double)
case bool(Bool)
case object([String: JSONValue])
case array([JSONValue])
case null
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if container.decodeNil() {
self = .null
} else if let value = try? container.decode(Bool.self) {
self = .bool(value)
} else if let value = try? container.decode(Double.self) {
self = .number(value)
} else if let value = try? container.decode(String.self) {
self = .string(value)
} else if let value = try? container.decode([String: JSONValue].self) {
self = .object(value)
} else {
self = .array(try container.decode([JSONValue].self))
}
}
}

View File

@@ -0,0 +1,27 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
APP_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
APP_NAME="AIWorkspace"
DIST_DIR="$APP_ROOT/dist"
APP_BUNDLE="$DIST_DIR/$APP_NAME.app"
DMG_STAGING="$DIST_DIR/dmg-staging"
DMG_PATH="$DIST_DIR/$APP_NAME.dmg"
bash "$SCRIPT_DIR/package-app.sh"
rm -rf "$DMG_STAGING" "$DMG_PATH"
mkdir -p "$DMG_STAGING"
cp -R "$APP_BUNDLE" "$DMG_STAGING/$APP_NAME.app"
ln -s /Applications "$DMG_STAGING/Applications"
hdiutil create \
-volname "AI Workspace" \
-srcfolder "$DMG_STAGING" \
-ov \
-format UDZO \
"$DMG_PATH"
rm -rf "$DMG_STAGING"
echo "Built $DMG_PATH"

View File

@@ -0,0 +1,40 @@
#!/usr/bin/env bash
set -euo pipefail
APP_PATH="${APP_PATH:-$HOME/Applications/AIWorkspace.app}"
PLIST_PATH="$HOME/Library/LaunchAgents/com.aiworkspace.menu.plist"
if [[ ! -d "$APP_PATH" ]]; then
echo "App not found: $APP_PATH" >&2
echo "Run apps/mac/AIWorkspace/scripts/package-app.sh --install first." >&2
exit 1
fi
mkdir -p "$HOME/Library/LaunchAgents"
cat > "$PLIST_PATH" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.aiworkspace.menu</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/open</string>
<string>-a</string>
<string>$APP_PATH</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>StandardOutPath</key>
<string>$HOME/Library/Logs/AIWorkspace-menu.log</string>
<key>StandardErrorPath</key>
<string>$HOME/Library/Logs/AIWorkspace-menu.err.log</string>
</dict>
</plist>
PLIST
launchctl unload "$PLIST_PATH" >/dev/null 2>&1 || true
launchctl load "$PLIST_PATH"
echo "Installed and loaded LaunchAgent: $PLIST_PATH"

View File

@@ -0,0 +1,35 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
INSTALL_LOGIN_ITEM=0
OPEN_APP=0
for arg in "$@"; do
case "$arg" in
--start-at-login)
INSTALL_LOGIN_ITEM=1
;;
--open)
OPEN_APP=1
;;
*)
echo "Unknown argument: $arg" >&2
echo "Usage: $0 [--start-at-login] [--open]" >&2
exit 1
;;
esac
done
bash "$SCRIPT_DIR/package-app.sh" --install
if [[ "$INSTALL_LOGIN_ITEM" == "1" ]]; then
bash "$SCRIPT_DIR/install-start-at-login.sh"
fi
if [[ "$OPEN_APP" == "1" ]]; then
open "$HOME/Applications/AIWorkspace.app"
fi
echo "AI Workspace app install complete."

View File

@@ -0,0 +1,64 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
APP_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
WORKSPACE_ROOT="$(cd "$APP_ROOT/../../.." && pwd)"
CONFIGURATION="${CONFIGURATION:-release}"
APP_NAME="AIWorkspace"
BUILD_DIR="$APP_ROOT/.build/$CONFIGURATION"
OUTPUT_DIR="$APP_ROOT/dist"
APP_BUNDLE="$OUTPUT_DIR/$APP_NAME.app"
INSTALL_DIR="${INSTALL_DIR:-$HOME/Applications}"
INSTALL=0
if [[ "${1:-}" == "--install" ]]; then
INSTALL=1
fi
swift build --package-path "$APP_ROOT" -c "$CONFIGURATION"
rm -rf "$APP_BUNDLE"
mkdir -p "$APP_BUNDLE/Contents/MacOS" "$APP_BUNDLE/Contents/Resources"
cp "$BUILD_DIR/$APP_NAME" "$APP_BUNDLE/Contents/MacOS/$APP_NAME"
cat > "$APP_BUNDLE/Contents/Info.plist" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$APP_NAME</string>
<key>CFBundleIdentifier</key>
<string>com.aiworkspace.menu</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>AI Workspace</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSMinimumSystemVersion</key>
<string>13.0</string>
<key>LSUIElement</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>Local AI Workspace utility.</string>
</dict>
</plist>
PLIST
echo "Built $APP_BUNDLE"
if [[ "$INSTALL" == "1" ]]; then
mkdir -p "$INSTALL_DIR"
rm -rf "$INSTALL_DIR/$APP_NAME.app"
cp -R "$APP_BUNDLE" "$INSTALL_DIR/$APP_NAME.app"
echo "Installed $INSTALL_DIR/$APP_NAME.app"
fi

View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
PLIST_PATH="$HOME/Library/LaunchAgents/com.aiworkspace.menu.plist"
launchctl unload "$PLIST_PATH" >/dev/null 2>&1 || true
rm -f "$PLIST_PATH"
echo "Removed LaunchAgent: $PLIST_PATH"

View File

@@ -51,7 +51,7 @@ See `core/memory/operational-memory.md` for the detailed rules.
Integrations extract evidence. They do not decide what becomes memory. Integrations extract evidence. They do not decide what becomes memory.
- live communication connectors write recent evidence to `ai/inbox/` - live communication connectors write recent evidence to `workspaces/<profile>/inbox/`
- historical archive connectors write selected evidence to `scripts/<source>/generated/` - historical archive connectors write selected evidence to `scripts/<source>/generated/`
- the agent promotes only high-confidence, project-relevant facts into memory - the agent promotes only high-confidence, project-relevant facts into memory

View File

@@ -18,7 +18,7 @@ Expected behavior:
- fetch recent messages from configured channels or conversations - fetch recent messages from configured channels or conversations
- support a forced refresh for "latest message" prompts - support a forced refresh for "latest message" prompts
- write the latest evidence to `ai/inbox/` - write the latest evidence to `workspaces/<profile>/inbox/`
- write status separately from project memory - write status separately from project memory
- fail safely without updating logs, state, or context - fail safely without updating logs, state, or context

View File

@@ -46,11 +46,11 @@ Technical runtime remains outside the vault:
- `scripts/` - `scripts/`
- `core/` - `core/`
- `profiles/` - `profiles/`
- `ai/inbox/` - `workspaces/<profile>/inbox/`
- `scripts/*/generated/` - `scripts/*/generated/`
- archives and local virtual environments - archives and local virtual environments
Communication evidence may exist under `ai/inbox/` or connector `generated/` folders, but promoted memory belongs in `project-knowledge/`. Communication evidence may exist under `workspaces/<profile>/inbox/` or connector `generated/` folders, but promoted memory belongs in the profile's project knowledge vault.
--- ---
@@ -76,7 +76,7 @@ Do not version local runtime state:
Recommended graph and search exclusions: Recommended graph and search exclusions:
- `ai/inbox/` - `workspaces/<profile>/inbox/`
- `archives/` - `archives/`
- `scripts/**/generated/` - `scripts/**/generated/`
- `scripts/**/.venv/` - `scripts/**/.venv/`

View File

@@ -0,0 +1,102 @@
---
type: service-design
status: active
updated: 2026-05-21
tags:
- ai-workspace
- rag
- index
---
# Local RAG Index
## Goal
Add retrieval over canonical workspace memory without replacing the human-readable `project-knowledge/` vault.
The local index is derived and disposable. If the index disagrees with Markdown, the Markdown wins.
---
## Current Implementation
The first implementation is dependency-free and lexical:
```text
scripts/aiw/indexer.py
```
It reads:
```text
project-knowledge/**/*.md
```
and writes:
```text
.aiw/indexes/<profile>/project-knowledge.jsonl
.aiw/indexes/<profile>/manifest.json
```
It skips:
```text
project-knowledge/09-templates/
```
so Obsidian templates do not appear as real memory.
---
## Commands
Build the index:
```bash
python3 scripts/aiw/indexer.py build --profile fidelity
```
Check index status:
```bash
python3 scripts/aiw/indexer.py status --profile fidelity
```
Search the index:
```bash
python3 scripts/aiw/indexer.py search "dismissal lifecycle" --profile fidelity
```
---
## MCP Exposure
`aiw-context-mcp` exposes:
```text
memory_hybrid_search
```
Current behavior:
- searches the derived local index when it exists
- returns cited paths, headings, snippets, scores, hashes, and mtimes
- falls back to live Markdown search when no index exists
- remains read-only
---
## Future Upgrade Path
This layer can later add:
- full-text ranking
- embeddings
- Qdrant or Chroma as a local vector store
- hybrid lexical + semantic search
- reranking
- Mattermost evidence indexing with strict source filters
Do not make the vector store canonical. It should remain rebuildable from Markdown and selected evidence.

View File

@@ -0,0 +1,67 @@
# macOS Installation Model
## How production macOS utilities commonly do it
Apps such as Cloudflare WARP, VPN clients, Docker Desktop, and device agents usually separate:
- a user-facing app or menu bar app;
- one or more background services;
- launchd configuration for automatic startup;
- privileged helpers only when system-level networking, drivers, packet filtering, or protected paths are required.
Common mechanisms:
- `LaunchAgent` in `~/Library/LaunchAgents` for per-user background/login startup.
- `LaunchDaemon` in `/Library/LaunchDaemons` for root/system services.
- `SMAppService` / login items for sandboxed or App Store-aligned apps.
- Privileged helper tools via `SMJobBless` when admin-level installation is required.
- `.pkg` installers when the install needs privileged locations, daemons, receipts, or managed deployment.
## Recommended AI Workspace approach
Use a staged model:
1. **Current local developer install**
- Build a real `.app` bundle into `apps/mac/AIWorkspace/dist/`.
- Install to `~/Applications/AIWorkspace.app`.
- Install a per-user `LaunchAgent` for start at login.
2. **Production-ready local install**
- Keep using a per-user LaunchAgent because services are local user tools and do not require root.
- Add a one-step installer script that builds, installs, optionally enables start at login, and opens the app.
- Avoid privileged helpers until a real system-level requirement appears.
3. **Future polished distribution**
- Create a signed/notarized `.app` distributed in a `.dmg` with an Applications shortcut, or a `.pkg` only if privileged installation becomes necessary.
- Use `SMAppService` for login item management from inside the app so the user can toggle Start at Login in the UI instead of running `launchctl` scripts manually.
- Add a small daemon API if the UI needs richer lifecycle control than shelling out to `services.py`.
## Desired user-grade install flow
For a Cloudflare/Docker-like local experience, the target should be:
1. User opens `AIWorkspace.dmg`.
2. User drags `AIWorkspace.app` to `/Applications` or `~/Applications`.
3. User launches the app.
4. App shows service status and a **Start at Login** toggle.
5. App registers/unregisters itself as a login item using `SMAppService`.
6. App starts/stops workspace services through the service manager.
The existing shell scripts remain useful for development and bootstrap, but should not be the primary end-user experience once the app handles login item registration itself.
## Current implementation state
- `AIWorkspace.app` can be packaged from `apps/mac/AIWorkspace/scripts/package-app.sh`.
- `AIWorkspace.dmg` can be built from `apps/mac/AIWorkspace/scripts/build-dmg.sh`.
- The app UI includes a **Start at Login** toggle backed by `SMAppService.mainApp`.
- LaunchAgent scripts remain as development fallbacks, not the preferred user path.
## Why not LaunchDaemon now
The current services are user-context services:
- Mattermost Desktop launching must happen in the user's GUI session.
- Photo Inbox writes to user-owned folders and uses clipboard/notifications.
- The MCP and proxy bind localhost ports and do not require root.
A root daemon would add unnecessary permission prompts and security risk. A per-user LaunchAgent is the correct production-leaning step for this stage.

View File

@@ -0,0 +1,81 @@
# AI Workspace Menu Bar App Design
## Goal
Provide a small native macOS control surface for the AI Workspace Service Manager.
The app should not reimplement service logic. It should call the service manager/daemon API or CLI and display status, actions, and diagnostics.
## Recommended Shape
- SwiftUI `MenuBarExtra` app.
- Local-only, no cloud dependency.
- Start at login optional through `LaunchAgent` later.
- Read-only status by default; explicit user actions for start/stop/restart.
- The UI should not own MCP profile selection. AI clients pass profiles to MCP tools/resources; the menu bar app is only a local operations surface.
## Initial UI
```text
AI Workspace ▾
Profile: Fidelity
Services
✓ Context MCP Running
✓ Mattermost Proxy Running
✓ Mattermost Desktop Launched
✓ Photo Inbox Running
Actions
Start Fidelity
Stop Fidelity
Restart Context MCP
Open Mattermost
Open Photo Inbox Folder
Copy Photo Inbox Upload URL
Copy Recent Logs
Open Project Knowledge
Open Logs
Diagnostics
Run Doctor
Copy Doctor JSON
Show Recent Errors
Settings
Start at Login
Open Config Folder
```
## Backend Contract
The first version can shell out to:
```bash
python3 scripts/aiw/services.py status --profile fidelity
python3 scripts/aiw/services.py status --profile fidelity --json
python3 scripts/aiw/services.py doctor --profile fidelity --json
python3 scripts/aiw/services.py start --profile fidelity
python3 scripts/aiw/services.py stop --profile fidelity
python3 scripts/aiw/services.py restart aiw-context-mcp --profile fidelity
```
Use `status --json` for frequent UI refreshes and `doctor --json` for explicit diagnostics. Longer term, prefer a small local daemon HTTP/Unix-socket API so the UI does not parse terminal text.
## Production-Ready Rules
- Do not store secrets in the app bundle.
- Do not expose services beyond localhost unless explicitly configured.
- Show whether a process is managed or externally running.
- Surface missing dependencies from doctor checks.
- Never let the app promote project memory automatically.
- Keep capture services and context MCP separate; the app only orchestrates lifecycle.
## Implementation Phases
1. CLI-backed SwiftUI menu bar app using `status --json` for live status and `doctor --json` for diagnostics.
2. Add profile selector and service action buttons.
3. Add LaunchAgent support for start at login.
4. Replace shell parsing with a daemon API if daily use proves stable.
See `multi-profile-runtime-model.md` for how this should evolve when multiple profiles run in parallel.

View File

@@ -0,0 +1,49 @@
# Multi-Profile Runtime Model
## Principle
Profiles are selected by clients and services, not by the menu bar UI.
The menu bar app is a local operations surface for the current operator machine. It can monitor the local services that are enabled now, but it should not become the source of truth for which project an AI client is allowed to query.
## Desired Model
- MCP clients choose the profile at call time, for example `{"profile":"fidelity"}`.
- Multiple profiles may run in parallel when their service ports and inbox paths do not conflict.
- Capture services can be profile-specific or shared:
- shared service: one Mattermost mirror with profile-scoped query filters;
- profile-specific service: separate mirror/output path/port per profile.
- The MCP query layer should remain profile-aware and read from profile manifests/config.
- The menu bar app should show local runtime health, not force a single global profile selection.
## Current Fidelity Setup
The first menu bar version targets the Fidelity service set because that is the active local workflow. This does not prevent MCP clients from querying other profiles when those profiles have canonical memory or context-source config.
## Parallel Profile Requirements
Before running another profile in parallel, define unique values for any conflicting service:
- MCP HTTP port if using separate MCP instances.
- Mattermost proxy listen port if using separate proxy instances.
- Photo Inbox port if using separate upload receivers.
- Mirror/inbox output directory.
- Profile-specific context channels.
Example future split:
```text
fidelity:
aiw-context-mcp: 127.0.0.1:8765
mattermost-proxy: 127.0.0.1:8080
photo-inbox: 0.0.0.0:8787
it-support:
aiw-context-mcp: same shared MCP, profile argument selects context
mattermost-proxy: 127.0.0.1:8081 if a separate capture session is needed
photo-inbox: 0.0.0.0:8788 if a separate receiver is needed
```
## Menu Bar Direction
The menu bar app should evolve toward showing service groups or all running profiles, but not a manual profile selector for MCP query behavior. Profile selection belongs in the MCP tool/resource arguments and client prompts.

View File

@@ -0,0 +1,28 @@
# AI Workspace Service Manager
## Principle
The AI Workspace should unify local service lifecycle without collapsing service responsibilities.
- Service manager: starts, stops, checks, and logs local services.
- Context MCP: exposes bounded read-only context to AI clients.
- Capture services: produce local evidence such as Mattermost mirror records or photo inbox files.
- Canonical memory remains under `project-knowledge/` and is updated by the agent using memory rules.
## Service Types
- `process`: long-running local command with PID, logs, and optional health check.
- `app-launcher`: one-shot command that opens an application or helper.
- `mcp`: a process service that exposes an MCP-compatible context interface.
## Profile Manifests
Project-specific services should be declared under `profiles/<profile>/services.json`.
Manifests should avoid project facts in reusable code. Profile-specific channel names, paths, ports, and enabled services belong in the profile manifest or local `.env` files.
## Responsibility Boundaries
Do not put capture lifecycle inside the context MCP. The MCP should query local evidence produced by capture services. The service manager may start both the MCP and capture services as one profile-level operation.
This keeps the same core usable for Fidelity, IT support, or another project with different communication sources.

68
docs/architecture.md Normal file
View File

@@ -0,0 +1,68 @@
# Architecture
AI Workspace is organized around explicit boundaries: profile configuration, raw evidence, canonical memory, derived retrieval, local services, and AI client adapters.
## System Flow
```text
Communication / screenshots / archives / manual notes
Raw inbox evidence
Agent or human curation
Canonical Markdown project knowledge
Derived local index
Read-only MCP context server
AI clients and agent workflows
```
## Responsibility Boundaries
| Layer | Responsibility | Canonical? |
|---|---|---|
| `core/` | Reusable architecture and operating model | yes, for workspace design |
| `profiles/<profile>/` | Project-specific configuration and assumptions | yes, for profile config |
| `workspaces/<profile>/project-knowledge/` | Human-readable project memory for the active profile | yes, for project facts |
| `workspaces/<profile>/inbox/` | Raw evidence captured from connectors | no |
| `.aiw/indexes/` | Rebuildable search indexes | no |
| `.aiw/runtime/` | PID files, logs, local service state | no |
| `scripts/aiw/` | Profile-aware service/index utilities | code source |
| `scripts/mcp/` | MCP servers exposing local context | code source |
| `apps/` | Local UI surfaces such as the macOS menu bar app | code source |
## Current Repository Shape
Profile-owned data lives under `workspaces/<profile>/`. Reusable code must resolve paths from `profiles/<profile>/workspace.json` rather than hardcoding profile-specific locations.
Current data layout:
```text
profiles/<profile>/workspace.json # where profile data lives
workspaces/<profile>/project-knowledge/
workspaces/<profile>/inbox/
.aiw/indexes/<profile>/
```
## Design Principles
- Keep the smallest useful context loaded by default.
- Prefer just-in-time retrieval over dumping the entire workspace into prompts.
- Keep human-readable Markdown as the project source of truth.
- Keep raw evidence outside canonical memory until explicitly promoted.
- Keep profile-specific facts out of `core/` and generic scripts.
- Make local services observable through a single service manager.
- Treat cloud memory systems as optional, not authoritative.
## Why This Shape
Current AI workflow guidance emphasizes context engineering: the model should receive the smallest high-signal context needed for the task. This workspace supports that by combining:
- structured Markdown memory for durable facts;
- raw evidence stores for auditability;
- local indexes for retrieval;
- MCP tools/resources for AI clients;
- profile-specific boundaries for reuse across projects.

56
docs/getting-started.md Normal file
View File

@@ -0,0 +1,56 @@
# Getting Started
AI Workspace is a local, profile-based context system for AI-assisted work. It keeps project memory, raw evidence, local services, and AI client integrations organized without making any single AI tool the source of truth.
## Core Idea
Use the workspace as a companion repo beside your real project or client work:
```text
implementation repo / corporate tools / chat evidence
AI Workspace inbox and memory curation
human-readable project knowledge
local index and MCP context server
OpenCode / Claude Code / Copilot / other AI clients
```
## First Run
From the repository root:
```bash
python3 scripts/aiw/services.py doctor --profile fidelity
python3 scripts/aiw/services.py status --profile fidelity
python3 scripts/aiw/indexer.py build --profile fidelity
```
The `fidelity` profile is the first real project profile in this repo. New projects should follow the same shape but keep their own profile configuration and project memory isolated.
## Daily Use
1. Open the project knowledge vault in Obsidian or your Markdown editor.
2. Start only the local services needed for the profile.
3. Capture raw evidence into the profile inbox.
4. Promote useful, verified facts into canonical Markdown.
5. Let AI clients query context through MCP or direct file reads.
## Key Rules
- Canonical project memory is Markdown, not chat history or vector storage.
- Inboxes contain evidence, not promoted memory.
- Indexes are derived and rebuildable.
- MCP is read-only by default.
- Secrets belong in ignored local `.env` files.
## Next Reading
- [Architecture](architecture.md)
- [Profiles](profiles.md)
- [Memory Model](memory-model.md)
- [MCP](mcp.md)
- [Services](services.md)
- [Security and Privacy](security-and-privacy.md)

67
docs/local-rag-index.md Normal file
View File

@@ -0,0 +1,67 @@
# Local RAG Index
The local RAG index is the first retrieval layer over canonical Markdown memory.
## Purpose
It helps AI clients quickly find relevant snippets without loading the whole project knowledge vault into context.
```text
project-knowledge/ Markdown
scripts/aiw/indexer.py
.aiw/indexes/<profile>/project-knowledge.jsonl
MCP tool: memory_hybrid_search
```
## Current Implementation
The current indexer is dependency-free and lexical. It is intentionally simple so it can run on a new machine without a vector database.
Build:
```bash
python3 scripts/aiw/indexer.py build --profile fidelity
```
Status:
```bash
python3 scripts/aiw/indexer.py status --profile fidelity
```
Search:
```bash
python3 scripts/aiw/indexer.py search "dismissal lifecycle" --profile fidelity
```
## What It Stores
- source path;
- heading;
- text chunk;
- mtime;
- content hash;
- chunk id.
## What It Does Not Do
- It does not replace Markdown.
- It does not write project facts.
- It does not index templates as real notes.
- It does not send data to a cloud service.
## Future Options
Future phases may add:
- better full-text ranking;
- semantic embeddings;
- Qdrant or Chroma as optional local vector stores;
- hybrid lexical + semantic search;
- index status in the menu bar app.
Keep this as a derived layer. The project knowledge vault remains canonical.

73
docs/mcp.md Normal file
View File

@@ -0,0 +1,73 @@
# MCP Integration
The Model Context Protocol (MCP) is the workspace's standard interface for exposing local context to AI clients.
## Role In AI Workspace
`aiw-context-mcp` is a read-only context server. It exposes bounded profile context through MCP tools and resources.
It should not:
- capture communication traffic;
- send messages;
- write canonical memory;
- promote facts automatically;
- expose secrets or raw credentials.
## MCP Concepts
MCP uses a host/client/server model:
- **Host**: the AI app, such as OpenCode, Claude Code, VS Code, Copilot, or another client.
- **Client**: the connection the host opens to a server.
- **Server**: a local or remote program that exposes context.
Servers expose primitives such as:
- **Tools**: callable functions.
- **Resources**: readable context objects.
- **Prompts**: reusable prompt templates.
AI Workspace currently focuses on tools and resources.
## Current Tools
Examples:
- `context_profiles`
- `project_current_context`
- `project_search_memory`
- `memory_hybrid_search`
- `communication_latest`
- `communication_date_context`
- `communication_standup_context`
- `photos_latest`
## Current Resources
Examples:
```text
aiw://profiles/fidelity/current-work
aiw://profiles/fidelity/work-items
aiw://profiles/fidelity/mattermost/latest
aiw://profiles/fidelity/photos/latest
```
## Security Posture
MCP tools can be model-controlled in many clients, so this workspace defaults to read-only context tools. If write tools are added later, they should require clear user intent, narrow scope, and audit-friendly outputs.
## Start The MCP Server
HTTP transport:
```bash
python3 scripts/aiw/services.py start aiw-context-mcp --profile fidelity
```
stdio transport:
```bash
python3 scripts/mcp/aiw-context-mcp/server.py --transport stdio
```

69
docs/memory-model.md Normal file
View File

@@ -0,0 +1,69 @@
# Memory Model
AI Workspace separates memory by purpose so that human-readable knowledge, raw evidence, agent behavior, and derived indexes do not collapse into one opaque store.
## Memory Layers
| Layer | Purpose | Examples | Canonical? |
|---|---|---|---|
| Canonical project memory | Durable project facts for humans and AI | current work, work items, people, decisions, daily notes | yes |
| Raw evidence | Captured data before curation | Mattermost mirror, photos, archives | no |
| Agent operating memory | Rules for agent behavior and workflows | promotion rules, communication style, verification rules | yes for agent behavior |
| Derived index | Fast retrieval over canonical memory | `.aiw/indexes/<profile>/` | no |
| External agent memory | Optional cross-agent recall | mem9, tool auto-memory | no for project truth |
## Canonical Markdown
The project knowledge vault should be readable without any AI tool:
```text
project-knowledge/
00-start/
01-current/
02-work-items/
03-context/
04-people/
05-decisions/
06-daily/
07-maps/
08-bases/
09-templates/
```
For future multi-profile setups, this same structure should live under a profile-specific workspace path.
## Evidence Promotion
Connectors write evidence. They do not decide what becomes memory.
Promotion flow:
```text
inbox evidence → verified fact → smallest correct Markdown file
```
Do not promote:
- secrets;
- sync status;
- generic chatter;
- unverified guesses;
- raw transcripts without curation.
## Local Index
The local RAG index is derived from canonical Markdown. It helps AI clients find relevant snippets quickly, but it is not the source of truth.
If index output conflicts with Markdown, Markdown wins.
## mem9 And Similar Memory Systems
mem9 can be useful as an optional cross-agent recall layer for preferences, reusable workflow habits, and non-sensitive operational memory. It should not replace the project knowledge vault.
Recommended stance:
```text
project-knowledge/ wins over mem9, vector stores, and chat memory.
```
For sensitive or corporate projects, avoid cloud memory ingestion unless the data policy is explicit and approved.

89
docs/profiles.md Normal file
View File

@@ -0,0 +1,89 @@
# Profiles
Profiles make the workspace reusable across projects, clients, or personal workflows.
Each profile should describe what the project is, where its memory lives, which communication sources matter, which local services are enabled, and how AI clients should access context.
## Current Profile Layout
```text
profiles/
fidelity/
profile.md
services.json
context-sources.json
example/
profile.md
```
## Recommended Future Layout
```text
profiles/<profile>/
profile.md # human-readable project profile
workspace.json # profile paths and defaults
services.json # local services for this profile
context-sources.json # communication/source filters
workspaces/<profile>/
project-knowledge/ # canonical Markdown vault
inbox/ # raw evidence for this profile
```
## Profile Files
### `profile.md`
Human-readable summary for agents and developers:
- project name;
- workspace role;
- communication sources;
- work-item system;
- stakeholders or roles;
- important domain themes;
- enabled workflows or skills.
### `workspace.json`
Profile path configuration:
```json
{
"profile": "example",
"display_name": "Example Project",
"knowledge_dir": "workspaces/example/project-knowledge",
"inbox_dir": "workspaces/example/inbox",
"index_dir": ".aiw/indexes/example"
}
```
Reusable scripts should resolve these paths through `scripts/aiw/profile.py`.
### `services.json`
Profile-specific local service manifest for `scripts/aiw/services.py`.
Examples:
- context MCP;
- communication proxy/mirror;
- photo inbox;
- future indexer or dashboard services.
### `context-sources.json`
Source filters for profile-bounded reads. For example, a Mattermost profile can define which channels count as high-signal context.
## Adding A New Project
1. Copy `profiles/example/` to `profiles/<new-project>/`.
2. Create or point to a project knowledge vault.
3. Define services only for integrations the project actually uses.
4. Put connector secrets in ignored `.env` files.
5. Build the local index.
6. Connect AI clients through the MCP server.
## Migration Rule
Reusable code should accept a `--profile` argument and resolve paths through profile configuration. Avoid adding new hardcoded references to Fidelity, channel names, ticket prefixes, or company-specific folders in generic scripts.

View File

@@ -0,0 +1,63 @@
# Security And Privacy
AI Workspace is designed for local-first, auditable context management. Treat it as a companion workspace that may contain sensitive project metadata and communication evidence.
## Rules
- Do not commit secrets, tokens, cookies, API keys, headers, or session IDs.
- Keep connector credentials in ignored `.env` files.
- Keep raw evidence outside canonical project memory until curated.
- Keep MCP read-only unless a write tool has explicit safety rules.
- Treat generated indexes as local artifacts because they may contain snippets from project notes.
- Prefer local services for corporate or confidential projects.
## Ignored Local State
Examples of local-only data:
```text
.aiw/runtime/
.aiw/indexes/
workspaces/*/inbox/mattermost-mirror/
scripts/*/.env
```
## Cloud Memory Systems
Tools such as mem9 or managed vector stores can be useful, but they introduce a data boundary.
Before enabling them for a project, decide:
- what data may be stored;
- whether cloud storage is allowed;
- whether self-hosting is required;
- who can inspect/delete memories;
- what happens when cloud memory conflicts with Markdown.
Default recommendation:
```text
Use cloud memory only for non-sensitive preferences unless a project policy approves broader use.
```
## MCP Safety
MCP clients may let models invoke tools automatically. For that reason, workspace MCP tools should stay read-only by default and return bounded, source-attributed context.
If future MCP write tools are added, require:
- explicit user intent;
- narrow target paths;
- clear diffs or summaries;
- no secret exposure;
- easy audit through git.
## Sharing The Repo
Before sharing or open-sourcing a reusable version:
1. Remove or isolate project-specific profile data.
2. Confirm ignored inbox/runtime files are not tracked.
3. Replace real profile examples with sanitized examples.
4. Keep reusable architecture docs in `docs/` and `core/`.
5. Keep confidential project knowledge in private profile/workspace data.

61
docs/services.md Normal file
View File

@@ -0,0 +1,61 @@
# Services
The AI Workspace Service Manager provides one profile-aware lifecycle surface for local services.
## Responsibility
The service manager starts, stops, checks, and tails logs. It does not merge service responsibilities.
```text
Service Manager → process lifecycle/status/logs
MCP Server → read-only context access
Connectors → raw evidence capture
Agent/Human → memory promotion
```
## Common Commands
```bash
python3 scripts/aiw/services.py status --profile fidelity
python3 scripts/aiw/services.py status --profile fidelity --json
python3 scripts/aiw/services.py doctor --profile fidelity
python3 scripts/aiw/services.py start --profile fidelity
python3 scripts/aiw/services.py stop --profile fidelity
python3 scripts/aiw/services.py logs aiw-context-mcp --profile fidelity
```
## Service Manifest
Services are declared per profile:
```text
profiles/<profile>/services.json
```
A service can define:
- command;
- kind;
- groups;
- dependencies;
- restart policy;
- doctor checks;
- health checks.
## Current Service Types
- `process`: long-running command with PID/log tracking.
- `app-launcher`: one-shot command that opens an app/helper.
- `mcp`: process service exposing an MCP-compatible context server.
## Runtime Files
Runtime artifacts are local and ignored:
```text
.aiw/runtime/pids/
.aiw/runtime/logs/
.aiw/runtime/state/
```
These files are operational state, not project memory.

View File

@@ -23,6 +23,11 @@
"OBSIDIAN_HOST": "127.0.0.1", "OBSIDIAN_HOST": "127.0.0.1",
"OBSIDIAN_PORT": "27124" "OBSIDIAN_PORT": "27124"
} }
},
"aiw-context-mcp": {
"type": "remote",
"url": "http://127.0.0.1:8765/mcp",
"enabled": true
} }
}, },
"permission": { "permission": {

View File

@@ -0,0 +1,8 @@
{
"profile": "example",
"display_name": "Example Project",
"description": "Sanitized example profile for adapting AI Workspace to a new project.",
"knowledge_dir": "workspaces/example/project-knowledge",
"inbox_dir": "workspaces/example/inbox",
"index_dir": ".aiw/indexes/example"
}

View File

@@ -0,0 +1,15 @@
{
"profile": "fidelity",
"communication_sources": {
"mattermost": {
"type": "mattermost_mirror",
"context_channels": [
"fidelity-preguntas",
"fidelity-standup",
"fidelity-code-review",
"fidelity-interface-meetings-on-calendar-outlook-team-etc",
"dm-david--jeff"
]
}
}
}

View File

@@ -20,9 +20,11 @@ It keeps Fidelity-specific context, integrations, commands, and skills separate
## Communication Sources ## Communication Sources
- Live communication: Mattermost - Live communication: Mattermost
- Preferred local Mattermost evidence source: proxy mirror under `workspaces/fidelity/inbox/mattermost-mirror/` when present; legacy `workspaces/fidelity/inbox/mattermost-latest.md` and `scripts/mattermost/generated/` are fallback evidence.
- Historical archive: Slack export - Historical archive: Slack export
- Preferred channel naming: readable channel names instead of raw IDs - Preferred channel naming: readable channel names instead of raw IDs
- Current high-signal channel: `fidelity-preguntas` - Current high-signal channel: `fidelity-preguntas`
- Focused Mattermost context for standups/latest project reads should come from configured profile/environment channels, not hardcoded connector defaults. For this profile, the useful context-channel set is currently `fidelity-preguntas`, `fidelity-standup`, `fidelity-code-review`, `fidelity-interface-meetings-on-calendar-outlook-team-etc`, and `dm-david--jeff`; keep that list in local `.env` as `AIW_MATTERMOST_CONTEXT_CHANNELS` or an equivalent profile setup when using the reusable Mattermost reader.
Compatibility environment variables: Compatibility environment variables:
@@ -38,22 +40,23 @@ Generic variables should be preferred for new setup:
- `AIW_SLACK_EXPORT_PATH` - `AIW_SLACK_EXPORT_PATH`
- `AIW_CHANNEL_PREFIX=fidelity` - `AIW_CHANNEL_PREFIX=fidelity`
- `AIW_PROJECT_PROFILE=fidelity` - `AIW_PROJECT_PROFILE=fidelity`
- `AIW_MATTERMOST_CONTEXT_CHANNELS=fidelity-preguntas,fidelity-standup,fidelity-code-review,fidelity-interface-meetings-on-calendar-outlook-team-etc,dm-david--jeff`
--- ---
## Work System ## Work System
- Work items are Jira-linked when available - Work items are Jira-linked when available
- Active ticket memory lives in `project-knowledge/02-work-items/` - Active ticket memory lives in `workspaces/fidelity/project-knowledge/02-work-items/`
- Compact active summary lives in `project-knowledge/01-current/work-items.md` - Compact active summary lives in `workspaces/fidelity/project-knowledge/01-current/work-items.md`
- Jira IDs and approved titles should remain visible for standups and manager updates - Jira IDs and approved titles should remain visible for standups and manager updates
--- ---
## Stakeholders ## Stakeholders
- Current manager mapping lives in `project-knowledge/04-people/manager.md` - Current manager mapping lives in `workspaces/fidelity/project-knowledge/04-people/manager.md`
- Person-specific collaboration context lives in `project-knowledge/04-people/` - Person-specific collaboration context lives in `workspaces/fidelity/project-knowledge/04-people/`
- Manager-facing messages should be concise, explicit, and natural professional US English - Manager-facing messages should be concise, explicit, and natural professional US English
--- ---
@@ -62,14 +65,14 @@ Generic variables should be preferred for new setup:
Core Fidelity context remains in: Core Fidelity context remains in:
- `project-knowledge/03-context/project.md` - `workspaces/fidelity/project-knowledge/03-context/project.md`
- `project-knowledge/03-context/systems/` - `workspaces/fidelity/project-knowledge/03-context/systems/`
- `project-knowledge/03-context/workstreams/` - `workspaces/fidelity/project-knowledge/03-context/workstreams/`
- `project-knowledge/03-context/ios/` - `workspaces/fidelity/project-knowledge/03-context/ios/`
- `project-knowledge/03-context/process/` - `workspaces/fidelity/project-knowledge/03-context/process/`
- `project-knowledge/05-decisions/` - `workspaces/fidelity/project-knowledge/05-decisions/`
Raw communication evidence remains outside the project knowledge vault under `ai/inbox/` and generated connector folders. Raw communication evidence remains outside the project knowledge vault under `workspaces/fidelity/inbox/` and generated connector folders.
Memory access should go through the project-agnostic `scripts/memory/` interface for note creation, search, Base queries, and health checks. Obsidian is the current visual and CLI adapter, not a Fidelity-specific dependency. Memory access should go through the project-agnostic `scripts/memory/` interface for note creation, search, Base queries, and health checks. Obsidian is the current visual and CLI adapter, not a Fidelity-specific dependency.

View File

@@ -0,0 +1,69 @@
{
"profile": "fidelity",
"description": "Local AI Workspace services for the Fidelity profile.",
"services": {
"aiw-context-mcp": {
"enabled": true,
"kind": "mcp",
"description": "Read-only AI Workspace context MCP server.",
"command": ["python3", "scripts/mcp/aiw-context-mcp/server.py", "--transport", "http"],
"groups": ["mcp", "context"],
"restart": "on-failure",
"doctor": {
"required_commands": ["python3"],
"required_paths": ["scripts/mcp/aiw-context-mcp/server.py"]
},
"health": {
"type": "http",
"url": "http://127.0.0.1:8765/health"
}
},
"mattermost-proxy": {
"enabled": true,
"kind": "process",
"description": "Local mitmproxy mirror that captures normalized Mattermost evidence.",
"command": ["scripts/mattermost-proxy/run-mirror.sh"],
"groups": ["communication", "mattermost", "capture"],
"restart": "on-failure",
"doctor": {
"required_commands": ["mitmdump"],
"optional_paths": ["scripts/mattermost-proxy/.env"]
},
"health": {
"type": "tcp",
"host": "127.0.0.1",
"port": 8080
}
},
"mattermost-desktop": {
"enabled": true,
"kind": "app-launcher",
"description": "Launch Mattermost Desktop through the local proxy.",
"command": ["scripts/mattermost-proxy/launch-mattermost.sh"],
"groups": ["communication", "mattermost"],
"depends_on": ["mattermost-proxy"],
"restart": "never",
"doctor": {
"required_paths": ["/Applications/Mattermost.app"],
"optional_paths": ["scripts/mattermost-proxy/.env"]
}
},
"photo-inbox": {
"enabled": true,
"kind": "process",
"description": "macOS HTTP receiver for iPhone photo uploads into the local inbox.",
"command": ["scripts/iphone-photo-inbox/run.sh"],
"groups": ["inbox", "photos", "capture"],
"restart": "on-failure",
"doctor": {
"required_commands": ["python3"],
"optional_commands": ["swiftc"],
"optional_paths": ["scripts/iphone-photo-inbox/.env"]
},
"health": {
"type": "http",
"url": "http://127.0.0.1:8787/health"
}
}
}
}

View File

@@ -0,0 +1,8 @@
{
"profile": "fidelity",
"display_name": "Fidelity",
"description": "Current Fidelity AI Workspace profile with isolated profile-owned data paths.",
"knowledge_dir": "workspaces/fidelity/project-knowledge",
"inbox_dir": "workspaces/fidelity/inbox",
"index_dir": ".aiw/indexes/fidelity"
}

View File

@@ -1,24 +0,0 @@
- Experiment-driven investigation
- Debug print auto added
- Iteratively detect now possible debug prints
- Structured ticket artifacts with jira, patch, prompts, sessions
- Tolerant to pre-experiment fix steps to fix any build error between experiments
- Swift skill, specialized to debug prints
- Xcode logs analysis
- Find and read the full Xcode artifacts
- Extract relevant logs
- Xcode Integration
- Efficient use of xcode commands to build, test, contexted for tuist, cocoapods, sample projects
- Execute unit test proficiently, like executing only new tests or related
- Investigation: Differences from skills with cli commands vs mcps
- Investigation: Differences of skills vs instructions from vscode copilot
- Investigation: Differences from agents vs skills using agent, what is more general? correct relationship and use
- Charles Proxy integration
- LaunchDarkly integration
- Teams integration
- Photo uploader
- Start as a service
- Auto categorize by context
- Multi photos session, copy multiples images in clipboard
- Swiftlint integration
- Auto validator

View File

@@ -1,6 +1,6 @@
# Manager Update Prompt # Manager Update Prompt
Use the current state, today's daily note, `project-knowledge/01-current/work-items.md`, the relevant files under `project-knowledge/02-work-items/`, `project-knowledge/03-context/project.md`, `project-knowledge/03-context/process/communication.md`, `project-knowledge/04-people/manager.md`, and `project-knowledge/04-people/index.md`. Use the current state, today's daily note, `workspaces/fidelity/project-knowledge/01-current/work-items.md`, the relevant files under `workspaces/fidelity/project-knowledge/02-work-items/`, `workspaces/fidelity/project-knowledge/03-context/project.md`, `workspaces/fidelity/project-knowledge/03-context/process/communication.md`, `workspaces/fidelity/project-knowledge/04-people/manager.md`, and `workspaces/fidelity/project-knowledge/04-people/index.md`.
Draft a Mattermost update for the current manager or stakeholder in concise, natural, professional US English. Draft a Mattermost update for the current manager or stakeholder in concise, natural, professional US English.

View File

@@ -2,14 +2,14 @@
> **Format note (2026-05-13):** Mattermost requires a visible blank line before section headers like `Today:` and `Blockers:` for proper rendering. Always emit two newline characters between the previous bullet/list item and the next section header; never place `Today:` immediately after a list item. > **Format note (2026-05-13):** Mattermost requires a visible blank line before section headers like `Today:` and `Blockers:` for proper rendering. Always emit two newline characters between the previous bullet/list item and the next section header; never place `Today:` immediately after a list item.
Use `project-knowledge/01-current/current-work.md`, `project-knowledge/01-current/work-items.md`, the detailed files referenced from that active-work summary, `project-knowledge/03-context/project.md`, `project-knowledge/03-context/workstreams/index.md`, `project-knowledge/03-context/process/communication.md`, `project-knowledge/04-people/manager.md`, the previous workday communication context, today's daily note if present, and the latest available communication context. Use `workspaces/fidelity/project-knowledge/01-current/current-work.md`, `workspaces/fidelity/project-knowledge/01-current/work-items.md`, the detailed files referenced from that active-work summary, `workspaces/fidelity/project-knowledge/03-context/project.md`, `workspaces/fidelity/project-knowledge/03-context/workstreams/index.md`, `workspaces/fidelity/project-knowledge/03-context/process/communication.md`, `workspaces/fidelity/project-knowledge/04-people/manager.md`, the previous workday communication context, today's daily note if present, and the latest available communication context.
Generate a standup update for the active project profile. Generate a standup update for the active project profile.
## Required refresh ## Required refresh
- At the start of the day, fetch Mattermost before drafting. - At the start of the day, fetch or read refreshed Mattermost evidence before drafting. Prefer the local proxy mirror through `scripts/mattermost-proxy/read-context.py` when it exists; legacy sync output is fallback evidence.
- Fetch both latest available messages and previous-workday activity when the connector supports both modes. - Fetch focused standup evidence with `python3 scripts/mattermost-proxy/read-context.py --mode standup --today YYYY-MM-DD`; this reads previous-workday and today records from date-bucketed mirror files and should filter through the active profile's configured `AIW_MATTERMOST_CONTEXT_CHANNELS` when available. Do not read broad `latest.md` for standups unless the focused date-bucketed view is unavailable and you explicitly label the fallback as broad/noisy.
- If Mattermost refresh fails, say so internally and use only saved workspace memory with clear caution; do not invent fresher context. - If Mattermost refresh fails, say so internally and use only saved workspace memory with clear caution; do not invent fresher context.
- Do not skip communication refresh for standup just to reduce latency, because stale standups cost more time to correct later. - Do not skip communication refresh for standup just to reduce latency, because stale standups cost more time to correct later.

View File

@@ -2,6 +2,7 @@
This directory contains helpers that automate: This directory contains helpers that automate:
- AI Workspace local service lifecycle
- context aggregation - context aggregation
- canonical memory access - canonical memory access
- standup generation - standup generation
@@ -15,13 +16,32 @@ The project-agnostic memory interface lives in:
Recommended commands: Recommended commands:
```bash
python3 scripts/aiw/services.py status --profile fidelity
python3 scripts/aiw/services.py doctor --profile fidelity
python3 scripts/aiw/services.py start --profile fidelity
```
The service manager reads `profiles/<profile>/services.json` and manages local
services such as the Mattermost proxy mirror and Photo Inbox. Runtime PID/state
and logs stay under `.aiw/runtime/`.
The local context MCP server lives in:
- `scripts/mcp/aiw-context-mcp/`
It exposes read-only workspace context to local AI clients and is started as the
`aiw-context-mcp` service for profiles that enable it.
Recommended memory commands:
```bash ```bash
bash scripts/memory/memory.sh health bash scripts/memory/memory.sh health
bash scripts/memory/memory.sh search "PDIAP-15765" bash scripts/memory/memory.sh search "PDIAP-15765"
bash scripts/memory/memory.sh create work-item pdiap-15999 "Example title" bash scripts/memory/memory.sh create work-item pdiap-15999 "Example title"
``` ```
This interface defaults to Markdown files under `project-knowledge/`, uses Obsidian CLI when useful and available, and falls back to direct file operations. This interface resolves the canonical Markdown directory from `profiles/<profile>/workspace.json`, uses Obsidian CLI when useful and available, and falls back to direct file operations.
The default workspace Mattermost extractor now lives in: The default workspace Mattermost extractor now lives in:
@@ -59,7 +79,7 @@ Expected behavior:
- avoid interactive prompts - avoid interactive prompts
- return a non-zero exit code on failure - return a non-zero exit code on failure
OpenCode can then use that output to refresh `ai/inbox/mattermost-latest.md` proactively. OpenCode can then use that output to refresh the active profile inbox proactively. When the local Mattermost proxy mirror is running, commands should prefer `<profile inbox>/mattermost-mirror/` through `scripts/mattermost-proxy/read-context.py --profile <profile>` and use legacy sync output only as fallback evidence.
Historical Slack exports can also be imported through: Historical Slack exports can also be imported through:

84
scripts/aiw/README.md Normal file
View File

@@ -0,0 +1,84 @@
# AI Workspace Service Manager
The service manager is the local lifecycle layer for AI Workspace services.
It reads `profiles/<profile>/services.json`, starts/stops enabled services, records logs under `.aiw/runtime/logs/`, and keeps PID/state files under `.aiw/runtime/`.
## Common commands
```bash
python3 scripts/aiw/services.py status --profile fidelity
python3 scripts/aiw/services.py status --profile fidelity --json
python3 scripts/aiw/services.py doctor --profile fidelity
python3 scripts/aiw/services.py doctor --profile fidelity --json
python3 scripts/aiw/services.py start --profile fidelity
python3 scripts/aiw/services.py stop --profile fidelity
python3 scripts/aiw/services.py logs mattermost-proxy --profile fidelity
```
Start a subset by group:
```bash
python3 scripts/aiw/services.py start --profile fidelity --group communication
python3 scripts/aiw/services.py start --profile fidelity --group inbox
```
## Current Fidelity services
- `mattermost-proxy`: runs the local Mattermost proxy mirror.
- `mattermost-desktop`: launches Mattermost Desktop through the proxy.
- `photo-inbox`: runs the local HTTP photo receiver.
- `aiw-context-mcp`: read-only context MCP server for local AI clients.
The service manager unifies startup and status. It does not move capture behavior into the MCP.
## Local project-knowledge index
The workspace includes a dependency-free local indexer for canonical Markdown memory. The index is derived from the profile's configured `knowledge_dir` and written under the profile's configured `index_dir`; it is safe to delete and rebuild.
```bash
python3 scripts/aiw/indexer.py build --profile fidelity
python3 scripts/aiw/indexer.py status --profile fidelity
python3 scripts/aiw/indexer.py search "dismissal lifecycle" --profile fidelity
```
`aiw-context-mcp` exposes the same derived search through the read-only `memory_hybrid_search` tool and falls back to live Markdown search if the index has not been built yet.
## Profile path configuration
Reusable scripts resolve profile-specific paths through:
```text
profiles/<profile>/workspace.json
```
Current fields:
```json
{
"knowledge_dir": "workspaces/fidelity/project-knowledge",
"inbox_dir": "workspaces/fidelity/inbox",
"index_dir": ".aiw/indexes/fidelity"
}
```
Use `scripts/aiw/profile.py` from new scripts instead of hardcoding root-level project memory or inbox paths.
## Robustness features
- Manifest validation before lifecycle actions.
- Dependency-aware startup through `depends_on`.
- Managed PID/state files under `.aiw/runtime/`.
- Per-service logs under `.aiw/runtime/logs/`.
- Simple log rotation before service start.
- TCP/HTTP health checks.
- Doctor checks for required commands and paths declared in the profile manifest.
- Protection against starting duplicate services when a matching health check is already passing externally.
## Tests
```bash
python3 scripts/aiw/test_services.py
python3 scripts/aiw/test_profile.py
python3 scripts/aiw/test_indexer.py
```

261
scripts/aiw/indexer.py Normal file
View File

@@ -0,0 +1,261 @@
#!/usr/bin/env python3
"""Dependency-free local indexer for AI Workspace canonical Markdown memory.
This is intentionally a small lexical/hybrid-ready index. It keeps
`project-knowledge/` as the source of truth and writes a derived, disposable
JSONL index under `.aiw/indexes/<profile>/`.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import re
import sys
import time
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[2]
DEFAULT_PROFILE = "fidelity"
MAX_CHARS = 1800
OVERLAP_CHARS = 180
sys.path.insert(0, str(Path(__file__).resolve().parent))
import profile as aiw_profile # noqa: E402
@dataclass(frozen=True)
class Chunk:
chunk_id: str
path: str
heading: str
text: str
mtime: float
sha256: str
def project_knowledge_dir(profile: str) -> Path:
return aiw_profile.knowledge_dir(profile, root=ROOT)
def index_dir(profile: str) -> Path:
return aiw_profile.index_dir(profile, root=ROOT)
def rel(path: Path) -> str:
return aiw_profile.relative_to_root(path, root=ROOT)
def index_path(profile: str) -> Path:
return index_dir(profile) / "project-knowledge.jsonl"
def manifest_path(profile: str) -> Path:
return index_dir(profile) / "manifest.json"
def normalize_space(text: str) -> str:
return re.sub(r"\s+", " ", text).strip()
def tokens(text: str) -> set[str]:
return {item for item in re.findall(r"[a-z0-9][a-z0-9_-]{1,}", text.lower()) if len(item) > 1}
def iter_markdown_files(base: Path) -> list[Path]:
files: list[Path] = []
for path in sorted(base.rglob("*.md")):
rel = path.relative_to(base)
if str(rel).startswith("09-templates/"):
continue
files.append(path)
return files
def heading_for_line(line: str, current: str) -> str:
stripped = line.strip()
if stripped.startswith("#"):
return stripped.lstrip("#").strip() or current
return current
def split_sections(text: str) -> list[tuple[str, str]]:
sections: list[tuple[str, list[str]]] = [("", [])]
current_heading = ""
for line in text.splitlines():
new_heading = heading_for_line(line, current_heading)
if new_heading != current_heading and line.strip().startswith("#"):
current_heading = new_heading
sections.append((current_heading, [line]))
else:
sections[-1][1].append(line)
return [(heading, "\n".join(lines).strip()) for heading, lines in sections if "\n".join(lines).strip()]
def chunk_text(section_text: str, max_chars: int = MAX_CHARS, overlap_chars: int = OVERLAP_CHARS) -> list[str]:
text = section_text.strip()
if len(text) <= max_chars:
return [text] if text else []
chunks: list[str] = []
start = 0
while start < len(text):
end = min(len(text), start + max_chars)
if end < len(text):
boundary = max(text.rfind("\n\n", start, end), text.rfind(". ", start, end))
if boundary > start + max_chars // 2:
end = boundary + 1
chunk = text[start:end].strip()
if chunk:
chunks.append(chunk)
if end >= len(text):
break
start = max(0, end - overlap_chars)
return chunks
def build_chunks(profile: str) -> list[Chunk]:
base = project_knowledge_dir(profile)
chunks: list[Chunk] = []
for path in iter_markdown_files(base):
raw = path.read_text(encoding="utf-8", errors="replace")
rel_path = rel(path)
digest = hashlib.sha256(raw.encode("utf-8", errors="replace")).hexdigest()
mtime = path.stat().st_mtime
for section_index, (heading, section) in enumerate(split_sections(raw)):
for chunk_index, chunk in enumerate(chunk_text(section)):
chunk_digest = hashlib.sha256(f"{rel_path}\n{section_index}\n{chunk_index}\n{chunk}".encode("utf-8")).hexdigest()[:16]
chunks.append(Chunk(chunk_id=chunk_digest, path=rel_path, heading=heading, text=chunk, mtime=mtime, sha256=digest))
return chunks
def write_index(profile: str) -> dict[str, Any]:
out_dir = index_dir(profile)
out_dir.mkdir(parents=True, exist_ok=True)
chunks = build_chunks(profile)
with index_path(profile).open("w", encoding="utf-8") as handle:
for chunk in chunks:
handle.write(json.dumps(chunk.__dict__, ensure_ascii=False, sort_keys=True) + "\n")
files = sorted({chunk.path for chunk in chunks})
manifest = {
"profile": profile,
"source": rel(project_knowledge_dir(profile)),
"canonical": False,
"derived_from": "project-knowledge",
"index_type": "lexical-markdown-chunks",
"created_at": datetime.now(timezone.utc).isoformat(),
"file_count": len(files),
"chunk_count": len(chunks),
"index_path": rel(index_path(profile)),
}
manifest_path(profile).write_text(json.dumps(manifest, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8")
return manifest
def read_index(profile: str) -> list[dict[str, Any]]:
path = index_path(profile)
if not path.is_file():
return []
rows: list[dict[str, Any]] = []
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
if not line.strip():
continue
try:
rows.append(json.loads(line))
except json.JSONDecodeError:
continue
return rows
def score_chunk(query: str, query_tokens: set[str], chunk: dict[str, Any]) -> float:
text = str(chunk.get("text") or "")
haystack = f"{chunk.get('path', '')} {chunk.get('heading', '')} {text}".lower()
exact = haystack.count(query.lower())
chunk_tokens = tokens(haystack)
overlap = len(query_tokens & chunk_tokens)
if exact == 0 and overlap == 0:
return 0.0
heading_bonus = 1.5 if query.lower() in str(chunk.get("heading") or "").lower() else 0.0
path_bonus = 1.0 if query.lower() in str(chunk.get("path") or "").lower() else 0.0
return exact * 5.0 + overlap * 1.25 + heading_bonus + path_bonus
def snippet_for(query: str, text: str, width: int = 520) -> str:
lowered = text.lower()
index = lowered.find(query.lower()) if query else -1
if index < 0:
query_terms = tokens(query)
candidates = [lowered.find(term) for term in query_terms if lowered.find(term) >= 0]
index = min(candidates) if candidates else 0
start = max(0, index - width // 2)
end = min(len(text), start + width)
return normalize_space(text[start:end])
def search_index(profile: str, query: str, limit: int = 10) -> dict[str, Any]:
query = query.strip()
if not query:
raise SystemExit("query is required")
rows = read_index(profile)
query_tokens = tokens(query)
scored: list[tuple[float, dict[str, Any]]] = []
for row in rows:
score = score_chunk(query, query_tokens, row)
if score > 0:
scored.append((score, row))
scored.sort(key=lambda item: (-item[0], item[1].get("path", ""), item[1].get("chunk_id", "")))
matches = []
for score, row in scored[:limit]:
matches.append({
"score": round(score, 3),
"path": row.get("path"),
"heading": row.get("heading"),
"chunk_id": row.get("chunk_id"),
"snippet": snippet_for(query, str(row.get("text") or "")),
"mtime": row.get("mtime"),
"sha256": row.get("sha256"),
})
manifest = {}
if manifest_path(profile).is_file():
manifest = json.loads(manifest_path(profile).read_text(encoding="utf-8"))
return {"profile": profile, "query": query, "canonical": False, "source": "derived-index", "manifest": manifest, "matches": matches}
def status(profile: str) -> dict[str, Any]:
manifest_file = manifest_path(profile)
if not manifest_file.is_file():
return {"profile": profile, "indexed": False, "index_path": rel(index_path(profile))}
manifest = json.loads(manifest_file.read_text(encoding="utf-8"))
path = index_path(profile)
manifest["indexed"] = path.is_file()
manifest["index_bytes"] = path.stat().st_size if path.is_file() else 0
manifest["age_seconds"] = int(time.time() - datetime.fromisoformat(manifest["created_at"]).timestamp()) if manifest.get("created_at") else None
return manifest
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
subparsers = parser.add_subparsers(dest="command", required=True)
for name in ["build", "status"]:
command = subparsers.add_parser(name)
command.add_argument("--profile", default=DEFAULT_PROFILE)
search = subparsers.add_parser("search")
search.add_argument("query")
search.add_argument("--profile", default=DEFAULT_PROFILE)
search.add_argument("--limit", type=int, default=10)
args = parser.parse_args()
if args.command == "build":
payload = write_index(args.profile)
elif args.command == "search":
payload = search_index(args.profile, args.query, limit=max(1, min(args.limit, 50)))
else:
payload = status(args.profile)
print(json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True))
if __name__ == "__main__":
main()

107
scripts/aiw/profile.py Normal file
View File

@@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""Profile path resolution for AI Workspace scripts.
Profiles own their configuration. Reusable scripts should call this module
instead of hardcoding root-level project paths.
"""
from __future__ import annotations
import json
import argparse
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[2]
DEFAULT_WORKSPACE = {
"knowledge_dir": "workspaces/{profile}/project-knowledge",
"inbox_dir": "workspaces/{profile}/inbox",
"index_dir": ".aiw/indexes/{profile}",
}
def workspace_config_path(profile: str, root: Path | None = None) -> Path:
base = root or ROOT
return base / "profiles" / profile / "workspace.json"
def load_workspace_config(profile: str, root: Path | None = None) -> dict[str, Any]:
base = root or ROOT
config = dict(DEFAULT_WORKSPACE)
config["profile"] = profile
path = workspace_config_path(profile, root=base)
if path.is_file():
try:
loaded = json.loads(path.read_text(encoding="utf-8"))
if isinstance(loaded, dict):
config.update(loaded)
except json.JSONDecodeError:
pass
return config
def resolve_path(raw: str | None, *, profile: str, root: Path | None = None, fallback: str) -> Path:
base = root or ROOT
value = (raw or fallback).format(profile=profile)
path = Path(value).expanduser()
return path if path.is_absolute() else base / path
def knowledge_dir(profile: str, root: Path | None = None) -> Path:
config = load_workspace_config(profile, root=root)
return resolve_path(config.get("knowledge_dir"), profile=profile, root=root, fallback="workspaces/{profile}/project-knowledge")
def inbox_dir(profile: str, root: Path | None = None) -> Path:
config = load_workspace_config(profile, root=root)
return resolve_path(config.get("inbox_dir"), profile=profile, root=root, fallback="workspaces/{profile}/inbox")
def index_dir(profile: str, root: Path | None = None) -> Path:
config = load_workspace_config(profile, root=root)
return resolve_path(config.get("index_dir"), profile=profile, root=root, fallback=".aiw/indexes/{profile}")
def relative_to_root(path: Path, root: Path | None = None) -> str:
base = root or ROOT
try:
return str(path.relative_to(base))
except ValueError:
return str(path)
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
subparsers = parser.add_subparsers(dest="command", required=True)
path_parser = subparsers.add_parser("path", help="Print a resolved profile path.")
path_parser.add_argument("kind", choices=["knowledge", "inbox", "index"])
path_parser.add_argument("--profile", default="fidelity")
config_parser = subparsers.add_parser("config", help="Print resolved workspace configuration as JSON.")
config_parser.add_argument("--profile", default="fidelity")
args = parser.parse_args()
if args.command == "path":
if args.kind == "knowledge":
print(knowledge_dir(args.profile))
elif args.kind == "inbox":
print(inbox_dir(args.profile))
else:
print(index_dir(args.profile))
return
config = load_workspace_config(args.profile)
config["resolved"] = {
"knowledge_dir": str(knowledge_dir(args.profile)),
"inbox_dir": str(inbox_dir(args.profile)),
"index_dir": str(index_dir(args.profile)),
}
print(json.dumps(config, ensure_ascii=False, indent=2, sort_keys=True))
if __name__ == "__main__":
main()

507
scripts/aiw/services.py Normal file
View File

@@ -0,0 +1,507 @@
#!/usr/bin/env python3
"""AI Workspace local service manager.
This is the profile-aware lifecycle layer for local capture/query services. It is
intentionally small and dependency-free so it can run before the future desktop
UI or MCP server exists.
"""
from __future__ import annotations
import argparse
import json
import os
import signal
import socket
import subprocess
import sys
import time
import urllib.error
import urllib.request
from dataclasses import dataclass
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[2]
RUNTIME_DIR = ROOT / ".aiw" / "runtime"
PID_DIR = RUNTIME_DIR / "pids"
LOG_DIR = RUNTIME_DIR / "logs"
STATE_DIR = RUNTIME_DIR / "state"
DEFAULT_LOG_MAX_BYTES = 5 * 1024 * 1024
DEFAULT_LOG_BACKUPS = 3
DEFAULT_STOP_TIMEOUT_SECONDS = 5.0
@dataclass(frozen=True)
class ServiceRef:
name: str
config: dict[str, Any]
def ensure_runtime() -> None:
for path in [PID_DIR, LOG_DIR, STATE_DIR]:
path.mkdir(parents=True, exist_ok=True)
def manifest_path(profile: str) -> Path:
return ROOT / "profiles" / profile / "services.json"
def load_manifest(profile: str) -> dict[str, Any]:
path = manifest_path(profile)
if not path.is_file():
raise SystemExit(f"services manifest not found: {path}")
return json.loads(path.read_text(encoding="utf-8"))
def validate_manifest(manifest: dict[str, Any]) -> list[str]:
errors: list[str] = []
services = manifest.get("services")
if not isinstance(services, dict):
return ["manifest must contain a services object"]
for name, config in services.items():
if not isinstance(config, dict):
errors.append(f"{name}: service config must be an object")
continue
command = config.get("command")
if config.get("enabled", True) and (not isinstance(command, list) or not command):
errors.append(f"{name}: enabled services require a non-empty command list")
kind = config.get("kind", "process")
if kind not in {"process", "app-launcher", "mcp"}:
errors.append(f"{name}: unsupported kind {kind!r}")
restart = config.get("restart", "never")
if restart not in {"never", "on-failure", "always"}:
errors.append(f"{name}: unsupported restart policy {restart!r}")
for dependency in config.get("depends_on") or []:
if dependency not in services:
errors.append(f"{name}: depends on unknown service {dependency!r}")
return errors
def service_items(manifest: dict[str, Any], include_disabled: bool = False) -> list[ServiceRef]:
services = manifest.get("services") or {}
refs: list[ServiceRef] = []
for name, config in services.items():
if not include_disabled and not config.get("enabled", True):
continue
refs.append(ServiceRef(name, config))
return refs
def select_services(manifest: dict[str, Any], names: list[str], group: str | None, include_disabled: bool = False) -> list[ServiceRef]:
refs = service_items(manifest, include_disabled=include_disabled)
by_name = {ref.name: ref for ref in refs}
if names:
selected: list[ServiceRef] = []
missing: list[str] = []
for name in names:
ref = by_name.get(name)
if ref is None:
missing.append(name)
else:
selected.append(ref)
if missing:
raise SystemExit("unknown or disabled service(s): " + ", ".join(missing))
return selected
if group:
return [ref for ref in refs if group in (ref.config.get("groups") or [])]
return refs
def pid_path(profile: str, service: str) -> Path:
return PID_DIR / profile / f"{service}.pid"
def state_path(profile: str, service: str) -> Path:
return STATE_DIR / profile / f"{service}.json"
def log_path(profile: str, service: str) -> Path:
return LOG_DIR / profile / f"{service}.log"
def resolve_workspace_path(raw: str) -> Path:
path = Path(raw).expanduser()
return path if path.is_absolute() else ROOT / path
def command_exists(command: str) -> 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
def rotate_log_if_needed(path: Path, max_bytes: int = DEFAULT_LOG_MAX_BYTES, backups: int = DEFAULT_LOG_BACKUPS) -> None:
if max_bytes <= 0 or backups <= 0 or not path.exists() or path.stat().st_size < max_bytes:
return
oldest = path.with_suffix(path.suffix + f".{backups}")
oldest.unlink(missing_ok=True)
for index in range(backups - 1, 0, -1):
src = path.with_suffix(path.suffix + f".{index}")
dst = path.with_suffix(path.suffix + f".{index + 1}")
if src.exists():
src.replace(dst)
path.replace(path.with_suffix(path.suffix + ".1"))
def read_pid(profile: str, service: str) -> int | None:
path = pid_path(profile, service)
if not path.is_file():
return None
try:
return int(path.read_text(encoding="utf-8").strip())
except ValueError:
return None
def is_running(pid: int | None) -> bool:
if not pid or pid <= 0:
return False
try:
os.kill(pid, 0)
except ProcessLookupError:
return False
except PermissionError:
return True
try:
result = subprocess.run(["ps", "-o", "stat=", "-p", str(pid)], check=False, capture_output=True, text=True)
if result.returncode != 0:
return False
state = result.stdout.strip()
if state.startswith("Z"):
return False
except OSError:
pass
return True
def write_state(profile: str, service: str, state: dict[str, Any]) -> None:
path = state_path(profile, service)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(state, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8")
def read_state(profile: str, service: str) -> dict[str, Any]:
path = state_path(profile, service)
if not path.is_file():
return {}
try:
return json.loads(path.read_text(encoding="utf-8"))
except json.JSONDecodeError:
return {}
def health_ok(config: dict[str, Any], timeout: float = 1.0) -> tuple[bool | None, str]:
health = config.get("health") or {}
kind = health.get("type")
if not kind:
return None, "no health check"
if kind == "tcp":
host = str(health.get("host") or "127.0.0.1")
port = int(health.get("port") or 0)
try:
with socket.create_connection((host, port), timeout=timeout):
return True, f"tcp {host}:{port} ok"
except OSError as error:
return False, f"tcp {host}:{port} failed: {error}"
if kind == "http":
url = str(health.get("url") or "")
try:
with urllib.request.urlopen(url, timeout=timeout) as response:
ok = 200 <= int(response.status) < 400
return ok, f"http {url} status {response.status}"
except (urllib.error.URLError, TimeoutError, OSError) as error:
return False, f"http {url} failed: {error}"
return None, f"unknown health type: {kind}"
def service_status(profile: str, ref: ServiceRef) -> dict[str, Any]:
enabled = ref.config.get("enabled", True)
kind = ref.config.get("kind", "process")
command = ref.config.get("command") or []
pid = read_pid(profile, ref.name) if enabled and kind != "app-launcher" else None
running = is_running(pid)
ok, detail = health_ok(ref.config) if enabled else (None, "health skipped")
if not enabled:
label = "disabled"
elif kind == "app-launcher":
label = "launcher"
elif running and ok is not False:
label = "running"
elif running:
label = "unhealthy"
elif ok is True:
label = "externally running"
else:
label = "stopped"
return {
"name": ref.name,
"enabled": enabled,
"kind": kind,
"status": label,
"pid": pid,
"command": command,
"health": {"ok": ok, "detail": detail},
"state": read_state(profile, ref.name),
}
def wait_for_health(config: dict[str, Any], seconds: float = 8.0) -> tuple[bool | None, str]:
deadline = time.time() + seconds
last: tuple[bool | None, str] = (None, "no health check")
while time.time() <= deadline:
last = health_ok(config)
if last[0] is True or last[0] is None:
return last
time.sleep(0.4)
return last
def start_service(profile: str, ref: ServiceRef, manifest: dict[str, Any], started: set[str]) -> None:
if ref.name in started:
return
for dependency in ref.config.get("depends_on") or []:
dep_config = (manifest.get("services") or {}).get(dependency)
if not dep_config or not dep_config.get("enabled", True):
raise SystemExit(f"{ref.name} depends on missing/disabled service: {dependency}")
start_service(profile, ServiceRef(dependency, dep_config), manifest, started)
kind = ref.config.get("kind", "process")
command = ref.config.get("command") or []
if not command:
raise SystemExit(f"{ref.name} has no command")
if not command_exists(str(command[0])):
raise SystemExit(f"{ref.name} command is not executable or not found: {command[0]}")
if kind != "app-launcher":
pid = read_pid(profile, ref.name)
if is_running(pid):
ok, detail = health_ok(ref.config)
status = "running" if ok is not False else "running unhealthy"
print(f"{ref.name}: {status} ({detail})")
started.add(ref.name)
return
ok, detail = health_ok(ref.config)
if ok is True:
print(f"{ref.name}: externally running ({detail}); not starting duplicate")
started.add(ref.name)
return
path = log_path(profile, ref.name)
path.parent.mkdir(parents=True, exist_ok=True)
rotate_log_if_needed(path, int(ref.config.get("log_max_bytes", DEFAULT_LOG_MAX_BYTES)), int(ref.config.get("log_backups", DEFAULT_LOG_BACKUPS)))
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)
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)
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")
ok, detail = wait_for_health(ref.config)
state = "started" if ok is not False else "started but health check failed"
write_state(profile, ref.name, {"pid": process.pid, "started_at": time.time(), "health": detail})
print(f"{ref.name}: {state} pid={process.pid} ({detail})")
started.add(ref.name)
def stop_service(profile: str, ref: ServiceRef) -> None:
kind = ref.config.get("kind", "process")
if kind == "app-launcher":
print(f"{ref.name}: launcher service has no managed process")
return
pid = read_pid(profile, ref.name)
if not is_running(pid):
ok, detail = health_ok(ref.config)
if ok is True:
print(f"{ref.name}: externally running ({detail}); no managed pid to stop")
return
print(f"{ref.name}: not running")
pid_path(profile, ref.name).unlink(missing_ok=True)
return
assert pid is not None
try:
os.killpg(pid, signal.SIGTERM)
except ProcessLookupError:
pass
except PermissionError:
os.kill(pid, signal.SIGTERM)
deadline = time.time() + float(ref.config.get("stop_timeout_seconds", DEFAULT_STOP_TIMEOUT_SECONDS))
while time.time() < deadline and is_running(pid):
time.sleep(0.2)
if is_running(pid):
try:
os.killpg(pid, signal.SIGKILL)
except Exception:
os.kill(pid, signal.SIGKILL)
print(f"{ref.name}: killed pid={pid}")
else:
print(f"{ref.name}: stopped")
pid_path(profile, ref.name).unlink(missing_ok=True)
write_state(profile, ref.name, {"stopped_at": time.time()})
def status_service(profile: str, ref: ServiceRef) -> None:
status = service_status(profile, ref)
if not status["enabled"]:
print(f"{ref.name}: disabled")
return
if status["kind"] == "app-launcher":
state = status["state"]
launched = state.get("launched_at")
suffix = f"last launched {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(launched))}" if launched else "not launched by manager"
print(f"{ref.name}: launcher ({suffix})")
return
print(f"{ref.name}: {status['status']} pid={status['pid'] or '-'} ({status['health']['detail']})")
def status_report(profile: str, refs: list[ServiceRef]) -> dict[str, Any]:
"""Return lightweight machine-readable live service state."""
return {
"profile": profile,
"workspace": str(ROOT),
"runtime": str(RUNTIME_DIR),
"services": [service_status(profile, ref) for ref in refs],
}
def tail_log(profile: str, service: str, lines: int) -> None:
path = log_path(profile, service)
if not path.is_file():
print(f"no log file: {path}")
return
content = path.read_text(encoding="utf-8", errors="replace").splitlines()
for line in content[-lines:]:
print(line)
def doctor_report(profile: str, manifest: dict[str, Any]) -> dict[str, Any]:
errors = validate_manifest(manifest)
service_reports = []
for ref in service_items(manifest, include_disabled=True):
command = ref.config.get("command") or []
first = command[0] if command else ""
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)})
for command_name in doctor.get("optional_commands") or []:
checks.append({"type": "optional_command", "name": command_name, "ok": command_exists(command_name)})
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["checks"] = checks
service_reports.append(status)
return {
"profile": profile,
"workspace": str(ROOT),
"manifest": str(manifest_path(profile)),
"runtime": str(RUNTIME_DIR),
"manifest_ok": not errors,
"manifest_errors": errors,
"services": service_reports,
}
def run_doctor(profile: str, manifest: dict[str, Any], json_output: bool = False) -> None:
report = doctor_report(profile, manifest)
if json_output:
print(json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True))
return
print(f"AI Workspace doctor profile={profile}")
print(f"workspace: {ROOT}")
print(f"manifest: {manifest_path(profile)}")
ensure_runtime()
print(f"runtime: {RUNTIME_DIR}")
errors = report["manifest_errors"]
if errors:
print("manifest: invalid")
for error in errors:
print(f" ! {error}")
else:
print("manifest: ok")
for service in report["services"]:
enabled_text = "enabled" if service["enabled"] else "disabled"
if not service["enabled"]:
print(f"- {service['name']}: {enabled_text}; command={'ok' if service['command_ok'] else 'missing'}; health skipped")
continue
health_text = service["health"]["detail"] if service["health"]["ok"] is not None else "no health check"
print(f"- {service['name']}: {enabled_text}; command={'ok' if service['command_ok'] else 'missing'}; {health_text}")
for check in service["checks"]:
label = check["type"].replace("_", " ")
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)
for directory in paths:
candidate = Path(directory) / command
if candidate.exists() and os.access(candidate, os.X_OK):
return str(candidate)
return None
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("action", choices=["start", "stop", "restart", "status", "logs", "doctor"])
parser.add_argument("services", nargs="*", help="Optional service names for start/stop/restart/status/logs.")
parser.add_argument("--profile", default=os.getenv("AIW_PROJECT_PROFILE", "fidelity"))
parser.add_argument("--group", default="", help="Start/stop/status services in a group, e.g. communication or inbox.")
parser.add_argument("--lines", type=int, default=80, help="Number of log lines for logs action.")
parser.add_argument("--json", action="store_true", help="Emit machine-readable JSON for supported actions such as doctor.")
args = parser.parse_args()
ensure_runtime()
manifest = load_manifest(args.profile)
if args.action == "doctor":
run_doctor(args.profile, manifest, json_output=args.json)
return
errors = validate_manifest(manifest)
if errors:
raise SystemExit("invalid services manifest:\n" + "\n".join(f"- {error}" for error in errors))
include_disabled = args.action == "status"
refs = select_services(manifest, args.services, args.group or None, include_disabled=include_disabled)
if args.action == "start":
started: set[str] = set()
for ref in refs:
start_service(args.profile, ref, manifest, started)
elif args.action == "stop":
for ref in reversed(refs):
stop_service(args.profile, ref)
elif args.action == "restart":
for ref in reversed(refs):
stop_service(args.profile, ref)
started = set()
for ref in refs:
start_service(args.profile, ref, manifest, started)
elif args.action == "status":
if args.json:
print(json.dumps(status_report(args.profile, refs), ensure_ascii=False, indent=2, sort_keys=True))
else:
for ref in refs:
status_service(args.profile, ref)
elif args.action == "logs":
if not args.services:
raise SystemExit("logs requires at least one service name")
for service in args.services:
print(f"==> {service} <==")
tail_log(args.profile, service, args.lines)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,76 @@
#!/usr/bin/env python3
from __future__ import annotations
import importlib.util
import json
import sys
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
INDEXER_PATH = Path(__file__).with_name("indexer.py")
SPEC = importlib.util.spec_from_file_location("aiw_indexer", INDEXER_PATH)
indexer = importlib.util.module_from_spec(SPEC)
assert SPEC.loader is not None
sys.modules[SPEC.name] = indexer
SPEC.loader.exec_module(indexer)
class IndexerTests(unittest.TestCase):
def test_build_skips_templates_and_searches_canonical_files(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
real = root / "workspaces" / "fidelity" / "project-knowledge" / "03-context" / "project.md"
template = root / "workspaces" / "fidelity" / "project-knowledge" / "09-templates" / "daily.md"
real.parent.mkdir(parents=True)
template.parent.mkdir(parents=True)
real.write_text("# XFlow\nDismissal lifecycle context", encoding="utf-8")
template.write_text("# XFlow\nTemplate-only text", encoding="utf-8")
with patch.object(indexer, "ROOT", root):
manifest = indexer.write_index("fidelity")
result = indexer.search_index("fidelity", "dismissal lifecycle", limit=5)
self.assertEqual(manifest["file_count"], 1)
self.assertEqual(len(result["matches"]), 1)
self.assertIn("03-context/project.md", result["matches"][0]["path"])
def test_status_reports_unindexed_profile(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
with patch.object(indexer, "ROOT", root):
result = indexer.status("fidelity")
self.assertFalse(result["indexed"])
self.assertIn(".aiw/indexes/fidelity/project-knowledge.jsonl", result["index_path"])
def test_cli_search_payload_is_json_serializable(self) -> None:
payload = {"matches": [{"path": "workspaces/fidelity/project-knowledge/01-current/current-work.md", "score": 1.0}]}
self.assertIsInstance(json.dumps(payload), str)
def test_build_uses_workspace_json_paths(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
config = root / "profiles" / "demo" / "workspace.json"
real = root / "workspaces" / "demo" / "project-knowledge" / "03-context" / "project.md"
config.parent.mkdir(parents=True)
real.parent.mkdir(parents=True)
config.write_text(json.dumps({
"knowledge_dir": "workspaces/demo/project-knowledge",
"index_dir": ".aiw/indexes/demo",
}), encoding="utf-8")
real.write_text("# Demo\nReusable profile memory", encoding="utf-8")
with patch.object(indexer, "ROOT", root):
manifest = indexer.write_index("demo")
result = indexer.search_index("demo", "profile memory", limit=5)
self.assertEqual(manifest["source"], "workspaces/demo/project-knowledge")
self.assertEqual(result["matches"][0]["path"], "workspaces/demo/project-knowledge/03-context/project.md")
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,53 @@
#!/usr/bin/env python3
from __future__ import annotations
import importlib.util
import json
import sys
import tempfile
import unittest
from pathlib import Path
PROFILE_PATH = Path(__file__).with_name("profile.py")
SPEC = importlib.util.spec_from_file_location("aiw_profile", PROFILE_PATH)
profile = importlib.util.module_from_spec(SPEC)
assert SPEC.loader is not None
sys.modules[SPEC.name] = profile
SPEC.loader.exec_module(profile)
class ProfileTests(unittest.TestCase):
def test_workspace_config_resolves_profile_paths(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
config = root / "profiles" / "demo" / "workspace.json"
config.parent.mkdir(parents=True)
config.write_text(json.dumps({
"knowledge_dir": "workspaces/demo/project-knowledge",
"inbox_dir": "workspaces/demo/inbox",
"index_dir": ".aiw/indexes/demo",
}), encoding="utf-8")
self.assertEqual(profile.knowledge_dir("demo", root=root), root / "workspaces" / "demo" / "project-knowledge")
self.assertEqual(profile.inbox_dir("demo", root=root), root / "workspaces" / "demo" / "inbox")
self.assertEqual(profile.index_dir("demo", root=root), root / ".aiw" / "indexes" / "demo")
def test_defaults_use_isolated_workspace_paths(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
self.assertEqual(profile.knowledge_dir("missing", root=root), root / "workspaces" / "missing" / "project-knowledge")
self.assertEqual(profile.inbox_dir("missing", root=root), root / "workspaces" / "missing" / "inbox")
self.assertEqual(profile.index_dir("missing", root=root), root / ".aiw" / "indexes" / "missing")
def test_relative_to_root_handles_external_paths(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
self.assertEqual(profile.relative_to_root(root / "a" / "b", root=root), "a/b")
self.assertEqual(profile.relative_to_root(Path("/external/path"), root=root), "/external/path")
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,197 @@
#!/usr/bin/env python3
from __future__ import annotations
import importlib.util
import io
import socket
import sys
import tempfile
import unittest
import warnings
from pathlib import Path
from contextlib import redirect_stdout
from unittest.mock import patch
SERVICES_PATH = Path(__file__).with_name("services.py")
SPEC = importlib.util.spec_from_file_location("aiw_services", SERVICES_PATH)
services = importlib.util.module_from_spec(SPEC)
assert SPEC.loader is not None
sys.modules[SPEC.name] = services
SPEC.loader.exec_module(services)
def sample_manifest() -> dict:
return {
"services": {
"alpha": {
"enabled": True,
"kind": "process",
"command": ["python3", "-c", "import time; time.sleep(60)"],
"groups": ["core"],
},
"beta": {
"enabled": True,
"kind": "process",
"command": ["python3", "-c", "import time; time.sleep(60)"],
"groups": ["capture"],
"depends_on": ["alpha"],
},
"disabled": {
"enabled": False,
"kind": "process",
"command": ["python3", "-c", "import time; time.sleep(60)"],
"groups": ["core"],
},
}
}
class ServiceManagerTests(unittest.TestCase):
def test_select_services_excludes_disabled_by_default(self) -> None:
selected = services.select_services(sample_manifest(), names=[], group=None)
self.assertEqual([item.name for item in selected], ["alpha", "beta"])
def test_select_services_can_include_disabled_for_status(self) -> None:
selected = services.select_services(sample_manifest(), names=[], group=None, include_disabled=True)
self.assertEqual([item.name for item in selected], ["alpha", "beta", "disabled"])
def test_select_services_filters_by_group(self) -> None:
selected = services.select_services(sample_manifest(), names=[], group="capture")
self.assertEqual([item.name for item in selected], ["beta"])
def test_health_ok_tcp_reports_open_port(self) -> None:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
server.bind(("127.0.0.1", 0))
server.listen(1)
port = server.getsockname()[1]
ok, detail = services.health_ok({"health": {"type": "tcp", "host": "127.0.0.1", "port": port}})
self.assertTrue(ok)
self.assertIn(f"127.0.0.1:{port}", detail)
def test_validate_manifest_reports_bad_dependencies(self) -> None:
manifest = {
"services": {
"bad": {
"enabled": True,
"kind": "process",
"command": ["python3", "-c", "pass"],
"depends_on": ["missing"],
}
}
}
errors = services.validate_manifest(manifest)
self.assertTrue(any("depends on unknown service" in error for error in errors))
def test_validate_manifest_reports_missing_command_for_enabled_service(self) -> None:
manifest = {"services": {"bad": {"enabled": True, "kind": "process"}}}
errors = services.validate_manifest(manifest)
self.assertTrue(any("non-empty command" in error for error in errors))
def test_command_exists_supports_relative_executable_paths(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
script = root / "bin" / "tool.sh"
script.parent.mkdir(parents=True)
script.write_text("#!/usr/bin/env bash\n", encoding="utf-8")
script.chmod(0o755)
with patch.object(services, "ROOT", root):
self.assertTrue(services.command_exists("bin/tool.sh"))
def test_rotate_log_if_needed_keeps_backups(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
log = Path(tmp) / "service.log"
log.write_text("old", encoding="utf-8")
services.rotate_log_if_needed(log, max_bytes=1, backups=2)
self.assertFalse(log.exists())
self.assertEqual(log.with_suffix(".log.1").read_text(encoding="utf-8"), "old")
def test_doctor_report_is_machine_readable(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
with patch.object(services, "ROOT", root), \
patch.object(services, "RUNTIME_DIR", root / ".aiw" / "runtime"), \
patch.object(services, "PID_DIR", root / ".aiw" / "runtime" / "pids"), \
patch.object(services, "LOG_DIR", root / ".aiw" / "runtime" / "logs"), \
patch.object(services, "STATE_DIR", root / ".aiw" / "runtime" / "state"):
report = services.doctor_report("test", sample_manifest())
self.assertTrue(report["manifest_ok"])
self.assertEqual(report["services"][0]["name"], "alpha")
self.assertIn("health", report["services"][0])
def test_status_report_is_lightweight_machine_readable(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
refs = services.service_items(sample_manifest(), include_disabled=True)
with patch.object(services, "ROOT", root), \
patch.object(services, "RUNTIME_DIR", root / ".aiw" / "runtime"), \
patch.object(services, "PID_DIR", root / ".aiw" / "runtime" / "pids"), \
patch.object(services, "LOG_DIR", root / ".aiw" / "runtime" / "logs"), \
patch.object(services, "STATE_DIR", root / ".aiw" / "runtime" / "state"):
report = services.status_report("test", refs)
self.assertEqual(report["profile"], "test")
self.assertEqual(len(report["services"]), 3)
self.assertIn("status", report["services"][0])
self.assertNotIn("checks", report["services"][0])
def test_read_pid_ignores_invalid_pid_file(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
pid_dir = Path(tmp) / "pids"
target = pid_dir / "fidelity"
target.mkdir(parents=True)
(target / "alpha.pid").write_text("not-a-pid\n", encoding="utf-8")
with patch.object(services, "PID_DIR", pid_dir):
self.assertIsNone(services.read_pid("fidelity", "alpha"))
def test_start_and_stop_managed_process(self) -> None:
manifest = {
"services": {
"sleeper": {
"enabled": True,
"kind": "process",
"command": [sys.executable, "-c", "import time; time.sleep(60)"],
"restart": "never",
}
}
}
ref = services.ServiceRef("sleeper", manifest["services"]["sleeper"])
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
with patch.object(services, "PID_DIR", root / "pids"), \
patch.object(services, "LOG_DIR", root / "logs"), \
patch.object(services, "STATE_DIR", root / "state"):
started: set[str] = set()
services.ensure_runtime()
with warnings.catch_warnings():
warnings.simplefilter("ignore", ResourceWarning)
with redirect_stdout(io.StringIO()):
services.start_service("test", ref, manifest, started)
pid = services.read_pid("test", "sleeper")
self.assertTrue(services.is_running(pid))
self.assertIn("sleeper", started)
with redirect_stdout(io.StringIO()):
services.stop_service("test", ref)
self.assertFalse(services.is_running(pid))
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,13 @@
# Copy this file to scripts/iphone-photo-inbox/.env and adjust local values.
PHOTO_INBOX_TOKEN=choose-a-token
PHOTO_INBOX_HOST=0.0.0.0
PHOTO_INBOX_PORT=8787
PHOTO_INBOX_DIR=/Users/david/Pictures/Photo Inbox
PHOTO_INBOX_DEBOUNCE_SECONDS=5
PHOTO_INBOX_CLIPBOARD=1
PHOTO_INBOX_NOTIFY=1
# Optional behavior
# PHOTO_INBOX_REVEAL=1
# PHOTO_INBOX_DEBUG=1

View File

@@ -1,39 +1,37 @@
# iPhone Photo Inbox # Photo Inbox
Local HTTP receiver for sending JPEGs from iPhone Shortcuts into Mac inboxes. macOS HTTP receiver for sending JPEG uploads into a local photo inbox. Clients
The Shortcut sends a `profile`, and the Mac decides the destination folder and can be iPhone Shortcuts, curl, another phone, a script, or any system that can
clipboard behavior. POST a JPEG file. The server currently supports macOS only because clipboard,
Finder reveal, and notifications use macOS APIs/tools.
## Profiles By default, each upload is saved locally and the current batch is copied to the
macOS clipboard as native file URLs.
`opencode` ## Behavior
- Saves to `ai/inbox/photos/` - Saves photos to `~/Pictures/Photo Inbox` by default.
- Copies a terminal-safe path to the clipboard - Groups consecutive uploads into a batch.
- Best for pasting into OpenCode running in a terminal - Every new photo extends the batch by `5s`.
- Every new photo immediately refreshes the clipboard with the full batch.
- When no new photo arrives before debounce expires, a summary notification is shown.
`mattermost` This uses a small Swift helper and `NSPasteboard.writeObjects`, which matches
Finder-style file clipboard behavior.
- Saves to `~/Pictures/iPhone Inbox`
- Copies the image data to the clipboard
- Best for pasting directly into Mattermost
`general`
- Saves to `~/Pictures/iPhone Inbox`
- Does not modify the clipboard
- Useful for plain capture
All profiles show a macOS notification by default.
## Start the receiver ## Start the receiver
Recommended: Recommended:
```bash ```bash
IPHONE_PHOTO_TOKEN="choose-a-token" python3 scripts/iphone-photo-inbox/receiver.py cp scripts/iphone-photo-inbox/.env.example scripts/iphone-photo-inbox/.env
scripts/iphone-photo-inbox/run.sh
``` ```
`.env` is loaded automatically and does not override variables already exported
in the shell. `run.sh` compiles the native pasteboard helper when it is missing
or older than the Swift source.
The receiver listens on: The receiver listens on:
```text ```text
@@ -52,7 +50,19 @@ If that does not return an IP, use:
ifconfig ifconfig
``` ```
## Shortcut config ## Generic client contract
Send a JPEG request body:
```bash
curl --request POST \
--data-binary @photo.jpg \
"http://MAC_IP:8787/upload?token=choose-a-token"
```
Successful response body is the saved local file path.
## iPhone Shortcuts guide
Use a Dictionary near the top of the Shortcut: Use a Dictionary near the top of the Shortcut:
@@ -60,29 +70,24 @@ Use a Dictionary near the top of the Shortcut:
mac_ip: 192.168.11.186 mac_ip: 192.168.11.186
port: 8787 port: 8787
token: choose-a-token token: choose-a-token
profile: opencode
``` ```
Build the URL from the dictionary: Build the URL from the dictionary:
```text ```text
http://[mac_ip]:[port]/upload?token=[token]&profile=[profile] http://[mac_ip]:[port]/upload?token=[token]
``` ```
Use `profile: opencode` when the next paste target is OpenCode. Use Camera Shortcut:
`profile: mattermost` when the next paste target is Mattermost.
## Camera shortcut
```text ```text
Dictionary Dictionary
mac_ip: 192.168.11.186 mac_ip: 192.168.11.186
port: 8787 port: 8787
token: choose-a-token token: choose-a-token
profile: opencode
Text Text
http://[mac_ip]:[port]/upload?token=[token]&profile=[profile] http://[mac_ip]:[port]/upload?token=[token]
Take Photo Take Photo
Show Camera Preview: On Show Camera Preview: On
@@ -94,15 +99,13 @@ Get Contents of URL
File: Photo File: Photo
Show Notification Show Notification
Sent to [profile] Sent to photo inbox
``` ```
On the tested iPhone flow, `Take Photo` already produces a JPEG, so no On the tested iPhone flow, `Take Photo` already produces a JPEG, so no
conversion step is needed. conversion step is needed.
## Existing photos shortcut Existing Photos Shortcut:
Use this when sending existing images from Photos:
```text ```text
Receive Images and Media from Share Sheet Receive Images and Media from Share Sheet
@@ -111,84 +114,99 @@ Repeat with Each Item in Shortcut Input
Image: Repeat Item Image: Repeat Item
Format: JPEG Format: JPEG
Get Contents of URL Get Contents of URL
URL: http://[mac_ip]:[port]/upload?token=[token]&profile=[profile] URL: http://[mac_ip]:[port]/upload?token=[token]
Method: POST Method: POST
Request Body: File Request Body: File
File: Converted Image File: Converted Image
End Repeat End Repeat
Show Notification Show Notification
Sent to [profile] Sent to photo inbox
``` ```
## Overrides ## Configuration
Profile folders: Common `.env` values:
```bash ```bash
IPHONE_PHOTO_OPENCODE_DIR="/path/to/opencode/photos" PHOTO_INBOX_TOKEN=choose-a-token
IPHONE_PHOTO_MATTERMOST_DIR="$HOME/Pictures/iPhone Inbox" PHOTO_INBOX_HOST=0.0.0.0
IPHONE_PHOTO_GENERAL_DIR="$HOME/Pictures/iPhone Inbox" PHOTO_INBOX_PORT=8787
``` PHOTO_INBOX_DIR=/Users/david/Pictures/Photo Inbox
PHOTO_INBOX_DEBOUNCE_SECONDS=5
Global folder override for all profiles: PHOTO_INBOX_CLIPBOARD=1
PHOTO_INBOX_NOTIFY=1
```bash
IPHONE_PHOTO_OUTPUT_DIR="$HOME/Pictures/iPhone Inbox" \
IPHONE_PHOTO_TOKEN="choose-a-token" \
python3 scripts/iphone-photo-inbox/receiver.py
```
Default profile when the URL does not include `profile=`:
```bash
IPHONE_PHOTO_PROFILE=mattermost \
IPHONE_PHOTO_TOKEN="choose-a-token" \
python3 scripts/iphone-photo-inbox/receiver.py
```
Clipboard override for all profiles:
```bash
IPHONE_PHOTO_CLIPBOARD=image
IPHONE_PHOTO_CLIPBOARD=terminal-path
IPHONE_PHOTO_CLIPBOARD=path
IPHONE_PHOTO_CLIPBOARD=file
IPHONE_PHOTO_CLIPBOARD=none
``` ```
Other useful options: Other useful options:
```bash ```bash
python3 scripts/iphone-photo-inbox/receiver.py --no-notify scripts/iphone-photo-inbox/run.sh --no-clipboard
python3 scripts/iphone-photo-inbox/receiver.py --reveal scripts/iphone-photo-inbox/run.sh --no-notify
scripts/iphone-photo-inbox/run.sh --reveal
PHOTO_INBOX_DEBUG=1 scripts/iphone-photo-inbox/run.sh
``` ```
## Project integration
Keep this utility as an isolated image mailbox. If a project wants easy access,
link the project inbox to the mailbox instead of making this utility know about
the project.
Example using the active profile inbox:
```bash
PROFILE_INBOX="$(python3 scripts/aiw/profile.py path inbox --profile fidelity)"
mkdir -p "$PROFILE_INBOX"
ln -s "$HOME/Pictures/Photo Inbox" "$PROFILE_INBOX/photos"
```
Or point the receiver at a project-owned folder from `.env`:
```bash
PHOTO_INBOX_DIR=/absolute/path/to/profile/inbox/photos
```
The symlink approach keeps this utility reusable across projects and devices.
## Troubleshooting ## Troubleshooting
Startup should print each active profile: Startup should print:
```text ```text
profile opencode: dir=... clipboard=terminal-path notify=True reveal=False saving to: ...
profile mattermost: dir=... clipboard=image notify=True reveal=False debounce seconds: 5
clipboard: True
notify: True
``` ```
After upload, expect: After each upload, expect:
```text ```text
notification sent clipboard updated count=2
clipboard mode applied: terminal-path saved ... batch_count=2
saved ... profile=opencode
``` ```
For Mattermost, expect: After the debounce window closes, expect:
```text ```text
clipboard mode applied: image batch notification sent count=2
batch finalized count=2 dir=...
``` ```
If files arrive but clipboard/notifications do not behave as expected, check: With `PHOTO_INBOX_DEBUG=1`, a two-photo batch should report:
- The Shortcut URL includes the intended `profile=`. ```text
- The receiver log shows the expected profile. pasteboard files=2 items=2
- macOS Focus/Do Not Disturb is not hiding notifications. ```
- Terminal/Codex has permission for AppleScript automation if macOS prompts.
The native file clipboard helper lives at:
```text
scripts/iphone-photo-inbox/copy_files_to_clipboard.swift
```
The compiled binary is ignored by git and generated by `run.sh`:
```text
scripts/iphone-photo-inbox/copy_files_to_clipboard
```

View File

@@ -0,0 +1,40 @@
#!/usr/bin/env swift
import AppKit
import Foundation
let paths = CommandLine.arguments.dropFirst()
guard !paths.isEmpty else {
fputs("usage: copy_files_to_clipboard.swift <path> [path...]\n", stderr)
exit(64)
}
let urls = paths.map { URL(fileURLWithPath: $0).standardizedFileURL }
let missing = urls.filter { !FileManager.default.fileExists(atPath: $0.path) }
guard missing.isEmpty else {
for url in missing {
fputs("missing file: \(url.path)\n", stderr)
}
exit(66)
}
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
let wroteObjects = pasteboard.writeObjects(urls as [NSURL])
let filenamesType = NSPasteboard.PasteboardType("NSFilenamesPboardType")
let pathsList = urls.map(\.path)
let wroteFilenames = pasteboard.setPropertyList(pathsList, forType: filenamesType)
let wrotePlainText = pasteboard.setString(pathsList.joined(separator: "\n"), forType: .string)
if wroteObjects || wroteFilenames || wrotePlainText {
let itemCount = pasteboard.pasteboardItems?.count ?? 0
fputs("pasteboard files=\(urls.count) items=\(itemCount) objects=\(wroteObjects) filenames=\(wroteFilenames) text=\(wrotePlainText)\n", stderr)
exit(0)
}
fputs("failed to write file URLs to pasteboard\n", stderr)
exit(1)

333
scripts/iphone-photo-inbox/receiver.py Executable file → Normal file
View File

@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Receive JPEG uploads from iPhone Shortcuts into local Mac inboxes.""" """Receive JPEG uploads into a local Mac photo inbox."""
from __future__ import annotations from __future__ import annotations
@@ -7,8 +7,8 @@ import argparse
import datetime as dt import datetime as dt
import json import json
import os import os
import shlex
import subprocess import subprocess
import threading
from dataclasses import dataclass from dataclasses import dataclass
from http import HTTPStatus from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
@@ -17,24 +17,32 @@ from tempfile import NamedTemporaryFile
from urllib.parse import parse_qs, urlparse from urllib.parse import parse_qs, urlparse
WORKSPACE_DIR = Path(__file__).resolve().parents[2] SCRIPT_DIR = Path(__file__).resolve().parent
DEFAULT_OPENCODE_DIR = WORKSPACE_DIR / "ai" / "inbox" / "photos" COPY_FILES_HELPER = SCRIPT_DIR / "copy_files_to_clipboard.swift"
DEFAULT_MATTERMOST_DIR = Path.home() / "Pictures" / "iPhone Inbox" COPY_FILES_HELPER_BIN = SCRIPT_DIR / "copy_files_to_clipboard"
DEFAULT_ENV_FILE = SCRIPT_DIR / ".env"
CLIPBOARD_NONE = "none" DEFAULT_OUTPUT_DIR = Path.home() / "Pictures" / "Photo Inbox"
CLIPBOARD_IMAGE = "image"
CLIPBOARD_PATH = "path"
CLIPBOARD_TERMINAL_PATH = "terminal-path"
CLIPBOARD_FILE = "file"
@dataclass(frozen=True) @dataclass(frozen=True)
class Profile: class Config:
name: str
output_dir: Path output_dir: Path
clipboard: str clipboard: bool
notify: bool = True notify: bool
reveal: bool = False reveal: bool
@dataclass
class Batch:
output_dir: Path
paths: list[Path]
started_at: dt.datetime
updated_at: dt.datetime
timer: threading.Timer | None = None
@property
def count(self) -> int:
return len(self.paths)
def env_flag(name: str, default: bool = False) -> bool: def env_flag(name: str, default: bool = False) -> bool:
@@ -44,20 +52,62 @@ def env_flag(name: str, default: bool = False) -> bool:
return value.strip().lower() in {"1", "true", "yes", "on"} return value.strip().lower() in {"1", "true", "yes", "on"}
def env_value(name: str) -> str | None:
return os.getenv(name)
def env_flag_value(name: str, default: bool = False) -> bool:
value = env_value(name)
if value is None:
return default
return value.strip().lower() in {"1", "true", "yes", "on"}
def env_path(name: str, default: Path) -> Path: def env_path(name: str, default: Path) -> Path:
value = os.getenv(name) value = env_value(name)
return Path(value).expanduser() if value else default return Path(value).expanduser() if value else default
def profile_defaults() -> dict[str, Profile]: def env_str(name: str, default: str) -> str:
opencode_dir = env_path("IPHONE_PHOTO_OPENCODE_DIR", DEFAULT_OPENCODE_DIR) value = env_value(name)
mattermost_dir = env_path("IPHONE_PHOTO_MATTERMOST_DIR", DEFAULT_MATTERMOST_DIR) return value if value is not None else default
general_dir = env_path("IPHONE_PHOTO_GENERAL_DIR", DEFAULT_MATTERMOST_DIR)
return {
"opencode": Profile("opencode", opencode_dir, CLIPBOARD_TERMINAL_PATH), def env_int(name: str, default: int) -> int:
"mattermost": Profile("mattermost", mattermost_dir, CLIPBOARD_IMAGE), return int(env_str(name, str(default)))
"general": Profile("general", general_dir, CLIPBOARD_NONE),
}
def env_float(name: str, default: float) -> float:
return float(env_str(name, str(default)))
def parse_env_line(line: str) -> tuple[str, str] | None:
stripped = line.strip()
if not stripped or stripped.startswith("#") or "=" not in stripped:
return None
key, value = stripped.split("=", 1)
key = key.strip()
value = value.strip()
if not key:
return None
if key.startswith("export "):
key = key.removeprefix("export ").strip()
if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}:
value = value[1:-1]
return key, value
def load_env_file(path: Path) -> None:
if not path.exists():
return
for line in path.read_text(encoding="utf-8").splitlines():
parsed = parse_env_line(line)
if parsed is None:
continue
key, value = parsed
os.environ.setdefault(key, value)
def timestamp() -> str: def timestamp() -> str:
@@ -77,10 +127,6 @@ def looks_like_jpeg(data: bytes) -> bool:
return data.startswith(b"\xff\xd8") and data.endswith(b"\xff\xd9") return data.startswith(b"\xff\xd8") and data.endswith(b"\xff\xd9")
def normalize_profile(value: str) -> str:
return value.strip().lower() or "opencode"
def run_macos_action(command: list[str]) -> bool: def run_macos_action(command: list[str]) -> bool:
try: try:
result = subprocess.run(command, check=False, capture_output=True, text=True) result = subprocess.run(command, check=False, capture_output=True, text=True)
@@ -93,6 +139,11 @@ def run_macos_action(command: list[str]) -> bool:
print(f"macOS action failed: {error}", flush=True) print(f"macOS action failed: {error}", flush=True)
return False return False
if env_flag_value("PHOTO_INBOX_DEBUG"):
output = "\n".join(part for part in [result.stdout.strip(), result.stderr.strip()] if part)
if output:
print(f"macOS action output: {output}", flush=True)
return True return True
@@ -105,47 +156,68 @@ def reveal_in_finder(path: Path) -> bool:
return run_macos_action(["open", "-R", str(path)]) return run_macos_action(["open", "-R", str(path)])
def copy_image_to_clipboard(path: Path) -> bool: def copy_files_to_clipboard(paths: list[Path]) -> bool:
script = f"set the clipboard to (read (POSIX file {json.dumps(str(path))}) as JPEG picture)" if not paths:
return run_macos_action(["osascript", "-e", script])
def copy_file_to_clipboard(path: Path) -> bool:
script = f"""
set theFile to POSIX file {json.dumps(str(path))} as alias
tell application "Finder"
set the clipboard to {{theFile}}
end tell
"""
return run_macos_action(["osascript", "-e", script])
def copy_text_to_clipboard(text: str) -> bool:
script = f"set the clipboard to {json.dumps(text)}"
return run_macos_action(["osascript", "-e", script])
def terminal_path_reference(path: Path) -> str:
return shlex.quote(str(path))
def apply_clipboard_mode(mode: str, path: Path) -> bool:
if mode == CLIPBOARD_NONE:
return True return True
if mode == CLIPBOARD_IMAGE:
return copy_image_to_clipboard(path) helper = COPY_FILES_HELPER
if mode == CLIPBOARD_PATH: if COPY_FILES_HELPER_BIN.exists():
return copy_text_to_clipboard(str(path)) source_mtime = COPY_FILES_HELPER.stat().st_mtime if COPY_FILES_HELPER.exists() else 0
if mode == CLIPBOARD_TERMINAL_PATH: helper_mtime = COPY_FILES_HELPER_BIN.stat().st_mtime
return copy_text_to_clipboard(terminal_path_reference(path)) if helper_mtime >= source_mtime:
if mode == CLIPBOARD_FILE: helper = COPY_FILES_HELPER_BIN
return copy_file_to_clipboard(path) else:
print(f"unsupported clipboard mode: {mode}", flush=True) print("compiled pasteboard helper is older than source; using Swift script", flush=True)
if helper.exists():
return run_macos_action([str(helper), *[str(path) for path in paths]])
print(f"native pasteboard helper missing: {COPY_FILES_HELPER_BIN}", flush=True)
return False return False
class BatchManager:
def __init__(self, debounce_seconds: float, config: Config) -> None:
self.debounce_seconds = debounce_seconds
self.config = config
self.lock = threading.Lock()
self.batch: Batch | None = None
def add(self, path: Path) -> tuple[int, list[Path]]:
with self.lock:
now = dt.datetime.now()
if self.batch is None:
self.batch = Batch(self.config.output_dir, [], now, now)
self.batch.paths.append(path)
self.batch.updated_at = now
if self.batch.timer is not None:
self.batch.timer.cancel()
self.batch.timer = threading.Timer(self.debounce_seconds, self.finish)
self.batch.timer.daemon = True
self.batch.timer.start()
return self.batch.count, list(self.batch.paths)
def finish(self) -> None:
with self.lock:
batch = self.batch
self.batch = None
if batch is None:
return
if self.config.notify:
plural = "photo" if batch.count == 1 else "photos"
message = f"{batch.count} {plural} ready; clipboard={'on' if self.config.clipboard else 'off'}"
if notify("Photo Inbox", message):
print(f"batch notification sent count={batch.count}", flush=True)
print(f"batch finalized count={batch.count} dir={batch.output_dir}", flush=True)
class UploadHandler(BaseHTTPRequestHandler): class UploadHandler(BaseHTTPRequestHandler):
server_version = "iPhonePhotoInbox/2.0" server_version = "iPhonePhotoInbox/3.0"
def do_GET(self) -> None: def do_GET(self) -> None:
if self.path == "/health": if self.path == "/health":
@@ -166,13 +238,6 @@ class UploadHandler(BaseHTTPRequestHandler):
self.send_text(HTTPStatus.UNAUTHORIZED, "bad token\n") self.send_text(HTTPStatus.UNAUTHORIZED, "bad token\n")
return return
profile_name = normalize_profile(query.get("profile", [self.server.default_profile])[0])
profile = self.server.profiles.get(profile_name)
if profile is None:
known = ", ".join(sorted(self.server.profiles))
self.send_text(HTTPStatus.BAD_REQUEST, f"unknown profile: {profile_name}; expected one of {known}\n")
return
content_length = self.headers.get("Content-Length") content_length = self.headers.get("Content-Length")
if content_length is None: if content_length is None:
self.send_text(HTTPStatus.LENGTH_REQUIRED, "missing content length\n") self.send_text(HTTPStatus.LENGTH_REQUIRED, "missing content length\n")
@@ -193,7 +258,7 @@ class UploadHandler(BaseHTTPRequestHandler):
self.send_text(HTTPStatus.UNSUPPORTED_MEDIA_TYPE, "expected jpeg\n") self.send_text(HTTPStatus.UNSUPPORTED_MEDIA_TYPE, "expected jpeg\n")
return return
output_dir = self.server.output_dir_override or profile.output_dir output_dir = self.server.config.output_dir
output_dir.mkdir(parents=True, exist_ok=True) output_dir.mkdir(parents=True, exist_ok=True)
target = unique_path(output_dir) target = unique_path(output_dir)
with NamedTemporaryFile(dir=output_dir, delete=False) as tmp: with NamedTemporaryFile(dir=output_dir, delete=False) as tmp:
@@ -201,17 +266,17 @@ class UploadHandler(BaseHTTPRequestHandler):
temp_path = Path(tmp.name) temp_path = Path(tmp.name)
temp_path.replace(target) temp_path.replace(target)
if profile.notify: if self.server.config.reveal:
if notify("iPhone Photo Inbox", f"{profile.name}: {target.name}"):
print("notification sent", flush=True)
if profile.reveal:
if reveal_in_finder(target): if reveal_in_finder(target):
print("revealed in Finder", flush=True) print("revealed in Finder", flush=True)
if apply_clipboard_mode(profile.clipboard, target):
print(f"clipboard mode applied: {profile.clipboard}", flush=True) batch_count, batch_paths = self.server.batch_manager.add(target)
if self.server.config.clipboard:
if copy_files_to_clipboard(batch_paths):
print(f"clipboard updated count={batch_count}", flush=True)
self.send_text(HTTPStatus.CREATED, f"{target}\n") self.send_text(HTTPStatus.CREATED, f"{target}\n")
print(f"saved {target} profile={profile.name}", flush=True) print(f"saved {target} batch_count={batch_count}", flush=True)
def log_message(self, format: str, *args: object) -> None: def log_message(self, format: str, *args: object) -> None:
print(f"{self.address_string()} - {format % args}", flush=True) print(f"{self.address_string()} - {format % args}", flush=True)
@@ -230,105 +295,61 @@ class UploadServer(ThreadingHTTPServer):
self, self,
server_address: tuple[str, int], server_address: tuple[str, int],
handler_class: type[BaseHTTPRequestHandler], handler_class: type[BaseHTTPRequestHandler],
profiles: dict[str, Profile], config: Config,
default_profile: str,
output_dir_override: Path | None,
upload_token: str, upload_token: str,
max_bytes: int, max_bytes: int,
debounce_seconds: float,
) -> None: ) -> None:
super().__init__(server_address, handler_class) super().__init__(server_address, handler_class)
self.profiles = profiles self.config = config
self.default_profile = default_profile
self.output_dir_override = output_dir_override
self.upload_token = upload_token self.upload_token = upload_token
self.max_bytes = max_bytes self.max_bytes = max_bytes
self.batch_manager = BatchManager(debounce_seconds, config)
def apply_legacy_clipboard_overrides(profile: Profile) -> Profile:
clipboard = profile.clipboard
if env_flag("IPHONE_PHOTO_COPY"):
clipboard = CLIPBOARD_IMAGE
if env_flag("IPHONE_PHOTO_COPY_FILE"):
clipboard = CLIPBOARD_FILE
if env_flag("IPHONE_PHOTO_COPY_PATH"):
clipboard = CLIPBOARD_PATH
if env_flag("IPHONE_PHOTO_COPY_TERMINAL_PATH"):
clipboard = CLIPBOARD_TERMINAL_PATH
notify_enabled = env_flag("IPHONE_PHOTO_NOTIFY", profile.notify)
reveal_enabled = env_flag("IPHONE_PHOTO_REVEAL", profile.reveal)
return Profile(profile.name, profile.output_dir, clipboard, notify_enabled, reveal_enabled)
def parse_args() -> argparse.Namespace: def parse_args() -> argparse.Namespace:
env_file = Path(env_str("PHOTO_INBOX_ENV_FILE", str(DEFAULT_ENV_FILE))).expanduser()
load_env_file(env_file)
parser = argparse.ArgumentParser(description=__doc__) parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--host", default=os.getenv("IPHONE_PHOTO_HOST", "0.0.0.0")) parser.add_argument("--host", default=env_str("PHOTO_INBOX_HOST", "0.0.0.0"))
parser.add_argument("--port", type=int, default=int(os.getenv("IPHONE_PHOTO_PORT", "8787"))) parser.add_argument("--port", type=int, default=env_int("PHOTO_INBOX_PORT", 8787))
parser.add_argument("--profile", default=os.getenv("IPHONE_PHOTO_PROFILE", "opencode")) parser.add_argument("--output-dir", type=Path, default=env_path("PHOTO_INBOX_DIR", DEFAULT_OUTPUT_DIR))
parser.add_argument("--output-dir", type=Path, default=os.getenv("IPHONE_PHOTO_OUTPUT_DIR")) parser.add_argument("--token", default=env_str("PHOTO_INBOX_TOKEN", ""))
parser.add_argument("--token", default=os.getenv("IPHONE_PHOTO_TOKEN", "")) parser.add_argument("--max-mb", type=int, default=env_int("PHOTO_INBOX_MAX_MB", 30))
parser.add_argument("--max-mb", type=int, default=int(os.getenv("IPHONE_PHOTO_MAX_MB", "30"))) parser.add_argument("--debounce-seconds", type=float, default=env_float("PHOTO_INBOX_DEBOUNCE_SECONDS", 5))
parser.add_argument( parser.add_argument("--clipboard", action=argparse.BooleanOptionalAction, default=env_flag_value("PHOTO_INBOX_CLIPBOARD", True))
"--clipboard", parser.add_argument("--notify", action=argparse.BooleanOptionalAction, default=env_flag_value("PHOTO_INBOX_NOTIFY", True))
choices=[CLIPBOARD_NONE, CLIPBOARD_IMAGE, CLIPBOARD_PATH, CLIPBOARD_TERMINAL_PATH, CLIPBOARD_FILE], parser.add_argument("--reveal", action="store_true", default=env_flag_value("PHOTO_INBOX_REVEAL"))
default=os.getenv("IPHONE_PHOTO_CLIPBOARD"),
)
parser.add_argument("--no-notify", action="store_true", default=env_flag("IPHONE_PHOTO_NO_NOTIFY"))
parser.add_argument("--reveal", action="store_true", default=env_flag("IPHONE_PHOTO_REVEAL"))
return parser.parse_args() return parser.parse_args()
def build_profiles(args: argparse.Namespace) -> dict[str, Profile]: def build_config(args: argparse.Namespace) -> Config:
profiles = { return Config(
name: apply_legacy_clipboard_overrides(profile) output_dir=args.output_dir.expanduser().resolve(),
for name, profile in profile_defaults().items() clipboard=args.clipboard,
} notify=args.notify,
reveal=args.reveal,
if args.clipboard: )
profiles = {
name: Profile(profile.name, profile.output_dir, args.clipboard, profile.notify, profile.reveal)
for name, profile in profiles.items()
}
if args.no_notify:
profiles = {
name: Profile(profile.name, profile.output_dir, profile.clipboard, False, profile.reveal)
for name, profile in profiles.items()
}
if args.reveal:
profiles = {
name: Profile(profile.name, profile.output_dir, profile.clipboard, profile.notify, True)
for name, profile in profiles.items()
}
return profiles
def main() -> None: def main() -> None:
args = parse_args() args = parse_args()
profiles = build_profiles(args) config = build_config(args)
default_profile = normalize_profile(args.profile)
if default_profile not in profiles:
known = ", ".join(sorted(profiles))
raise SystemExit(f"unknown default profile: {default_profile}; expected one of {known}")
output_dir_override = Path(args.output_dir).expanduser().resolve() if args.output_dir else None
server = UploadServer( server = UploadServer(
(args.host, args.port), (args.host, args.port),
UploadHandler, UploadHandler,
profiles, config,
default_profile,
output_dir_override,
args.token, args.token,
args.max_mb * 1024 * 1024, args.max_mb * 1024 * 1024,
args.debounce_seconds,
) )
print(f"listening on http://{args.host}:{args.port}/upload", flush=True) print(f"listening on http://{args.host}:{args.port}/upload", flush=True)
print(f"default profile: {default_profile}", flush=True) print(f"saving to: {config.output_dir}", flush=True)
for profile in profiles.values(): print(f"debounce seconds: {args.debounce_seconds:g}", flush=True)
output_dir = output_dir_override or profile.output_dir print(f"clipboard: {config.clipboard}", flush=True)
print( print(f"notify: {config.notify}", flush=True)
f"profile {profile.name}: dir={output_dir.expanduser().resolve()} " print(f"reveal: {config.reveal}", flush=True)
f"clipboard={profile.clipboard} notify={profile.notify} reveal={profile.reveal}",
flush=True,
)
if not args.token: if not args.token:
print("warning: no token configured; anyone on this network can upload", flush=True) print("warning: no token configured; anyone on this network can upload", flush=True)
server.serve_forever() server.serve_forever()

View File

@@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
HELPER_SRC="$SCRIPT_DIR/copy_files_to_clipboard.swift"
HELPER_BIN="$SCRIPT_DIR/copy_files_to_clipboard"
if command -v swiftc >/dev/null 2>&1; then
if [[ ! -x "$HELPER_BIN" || "$HELPER_SRC" -nt "$HELPER_BIN" ]]; then
swiftc "$HELPER_SRC" -o "$HELPER_BIN"
fi
else
echo "warning: swiftc not found; receiver will run the Swift helper script directly" >&2
fi
exec python3 "$SCRIPT_DIR/receiver.py" "$@"

View File

@@ -0,0 +1,85 @@
#!/usr/bin/env python3
from __future__ import annotations
import importlib.util
import sys
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
RECEIVER_PATH = Path(__file__).with_name("receiver.py")
SPEC = importlib.util.spec_from_file_location("iphone_photo_receiver", RECEIVER_PATH)
receiver = importlib.util.module_from_spec(SPEC)
assert SPEC.loader is not None
sys.modules[SPEC.name] = receiver
SPEC.loader.exec_module(receiver)
class ReceiverTests(unittest.TestCase):
def tearDown(self) -> None:
batch = getattr(self, "batch", None)
if batch is not None and batch.timer is not None:
batch.timer.cancel()
def test_load_env_file_does_not_override_existing_environment(self) -> None:
with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8") as env_file, \
patch.dict(receiver.os.environ, {"PHOTO_INBOX_TOKEN": "existing"}, clear=False):
env_file.write("PHOTO_INBOX_TOKEN=from-file\n")
env_file.write('PHOTO_INBOX_DEBOUNCE_SECONDS="5"\n')
env_file.flush()
receiver.load_env_file(Path(env_file.name))
self.assertEqual(receiver.os.environ["PHOTO_INBOX_TOKEN"], "existing")
self.assertEqual(receiver.os.environ["PHOTO_INBOX_DEBOUNCE_SECONDS"], "5")
def test_batch_add_accumulates_paths(self) -> None:
config = receiver.Config(Path("/tmp"), clipboard=True, notify=False, reveal=False)
manager = receiver.BatchManager(debounce_seconds=60, config=config)
count1, paths1 = manager.add(Path("/tmp/one.jpg"))
count2, paths2 = manager.add(Path("/tmp/two.jpg"))
self.batch = manager.batch
self.assertEqual(count1, 1)
self.assertEqual(paths1, [Path("/tmp/one.jpg")])
self.assertEqual(count2, 2)
self.assertEqual(paths2, [Path("/tmp/one.jpg"), Path("/tmp/two.jpg")])
def test_files_clipboard_passes_all_paths_to_native_helper(self) -> None:
paths = [Path("/tmp/one.jpg"), Path("/tmp/two.jpg")]
with tempfile.NamedTemporaryFile() as helper:
helper_path = Path(helper.name)
with patch.object(receiver, "COPY_FILES_HELPER_BIN", Path("/tmp/missing-helper-bin")), \
patch.object(receiver, "COPY_FILES_HELPER", helper_path), \
patch.object(receiver, "run_macos_action", return_value=True) as run_action:
self.assertTrue(receiver.copy_files_to_clipboard(paths))
run_action.assert_called_once_with([
str(helper_path),
"/tmp/one.jpg",
"/tmp/two.jpg",
])
def test_build_config_uses_boolean_clipboard(self) -> None:
args = type("Args", (), {
"output_dir": Path("/tmp/photos"),
"clipboard": False,
"notify": True,
"reveal": False,
})()
config = receiver.build_config(args)
self.assertEqual(config.output_dir, Path("/tmp/photos").resolve())
self.assertFalse(config.clipboard)
self.assertTrue(config.notify)
self.assertFalse(config.reveal)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,36 @@
# Mattermost proxy mirror configuration.
# Copy to .env if you want local overrides. Do not commit .env.
# Optional: restrict capture to the Mattermost host. Use the host only, no scheme.
# If empty, the addon captures /api/v4 traffic from the proxied Mattermost app.
# Example: mm.all-win-solutions.app
MATTERMOST_MIRROR_HOST_ALLOW=
# Output directory for raw evidence and normalized AI-readable context.
# Optional. If omitted, run-mirror.sh writes to the active profile inbox:
# <inbox_dir from profiles/<profile>/workspace.json>/mattermost-mirror
# MATTERMOST_MIRROR_DIR=/absolute/path/to/mattermost-mirror
# mitmproxy listener used by launch-mattermost.sh.
MATTERMOST_MIRROR_LISTEN_HOST=127.0.0.1
MATTERMOST_MIRROR_LISTEN_PORT=8080
# Keep the small AI context window bounded.
MATTERMOST_MIRROR_LATEST_LIMIT=200
# Optional channel allowlist. Comma-separated channel IDs. Empty means all captured channels.
MATTERMOST_MIRROR_CHANNEL_IDS=
# Optional AI context channel filter for reader commands such as:
# python3 scripts/mattermost-proxy/read-context.py --mode standup --today YYYY-MM-DD
# Use readable channel names or channel IDs. Keep project-specific values in your local .env
# or active profile setup, not in reusable scripts.
# Example: project-main,project-standup,dm-you--manager
AIW_MATTERMOST_CONTEXT_CHANNELS=
# Write compact raw REST/WebSocket evidence in addition to normalized messages.
# Keep disabled by default to avoid large files.
MATTERMOST_MIRROR_WRITE_RAW=0
# Mattermost desktop app bundle.
MATTERMOST_APP_PATH=/Applications/Mattermost.app

View File

@@ -0,0 +1,176 @@
# Mattermost Proxy Mirror
Local read-only Mattermost Desktop mirror for AI workspace context.
This is for **raw evidence only**. By default it writes under the active profile inbox at `<inbox_dir>/mattermost-mirror/`; durable project memory still belongs in the profile's canonical project knowledge vault after normal promotion rules.
## Why this exists
Mattermost Team Edition 11.4.2 exposes normal `/api/v4` REST and WebSocket traffic. When Mattermost Desktop is launched with Chromium/Electron's `--proxy-server` flag, `mitmproxy` can capture only that app without changing the macOS system proxy.
## Setup
1. Install `mitmproxy`.
2. Trust the mitmproxy certificate if HTTPS interception is not already working:
- Start `scripts/mattermost-proxy/run-mirror.sh`
- Open `http://mitm.it`
- Install/trust the certificate in Keychain.
3. Optional: copy `.env.example` to `.env` and set `MATTERMOST_MIRROR_HOST_ALLOW` to the exact Mattermost host, for example `mm.all-win-solutions.app`.
## Run day to day
Terminal 1:
```bash
scripts/mattermost-proxy/run-mirror.sh
```
Terminal 2:
```bash
scripts/mattermost-proxy/launch-mattermost.sh
```
This launches Mattermost Desktop through macOS LaunchServices with:
```bash
--proxy-server=http://127.0.0.1:8080
```
No global macOS proxy is required.
The helper intentionally uses `open -n /Applications/Mattermost.app --args ...`
instead of invoking `/Applications/Mattermost.app/Contents/MacOS/Mattermost`
directly. Direct binary launch can crash sandboxed Electron apps with Mach
rendezvous errors because their expected app/container parent process is
missing.
## Output layout
```text
<profile inbox>/mattermost-mirror/
latest.jsonl # bounded AI-readable window
latest.md # bounded Markdown view
state.json # last seen by channel and user cache
index.json # date/channel/thread file map
refs/
channels.json # channel_id -> channel_name
users.json # user_id -> username
channels/<channel-name>/YYYY/MM/YYYY-MM-DD.jsonl
by-date/YYYY/MM/YYYY-MM-DD.jsonl
threads/<root-or-post-id>.jsonl
raw/YYYY/MM/YYYY-MM-DD-websocket.jsonl # only if MATTERMOST_MIRROR_WRITE_RAW=1
raw/YYYY/MM/YYYY-MM-DD-rest-flows.jsonl # only if MATTERMOST_MIRROR_WRITE_RAW=1
```
Use `latest.md` or `latest.jsonl` for quick AI context. Use `channels/...`
for conversation-focused analysis, `by-date/...` for standups or daily review,
and `threads/...` when a single discussion thread is the relevant evidence.
This mirrors Slack's export pattern of one folder per conversation with one file
per date, while adding Mattermost-specific thread views.
For standup generation, prefer the focused reader instead of loading broad
`latest.md` directly:
```bash
python3 scripts/mattermost-proxy/read-context.py --mode standup --today YYYY-MM-DD
```
`standup` mode reads only date-bucketed records for the previous workday and
today. To avoid spending tokens on unrelated channels or stale global `latest.md`
content, configure project-specific context channels in the connector-local
`.env` or pass them explicitly. Keep those channel values out of reusable scripts.
```bash
AIW_MATTERMOST_CONTEXT_CHANNELS="project-main,project-standup,dm-you--manager" \
python3 scripts/mattermost-proxy/read-context.py --mode standup --today YYYY-MM-DD
python3 scripts/mattermost-proxy/read-context.py --mode standup --today YYYY-MM-DD \
--channels "project-main,project-standup,dm-you--manager"
```
If no context channel filter is configured, `standup` mode still avoids
`latest.md` and reads date-bucketed records only, but it will include all mirrored
channels for those dates.
Direct-message channels are labeled as `dm-<user-a>--<user-b>` when the mirror
has seen enough user metadata to resolve the Mattermost channel ID. Group DMs
use `group-...`. If a DM was first captured before the relevant user metadata
arrived, the folder can temporarily use raw IDs; later captures use the readable
label and `refs/channels.json` remains the source for resolving channel IDs.
The mirror writes any post payload it sees, including older messages returned
when the desktop app loads channel history or a thread. It dedupes by `post_id`,
so scrolling back through useful history is a safe way to backfill missing local
evidence without creating repeated entries.
## Normalized message schema
Each line in the normalized JSONL contains:
```json
{
"source": "websocket|rest",
"captured_at": "2026-05-19T...Z",
"created_at": "2026-05-19T...Z",
"created_at_ms": 1779190000000,
"channel_id": "...",
"channel_name": "fidelity-preguntas",
"post_id": "...",
"root_id": "...",
"thread_id": "...",
"user_id": "...",
"username": "jeff",
"message": "...",
"type": "channel_post|thread_reply",
"raw_event": "posted|posts|post"
}
```
## Safety rules
- The addon allowlists Mattermost hosts and `/api/v4` traffic only.
- Headers such as `Authorization`, `Cookie`, `Set-Cookie`, and CSRF are redacted in optional raw output.
- Optional raw output is disabled by default to prevent large files.
- Attachments are not downloaded by this mirror.
- The mirror is evidence, not canonical memory.
## Useful environment variables
- `MATTERMOST_MIRROR_HOST_ALLOW`: exact host or parent domain to capture.
- `MATTERMOST_MIRROR_DIR`: optional output directory. If omitted, `run-mirror.sh` uses `<inbox_dir from profiles/<profile>/workspace.json>/mattermost-mirror`.
- `MATTERMOST_MIRROR_LATEST_LIMIT`: number of messages in `latest.*`, default `200`.
- `MATTERMOST_MIRROR_CHANNEL_IDS`: optional comma-separated channel ID allowlist.
- `MATTERMOST_MIRROR_WRITE_RAW`: set to `1` to save compact raw REST/WebSocket evidence.
- `MATTERMOST_APP_PATH`: Mattermost Desktop `.app` bundle path.
- `AIW_MATTERMOST_PROJECT_CHANNELS`: optional comma-separated channel names or IDs for focused standup reads.
## Troubleshooting
### TLS certificate warnings
Mitmproxy uses a persistent local CA under `~/.mitmproxy`. If the desktop app
asks about the certificate after every proxy restart, install and trust that CA
in macOS Keychain instead of approving it only in the app prompt:
1. Start `scripts/mattermost-proxy/run-mirror.sh`.
2. Open `http://mitm.it` from a browser on this Mac and download the macOS certificate.
3. Add it to Keychain Access and set it to **Always Trust**.
4. Restart Mattermost Desktop through `launch-mattermost.sh`.
Warnings for unrelated hosts such as `releases.mattermost.com` or OpenGraph
preview hosts are not required for message capture. The mirror only writes
normalized messages from Mattermost `/api/v4` REST/WebSocket payloads.
### Proxy logs show traffic but no `latest.md`
The mirror writes files only after it sees a post payload. Startup calls such as
`/api/v4/teams`, `/api/v4/users`, `/api/v4/files`, or WebSocket ping/ack events
do not create message files. Open a channel, open a thread, scroll slightly in
history, or wait for/send a new message. Then check:
```text
<profile inbox>/mattermost-mirror/latest.md
<profile inbox>/mattermost-mirror/channels/<channel-name>/YYYY/MM/YYYY-MM-DD.jsonl
<profile inbox>/mattermost-mirror/by-date/YYYY/MM/YYYY-MM-DD.jsonl
```

View File

@@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [ -f "$SCRIPT_DIR/.env" ]; then
set -a
# shellcheck source=/dev/null
source "$SCRIPT_DIR/.env"
set +a
fi
APP_PATH="${MATTERMOST_APP_PATH:-/Applications/Mattermost.app}"
PROXY_HOST="${MATTERMOST_MIRROR_LISTEN_HOST:-127.0.0.1}"
PROXY_PORT="${MATTERMOST_MIRROR_LISTEN_PORT:-8080}"
if [ ! -d "$APP_PATH" ]; then
echo "Mattermost app bundle not found: $APP_PATH" >&2
echo "Set MATTERMOST_APP_PATH in scripts/mattermost-proxy/.env if needed." >&2
exit 1
fi
# Prefer macOS LaunchServices over invoking the Electron binary directly.
# Direct binary launch can crash sandboxed Electron apps with Mach rendezvous
# errors because their expected app/container parent process is missing.
exec open -n "$APP_PATH" --args --proxy-server="http://${PROXY_HOST}:${PROXY_PORT}"

View File

@@ -0,0 +1,514 @@
"""mitmproxy addon for a local Mattermost Desktop mirror.
This addon is intentionally narrow:
- allowlist a Mattermost host
- inspect only /api/v4 REST and WebSocket traffic
- redact secrets
- normalize posts into date-rotated JSONL files for AI context
The output under the profile inbox is raw evidence, not canonical project memory.
"""
from __future__ import annotations
import json
import os
import re
import tempfile
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from urllib.parse import urlparse
from mitmproxy import http
DEFAULT_OUT_DIR = "mattermost-mirror"
POST_ID_RE = re.compile(r"^[a-z0-9]{26}$")
SAFE_NAME_RE = re.compile(r"[^a-zA-Z0-9._-]+")
def env_bool(name: str, default: bool = False) -> bool:
raw = os.getenv(name)
if raw is None:
return default
return raw.strip().lower() in {"1", "true", "yes", "on"}
def split_csv(raw: str) -> set[str]:
return {item.strip() for item in raw.replace("\n", ",").split(",") if item.strip()}
class MattermostMirror:
def __init__(self) -> None:
self.out_dir = Path(os.getenv("MATTERMOST_MIRROR_DIR", DEFAULT_OUT_DIR)).resolve()
self.host_allow = os.getenv("MATTERMOST_MIRROR_HOST_ALLOW", "").strip().lower()
self.channel_allow = split_csv(os.getenv("MATTERMOST_MIRROR_CHANNEL_IDS", ""))
self.latest_limit = int(os.getenv("MATTERMOST_MIRROR_LATEST_LIMIT", "200"))
self.write_raw = env_bool("MATTERMOST_MIRROR_WRITE_RAW", default=False)
self.channels_dir = self.out_dir / "channels"
self.by_date_dir = self.out_dir / "by-date"
self.threads_dir = self.out_dir / "threads"
self.refs_dir = self.out_dir / "refs"
self.raw_dir = self.out_dir / "raw"
self.state_path = self.out_dir / "state.json"
self.index_path = self.out_dir / "index.json"
self.latest_jsonl_path = self.out_dir / "latest.jsonl"
self.latest_md_path = self.out_dir / "latest.md"
self.seen_post_ids: set[str] = set()
self.seen_by_file: dict[Path, set[str]] = {}
self.users: dict[str, str] = {}
self.channels: dict[str, str] = {}
self.channel_meta: dict[str, dict[str, Any]] = {}
self.state: dict[str, Any] = {"channels": {}, "users": {}, "updated_at": None}
self._ensure_dirs()
self._load_state()
self._load_recent_seen_ids()
def _ensure_dirs(self) -> None:
self.channels_dir.mkdir(parents=True, exist_ok=True)
self.by_date_dir.mkdir(parents=True, exist_ok=True)
self.threads_dir.mkdir(parents=True, exist_ok=True)
self.refs_dir.mkdir(parents=True, exist_ok=True)
self.raw_dir.mkdir(parents=True, exist_ok=True)
def _load_state(self) -> None:
if not self.state_path.exists():
return
try:
self.state = json.loads(self.state_path.read_text(encoding="utf-8"))
self.users = dict(self.state.get("users") or {})
self.channel_meta = dict(self.state.get("channel_meta") or {})
for channel_id, value in (self.state.get("channels") or {}).items():
if isinstance(value, dict):
name = value.get("channel_name") or value.get("name")
if name:
self.channels[channel_id] = name
except Exception:
self.state = {"channels": {}, "users": {}, "updated_at": None}
def _load_recent_seen_ids(self) -> None:
# Bound startup work: latest.jsonl contains the hot dedupe window. Daily
# files are loaded lazily when older/backfilled messages are encountered.
today = datetime.now(timezone.utc)
for path in [self.latest_jsonl_path, self._daily_by_date_path(today)]:
if not path.exists():
continue
try:
ids = self._load_seen_ids_for_file(path)
self.seen_post_ids.update(ids)
except Exception:
continue
def _load_seen_ids_for_file(self, path: Path) -> set[str]:
if path in self.seen_by_file:
return self.seen_by_file[path]
ids: set[str] = set()
if path.exists():
try:
with path.open("r", encoding="utf-8") as handle:
for line in handle:
if not line.strip():
continue
obj = json.loads(line)
post_id = obj.get("post_id")
if post_id:
ids.add(post_id)
except Exception:
ids = set()
self.seen_by_file[path] = ids
return ids
def _atomic_write_text(self, path: Path, text: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=str(path.parent), delete=False) as tmp:
tmp.write(text)
tmp_path = Path(tmp.name)
tmp_path.replace(path)
def _append_jsonl(self, path: Path, obj: dict[str, Any]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(obj, ensure_ascii=False, sort_keys=True) + "\n")
def _dt_from_ms(self, value: Any) -> datetime:
try:
ms = int(value)
if ms > 0:
return datetime.fromtimestamp(ms / 1000, timezone.utc)
except Exception:
pass
return datetime.now(timezone.utc)
def _safe_name(self, value: str | None, fallback: str = "unknown") -> str:
raw = (value or fallback).strip() or fallback
safe = SAFE_NAME_RE.sub("-", raw).strip("-._")
return safe or fallback
def _daily_channel_path(self, dt: datetime, channel_name: str | None, channel_id: str | None) -> Path:
channel_slug = self._safe_name(channel_name or channel_id, fallback="unknown-channel")
return self.channels_dir / channel_slug / f"{dt:%Y}" / f"{dt:%m}" / f"{dt:%Y-%m-%d}.jsonl"
def _daily_by_date_path(self, dt: datetime) -> Path:
return self.by_date_dir / f"{dt:%Y}" / f"{dt:%m}" / f"{dt:%Y-%m-%d}.jsonl"
def _thread_path(self, thread_id: str | None) -> Path | None:
if not thread_id:
return None
return self.threads_dir / f"{self._safe_name(thread_id)}.jsonl"
def _daily_raw_path(self, dt: datetime, suffix: str) -> Path:
return self.raw_dir / f"{dt:%Y}" / f"{dt:%m}" / f"{dt:%Y-%m-%d}-{suffix}.jsonl"
def _safe_url(self, url: str) -> str:
parsed = urlparse(url)
return parsed._replace(query=parsed.query, fragment="").geturl()
def _is_allowed_host(self, host: str) -> bool:
host = host.lower()
if self.host_allow:
return host == self.host_allow or host.endswith(f".{self.host_allow}")
# The launched Mattermost Desktop app is already scoped to this proxy.
# Some company hosts do not include "mattermost" in the hostname
# (for example, mm.example.com), so default to allowing the proxied
# app's /api/v4 traffic when no explicit host allowlist is configured.
return True
def _is_allowed_channel(self, channel_id: str | None) -> bool:
if not self.channel_allow:
return True
return bool(channel_id and channel_id in self.channel_allow)
def _capture_flow(self, flow: http.HTTPFlow) -> bool:
return self._is_allowed_host(flow.request.pretty_host) and "/api/v4/" in flow.request.path
def _redact_headers(self, headers: Any) -> dict[str, str]:
redacted: dict[str, str] = {}
for key, value in headers.items():
lowered = key.lower()
if lowered in {"authorization", "cookie", "set-cookie", "x-csrf-token"}:
redacted[key] = "[REDACTED]"
else:
redacted[key] = str(value)
return redacted
def _remember_user(self, user: dict[str, Any]) -> None:
user_id = user.get("id")
if not user_id:
return
username = user.get("username") or user.get("nickname") or user.get("first_name") or user_id
self.users[user_id] = username
self._write_refs()
def _remember_channel(self, channel: dict[str, Any]) -> None:
channel_id = channel.get("id")
if not channel_id:
return
self.channel_meta[channel_id] = channel
name = self._channel_label(channel)
self.channels[channel_id] = name
self._write_refs()
def _user_label(self, user_id: str | None) -> str | None:
if not user_id:
return None
return self.users.get(user_id) or user_id
def _channel_label(self, channel: dict[str, Any]) -> str:
channel_id = channel.get("id") or "unknown-channel"
channel_type = channel.get("type")
display_name = (channel.get("display_name") or "").strip()
name = (channel.get("name") or "").strip()
if channel_type == "D":
user_ids = [item for item in name.split("__") if item]
labels = [self._user_label(user_id) or user_id for user_id in user_ids]
if labels:
return "dm-" + "--".join(labels)
if channel_type == "G":
if display_name:
return "group-" + display_name
user_ids = [item for item in name.split("__") if item]
labels = [self._user_label(user_id) or user_id for user_id in user_ids]
if labels:
return "group-" + "--".join(labels)
return display_name or name or channel_id
def _refresh_channel_labels(self) -> None:
changed = False
for channel_id, meta in self.channel_meta.items():
label = self._channel_label(meta)
if label and self.channels.get(channel_id) != label:
self.channels[channel_id] = label
changed = True
if changed:
self._write_refs()
def _write_refs(self) -> None:
users_path = self.refs_dir / "users.json"
channels_path = self.refs_dir / "channels.json"
self._atomic_write_text(users_path, json.dumps(self.users, ensure_ascii=False, indent=2, sort_keys=True) + "\n")
self._atomic_write_text(channels_path, json.dumps(self.channels, ensure_ascii=False, indent=2, sort_keys=True) + "\n")
def _ingest_reference_payload(self, payload: Any) -> None:
if isinstance(payload, list):
for item in payload:
self._ingest_reference_payload(item)
return
if not isinstance(payload, dict):
return
if payload.get("id") and ("username" in payload or "first_name" in payload):
self._remember_user(payload)
if payload.get("id") and ("display_name" in payload or "team_id" in payload) and "type" in payload:
self._remember_channel(payload)
users = payload.get("users")
if isinstance(users, dict):
for user in users.values():
if isinstance(user, dict):
self._remember_user(user)
elif isinstance(users, list):
for user in users:
if isinstance(user, dict):
self._remember_user(user)
channels = payload.get("channels")
if isinstance(channels, list):
for channel in channels:
if isinstance(channel, dict):
self._remember_channel(channel)
self._refresh_channel_labels()
def _normalize_post(self, post: dict[str, Any], source: str, raw_event: str | None = None) -> dict[str, Any] | None:
post_id = post.get("id")
channel_id = post.get("channel_id")
if not post_id or not POST_ID_RE.match(str(post_id)):
return None
if not self._is_allowed_channel(channel_id):
return None
created_dt = self._dt_from_ms(post.get("create_at"))
root_id = post.get("root_id") or None
user_id = post.get("user_id") or None
message = post.get("message") or ""
message_type = "thread_reply" if root_id else "channel_post"
return {
"source": source,
"captured_at": datetime.now(timezone.utc).isoformat(),
"created_at": created_dt.isoformat(),
"created_at_ms": int(post.get("create_at") or created_dt.timestamp() * 1000),
"updated_at_ms": int(post.get("update_at") or 0),
"channel_id": channel_id,
"channel_name": self.channels.get(channel_id) if channel_id else None,
"post_id": post_id,
"root_id": root_id,
"thread_id": root_id or post_id,
"user_id": user_id,
"username": self.users.get(user_id) if user_id else None,
"message": message,
"type": message_type,
"raw_event": raw_event,
"props": post.get("props") or {},
}
def _write_message(self, msg: dict[str, Any]) -> None:
post_id = msg["post_id"]
created_dt = self._dt_from_ms(msg.get("created_at_ms"))
channel_path = self._daily_channel_path(created_dt, msg.get("channel_name"), msg.get("channel_id"))
by_date_path = self._daily_by_date_path(created_dt)
thread_path = self._thread_path(msg.get("thread_id"))
channel_seen = self._load_seen_ids_for_file(channel_path)
by_date_seen = self._load_seen_ids_for_file(by_date_path)
if post_id in self.seen_post_ids or post_id in channel_seen or post_id in by_date_seen:
return
self.seen_post_ids.add(post_id)
channel_seen.add(post_id)
by_date_seen.add(post_id)
self._append_jsonl(channel_path, msg)
self._append_jsonl(by_date_path, msg)
if thread_path:
thread_seen = self._load_seen_ids_for_file(thread_path)
if post_id not in thread_seen:
thread_seen.add(post_id)
self._append_jsonl(thread_path, msg)
self._update_state(msg)
self._update_latest(msg)
self._update_index(created_dt, msg)
def _update_state(self, msg: dict[str, Any]) -> None:
channel_id = msg.get("channel_id") or "unknown"
channels = self.state.setdefault("channels", {})
entry = channels.setdefault(channel_id, {})
if msg.get("channel_name"):
entry["channel_name"] = msg.get("channel_name")
entry["last_seen_create_at"] = max(int(entry.get("last_seen_create_at") or 0), int(msg.get("created_at_ms") or 0))
entry["last_seen_post_id"] = msg.get("post_id")
self.state["users"] = self.users
self.state["channel_meta"] = self.channel_meta
self.state["updated_at"] = datetime.now(timezone.utc).isoformat()
self._atomic_write_text(self.state_path, json.dumps(self.state, ensure_ascii=False, indent=2, sort_keys=True) + "\n")
self._write_refs()
def _read_jsonl(self, path: Path) -> list[dict[str, Any]]:
if not path.exists():
return []
records: list[dict[str, Any]] = []
try:
with path.open("r", encoding="utf-8") as handle:
for line in handle:
if line.strip():
records.append(json.loads(line))
except Exception:
return []
return records
def _update_latest(self, msg: dict[str, Any]) -> None:
records = self._read_jsonl(self.latest_jsonl_path)
by_id: dict[str, dict[str, Any]] = {item.get("post_id"): item for item in records if item.get("post_id")}
by_id[msg["post_id"]] = msg
latest = sorted(by_id.values(), key=lambda item: int(item.get("created_at_ms") or 0))[-self.latest_limit :]
jsonl = "".join(json.dumps(item, ensure_ascii=False, sort_keys=True) + "\n" for item in latest)
self._atomic_write_text(self.latest_jsonl_path, jsonl)
self._atomic_write_text(self.latest_md_path, self._render_latest_md(latest))
def _render_latest_md(self, records: list[dict[str, Any]]) -> str:
lines = ["# Latest Mattermost Mirror", "", "Generated from local proxy mirror evidence.", ""]
current_channel = None
for item in records:
channel = item.get("channel_name") or item.get("channel_id") or "unknown-channel"
if channel != current_channel:
lines.extend([f"## {channel}", ""])
current_channel = channel
author = item.get("username") or item.get("user_id") or "unknown-user"
created = item.get("created_at") or "unknown-time"
prefix = "reply" if item.get("type") == "thread_reply" else "post"
text = (item.get("message") or "").strip()
lines.append(f"- {created} {author} ({prefix} `{item.get('post_id')}`): {text}")
lines.append("")
return "\n".join(lines)
def _update_index(self, dt: datetime, msg: dict[str, Any]) -> None:
index: dict[str, Any] = {"dates": [], "channels": {}, "updated_at": None}
if self.index_path.exists():
try:
index = json.loads(self.index_path.read_text(encoding="utf-8"))
except Exception:
pass
date_key = f"{dt:%Y-%m-%d}"
channel_path = self._daily_channel_path(dt, msg.get("channel_name"), msg.get("channel_id"))
by_date_path = self._daily_by_date_path(dt)
thread_path = self._thread_path(msg.get("thread_id"))
channel_rel_path = str(channel_path.relative_to(self.out_dir))
by_date_rel_path = str(by_date_path.relative_to(self.out_dir))
dates = set(index.get("dates") or [])
dates.add(date_key)
index["dates"] = sorted(dates)
by_date = index.setdefault("by_date", {})
by_date[date_key] = by_date_rel_path
channel_key = msg.get("channel_name") or msg.get("channel_id") or "unknown-channel"
channels = index.setdefault("channels", {})
channel_entry = channels.setdefault(channel_key, {"channel_id": msg.get("channel_id"), "files": []})
channel_entry["channel_id"] = msg.get("channel_id")
files = set(channel_entry.get("files") or [])
files.add(channel_rel_path)
channel_entry["files"] = sorted(files)
if thread_path:
threads = index.setdefault("threads", {})
threads[msg.get("thread_id")] = str(thread_path.relative_to(self.out_dir))
index["updated_at"] = datetime.now(timezone.utc).isoformat()
self._atomic_write_text(self.index_path, json.dumps(index, ensure_ascii=False, indent=2, sort_keys=True) + "\n")
def _write_raw(self, suffix: str, obj: dict[str, Any]) -> None:
if not self.write_raw:
return
self._append_jsonl(self._daily_raw_path(datetime.now(timezone.utc), suffix), obj)
def response(self, flow: http.HTTPFlow) -> None:
if not self._capture_flow(flow) or not flow.response:
return
content_type = flow.response.headers.get("content-type", "")
if "json" not in content_type:
return
try:
payload = flow.response.json()
except Exception:
return
self._ingest_reference_payload(payload)
path = flow.request.path
raw_record = {
"captured_at": datetime.now(timezone.utc).isoformat(),
"method": flow.request.method,
"url": self._safe_url(flow.request.pretty_url),
"path": path,
"status_code": flow.response.status_code,
"request_headers": self._redact_headers(flow.request.headers),
"response": payload,
}
self._write_raw("rest-flows", raw_record)
# Mattermost post-list shape: { order: [...], posts: {post_id: {...}} }
if isinstance(payload, dict) and isinstance(payload.get("posts"), dict):
for post in payload["posts"].values():
if isinstance(post, dict):
normalized = self._normalize_post(post, source="rest", raw_event="posts")
if normalized:
self._write_message(normalized)
elif isinstance(payload, dict) and payload.get("id") and payload.get("message") is not None:
normalized = self._normalize_post(payload, source="rest", raw_event="post")
if normalized:
self._write_message(normalized)
def websocket_message(self, flow: http.HTTPFlow) -> None:
if not self._is_allowed_host(flow.request.pretty_host):
return
if "/api/v4/websocket" not in flow.request.path:
return
if not flow.websocket or not flow.websocket.messages:
return
message = flow.websocket.messages[-1]
if message.from_client:
return
try:
text = message.content.decode("utf-8") if isinstance(message.content, bytes) else str(message.content)
payload = json.loads(text)
except Exception:
return
self._write_raw("websocket", {
"captured_at": datetime.now(timezone.utc).isoformat(),
"url": self._safe_url(flow.request.pretty_url),
"event": payload.get("event"),
"seq": payload.get("seq"),
"data": payload.get("data"),
"broadcast": payload.get("broadcast"),
})
event = payload.get("event")
if event != "posted":
return
data = payload.get("data") or {}
post_raw = data.get("post")
if not post_raw:
return
try:
post = json.loads(post_raw)
except Exception:
return
normalized = self._normalize_post(post, source="websocket", raw_event=event)
if normalized:
self._write_message(normalized)
addons = [MattermostMirror()]

View File

@@ -0,0 +1,241 @@
#!/usr/bin/env python3
"""Read local Mattermost mirror evidence for agent prompts.
The proxy mirror under the active profile inbox is the preferred Mattermost
evidence source when present. This reader gives commands a stable way to load a
small focused view and fall back to the older sync artifacts when the mirror is
not available.
"""
from __future__ import annotations
import argparse
import json
import os
import shlex
import sys
from datetime import date, datetime, timedelta
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "scripts" / "aiw"))
import profile as aiw_profile # noqa: E402
DEFAULT_PROFILE = os.getenv("AIW_PROJECT_PROFILE", "fidelity")
MIRROR_DIR = aiw_profile.inbox_dir(DEFAULT_PROFILE, root=ROOT) / "mattermost-mirror"
LEGACY_LATEST = aiw_profile.inbox_dir(DEFAULT_PROFILE, root=ROOT) / "mattermost-latest.md"
LEGACY_GENERATED = ROOT / "scripts" / "mattermost" / "generated" / "mattermost_context.jsonl"
LOCAL_ENV = Path(__file__).resolve().parent / ".env"
def configure_profile_paths(profile: str) -> None:
global MIRROR_DIR, LEGACY_LATEST
inbox = aiw_profile.inbox_dir(profile, root=ROOT)
MIRROR_DIR = inbox / "mattermost-mirror"
LEGACY_LATEST = inbox / "mattermost-latest.md"
def load_local_env(path: Path = LOCAL_ENV) -> None:
"""Load simple KEY=VALUE pairs from the connector-local .env.
Existing process environment values win. This keeps the reusable reader
project-agnostic while allowing each workspace/profile to provide its own
channel filters without hardcoding them in Python.
"""
if not path.is_file():
return
for raw_line in path.read_text(encoding="utf-8").splitlines():
line = raw_line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
if line.startswith("export "):
line = line[len("export ") :].strip()
key, value = line.split("=", 1)
key = key.strip()
value = value.strip()
if not key or key in os.environ:
continue
try:
parsed = shlex.split(value, comments=False, posix=True)
value = parsed[0] if parsed else ""
except ValueError:
value = value.strip('"\'')
os.environ[key] = value
def previous_workday(today: date) -> date:
day = today - timedelta(days=1)
while day.weekday() >= 5:
day -= timedelta(days=1)
return day
def daily_by_date_path(day: date) -> Path:
return MIRROR_DIR / "by-date" / f"{day:%Y}" / f"{day:%m}" / f"{day:%Y-%m-%d}.jsonl"
def print_file(path: Path) -> bool:
if path.is_file() and path.stat().st_size > 0:
print(path.read_text(encoding="utf-8"))
return True
return False
def read_jsonl(path: Path) -> list[dict]:
records = []
if not path.is_file():
return records
for line in path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line:
continue
try:
records.append(json.loads(line))
except json.JSONDecodeError:
continue
return records
def parse_channels(raw: str | None) -> set[str]:
if not raw:
env_raw = os.getenv("AIW_MATTERMOST_CONTEXT_CHANNELS", "") or os.getenv(
"AIW_MATTERMOST_PROJECT_CHANNELS", ""
)
raw = env_raw
if not raw:
return set()
return {item.strip() for item in raw.split(",") if item.strip()}
def filter_channels(records: list[dict], channels: set[str] | None) -> list[dict]:
if not channels:
return records
lowered = {channel.lower() for channel in channels}
return [
record
for record in records
if str(record.get("channel_name", "")).lower() in lowered
or str(record.get("channel_id", "")).lower() in lowered
]
def trim_records(records: list[dict], limit: int | None) -> list[dict]:
if limit is None or limit <= 0 or len(records) <= limit:
return records
return records[-limit:]
def print_jsonl(records: list[dict]) -> bool:
if not records:
return False
for record in records:
print(json.dumps(record, ensure_ascii=False, sort_keys=True))
return True
def fallback() -> None:
if print_file(LEGACY_LATEST):
return
if print_file(LEGACY_GENERATED):
return
print("No Mattermost context available.")
def mode_latest() -> None:
print("## Mattermost mirror latest context")
if print_file(MIRROR_DIR / "latest.md"):
return
records = read_jsonl(MIRROR_DIR / "latest.jsonl")
if print_jsonl(records):
return
print("No proxy mirror latest context available; falling back to legacy sync artifacts.")
fallback()
def print_records_section(title: str, records: list[dict], limit: int | None = None) -> bool:
print(title)
records = trim_records(records, limit)
if print_jsonl(records):
return True
print("No matching Mattermost mirror records.")
return False
def mode_previous_workday(today_raw: str | None, channels: set[str] | None = None, limit: int | None = None) -> None:
today = datetime.strptime(today_raw, "%Y-%m-%d").date() if today_raw else date.today()
day = previous_workday(today)
path = daily_by_date_path(day)
records = filter_channels(read_jsonl(path), channels)
if print_records_section(f"## Mattermost mirror previous-workday context ({day.isoformat()})", records, limit):
return
if channels:
print("Filtered to project channels: " + ", ".join(sorted(channels)))
print("No proxy mirror previous-workday project context available; not falling back to broad latest context.")
def mode_standup(today_raw: str | None, channels: set[str], limit: int | None) -> None:
"""Print a compact standup-focused view.
Standup mode intentionally avoids latest.md because latest.md is a bounded
global window and can contain stale or unrelated channels. Use date-bucketed
mirror files filtered to known project channels instead.
"""
today = datetime.strptime(today_raw, "%Y-%m-%d").date() if today_raw else date.today()
day = previous_workday(today)
previous_records = filter_channels(read_jsonl(daily_by_date_path(day)), channels)
today_records = filter_channels(read_jsonl(daily_by_date_path(today)), channels)
print("## Mattermost mirror standup context")
if channels:
print("Filtered to configured context channels: " + ", ".join(sorted(channels)))
else:
print("No context channel filter configured; using all mirrored date-bucket records.")
print_records_section(f"\n### Previous workday ({day.isoformat()})", previous_records, limit)
print_records_section(f"\n### Today so far ({today.isoformat()})", today_records, limit)
def mode_focused() -> None:
records = read_jsonl(MIRROR_DIR / "latest.jsonl")
if not records:
records = read_jsonl(LEGACY_GENERATED)
if not records:
print("No Mattermost context available.")
return
manager_names = {"jeff", "jeff.dewitte"}
manager_records = [record for record in records if str(record.get("username", "")).lower() in manager_names]
focused = manager_records[-10:] if manager_records else records[-15:]
print_jsonl(focused)
def main() -> None:
load_local_env()
parser = argparse.ArgumentParser()
parser.add_argument("--profile", default=DEFAULT_PROFILE)
parser.add_argument("--mode", choices=["latest", "previous-workday", "standup", "focused"], default="latest")
parser.add_argument("--today", default="")
parser.add_argument(
"--channels",
default="",
help="Comma-separated channel names or IDs. Defaults to AIW_MATTERMOST_CONTEXT_CHANNELS from environment/.env when set.",
)
parser.add_argument("--limit", type=int, default=80, help="Max records per section; use 0 for no limit.")
args = parser.parse_args()
configure_profile_paths(args.profile)
channels = parse_channels(args.channels or None)
limit = args.limit if args.limit > 0 else None
if args.mode == "latest":
mode_latest()
elif args.mode == "previous-workday":
mode_previous_workday(args.today or None, channels=None, limit=limit)
elif args.mode == "standup":
mode_standup(args.today or None, channels, limit)
elif args.mode == "focused":
mode_focused()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORKSPACE_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
if [ -f "$SCRIPT_DIR/.env" ]; then
set -a
# shellcheck source=/dev/null
source "$SCRIPT_DIR/.env"
set +a
fi
PROFILE="${AIW_PROJECT_PROFILE:-fidelity}"
PROFILE_INBOX_DIR="$(python3 "$WORKSPACE_ROOT/scripts/aiw/profile.py" path inbox --profile "$PROFILE")"
export MATTERMOST_MIRROR_DIR="${MATTERMOST_MIRROR_DIR:-$PROFILE_INBOX_DIR/mattermost-mirror}"
export MATTERMOST_MIRROR_LISTEN_HOST="${MATTERMOST_MIRROR_LISTEN_HOST:-127.0.0.1}"
export MATTERMOST_MIRROR_LISTEN_PORT="${MATTERMOST_MIRROR_LISTEN_PORT:-8080}"
mkdir -p "$MATTERMOST_MIRROR_DIR"
echo "Mattermost proxy mirror output: $MATTERMOST_MIRROR_DIR"
echo "Listening on ${MATTERMOST_MIRROR_LISTEN_HOST}:${MATTERMOST_MIRROR_LISTEN_PORT}"
echo "Launch Mattermost Desktop with: scripts/mattermost-proxy/launch-mattermost.sh"
if [ -z "${MATTERMOST_MIRROR_HOST_ALLOW:-}" ]; then
echo "MATTERMOST_MIRROR_HOST_ALLOW is not set; capturing /api/v4 traffic from the proxied app."
fi
exec mitmdump \
--listen-host "$MATTERMOST_MIRROR_LISTEN_HOST" \
--listen-port "$MATTERMOST_MIRROR_LISTEN_PORT" \
-s "$SCRIPT_DIR/mattermost_mirror.py"

Some files were not shown because too many files have changed in this diff Show More