跳到主要内容

会议议程与行动清单

收藏0

判断是否需要开会,整理带时长和目标的议程,把会议笔记转换为负责人、截止日期明确的行动项。

让 AI 安装

github最新版本 0.1.1更新于 2026-10-21下载 2 次2 个版本

scripts/meeting_cost_calculator.py

8.0 KB
1#!/usr/bin/env python3
2"""meeting_cost_calculator.py — Price a meeting in real dollars and gate whether it should exist.
3
4The `meetings` skill's first discipline: a meeting is a purchase. This script prices it
5(attendees x minutes x hourly rate, optionally + a 23-minute refocus overhead per attendee,
6per Gloria Mark's interruption-recovery research) and then applies the should-this-exist gate:
7
8 1. Is there a DECISION to make? (--has-decision) -> without one, it's a status update
9 2. Is there an AGENDA? (--has-agenda) -> without one, nobody can prepare
10 3. Is there a named meeting OWNER? (--has-owner) -> without one, nobody is accountable
11
12Verdicts:
13 ASYNC — no decision needed. Send a memo/thread instead. The cheapest meeting is no meeting.
14 NOT-READY — a decision exists but the agenda and/or owner is missing (named explicitly).
15 MEET — decision + agenda + owner all present. Cost printed, plus a cost-per-minute line
16 so agenda timeboxes get budgeted like money.
17
18NO LLM CALLS. Pure arithmetic + thresholds. Stdlib only. Never sends or schedules anything.
19
20Usage:
21 python meeting_cost_calculator.py --attendees 6 --minutes 60 --avg-rate 90 \
22 --include-refocus --has-decision --has-agenda --has-owner
23 python meeting_cost_calculator.py --attendees 8 --minutes 30 # no decision -> ASYNC
24 python meeting_cost_calculator.py --sample
25 python meeting_cost_calculator.py --sample --json
26"""
27
28import argparse
29import json
30import sys
31from typing import Any, Dict, List
32
33REFOCUS_MINUTES = 23 # avg time to refocus after a context switch (Gloria Mark, UC Irvine)
34
35EPILOG = """\
36exit codes:
37 0 MEET — decision + agenda + owner present; cost printed
38 1 usage error — bad or missing flags (never a verdict)
39 2 ASYNC — no decision needed; recommend a memo/thread instead of a meeting
40 3 NOT-READY — decision exists but agenda and/or owner missing (named in output)
41
42--help and --sample always exit per the verdict rules above (--sample is a MEET scenario, exit 0).
43"""
44
45
46def evaluate(attendees: int, minutes: int, avg_rate: float, include_refocus: bool,
47 has_decision: bool, has_agenda: bool, has_owner: bool) -> Dict[str, Any]:
48 direct_cost = round(attendees * (minutes / 60.0) * avg_rate, 2)
49 refocus_cost = round(attendees * (REFOCUS_MINUTES / 60.0) * avg_rate, 2) if include_refocus else 0.0
50 total_cost = round(direct_cost + refocus_cost, 2)
51 cost_per_minute = round(total_cost / minutes, 2) if minutes else 0.0
52
53 missing: List[str] = []
54 if not has_agenda:
55 missing.append("agenda")
56 if not has_owner:
57 missing.append("meeting owner")
58
59 if not has_decision:
60 verdict, exit_code = "ASYNC", 2
61 headline = (
62 "No decision to make -> this is a status update, not a meeting. "
63 f"Send it as a memo/thread and save the ${total_cost:,.2f}. "
64 "The cheapest meeting is the one you don't hold."
65 )
66 elif missing:
67 verdict, exit_code = "NOT-READY", 3
68 headline = (
69 "A decision exists, but the meeting is not ready to be called. "
70 f"Missing: {' and '.join(missing)}. "
71 "Fix that, re-run the gate, and only then send the invite."
72 )
73 else:
74 verdict, exit_code = "MEET", 0
75 headline = (
76 "Gate passed: decision + agenda + owner. This meeting has earned its slot. "
77 f"It costs ${total_cost:,.2f} — budget the agenda timeboxes like money."
78 )
79
80 return {
81 "inputs": {
82 "attendees": attendees,
83 "minutes": minutes,
84 "avg_rate_per_hour": avg_rate,
85 "include_refocus": include_refocus,
86 "has_decision": has_decision,
87 "has_agenda": has_agenda,
88 "has_owner": has_owner,
89 },
90 "direct_cost": direct_cost,
91 "refocus_cost": refocus_cost,
92 "refocus_minutes_per_attendee": REFOCUS_MINUTES if include_refocus else 0,
93 "total_cost": total_cost,
94 "cost_per_minute": cost_per_minute,
95 "missing": missing,
96 "verdict": verdict,
97 "exit_code": exit_code,
98 "headline": headline,
99 }
100
101
102def render_human(r: Dict[str, Any]) -> str:
103 i = r["inputs"]
104 out: List[str] = []
105 out.append("Meeting Cost Gate (should this meeting exist?)")
106 out.append("=" * 64)
107 out.append(f" Attendees: {i['attendees']} Length: {i['minutes']} min "
108 f"Rate: ${i['avg_rate_per_hour']:,.0f}/hr")
109 out.append(f" Direct cost: ${r['direct_cost']:>10,.2f} "
110 f"({i['attendees']} x {i['minutes']} min x ${i['avg_rate_per_hour']:,.0f}/hr)")
111 if i["include_refocus"]:
112 out.append(f" Refocus cost: ${r['refocus_cost']:>10,.2f} "
113 f"({REFOCUS_MINUTES} min refocus overhead per attendee)")
114 out.append(f" TOTAL COST: ${r['total_cost']:>10,.2f}")
115 out.append("")
116 out.append(f" Gate: decision={'yes' if i['has_decision'] else 'NO'} | "
117 f"agenda={'yes' if i['has_agenda'] else 'NO'} | "
118 f"owner={'yes' if i['has_owner'] else 'NO'}")
119 out.append("")
120 out.append(f" VERDICT: {r['verdict']}")
121 out.append(f" {r['headline']}")
122 if r["verdict"] == "MEET":
123 out.append("")
124 out.append(f" Every minute of this meeting costs ${r['cost_per_minute']:,.2f} — "
125 "price each agenda topic's timebox against that.")
126 return "\n".join(out)
127
128
129SAMPLE = dict(attendees=6, minutes=60, avg_rate=90.0, include_refocus=True,
130 has_decision=True, has_agenda=True, has_owner=True)
131
132
133def main(argv: List[str]) -> int:
134 p = argparse.ArgumentParser(
135 description="Price a meeting and gate whether it should exist (ASYNC / NOT-READY / MEET).",
136 formatter_class=argparse.RawDescriptionHelpFormatter,
137 epilog=EPILOG,
138 )
139 p.add_argument("--attendees", type=int, help="Number of attendees")
140 p.add_argument("--minutes", type=int, help="Scheduled meeting length in minutes")
141 p.add_argument("--avg-rate", type=float, default=90.0,
142 help="Average fully-loaded hourly rate per attendee in dollars (default: 90)")
143 p.add_argument("--include-refocus", action="store_true",
144 help=f"Add a {REFOCUS_MINUTES}-minute refocus overhead per attendee to the cost")
145 p.add_argument("--has-decision", action="store_true",
146 help="A specific decision will be made in this meeting")
147 p.add_argument("--has-agenda", action="store_true",
148 help="A timeboxed agenda with desired outcomes exists")
149 p.add_argument("--has-owner", action="store_true",
150 help="A named meeting owner is accountable for outcome + follow-through")
151 p.add_argument("--sample", action="store_true", help="Run the embedded sample (a MEET scenario)")
152 p.add_argument("--json", action="store_true", help="Emit machine-readable JSON instead of text")
153 args = p.parse_args(argv)
154
155 if args.sample:
156 vals = dict(SAMPLE)
157 elif args.attendees is not None and args.minutes is not None:
158 if args.attendees < 1 or args.minutes < 1:
159 print("error: --attendees and --minutes must both be >= 1", file=sys.stderr)
160 return 1
161 vals = dict(attendees=args.attendees, minutes=args.minutes, avg_rate=args.avg_rate,
162 include_refocus=args.include_refocus, has_decision=args.has_decision,
163 has_agenda=args.has_agenda, has_owner=args.has_owner)
164 else:
165 p.print_help()
166 print("\nerror: provide --attendees and --minutes, or --sample", file=sys.stderr)
167 return 1
168
169 result = evaluate(**vals)
170 if args.json:
171 print(json.dumps(result, indent=2))
172 else:
173 print(render_human(result))
174 return result["exit_code"]
175
176
177if __name__ == "__main__":
178 sys.exit(main(sys.argv[1:]))

文件内容是平台检查时读出的文本,只供查看。平台不会运行包里的任何脚本或程序。