skilldrop-cli 1.9.0 → 1.11.0

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 (46) hide show
  1. package/README.md +3 -2
  2. package/catalogue.json +11 -2
  3. package/model-routing.json +12 -0
  4. package/package.json +1 -1
  5. package/packs/educator/pack.json +25 -0
  6. package/packs/educator/skills/exam-paper-check/SKILL.md +125 -0
  7. package/packs/educator/skills/exam-paper-check/evals/eval_queries.json +30 -0
  8. package/packs/educator/skills/exam-paper-check/evals/evals.json +73 -0
  9. package/packs/educator/skills/exam-paper-check/evals/files/1/year9-science-scheme.md +16 -0
  10. package/packs/educator/skills/exam-paper-check/evals/files/1/year9-science.md +58 -0
  11. package/packs/educator/skills/exam-paper-check/evals/files/2/mock-paper1-ms.md +32 -0
  12. package/packs/educator/skills/exam-paper-check/evals/files/2/mock-paper1.md +60 -0
  13. package/packs/educator/skills/exam-paper-check/evals/files/3/hist1042-quiz-markscheme.md +37 -0
  14. package/packs/educator/skills/exam-paper-check/evals/files/3/hist1042-quiz.md +50 -0
  15. package/packs/educator/skills/exam-paper-check/evals/files/4/chem-mock-paper.md +51 -0
  16. package/packs/educator/skills/exam-paper-check/evals/files/4/chem-mock-scheme.md +32 -0
  17. package/packs/educator/skills/exam-paper-check/evals/files/5/econ-p2-paper.md +57 -0
  18. package/packs/educator/skills/exam-paper-check/evals/files/5/econ-p2-scheme.md +34 -0
  19. package/packs/educator/skills/exam-paper-check/examples/inputs/spec.json +3 -0
  20. package/packs/educator/skills/exam-paper-check/examples/inputs/year9-science-scheme.md +16 -0
  21. package/packs/educator/skills/exam-paper-check/examples/inputs/year9-science.md +58 -0
  22. package/packs/educator/skills/exam-paper-check/examples/year9-forces.md +68 -0
  23. package/packs/educator/skills/exam-paper-check/manifest.json +33 -0
  24. package/packs/educator/skills/exam-paper-check/scripts/exam_check.py +328 -0
  25. package/packs/educator/skills/exam-paper-check/templates/spec.json +11 -0
  26. package/packs/startup-legal/pack.json +20 -4
  27. package/packs/startup-legal/skills/lease-check/SKILL.md +119 -0
  28. package/packs/startup-legal/skills/lease-check/evals/eval_queries.json +30 -0
  29. package/packs/startup-legal/skills/lease-check/evals/evals.json +45 -0
  30. package/packs/startup-legal/skills/lease-check/evals/files/1/heads-of-terms.md +15 -0
  31. package/packs/startup-legal/skills/lease-check/evals/files/2/heads-of-terms.md +32 -0
  32. package/packs/startup-legal/skills/lease-check/evals/files/3/heads-of-agreement.md +29 -0
  33. package/packs/startup-legal/skills/lease-check/examples/first-office.md +75 -0
  34. package/packs/startup-legal/skills/lease-check/examples/inputs/heads-of-terms.md +15 -0
  35. package/packs/startup-legal/skills/lease-check/manifest.json +33 -0
  36. package/packs/startup-legal/skills/lease-check/reference.md +70 -0
  37. package/packs/startup-legal/skills/lease-check/scripts/lease_terms.py +276 -0
  38. package/packs/startup-legal/skills/lease-check/templates/terms.json +15 -0
  39. package/packs/startup-legal/skills/vesting-schedule/SKILL.md +122 -0
  40. package/packs/startup-legal/skills/vesting-schedule/evals/eval_queries.json +30 -0
  41. package/packs/startup-legal/skills/vesting-schedule/evals/evals.json +70 -0
  42. package/packs/startup-legal/skills/vesting-schedule/examples/cofounder-leaving.md +66 -0
  43. package/packs/startup-legal/skills/vesting-schedule/examples/inputs/vesting.json +8 -0
  44. package/packs/startup-legal/skills/vesting-schedule/manifest.json +33 -0
  45. package/packs/startup-legal/skills/vesting-schedule/scripts/vesting.py +280 -0
  46. package/packs/startup-legal/skills/vesting-schedule/templates/vesting.json +12 -0
@@ -0,0 +1,30 @@
1
+ [
2
+ {
3
+ "query": "How much has my co-founder vested if she leaves next month?",
4
+ "should_trigger": true
5
+ },
6
+ {
7
+ "query": "When is the 83(b) deadline for the stock we signed last week?",
8
+ "should_trigger": true
9
+ },
10
+ {
11
+ "query": "Build the vesting schedule for our first three hires' options",
12
+ "should_trigger": true
13
+ },
14
+ {
15
+ "query": "What does double-trigger acceleration give our team if we're acquired?",
16
+ "should_trigger": true
17
+ },
18
+ {
19
+ "query": "How much of the company will I own after the Series A?",
20
+ "should_trigger": false
21
+ },
22
+ {
23
+ "query": "Review this employment contract before I send it",
24
+ "should_trigger": false
25
+ },
26
+ {
27
+ "query": "What strike price should our options have?",
28
+ "should_trigger": false
29
+ }
30
+ ]
@@ -0,0 +1,70 @@
1
+ {
2
+ "skill_name": "vesting-schedule",
3
+ "evals": [
4
+ {
5
+ "id": 1,
6
+ "prompt": "My co-founder Alex is leaving on 30 September 2026. 3,000,000 founder shares, four-year vesting, one-year cliff, monthly, from 1 July 2025; the stock purchase agreement was signed 15 September 2025. How much does Alex keep, and does it matter if Alex never filed the 83(b)?",
7
+ "assertions": [
8
+ "Says Alex keeps 875,000 shares and 2,125,000 are unvested and can be bought back",
9
+ "Gives the cliff as 1 July 2026 with 750,000 shares",
10
+ "Gives the 83(b) deadline as 15 October 2025, 30 days after the purchase, and says it has passed",
11
+ "Explains plainly what a missed 83(b) means: each vesting tranche is taxed as it vests",
12
+ "Does not mention a script or the skill's name, and does not invent terms (acceleration, repurchase price) not in the prompt"
13
+ ]
14
+ },
15
+ {
16
+ "id": 2,
17
+ "held_out": "2026-11-08",
18
+ "prompt": "my cofounder Dana Okafor has 850,000 founder shares, 4 year vesting with a 1 year cliff, monthly after that, vesting start 31 Jan 2024. she's leaving on 20 Sep 2026. how much has she vested, when did the cliff hit, and what happens to the rest?",
19
+ "assertions": [
20
+ "Cliff is 31 January 2025, when 212,500 shares vest (850,000 x 12 / 48)",
21
+ "Handles the 31st start in short months: the February 2025 vesting date is 28 February 2025 (not 3 March or skipped), with cumulative vested 230,208 (floor of 850,000 x 13 / 48); later dates fall on the last day of shorter months, e.g. 30 June",
22
+ "Vested on leaving (20 September 2026) is 548,958 shares - 31 months, last vesting date 31 August 2026 (floor of 850,000 x 31 / 48 = 548,958.33); accept 548,959 only if a different rounding is stated",
23
+ "The September 2026 vesting date (30 September 2026) falls after she leaves, so it does not vest",
24
+ "Unvested shares are 301,042 (850,000 - 548,958), and the answer says that for founder stock these are normally subject to the company's repurchase right (typically at the original price) or forfeited under the stock agreement, rather than describing an option exercise window",
25
+ "Does not invent acceleration, a repurchase price, a leaving reason, or 83(b) facts that are not in the prompt, and says the stock agreement governs"
26
+ ]
27
+ },
28
+ {
29
+ "id": 3,
30
+ "held_out": "2026-11-08",
31
+ "prompt": "We're Larkspur Robotics, Inc. (Delaware) and I need our vesting worked out before a board meeting. Grants:\n\n1. Mei Tanaka (co-founder, CEO): 3,000,000 shares of restricted common stock, vesting commencement date 1 March 2025, 48 months, 12-month cliff, then monthly. Double-trigger acceleration: 100% of unvested shares vest if she is terminated without cause within 12 months after a change of control.\n2. Tomas Herrera (co-founder, CTO): 2,000,000 shares of restricted common stock, same schedule and start date as Mei. Single-trigger acceleration: 25% of his then-unvested shares vest on a change of control. No other acceleration.\n3. Noor Haddad (joining as third co-founder): buys 650,000 shares of restricted common stock on 25 November 2026, vesting commencement date 25 November 2026, 48 months, 12-month cliff, then monthly. No acceleration.\n\nQuestions: (a) What is Noor's 83(b) filing deadline? (b) When is her cliff and how many shares vest then? (c) We have an offer that would close on 15 June 2027. What does each of the three have vested at closing, after any acceleration? (d) If the buyer terminates Mei without cause on 30 September 2027, what does she end up with? (e) What if instead they terminate her without cause on 20 July 2028?",
32
+ "assertions": [
33
+ "Noor's 83(b) deadline: 30 days after 25 November 2026 is Friday 25 December 2026, Christmas Day (a federal holiday), followed by a weekend, so the deadline moves to Monday 28 December 2026; the answer advises filing well before it",
34
+ "Noor's cliff is 25 November 2027 with 162,500 shares (650,000 x 12 / 48); at the 15 June 2027 closing she has 0 vested and, having no acceleration, still 0 after closing",
35
+ "At closing on 15 June 2027 (27 months, last vesting date 1 June 2027): Mei has 1,687,500 vested (3,000,000 x 27 / 48) and the sale alone does not accelerate her shares (double-trigger needs the termination too)",
36
+ "Tomas at closing: 1,125,000 vested by schedule (2,000,000 x 27 / 48) plus 25% of his 875,000 unvested = 218,750 accelerated, giving 1,343,750; applies the 25% to unvested shares, not to the whole grant (which would wrongly give 500,000)",
37
+ "Mei terminated without cause on 30 September 2027 is within 12 months of closing, so double-trigger applies: 1,875,000 vested by schedule (30 months, last vest 1 September 2027) plus 1,125,000 accelerated = all 3,000,000 shares vested",
38
+ "Mei terminated on 20 July 2028 is outside the 12-month window (which ended 15 June 2028), so there is no acceleration: 2,500,000 vested (40 months, last vest 1 July 2028) and 500,000 unvested, subject to repurchase or forfeiture",
39
+ "Does not invent an option exercise window for this restricted stock, acceleration for Noor, a purchase or repurchase price, or deal terms not in the prompt"
40
+ ]
41
+ },
42
+ {
43
+ "id": 4,
44
+ "held_out": "2026-11-09",
45
+ "prompt": "My co-founder Ravi Okafor has 1,250,000 founder shares, 4-year vesting, 1-year cliff, monthly after that, vesting start 31 Aug 2025. He signed his stock purchase agreement on 27 Oct 2026 and hasn't filed an 83(b) yet. Today is 9 Nov 2026. How much has he vested, when did the cliff hit, and what's his 83(b) deadline?",
46
+ "assertions": [
47
+ "Cliff date is 31 Aug 2026, with 312,500 shares vesting then (1,250,000 x 12 / 48)",
48
+ "Vested today (9 Nov 2026) is 364,583 shares: 14 monthly dates have passed (to 31 Oct 2026), floor(1,250,000 x 14 / 48) = floor(364,583.33); unvested 885,417",
49
+ "Vesting dates fall on the 31st or the last day of shorter months (30 Sep 2025, 30 Nov 2026, 28 Feb 2027 etc.); the next vesting is 30 Nov 2026 for 26,042 shares (390,625 - 364,583), and vesting finishes on 31 Aug 2029",
50
+ "83(b) deadline: 30 days after the 27 Oct 2026 transfer is Thursday 26 Nov 2026, which is Thanksgiving (a US federal holiday), so the deadline moves to Friday 27 Nov 2026",
51
+ "Says the 30-day clock runs from the 27 Oct 2026 purchase/transfer date, not the 31 Aug 2025 vesting start, and urges filing promptly since the deadline cannot be extended",
52
+ "Does not invent a purchase price, a tax amount, acceleration terms or other details not in the prompt"
53
+ ]
54
+ },
55
+ {
56
+ "id": 5,
57
+ "held_out": "2026-11-09",
58
+ "prompt": "We're Halcyon Loom Inc. and we're in the middle of an acquisition, so I need two vesting questions answered precisely. Today is 9 Nov 2026.\n\n1. Lena Varga, a co-founder, got 1,100,000 shares of restricted stock with a vesting start of 29 March 2024: 48 months, 1-year cliff, monthly vesting after. She resigned effective 4 September 2026. Her stock purchase agreement gives the company the right to repurchase unvested shares at her original price, exercisable by written notice within 60 days of her termination date. We haven't sent any notice. How many shares has she vested, how many can we buy back, and are we still in time?\n\n2. Jonah Pike, an early employee, holds an option over 50,000 shares, vesting start 31 January 2025, same 4-year / 1-year-cliff / monthly terms. His grant has double-trigger acceleration: 100% of unvested shares vest if he is terminated without cause within 12 months after a change of control. The sale is due to close on 15 December 2026 and the buyer says it will keep him. How much is vested at closing, does his acceleration kick in at closing, and if the buyer lets him go without cause on 10 March 2027, how much will he have and what's his last day to exercise? His option agreement gives 90 days after termination to exercise.",
59
+ "assertions": [
60
+ "Lena: vesting dates fall on the 29th, except 28 Feb 2025 (and other Februaries); the cliff was 29 Mar 2025; at her 4 Sep 2026 departure 29 months had vested (last date 29 Aug 2026): floor(1,100,000 x 29 / 48) = floor(664,583.33) = 664,583 vested, leaving 435,417 unvested shares subject to repurchase",
61
+ "Lena's repurchase window: 60 days after 4 Sep 2026 is 3 Nov 2026, which is before today (9 Nov 2026), so the window closed 6 days ago with no notice sent; says the company appears to have lost the right under the agreement as described and should check the agreement's exact wording with counsel, rather than telling them they can still buy back",
62
+ "Jonah at closing (15 Dec 2026): 22 monthly dates have passed (last 30 Nov 2026, the month-end because November has 30 days): floor(50,000 x 22 / 48) = floor(22,916.67) = 22,916 vested; the cliff was 31 Jan 2026 with 12,500 vesting",
63
+ "States the double trigger is not met at closing because he is kept on (sale alone is one trigger), so nothing accelerates at closing",
64
+ "If terminated without cause on 10 Mar 2027 (85 days after closing, within the 12-month window), both triggers are met: without acceleration he would have 26,041 vested (25 months, last date 28 Feb 2027, floor(50,000 x 25 / 48)); acceleration vests the remaining 23,959, so all 50,000 are vested",
65
+ "Jonah's last day to exercise is 8 Jun 2027 (90 days after 10 Mar 2027)",
66
+ "Does not invent repurchase prices, exercise prices, tax figures, deal value or terms not in the prompt"
67
+ ]
68
+ }
69
+ ]
70
+ }
@@ -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
+ }