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,29 @@
1
+ # Heads of Agreement (non-binding) - Commercial Lease
2
+
3
+ **Premises:** Lot 7, 112 Wattle Street, Brunswick VIC 3056 - ground floor laboratory and office, approx. 410 m2
4
+
5
+ **Lessor:** Wattle Street Holdings Pty Ltd
6
+
7
+ **Lessee:** Quokka Bio Pty Ltd (ACN 600 000 000)
8
+
9
+ **Term:** 3 years commencing 1 September 2027 and expiring 31 August 2030
10
+
11
+ **Option to renew:** One further term of 3 years. The option must be exercised by written notice given not more than 6 months and not less than 3 months before the expiry date.
12
+
13
+ **Base rent:** A$96,000 per annum plus GST, payable monthly in advance.
14
+
15
+ **Rent-free (incentive):** 3 months' rent-free from the commencement date. Rent commencement date: 1 November 2027.
16
+
17
+ **Annual rent reviews:** On each anniversary of the commencement date (1 September 2028 and 1 September 2029) the base rent is reviewed by CPI (All Groups, Melbourne), subject to a minimum increase of 2% and a maximum increase of 5% per review.
18
+
19
+ **Review on renewal:** At the start of any option term the rent is reviewed to current market rent, provided the rent will not be less than the rent payable immediately before the review.
20
+
21
+ **Outgoings:** Lessee to pay 100% of the outgoings for the premises (rates, land tax, insurance, building services, common area cleaning and maintenance). Estimate for year 1: A$14,400 per annum plus GST. No cap on annual increases.
22
+
23
+ **Lessee break:** The lessee may end the lease on 31 August 2029 by giving not less than 6 months' written notice, provided that (a) the lessee is not in breach of any term of the lease at the date of the notice or at the break date, and (b) the lessee pays a break fee equal to 3 months' base rent at the rate payable at the break date.
24
+
25
+ **Make good:** At the end of the lease the lessee must remove all fit-out and laboratory services and return the premises to base-building condition, at its cost.
26
+
27
+ **Permitted use:** PC2 laboratory and ancillary office.
28
+
29
+ **Security:** Bank guarantee equal to 3 months' base rent plus GST.
@@ -0,0 +1,75 @@
1
+ # Worked example: heads of terms for a first office
2
+
3
+ ## Input
4
+
5
+ > *"We've been offered heads of terms for our first office
6
+ > ([inputs/heads-of-terms.md](inputs/heads-of-terms.md)). The agent wants an answer this week.
7
+ > What are we committing to, and when could we get out?"*
8
+
9
+ The terms extracted are [`../templates/terms.json`](../templates/terms.json).
10
+
11
+ ## What it computed
12
+
13
+ ```text
14
+ # Lease: dates and money
15
+
16
+ Term: 2027-01-01 to 2031-12-31 (60 months).
17
+
18
+ ## Deadlines
19
+
20
+ | Date | What | Detail |
21
+ |---|---|---|
22
+ | 2027-04-01 | rent starts | after 3 months rent-free |
23
+ | 2029-07-01 | tenant break notice | for the break on 2030-01-01; conditions: all rent paid; vacant possession |
24
+ | 2030-01-01 | rent review | cpi, upward only |
25
+ | 2031-06-30 | notice to leave at the end | for the end on 2031-12-31 |
26
+
27
+ ## Committed cost
28
+
29
+ | Up to | Rent | Service charge | Total |
30
+ |---|---:|---:|---:|
31
+ | to the break on 2030-01-01 | £99,000 | £14,400 | £113,400 |
32
+ | to the end of the term (2031-12-31) | £179,990 | £24,000 | £203,990 |
33
+
34
+ ## Rent by year
35
+
36
+ | Year | From | Yearly rent | Paid that year |
37
+ |---:|---|---:|---:|
38
+ | 1 | 2027-01-01 | £36,000 | £27,000 |
39
+ | 2 | 2028-01-01 | £36,000 | £36,000 |
40
+ | 3 | 2029-01-01 | £36,000 | £36,000 |
41
+ | 4 | 2030-01-01 | £40,495 | £40,495 |
42
+ | 5 | 2031-01-01 | £40,495 | £40,495 |
43
+
44
+ Deposit: £18,000.
45
+
46
+ WARN the break on 2030-01-01 is conditional (all rent paid; vacant possession): one unmet condition, such as a small unpaid sum, can make it fail
47
+ ```
48
+
49
+ ## Reply
50
+
51
+ **Signing commits Brightloop to £113,400 up to the break on 1 January 2030** (33 months' rent
52
+ after the rent-free, plus service charge), **and £203,990 if you stay to the end** on 31
53
+ December 2031. The break needs written notice by **1 July 2029**, and as drafted it fails if any
54
+ sum is unpaid or the unit isn't empty on the day, which would keep you in to 2031 for another
55
+ £90,590.
56
+
57
+ **Fixes before signing**
58
+ - 🟥 **Personal guarantee.** Ask for: *"No personal guarantee; the Tenant will provide a rent
59
+ deposit of six months' rent instead."* The deposit is already there.
60
+ - 🟥 **Break conditions.** Ask for: *"The only condition of the break is that the Tenant has paid
61
+ the principal rent due up to the break date."*
62
+ - 🟧 **Full repairing with no schedule of condition.** Ask for a photographic schedule of
63
+ condition, attached to the lease, and repair limited to that condition.
64
+ - 🟧 **Service charge with no cap.** Ask for a cap at £4,800 rising with CPI, and no charges for
65
+ improvements or replacing plant.
66
+ - 🟨 **No sharing.** Ask for the right to share with group companies without consent, and to
67
+ sublet with consent not to be unreasonably withheld.
68
+
69
+ **Deadlines to diarise**
70
+ - 1 April 2027: rent starts.
71
+ - 1 July 2029: last day to serve the break notice.
72
+ - 1 January 2030: rent review, to £40,495 a year at the capped 4 % (CPI below the cap would mean less).
73
+ - 30 June 2031: notice to leave at the end.
74
+
75
+ **Worth a lawyer's hour:** the personal guarantee, if the landlord won't drop it.
@@ -0,0 +1,15 @@
1
+ # Heads of terms — Unit 4, Foundry Yard, Example Town
2
+
3
+ - Landlord: Foundry Yard Estates Ltd
4
+ - Tenant: Brightloop Labs Ltd
5
+ - Premises: Unit 4, first floor, 2,100 sq ft
6
+ - Term: 5 years from 1 January 2027
7
+ - Rent: £36,000 a year exclusive, payable quarterly in advance
8
+ - Rent-free: first 3 months
9
+ - Rent review: at the end of year 3, to CPI over the preceding three years, compounded, with a cap of 4% a year and a collar of 1% a year; upward only
10
+ - Service charge: estimated £4,800 a year, no cap
11
+ - Deposit: 6 months' rent
12
+ - Tenant break: end of year 3, on 6 months' prior written notice, conditional on all sums due under the lease having been paid and vacant possession being given
13
+ - Repair: full repairing; no schedule of condition
14
+ - Alienation: no subletting or sharing
15
+ - Personal guarantee: from one director for the first 3 years
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "lease-check",
3
+ "version": "0.1.0",
4
+ "description": "Check a commercial lease before a startup signs it for its first office, studio or lab — a script turns the lease's terms into every date and sum the tenant must plan around (rent year by year through stepped rents, fixed and index-linked reviews with caps and collars, rent-free months, what the company is committed to pay up to each break and to the end, and each break, renewal and notice deadline as a date), then flags terms that can't all be true and deadlines already passed — and reads the lease for the clauses that hurt a small tenant. Use when a founder says \"review this office lease\", \"what's our break clause notice date\", \"how much are we committing to on this lease\", \"check this lease before I sign\", \"when can we get out of this lease\", or \"what does the rent review do\".",
5
+ "entrypoint": "SKILL.md",
6
+ "deps": {
7
+ "npm": [],
8
+ "pip": []
9
+ },
10
+ "env": {
11
+ "required": [],
12
+ "optional": []
13
+ },
14
+ "related": [],
15
+ "tags": [
16
+ "lease",
17
+ "office",
18
+ "rent-review",
19
+ "break-clause",
20
+ "startup",
21
+ "property"
22
+ ],
23
+ "permissions": {
24
+ "network": [],
25
+ "commands": [],
26
+ "files": "named-paths",
27
+ "notes": "Reads the terms.json it is given and writes nothing."
28
+ },
29
+ "model": {
30
+ "tier": "standard",
31
+ "rationale": "Extracting lease terms and reading clauses from the tenant's side is bounded; the script does the dates and sums."
32
+ }
33
+ }
@@ -0,0 +1,70 @@
1
+ # Lease clauses that hurt a small tenant
2
+
3
+ For each: what to look for, why it hurts, and wording to ask for. Adapt numbers to the deal.
4
+
5
+ ## Break clause
6
+
7
+ - **Look for:** conditions ("all rent paid", "material compliance with covenants", "vacant
8
+ possession"); notice in a strict form or to a specific address; a penalty; rent-free clawed
9
+ back on breaking.
10
+ - **Why it hurts:** courts apply break conditions strictly. A few pounds of unpaid service charge
11
+ or one item left behind can void the break and keep the tenant in to the end.
12
+ - **Ask for:** *"The only condition of the break is that the Tenant has paid the principal rent
13
+ due up to the break date."* Delete "material compliance" and "vacant possession" conditions, or
14
+ narrow vacant possession to "free of the Tenant's occupation".
15
+
16
+ ## Rent review
17
+
18
+ - **Look for:** open-market reviews (unknown increase); upward-only wording; index-linked reviews
19
+ that compound several years at once; no cap.
20
+ - **Ask for:** a fixed increase, or index-linked with a cap of 3–4 % a year (compounded) and no
21
+ collar; reviews no more often than every three years.
22
+
23
+ ## Repair, dilapidations and reinstatement
24
+
25
+ - **Look for:** "full repairing" obligations, especially on an older building; reinstatement of
26
+ all alterations at the end; no schedule of condition.
27
+ - **Why it hurts:** the tenant can be billed at the end to put the building in better condition
28
+ than when it moved in.
29
+ - **Ask for:** *"The Tenant is not required to put the Premises in better repair than shown in
30
+ the attached Schedule of Condition."* Photograph and attach it. Reinstatement only of
31
+ alterations the tenant made, and only if the landlord asks six months before the end.
32
+
33
+ ## Service charge
34
+
35
+ - **Look for:** no cap; capital improvements or replacement of plant charged to tenants;
36
+ management fees as a percentage with no limit.
37
+ - **Ask for:** a cap at a stated sum rising with CPI; no charges for improvements, replacing
38
+ structure or plant, or remedying defects in the original build.
39
+
40
+ ## Alienation (sharing, subletting, assigning)
41
+
42
+ - **Look for:** an absolute ban on sharing or subletting; consent needed to share with a group
43
+ company.
44
+ - **Ask for:** the right to share with group companies without consent, and to sublet or assign
45
+ with the landlord's consent "not to be unreasonably withheld or delayed".
46
+
47
+ ## Guarantees and deposits
48
+
49
+ - **Look for:** a personal guarantee from a founder; a deposit of more than six months' rent;
50
+ no terms for its return.
51
+ - **Ask for:** no personal guarantee; a deposit of three to six months' rent, held in a
52
+ separate account, returned within 28 days of the end, and released early after two years of
53
+ on-time payment.
54
+
55
+ ## Use, alterations and insurance
56
+
57
+ - **Look for:** a narrow permitted use that blocks growth (no lab use, no events); consent needed
58
+ for any internal alteration; insurance excluding terrorism or uninsured risks.
59
+ - **Ask for:** use "as offices and any other use within the same planning class"; internal
60
+ non-structural alterations without consent; rent suspended if the premises can't be used
61
+ after damage, including by uninsured risks, with a right to terminate if not restored within
62
+ a year.
63
+
64
+ ## When a lawyer earns the fee
65
+
66
+ - a personal guarantee;
67
+ - full repairing terms on a building older than about 20 years;
68
+ - a break with conditions the landlord won't narrow;
69
+ - a lease that excludes statutory renewal protection where the law gives it;
70
+ - a long term (over five years) with open-market reviews.
@@ -0,0 +1,276 @@
1
+ #!/usr/bin/env python3
2
+ """Turn a commercial lease's terms into the dates and money a tenant has to plan around. Stdlib only.
3
+
4
+ Reads a terms.json (start, length, rent and how it changes, rent-free months, service charge,
5
+ deposit, break clauses, renewal option, notice periods) and prints:
6
+ - rent year by year through stepped rents, fixed and index-linked reviews (with any cap and
7
+ collar), rent-free months and upward-only reviews
8
+ - what the tenant is committed to pay up to each break date and to the end of the term,
9
+ rent and service charge together
10
+ - every deadline as a date: each break notice, the renewal option's window, notice to leave,
11
+ each rent review, the end of the rent-free period
12
+ - deadlines already passed on the given date, and terms that can't all be true (a break
13
+ after the end of the term, a notice longer than the time left, rent-free longer than the term)
14
+ An open-market review can't be priced in advance; it is shown as unknown from that date on.
15
+
16
+ Usage:
17
+ python3 lease_terms.py terms.json [--as-of 2027-03-01] [--json]
18
+ Exit codes: 0 computed, 1 a deadline passed or the terms contradict each other, 2 bad input.
19
+ """
20
+ import argparse
21
+ import calendar
22
+ import datetime as dt
23
+ import json
24
+ import math
25
+ import sys
26
+
27
+
28
+ class InputError(Exception):
29
+ pass
30
+
31
+
32
+ def day(v, what):
33
+ try:
34
+ return dt.date.fromisoformat(str(v))
35
+ except ValueError:
36
+ raise InputError(f"{what} must be a date as YYYY-MM-DD")
37
+
38
+
39
+ def add_months(d, n):
40
+ y, m = divmod(d.year * 12 + d.month - 1 + n, 12)
41
+ return dt.date(y, m + 1, min(d.day, calendar.monthrange(y, m + 1)[1]))
42
+
43
+
44
+ def num(v, what, lo=0.0):
45
+ if isinstance(v, bool) or not isinstance(v, (int, float)) or not math.isfinite(v) or v < lo:
46
+ raise InputError(f"{what} must be a number of at least {lo:g}")
47
+ return float(v)
48
+
49
+
50
+ def rate(v, what):
51
+ v = num(v, what, -100)
52
+ return v / 100 if abs(v) >= 1 else v
53
+
54
+
55
+ def whole(v, what, lo=0):
56
+ if isinstance(v, bool) or not isinstance(v, int) or v < lo:
57
+ raise InputError(f"{what} must be a whole number of months")
58
+ return v
59
+
60
+
61
+ def load(d):
62
+ if not isinstance(d, dict):
63
+ raise InputError("terms.json must be an object")
64
+ start = day(d.get("start"), "start")
65
+ term = whole(d.get("term_months"), "term_months", 1)
66
+ rent = d.get("rent") or {}
67
+ if not isinstance(rent, dict) or "annual" not in rent:
68
+ raise InputError("`rent` needs `annual`, the starting yearly rent")
69
+ reviews = []
70
+ for i, r in enumerate(rent.get("reviews") or [], 1):
71
+ kind = str(r.get("type", "fixed")).lower()
72
+ if kind not in ("fixed", "index", "cpi", "rpi", "open market", "market"):
73
+ raise InputError(f"review {i}: type must be fixed, index (cpi/rpi) or open market")
74
+ rv = {"month": whole(r.get("month"), f"review {i}: month", 1), "type": kind,
75
+ "upward_only": bool(r.get("upward_only", True))}
76
+ if kind == "fixed":
77
+ rv["increase"] = rate(r.get("increase", 0), f"review {i}: increase")
78
+ elif kind in ("index", "cpi", "rpi"):
79
+ rv["assumed"] = rate(r.get("assumed", 0), f"review {i}: assumed index rise")
80
+ rv["cap"] = rate(r["cap"], f"review {i}: cap") if r.get("cap") is not None else None
81
+ rv["collar"] = rate(r["collar"], f"review {i}: collar") if r.get("collar") is not None else None
82
+ rv["compounded_years"] = whole(r.get("years", 1), f"review {i}: years", 1)
83
+ reviews.append(rv)
84
+ steps = [{"month": whole(s.get("from_month"), "stepped rent: from_month", 1),
85
+ "annual": num(s.get("annual"), "stepped rent: annual")} for s in rent.get("stepped") or []]
86
+ breaks = []
87
+ for i, b in enumerate(d.get("breaks") or [], 1):
88
+ bdate = day(b["date"], f"break {i}: date") if b.get("date") else None
89
+ if bdate is None and b.get("month") is None:
90
+ raise InputError(f"break {i}: give `month` (months from the start) or the lease's own `date`")
91
+ if bdate is not None:
92
+ months = (bdate.year - start.year) * 12 + bdate.month - start.month + (1 if bdate.day >= start.day - 1 else 0)
93
+ b = dict(b, month=b.get("month", max(1, months)))
94
+ breaks.append({"month": whole(b.get("month"), f"break {i}: month", 1), "date": bdate,
95
+ "notice": whole(b.get("notice_months"), f"break {i}: notice_months", 0),
96
+ "party": str(b.get("party", "tenant")),
97
+ "conditions": [str(c) for c in b.get("conditions") or []],
98
+ "penalty_months": num(b.get("penalty_months", 0), f"break {i}: penalty_months")})
99
+ sc = d.get("service_charge") or {}
100
+ dep = d.get("deposit") or {}
101
+ renewal = d.get("renewal_option")
102
+ return {
103
+ "currency": str(d.get("currency", "GBP")), "start": start, "term": term,
104
+ "annual": num(rent["annual"], "rent.annual"), "payable": str(rent.get("payable", "quarterly")).lower(),
105
+ "rent_free": whole(rent.get("rent_free_months", 0), "rent_free_months"),
106
+ "reviews": sorted(reviews, key=lambda r: r["month"]), "steps": sorted(steps, key=lambda s: s["month"]),
107
+ "service": num(sc.get("annual", 0), "service_charge.annual"),
108
+ "service_cap": num(sc["cap"], "service_charge.cap") if sc.get("cap") is not None else None,
109
+ "deposit": num(dep["amount"], "deposit.amount") if dep.get("amount") is not None else (
110
+ num(dep.get("months", 0), "deposit.months") / 12 * num(rent["annual"], "rent.annual")),
111
+ "breaks": sorted(breaks, key=lambda b: b["month"]),
112
+ "renewal": None if not renewal else {
113
+ "notice": whole(renewal.get("notice_months"), "renewal_option.notice_months", 0),
114
+ "earliest": whole(renewal.get("earliest_months_before_end", renewal.get("notice_months", 0)),
115
+ "renewal_option.earliest_months_before_end", 0)},
116
+ "leave_notice": whole(d["notice_to_leave_months"], "notice_to_leave_months")
117
+ if d.get("notice_to_leave_months") is not None else None,
118
+ }
119
+
120
+
121
+ def rent_by_month(t):
122
+ """Yearly rate in force for each month, and None once an open-market review makes it unknown."""
123
+ rates = []
124
+ current = t["annual"]
125
+ steps = {s["month"]: s["annual"] for s in t["steps"]}
126
+ reviews = {r["month"]: r for r in t["reviews"]}
127
+ notes = []
128
+ for m in range(t["term"]):
129
+ if m in steps and current is not None:
130
+ current = steps[m]
131
+ if m in reviews and m > 0 and current is not None:
132
+ r = reviews[m]
133
+ if r["type"] == "fixed":
134
+ new = current * (1 + r["increase"])
135
+ elif r["type"] in ("index", "cpi", "rpi"):
136
+ rise = (1 + r["assumed"]) ** r["compounded_years"] - 1
137
+ lo = ((1 + r["collar"]) ** r["compounded_years"] - 1) if r["collar"] is not None else None
138
+ hi = ((1 + r["cap"]) ** r["compounded_years"] - 1) if r["cap"] is not None else None
139
+ if hi is not None:
140
+ rise = min(rise, hi)
141
+ if lo is not None:
142
+ rise = max(rise, lo)
143
+ new = current * (1 + rise)
144
+ else:
145
+ new = None
146
+ notes.append(f"open-market review at month {m}: rent unknown from then; budget for an increase")
147
+ if new is not None and r["upward_only"]:
148
+ new = max(new, current)
149
+ current = new
150
+ rates.append(current)
151
+ return rates, notes
152
+
153
+
154
+ def cost(t, rates, months):
155
+ """Rent (after rent-free) and service charge payable in the first `months` months."""
156
+ rent = 0.0
157
+ unknown = False
158
+ for m in range(min(months, t["term"])):
159
+ if m < t["rent_free"]:
160
+ continue
161
+ if rates[m] is None:
162
+ unknown = True
163
+ continue
164
+ rent += rates[m] / 12
165
+ service = t["service"] * min(months, t["term"]) / 12
166
+ if t["service_cap"] is not None:
167
+ service = min(service, t["service_cap"] * min(months, t["term"]) / 12)
168
+ return rent, service, unknown
169
+
170
+
171
+ def analyse(t, as_of):
172
+ rates, notes = rent_by_month(t)
173
+ end = add_months(t["start"], t["term"]) - dt.timedelta(days=1) # a 5-year lease from 1 Jan ends on 31 Dec
174
+ fails, warns, deadlines = [], [], []
175
+ if t["rent_free"] >= t["term"]:
176
+ fails.append("the rent-free period is as long as the term")
177
+ if t["rent_free"]:
178
+ deadlines.append(("rent starts", add_months(t["start"], t["rent_free"]), f"after {t['rent_free']} months rent-free"))
179
+ for r in t["reviews"]:
180
+ when = add_months(t["start"], r["month"])
181
+ if r["month"] >= t["term"]:
182
+ fails.append(f"a rent review at month {r['month']} falls after the lease ends")
183
+ deadlines.append(("rent review", when, r["type"] + (", upward only" if r["upward_only"] else "")))
184
+ commitments = []
185
+ for b in t["breaks"]:
186
+ bdate = b["date"] or add_months(t["start"], b["month"])
187
+ notice_by = add_months(bdate, -b["notice"])
188
+ if b["month"] >= t["term"]:
189
+ fails.append(f"a break at month {b['month']} ({bdate}) is after the lease ends ({end})")
190
+ continue
191
+ if notice_by < t["start"]:
192
+ fails.append(f"the break on {bdate} needs {b['notice']} months' notice, which runs back before the lease starts")
193
+ deadlines.append((f"{b['party']} break notice", notice_by, f"for the break on {bdate}"
194
+ + (f"; conditions: {'; '.join(b['conditions'])}" if b["conditions"] else "")))
195
+ rent, service, unknown = cost(t, rates, b["month"])
196
+ penalty = (rates[b["month"] - 1] or 0) / 12 * b["penalty_months"] if b["penalty_months"] else 0
197
+ commitments.append((f"to the break on {bdate}", rent + penalty, service, unknown, penalty))
198
+ if b["party"].lower() in ("landlord", "mutual", "either"):
199
+ warns.append(f"the landlord can end the lease on {bdate}; plan for having to move then")
200
+ if b["conditions"]:
201
+ warns.append(f"the break on {bdate} is conditional ({'; '.join(b['conditions'])}): one unmet condition, "
202
+ "such as a small unpaid sum, can make it fail")
203
+ rent, service, unknown = cost(t, rates, t["term"])
204
+ commitments.append((f"to the end of the term ({end})", rent, service, unknown, 0))
205
+ if t["renewal"]:
206
+ latest = add_months(end, -t["renewal"]["notice"])
207
+ earliest = add_months(end, -t["renewal"]["earliest"])
208
+ deadlines.append(("renewal option", latest, f"window opens {earliest}" if earliest < latest else "exact date"))
209
+ if t["leave_notice"] is not None:
210
+ deadlines.append(("notice to leave at the end", add_months(end, -t["leave_notice"]), f"for the end on {end}"))
211
+ deadlines.sort(key=lambda d: d[1])
212
+ for what, when, _ in deadlines:
213
+ if as_of and when < as_of and what not in ("rent review", "rent starts"):
214
+ fails.append(f"the {what} deadline ({when}) has already passed")
215
+ years = []
216
+ for y in range(math.ceil(t["term"] / 12)):
217
+ ms = range(12 * y, min(12 * (y + 1), t["term"]))
218
+ known = [rates[m] for m in ms if rates[m] is not None]
219
+ paid = sum((rates[m] or 0) / 12 for m in ms if m >= t["rent_free"])
220
+ years.append({"year": y + 1, "from": add_months(t["start"], 12 * y),
221
+ "rate": known[-1] if len(known) == len(ms) else None, "rent_paid": paid,
222
+ "unknown": len(known) < len(ms)})
223
+ return {"end": end, "years": years, "commitments": commitments, "deadlines": deadlines,
224
+ "notes": notes, "fails": fails, "warns": warns, "deposit": t["deposit"]}
225
+
226
+
227
+ def money(v, cur):
228
+ sym = {"GBP": "£", "USD": "$", "EUR": "€", "INR": "₹", "AUD": "A$", "CAD": "C$"}.get(cur.upper(), "")
229
+ return f"{sym}{v:,.0f}" if sym else f"{v:,.0f} {cur}"
230
+
231
+
232
+ def report(t, r):
233
+ cur = t["currency"]
234
+ L = ["# Lease: dates and money", "", f"Term: {t['start']} to {r['end']} ({t['term']} months).", "",
235
+ "## Deadlines", "", "| Date | What | Detail |", "|---|---|---|"]
236
+ for what, when, detail in r["deadlines"]:
237
+ L.append(f"| {when} | {what} | {detail} |")
238
+ L += ["", "## Committed cost", "", "| Up to | Rent | Service charge | Total |", "|---|---:|---:|---:|"]
239
+ for label, rent, service, unknown, penalty in r["commitments"]:
240
+ flag = " + unknown open-market rent" if unknown else ""
241
+ pen = f" (incl. {money(penalty, cur)} break penalty)" if penalty else ""
242
+ L.append(f"| {label} | {money(rent, cur)}{pen}{flag} | {money(service, cur)} | {money(rent + service, cur)}{flag} |")
243
+ L += ["", "## Rent by year", "", "| Year | From | Yearly rent | Paid that year |", "|---:|---|---:|---:|"]
244
+ for y in r["years"]:
245
+ rate_s = money(y["rate"], cur) if y["rate"] is not None else "unknown (open market)"
246
+ L.append(f"| {y['year']} | {y['from']} | {rate_s} | {money(y['rent_paid'], cur)}{' + unknown' if y['unknown'] else ''} |")
247
+ if r["deposit"]:
248
+ L += ["", f"Deposit: {money(r['deposit'], cur)}."]
249
+ L += [""] + [f"NOTE {n}" for n in r["notes"]]
250
+ L += [f"FAIL {f}" for f in r["fails"]] + [f"WARN {w}" for w in r["warns"]]
251
+ return "\n".join(L).rstrip() + "\n"
252
+
253
+
254
+ def main(argv=None):
255
+ ap = argparse.ArgumentParser(description="A lease's dates and money.")
256
+ ap.add_argument("terms")
257
+ ap.add_argument("--as-of", help="YYYY-MM-DD; deadlines before it are reported as passed")
258
+ ap.add_argument("--json", action="store_true")
259
+ args = ap.parse_args(argv)
260
+ try:
261
+ with open(args.terms, encoding="utf-8-sig") as f:
262
+ t = load(json.load(f))
263
+ as_of = day(args.as_of, "--as-of") if args.as_of else None
264
+ except (OSError, ValueError, UnicodeDecodeError) as e:
265
+ print(f"FAIL cannot read input: {e}")
266
+ return 2
267
+ except InputError as e:
268
+ print(f"FAIL {e}")
269
+ return 2
270
+ r = analyse(t, as_of)
271
+ print(json.dumps(r, indent=2, default=str) if args.json else report(t, r), end="" if not args.json else "\n")
272
+ return 1 if r["fails"] else 0
273
+
274
+
275
+ if __name__ == "__main__":
276
+ sys.exit(main())
@@ -0,0 +1,15 @@
1
+ {
2
+ "currency": "GBP",
3
+ "start": "2027-01-01",
4
+ "term_months": 60,
5
+ "rent": {
6
+ "annual": 36000,
7
+ "payable": "quarterly",
8
+ "rent_free_months": 3,
9
+ "reviews": [{"month": 36, "type": "cpi", "assumed": 0.04, "cap": 0.04, "collar": 0.01, "years": 3, "upward_only": true}]
10
+ },
11
+ "service_charge": {"annual": 4800},
12
+ "deposit": {"months": 6},
13
+ "breaks": [{"month": 36, "notice_months": 6, "party": "tenant", "conditions": ["all rent paid", "vacant possession"]}],
14
+ "notice_to_leave_months": 6
15
+ }
@@ -0,0 +1,122 @@
1
+ ---
2
+ name: vesting-schedule
3
+ 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".
4
+ ---
5
+
6
+ # vesting-schedule
7
+
8
+ Founders and early employees ask the same questions at the worst moments: a co-founder is
9
+ leaving, an acquirer is asking, a grant was signed last week and nobody remembers the 83(b).
10
+ The answers are date arithmetic that is easy to get wrong in ways that matter: a vesting start
11
+ on the 31st in a month with 30 days, a cliff that falls between two quarterly dates, rounding
12
+ that runs ahead of the grant, a 30-day deadline that lands on the Fourth of July. A script
13
+ computes every grant exactly; the agent turns the founder's documents into its input and the
14
+ result into the answer they need.
15
+
16
+ ## How to respond
17
+
18
+ **These rules come before the steps and outrank them:**
19
+
20
+ - **Answer the question asked, first.** ✅ *"If Sam leaves on 20 August, 45,000 of their
21
+ 120,000 options have vested and the other 75,000 lapse. They have until 18 November to
22
+ exercise."* Never mention a script, a JSON file, this skill's name, a path inside its folder
23
+ or a temporary path, and never call your work "the check" or "a tool".
24
+ - **Every share count and date comes from the computed schedule,** run on the final inputs.
25
+ Each scenario the person asks about (a leaving date, a closing date, "what if she's let go
26
+ in July instead") is its own run with that date. Never count months by hand: a 30-month
27
+ answer written as 31 is the commonest error in this work.
28
+ - **Stay on the questions asked,** in their order, and answer each fully. For a leaver that
29
+ means what happens to the unvested shares: who can buy them back, at what price if the
30
+ documents say, and the deadline for doing it as a date, against today's date, plus the last
31
+ day to exercise vested options.
32
+ - **The 83(b) deadline is never approximate.** Give the date, say it is 30 days from the grant
33
+ (or from an early exercise), and when day 30 falls on a weekend or federal holiday, say that
34
+ the deadline moves to the next business day and advise filing well before day 30 anyway.
35
+ When it has passed, say so plainly and what that means: the shares are taxed as they vest
36
+ instead of at grant. Recommend a tax adviser only for what to do next, not for the date.
37
+ - **Say which terms you assumed.** The vesting start (often earlier than the grant date), the
38
+ cliff, monthly or quarterly vesting, the exercise window after leaving (90 days is usual), the
39
+ company's buy-back window, what counts as a "good reason" resignation for double-trigger, and
40
+ how acceleration is ordered (single trigger here applies to what is unvested at the sale;
41
+ documents sometimes accelerate from the end of the schedule instead). Tag each `[assumption]` and
42
+ give the figure under the other reading when it changes the answer.
43
+ - **Name what is worth a lawyer's hour, and only that.** One or two lines: a leaver dispute,
44
+ acceleration terms an acquirer is pushing back on, a missed 83(b). Never refuse, never open
45
+ with a disclaimer.
46
+ - **Working files stay out of sight.** The input goes in a private temporary folder
47
+ (`mktemp -d`) and is deleted before replying. A spreadsheet they asked for goes in their
48
+ working directory under a plain name.
49
+
50
+ 1. **Gather each grant** from the documents or the conversation: holder, type (founder stock,
51
+ restricted stock, option, RSU), shares, grant date, vesting start, months, cliff, frequency,
52
+ acceleration (single trigger: on a sale; double trigger: a sale and then losing the job within
53
+ a window), any leaving date, any early exercise, and whether an 83(b) was filed. Defaults
54
+ when unsaid: 48 months, a 12-month cliff, monthly, US. Ask at most two questions, only for a
55
+ missing share count or start date.
56
+
57
+ 2. **Write the input** in the shape of [`templates/vesting.json`](templates/vesting.json). A sale
58
+ goes in `change_of_control` at the top.
59
+
60
+ 3. **Run it** on the date they care about (today, a leaving date, a closing date).
61
+
62
+ ```bash
63
+ work=$(mktemp -d 2>/dev/null || mktemp -d ./.work.XXXXXX) # a private folder for this run only
64
+ # Claude Code
65
+ python3 "${CLAUDE_SKILL_DIR}/scripts/vesting.py" "$work/vesting.json" --as-of 2027-06-30
66
+ # Other IDEs (from the skill folder); --csv writes every vesting date
67
+ python3 scripts/vesting.py "$work/vesting.json" --csv vesting-schedule.csv --full
68
+ ```
69
+
70
+ Exit 0 means computed, 1 means a check failed (a schedule that doesn't sum to its grant, or a
71
+ passed 83(b) deadline), 2 means the input can't be read. Rerun with the other reading of any
72
+ assumption that changes the answer.
73
+
74
+ 4. **Reply in this order:**
75
+ 1. **The answer** to their question, with the numbers and dates.
76
+ 2. **Each grant's summary:** cliff date and amount, fully vested date, vested and unvested now.
77
+ 3. **Deadlines:** 83(b) dates, last days to exercise.
78
+ 4. **The schedule,** yearly in the reply (every date in a spreadsheet when asked).
79
+ 5. **Assumptions,** one line each.
80
+ 6. **What to have a lawyer or tax adviser look at,** one or two lines.
81
+
82
+ **Non-interactive runs** (subagent, CI, headless): unsaid terms take the defaults above, tagged
83
+ `[assumption]`. With no share count or no start date for a grant, emit `BLOCKED: need each
84
+ grant's shares and vesting start date`.
85
+
86
+ ## Useful references in this skill
87
+
88
+ - [`templates/vesting.json`](templates/vesting.json) — the input: a founder with double-trigger acceleration, an engineer who leaves, an advisor on quarterly vesting
89
+ - [`scripts/vesting.py`](scripts/vesting.py) — cliffs, month-end dates, whole-share rounding, leavers, acceleration, 83(b) deadlines with US holidays
90
+ - [`examples/cofounder-leaving.md`](examples/cofounder-leaving.md) — worked example: a co-founder leaving 14 months in, with a missed 83(b) question
91
+
92
+ ## Quality bar
93
+
94
+ - **Every share count and date comes from the computed schedule,** and each grant's dates sum to its shares.
95
+ - **A start on the 29th–31st vests on the last day of shorter months,** and leap years count.
96
+ - **The 83(b) deadline is an exact date,** moved past weekends and federal holidays, and a passed one is said plainly.
97
+ - **Leaving gives vested, unvested and the exercise deadline,** each from the leaving date, not today.
98
+ - **Every assumed term is stated,** with the other reading's figure when it changes the answer.
99
+ - **The reply answers the question first,** with no mention of scripts or files.
100
+
101
+ ## When to use this skill
102
+
103
+ - ✅ A co-founder or early employee is leaving and everyone needs the numbers
104
+ - ✅ A new grant was signed: when does it cliff, and when is the 83(b) due
105
+ - ✅ An acquirer or investor asks what vests on a sale
106
+ - ✅ Building the schedule for a founder agreement or an option plan
107
+
108
+ ## When NOT to use this skill
109
+
110
+ - ❌ Tax: what an 83(b) or an exercise costs, ISOs against NSOs, EMI or ESOP rules
111
+ - ❌ Ownership after a funding round: that's a cap table
112
+ - ❌ Drafting the stock purchase or option agreement
113
+ - ❌ Valuing the shares or setting a strike price
114
+
115
+ ## Anti-patterns to avoid
116
+
117
+ - ❌ **"About 30 days from the grant."** The 83(b) date is exact, and a weekend moves it.
118
+ - ❌ **Vesting from the grant date when the agreement says an earlier start.** Founders often backdate the start to when they began.
119
+ - ❌ **Rounding each month up.** A schedule of 2,500.4 shares a month that rounds up overshoots the grant; round the running total down and settle on the last date.
120
+ - ❌ **Treating double-trigger as automatic on a sale.** It needs the second trigger.
121
+ - ❌ **Counting the leaver's vesting to today** instead of to their leaving date.
122
+ - ❌ **"The schedule shows…"** The founder wants the number.