
Week Report System
- 4 installs
- 8 repo stars
- Updated August 4, 2026
- wangyendt/wayne-skills
Helps with ai & agent building tasks.
About
week-report-system is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- week-report-system
- AI & Agent Building
- AI-coding skill
Week Report System by the numbers
- 4 all-time installs (skills.sh)
- Ranked #13,348 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/wangyendt/wayne-skills --skill week-report-systemAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 4 |
|---|---|
| repo stars | ★ 8 |
| Last updated | August 4, 2026 |
| Repository | wangyendt/wayne-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
Week Report System
Material-first weekly report workflow for work conversations, documents, and recent repository activity.
What This Skill Does
- Capture work materials into a Git-backed weekly knowledge base
- Support three material types:
conversation,document,repo_activity - Generate weekly reports from structured materials, with legacy conversation logs as fallback
Hard Rules
1. If the user explicitly says 纳入周报素材, 记入周报, 作为周报素材, or equivalent, you must execute material capture. 2. When generating a weekly report, you must check {year}/week{WW}/materials/ first. 3. If any readable materials/*.jsonl files exist, the main report analysis must be based on them. 4. You must not generate a report only from *.txt when structured materials are present. 5. *.txt digests are a compatibility/debug artifact, not the source of truth. 6. If Git sync fails after a capture attempt, you must preserve local status evidence. 7. If one session crosses midnight, captured materials must be split by calendar date in file naming.
Important Boundary
This skill can do best-effort automatic capture only when the host agent actually invokes it.
- A skill description is not a system-level post-turn hook
- Do not claim guaranteed logging for every conversation
- When the user cares about completeness, explicitly ingest materials via
scripts/material_ingestor.py
Step 0: First Time — Check Environment
Always run this check first:
echo "USERNAME: ${WEEK_REPORT_GIT_USERNAME:-MISSING}"
echo "TOKEN: $([ -n \"$WEEK_REPORT_GIT_PERSONAL_TOKEN\" ] && echo 'SET' || echo 'MISSING')"
echo "REPO: ${WEEK_REPORT_GIT_REPO:-MISSING}"- If all three are set, continue with the request
- If any is missing, read
references/setup_guide.mdand guide the user through setup
Determine User Intent
| User intent | Action |
|---|---|
| "写周报" / "生成周报" / "本周总结" / "上周周报" | Generate report → references/report_generation.md |
| "把这个纳入周报素材" / "记录这次讨论" | Must capture as material → references/material_ingestion.md |
| "总结这个 plan/experiment 文档并纳入周报" | Ingest as document material |
| "看某个 repo 最近几天提交并纳入周报" | Ingest as repo_activity material |
| "周报系统怎么用" / "怎么设置" / "skill介绍" | Show guide → references/user_guide.md |
| Any other work-related message | Answer normally, then best-effort capture a conversation material |
Material Capture Workflow
Full instructions: references/material_ingestion.md
Quick summary: 1. Build a structured material event with source_type, project, title, summary, evidence, tags, outcome, next_actions 2. Use scripts/material_ingestor.py or scripts/conversation_logger.py to persist the event 3. Treat materials/*.jsonl as the source of truth 4. Write *.txt digest only as a compatibility/debug view 5. Update local sync status files for observability 6. Fail silently for the user, but leave local status/queue evidence for debugging
Weekly Report Generation
Read references/report_generation.md for the full process.
Quick summary: 1. Pull latest data from Git 2. Check materials/*.jsonl under {year}/week{WW}/ first 3. If any structured materials exist, base the report on them 4. Fall back to legacy .txt logs only if structured materials are missing or unreadable 5. Group by project / workstream, extract evidence and outcomes, then render the report 6. Optionally save the report to {year}/week{WW}/report-{YYYYMMDD}-{HHmmss}.md
Reference Files
| File | When to read |
|---|---|
references/setup_guide.md | Missing env vars |
references/material_ingestion.md | Capturing conversation, document, or repo materials |
references/conversation_tracking.md | Best-effort conversation capture and local observability |
references/report_generation.md | Generating reports |
references/user_guide.md | User asks how to use the system |
Scripts
| Script | Purpose |
|---|---|
scripts/git_operations.py | Clone / pull / push the report repo |
scripts/conversation_logger.py | Capture conversation materials and maintain digest files |
scripts/material_ingestor.py | Explicitly ingest conversation, document, or repo_activity materials |
Important Notes
- Best-effort auto capture is useful, but not a guarantee of complete logging
- Prefer explicit material ingestion for important plans, experiments, or repo analysis
- Skip recording only for high-confidence sensitive content such as passwords, private keys, PATs, or explicit "不要记录"
- Recording failures must not interrupt the user response, but they should leave status clues locally
- Structured material events live in
materials/*.jsonland are the primary source for report generation - One session can append to one digest file per calendar date for compatibility/debugging
{
"skill_name": "week-report-system",
"evals": [
{
"id": 1,
"name": "environment-setup",
"prompt": "我想使用周报系统,写一份上周的工作总结",
"expected_output": "应该检测到环境变量缺失,并引导用户进行GitHub仓库和Token的配置",
"files": [],
"assertions": [
{
"name": "checks-env-vars",
"description": "检查是否正确识别环境变量缺失",
"expected": "Should detect missing WEEK_REPORT_GIT_USERNAME, WEEK_REPORT_GIT_PERSONAL_TOKEN, or WEEK_REPORT_GIT_REPO and guide user to setup"
}
]
},
{
"id": 2,
"name": "conversation-material-capture",
"prompt": "帮我写一个Python函数,实现斐波那契数列,(测试对话记录功能,环境变量已配置好)",
"expected_output": "生成斐波那契函数后,应该以 conversation material 的方式在后台记录这次对话",
"files": [],
"assertions": [
{
"name": "generates-code",
"description": "生成了有效的Python代码",
"expected": "Should output a working Python function for Fibonacci sequence"
},
{
"name": "captures-conversation-material",
"description": "按 conversation material 的语义进行后台记录",
"expected": "Should capture the exchange as a conversation material with summary/outcome fields"
}
]
},
{
"id": 3,
"name": "document-material-ingestion",
"prompt": "把这份 experiment 方案总结成周报素材,项目是 rayneo_hotword_tflm",
"expected_output": "应该识别为 document 类型的周报素材,并提炼出标题、摘要、结果与下一步",
"files": [],
"assertions": [
{
"name": "uses-document-material-shape",
"description": "使用文档素材结构",
"expected": "Should capture document material with project, title, summary, outcome, and next_actions"
}
]
},
{
"id": 4,
"name": "weekly-report-generation",
"prompt": "总结2024年第51周的工作",
"expected_output": "优先从结构化 materials 读取该周素材,必要时回退到 legacy 对话摘要,并生成结构化周报",
"files": [],
"assertions": [
{
"name": "generates-structured-report",
"description": "生成结构化的周报",
"expected": "Should generate a report with: overview, project sections, highlights, and summary"
},
{
"name": "total-part-total-structure",
"description": "遵循总分总结构",
"expected": "Report should follow Total-Part-Total structure (overview at beginning, detailed sections in middle, summary at end)"
},
{
"name": "prioritizes-materials",
"description": "优先使用结构化素材",
"expected": "Should prioritize structured materials and use legacy conversation digests only as fallback"
}
]
}
]
}
Conversation Tracking
This document describes the best-effort conversation capture flow used by the Week Report System.
Core Principle
Conversation tracking is best-effort, not guaranteed.
- It works only when the host agent invokes this skill or its scripts
- It should never block the user response
- Important conversations should still be explicitly ingested as materials
Output Files
For a given ISO week:
{year}/week{WW}/
├── materials/{YYYYMMDD}-{guid}.jsonl
└── {YYYYMMDD}-{guid}.txtmaterials/*.jsonlstores structured events*.txtstores a readable digest for quick browsing and legacy compatibility
Local Observability Files
Outside the Git repo, the recorder keeps lightweight status files in ~/.week-report-repo/:
last_sync_status.json: last success or failurelocal_queue.jsonl: failed capture attempts retained for debugging or replay
Best-Effort Recording Process
1. Reuse or create a session GUID from /tmp/week_report_session.txt 2. Reuse or create the current calendar date from /tmp/week_report_session_date.txt 3. Compress the user message when needed 4. Summarize the assistant response into a short outcome 5. Build a conversation material event 6. Append the event to both:
materials/{YYYYMMDD}-{guid}.jsonl{YYYYMMDD}-{guid}.txt
7. Try pull -> append -> commit -> push 8. If sync fails, update local status files and swallow the exception
Session and Date Behavior
- The session GUID may stay the same within the active session window
- File naming is still split by calendar date
- If the same session crosses midnight, the next event goes to a new daily file such as:
20260403-02a24577.txt20260404-02a24577.txt
Conversation Event Shape
{
"timestamp": "2026-04-03T20:40:00+08:00",
"source_type": "conversation",
"project": "optional project name",
"title": "Work conversation",
"summary": "Compressed user request",
"evidence": [],
"tags": ["conversation"],
"outcome": "Short assistant outcome summary",
"next_actions": [],
"content": "Optional compressed context",
"metadata": {
"session_guid": "02a24577"
}
}Digest Format
# Material Log: 02a24577
# Date: 2026-04-03 20:40:00
# Week: 2026-W14
## [20:40:00] conversation | rayneo_hotword_tflm
Title: Work conversation
Summary: 定位在线离线 TFLite 输出不一致
Outcome: 已确认问题收敛到 Xtensa FC per-channel 路径实现差异
Tags: conversation, debug, tflitePrivacy Filter
Skip recording only for strong-sensitive phrases:
passwordprivate keyapi keypersonal access tokencredential不要记录off record
Generic engineering words like token should not automatically suppress logging.
Material Ingestion
This document defines how to capture weekly report materials in a structured way.
Why Materials Instead of Only Chat Logs
Weekly reports should be built from reusable work materials, not just raw conversation snippets.
Hard Rules
1. If the user explicitly asks to include something in the weekly report materials, capture is mandatory. 2. The canonical record must be a structured JSONL material event. 3. *.txt is only a readable digest and must not replace JSONL as the canonical source. 4. Preserve date-based file splitting even when the same session GUID spans multiple calendar days.
Supported material types:
conversation: normal work discussion with the AIdocument: plan, experiment note, meeting summary, design note, review noterepo_activity: recent commits, PRs, issues, or reviews from a repository
Recommended Trigger Phrases
Use this workflow when the user says things like:
- "把这次讨论纳入周报素材"
- "把这个 plan 记到本周周报"
- "把这个 experiment 总结成周报素材"
- "看这个 repo 最近 3 天提交,纳入本周周报"
- "把这份文档按项目整理进周报素材"
Material Schema
Each captured event should contain the following fields whenever possible:
{
"timestamp": "2026-04-03T20:35:00+08:00",
"source_type": "document",
"project": "rayneo_hotword_tflm",
"title": "Xtensa FC per-channel 对齐排查",
"summary": "确认问题已收敛到 FC per-channel 路径识别和实现差异。",
"evidence": [
"commit:abc1234",
"doc:plans/xtensa-debug-plan.md"
],
"tags": ["debug", "tflite", "xtensa"],
"outcome": "完成问题收敛和下一步排查方向定义。",
"next_actions": [
"确认运行时是否进入 pointwise conv 路径"
],
"content": "Optional longer compressed source content",
"metadata": {
"repo": "wangyendt/week-reports",
"days": 3
}
}Storage Layout
Within the week directory:
{year}/week{WW}/
├── materials/
│ └── {YYYYMMDD}-{guid}.jsonl
├── {YYYYMMDD}-{guid}.txt
└── report-{YYYYMMDD}-{HHmmss}.mdmaterials/*.jsonl: source-of-truth material events*.txt: human-readable digest, useful for quick review and backward compatibilityreport-*.md: generated weekly reports- The same session GUID can appear on different dates, but each calendar day gets its own file prefix
Capture Rules
Conversation
Capture:
- compressed user intent
- assistant outcome summary
- optional inferred project and tags
Do not capture:
- full code blocks
- long stack traces unless they are the main evidence
Document
Capture:
- document type and topic
- project/workstream
- distilled summary
- concrete decisions, outcomes, next steps
Recommended evidence:
doc:path/to/file.mdnote:meeting-20260403
Repo Activity
Capture:
- repository name
- date range or commit range
- main workstreams
- concrete evidence such as commit SHAs or PR numbers
Recommended evidence:
commit:abc1234pr:123issue:456
Privacy Rules
Skip capture only when the content contains strong indicators such as:
passwordprivate keyapi keypersonal access tokencredential不要记录off record
Do not skip merely because generic words like token or private appear in a normal engineering context.
CLI Examples
Ingest a conversation
python week-report-system/scripts/material_ingestor.py conversation \
--project rayneo_hotword_tflm \
--user-message "定位在线离线 TFLite 输出不一致" \
--assistant-response "已确认问题收敛到 Xtensa FC per-channel 路径实现差异" \
--tag debug \
--tag tfliteIngest a document
python week-report-system/scripts/material_ingestor.py document \
--project rayneo_hotword_tflm \
--title "实验计划:Xtensa FC 对齐" \
--summary "整理了 FC 对齐实验路径、验证点和后续步骤" \
--evidence doc:plans/xtensa_fc_plan.md \
--tag experiment \
--tag planIngest repo activity
python week-report-system/scripts/material_ingestor.py repo-activity \
--project rayneo_hotword_tflm \
--title "最近 3 天提交总结" \
--summary "提炼了模型对齐与 Xtensa 调试相关提交" \
--evidence commit:abc1234 \
--evidence commit:def5678 \
--metadata repo=rayneo_hotword_tflm \
--metadata days=3Weekly Report Generation
This document describes how to generate weekly reports from structured materials.
Source Priority
When generating a report for {year}/week{WW}, use sources in this order:
1. materials/*.jsonl 2. legacy *.txt conversation digests 3. ad-hoc evidence the user asks to include during the current request
The structured materials are the source of truth whenever they exist.
Hard Rules
1. You must inspect materials/*.jsonl before reading legacy *.txt. 2. If any readable structured materials exist, they must drive the main analysis. 3. Legacy *.txt is fallback-only and must not be the sole basis of the report when materials are present. 4. If both sources exist and disagree, prefer the structured material and use *.txt only as supporting context.
Report Structure
Keep the report concise, work-focused, and evidence-backed.
# 周工作汇报 - {YYYY}年第{WW}周
> 报告周期: {start_date} 至 {end_date}
> 生成时间: {generation_time}
## 📋 本周工作概览
[2-3 sentences: main projects, outcomes, overall status]
---
## 📊 分项目工作详情
### {Project Name}
- [Concrete progress]
- [Outcome or decision]
- [Evidence-backed result]
---
## 🎯 本周亮点
1. **[Achievement]**: [Impact]
2. **[Achievement]**: [Impact]
3. **[Achievement]**: [Impact]
---
## 📝 总结与下周计划
**总结:** [1-2 sentences]
**下周计划:**
- [ ] [Plan 1]
- [ ] [Plan 2]
- [ ] [Plan 3]Writing Rules
- Prefer project outcomes over raw chat history
- Use evidence when available: commits, PRs, docs, experiments, milestones
- Do not include code-line statistics
- Keep project sections factual and short
- Deduplicate repeated conversations that refer to the same outcome
Generation Process
Step 1: Parse the target week
Support:
- "总结2026年第14周"
- "本周工作总结"
- "上周周报"
Step 2: Pull latest data
Use scripts/git_operations.py to update the local report repository.
Step 3: Read structured materials
Read all JSON lines from:
{year}/week{WW}/materials/*.jsonlNormalize fields:
projectsource_typetitlesummaryevidencetagsoutcomenext_actions
Step 4: Fallback to legacy digest files
Only if materials are missing or unreadable, read:
{year}/week{WW}/*.txtUse them as supporting context, not the primary source.
Step 5: Group and deduplicate
Group items by:
project- workstream or topic
- time window
Merge duplicates when multiple entries describe the same work item with incremental updates.
Step 6: Generate the report
For each project, answer:
- What changed this week?
- What concrete outcome was achieved?
- What evidence supports that claim?
- What is the next step?
Suggested Project Analysis Prompt
Read the weekly materials and produce a concise work report.
Requirements:
- Group by project/workstream
- Prefer concrete outcomes and decisions
- Mention evidence when available
- Keep each project section within 3-6 bullets
- Avoid code metrics such as lines changedSaving Reports
Save generated reports as:
{year}/week{WW}/report-{YYYYMMDD}-{HHmmss}.mdUse a timestamped filename to avoid collisions between agents or devices.
Environment Setup Guide
This guide walks through setting up the three required environment variables for the Week Report System, and shows how to write them permanently to the user's shell profile.
Required Variables
| Variable | Description | Example |
|---|---|---|
WEEK_REPORT_GIT_USERNAME | GitHub username | zhangsan |
WEEK_REPORT_GIT_PERSONAL_TOKEN | GitHub Personal Access Token | ghp_xxxxxxxxxxxx |
WEEK_REPORT_GIT_REPO | Repository path (username/repo) | zhangsan/week-reports |
---
Step 1: Create a GitHub Repository
Tell the user:
Please create a new GitHub repository to store your work logs:
>
1. Go to https://github.com/new
2. Repository name: week-reports (or any name you like)3. Visibility: Private (recommended — work logs are personal)
4. Leave all "Initialize" options unchecked (empty repo)
5. Click "Create repository"
6. Note the full path: {your-username}/week-reports---
Step 2: Create a Personal Access Token
Tell the user:
Now create a token so the system can read and write to this repository:
>
1. Go to https://github.com/settings/tokens
2. Click "Generate new token" → "Generate new token (classic)"
3. Note field: Week Report System4. Expiration: 90 days (or "No expiration" for convenience)
5. Scopes: check `repo` (full control of private repositories)
6. Click "Generate token"
7. Copy the token immediately — GitHub won't show it again!
Format: ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx---
Step 3: Write Variables to Shell Profile
Once the user provides all three values, detect the shell and write them permanently.
Detect shell and profile file
# Detect shell profile
if [ -n "$ZSH_VERSION" ] || [ "$SHELL" = "/bin/zsh" ] || [ "$SHELL" = "/usr/bin/zsh" ]; then
PROFILE_FILE="$HOME/.zshrc"
elif [ -n "$BASH_VERSION" ] || [ "$SHELL" = "/bin/bash" ]; then
PROFILE_FILE="$HOME/.bashrc"
# macOS uses .bash_profile for login shells
[ "$(uname)" = "Darwin" ] && PROFILE_FILE="$HOME/.bash_profile"
else
PROFILE_FILE="$HOME/.profile"
fi
echo "Will write to: $PROFILE_FILE"Write the variables
Replace YOUR_USERNAME, YOUR_TOKEN, YOUR_REPO with the user's actual values:
# Remove any existing entries to avoid duplicates
sed -i'' -e '/WEEK_REPORT_GIT_USERNAME/d' \
-e '/WEEK_REPORT_GIT_PERSONAL_TOKEN/d' \
-e '/WEEK_REPORT_GIT_REPO/d' \
"$PROFILE_FILE"
# Append new values
cat >> "$PROFILE_FILE" << 'ENVEOF'
# Week Report System Configuration
export WEEK_REPORT_GIT_USERNAME="YOUR_USERNAME"
export WEEK_REPORT_GIT_PERSONAL_TOKEN="YOUR_TOKEN"
export WEEK_REPORT_GIT_REPO="YOUR_REPO"
ENVEOF
echo "Written to $PROFILE_FILE"Load immediately in current session
export WEEK_REPORT_GIT_USERNAME="YOUR_USERNAME"
export WEEK_REPORT_GIT_PERSONAL_TOKEN="YOUR_TOKEN"
export WEEK_REPORT_GIT_REPO="YOUR_REPO"Verify
echo "Username: $WEEK_REPORT_GIT_USERNAME"
echo "Token set: $([ -n \"$WEEK_REPORT_GIT_PERSONAL_TOKEN\" ] && echo 'YES' || echo 'NO')"
echo "Repo: $WEEK_REPORT_GIT_REPO"---
Windows Setup
PowerShell (User-level, persistent)
[Environment]::SetEnvironmentVariable("WEEK_REPORT_GIT_USERNAME", "YOUR_USERNAME", "User")
[Environment]::SetEnvironmentVariable("WEEK_REPORT_GIT_PERSONAL_TOKEN", "YOUR_TOKEN", "User")
[Environment]::SetEnvironmentVariable("WEEK_REPORT_GIT_REPO", "YOUR_REPO", "User")Restart your terminal after running these.
Command Prompt
setx WEEK_REPORT_GIT_USERNAME "YOUR_USERNAME"
setx WEEK_REPORT_GIT_PERSONAL_TOKEN "YOUR_TOKEN"
setx WEEK_REPORT_GIT_REPO "YOUR_REPO"---
Security Best Practices
1. Keep your token secret — never commit it to any repository 2. Use a private repository — work logs are personal 3. Rotate tokens regularly — regenerate every 90 days 4. Revoke compromised tokens — GitHub Settings → Tokens → Revoke
---
Troubleshooting
| Error | Likely Cause | Fix |
|---|---|---|
| Authentication failed | Token expired or wrong scope | Regenerate with repo scope |
| Repository not found | Wrong WEEK_REPORT_GIT_REPO format | Must be username/repo-name |
| Variables not loading | Terminal not restarted | Run source ~/.zshrc or open new terminal |
| Permission denied | Token missing repo scope | Regenerate token and check scopes |
Week Report System User Guide
Week Report System is a Git-backed weekly reporting workflow built around structured work materials.
What It Can Track
| Material type | Typical source | Recommended usage |
|---|---|---|
conversation | daily AI work chats | best-effort auto capture plus explicit ingestion when needed |
document | plan, experiment, review, meeting note | explicit ingestion |
repo_activity | recent commits, PRs, issues | explicit ingestion |
Source of Truth
materials/*.jsonlis the canonical weekly-report data*.txtis only a readable digest for compatibility and debugging- If both exist, weekly report generation should rely on
materials/*.jsonlfirst
What It Does Not Guarantee
- It does not guarantee that every work conversation is captured automatically
- A skill description is not the same as a host-level post-turn hook
- For important work items, explicitly say "纳入周报素材"
Typical Commands
Generate a report
写周报
本周工作总结
总结2026年第14周
上周周报Capture a document as material
把这个 plan 纳入本周周报素材
把这个 experiment 总结成周报素材Capture recent repo activity
看这个 repo 最近 3 天提交,纳入本周周报
总结这个仓库最近一周提交,作为周报素材Force a conversation to be captured
把这次讨论记入周报素材
记录这次工作讨论Storage Layout
week-reports/
└── 2026/
└── week14/
├── materials/
│ └── 20260403-02a24577.jsonl
├── 20260403-02a24577.txt
└── report-20260403-143500.mdPrivacy Rules
The system skips capture only for strongly sensitive content, such as:
- passwords
- private keys
- API keys / PATs
- explicit "不要记录" / "off record"
Troubleshooting
If you suspect capture did not happen, inspect local status files under ~/.week-report-repo/:
last_sync_status.jsonlocal_queue.jsonl
These help distinguish:
- skill not triggered
- capture attempted but sync failed
- last sync succeeded
#!/usr/bin/env python3
"""
Conversation and material logger for the Week Report System.
This module stores structured weekly-report materials in JSONL form and keeps a
human-readable digest for quick inspection and backward compatibility.
"""
import argparse
import json
import logging
import os
import re
import uuid
from dataclasses import asdict, dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional
try:
from git_operations import create_git_manager
except ImportError: # pragma: no cover - script/package dual use
from .git_operations import create_git_manager
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
class SessionManager:
"""Manage a coarse-grained session id shared across one active chat session."""
SESSION_FILE = "/tmp/week_report_session.txt"
SESSION_DATE_FILE = "/tmp/week_report_session_date.txt"
SESSION_TTL_SECONDS = 86400
@classmethod
def _read_recent_value(cls, path: str) -> Optional[str]:
if not os.path.exists(path):
return None
try:
file_time = os.path.getmtime(path)
if (datetime.now().timestamp() - file_time) >= cls.SESSION_TTL_SECONDS:
return None
with open(path, 'r', encoding='utf-8') as handle:
value = handle.read().strip()
return value or None
except Exception as exc: # pragma: no cover - defensive
logger.warning("Failed to read session file %s: %s", path, exc)
return None
@classmethod
def _write_value(cls, path: str, value: str) -> None:
try:
with open(path, 'w', encoding='utf-8') as handle:
handle.write(value)
except Exception as exc: # pragma: no cover - defensive
logger.warning("Failed to write session file %s: %s", path, exc)
@classmethod
def get_or_create_guid(cls) -> str:
guid = cls._read_recent_value(cls.SESSION_FILE)
if guid:
return guid
guid = uuid.uuid4().hex[:8]
cls._write_value(cls.SESSION_FILE, guid)
return guid
@classmethod
def get_or_create_start_date(cls, reference_dt: Optional[datetime] = None) -> str:
current_date = (reference_dt or datetime.now()).strftime("%Y%m%d")
start_date = cls._read_recent_value(cls.SESSION_DATE_FILE)
if start_date == current_date:
return start_date
# Split files by calendar day even if the same session crosses midnight.
cls._write_value(cls.SESSION_DATE_FILE, current_date)
return current_date
@classmethod
def reset(cls) -> None:
for path in [cls.SESSION_FILE, cls.SESSION_DATE_FILE]:
if os.path.exists(path):
os.remove(path)
class PrivacyFilter:
"""Skip only for strong-sensitive phrases to reduce false positives."""
SKIP_PHRASES = [
'password',
'private key',
'api key',
'personal access token',
'credential',
"don't record",
'off record',
'off the record',
'不要记录',
'跳过记录',
'私密',
]
@classmethod
def should_skip(cls, texts: List[str]) -> bool:
combined = "\n".join(text for text in texts if text).lower()
return any(phrase in combined for phrase in cls.SKIP_PHRASES)
class MessageCompressor:
"""Compress long user-provided content into a reusable material summary."""
MAX_LENGTH = 500
@classmethod
def compress(cls, message: str) -> str:
if len(message) <= cls.MAX_LENGTH:
return message.strip()
# Keep lines with obvious requests, outcomes, or constraints.
lines = [line.strip() for line in message.splitlines() if line.strip()]
indicators = [
'需要', '要求', '请', '帮我', '如何', '怎么', '总结', '纳入', 'repo',
'plan', 'experiment', 'debug', 'issue', 'pr', 'commit',
'need', 'please', 'help', 'how', 'why', 'summary'
]
selected: List[str] = []
for line in lines:
lowered = line.lower()
if any(indicator in lowered for indicator in indicators):
selected.append(line)
if len(" ".join(selected)) >= cls.MAX_LENGTH:
break
compressed = " ".join(selected) if selected else " ".join(lines[:5])
compressed = re.sub(r'\s+', ' ', compressed).strip()
if len(compressed) > cls.MAX_LENGTH:
compressed = compressed[:cls.MAX_LENGTH - 3] + "..."
return compressed or message[:cls.MAX_LENGTH - 3] + "..."
class ResponseSummarizer:
"""Condense an assistant response into a short outcome statement."""
MAX_CHARS = 300
@classmethod
def summarize(cls, response: str) -> str:
text = re.sub(r'```.*?```', ' ', response, flags=re.S)
text = re.sub(r'\s+', ' ', text).strip()
if not text:
return "AI provided assistance."
# Split conservatively and keep the first few meaningful fragments.
fragments = [frag.strip() for frag in re.split(r'[。!?!?]\s*', text) if frag.strip()]
summary = "。".join(fragments[:3]).strip()
if not summary:
summary = text
if len(summary) > cls.MAX_CHARS:
summary = summary[:cls.MAX_CHARS - 3] + "..."
return summary
@dataclass
class MaterialEvent:
timestamp: str
source_type: str
title: str
summary: str
project: str = ""
evidence: List[str] = field(default_factory=list)
tags: List[str] = field(default_factory=list)
outcome: str = ""
next_actions: List[str] = field(default_factory=list)
content: str = ""
metadata: Dict[str, Any] = field(default_factory=dict)
def to_json_line(self) -> str:
return json.dumps(asdict(self), ensure_ascii=False) + "\n"
@property
def dt(self) -> datetime:
return datetime.fromisoformat(self.timestamp)
class SyncStatusTracker:
"""Persist local sync status for observability."""
ROOT = Path.home() / ".week-report-repo"
STATUS_FILE = ROOT / "last_sync_status.json"
QUEUE_FILE = ROOT / "local_queue.jsonl"
@classmethod
def _ensure_root(cls) -> None:
cls.ROOT.mkdir(parents=True, exist_ok=True)
@classmethod
def update_success(cls, payload: Dict[str, Any]) -> None:
cls._ensure_root()
status = {
"status": "success",
"updated_at": datetime.now().isoformat(timespec='seconds'),
**payload,
}
cls.STATUS_FILE.write_text(json.dumps(status, ensure_ascii=False, indent=2) + "\n", encoding='utf-8')
@classmethod
def update_failure(cls, payload: Dict[str, Any]) -> None:
cls._ensure_root()
status = {
"status": "failed",
"updated_at": datetime.now().isoformat(timespec='seconds'),
**payload,
}
cls.STATUS_FILE.write_text(json.dumps(status, ensure_ascii=False, indent=2) + "\n", encoding='utf-8')
with cls.QUEUE_FILE.open('a', encoding='utf-8') as handle:
handle.write(json.dumps(status, ensure_ascii=False) + "\n")
class MaterialFormatter:
"""Derive repo-relative paths and digest content."""
@staticmethod
def get_week_path(ts: datetime) -> str:
week = ts.isocalendar()
return f"{week[0]}/week{week[1]:02d}"
@staticmethod
def material_path(ts: datetime, start_date: str, guid: str) -> str:
week_path = MaterialFormatter.get_week_path(ts)
return f"{week_path}/materials/{start_date}-{guid}.jsonl"
@staticmethod
def digest_path(ts: datetime, start_date: str, guid: str) -> str:
week_path = MaterialFormatter.get_week_path(ts)
return f"{week_path}/{start_date}-{guid}.txt"
@staticmethod
def format_digest_header(guid: str, ts: datetime) -> str:
week = ts.isocalendar()
return (
f"# Material Log: {guid}\n"
f"# Date: {ts.strftime('%Y-%m-%d %H:%M:%S')}\n"
f"# Week: {week[0]}-W{week[1]:02d}\n\n"
)
@staticmethod
def format_digest_entry(event: MaterialEvent) -> str:
ts = event.dt.strftime("%H:%M:%S")
project = event.project or "-"
tags = ", ".join(event.tags) if event.tags else "-"
evidence = ", ".join(event.evidence) if event.evidence else "-"
next_actions = "; ".join(event.next_actions) if event.next_actions else "-"
return (
f"## [{ts}] {event.source_type} | {project}\n"
f"Title: {event.title}\n"
f"Summary: {event.summary}\n"
f"Outcome: {event.outcome or '-'}\n"
f"Evidence: {evidence}\n"
f"Tags: {tags}\n"
f"Next: {next_actions}\n\n"
)
class WeekReportRecorder:
"""Capture structured materials and sync them to the Git-backed repository."""
def __init__(self, git_manager):
self.git = git_manager
def record_event(
self,
event: MaterialEvent,
*,
session_guid: Optional[str] = None,
session_start_date: Optional[str] = None,
max_retries: int = 3,
) -> bool:
if PrivacyFilter.should_skip([event.title, event.summary, event.outcome, event.content]):
logger.info("Skipping material capture for privacy")
SyncStatusTracker.update_success({
"reason": "skipped_privacy",
"source_type": event.source_type,
"title": event.title,
})
return True
guid = session_guid or SessionManager.get_or_create_guid()
start_date = session_start_date or SessionManager.get_or_create_start_date(event.dt)
material_path = MaterialFormatter.material_path(event.dt, start_date, guid)
digest_path = MaterialFormatter.digest_path(event.dt, start_date, guid)
week_path = MaterialFormatter.get_week_path(event.dt)
try:
self.git.pull()
digest_full_path = os.path.join(self.git.repo_path, digest_path)
if not os.path.exists(digest_full_path):
self.git.append_to_file(digest_path, MaterialFormatter.format_digest_header(guid, event.dt))
self.git.append_to_file(material_path, event.to_json_line())
self.git.append_to_file(digest_path, MaterialFormatter.format_digest_entry(event))
commit_message = f"Capture {event.source_type} {guid} [{week_path}]"
success = self.git.commit_and_push(commit_message, max_retries=max_retries)
payload = {
"source_type": event.source_type,
"project": event.project,
"title": event.title,
"guid": guid,
"week_path": week_path,
"material_path": material_path,
"digest_path": digest_path,
}
if success:
SyncStatusTracker.update_success(payload)
logger.info("Captured material: %s", material_path)
else:
SyncStatusTracker.update_failure({**payload, "error": "commit_or_push_failed"})
logger.warning("Failed to sync material after retries: %s", material_path)
return success
except Exception as exc: # pragma: no cover - defensive
payload = {
"source_type": event.source_type,
"project": event.project,
"title": event.title,
"guid": guid,
"week_path": week_path,
"material_path": material_path,
"digest_path": digest_path,
"error": str(exc),
"event": asdict(event),
}
SyncStatusTracker.update_failure(payload)
logger.error("Error capturing material: %s", exc)
return False
def record_conversation(
self,
user_message: str,
assistant_response: str,
*,
project: str = "",
tags: Optional[List[str]] = None,
evidence: Optional[List[str]] = None,
next_actions: Optional[List[str]] = None,
) -> bool:
event = MaterialEvent(
timestamp=datetime.now().isoformat(timespec='seconds'),
source_type="conversation",
title="Work conversation",
summary=MessageCompressor.compress(user_message),
project=project,
evidence=evidence or [],
tags=['conversation', *(tags or [])],
outcome=ResponseSummarizer.summarize(assistant_response),
next_actions=next_actions or [],
content=MessageCompressor.compress(user_message),
metadata={"session_guid": SessionManager.get_or_create_guid()},
)
return self.record_event(event)
def load_week_materials(self, year: int, week: int) -> List[Dict[str, Any]]:
"""Read structured materials for a week from the local repo clone."""
week_root = Path(self.git.repo_path) / f"{year}/week{week:02d}/materials"
if not week_root.exists():
return []
items: List[Dict[str, Any]] = []
for path in sorted(week_root.glob("*.jsonl")):
with path.open('r', encoding='utf-8') as handle:
for line in handle:
line = line.strip()
if not line:
continue
try:
items.append(json.loads(line))
except json.JSONDecodeError:
logger.warning("Skipping malformed JSONL line in %s", path)
items.sort(key=lambda item: item.get("timestamp", ""))
return items
def build_material_event(
source_type: str,
title: str,
summary: str,
*,
project: str = "",
tags: Optional[List[str]] = None,
evidence: Optional[List[str]] = None,
outcome: str = "",
next_actions: Optional[List[str]] = None,
content: str = "",
metadata: Optional[Dict[str, Any]] = None,
) -> MaterialEvent:
return MaterialEvent(
timestamp=datetime.now().isoformat(timespec='seconds'),
source_type=source_type,
title=title,
summary=MessageCompressor.compress(summary),
project=project,
evidence=evidence or [],
tags=tags or [],
outcome=ResponseSummarizer.summarize(outcome) if outcome else "",
next_actions=next_actions or [],
content=MessageCompressor.compress(content) if content else "",
metadata=metadata or {},
)
def _build_arg_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Capture a conversation material for week reports.")
parser.add_argument("--project", default="", help="Project or workstream name")
parser.add_argument("--tag", action="append", default=[], help="Tag, repeatable")
parser.add_argument("--evidence", action="append", default=[], help="Evidence item, repeatable")
parser.add_argument("--next-action", action="append", default=[], help="Next action, repeatable")
parser.add_argument("--user-message", required=True, help="Original user message")
parser.add_argument("--assistant-response", required=True, help="Assistant response")
return parser
def main() -> int:
parser = _build_arg_parser()
args = parser.parse_args()
git = create_git_manager()
if not git:
logger.error("Missing Git environment variables for week report system")
return 1
recorder = WeekReportRecorder(git)
success = recorder.record_conversation(
args.user_message,
args.assistant_response,
project=args.project,
tags=args.tag,
evidence=args.evidence,
next_actions=args.next_action,
)
return 0 if success else 1
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env python3
"""
Git Operations for Week Report System
This script handles all Git-related operations including:
- Repository cloning and pulling
- Conflict resolution with automatic retry
- Conversation file management
"""
import os
import subprocess
import time
import logging
from typing import Optional
from pathlib import Path
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
class GitOperationError(Exception):
"""Custom exception for Git operation failures."""
pass
class PushConflictError(GitOperationError):
"""Exception raised when push fails due to remote changes."""
pass
class GitManager:
"""Manages Git operations for the week report system."""
def __init__(
self,
username: str,
token: str,
repo: str,
local_path: Optional[str] = None
):
"""
Initialize Git manager.
Args:
username: GitHub username
token: GitHub personal access token
repo: Repository name in format "username/repo"
local_path: Local path for repository (default: ~/.week-report-repo)
"""
self.username = username
self.token = token
self.repo = repo
self.repo_url = f"https://{username}:{token}@github.com/{repo}.git"
self.local_path = local_path or os.path.expanduser("~/.week-report-repo")
self.repo_path = os.path.join(self.local_path, repo.split('/')[-1])
def _run_git_command(self, *args, retry_on_conflict: bool = False) -> tuple:
"""
Run a git command and return (success, output, error).
Args:
*args: Git command arguments
retry_on_conflict: Whether to handle conflicts automatically
Returns:
Tuple of (success: bool, stdout: str, stderr: str)
"""
cmd = ['git'] + list(args)
logger.debug(f"Running: git {' '.join(args)}")
try:
result = subprocess.run(
cmd,
cwd=self.repo_path,
capture_output=True,
text=True,
timeout=60
)
if result.returncode != 0:
error_msg = result.stderr.strip()
# Check for push conflict
if 'non-fast-forward' in error_msg or 'fetch first' in error_msg:
if retry_on_conflict:
raise PushConflictError("Remote has new commits")
return False, result.stdout, error_msg
logger.error(f"Git command failed: {error_msg}")
return False, result.stdout, error_msg
return True, result.stdout, result.stderr
except subprocess.TimeoutExpired:
logger.error("Git command timed out")
return False, "", "Command timed out"
except Exception as e:
logger.error(f"Git command exception: {str(e)}")
return False, "", str(e)
def is_repo_initialized(self) -> bool:
"""Check if repository is already cloned."""
return os.path.exists(os.path.join(self.repo_path, '.git'))
def clone(self) -> bool:
"""Clone the repository, cleaning up any corrupt local state first."""
import shutil
try:
os.makedirs(self.local_path, exist_ok=True)
# If directory exists but has no .git, it's corrupt — remove and re-clone
if os.path.exists(self.repo_path) and not os.path.exists(os.path.join(self.repo_path, '.git')):
logger.warning(f"Directory exists but is not a git repo, removing: {self.repo_path}")
shutil.rmtree(self.repo_path)
cmd = ['git', 'clone', self.repo_url, self.repo_path]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
if result.returncode != 0:
# Empty repo is OK — init locally and point at remote
if 'empty repository' in result.stderr or 'repository is empty' in result.stderr.lower():
logger.info("Repository is empty, initializing locally")
os.makedirs(self.repo_path, exist_ok=True)
subprocess.run(['git', 'init'], cwd=self.repo_path, capture_output=True)
subprocess.run(['git', 'remote', 'add', 'origin', self.repo_url],
cwd=self.repo_path, capture_output=True)
return True
logger.error(f"Clone failed: {result.stderr}")
return False
logger.info(f"Repository cloned to {self.repo_path}")
return True
except Exception as e:
logger.error(f"Clone exception: {str(e)}")
return False
def pull(self, force: bool = False) -> bool:
"""
Pull latest changes from remote.
Args:
force: If True, reset local changes before pulling
Returns:
True if successful, False otherwise
"""
if not self.is_repo_initialized():
return self.clone()
try:
if force:
# Reset any local changes
self._run_git_command('reset', '--hard', 'HEAD')
self._run_git_command('clean', '-fd')
# Fetch and merge
success, _, error = self._run_git_command('pull', '--rebase', 'origin', 'main')
if not success:
# Try 'master' branch if 'main' fails
success, _, _ = self._run_git_command('pull', '--rebase', 'origin', 'master')
return success
except Exception as e:
logger.error(f"Pull failed: {str(e)}")
return False
def push(self) -> bool:
"""Push changes to remote."""
# Try main branch first, then master
success, _, _ = self._run_git_command('push', 'origin', 'main')
if not success:
success, _, _ = self._run_git_command('push', 'origin', 'master')
if not success:
raise PushConflictError("Push failed - remote may have new commits")
return True
def commit_and_push(self, message: str, max_retries: int = 3) -> bool:
"""
Commit and push changes with automatic retry on conflicts.
Separates commit (done once) from push (retried on conflict).
Args:
message: Commit message
max_retries: Maximum number of retry attempts for push
Returns:
True if successful, False otherwise
"""
try:
# Stage and commit once
self._run_git_command('add', '-A')
success, _, _ = self._run_git_command('commit', '-m', message)
if not success:
# Nothing to commit
return True
except Exception as e:
logger.error(f"Commit failed: {str(e)}")
return False
# Push with retry on conflict
for attempt in range(max_retries):
try:
self.push()
logger.info(f"Successfully committed and pushed: {message}")
return True
except PushConflictError:
if attempt < max_retries - 1:
logger.warning(f"Push conflict, retrying ({attempt + 1}/{max_retries})")
time.sleep(1)
self.pull() # Rebase local commit on top of remote changes
else:
logger.error("Max retries reached, giving up")
return False
except Exception as e:
logger.error(f"Push failed: {str(e)}")
return False
return False
def append_to_file(self, file_path: str, content: str) -> bool:
"""
Append content to a file in the repository.
Args:
file_path: Relative path from repo root
content: Content to append
Returns:
True if successful
"""
full_path = os.path.join(self.repo_path, file_path)
# Create directory if needed
os.makedirs(os.path.dirname(full_path), exist_ok=True)
# Append content
with open(full_path, 'a', encoding='utf-8') as f:
f.write(content)
return True
def read_file(self, file_path: str) -> Optional[str]:
"""Read file content from repository."""
full_path = os.path.join(self.repo_path, file_path)
if not os.path.exists(full_path):
return None
with open(full_path, 'r', encoding='utf-8') as f:
return f.read()
def list_files(self, directory: str = "") -> list:
"""List all files in a directory."""
full_path = os.path.join(self.repo_path, directory)
if not os.path.exists(full_path):
return []
files = []
for root, _, filenames in os.walk(full_path):
for filename in filenames:
if filename.endswith('.txt'):
rel_path = os.path.relpath(os.path.join(root, filename), self.repo_path)
files.append(rel_path)
return files
def create_git_manager() -> Optional[GitManager]:
"""
Create a GitManager instance from environment variables.
Returns:
GitManager if all env vars are set, None otherwise
"""
username = os.environ.get('WEEK_REPORT_GIT_USERNAME')
token = os.environ.get('WEEK_REPORT_GIT_PERSONAL_TOKEN')
repo = os.environ.get('WEEK_REPORT_GIT_REPO')
if not all([username, token, repo]):
logger.warning("Missing environment variables for Git configuration")
return None
return GitManager(username, token, repo)
# CLI interface for testing
if __name__ == "__main__":
import sys
if len(sys.argv) < 2:
print("Usage: python git_operations.py <command> [args]")
print("Commands: clone, pull, status")
sys.exit(1)
command = sys.argv[1]
git = create_git_manager()
if not git:
print("Error: Please set environment variables first")
sys.exit(1)
if command == "clone":
success = git.clone()
print(f"Clone: {'Success' if success else 'Failed'}")
elif command == "pull":
success = git.pull()
print(f"Pull: {'Success' if success else 'Failed'}")
elif command == "status":
if git.is_repo_initialized():
print(f"Repository initialized at: {git.repo_path}")
files = git.list_files()
print(f"Files: {len(files)}")
else:
print("Repository not initialized")
#!/usr/bin/env python3
"""
CLI for explicitly ingesting weekly report materials.
"""
import argparse
import logging
from typing import Dict
try:
from conversation_logger import WeekReportRecorder, build_material_event
from git_operations import create_git_manager
except ImportError: # pragma: no cover - script/package dual use
from .conversation_logger import WeekReportRecorder, build_material_event
from .git_operations import create_git_manager
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
def _parse_metadata(entries) -> Dict[str, str]:
metadata: Dict[str, str] = {}
for item in entries:
if "=" not in item:
metadata[item] = ""
continue
key, value = item.split("=", 1)
metadata[key] = value
return metadata
def _build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Ingest a material into the week report repository.")
subparsers = parser.add_subparsers(dest="source_type", required=True)
conversation = subparsers.add_parser("conversation", help="Capture a conversation material")
conversation.add_argument("--project", default="")
conversation.add_argument("--tag", action="append", default=[])
conversation.add_argument("--evidence", action="append", default=[])
conversation.add_argument("--next-action", action="append", default=[])
conversation.add_argument("--user-message", required=True)
conversation.add_argument("--assistant-response", required=True)
for name in ["document", "repo-activity"]:
parser_i = subparsers.add_parser(name, help=f"Capture a {name} material")
parser_i.add_argument("--project", default="")
parser_i.add_argument("--title", required=True)
parser_i.add_argument("--summary", required=True)
parser_i.add_argument("--outcome", default="")
parser_i.add_argument("--content", default="")
parser_i.add_argument("--tag", action="append", default=[])
parser_i.add_argument("--evidence", action="append", default=[])
parser_i.add_argument("--next-action", action="append", default=[])
parser_i.add_argument("--metadata", action="append", default=[], help="key=value, repeatable")
return parser
def main() -> int:
parser = _build_parser()
args = parser.parse_args()
git = create_git_manager()
if not git:
logger.error("Missing Git environment variables for week report system")
return 1
recorder = WeekReportRecorder(git)
if args.source_type == "conversation":
success = recorder.record_conversation(
args.user_message,
args.assistant_response,
project=args.project,
tags=args.tag,
evidence=args.evidence,
next_actions=args.next_action,
)
return 0 if success else 1
source_type = "repo_activity" if args.source_type == "repo-activity" else "document"
event = build_material_event(
source_type=source_type,
title=args.title,
summary=args.summary,
project=args.project,
tags=args.tag,
evidence=args.evidence,
outcome=args.outcome,
next_actions=args.next_action,
content=args.content,
metadata=_parse_metadata(args.metadata),
)
success = recorder.record_event(event)
return 0 if success else 1
if __name__ == "__main__":
raise SystemExit(main())