昨日どこまでやったっけ?複数案件の「今日できること」をAIが毎朝まとめる
📝 プロンプトの説明
仕事をいくつも並行していると、作業を再開するたびに「昨日どこまで進めたっけ?」「次は何をするんだっけ?」の確認から始まりがちです。 このSkillは、案件ごとの現在地、完了したこと、確認待ち、次に進められることを記録し、**複数案件を横断した朝の一覧**を作ります。 単に更新日時が新しいファイルを並べるのではありません。実際の成果物や記録を照合し、根拠が足りないものは「未確認」のまま扱います。AIからの提案も、承認されるまでは勝手にタスクへ混ぜません。 ■ できること ・アクセスできる範囲から、朝の一覧へ登録できる案件候補をAIが探す ・プロジェクト名やフォルダパスを手入力せず、番号で登録対象を選ぶ ・1案件が複数フォルダにまたがる場合も、同じ案件として進捗を確認 ・全案件の現在地、前回以降の更新、今日できることを一覧化 ・期限が明示された作業を優先して表示 ・確認待ちや進められない作業を、着手可能な作業と分けて表示 ・未確認のAI提案を翌日も「未確認・再掲」として残す ・「たぶん終わった」のような曖昧な報告を、勝手に完了扱いしない ■ 対応AI Codex / Claude Code / Cursor / Coworkなど、Agent Skills、ファイル操作、Python実行に対応したAI 定期実行に対応しているか、ローカルファイルへアクセスできるかは環境によって異なります。定期実行できない環境でも、「今日できることをまとめて」と依頼すれば手動で同じ一覧を作れます。 ■ こんな方におすすめ ・複数の案件を並行していて、再開前の状況確認に時間がかかる方 ・AIとの作業が翌日になると、どこまで進んだか分からなくなる方 ・複数フォルダに分かれた成果物を、案件単位で追いかけたい方 ・AIの推測ではなく、実際のファイルや完了記録を根拠に進捗を見たい方
画像
プロンプト
以下のファイル一式を、記載した相対パスのまま同じAgent Skillフォルダへ保存し、Agent Skillとして登録してください。
要件:
- ファイル内容を省略、要約、独自変更しない
- `SKILL.md`だけでなく、`references/`、`scripts/`、`agents/`も保存する
- Pythonスクリプトの構文確認と、`python3 scripts/manage_handoff.py --help`の実行確認を行う
- 登録方法が環境によって異なる場合は、その環境に合う保存先を使う
- 登録後、まだ案件を勝手に追加しない
- 最後に「登録できました。次に『朝の一覧に含める案件候補を探して』と送ってください」と案内する
=== FILE: SKILL.md ===
---
name: daily-project-briefing
description: 複数プロジェクトの進捗を案件別に記録し、前回の朝レポート以降の更新、今日着手できる作業、未確認のAI提案、確認待ちを横断一覧にするAgent Skill。完了・決定・保留・次の作業を記録するとき、昨日までの続きを確認するとき、朝の一覧や定期実行を作るとき、「朝の一覧に含める案件候補を探して」と初回登録を頼まれたときに使う。初回はパス入力を求めず、アクセス可能なワークスペースから候補を提示し、番号選択後に登録する。
---
# 毎朝のプロジェクト引き継ぎ
案件ごとに現在地を保存し、登録済み案件を横断して、そのまま再開できる朝レポートを作る。記録は案件単位、レポートは全案件横断とし、レポート生成中は成果物の編集・投稿・送信などの実作業を始めない。
状態は`scripts/manage_handoff.py`だけで読み書きし、JSONや履歴を直接編集しない。コマンド、状態、保存ファイルは[references/state-schema.md](references/state-schema.md)を参照する。
```bash
python3 <skill-dir>/scripts/manage_handoff.py --help
```
レジストリは`--registry`、`AI_HANDOFF_REGISTRY`、`~/.ai-handoff/projects.json`の優先順で使う。
## 初回登録
案件登録と定期実行設定を分け、次の順で行う。
1. 「朝の一覧に含める案件候補を探して」を受け付ける。
2. 現在アクセスを許可されたワークスペースだけを確認する。ホームやPC全体へ広げない。
3. `.project-handoffs/*/.ai-handoff/project.json`、`AI_HANDOFF.md`、Gitルート、README、計画・進捗ファイルから候補を探す。専用のREADMEや計画から独立案件と判別できるサブフォルダも論理案件とする。
4. 論理案件と親ワークスペースが重複する場合は、論理案件だけを番号付き候補にし、親は重複候補として番号なしで注記する。親に固有の目的と証拠がある場合は、子の範囲を除いた固有範囲を別の番号付き候補として必ず提示し、親全体は登録しない。
5. 候補名、確認範囲、候補理由を番号付きで示し、「朝の一覧に含める番号を選んでください」と一度だけ確認する。名前、パス、設定ファイルの入力を求めない。
6. 選ばれた候補だけを登録する。見つからなければ、対象フォルダを開くか接続してもらう。
7. 登録後、定期実行は別の依頼として受け付ける。
管理場所は`AI_HANDOFF.md`と`.ai-handoff/`を置く1フォルダ、確認範囲は進捗確認に使う1つ以上のフォルダまたはファイルとする。1案件が複数場所にまたがる場合は`--scope`を繰り返す。独立案件が同じルートに混在する場合は、ルート全体ではなく案件に関係する範囲だけを登録する。
了承後に初期化する。1フォルダで完結する案件は`--scope`を省略できる。
```bash
python3 <skill-dir>/scripts/manage_handoff.py init \
--project <management-directory> --name "<project-name>" \
--scope <evidence-path> --scope <another-evidence-path>
```
範囲の全置換には`scope-set`を使う。登録後は`collect`の`file_context.status_candidates`と`recent_files`を調べ、計画書と実在成果物を照合して`current-state`を設定する。計画の未更新だけで未着手、ファイルの存在だけで完成と断定せず、食い違いを明記する。現在地の根拠には実在するファイルまたはURLを1件以上渡す。
## 進捗を記録する
完了、決定、保留・失敗・確認待ち、次の作業、明示期限の追加・変更が確認できた直後に記録する。途中の会話や思考は残さない。
### タスク
`task-add`と`task-update`を使い、状態は`todo`、`doing`、`blocked`、`waiting`、`done`に限る。
- `done`: 成果物、実行結果、公開URLなどを`--evidence`で渡す。ユーザーが確定的に完了を明言した場合だけ`--confirmed-by-user`を使う。
- `waiting`/`blocked`: `--waiting-for`に理由を必ず残す。
- 「たぶん」「と思う」「はず」「かも」など不確実な完了報告は、文中に完了指示があっても確定扱いしない。証拠を確認するか質問し、それまでは`doing`または`waiting`にする。
### 決定・記録
`record --kind`には`decision`、`completed`、`blocked`、`note`を使う。`completed`にもタスクと同じ完了証跡規則を適用する。
### AI提案
新しい提案は承認前にタスクへ入れず、`suggestion-add`で登録する。同じ未確認・見送り提案を重複登録しない。
- `pending`: 未確認として翌日も再掲する。
- `accepted`: 同内容のタスクを1件だけ追加する。
- `dismissed`: 再掲しない。ユーザーが前提変更を示した場合だけ`pending`へ戻せる。
未確認提案がある間は関連性の低い新規提案を増やさず、無反応を見送りと解釈しない。
### 案件状態
`project-status`で`active`、`paused`、`completed`を設定する。`paused`は全体表だけに表示し、`completed`は朝レポートから除外する。`paused`/`completed`はユーザーの明示または確認できる根拠がある場合だけ使い、`completed`には証拠かユーザー確認を必須とする。
## 朝レポート
### 1. 収集と確認
```bash
python3 <skill-dir>/scripts/manage_handoff.py collect --output <snapshot.json>
```
`collect`は既読位置を変えず、読めない登録先を`inaccessible_projects`へ残す。各案件の`current_state`、`completed_tasks`、未完了`tasks`、`decisions`、`file_changes`、`file_context`を確認する。
重要そうな変更ファイルは中身を読み、名前や更新時刻だけで進捗・完了を断定しない。確認内容が`current_state`と異なれば根拠付きで更新する。朝に変更できるのは`.ai-handoff/`と`AI_HANDOFF.md`だけで、成果物本体は変更しない。
### 2. 整理
次の順でMarkdownを作る。
```markdown
## 全プロジェクトの現在地
| プロジェクト | 状態 | 現在地 | 次に進められること |
|---|---|---|---|
## 前回の朝レポート以降に更新されたこと
## 今日できること
## AIからの提案
## 確認待ち・進められないこと
## 再開するには
```
次を守る。
- 全体表には`active`と`paused`を全件載せ、表示は`進行中`、`保留`とする。
- 現在地は`current_state`、完了証跡、実在成果物を照合する。根拠不足は`未確認`とし、最後のイベントやファイル更新を進捗へ言い換えない。
- 管理操作を成果として強調しない。`is_first_briefing`なら「初回レポートのため比較できる前回情報なし」とする。
- 更新欄は前回以降の実質的なイベントと、内容を確認した実ファイルの変化だけを載せる。変化なしは、記録との矛盾やアクセス失敗がある場合だけ触れる。
- 記録とファイルが矛盾する場合は勝手に決めず確認待ちへ載せる。
- 今日できることは、現在の情報とツールで着手できる`todo`/`doing`だけにする。`waiting`、`blocked`、不足情報、依頼範囲外は混ぜない。
- 並び順は、明示期限の近い順(超過を明記)、ユーザー指定優先、実行可能な継続作業、その他とする。期限、重要度、所要時間を推測しない。根拠がなければ案件別にまとめる。
- 提案には`新規`または`未確認・再掲`を付け、未確認提案をタスク化しない。
- 読めない登録先を明記し、最後に番号で送れる再開指示を1つ示す。
新規提案は現在の目的と次工程から直接導けるものだけにし、未確認提案があれば再掲を優先する。
### 3. 保存と既読更新
完成したレポートを保存できた後だけ実行する。生成・保存に失敗した場合は実行せず、次回も同じ更新を再掲する。
```bash
python3 <skill-dir>/scripts/manage_handoff.py acknowledge --snapshot <snapshot.json>
```
## 定期実行
初回登録後、利用環境に定期実行機能がある場合だけ、時刻と対象レジストリを確認して別途設定する。時刻を固定せず、設定できない場合やローカルファイルへアクセスできない場合はその事実と必要な対応を伝える。手動依頼「今日できることを出して」「昨日の続きをまとめて」でも同じ手順を使う。設定したふりをしない。
=== END FILE ===
=== FILE: references/state-schema.md ===
# 状態とコマンド
## 保存先
- `AI_HANDOFF.md`: `project.json`から再生成する人向けの現在地。
- `.ai-handoff/project.json`: 名前、状態、確認範囲`scopes`、現在地、タスク、決定、メモ、提案。直接編集しない。
- `.ai-handoff/events.jsonl`: 連番イベント。前回レポートとの差分に使う。
- `.ai-handoff/YYYY-MM-DD.md`: 日別の人向け履歴。
- `.ai-handoff/file-baseline.json`: 前回確認済みのパス、更新時刻、サイズ。変化は証拠候補であり、完了証拠ではない。
- レジストリ: 登録案件と`last_reported_sequence`。レポート保存後だけ既読を進める。
## 状態
- タスク: `todo`/`doing`は着手可能、`blocked`/`waiting`は理由必須、`done`は証拠または確定的なユーザー確認必須。
- 提案: `pending`は再掲、`accepted`はタスク化、`dismissed`は前提変更時だけ再確認。
- 案件: `active`は全欄、`paused`は全体表のみ、`completed`は非表示。
## コマンド
```text
init 初期化・登録
scope-set 確認範囲を全置換
task-add タスク追加
task-update タスク更新
record 決定・完了・停止・メモ
current-state 証拠付き現在地
suggestion-add 未確認提案を追加
suggestion-update 提案を採用・見送り
project-status 案件状態を変更
collect 朝レポート用スナップショット
acknowledge 保存済みレポートを既読化
```
引数は`python3 scripts/manage_handoff.py <command> --help`で確認する。
=== END FILE ===
=== FILE: scripts/manage_handoff.py ===
"""Maintain project handoffs and collect cross-project morning brief data."""
import argparse
import json
import os
import re
import sys
import tempfile
import uuid
from datetime import date, datetime, timezone
from pathlib import Path
TASK_STATUSES = {'todo', 'doing', 'blocked', 'waiting', 'done'}
PROJECT_STATUSES = {'active', 'paused', 'completed'}
SUGGESTION_STATUSES = {'pending', 'accepted', 'dismissed'}
RECORD_KINDS = {'decision', 'completed', 'blocked', 'note'}
EXCLUDED_DIRS = {'.git', '.ai-handoff', '.project-handoffs', '.venv', 'build', 'dist', 'node_modules', 'vendor', '__pycache__'}
STATUS_NAME_PARTS = {'readme', 'plan', 'progress', 'roadmap', 'status', 'task', 'todo', '計画', '進捗', 'タスク'}
def now_iso():
return datetime.now().astimezone().isoformat(timespec='seconds')
def registry_path(value):
raw = value or os.environ.get('AI_HANDOFF_REGISTRY') or '~/.ai-handoff/projects.json'
return Path(raw).expanduser().resolve()
def project_root(value):
return Path(value).expanduser().resolve()
def state_path(root):
return root / '.ai-handoff' / 'project.json'
def events_path(root):
return root / '.ai-handoff' / 'events.jsonl'
def baseline_path(root):
return root / '.ai-handoff' / 'file-baseline.json'
def read_json(path, default=None):
if not path.exists():
if default is not None:
return default
raise FileNotFoundError(path)
with path.open('r', encoding='utf-8') as handle:
return json.load(handle)
def write_json(path, data):
path.parent.mkdir(parents=True, exist_ok=True)
(fd, temporary) = tempfile.mkstemp(prefix=f'.{path.name}.', dir=path.parent)
try:
with os.fdopen(fd, 'w', encoding='utf-8') as handle:
json.dump(data, handle, ensure_ascii=False, indent=2)
handle.write('\n')
os.replace(temporary, path)
except Exception:
try:
os.unlink(temporary)
except FileNotFoundError:
pass
raise
def normalize_text(value):
return re.sub('\\s+', ' ', value).strip().casefold()
def scan_files(scopes):
manifest = {}
for scope in scopes:
if scope.is_file():
try:
stat = scope.stat()
except OSError:
continue
manifest[str(scope)] = {'mtime_ns': stat.st_mtime_ns, 'size': stat.st_size}
continue
for (current, dirs, files) in os.walk(scope):
dirs[:] = sorted((directory for directory in dirs if directory not in EXCLUDED_DIRS))
current_path = Path(current)
for filename in sorted(files):
path = current_path / filename
if filename == 'AI_HANDOFF.md':
continue
try:
stat = path.stat()
except OSError:
continue
if not path.is_file():
continue
manifest[str(path.resolve())] = {'mtime_ns': stat.st_mtime_ns, 'size': stat.st_size}
return manifest
def compare_manifests(before, after):
changes = []
for relative in sorted(set(before) | set(after)):
if relative not in before:
changes.append({'path': relative, 'change': 'created', **after[relative]})
elif relative not in after:
changes.append({'path': relative, 'change': 'deleted', **before[relative]})
elif before[relative] != after[relative]:
changes.append({'path': relative, 'change': 'modified', **after[relative]})
changes.sort(key=lambda item: int(item.get('mtime_ns', 0)), reverse=True)
return changes
def file_context(manifest):
recent = sorted(({'path': path, **metadata} for (path, metadata) in manifest.items()), key=lambda item: int(item['mtime_ns']), reverse=True)[:30]
status_candidates = [path for path in manifest if any((part in Path(path).name.casefold() for part in STATUS_NAME_PARTS))][:30]
return {'file_count': len(manifest), 'status_candidates': status_candidates, 'recent_files': recent}
def short_id(prefix):
return f'{prefix}-{uuid.uuid4().hex[:8]}'
def state_scopes(state, root):
return [Path(value).expanduser().resolve() for value in state.get('scopes', [str(root)])]
def validate_scopes(values, fallback):
scopes = [Path(value).expanduser().resolve() for value in values or [str(fallback)]]
missing = [str(scope) for scope in scopes if not scope.exists()]
if missing:
raise ValueError(f"Project scope does not exist: {', '.join(missing)}")
unique = list(dict.fromkeys((str(scope) for scope in scopes)))
return unique
def load_state(root):
path = state_path(root)
if not path.exists():
raise ValueError(f'Project is not initialized: {root}')
return read_json(path)
def load_registry(path):
return read_json(path, {'version': 1, 'projects': [], 'last_briefing_at': None})
def find_registry_entry(registry, root):
target = str(root)
return next((item for item in registry['projects'] if item['path'] == target), None)
def append_event(root, state, kind, text, **extra):
sequence = int(state.get('next_sequence', 1))
event = {'sequence': sequence, 'timestamp': now_iso(), 'kind': kind, 'text': text, **{key: value for (key, value) in extra.items() if value not in (None, '')}}
state['next_sequence'] = sequence + 1
state['updated_at'] = event['timestamp']
state['last_update'] = {'kind': kind, 'text': text, 'timestamp': event['timestamp']}
log_path = events_path(root)
log_path.parent.mkdir(parents=True, exist_ok=True)
with log_path.open('a', encoding='utf-8') as handle:
handle.write(json.dumps(event, ensure_ascii=False) + '\n')
day_path = root / '.ai-handoff' / f'{date.today().isoformat()}.md'
if not day_path.exists():
day_path.write_text(f'# {date.today().isoformat()} 更新履歴\n\n', encoding='utf-8')
detail = []
if event.get('deadline'):
detail.append(f"期限: {event['deadline']}")
if event.get('evidence'):
detail.append(f"証跡: {event['evidence']}")
suffix = f" ({' / '.join(detail)})" if detail else ''
with day_path.open('a', encoding='utf-8') as handle:
handle.write(f"- {event['timestamp']} [{kind}] {text}{suffix}\n")
return event
def load_events(root, after_sequence):
path = events_path(root)
if not path.exists():
return []
events = []
with path.open('r', encoding='utf-8') as handle:
for line in handle:
if not line.strip():
continue
event = json.loads(line)
if int(event['sequence']) > after_sequence:
events.append(event)
return events
def validate_completion(status, evidence, confirmed):
if status == 'done' and (not evidence) and (not confirmed):
raise ValueError('done requires --evidence or --confirmed-by-user')
def validate_waiting(status, waiting_for):
if status in {'waiting', 'blocked'} and (not waiting_for):
raise ValueError(f'{status} requires --waiting-for')
def render_handoff(root, state):
lines = [f"# {state['name']} 引き継ぎ", '', f"- 状態: {state['status']}", f"- 最終更新: {state['updated_at']}", f"- 確認範囲: {', '.join(state.get('scopes', [state['path']]))}", '', '## 現在地', '']
if state.get('current_state'):
current = state['current_state']
lines.append(f"- {current['text']}")
lines.append(f"- 確認根拠: {', '.join(current['evidence'])}")
lines.append(f"- 確認日時: {current['verified_at']}")
else:
lines.append('- 未確認')
lines.extend(['', '## タスク', ''])
if state['tasks']:
for task in state['tasks']:
deadline = f" / 期限: {task['deadline']}" if task.get('deadline') else ''
evidence = f" / 証跡: {task['evidence']}" if task.get('evidence') else ''
waiting = f" / 待ち: {task['waiting_for']}" if task.get('waiting_for') else ''
lines.append(f"- [{task['status']}] {task['id']}: {task['text']}{deadline}{waiting}{evidence}")
else:
lines.append('- なし')
lines.extend(['', '## 決定事項', ''])
if state['decisions']:
lines.extend((f"- {item['text']}" for item in state['decisions']))
else:
lines.append('- なし')
lines.extend(['', '## AIからの提案', ''])
pending = [item for item in state['suggestions'] if item['status'] == 'pending']
if pending:
lines.extend((f"- [未確認] {item['id']}: {item['text']}" for item in pending))
else:
lines.append('- なし')
lines.extend(['', '## メモ', ''])
if state['notes']:
lines.extend((f"- {item['text']}" for item in state['notes']))
else:
lines.append('- なし')
(root / 'AI_HANDOFF.md').write_text('\n'.join(lines) + '\n', encoding='utf-8')
def save_project(root, state):
write_json(state_path(root), state)
render_handoff(root, state)
def cmd_init(args):
root = project_root(args.project)
root.mkdir(parents=True, exist_ok=True)
requested_scopes = validate_scopes(args.scope, root)
path = state_path(root)
if path.exists():
state = read_json(path)
if args.scope:
state['scopes'] = requested_scopes
append_event(root, state, 'scopes_updated', 'プロジェクトの確認範囲を更新')
save_project(root, state)
write_json(baseline_path(root), scan_files(state_scopes(state, root)))
else:
timestamp = now_iso()
state = {'version': 1, 'id': short_id('project'), 'name': args.name, 'path': str(root), 'scopes': requested_scopes, 'status': 'active', 'created_at': timestamp, 'updated_at': timestamp, 'next_sequence': 1, 'current_state': None, 'tasks': [], 'decisions': [], 'notes': [], 'suggestions': []}
append_event(root, state, 'project_registered', f'{args.name}を登録')
save_project(root, state)
write_json(baseline_path(root), scan_files(state_scopes(state, root)))
reg_path = registry_path(args.registry)
registry = load_registry(reg_path)
entry = find_registry_entry(registry, root)
if entry is None:
registry['projects'].append({'id': state['id'], 'name': state['name'], 'path': str(root), 'status': state['status'], 'last_reported_sequence': 0, 'registered_at': now_iso()})
else:
entry.update(name=state['name'], status=state['status'])
write_json(reg_path, registry)
print(json.dumps({'project': str(root), 'registry': str(reg_path)}, ensure_ascii=False))
def cmd_task_add(args):
root = project_root(args.project)
state = load_state(root)
validate_completion(args.status, args.evidence, args.confirmed_by_user)
validate_waiting(args.status, args.waiting_for)
task = {'id': short_id('task'), 'text': args.text.strip(), 'status': args.status, 'deadline': args.deadline, 'evidence': args.evidence, 'waiting_for': args.waiting_for, 'priority': bool(args.priority), 'source': args.source, 'updated_at': now_iso()}
state['tasks'].append(task)
append_event(root, state, 'task_added', task['text'], item_id=task['id'], status=task['status'], deadline=task['deadline'], evidence=task['evidence'], waiting_for=task['waiting_for'])
save_project(root, state)
print(task['id'])
def find_item(items, item_id, label):
item = next((candidate for candidate in items if candidate['id'] == item_id), None)
if item is None:
raise ValueError(f'Unknown {label} id: {item_id}')
return item
def cmd_task_update(args):
root = project_root(args.project)
state = load_state(root)
task = find_item(state['tasks'], args.id, 'task')
new_status = args.status or task['status']
evidence = args.evidence if args.evidence is not None else task.get('evidence')
waiting_for = args.waiting_for if args.waiting_for is not None else task.get('waiting_for')
if new_status not in {'waiting', 'blocked'}:
waiting_for = None
validate_completion(new_status, evidence, args.confirmed_by_user)
validate_waiting(new_status, waiting_for)
if args.text is not None:
task['text'] = args.text.strip()
task['status'] = new_status
task['evidence'] = evidence
task['waiting_for'] = waiting_for
if args.deadline is not None:
task['deadline'] = args.deadline or None
if args.priority is not None:
task['priority'] = args.priority == 'yes'
task['updated_at'] = now_iso()
append_event(root, state, 'task_updated', task['text'], item_id=task['id'], status=task['status'], deadline=task.get('deadline'), evidence=task.get('evidence'), waiting_for=task.get('waiting_for'))
save_project(root, state)
print(task['id'])
def cmd_record(args):
root = project_root(args.project)
state = load_state(root)
if args.kind == 'completed' and (not args.evidence) and (not args.confirmed_by_user):
raise ValueError('completed requires --evidence or --confirmed-by-user')
entry = {'text': args.text.strip(), 'timestamp': now_iso()}
if args.evidence:
entry['evidence'] = args.evidence
if args.kind == 'decision':
state['decisions'].append(entry)
else:
state['notes'].append({'kind': args.kind, **entry})
append_event(root, state, args.kind, args.text.strip(), evidence=args.evidence)
save_project(root, state)
def cmd_current_state(args):
root = project_root(args.project)
state = load_state(root)
if not args.evidence:
raise ValueError('current-state requires at least one --evidence')
missing = []
for evidence in args.evidence:
if evidence.startswith(('http://', 'https://')):
continue
candidate = Path(evidence).expanduser()
candidates = [candidate] if candidate.is_absolute() else [root / candidate, *(scope / candidate for scope in state_scopes(state, root) if scope.is_dir())]
matches = list(dict.fromkeys((str(path.resolve()) for path in candidates if path.exists())))
if not matches:
missing.append(evidence)
elif len(matches) > 1:
raise ValueError(f'current-state evidence is ambiguous: {evidence}')
if missing:
raise ValueError(f"current-state evidence does not exist: {', '.join(missing)}")
state['current_state'] = {'text': args.text.strip(), 'evidence': args.evidence, 'verified_at': now_iso()}
append_event(root, state, 'current_state', args.text.strip(), evidence=', '.join(args.evidence))
save_project(root, state)
def cmd_suggestion_add(args):
root = project_root(args.project)
state = load_state(root)
normalized = normalize_text(args.text)
existing = next((item for item in state['suggestions'] if normalize_text(item['text']) == normalized and item['status'] in {'pending', 'dismissed'}), None)
if existing is not None:
print(existing['id'])
return
item = {'id': short_id('suggestion'), 'text': args.text.strip(), 'status': 'pending', 'created_at': now_iso(), 'updated_at': now_iso()}
state['suggestions'].append(item)
append_event(root, state, 'suggestion_added', item['text'], item_id=item['id'])
save_project(root, state)
print(item['id'])
def cmd_suggestion_update(args):
root = project_root(args.project)
state = load_state(root)
item = find_item(state['suggestions'], args.id, 'suggestion')
item['status'] = args.status
item['updated_at'] = now_iso()
append_event(root, state, 'suggestion_updated', item['text'], item_id=item['id'], status=item['status'])
if args.status == 'accepted':
duplicate = next((task for task in state['tasks'] if normalize_text(task['text']) == normalize_text(item['text'])), None)
if duplicate is None:
task = {'id': short_id('task'), 'text': item['text'], 'status': 'todo', 'deadline': None, 'evidence': None, 'waiting_for': None, 'priority': False, 'source': 'accepted-suggestion', 'updated_at': now_iso()}
state['tasks'].append(task)
append_event(root, state, 'task_added', task['text'], item_id=task['id'], status='todo')
save_project(root, state)
def cmd_project_status(args):
root = project_root(args.project)
state = load_state(root)
if args.status == 'completed' and (not args.evidence) and (not args.confirmed_by_user):
raise ValueError('completed project status requires --evidence or --confirmed-by-user')
state['status'] = args.status
append_event(root, state, 'project_status', f'プロジェクト状態を{args.status}へ変更', status=args.status, evidence=args.evidence)
save_project(root, state)
reg_path = registry_path(args.registry)
registry = load_registry(reg_path)
entry = find_registry_entry(registry, root)
if entry is None:
raise ValueError(f'Project is not registered: {root}')
entry['status'] = args.status
write_json(reg_path, registry)
def cmd_scope_set(args):
root = project_root(args.project)
state = load_state(root)
state['scopes'] = validate_scopes(args.scope, root)
append_event(root, state, 'scopes_updated', 'プロジェクトの確認範囲を更新')
save_project(root, state)
write_json(baseline_path(root), scan_files(state_scopes(state, root)))
print(json.dumps({'project': str(root), 'scopes': state['scopes']}, ensure_ascii=False))
def cmd_collect(args):
reg_path = registry_path(args.registry)
registry = load_registry(reg_path)
snapshot = {'generated_at': now_iso(), 'registry': str(reg_path), 'projects': [], 'inaccessible_projects': [], 'checkpoint_token': []}
for entry in registry['projects']:
if entry['status'] == 'completed':
continue
root = Path(entry['path'])
try:
state = load_state(root)
events = load_events(root, int(entry.get('last_reported_sequence', 0)))
scopes = state_scopes(state, root)
current_manifest = scan_files(scopes)
previous_manifest = read_json(baseline_path(root), current_manifest)
file_changes = compare_manifests(previous_manifest, current_manifest)
except (OSError, ValueError, json.JSONDecodeError) as exc:
snapshot['inaccessible_projects'].append({'name': entry['name'], 'path': entry['path'], 'reason': str(exc)})
continue
pending_tasks = [task for task in state['tasks'] if task['status'] in {'todo', 'doing', 'blocked', 'waiting'}]
pending_suggestions = [item for item in state['suggestions'] if item['status'] == 'pending']
max_sequence = max([int(event['sequence']) for event in events], default=int(entry.get('last_reported_sequence', 0)))
snapshot['projects'].append({'id': state['id'], 'name': state['name'], 'path': str(root), 'scopes': [str(scope) for scope in scopes], 'status': state['status'], 'updated_at': state['updated_at'], 'last_update': state.get('last_update'), 'current_state': state.get('current_state'), 'tasks': pending_tasks, 'completed_tasks': [task for task in state['tasks'] if task['status'] == 'done'], 'decisions': state['decisions'], 'pending_suggestions': pending_suggestions, 'new_events': events, 'is_first_briefing': int(entry.get('last_reported_sequence', 0)) == 0, 'file_changes': file_changes, 'file_context': file_context(current_manifest)})
snapshot['checkpoint_token'].append({'path': str(root), 'sequence': max_sequence, 'file_manifest': current_manifest})
output = Path(args.output).expanduser().resolve()
write_json(output, snapshot)
print(str(output))
def cmd_acknowledge(args):
snapshot_path = Path(args.snapshot).expanduser().resolve()
snapshot = read_json(snapshot_path)
reg_path = Path(snapshot['registry']).expanduser().resolve()
registry = load_registry(reg_path)
for token in snapshot.get('checkpoint_token', []):
entry = next((item for item in registry['projects'] if item['path'] == token['path']), None)
if entry is None:
continue
entry['last_reported_sequence'] = max(int(entry.get('last_reported_sequence', 0)), int(token['sequence']))
try:
write_json(baseline_path(Path(token['path'])), token.get('file_manifest', {}))
except OSError:
continue
registry['last_briefing_at'] = now_iso()
write_json(reg_path, registry)
print(registry['last_briefing_at'])
def build_parser():
parser = argparse.ArgumentParser(description=__doc__)
subparsers = parser.add_subparsers(dest='command', required=True)
def option(flags, **kwargs):
return (flags.split(), kwargs)
def command(name, help_text, func, *options, project=False):
subparser = subparsers.add_parser(name, help=help_text)
if project:
subparser.add_argument('--project', required=True)
for (flags, kwargs) in options:
subparser.add_argument(*flags, **kwargs)
subparser.set_defaults(func=func)
required = {'required': True}
append_required = {'action': 'append', 'required': True}
flag = {'action': 'store_true'}
command('init', 'Initialize and register a project', cmd_init, option('--name', **required), option('--registry'), option('--scope', action='append'), project=True)
command('task-add', 'Add a project task', cmd_task_add, option('--text', **required), option('--status', choices=sorted(TASK_STATUSES), default='todo'), option('--deadline'), option('--evidence'), option('--waiting-for'), option('--confirmed-by-user', **flag), option('--priority', **flag), option('--source', choices=['user', 'agent'], default='user'), project=True)
command('task-update', 'Update a task', cmd_task_update, option('--id', **required), option('--text'), option('--status', choices=sorted(TASK_STATUSES)), option('--deadline'), option('--evidence'), option('--waiting-for'), option('--confirmed-by-user', **flag), option('--priority', choices=['yes', 'no']), project=True)
command('record', 'Record a project event', cmd_record, option('--kind', choices=sorted(RECORD_KINDS), **required), option('--text', **required), option('--evidence'), option('--confirmed-by-user', **flag), project=True)
command('current-state', 'Set an evidence-backed current state', cmd_current_state, option('--text', **required), option('--evidence', **append_required), project=True)
command('suggestion-add', 'Add an AI suggestion', cmd_suggestion_add, option('--text', **required), project=True)
command('suggestion-update', 'Update a suggestion', cmd_suggestion_update, option('--id', **required), option('--status', choices=sorted(SUGGESTION_STATUSES), **required), project=True)
command('project-status', 'Change project status', cmd_project_status, option('--status', choices=sorted(PROJECT_STATUSES), **required), option('--evidence'), option('--confirmed-by-user', **flag), option('--registry'), project=True)
command('scope-set', 'Replace project evidence scopes', cmd_scope_set, option('--scope', **append_required), project=True)
command('collect', 'Collect morning briefing data', cmd_collect, option('--registry'), option('--output', **required))
command('acknowledge', 'Advance report checkpoints', cmd_acknowledge, option('--snapshot', **required))
return parser
def main():
parser = build_parser()
args = parser.parse_args()
try:
args.func(args)
except (OSError, ValueError, json.JSONDecodeError) as exc:
print(f'error: {exc}', file=sys.stderr)
return 1
return 0
if __name__ == '__main__':
raise SystemExit(main())
=== END FILE ===
=== FILE: agents/openai.yaml ===
interface:
display_name: "毎朝のプロジェクト引き継ぎ"
short_description: "全プロジェクトの進捗から、今日着手できる作業を朝に整理します"
default_prompt: "登録済みプロジェクトの進捗を確認し、今日できることをまとめてください。"
=== END FILE ===