skilldrop-cli 1.10.0 → 1.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +3 -2
  2. package/catalogue.json +9 -1
  3. package/model-routing.json +8 -0
  4. package/package.json +1 -1
  5. package/packs/converters/skills/file-to-markdown/manifest.json +1 -1
  6. package/packs/converters/skills/file-to-markdown/scripts/to_markdown.py +13 -4
  7. package/packs/educator/pack.json +25 -0
  8. package/packs/educator/skills/exam-paper-check/SKILL.md +125 -0
  9. package/packs/educator/skills/exam-paper-check/evals/eval_queries.json +30 -0
  10. package/packs/educator/skills/exam-paper-check/evals/evals.json +73 -0
  11. package/packs/educator/skills/exam-paper-check/evals/files/1/year9-science-scheme.md +16 -0
  12. package/packs/educator/skills/exam-paper-check/evals/files/1/year9-science.md +58 -0
  13. package/packs/educator/skills/exam-paper-check/evals/files/2/mock-paper1-ms.md +32 -0
  14. package/packs/educator/skills/exam-paper-check/evals/files/2/mock-paper1.md +60 -0
  15. package/packs/educator/skills/exam-paper-check/evals/files/3/hist1042-quiz-markscheme.md +37 -0
  16. package/packs/educator/skills/exam-paper-check/evals/files/3/hist1042-quiz.md +50 -0
  17. package/packs/educator/skills/exam-paper-check/evals/files/4/chem-mock-paper.md +51 -0
  18. package/packs/educator/skills/exam-paper-check/evals/files/4/chem-mock-scheme.md +32 -0
  19. package/packs/educator/skills/exam-paper-check/evals/files/5/econ-p2-paper.md +57 -0
  20. package/packs/educator/skills/exam-paper-check/evals/files/5/econ-p2-scheme.md +34 -0
  21. package/packs/educator/skills/exam-paper-check/examples/inputs/spec.json +3 -0
  22. package/packs/educator/skills/exam-paper-check/examples/inputs/year9-science-scheme.md +16 -0
  23. package/packs/educator/skills/exam-paper-check/examples/inputs/year9-science.md +58 -0
  24. package/packs/educator/skills/exam-paper-check/examples/year9-forces.md +68 -0
  25. package/packs/educator/skills/exam-paper-check/manifest.json +33 -0
  26. package/packs/educator/skills/exam-paper-check/scripts/exam_check.py +328 -0
  27. package/packs/educator/skills/exam-paper-check/templates/spec.json +11 -0
  28. package/packs/startup-legal/pack.json +10 -2
  29. package/packs/startup-legal/skills/vesting-schedule/SKILL.md +122 -0
  30. package/packs/startup-legal/skills/vesting-schedule/evals/eval_queries.json +30 -0
  31. package/packs/startup-legal/skills/vesting-schedule/evals/evals.json +70 -0
  32. package/packs/startup-legal/skills/vesting-schedule/examples/cofounder-leaving.md +66 -0
  33. package/packs/startup-legal/skills/vesting-schedule/examples/inputs/vesting.json +8 -0
  34. package/packs/startup-legal/skills/vesting-schedule/manifest.json +33 -0
  35. package/packs/startup-legal/skills/vesting-schedule/scripts/vesting.py +280 -0
  36. package/packs/startup-legal/skills/vesting-schedule/templates/vesting.json +12 -0
@@ -0,0 +1,66 @@
1
+ # Worked example: a co-founder leaving 14 months in
2
+
3
+ ## Input
4
+
5
+ > *"My co-founder Alex is leaving at the end of September. Alex has 3M shares, four-year vesting
6
+ > with a one-year cliff from 1 July 2025 when we started; the stock purchase agreement was
7
+ > signed on 15 September 2025. How much does Alex keep, and we're not sure Alex ever filed the
8
+ > 83(b) — does that matter?"*
9
+
10
+ ## Input written (in a private temporary folder)
11
+
12
+ ```json
13
+ {
14
+ "company": "Brightloop Labs, Inc.",
15
+ "jurisdiction": "US",
16
+ "grants": [
17
+ {"holder": "Alex (co-founder)", "type": "founder stock", "shares": 3000000, "grant_date": "2025-09-15", "vesting_start": "2025-07-01",
18
+ "months": 48, "cliff_months": 12, "frequency": "monthly", "leave_date": "2026-09-30"}
19
+ ]
20
+ }
21
+ ```
22
+
23
+ ## What it computed
24
+
25
+ ```text
26
+ # Brightloop Labs, Inc.: vesting
27
+
28
+ | Holder | Type | Shares | Cliff | Fully vested | Vested on date | Unvested |
29
+ |---|---|---:|---|---|---:|---:|
30
+ | Alex (co-founder) | founder stock | 3,000,000 | 2026-07-01: 750,000 | 2029-07-01 | 875,000 (2026-09-30) | 2,125,000 |
31
+
32
+ ## Alex (co-founder)
33
+ - 83(b) election: file by **2025-10-15**, 30 days from 2025-09-15.
34
+ - left 2026-09-30: 875,000 vested, 2,125,000 unvested shares the company can buy back
35
+
36
+ | Date | Vests | Total vested |
37
+ |---|---:|---:|
38
+ | 2026-07-01 | 750,000 | 750,000 |
39
+ | 2026-08-01 | 62,500 | 812,500 |
40
+ | 2027-07-01 | 62,500 | 1,500,000 |
41
+ | 2028-07-01 | 62,500 | 2,250,000 |
42
+ | 2029-07-01 | 62,500 | 3,000,000 |
43
+ | … 37 vesting dates in all (--full for every one) | | |
44
+ FAIL Alex (co-founder): the 83(b) deadline was 2025-10-15; it has passed unless it was filed
45
+ ```
46
+
47
+ ## Reply
48
+
49
+ **Alex keeps 875,000 of the 3,000,000 shares.** Vesting started on 1 July 2025, the cliff
50
+ vested 750,000 on 1 July 2026, and 62,500 more vested on 1 August and on 1 September. Leaving on
51
+ 30 September, the other 2,125,000 are unvested, and under a standard founder stock agreement
52
+ the company can buy them back at the price Alex paid. Check the repurchase clause for how
53
+ long the company has to exercise that right (often 90 days) and diarise it.
54
+
55
+ **The 83(b) matters, and its deadline was 15 October 2025**, 30 days after the stock was bought
56
+ on 15 September. If it was filed, Alex was taxed on the shares' value at purchase, which for
57
+ founder stock is usually close to nothing. If it wasn't, each tranche that vested (the 750,000
58
+ in July, and 62,500 a month since) counts as income at its value on the day it vested. Ask
59
+ Alex for the IRS acknowledgement or the copy sent with the filing.
60
+
61
+ **Assumptions**
62
+ - `[assumption]` Monthly vesting after the cliff, on the 1st. If the agreement vests
63
+ quarterly, Alex has 750,000: the next tranche would have been 1 October.
64
+
65
+ **Worth an hour with a tax adviser:** if Alex didn't file the 83(b), how the vested tranches
66
+ should be reported, before the next tax filing.
@@ -0,0 +1,8 @@
1
+ {
2
+ "company": "Brightloop Labs, Inc.",
3
+ "jurisdiction": "US",
4
+ "grants": [
5
+ {"holder": "Alex (co-founder)", "type": "founder stock", "shares": 3000000, "grant_date": "2025-09-15", "vesting_start": "2025-07-01",
6
+ "months": 48, "cliff_months": 12, "frequency": "monthly", "leave_date": "2026-09-30"}
7
+ ]
8
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "vesting-schedule",
3
+ "version": "0.1.0",
4
+ "description": "Work out vesting for founder stock, restricted stock, options and RSUs — each grant's cliff date and amount, every vesting date in whole shares, what is vested on any date, what happens when someone leaves (vested, forfeited or repurchasable, and the last day to exercise), single- and double-trigger acceleration on a sale, and the US 83(b) election deadline moved past weekends and holidays — computed by a script that keeps the totals exact. Use when a founder asks \"how much has my co-founder vested\", \"what happens to her shares if she leaves\", \"when does the cliff hit\", \"build our vesting schedule\", \"when is the 83(b) deadline\", or \"what does double-trigger acceleration give us in a sale\".",
5
+ "entrypoint": "SKILL.md",
6
+ "deps": {
7
+ "npm": [],
8
+ "pip": []
9
+ },
10
+ "env": {
11
+ "required": [],
12
+ "optional": []
13
+ },
14
+ "related": [],
15
+ "tags": [
16
+ "vesting",
17
+ "equity",
18
+ "83b",
19
+ "options",
20
+ "founder",
21
+ "startup"
22
+ ],
23
+ "permissions": {
24
+ "network": [],
25
+ "commands": [],
26
+ "files": "named-paths",
27
+ "notes": "Reads the vesting.json it is given and writes a CSV only to the path passed with --csv."
28
+ },
29
+ "model": {
30
+ "tier": "standard",
31
+ "rationale": "Turning grant documents into the script's input and explaining leavers and acceleration is bounded; the script does the date arithmetic."
32
+ }
33
+ }
@@ -0,0 +1,280 @@
1
+ #!/usr/bin/env python3
2
+ """Work out vesting schedules for founder stock, restricted stock, options and RSUs. Stdlib only.
3
+
4
+ Reads a vesting.json (each grant's holder, type, shares, grant date, vesting start, months,
5
+ cliff and frequency, and any acceleration, leaving date or change of control) and prints:
6
+ - each grant's cliff date and amount, the date it fully vests, and what is vested on a date
7
+ - a dated schedule (every vesting date, how many vest, the running total), whole shares only,
8
+ with the rounding left to the last date so the total is exact
9
+ - what happens when someone leaves: vested, unvested (forfeited or repurchasable), and the
10
+ last day to exercise options
11
+ - single- and double-trigger acceleration on a change of control
12
+ - for US restricted stock (and early-exercised options), the 83(b) election deadline: 30 days
13
+ from the grant, moved to the next business day when it falls on a weekend or federal holiday
14
+ A vesting date on the 29th–31st falls on the last day of shorter months.
15
+
16
+ Usage:
17
+ python3 vesting.py vesting.json [--as-of 2027-06-30] [--csv schedule.csv] [--full]
18
+ Exit codes: 0 computed, 1 a check failed (a schedule that doesn't sum to its grant, a passed
19
+ 83(b) deadline), 2 bad input.
20
+ """
21
+ import argparse
22
+ import calendar
23
+ import csv
24
+ import datetime as dt
25
+ import json
26
+ import math
27
+ import sys
28
+
29
+ STEP = {"monthly": 1, "quarterly": 3, "annual": 12, "annually": 12, "yearly": 12}
30
+ STOCK = {"founder stock", "restricted stock", "rsa"}
31
+
32
+
33
+ class InputError(Exception):
34
+ pass
35
+
36
+
37
+ def day(v, what, allow_none=False):
38
+ if v is None and allow_none:
39
+ return None
40
+ try:
41
+ return dt.date.fromisoformat(str(v))
42
+ except ValueError:
43
+ raise InputError(f"{what} must be a date as YYYY-MM-DD")
44
+
45
+
46
+ def add_months(d, n, anchor_day=None):
47
+ """d plus n months, on anchor_day or the month's last day when the month is shorter."""
48
+ y, m = divmod(d.year * 12 + d.month - 1 + n, 12)
49
+ want = anchor_day or d.day
50
+ return dt.date(y, m + 1, min(want, calendar.monthrange(y, m + 1)[1]))
51
+
52
+
53
+ def nth_weekday(y, m, weekday, n):
54
+ d = dt.date(y, m, 1)
55
+ d += dt.timedelta(days=(weekday - d.weekday()) % 7)
56
+ return d + dt.timedelta(weeks=n - 1)
57
+
58
+
59
+ def last_weekday(y, m, weekday):
60
+ d = dt.date(y, m, calendar.monthrange(y, m)[1])
61
+ return d - dt.timedelta(days=(d.weekday() - weekday) % 7)
62
+
63
+
64
+ def us_holidays(y):
65
+ """US federal legal holidays, plus DC Emancipation Day, as observed (IRC §7503 counts both)."""
66
+ fixed = [dt.date(y, 1, 1), dt.date(y, 6, 19), dt.date(y, 7, 4), dt.date(y, 11, 11), dt.date(y, 12, 25),
67
+ dt.date(y, 4, 16)]
68
+ out = set()
69
+ for d in fixed:
70
+ if d.weekday() == 5:
71
+ out.add(d - dt.timedelta(days=1))
72
+ elif d.weekday() == 6:
73
+ out.add(d + dt.timedelta(days=1))
74
+ else:
75
+ out.add(d)
76
+ out |= {nth_weekday(y, 1, 0, 3), nth_weekday(y, 2, 0, 3), last_weekday(y, 5, 0), nth_weekday(y, 9, 0, 1),
77
+ nth_weekday(y, 10, 0, 2), nth_weekday(y, 11, 3, 4)}
78
+ return out
79
+
80
+
81
+ def next_business_day(d):
82
+ while d.weekday() >= 5 or d in us_holidays(d.year) or d in us_holidays(d.year + 1):
83
+ d += dt.timedelta(days=1)
84
+ return d
85
+
86
+
87
+ def load(data):
88
+ if not isinstance(data, dict) or not isinstance(data.get("grants"), list) or not data["grants"]:
89
+ raise InputError("vesting.json needs `grants`: a list of grants")
90
+ grants = []
91
+ for i, g in enumerate(data["grants"], 1):
92
+ if not isinstance(g, dict) or not str(g.get("holder", "")).strip():
93
+ raise InputError(f"grant {i} needs a `holder`")
94
+ who = str(g["holder"]).strip()
95
+ shares = g.get("shares")
96
+ if isinstance(shares, bool) or not isinstance(shares, int) or shares < 1:
97
+ raise InputError(f"{who}: shares must be a whole number of at least 1")
98
+ months = g.get("months", 48)
99
+ cliff = g.get("cliff_months", 12)
100
+ for v, what in ((months, "months"), (cliff, "cliff_months")):
101
+ if isinstance(v, bool) or not isinstance(v, int) or v < 0:
102
+ raise InputError(f"{who}: {what} must be a whole number")
103
+ if months < 1 or cliff > months:
104
+ raise InputError(f"{who}: the cliff can't be longer than the vesting period")
105
+ freq = str(g.get("frequency", "monthly")).lower()
106
+ if freq not in STEP:
107
+ raise InputError(f"{who}: frequency must be monthly, quarterly or annual")
108
+ kind = str(g.get("type", "option")).lower()
109
+ acc = g.get("acceleration") or {}
110
+ if not isinstance(acc, dict):
111
+ raise InputError(f"{who}: acceleration must be an object")
112
+ for k in ("single", "double"):
113
+ v = acc.get(k)
114
+ if v is not None and (isinstance(v, bool) or not isinstance(v, (int, float)) or not 0 <= v <= 100):
115
+ raise InputError(f"{who}: acceleration.{k} must be a fraction (0.5) or a percent (50)")
116
+ grant_date = day(g.get("grant_date") or g.get("vesting_start"), f"{who}: grant_date")
117
+ grants.append({
118
+ "holder": who, "type": kind, "shares": shares, "grant_date": grant_date,
119
+ "start": day(g.get("vesting_start") or g.get("grant_date"), f"{who}: vesting_start"),
120
+ "months": months, "cliff": cliff, "step": STEP[freq], "frequency": freq,
121
+ "single": frac(acc.get("single")), "double": frac(acc.get("double")),
122
+ "window": int(acc.get("window_months", 12)),
123
+ "leave": day(g.get("leave_date"), f"{who}: leave_date", True),
124
+ "pte_days": g.get("exercise_window_days"),
125
+ "repurchase_days": g.get("repurchase_window_days"),
126
+ "early_exercise": day(g.get("early_exercise_date"), f"{who}: early_exercise_date", True),
127
+ "already_filed_83b": bool(g.get("filed_83b", False)),
128
+ })
129
+ return {"company": str(data.get("company", "The company")),
130
+ "change_of_control": day(data.get("change_of_control"), "change_of_control", True),
131
+ "us": str(data.get("jurisdiction", "US")).upper() in ("US", "USA", "UNITED STATES"),
132
+ "grants": grants}
133
+
134
+
135
+ def frac(v):
136
+ if v is None:
137
+ return 0.0
138
+ return v / 100 if v > 1 else float(v)
139
+
140
+
141
+ def schedule(g):
142
+ """[(date, vesting now, cumulative)] in whole shares. Each date's total is the grant times the
143
+ months elapsed over the months in all, rounded down, so rounding never runs ahead of the
144
+ grant and the last date carries the remainder. A cliff that falls between two vesting dates
145
+ vests on the cliff date itself."""
146
+ ks = set(range(g["step"], g["months"] + 1, g["step"])) | {g["months"]}
147
+ if g["cliff"]:
148
+ ks.add(g["cliff"])
149
+ rows, done = [], 0
150
+ for k in sorted(k for k in ks if k >= g["cliff"]):
151
+ cum = g["shares"] if k == g["months"] else math.floor(g["shares"] * k / g["months"])
152
+ rows.append((add_months(g["start"], k, g["start"].day), cum - done, cum))
153
+ done = cum
154
+ return rows
155
+
156
+
157
+ def vested_on(rows, when):
158
+ return max((c for d, _, c in rows if d <= when), default=0)
159
+
160
+
161
+ def outcome(cap, g, as_of):
162
+ rows = schedule(g)
163
+ fails, warns, notes = [], [], []
164
+ if rows[-1][2] != g["shares"] or sum(n for _, n, _ in rows) != g["shares"]:
165
+ fails.append(f"{g['holder']}: the schedule sums to {rows[-1][2]:,}, not {g['shares']:,}")
166
+ cliff_date = add_months(g["start"], g["cliff"], g["start"].day) if g["cliff"] else None
167
+ end = rows[-1][0]
168
+ stop = g["leave"]
169
+ coc = cap["change_of_control"]
170
+ at = min(as_of, stop) if stop else as_of
171
+ single = double = 0
172
+ if coc and g["single"] and coc <= at:
173
+ single = math.floor((g["shares"] - vested_on(rows, coc)) * g["single"])
174
+ notes.append(f"single-trigger acceleration on the change of control ({coc}): {single:,} more shares vest")
175
+ if coc and g["double"] and stop and coc <= stop <= add_months(coc, g["window"]):
176
+ double = math.floor((g["shares"] - vested_on(rows, stop) - single) * g["double"])
177
+ notes.append(f"double-trigger acceleration: left on {stop}, within {g['window']} months of the change of "
178
+ f"control, so {double:,} more shares vest")
179
+ elif coc and g["double"] and stop is None:
180
+ notes.append("double-trigger acceleration applies only if they are let go (or resign for good reason) "
181
+ f"within {g['window']} months of the change of control")
182
+ # double trigger only counts once its second trigger (leaving) has happened by the date asked about
183
+ vested = min(g["shares"], vested_on(rows, at) + single + (double if stop and at >= stop else 0))
184
+ leave = None
185
+ if stop:
186
+ at_leave = min(g["shares"], vested_on(rows, stop) + double +
187
+ (single if coc and coc <= stop else 0))
188
+ unvested = g["shares"] - at_leave
189
+ vested_leave = at_leave
190
+ if g["type"] in STOCK or g["early_exercise"]:
191
+ leave = f"left {stop}: {vested_leave:,} vested, {unvested:,} unvested shares the company can buy back"
192
+ if isinstance(g["repurchase_days"], int):
193
+ leave += f"; the company must exercise the buy-back by {stop + dt.timedelta(days=g['repurchase_days'])} " \
194
+ f"({g['repurchase_days']} days), or it lapses"
195
+ else:
196
+ warns.append(f"{g['holder']}: the buy-back window isn't given; read it from the stock purchase agreement")
197
+ else:
198
+ days = g["pte_days"] if isinstance(g["pte_days"], int) else 90
199
+ if not isinstance(g["pte_days"], int):
200
+ warns.append(f"{g['holder']}: exercise window after leaving not given; assumed 90 days, the usual term")
201
+ leave = (f"left {stop}: {vested_leave:,} vested, {unvested:,} forfeited; last day to exercise the vested options "
202
+ f"{stop + dt.timedelta(days=days)} ({days} days)")
203
+ election = None
204
+ needs_83b = cap["us"] and (g["type"] in STOCK or g["early_exercise"])
205
+ if needs_83b:
206
+ start = g["early_exercise"] or g["grant_date"]
207
+ raw = start + dt.timedelta(days=30)
208
+ deadline = next_business_day(raw)
209
+ moved = f" (day 30, {raw}, is a weekend or holiday)" if deadline != raw else ""
210
+ election = {"from": start, "deadline": deadline, "moved": moved}
211
+ if as_of > deadline and not g["already_filed_83b"]:
212
+ fails.append(f"{g['holder']}: the 83(b) deadline was {deadline}; it has passed unless it was filed")
213
+ if g["start"].day > 28:
214
+ nth = {29: "29th", 30: "30th", 31: "31st"}[g["start"].day]
215
+ notes.append(f"vests on the {nth}, so on the last day of shorter months")
216
+ return {"holder": g["holder"], "type": g["type"], "shares": g["shares"], "rows": rows, "cliff_date": cliff_date,
217
+ "cliff_amount": vested_on(rows, cliff_date) if cliff_date else rows[0][2], "fully_vested": end,
218
+ "vested": vested, "unvested": g["shares"] - vested, "as_of": at, "leave": leave, "election": election,
219
+ "notes": notes, "fails": fails, "warns": warns}
220
+
221
+
222
+ def report(cap, results, full):
223
+ L = [f"# {cap['company']}: vesting", "",
224
+ "| Holder | Type | Shares | Cliff | Fully vested | Vested on date | Unvested |", "|---|---|---:|---|---|---:|---:|"]
225
+ for r in results:
226
+ cliff = f"{r['cliff_date']}: {r['cliff_amount']:,}" if r["cliff_date"] else "none"
227
+ L.append(f"| {r['holder']} | {r['type']} | {r['shares']:,} | {cliff} | {r['fully_vested']} | "
228
+ f"{r['vested']:,} ({r['as_of']}) | {r['unvested']:,} |")
229
+ for r in results:
230
+ L += ["", f"## {r['holder']}"]
231
+ if r["election"]:
232
+ e = r["election"]
233
+ L.append(f"- 83(b) election: file by **{e['deadline']}**, 30 days from {e['from']}{e['moved']}.")
234
+ if r["leave"]:
235
+ L.append(f"- {r['leave']}")
236
+ for n in r["notes"]:
237
+ L.append(f"- {n}")
238
+ rows = r["rows"] if full or len(r["rows"]) <= 8 else (
239
+ r["rows"][:2] + [x for x in r["rows"][2:-1] if x[0].month == r["rows"][0][0].month] + r["rows"][-1:])
240
+ L += ["", "| Date | Vests | Total vested |", "|---|---:|---:|"]
241
+ for d, n, c in rows:
242
+ L.append(f"| {d} | {n:,} | {c:,} |")
243
+ if rows is not r["rows"]:
244
+ L.append(f"| … {len(r['rows'])} vesting dates in all (--full for every one) | | |")
245
+ for r in results:
246
+ L += [f"FAIL {f}" for f in r["fails"]] + [f"WARN {w}" for w in r["warns"]]
247
+ return "\n".join(L) + "\n"
248
+
249
+
250
+ def main(argv=None):
251
+ ap = argparse.ArgumentParser(description="Vesting schedules, leavers, acceleration and 83(b) deadlines.")
252
+ ap.add_argument("vesting")
253
+ ap.add_argument("--as-of", help="YYYY-MM-DD to report what is vested (default: today)")
254
+ ap.add_argument("--csv", help="write every vesting date for every grant here")
255
+ ap.add_argument("--full", action="store_true", help="show every vesting date, not one a year")
256
+ args = ap.parse_args(argv)
257
+ try:
258
+ with open(args.vesting, encoding="utf-8-sig") as f:
259
+ cap = load(json.load(f))
260
+ as_of = day(args.as_of, "--as-of") if args.as_of else dt.date.today()
261
+ except (OSError, ValueError, UnicodeDecodeError) as e:
262
+ print(f"FAIL cannot read input: {e}")
263
+ return 2
264
+ except InputError as e:
265
+ print(f"FAIL {e}")
266
+ return 2
267
+ results = [outcome(cap, g, as_of) for g in cap["grants"]]
268
+ print(report(cap, results, args.full), end="")
269
+ if args.csv:
270
+ with open(args.csv, "w", encoding="utf-8", newline="") as f:
271
+ w = csv.writer(f)
272
+ w.writerow(["Holder", "Date", "Vests", "Total vested"])
273
+ for r in results:
274
+ for d, n, c in r["rows"]:
275
+ w.writerow([r["holder"], d.isoformat(), n, c])
276
+ return 1 if any(r["fails"] for r in results) else 0
277
+
278
+
279
+ if __name__ == "__main__":
280
+ sys.exit(main())
@@ -0,0 +1,12 @@
1
+ {
2
+ "company": "Example Labs, Inc.",
3
+ "jurisdiction": "US",
4
+ "grants": [
5
+ {"holder": "Founder A", "type": "founder stock", "shares": 4500000, "grant_date": "2026-01-31", "vesting_start": "2026-01-31",
6
+ "months": 48, "cliff_months": 12, "frequency": "monthly", "acceleration": {"double": 1.0, "window_months": 12}},
7
+ {"holder": "First engineer", "type": "option", "shares": 120000, "grant_date": "2026-03-02", "vesting_start": "2026-02-16",
8
+ "months": 48, "cliff_months": 12, "frequency": "monthly", "leave_date": "2027-08-20", "exercise_window_days": 90},
9
+ {"holder": "Advisor", "type": "option", "shares": 25000, "grant_date": "2026-05-01", "vesting_start": "2026-05-01",
10
+ "months": 24, "cliff_months": 3, "frequency": "quarterly"}
11
+ ]
12
+ }