Imported from kody-w/RAR (
scout/workflows/rappstore-kody-w-project-tracker-singleton/skills/rappstore-kody-w-project-tracker-singleton/SKILL.md). Install upstream withnpx skills add kody-w/RAR --skill rappstore-kody-w-project-tracker-singleton. Copyright stays with the author.
Microsoft Scout runtime
This is the reversible Scout projection of @kody-w/project-tracker-singleton. The original RAPP
agent is preserved byte-for-byte in project_tracker_agent.py and in the RCI capsule.
When Scout can execute local files, resolve this skill directory and run:
python3 scripts/run_agent.py --preflight
echo '{}' | python3 scripts/run_agent.py
Pass the real JSON arguments instead of {}. The runner verifies the linked
agent SHA-256 before importing it. If preflight reports a host dependency that
Scout cannot satisfy, use the brainstem_chat MCP tool to run the canonical
agent in the user's Brainstem. Never paraphrase the factory or agent into a new
implementation. The generic direct-file commands in the generated Toaster
section are recovery guidance; Scout should prefer the verified runner.
project_tracker_agent.py — Local-first project & agent portfolio tracker.
A headless, drop-in RAPP agent that manages a portfolio of AI-agent opportunities in the EXACT data shape of the aibast-agents-library "local-first project tracker" web tool:
microsoft.github.io/aibast-agents-library/tools/localfirst_project_tracker_tool.html
Whatever this agent writes exports and imports straight into that tool's
"Merge Import Data (JSON)" (and vice-versa) because it speaks the tool's native
projectTrackerData schema:
appData = {
"projects": [ { id, customerName, status, type, description, stakeholders,
competingSolution, contractDetails, agents:[names], notes,
mvpUseCase, mvpDescription, mvpTimeline,
createdDate, updatedDate } ],
"agents": { "builtin": [ {name, description, category, status} ],
"custom": [ ... ] },
"timeline": [ { date, title, description } ]
}
Local-first: the portfolio lives on THIS device at $PROJECT_TRACKER_DIR or $RAPP_HOME/project-tracker or ~/.rapp/project-tracker as projectTrackerData.json. It is deliberately kept OUTSIDE any twin workspace, so packing this twin into an .egg carries the engine, never the portfolio (which may hold customer data). Nothing here writes customer data into a repo.
Drop into any brainstem's agents/ dir; the next /chat request exposes a
ProjectTracker tool. Stdlib + BasicAgent only — no network, no extra deps.
Run this — do not improvise
This capability's deterministic implementation is a RAPP single-file agent, linked beside this file as project_tracker_agent.py and embedded as the fenced Python below (sha256 b6270faf10379411…; a byte-exact copy is also vaulted in the capsule comment at the end of this file). On a host with sandbox execution, run the linked file directly — if it is missing, write the fence contents verbatim to project_tracker_agent.py first:
python3 project_tracker_agent.py '{"key": "value"}' # arguments as one JSON object
echo '{"key": "value"}' | python3 project_tracker_agent.py # or on stdin
python3 project_tracker_agent.py --tool # emit the JSON tool contract
Treat stdout as a tool result. If it reports missing or unresolved inputs, stop and collect them. If it returns steps, execute those steps in order exactly as returned; if it returns instructions, follow them with the supplied inputs. Otherwise use the result verbatim. Do not invent behavior beyond that output. On a host without code execution, treat the Parameters schema and the code below as the exact specification and never paraphrase a step. Never edit inside the generated markers; a converter-equipped host can instead restore the original file checksum-verified with the installed rapp-agent-converter/scripts/toast.py convert SKILL.md --to agent.
"""project_tracker_agent.py — Local-first project & agent portfolio tracker.
A headless, drop-in RAPP agent that manages a portfolio of AI-agent
opportunities in the EXACT data shape of the aibast-agents-library
"local-first project tracker" web tool:
microsoft.github.io/aibast-agents-library/tools/localfirst_project_tracker_tool.html
Whatever this agent writes exports and imports straight into that tool's
"Merge Import Data (JSON)" (and vice-versa) because it speaks the tool's native
`projectTrackerData` schema:
appData = {
"projects": [ { id, customerName, status, type, description, stakeholders,
competingSolution, contractDetails, agents:[names], notes,
mvpUseCase, mvpDescription, mvpTimeline,
createdDate, updatedDate } ],
"agents": { "builtin": [ {name, description, category, status} ],
"custom": [ ... ] },
"timeline": [ { date, title, description } ]
}
Local-first: the portfolio lives on THIS device at
$PROJECT_TRACKER_DIR or $RAPP_HOME/project-tracker or ~/.rapp/project-tracker
as projectTrackerData.json. It is deliberately kept OUTSIDE any twin workspace,
so packing this twin into an .egg carries the *engine*, never the *portfolio*
(which may hold customer data). Nothing here writes customer data into a repo.
Drop into any brainstem's agents/ dir; the next /chat request exposes a
`ProjectTracker` tool. Stdlib + BasicAgent only — no network, no extra deps.
"""
import json
import os
import re
import time
from agents.basic_agent import BasicAgent
__manifest__ = {
"schema": "rapp-agent/1.0",
"name": "@kody-w/project_tracker_agent",
"version": "1.0.0",
"display_name": "Project Tracker",
"description": (
"Local-first tracker for a portfolio of AI-agent projects and their MVP "
"statements. Speaks the aibast-agents-library project-tracker web tool's "
"native JSON (projectTrackerData) so data round-trips via its JSON "
"import/export. Add/update projects, set the MVP use case/description/"
"timeline, register agents, and export a file ready to merge-import into "
"the web tool. Data stays on this device — never in a repo or egg."
),
"author": "kody-w",
"tags": ["project", "tracker", "portfolio", "mvp", "local-first", "aibast", "rapplication"],
"category": "productivity",
"quality_tier": "community",
"requires_env": [],
"dependencies": ["@rapp/basic_agent"],
"example_call": {"args": {"action": "list_projects"}},
}
# ── Contract enums (must match the web tool's <option> values exactly) ────────
VALID_STATUS = ("planning", "poc", "active", "production", "completed")
# Must match the web tool's <select id="project-type"> values EXACTLY so a
# project's type renders after a merge-import (no "operations"; IT is "it").
VALID_TYPE = ("legal", "hr", "it", "compliance", "customer-service", "other")
VALID_AGENT_CATEGORY = ("contract", "analysis", "workflow", "integration", "other")
VALID_AGENT_STATUS = ("existing", "new", "required")
# The web tool seeds these 8 builtin agents. Mirror them so an export imports
# cleanly (the tool merges builtin agents by name and won't duplicate).
DEFAULT_BUILTIN_AGENTS = [
{"name": "SharePointDocumentExtractor", "description": "Extract content from SharePoint documents", "category": "integration", "status": "existing"},
{"name": "Dynamics365CRUD", "description": "CRUD operations with Dynamics 365", "category": "integration", "status": "existing"},
{"name": "PowerPoint", "description": "Generate PowerPoint presentations", "category": "other", "status": "existing"},
{"name": "ManageMemory", "description": "Memory management for conversations", "category": "other", "status": "existing"},
{"name": "ContractTemplate", "description": "Generate contracts from templates", "category": "contract", "status": "new"},
{"name": "ContractAnalysis", "description": "Analyze contract content and risks", "category": "analysis", "status": "new"},
{"name": "ContractRouting", "description": "Route contracts for approval", "category": "workflow", "status": "new"},
{"name": "ContractMonitoring", "description": "Monitor contract lifecycle", "category": "contract", "status": "new"},
]
# ── Local-first storage ───────────────────────────────────────────────────────
def _rapp_home():
return os.environ.get("RAPP_HOME") or os.path.join(os.path.expanduser("~"), ".rapp")
def _data_dir():
return os.environ.get("PROJECT_TRACKER_DIR") or os.path.join(_rapp_home(), "project-tracker")
def _db_path():
return os.path.join(_data_dir(), "projectTrackerData.json")
def _now_iso():
return time.strftime("%Y-%m-%dT%H:%M:%S.000Z", time.gmtime())
def _new_id():
# Mirror the web tool's Date.now().toString() (epoch millis as a string).
return str(int(time.time() * 1000))
def _empty_appdata():
return {
"projects": [],
"agents": {"builtin": [dict(a) for a in DEFAULT_BUILTIN_AGENTS], "custom": []},
"timeline": [],
}
def _load():
path = _db_path()
if not os.path.exists(path):
return _empty_appdata()
try:
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
except (json.JSONDecodeError, OSError):
return _empty_appdata()
# Normalize shape defensively.
if not isinstance(data, dict):
return _empty_appdata()
data.setdefault("projects", [])
ag = data.setdefault("agents", {"builtin": [], "custom": []})
if not isinstance(ag, dict):
ag = {"builtin": [], "custom": []}
data["agents"] = ag
ag.setdefault("builtin", [])
ag.setdefault("custom", [])
data.setdefault("timeline", [])
# Ensure the builtin defaults are present (merge by name, like the tool).
have = {a.get("name") for a in ag["builtin"] if isinstance(a, dict)}
for a in DEFAULT_BUILTIN_AGENTS:
if a["name"] not in have:
ag["builtin"].append(dict(a))
return data
def _save(data):
d = _data_dir()
os.makedirs(d, exist_ok=True)
tmp = _db_path() + ".tmp"
with open(tmp, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
os.replace(tmp, _db_path())
def _timeline(data, title, description):
data["timeline"].append({"date": _now_iso(), "title": title, "description": description})
# ── Validation / coercion ─────────────────────────────────────────────────────
def _coerce(value, valid, default):
v = (value or "").strip().lower()
return v if v in valid else default
def _all_agent_names(data):
names = set()
for a in data["agents"]["builtin"] + data["agents"]["custom"]:
if isinstance(a, dict) and a.get("name"):
names.add(a["name"])
return names
def _register_missing_agents(data, agent_names):
"""Any project agent name not already known is registered as a custom
'required' agent so the web tool renders it in the Agent Library."""
known = _all_agent_names(data)
for name in agent_names:
if name and name not in known:
data["agents"]["custom"].append({
"name": name,
"description": "Referenced by a tracked project",
"category": "other",
"status": "required",
})
known.add(name)
def _find_project(data, project_id=None, customer_name=None):
if project_id:
for p in data["projects"]:
if p.get("id") == project_id:
return p
if customer_name:
cn = customer_name.strip().lower()
for p in data["projects"]:
if (p.get("customerName") or "").strip().lower() == cn:
return p
return None
def _project_summary(p):
return {
"id": p.get("id"),
"customerName": p.get("customerName"),
"status": p.get("status"),
"type": p.get("type"),
"mvpUseCase": p.get("mvpUseCase"),
"agents": p.get("agents", []),
"updatedDate": p.get("updatedDate"),
}
# ── Merge (mirrors the web tool's mergeData) ──────────────────────────────────
def _merge(existing, incoming):
import copy
merged = copy.deepcopy(existing)
by_id = {p.get("id"): i for i, p in enumerate(merged["projects"])}
for np in (incoming.get("projects") or []):
if not isinstance(np, dict):
continue
pid = np.get("id")
if pid in by_id:
merged["projects"][by_id[pid]] = {**merged["projects"][by_id[pid]], **np,
"updatedDate": _now_iso()}
else:
merged["projects"].append({**np,
"id": pid or _new_id(),
"createdDate": np.get("createdDate") or _now_iso(),
"updatedDate": _now_iso()})
by_id[np.get("id") or merged["projects"][-1]["id"]] = len(merged["projects"]) - 1
inc_ag = incoming.get("agents") or {}
have_builtin = {a.get("name") for a in merged["agents"]["builtin"]}
for a in (inc_ag.get("builtin") or []):
if isinstance(a, dict) and a.get("name") not in have_builtin:
merged["agents"]["builtin"].append(a)
have_builtin.add(a.get("name"))
custom_idx = {a.get("name"): i for i, a in enumerate(merged["agents"]["custom"])}
for a in (inc_ag.get("custom") or []):
if not isinstance(a, dict):
continue
if a.get("name") in custom_idx:
i = custom_idx[a["name"]]
merged["agents"]["custom"][i] = {**merged["agents"]["custom"][i], **a}
else:
merged["agents"]["custom"].append(a)
custom_idx[a.get("name")] = len(merged["agents"]["custom"]) - 1
for ev in (incoming.get("timeline") or []):
if not isinstance(ev, dict):
continue
dup = any(e.get("date") == ev.get("date") and e.get("title") == ev.get("title")
for e in merged["timeline"])
if not dup:
merged["timeline"].append(ev)
return merged
# ── The cartridge ─────────────────────────────────────────────────────────────
class ProjectTrackerAgent(BasicAgent):
def __init__(self):
self.name = "ProjectTracker"
self.metadata = {
"name": self.name,
"description": (
"Manage a local-first portfolio of AI-agent projects and their MVP "
"statements, in the aibast project-tracker web tool's native JSON so "
"data round-trips via its import/export. Pick an action:\n"
" • add_project — create a project (customer_name required)\n"
" • update_project — patch fields on a project (by project_id or customer_name)\n"
" • set_mvp — set mvp_use_case / mvp_description / mvp_timeline\n"
" • attach_agents — set a project's agents to a list of agent names\n"
" • add_agent — register a custom agent (name+description)\n"
" • list_projects — summaries of all projects\n"
" • get_project — full record of one project\n"
" • stats — portfolio metrics\n"
" • export_tracker — write a JSON file ready to merge-import into the web tool\n"
" • import_tracker — merge a web-tool export JSON (path) into local data\n"
"Data lives on this device only; it never enters a repo or egg."
),
"parameters": {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["add_project", "update_project", "set_mvp", "attach_agents",
"add_agent", "list_projects", "get_project", "stats",
"export_tracker", "import_tracker"],
"description": "What to do.",
},
"project_id": {"type": "string", "description": "Target project id (update/get/set_mvp/attach_agents)."},
"customer_name": {"type": "string", "description": "Customer name. Required for add_project; usable as a lookup key elsewhere."},
"status": {"type": "string", "enum": list(VALID_STATUS), "description": "Project status. Default 'planning'."},
"type": {"type": "string", "enum": list(VALID_TYPE), "description": "Project type. Default 'other'."},
"description": {"type": "string", "description": "Project description."},
"stakeholders": {"type": "string", "description": "Key stakeholders (free text)."},
"competing_solution": {"type": "string", "description": "Competing solution (e.g. Google/AWS)."},
"contract_details": {"type": "string", "description": "Contract/commercial details."},
"notes": {"type": "string", "description": "Free-form notes."},
"mvp_use_case": {"type": "string", "description": "One-line MVP use case."},
"mvp_description": {"type": "string", "description": "The MVP statement (paragraphs)."},
"mvp_timeline": {"type": "string", "description": "MVP timeline text."},
"agents": {"type": "array", "items": {"type": "string"}, "description": "Agent names to attach to the project (attach_agents / add_project)."},
"agent_name": {"type": "string", "description": "Custom agent name (add_agent)."},
"agent_description": {"type": "string", "description": "Custom agent description (add_agent)."},
"agent_category": {"type": "string", "enum": list(VALID_AGENT_CATEGORY), "description": "Custom agent category (add_agent). Default 'other'."},
"agent_status": {"type": "string", "enum": list(VALID_AGENT_STATUS), "description": "Custom agent status (add_agent). Default 'new'."},
"path": {"type": "string", "description": "File path for export_tracker (output) / import_tracker (input)."},
},
"required": ["action"],
},
}
super().__init__(name=self.name, metadata=self.metadata)
def perform(self, **kwargs):
action = (kwargs.get("action") or "").strip()
try:
handler = {
"add_project": self._add_project,
"update_project": self._update_project,
"set_mvp": self._set_mvp,
"attach_agents": self._attach_agents,
"add_agent": self._add_agent,
"list_projects": self._list_projects,
"get_project": self._get_project,
"stats": self._stats,
"export_tracker": self._export_tracker,
"import_tracker": self._import_tracker,
}.get(action)
if handler is None:
return json.dumps({"status": "error",
"summary": f"unknown action {action!r}. See the tool description for valid actions."})
return json.dumps(handler(**kwargs))
except Exception as e: # never crash the brainstem loop
return json.dumps({"status": "error", "summary": f"{type(e).__name__}: {e}"})
# ── add_project ──────────────────────────────────────────────────────────
def _add_project(self, **k):
customer = (k.get("customer_name") or "").strip()
if not customer:
return {"status": "error", "summary": "customer_name is required for add_project"}
data = _load()
if _find_project(data, customer_name=customer):
return {"status": "error",
"summary": f"a project for '{customer}' already exists; use update_project / set_mvp instead"}
agents = [a for a in (k.get("agents") or []) if isinstance(a, str) and a.strip()]
_register_missing_agents(data, agents)
now = _now_iso()
project = {
"id": _new_id(),
"customerName": customer,
"status": _coerce(k.get("status"), VALID_STATUS, "planning"),
"type": _coerce(k.get("type"), VALID_TYPE, "other"),
"description": k.get("description") or "",
"stakeholders": k.get("stakeholders") or "",
"competingSolution": k.get("competing_solution") or "",
"contractDetails": k.get("contract_details") or "",
"agents": agents,
"notes": k.get("notes") or "",
"mvpUseCase": k.get("mvp_use_case") or "",
"mvpDescription": k.get("mvp_description") or "",
"mvpTimeline": k.get("mvp_timeline") or "",
"createdDate": now,
"updatedDate": now,
}
data["projects"].append(project)
_timeline(data, f"Created new project for {customer}",
f"{project['type']} implementation with {len(agents)} agents")
_save(data)
return {"status": "ok", "action": "add_project", "id": project["id"],
"summary": f"Added project '{customer}' (id={project['id']}, status={project['status']}, {len(agents)} agents).",
"project": _project_summary(project), "db": _db_path()}
# ── update_project ───────────────────────────────────────────────────────
def _update_project(self, **k):
data = _load()
p = _find_project(data, k.get("project_id"), k.get("customer_name"))
if not p:
return {"status": "error", "summary": "project not found (give project_id or an existing customer_name)"}
field_map = {
"status": ("status", lambda v: _coerce(v, VALID_STATUS, p.get("status", "planning"))),
"type": ("type", lambda v: _coerce(v, VALID_TYPE, p.get("type", "other"))),
"description": ("description", lambda v: v),
"stakeholders": ("stakeholders", lambda v: v),
"competing_solution": ("competingSolution", lambda v: v),
"contract_details": ("contractDetails", lambda v: v),
"notes": ("notes", lambda v: v),
"mvp_use_case": ("mvpUseCase", lambda v: v),
"mvp_description": ("mvpDescription", lambda v: v),
"mvp_timeline": ("mvpTimeline", lambda v: v),
}
changed = []
for arg, (field, fn) in field_map.items():
if k.get(arg) is not None:
p[field] = fn(k.get(arg))
changed.append(field)
if k.get("agents") is not None:
agents = [a for a in k["agents"] if isinstance(a, str) and a.strip()]
_register_missing_agents(data, agents)
p["agents"] = agents
changed.append("agents")
p["updatedDate"] = _now_iso()
_timeline(data, f"Updated {p.get('customerName')} project",
f"Status: {p.get('status')}, Type: {p.get('type')}")
_save(data)
return {"status": "ok", "action": "update_project", "id": p["id"],
"changed": changed,
"summary": f"Updated {p.get('customerName')} ({', '.join(changed) or 'no fields'}).",
"project": _project_summary(p)}
# ── set_mvp ──────────────────────────────────────────────────────────────
def _set_mvp(self, **k):
data = _load()
p = _find_project(data, k.get("project_id"), k.get("customer_name"))
if not p:
return {"status": "error", "summary": "project not found (give project_id or an existing customer_name)"}
for arg, field in (("mvp_use_case", "mvpUseCase"),
("mvp_description", "mvpDescription"),
("mvp_timeline", "mvpTimeline")):
if k.get(arg) is not None:
p[field] = k[arg]
p["updatedDate"] = _now_iso()
_timeline(data, f"Set MVP for {p.get('customerName')}",
(p.get("mvpUseCase") or "")[:120])
_save(data)
return {"status": "ok", "action": "set_mvp", "id": p["id"],
"summary": f"MVP set for {p.get('customerName')}: {(p.get('mvpUseCase') or '')[:80]}",
"project": _project_summary(p)}
# ── attach_agents ────────────────────────────────────────────────────────
def _attach_agents(self, **k):
data = _load()
p = _find_project(data, k.get("project_id"), k.get("customer_name"))
if not p:
return {"status": "error", "summary": "project not found"}
agents = [a for a in (k.get("agents") or []) if isinstance(a, str) and a.strip()]
_register_missing_agents(data, agents)
p["agents"] = agents
p["updatedDate"] = _now_iso()
_save(data)
return {"status": "ok", "action": "attach_agents", "id": p["id"],
"summary": f"Attached {len(agents)} agents to {p.get('customerName')}.",
"agents": agents}
# ── add_agent ────────────────────────────────────────────────────────────
def _add_agent(self, **k):
name = (k.get("agent_name") or "").strip()
if not name:
return {"status": "error", "summary": "agent_name is required for add_agent"}
data = _load()
if name in _all_agent_names(data):
return {"status": "error", "summary": f"an agent named '{name}' already exists"}
agent = {
"name": name,
"description": k.get("agent_description") or "",
"category": _coerce(k.get("agent_category"), VALID_AGENT_CATEGORY, "other"),
"status": _coerce(k.get("agent_status"), VALID_AGENT_STATUS, "new"),
}
data["agents"]["custom"].append(agent)
_timeline(data, f"Added custom agent: {name}", agent["description"])
_save(data)
return {"status": "ok", "action": "add_agent",
"summary": f"Registered custom agent '{name}' ({agent['category']}/{agent['status']}).",
"agent": agent}
# ── list_projects ────────────────────────────────────────────────────────
def _list_projects(self, **k):
data = _load()
projects = [_project_summary(p) for p in data["projects"]]
return {"status": "ok", "action": "list_projects", "count": len(projects),
"summary": f"{len(projects)} project(s) tracked.",
"projects": projects, "db": _db_path()}
# ── get_project ──────────────────────────────────────────────────────────
def _get_project(self, **k):
data = _load()
p = _find_project(data, k.get("project_id"), k.get("customer_name"))
if not p:
return {"status": "error", "summary": "project not found"}
return {"status": "ok", "action": "get_project", "project": p}
# ── stats ────────────────────────────────────────────────────────────────
def _stats(self, **k):
data = _load()
ps = data["projects"]
active = [p for p in ps if p.get("status") in ("active", "poc")]
success = [p for p in ps if p.get("status") in ("production", "completed")]
total_agents = len(data["agents"]["builtin"]) + len(data["agents"]["custom"])
return {"status": "ok", "action": "stats",
"summary": (f"{len(ps)} projects · {len(active)} active/poc · "
f"{len(success)} in production/completed · {total_agents} agents."),
"totalProjects": len(ps), "activeProjects": len(active),
"successfulPocs": len(success), "totalAgents": total_agents}
# ── export_tracker ───────────────────────────────────────────────────────
def _export_tracker(self, **k):
data = _load()
path = k.get("path")
if not path:
exports = os.path.join(_data_dir(), "exports")
os.makedirs(exports, exist_ok=True)
day = time.strftime("%Y-%m-%d", time.gmtime())
path = os.path.join(exports, f"project-tracker-export-{day}.json")
path = os.path.expanduser(path)
os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
return {"status": "ok", "action": "export_tracker", "path": path,
"projects": len(data["projects"]),
"summary": (f"Exported {len(data['projects'])} project(s) to {path}. "
f"Open the web tool → Data Management → 'Merge Import Data (JSON)' → "
f"choose this file to load it in.")}
# ── import_tracker ───────────────────────────────────────────────────────
def _import_tracker(self, **k):
path = k.get("path")
if not path:
return {"status": "error", "summary": "path to a web-tool export JSON is required"}
path = os.path.expanduser(path)
if not os.path.exists(path):
return {"status": "error", "summary": f"file not found: {path}"}
try:
with open(path, "r", encoding="utf-8") as f:
incoming = json.load(f)
except (json.JSONDecodeError, OSError) as e:
return {"status": "error", "summary": f"could not read JSON: {e}"}
before = _load()
merged = _merge(before, incoming if isinstance(incoming, dict) else {})
_save(merged)
return {"status": "ok", "action": "import_tracker",
"summary": (f"Merged {len(incoming.get('projects', []) if isinstance(incoming, dict) else [])} "
f"incoming project(s); portfolio now holds {len(merged['projects'])}."),
"totalProjects": len(merged["projects"])}