skilldrop-cli 1.8.0 → 1.10.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.
- package/README.md +2 -1
- package/catalogue.json +10 -1
- package/model-routing.json +12 -0
- package/package.json +1 -1
- package/packs/startup-legal/pack.json +66 -0
- package/packs/startup-legal/skills/cap-table/SKILL.md +131 -0
- package/packs/startup-legal/skills/cap-table/evals/eval_queries.json +30 -0
- package/packs/startup-legal/skills/cap-table/evals/evals.json +46 -0
- package/packs/startup-legal/skills/cap-table/examples/seed-to-series-a.md +81 -0
- package/packs/startup-legal/skills/cap-table/manifest.json +34 -0
- package/packs/startup-legal/skills/cap-table/scripts/cap_table.py +344 -0
- package/packs/startup-legal/skills/cap-table/templates/cap.json +23 -0
- package/packs/startup-legal/skills/contract-check/SKILL.md +118 -0
- package/packs/startup-legal/skills/contract-check/evals/eval_queries.json +30 -0
- package/packs/startup-legal/skills/contract-check/evals/evals.json +46 -0
- package/packs/startup-legal/skills/contract-check/evals/files/1/saas-agreement.md +68 -0
- package/packs/startup-legal/skills/contract-check/evals/files/2/consulting-agreement.md +64 -0
- package/packs/startup-legal/skills/contract-check/evals/files/3/reseller-agreement.md +69 -0
- package/packs/startup-legal/skills/contract-check/examples/inputs/saas-agreement.md +68 -0
- package/packs/startup-legal/skills/contract-check/examples/saas-agreement.md +72 -0
- package/packs/startup-legal/skills/contract-check/manifest.json +34 -0
- package/packs/startup-legal/skills/contract-check/reference.md +109 -0
- package/packs/startup-legal/skills/contract-check/scripts/contract_check.py +413 -0
- package/packs/startup-legal/skills/lease-check/SKILL.md +119 -0
- package/packs/startup-legal/skills/lease-check/evals/eval_queries.json +30 -0
- package/packs/startup-legal/skills/lease-check/evals/evals.json +45 -0
- package/packs/startup-legal/skills/lease-check/evals/files/1/heads-of-terms.md +15 -0
- package/packs/startup-legal/skills/lease-check/evals/files/2/heads-of-terms.md +32 -0
- package/packs/startup-legal/skills/lease-check/evals/files/3/heads-of-agreement.md +29 -0
- package/packs/startup-legal/skills/lease-check/examples/first-office.md +75 -0
- package/packs/startup-legal/skills/lease-check/examples/inputs/heads-of-terms.md +15 -0
- package/packs/startup-legal/skills/lease-check/manifest.json +33 -0
- package/packs/startup-legal/skills/lease-check/reference.md +70 -0
- package/packs/startup-legal/skills/lease-check/scripts/lease_terms.py +276 -0
- package/packs/startup-legal/skills/lease-check/templates/terms.json +15 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Check an agreement for the mechanical faults that slip past a read-through. Stdlib only.
|
|
3
|
+
|
|
4
|
+
Reads a contract as Markdown, plain text or Word (.docx) and reports, each with where it is:
|
|
5
|
+
- blanks left in: [●], [•], [___], ____, [DATE], [insert …], TBD
|
|
6
|
+
- cross-references to a clause, section, schedule, exhibit, annex or appendix that doesn't exist
|
|
7
|
+
- clause numbering that skips or repeats
|
|
8
|
+
- a defined term defined twice, defined and never used, or used in lower case once defined
|
|
9
|
+
- a role or object used as if defined (Customer, Services, Confidential Information …) but never defined
|
|
10
|
+
- an amount or period in words that disagrees with its figure: "thirty (60) days", "ten thousand ($1,000)"
|
|
11
|
+
- a date that doesn't exist (31 June, 29 February in a non-leap year)
|
|
12
|
+
and lists every notice period, so deadlines can be worked out.
|
|
13
|
+
|
|
14
|
+
It checks the document's consistency, not whether its terms are fair or lawful.
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
python3 contract_check.py agreement.docx [--json]
|
|
18
|
+
Exit codes: 0 no failures, 1 a failure, 2 the file can't be read.
|
|
19
|
+
"""
|
|
20
|
+
import argparse
|
|
21
|
+
import json
|
|
22
|
+
import re
|
|
23
|
+
import sys
|
|
24
|
+
import zipfile
|
|
25
|
+
from xml.etree import ElementTree
|
|
26
|
+
|
|
27
|
+
W = "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def read_text(path):
|
|
31
|
+
if path.lower().endswith(".docx"):
|
|
32
|
+
with zipfile.ZipFile(path) as z:
|
|
33
|
+
root = ElementTree.fromstring(z.read("word/document.xml"))
|
|
34
|
+
lines = []
|
|
35
|
+
for p in root.iter(W + "p"):
|
|
36
|
+
text = "".join(t.text or "" for t in p.iter(W + "t"))
|
|
37
|
+
lines.append(text)
|
|
38
|
+
return lines
|
|
39
|
+
with open(path, encoding="utf-8-sig") as f:
|
|
40
|
+
return f.read().splitlines()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ---------- numbering and cross-references ----------
|
|
44
|
+
|
|
45
|
+
MULTI = re.compile(r"^\s*(?:#+\s*)?(?:\*\*)?\s*(?:(?:clause|section|article)\s+)?(\d+(?:\.\d+){1,3})\.?(?=\s|\*\*|$)", re.I)
|
|
46
|
+
SINGLE = re.compile(r"^\s*(#+\s*)?(\*\*)?\s*((?:clause|section|article)\s+)?(\d{1,3})\.?(?=\s|\*\*|$)(.*)$", re.I)
|
|
47
|
+
APPENDIX_HEAD = re.compile(r"^\s*(?:#+\s*)?(?:\*\*)?\s*(schedule|exhibit|annex|appendix|annexure)\s+([A-Z0-9]{1,3})\b", re.I)
|
|
48
|
+
REF = re.compile(r"\b(clauses?|sections?|articles?)\s+(\d+(?:\.\d+){0,3})((?:\s*(?:,|and|or|to|-|–)\s*\d+(?:\.\d+){0,3})*)", re.I)
|
|
49
|
+
APPENDIX_REF = re.compile(r"\b(schedule|exhibit|annex|appendix|annexure)\s+([A-Z0-9]{1,3})\b", re.I)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def clause_number(line):
|
|
53
|
+
"""The clause number a line starts, or None. "7.2 …" anywhere counts; a bare "7." counts only
|
|
54
|
+
as a heading (Markdown #, bold, "Clause 7", or a short title line), so a numbered list inside
|
|
55
|
+
a clause isn't read as clauses."""
|
|
56
|
+
m = MULTI.match(line)
|
|
57
|
+
if m:
|
|
58
|
+
return m.group(1)
|
|
59
|
+
m = SINGLE.match(line)
|
|
60
|
+
if not m:
|
|
61
|
+
return None
|
|
62
|
+
hashes, bold, word, n, rest = m.groups()
|
|
63
|
+
rest = rest.strip().strip("*").strip()
|
|
64
|
+
if hashes or bold or word:
|
|
65
|
+
return n
|
|
66
|
+
if rest and len(rest) <= 60 and not rest.endswith((".", ";", ",")) and rest[:1].isupper():
|
|
67
|
+
return n
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def appendix_at(line):
|
|
72
|
+
a = APPENDIX_HEAD.match(line)
|
|
73
|
+
if a and len(line.strip()) < 120:
|
|
74
|
+
return (a.group(1).lower().replace("annexure", "annex"), a.group(2).upper())
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def scan(lines):
|
|
79
|
+
"""Clause numbers in the main body, appendices present, and each numbered line with its scope."""
|
|
80
|
+
nums, apps, numbered = {}, {}, []
|
|
81
|
+
scope = "body"
|
|
82
|
+
for i, line in enumerate(lines, 1):
|
|
83
|
+
key = appendix_at(line)
|
|
84
|
+
if key:
|
|
85
|
+
apps.setdefault(key, i)
|
|
86
|
+
scope = key
|
|
87
|
+
continue
|
|
88
|
+
n = clause_number(line)
|
|
89
|
+
if n:
|
|
90
|
+
numbered.append((scope, n, i))
|
|
91
|
+
if scope == "body":
|
|
92
|
+
nums.setdefault(n, i)
|
|
93
|
+
return nums, apps, numbered
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def numbering_faults(numbered):
|
|
97
|
+
out = []
|
|
98
|
+
seen = {}
|
|
99
|
+
last = {}
|
|
100
|
+
for scope, n, i in numbered:
|
|
101
|
+
if (scope, n) in seen:
|
|
102
|
+
out.append(("FAIL", i, f"clause {n} is numbered twice (also line {seen[(scope, n)]})"))
|
|
103
|
+
continue
|
|
104
|
+
seen[(scope, n)] = i
|
|
105
|
+
parts = [int(x) for x in n.split(".")]
|
|
106
|
+
parent = (scope,) + tuple(parts[:-1])
|
|
107
|
+
prev = last.get(parent)
|
|
108
|
+
if prev is not None and parts[-1] > prev + 1:
|
|
109
|
+
want = ".".join(map(str, parts[:-1] + [prev + 1]))
|
|
110
|
+
out.append(("WARN", i, f"clause numbering jumps to {n}; {want} is missing"))
|
|
111
|
+
last[parent] = parts[-1]
|
|
112
|
+
for k in [k for k in last if len(k) > len(parent) and k[:len(parent)] == parent and k[len(parent)] != parts[-1]]:
|
|
113
|
+
del last[k]
|
|
114
|
+
return out
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def ref_faults(lines, nums, apps):
|
|
118
|
+
out = []
|
|
119
|
+
tops = {n.split(".")[0] for n in nums}
|
|
120
|
+
if not nums:
|
|
121
|
+
return [("WARN", 0, "no numbered clauses found, so cross-references were not checked")]
|
|
122
|
+
for i, line in enumerate(lines, 1):
|
|
123
|
+
body = MULTI.sub("", line, count=1) if MULTI.match(line) else line
|
|
124
|
+
for m in REF.finditer(body):
|
|
125
|
+
targets = [m.group(2)] + re.findall(r"\d+(?:\.\d+){0,3}", m.group(3) or "")
|
|
126
|
+
for t in targets:
|
|
127
|
+
if t in nums or (("." not in t) and t in tops):
|
|
128
|
+
continue
|
|
129
|
+
out.append(("FAIL", i, f"refers to {m.group(1).rstrip('s').lower()} {t}, which doesn't exist"))
|
|
130
|
+
for m in APPENDIX_REF.finditer(line):
|
|
131
|
+
key = (m.group(1).lower().replace("annexure", "annex"), m.group(2).upper())
|
|
132
|
+
if appendix_at(line) and m.start() < 12:
|
|
133
|
+
continue
|
|
134
|
+
if key not in apps and not re.match(r"^(this|the|each|any|a)$", m.group(2), re.I):
|
|
135
|
+
out.append(("FAIL", i, f"refers to {key[0].title()} {key[1]}, which isn't in the document"))
|
|
136
|
+
return dedupe(out)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# ---------- blanks ----------
|
|
140
|
+
|
|
141
|
+
BLANKS = [
|
|
142
|
+
(re.compile(r"\[\s*[●•]+\s*\]|[●•]{1,}"), "a [●] placeholder"),
|
|
143
|
+
(re.compile(r"\[\s*_+\s*\]|_{4,}"), "a blank line to fill in"),
|
|
144
|
+
(re.compile(r"\[(?:insert|enter|add|date|name|address|amount|company|party|tbc|tbd|xx+)[^\]]{0,40}\]", re.I), "a bracketed placeholder"),
|
|
145
|
+
(re.compile(r"\bTB[CD]\b"), "TBD/TBC"),
|
|
146
|
+
(re.compile(r"\bX{2,}\b"), "an XX placeholder"),
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def blank_faults(lines):
|
|
151
|
+
out = []
|
|
152
|
+
for i, line in enumerate(lines, 1):
|
|
153
|
+
for rx, what in BLANKS:
|
|
154
|
+
m = rx.search(line)
|
|
155
|
+
if m:
|
|
156
|
+
out.append(("FAIL", i, f"{what} is still in: “{snippet(line, m.start())}”"))
|
|
157
|
+
break
|
|
158
|
+
return out
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# ---------- defined terms ----------
|
|
162
|
+
|
|
163
|
+
DEF_PATTERNS = [
|
|
164
|
+
re.compile(r"\(\s*(?:the\s+|each\s+a\s+|together\s+the\s+|hereinafter\s+(?:the\s+)?)?[\"“]([A-Z][A-Za-z0-9&'\- ]{1,60}?)[\"”]\s*\)"),
|
|
165
|
+
re.compile(r"[\"“]([A-Z][A-Za-z0-9&'\- ]{1,60}?)[\"”]\s+(?:means|shall mean|has the meaning|includes|refers to)\b"),
|
|
166
|
+
re.compile(r"\*\*[\"“]?([A-Z][A-Za-z0-9&'\- ]{1,60}?)[\"”]?\*\*\s+(?:means|shall mean|has the meaning)\b"),
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
COMMON = ["Agreement", "Customer", "Client", "Company", "Supplier", "Vendor", "Provider", "Service Provider",
|
|
170
|
+
"Licensee", "Licensor", "Contractor", "Consultant", "Services", "Deliverables", "Software", "Fees",
|
|
171
|
+
"Effective Date", "Term", "Territory", "Confidential Information", "Intellectual Property",
|
|
172
|
+
"Intellectual Property Rights", "Affiliate", "Personal Data", "Products", "Order Form",
|
|
173
|
+
"Statement of Work", "Disclosing Party", "Receiving Party", "Purpose", "Business Day",
|
|
174
|
+
"Commencement Date", "Investor", "Founder", "Employee", "Platform", "Subscription Term",
|
|
175
|
+
"Documentation", "Customer Data", "Work Product", "Background IP", "Licensed Materials"]
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def definitions(lines):
|
|
179
|
+
defs = {}
|
|
180
|
+
for i, line in enumerate(lines, 1):
|
|
181
|
+
for rx in DEF_PATTERNS:
|
|
182
|
+
for m in rx.finditer(line):
|
|
183
|
+
term = m.group(1).strip()
|
|
184
|
+
if len(term) < 2 or term.lower() in ("the", "a", "an"):
|
|
185
|
+
continue
|
|
186
|
+
defs.setdefault(term, []).append(i)
|
|
187
|
+
return defs
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def term_faults(lines, defs):
|
|
191
|
+
out = []
|
|
192
|
+
text = "\n".join(lines)
|
|
193
|
+
for term, where in defs.items():
|
|
194
|
+
places = sorted(set(where))
|
|
195
|
+
if len(places) > 1:
|
|
196
|
+
out.append(("FAIL", places[1], f"“{term}” is defined more than once (lines {', '.join(map(str, places))})"))
|
|
197
|
+
uses = len(re.findall(r"(?<![A-Za-z])" + re.escape(term) + r"(?![A-Za-z])", text))
|
|
198
|
+
if uses <= len(where):
|
|
199
|
+
out.append(("WARN", places[0], f"“{term}” is defined but never used"))
|
|
200
|
+
lower = [n for n, l in enumerate(lines, 1) if n not in places and
|
|
201
|
+
re.search(r"(?<![A-Za-z])" + re.escape(term.lower()) + r"(?![A-Za-z])", l) and len(term.split()) > 1]
|
|
202
|
+
if lower:
|
|
203
|
+
out.append(("WARN", lower[0], f"“{term.lower()}” appears in lower case although “{term}” is a "
|
|
204
|
+
f"defined term (lines {', '.join(map(str, lower[:5]))})"))
|
|
205
|
+
defined_lower = {t.lower() for t in defs}
|
|
206
|
+
for term in COMMON:
|
|
207
|
+
if term.lower() in defined_lower:
|
|
208
|
+
continue
|
|
209
|
+
rx = re.compile(r"(?<=[a-z,;] )(?:the )?" + re.escape(term) + r"(?![A-Za-z])")
|
|
210
|
+
hits = [(n, len(rx.findall(l))) for n, l in enumerate(lines, 1) if rx.search(l)]
|
|
211
|
+
count = sum(c for _, c in hits)
|
|
212
|
+
if count >= 2:
|
|
213
|
+
out.append(("WARN", hits[0][0], f"“{term}” is used as a defined term (capitalised, {count} times) but "
|
|
214
|
+
"never defined"))
|
|
215
|
+
return out
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# ---------- amounts in words and figures ----------
|
|
219
|
+
|
|
220
|
+
UNITS = {w: i for i, w in enumerate("zero one two three four five six seven eight nine ten eleven twelve thirteen "
|
|
221
|
+
"fourteen fifteen sixteen seventeen eighteen nineteen".split())}
|
|
222
|
+
TENS = {w: 10 * i for i, w in enumerate("_ _ twenty thirty forty fifty sixty seventy eighty ninety".split()) if w != "_"}
|
|
223
|
+
SCALES = {"hundred": 100, "thousand": 1000, "lakh": 100000, "lakhs": 100000, "million": 10 ** 6,
|
|
224
|
+
"crore": 10 ** 7, "crores": 10 ** 7, "billion": 10 ** 9}
|
|
225
|
+
WORD = r"(?:zero|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|thirteen|fourteen|fifteen|sixteen|" \
|
|
226
|
+
r"seventeen|eighteen|nineteen|twenty|thirty|forty|fifty|sixty|seventy|eighty|ninety|hundred|thousand|lakhs?|" \
|
|
227
|
+
r"million|crores?|billion|and|point)"
|
|
228
|
+
WORDS_FIG = re.compile(r"\b((?:" + WORD + r")(?:[\s-]+" + WORD + r")*)\b(?:\s+[A-Za-z]+){0,3}?\s*\(\s*([$€£₹]?\s?[\d,]+(?:\.\d+)?)\s*\)", re.I)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def words_to_number(words):
|
|
232
|
+
total, current = 0, 0
|
|
233
|
+
toks = [t for t in re.split(r"[\s-]+", words.lower()) if t and t != "and"]
|
|
234
|
+
if not toks or "point" in toks:
|
|
235
|
+
return None
|
|
236
|
+
for t in toks:
|
|
237
|
+
if t in UNITS:
|
|
238
|
+
current += UNITS[t]
|
|
239
|
+
elif t in TENS:
|
|
240
|
+
current += TENS[t]
|
|
241
|
+
elif t == "hundred":
|
|
242
|
+
current = (current or 1) * 100
|
|
243
|
+
elif t in SCALES:
|
|
244
|
+
total += (current or 1) * SCALES[t]
|
|
245
|
+
current = 0
|
|
246
|
+
else:
|
|
247
|
+
return None
|
|
248
|
+
return total + current
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def amount_faults(lines):
|
|
252
|
+
out = []
|
|
253
|
+
for i, line in enumerate(lines, 1):
|
|
254
|
+
for m in WORDS_FIG.finditer(line):
|
|
255
|
+
words = m.group(1)
|
|
256
|
+
if words.strip().lower() in ("and", "point", "one"):
|
|
257
|
+
if words.strip().lower() != "one":
|
|
258
|
+
continue
|
|
259
|
+
val = words_to_number(words)
|
|
260
|
+
fig = float(re.sub(r"[^\d.]", "", m.group(2)) or "nan")
|
|
261
|
+
if val is None or fig != fig:
|
|
262
|
+
continue
|
|
263
|
+
if abs(val - fig) > 1e-9:
|
|
264
|
+
out.append(("FAIL", i, f"“{m.group(0).strip()}”: the words say {val:,} and the figure says {fig:,.0f}"))
|
|
265
|
+
return out
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
# ---------- dates and notice periods ----------
|
|
269
|
+
|
|
270
|
+
MONTHS = {m: i for i, m in enumerate(["january", "february", "march", "april", "may", "june", "july", "august",
|
|
271
|
+
"september", "october", "november", "december"], 1)}
|
|
272
|
+
DATE1 = re.compile(r"\b(\d{1,2})(?:st|nd|rd|th)?\s+(?:day\s+of\s+)?(" + "|".join(MONTHS) + r"),?\s+(\d{4})\b", re.I)
|
|
273
|
+
DATE2 = re.compile(r"\b(" + "|".join(MONTHS) + r")\s+(\d{1,2})(?:st|nd|rd|th)?,?\s+(\d{4})\b", re.I)
|
|
274
|
+
NOTICE = re.compile(r"\(?(\d{1,3})\)?\s+(business\s+days?|calendar\s+days?|days?|weeks?|months?)[’']?\s*"
|
|
275
|
+
r"(?:(?:prior\s+)?(?:written\s+)?notice|(?:prior\s+to|before)\s+(?:the\s+)?(?:end|expiry|expiration|renewal))",
|
|
276
|
+
re.I)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def date_faults(lines):
|
|
280
|
+
import calendar
|
|
281
|
+
out = []
|
|
282
|
+
for i, line in enumerate(lines, 1):
|
|
283
|
+
for m in list(DATE1.finditer(line)) + list(DATE2.finditer(line)):
|
|
284
|
+
if m.re is DATE1:
|
|
285
|
+
d, mon, y = int(m.group(1)), MONTHS[m.group(2).lower()], int(m.group(3))
|
|
286
|
+
else:
|
|
287
|
+
mon, d, y = MONTHS[m.group(1).lower()], int(m.group(2)), int(m.group(3))
|
|
288
|
+
if not 1 <= d <= calendar.monthrange(y, mon)[1]:
|
|
289
|
+
out.append(("FAIL", i, f"“{m.group(0)}” is not a real date"))
|
|
290
|
+
return out
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def notices(lines, term_end=None):
|
|
294
|
+
found = []
|
|
295
|
+
for i, line in enumerate(lines, 1):
|
|
296
|
+
for m in NOTICE.finditer(line):
|
|
297
|
+
n = {"line": i, "period": f"{m.group(1)} {m.group(2).lower()}", "text": snippet(line, m.start(), 90)}
|
|
298
|
+
before_end = re.search(r"before|prior to|renew|expir|end of", line[m.start():], re.I)
|
|
299
|
+
if term_end and before_end:
|
|
300
|
+
n["notice_by"] = notice_by(term_end, int(m.group(1)), m.group(2).lower()).isoformat()
|
|
301
|
+
found.append(n)
|
|
302
|
+
return found
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def notice_by(end, n, unit):
|
|
306
|
+
"""The last day notice can be given so that n units still remain before `end`."""
|
|
307
|
+
import calendar
|
|
308
|
+
import datetime as dt
|
|
309
|
+
if unit.startswith("business"):
|
|
310
|
+
d, left = end, n
|
|
311
|
+
while left:
|
|
312
|
+
d -= dt.timedelta(days=1)
|
|
313
|
+
if d.weekday() < 5:
|
|
314
|
+
left -= 1
|
|
315
|
+
return d
|
|
316
|
+
if unit.startswith("week"):
|
|
317
|
+
return end - dt.timedelta(weeks=n)
|
|
318
|
+
if unit.startswith("month"):
|
|
319
|
+
y, mth = divmod(end.year * 12 + end.month - 1 - n, 12)
|
|
320
|
+
return end.replace(year=y, month=mth + 1, day=min(end.day, calendar.monthrange(y, mth + 1)[1]))
|
|
321
|
+
return end - dt.timedelta(days=n)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# ---------- output ----------
|
|
325
|
+
|
|
326
|
+
def snippet(line, at, width=60):
|
|
327
|
+
start = max(0, at - 20)
|
|
328
|
+
s = line[start:start + width].strip()
|
|
329
|
+
return ("…" if start else "") + s + ("…" if start + width < len(line) else "")
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def dedupe(items):
|
|
333
|
+
seen, out = set(), []
|
|
334
|
+
for it in items:
|
|
335
|
+
if it not in seen:
|
|
336
|
+
seen.add(it)
|
|
337
|
+
out.append(it)
|
|
338
|
+
return out
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def check(lines, term_end=None):
|
|
342
|
+
nums, apps, numbered = scan(lines)
|
|
343
|
+
defs = definitions(lines)
|
|
344
|
+
findings = (blank_faults(lines) + ref_faults(lines, nums, apps) + numbering_faults(numbered) +
|
|
345
|
+
term_faults(lines, defs) + amount_faults(lines) + date_faults(lines))
|
|
346
|
+
findings = dedupe(findings)
|
|
347
|
+
findings.sort(key=lambda f: (f[0] != "FAIL", f[1]))
|
|
348
|
+
return {"findings": findings, "clauses": len(nums), "appendices": sorted(f"{k[0].title()} {k[1]}" for k in apps),
|
|
349
|
+
"defined_terms": sorted(defs), "notices": notices(lines, term_end)}
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def clause_at(lines, n):
|
|
353
|
+
"""The nearest numbered clause at or above line n, for a reader who has the document open."""
|
|
354
|
+
for i in range(min(n, len(lines)), 0, -1):
|
|
355
|
+
if appendix_at(lines[i - 1]):
|
|
356
|
+
return None
|
|
357
|
+
c = clause_number(lines[i - 1])
|
|
358
|
+
if c:
|
|
359
|
+
return c
|
|
360
|
+
return None
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def report(path, lines, r):
|
|
364
|
+
L = [f"# Contract check: {path}", "",
|
|
365
|
+
f"{r['clauses']} numbered clauses, {len(r['defined_terms'])} defined terms, "
|
|
366
|
+
f"{len(r['appendices'])} schedules or exhibits ({', '.join(r['appendices']) or 'none'}).", ""]
|
|
367
|
+
if not any(f[0] == "FAIL" for f in r["findings"]):
|
|
368
|
+
L.append("PASS no blanks, broken cross-references, repeated clause numbers, doubly defined terms, "
|
|
369
|
+
"mismatched amounts or impossible dates.")
|
|
370
|
+
for level, line, msg in r["findings"]:
|
|
371
|
+
where = f"line {line}" if line else "document"
|
|
372
|
+
c = clause_at(lines, line) if line else None
|
|
373
|
+
if c:
|
|
374
|
+
where += f", clause {c}"
|
|
375
|
+
L.append(f"{level} [{where}] {msg}")
|
|
376
|
+
if r["notices"]:
|
|
377
|
+
L += ["", "Notice periods:"]
|
|
378
|
+
for n in r["notices"]:
|
|
379
|
+
c = clause_at(lines, n["line"])
|
|
380
|
+
by = f" — give notice by {n['notice_by']}" if n.get("notice_by") else ""
|
|
381
|
+
L.append(f"- {n['period']} (line {n['line']}{', clause ' + c if c else ''}){by}: {n['text']}")
|
|
382
|
+
return "\n".join(L) + "\n"
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def main(argv=None):
|
|
386
|
+
ap = argparse.ArgumentParser(description="Check an agreement's mechanics.")
|
|
387
|
+
ap.add_argument("document")
|
|
388
|
+
ap.add_argument("--json", action="store_true")
|
|
389
|
+
ap.add_argument("--term-end", help="YYYY-MM-DD the current term ends, to date each notice deadline")
|
|
390
|
+
args = ap.parse_args(argv)
|
|
391
|
+
term_end = None
|
|
392
|
+
if args.term_end:
|
|
393
|
+
import datetime as dt
|
|
394
|
+
try:
|
|
395
|
+
term_end = dt.date.fromisoformat(args.term_end)
|
|
396
|
+
except ValueError:
|
|
397
|
+
print("FAIL --term-end must be a date as YYYY-MM-DD")
|
|
398
|
+
return 2
|
|
399
|
+
try:
|
|
400
|
+
lines = read_text(args.document)
|
|
401
|
+
except (OSError, UnicodeDecodeError, zipfile.BadZipFile, KeyError, ElementTree.ParseError) as e:
|
|
402
|
+
print(f"FAIL cannot read {args.document}: {e}")
|
|
403
|
+
return 2
|
|
404
|
+
r = check(lines, term_end)
|
|
405
|
+
if args.json:
|
|
406
|
+
print(json.dumps(r, indent=2, ensure_ascii=False))
|
|
407
|
+
else:
|
|
408
|
+
print(report(args.document, lines, r), end="")
|
|
409
|
+
return 1 if any(f[0] == "FAIL" for f in r["findings"]) else 0
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
if __name__ == "__main__":
|
|
413
|
+
sys.exit(main())
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lease-check
|
|
3
|
+
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".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# lease-check
|
|
7
|
+
|
|
8
|
+
A first office lease is often the biggest commitment a young company signs, and founders sign
|
|
9
|
+
it on the headline rent. What hurts is in the detail. The break clause needs six months'
|
|
10
|
+
notice and any unpaid sum voids it. The CPI review compounds three years at once. The rent-free
|
|
11
|
+
months are forfeited if the break is used. Repair obligations hand the tenant the roof. A
|
|
12
|
+
script turns the lease's terms into the numbers: every deadline as a date, the rent each year,
|
|
13
|
+
and what the company has committed to pay up to each exit. The agent reads the lease from the
|
|
14
|
+
tenant's side.
|
|
15
|
+
|
|
16
|
+
## How to respond
|
|
17
|
+
|
|
18
|
+
**These rules come before the steps and outrank them:**
|
|
19
|
+
|
|
20
|
+
- **Lead with the commitment and the exits, in a founder's words.** ✅ *"Signing commits you
|
|
21
|
+
to £113,400 up to the break in January 2030. To use the break you must give notice by 1 July
|
|
22
|
+
2029 and have paid everything, down to the last service-charge invoice, or the break fails
|
|
23
|
+
and you're in to 2031 for another £90,590."* Never mention a script, a JSON file, this
|
|
24
|
+
skill's name, a path inside its folder or a temporary path, and never call your work "the
|
|
25
|
+
check" or "a tool".
|
|
26
|
+
- **Every date and sum comes from the computed terms,** with the clause behind it.
|
|
27
|
+
- **Read for the tenant's side,** and give the wording to ask for on each problem, ready to
|
|
28
|
+
paste into an email to the agent or landlord.
|
|
29
|
+
- **Say what the terms can't price:** an open-market review, a service charge with no cap,
|
|
30
|
+
dilapidations at the end. Name each one, and suggest the ask that bounds it.
|
|
31
|
+
- **Name what is worth a lawyer's hour, and only that.** One or two lines: a personal
|
|
32
|
+
guarantee, full repairing terms on an old building, a break with conditions the landlord
|
|
33
|
+
won't soften, or a lease outside the protection of business-tenancy law where that applies.
|
|
34
|
+
Never refuse, never open with a disclaimer.
|
|
35
|
+
- **Working files stay out of sight.** The terms you extract go in a private temporary folder
|
|
36
|
+
(`mktemp -d`) and are deleted before replying. Never overwrite their lease.
|
|
37
|
+
|
|
38
|
+
1. **Read the lease or heads of terms** and extract: start date, term in months, starting
|
|
39
|
+
rent, how it's paid, rent-free months, stepped rents, each rent review (month, type, any
|
|
40
|
+
cap, collar, upward-only), service charge and any cap, deposit, each break (month, notice,
|
|
41
|
+
who can break, conditions, any penalty), renewal option, notice to leave. Write down the
|
|
42
|
+
clause for each.
|
|
43
|
+
|
|
44
|
+
2. **Write the terms** in the shape of [`templates/terms.json`](templates/terms.json). An
|
|
45
|
+
index-linked review over several years takes `years` so the cap and collar compound the way
|
|
46
|
+
the lease says; check the lease's wording, which differs on this. When the lease names a
|
|
47
|
+
break by its day ("the last day of the third year" is 28 February; "the third anniversary"
|
|
48
|
+
is 1 March), give it as the break's `date`: the notice date follows from it.
|
|
49
|
+
|
|
50
|
+
3. **Run it** on today's date, so passed deadlines are flagged.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
work=$(mktemp -d 2>/dev/null || mktemp -d ./.work.XXXXXX) # a private folder for this run only
|
|
54
|
+
# Claude Code
|
|
55
|
+
python3 "${CLAUDE_SKILL_DIR}/scripts/lease_terms.py" "$work/terms.json" --as-of 2026-11-08
|
|
56
|
+
# Other IDEs (from the skill folder)
|
|
57
|
+
python3 scripts/lease_terms.py "$work/terms.json"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Exit 0 means computed, 1 means a deadline has passed or the terms contradict each other (a
|
|
61
|
+
break after the end, a notice that runs back before the start, rent-free as long as the
|
|
62
|
+
term), and 2 means the input can't be read.
|
|
63
|
+
|
|
64
|
+
4. **Read the lease for the tenant** against [`reference.md`](reference.md): break conditions,
|
|
65
|
+
repair and reinstatement, dilapidations, service charge, alienation (subletting, assignment,
|
|
66
|
+
sharing with a group company), use, alterations, guarantees and rent deposits, insurance,
|
|
67
|
+
and whether the rent-free survives a break. For each problem: clause, why it hurts, wording
|
|
68
|
+
to ask for. Mark severity: 🟥 don't sign as is, 🟧 push back, 🟨 worth asking.
|
|
69
|
+
|
|
70
|
+
5. **Reply in this order:**
|
|
71
|
+
1. **The commitment and the exits:** what signing commits to up to each break and to the
|
|
72
|
+
end, and the notice date for each break.
|
|
73
|
+
2. **Fixes before signing,** most severe first, with wording to ask for.
|
|
74
|
+
3. **Deadlines to diarise,** each as a date with its clause.
|
|
75
|
+
4. **Rent by year,** and what can't be priced yet.
|
|
76
|
+
5. **What to have a lawyer look at,** one or two lines.
|
|
77
|
+
|
|
78
|
+
**Non-interactive runs** (subagent, CI, headless): unsaid terms are left out and named as
|
|
79
|
+
missing, never filled with a typical value. With no start date, term or rent, emit `BLOCKED:
|
|
80
|
+
need the lease (or heads of terms) with its start date, length and rent`.
|
|
81
|
+
|
|
82
|
+
## Useful references in this skill
|
|
83
|
+
|
|
84
|
+
- [`reference.md`](reference.md) — the lease clauses that hurt a small tenant, and what to ask for in each
|
|
85
|
+
- [`templates/terms.json`](templates/terms.json) — the input: a five-year lease with rent-free, a capped CPI review and a conditional break
|
|
86
|
+
- [`scripts/lease_terms.py`](scripts/lease_terms.py) — rent by year through reviews, committed cost to each break and the end, deadlines, contradictions
|
|
87
|
+
- [`examples/first-office.md`](examples/first-office.md) — worked example: heads of terms for a first office, and what the break really costs
|
|
88
|
+
|
|
89
|
+
## Quality bar
|
|
90
|
+
|
|
91
|
+
- **The committed cost to each break and to the end is a sum,** rent and service charge, after rent-free.
|
|
92
|
+
- **Every deadline is a date with its clause,** and a passed one is said plainly.
|
|
93
|
+
- **Index-linked reviews compound the way the lease says,** within its cap and collar, and upward-only is applied.
|
|
94
|
+
- **What can't be priced is named,** with the ask that bounds it.
|
|
95
|
+
- **Every problem has the wording to ask for.**
|
|
96
|
+
- **The reply leads with the commitment and the exits,** with no mention of scripts or files.
|
|
97
|
+
|
|
98
|
+
## When to use this skill
|
|
99
|
+
|
|
100
|
+
- ✅ Heads of terms or a draft lease for a first office, studio, lab or shop
|
|
101
|
+
- ✅ "When can we get out of this lease, and what does it cost to stay till then?"
|
|
102
|
+
- ✅ A rent review letter, or a break date coming up
|
|
103
|
+
- ✅ Comparing two buildings on what each really commits the company to
|
|
104
|
+
|
|
105
|
+
## When NOT to use this skill
|
|
106
|
+
|
|
107
|
+
- ❌ A coworking membership with monthly terms: read its notice period, no schedule needed
|
|
108
|
+
- ❌ Residential tenancies
|
|
109
|
+
- ❌ A dispute with a landlord, or a dilapidations claim already made: get a surveyor or a lawyer
|
|
110
|
+
- ❌ Buying a property
|
|
111
|
+
|
|
112
|
+
## Anti-patterns to avoid
|
|
113
|
+
|
|
114
|
+
- ❌ **Quoting the headline rent as the cost.** Rent-free, reviews and service charge move it; give the committed sum.
|
|
115
|
+
- ❌ **"The break is in year three."** The notice date is what matters, and it's six months earlier.
|
|
116
|
+
- ❌ **Treating a conditional break as certain.** One unpaid invoice can void it.
|
|
117
|
+
- ❌ **Compounding a three-year CPI review as one year,** or ignoring its cap.
|
|
118
|
+
- ❌ **Missing that rent-free is clawed back** if the break is used, when the lease says so.
|
|
119
|
+
- ❌ **"The calculation shows…"** The founder wants the sum and the date.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"query": "Review this office lease before we sign",
|
|
4
|
+
"should_trigger": true
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
"query": "What's the notice date for our lease break clause?",
|
|
8
|
+
"should_trigger": true
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"query": "How much are we committing to on this lease?",
|
|
12
|
+
"should_trigger": true
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"query": "When can we get out of our office lease and what does it cost to stay?",
|
|
16
|
+
"should_trigger": true
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"query": "Review this SaaS agreement before I sign it",
|
|
20
|
+
"should_trigger": false
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"query": "Find us coworking spaces in Manchester",
|
|
24
|
+
"should_trigger": false
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"query": "Should we rent or buy a flat?",
|
|
28
|
+
"should_trigger": false
|
|
29
|
+
}
|
|
30
|
+
]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"skill_name": "lease-check",
|
|
3
|
+
"evals": [
|
|
4
|
+
{
|
|
5
|
+
"id": 1,
|
|
6
|
+
"prompt": "We've been offered heads of terms for our first office (heads-of-terms.md). The agent wants an answer this week. What are we committing to, and when could we get out?",
|
|
7
|
+
"assertions": [
|
|
8
|
+
"Gives the committed cost to the break on 1 January 2030 as about £113,400 (rent after 3 months rent-free plus service charge) and to the end as about £203,990",
|
|
9
|
+
"Gives the break notice deadline as 1 July 2029",
|
|
10
|
+
"Warns that the break's conditions (all sums paid, vacant possession) can make it fail, and gives wording to narrow them",
|
|
11
|
+
"Flags the personal guarantee, the uncapped service charge and full repairing with no schedule of condition, with wording to ask for",
|
|
12
|
+
"Computes the year-3 review at the 4% cap compounded over three years (about £40,495) or explains the range",
|
|
13
|
+
"Does not mention a script or the skill's name, and does not invent lease terms not in the heads of terms"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"id": 2,
|
|
18
|
+
"held_out": "2026-11-08",
|
|
19
|
+
"prompt": "check this lease before I sign - heads-of-terms.md. how much are we committing to and when's the break notice date?",
|
|
20
|
+
"assertions": [
|
|
21
|
+
"Notices that a 5-year term from 1 March 2027 ends on 29 February 2032 (2032 is a leap year), and flags the heads of terms' 'expiring on 1 March 2032' as inconsistent with the stated term",
|
|
22
|
+
"Rent year by year: year 1 GBP 12,000 (6 months rent-free, then 6 x 2,000), year 2 GBP 24,000, years 3, 4 and 5 GBP 26,400 each",
|
|
23
|
+
"Committed to the break (to 28 February 2030): rent 12,000 + 24,000 + 26,400 = GBP 62,400 plus service charge 3 x 3,600 = GBP 10,800, total GBP 73,200 (exclusive of VAT, service charge at the current budget), with the convention stated",
|
|
24
|
+
"Committed to the end of the term: rent 12,000 + 24,000 + 3 x 26,400 = GBP 115,200 plus service charge 5 x 3,600 = GBP 18,000, total GBP 133,200",
|
|
25
|
+
"Break notice must be served on or before 28 August 2029 (6 months before the 28 February 2030 break date), and the answer warns that the break fails if any sum is unpaid or vacant possession is not given",
|
|
26
|
+
"Flags at least two tenant-hostile terms: the director's personal guarantee for the whole term, the uncapped service charge, and full repairing with no schedule of condition",
|
|
27
|
+
"Does not invent a rent deposit, VAT rate, open-market rent review, or other terms not in the heads of terms"
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": 3,
|
|
32
|
+
"held_out": "2026-11-08",
|
|
33
|
+
"prompt": "We're Quokka Bio, a 9-person biotech, and this would be our first lab. Our landlord's agent sent heads-of-agreement.md and wants it signed off this week. Can you work out what we'd be committing to pay to the break and to the end of the term, give me every notice date we need in the calendar, and tell me which clauses I should push back on? Treat all money as ex GST, and use the outgoings estimate as flat for the whole term.",
|
|
34
|
+
"assertions": [
|
|
35
|
+
"Flags the conflict between '3 months rent-free from the commencement date' (rent would start 1 December 2027) and the stated rent commencement date of 1 November 2027 (only 2 months), says which it used and asks for it to be fixed",
|
|
36
|
+
"Gives the CPI reviews as a range using the collar and cap: from 1 September 2028 rent is A$97,920 (2%) to A$100,800 (5%); from 1 September 2029 A$99,878.40 (2% on 2%) to A$105,840 (5% on 5%); the minimum is what is committed",
|
|
37
|
+
"Committed to the 31 August 2029 break using the 3-month rent-free: rent 72,000 (9 months of year 1) + 97,920 = A$169,920 plus outgoings 2 x 14,400 = A$28,800, total A$198,720 at the 2% collar (A$201,600 at the 5% cap), plus a break fee of 3 months' rent of A$24,480 (A$25,200 at cap), so about A$223,200 to A$226,800 to exit at the break; accept figures recomputed consistently on the 2-month reading if the convention is stated",
|
|
38
|
+
"Committed to the end of the term (31 August 2030): rent 72,000 + 97,920 + 99,878.40 = A$269,798.40 plus outgoings 3 x 14,400 = A$43,200, total A$312,998.40 at the collar (A$321,840 at the cap)",
|
|
39
|
+
"Break notice must be given no later than 28 February 2029 (6 months before 31 August 2029; there is no 29 February in 2029); the option to renew can only be exercised between 28 February 2030 (accept 1 March 2030) and 31 May 2030",
|
|
40
|
+
"Flags as hostile to a small tenant at least: the break condition voided by any breach at notice or break date, the upward-only market review on renewal, the uncapped outgoings, and the make-good to base-building condition",
|
|
41
|
+
"Does not invent a CPI figure presented as fact, a guarantor, a fit-out contribution, or other terms not in the heads of agreement"
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
}
|
|
@@ -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,32 @@
|
|
|
1
|
+
# Heads of Terms - Subject to Contract
|
|
2
|
+
|
|
3
|
+
**Property:** Unit 4, Gorse Mill, Canal Street, Halbrook HB3 7QT (ground floor studio, approx. 1,850 sq ft)
|
|
4
|
+
|
|
5
|
+
**Landlord:** Gorse Mill Estates Limited
|
|
6
|
+
|
|
7
|
+
**Tenant:** Fernlight Studio Ltd
|
|
8
|
+
|
|
9
|
+
**Guarantor:** Imogen Marsh (director), personal guarantee of all tenant obligations for the whole term
|
|
10
|
+
|
|
11
|
+
**Term:** 5 years from and including 1 March 2027, expiring on 1 March 2032
|
|
12
|
+
|
|
13
|
+
**Rent:**
|
|
14
|
+
- Years 1 and 2 (1 March 2027 to 28 February 2029): GBP 24,000 per annum exclusive
|
|
15
|
+
- Years 3 to 5 (1 March 2029 to end of term): fixed increase to GBP 26,400 per annum exclusive
|
|
16
|
+
- Payable quarterly in advance on the usual quarter days
|
|
17
|
+
|
|
18
|
+
**Rent-free:** The first 6 months of the term are rent-free. Service charge is payable from the start of the term and is not covered by the rent-free period.
|
|
19
|
+
|
|
20
|
+
**Service charge:** Tenant to pay a fair proportion of the landlord's costs of the estate. Current budget GBP 3,600 per annum. No cap.
|
|
21
|
+
|
|
22
|
+
**Repair:** Full repairing and insuring. Tenant to keep the premises in good and substantial repair and condition and yield up in that condition. No schedule of condition.
|
|
23
|
+
|
|
24
|
+
**Tenant break:** Tenant may terminate the lease at the end of the third year of the term (28 February 2030) by giving the landlord not less than 6 months' prior written notice. The break is conditional on: (i) all rent and other sums due under the lease having been paid in full, and (ii) the tenant giving vacant possession on the break date.
|
|
25
|
+
|
|
26
|
+
**Use:** Design studio and ancillary offices (Class E).
|
|
27
|
+
|
|
28
|
+
**Alienation:** No assignment or subletting of part. Assignment of the whole with landlord's consent, not to be unreasonably withheld, subject to an authorised guarantee agreement.
|
|
29
|
+
|
|
30
|
+
**Security of tenure:** The lease is to be contracted out of the security of tenure provisions of the Landlord and Tenant Act 1954.
|
|
31
|
+
|
|
32
|
+
**Costs:** Each party to bear its own legal costs.
|