Skip to content

vt-c-project-updates

Analyze PO-Weekly meeting transcripts and generate Asana project status updates. Orchestrates a 7-stage pipeline: parse transcript, segment by project, match to Asana projects, extract status updates, interactive review, write to Asana, generate summary report.

Plugin: project-updates
Category: Other
Command: /vt-c-project-updates


PO-Weekly Project Updates

Analyze a PO-Weekly meeting transcript and generate Asana project status updates automatically. High-confidence updates are applied directly; uncertain ones are presented for interactive review.

Prerequisites

Before running the pipeline, verify the environment:

  1. Check that ASANA_PAT is set:
    import os
    pat = os.environ.get("ASANA_PAT", "").strip()
    
  2. If pat is empty: display this error and stop:

    Error: ASANA_PAT environment variable is not set.
    Set it with: export ASANA_PAT='your-token-here'
    (get a token from https://app.asana.com/0/my-apps)
    

  3. Resolve the transcript file:

  4. If a transcript path was provided as argument: use it directly
  5. If no argument: search 00-Inbox/ in the current working directory for files matching *Transcript*PO-Weekly*.md, sorted by modification date descending
  6. If exactly one match: confirm with AskUserQuestion — "Found transcript: [filename]. Use this file?"
  7. If multiple matches: present the list via AskUserQuestion for selection
  8. If no matches: display error and stop:

    Error: No transcript file found.
    Provide a path: /vt-c-project-updates path/to/transcript.md
    Or place the transcript in 00-Inbox/ with "Transcript" and "PO-Weekly" in the filename.
    

  9. Resolve the config file:

  10. Look for TOOLKIT_ROOT/configs/project-config.yaml
  11. If missing: run Stage 3 (seed from Asana API) before proceeding
  12. If present: validate basic structure (must have portfolios key)

Stage 1: Parse Transcript (SPEC-061)

Purpose: Parse the markdown transcript into structured speaker segments.

Input: Raw markdown transcript file (Granola/Teams format with **[MM:SS] Speaker Name:** pattern) Output: List of {timestamp, speaker, text} segments

Script: TOOLKIT_ROOT/scripts/parse_transcript.py

python3 TOOLKIT_ROOT/scripts/parse_transcript.py "<transcript_path>"

Implemented by SPEC-061

Stage 2: Segment by Project (SPEC-062)

Purpose: Group speaker segments into project-level sections by detecting moderator project announcements.

Input: Parsed speaker segments from Stage 1 Output: List of {project_name_spoken, segments[], speakers[]} project sections

Script: TOOLKIT_ROOT/scripts/segment_projects.py

python3 TOOLKIT_ROOT/scripts/segment_projects.py "<parsed_segments_json>"

Implemented by SPEC-062

Stage 3: Load Project Config (SPEC-063)

Purpose: Load or seed the persistent project-to-Asana mapping configuration.

Input: Asana API access (via ASANA_PAT) Output: Loaded project-config.yaml with all portfolios and projects

Script: TOOLKIT_ROOT/scripts/seed_config.py

python3 TOOLKIT_ROOT/scripts/seed_config.py --config "TOOLKIT_ROOT/configs/project-config.yaml"

If configs/project-config.yaml does not exist, this script pulls all portfolios and projects from Asana and creates the initial config. If it exists, the script validates and returns the existing config.

Implemented by SPEC-063

Stage 4: Match to Asana Projects (SPEC-064)

Purpose: Match spoken project names from transcript segments against the config using fuzzy matching and alias lookup.

Input: Project sections from Stage 2 + project config from Stage 3 Output: Matched sections with asana_project_gid and asana_project_name resolved. Unmatched projects flagged for review.

Script: TOOLKIT_ROOT/scripts/match_projects.py

python3 TOOLKIT_ROOT/scripts/match_projects.py "<segments_json>" "<config_path>"

When a fuzzy match is confirmed by the user during interactive review (Stage 6), the spoken name is added to aliases in the config for future runs.

Implemented by SPEC-064

Stage 5: Extract Status Updates (SPEC-065)

Purpose: For each matched project section, extract structured status information using LLM analysis.

Input: Matched project sections with transcript text Output: List of {project_gid, was_passiert, was_kommt, status_farbe, konfidenz, konfidenz_grund, quellen[]}

This stage runs scripts/extract_status.py for the rule-based work (status_farbe, konfidenz, quellen, cross_ref_candidates) and then uses Claude's LLM capabilities to enrich the result with: - was_passiert: What happened this week - was_kommt: What's coming next - status_farbe: One of planmaessig, gefaehrdet, unplanmaessig, zurueckgestellt, erledigt, verworfen - konfidenz: hoch, mittel, or niedrig - konfidenz_grund: Why this confidence level was assigned - quellen: Relevant quotes from the transcript

Implemented by SPEC-065

Stage 5b: Classify Cross-Reference Candidates (SPEC-069)

For each StatusExtraction produced by Stage 5 whose cross_ref_candidates list is non-empty, classify each candidate and write the result back to that StatusExtraction's cross_references list. Do NOT modify status_farbe, was_passiert, was_kommt, or quellen — those are read-only at this step.

Classification kinds:

  • informational — the source block mentions the target project as context only ("verwandt mit X", linked via shared infrastructure). No follow-up action needed.
  • status_relevant — the source block contains information that semantically belongs in the target project's status update ("Buchhaltungsbereich ist mit in Mango Pay drin").
  • absorption — the source project's content has been moved into the target project ("Notfallkoffer komplett ins Handbuch übernommen"). Source project may be closable.

For each candidate, produce a CrossRef object with: - target_project_name: canonical name (resolve target_hint: "previous_block" candidates using the immediately preceding StatusExtraction's project_name; if no preceding block exists, classify as informational and set reason to "kein vorheriger Block — Verweis nicht auflösbar") - target_asana_gid: copy from the candidate when present, else null - kind: one of the three literals above - quote: the candidate's window_text trimmed to ≤150 chars - reason: one-line German rationale

When at least one CrossRef has kind == "absorption", also set the StatusExtraction's closure_suggested field to a single one-line German string merging the absorption reasons (≤200 chars).

Few-shot examples:

Input candidate (Pattern 1, embedding): window_text: "buchhaltungsbereich ist oben mit in dem mango pay bereich auch mit drin" target_project_name: "MangoPay Guthabenmodell" Output CrossRef: kind: "status_relevant" reason: "Buchhaltungsbereich-Aktivität gehört thematisch zu Mango Pay; Information sollte auch dort erscheinen."

Input candidate (Pattern 3, absorption): window_text: "notfallkoffer komplett ins handbuch übernommen" target_project_name: "V004-Handbuch" Output CrossRef: kind: "absorption" reason: "Inhalt vollständig in V004-Handbuch übergegangen." Plus on parent: closure_suggested: "Inhalt vollständig in V004-Handbuch übergegangen — Schließung empfohlen."

Implemented by SPEC-069

Stage 6: Interactive Review (SPEC-066)

Purpose: Present uncertain updates for user confirmation and flag projects not mentioned in the meeting.

Input: Extracted status updates from Stage 5 + full project list from config Output: Confirmed/corrected status updates ready for Asana

Flow: 1. Auto-apply updates with konfidenz: hoch (show count) 2. Present each konfidenz: mittel or niedrig update via AskUserQuestion: - Show project name, proposed status, confidence reason, and source quotes - Options: Confirm / Edit / Skip 3. List projects from config that were not mentioned in the transcript: - "These N projects were not discussed. Keep current status?" - Allow user to update individual ones

Cross-reference visibility (SPEC-069): When presenting a StatusExtraction for review, if its cross_references list is non- empty, prepend "⚠ Querverweise gefunden: N Eintrag/Einträge — siehe cross_references" to the existing prompt (N = len(cross_references)). If closure_suggested is set, also prepend "💡 Schließungsvorschlag: {closure_suggested}". The user sees these automatically and can dig into the JSON output for details.

Implemented by SPEC-066

Stage 7: Write to Asana & Report (SPEC-067, SPEC-068)

Purpose: Write confirmed status updates to Asana via API and generate a summary report.

Input: Confirmed status updates from Stage 6 Output: Asana status updates created + summary report displayed

Asana API calls (per project):

POST /projects/{project_gid}/status_updates
{
  "text": "{was_passiert}\n\nNächste Schritte:\n{was_kommt}",
  "color": "{mapped_status_color}"
}

Status color mapping (German enum to Asana API color): - planmaessiggreen - gefaehrdetyellow - unplanmaessigred - zurueckgestelltblue - erledigtcomplete - verworfenred

Summary report displayed after all writes:

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PO-Weekly Status Update — [date]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  X updates auto-applied (high confidence)
  Y updates reviewed and confirmed
  Z projects not discussed (status unchanged)
  W updates failed (see errors below)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Archiving: Move the processed transcript to 02-Knowledge/04-Dokumente/ with date prefix.

Implemented by SPEC-067 (Asana write) and SPEC-068 (report + archiving)

Config File Format

The persistent project configuration lives at TOOLKIT_ROOT/configs/project-config.yaml:

generated_from_asana: "2026-03-13"
portfolios:
  - name: "VisiTrans"
    asana_portfolio_gid: "1234567890"
    projects:
      - asana_gid: "9876543210"
        asana_name: "V004-VisiTrans Handbuch ist erstellt und aktuell"
        sort_order: 4
        aliases:
          - "Handbuch Rolf"
          - "Handbuch"
        last_updated: "2026-03-13"

See plugins/project-updates/skills/project-updates/config/README.md for the full schema documentation.