@appchy/jarvis 0.1.36 → 0.1.39
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/bin/config-change.mjs +1 -1
- package/bin/jarvis.mjs +8 -12
- package/bin/pre-tool-use.mjs +2 -2
- package/bin/session-start.mjs +1 -1
- package/bin/stop.mjs +1 -1
- package/bin/user-prompt-submit.mjs +1 -1
- package/dist/bin.js +10104 -37174
- package/dist/bin.js.map +1 -1
- package/dist/data/backends.mjs +469 -0
- package/dist/data/chunk-7REP35VA.mjs +462 -0
- package/dist/data/chunk-AKQQC5IT.mjs +1224 -0
- package/dist/data/chunk-AYOJSS2F.mjs +72 -0
- package/dist/data/chunk-RRJ6KKYL.mjs +42 -0
- package/dist/data/chunk-YWSWQEJN.mjs +138 -0
- package/dist/data/embedders.mjs +80 -0
- package/dist/data/finders.mjs +35 -0
- package/dist/data/index.mjs +137 -0
- package/dist/data/linkers.mjs +83 -0
- package/dist/data/mcp.mjs +2624 -0
- package/dist/data/persistences.mjs +8 -0
- package/dist/data/rerankers.mjs +52 -0
- package/dist/data/stores.mjs +38 -0
- package/dist/hooks/config-change.js +7 -29
- package/dist/hooks/config-change.js.map +1 -1
- package/dist/hooks/pre-tool-use.js +129 -32
- package/dist/hooks/pre-tool-use.js.map +1 -1
- package/dist/hooks/session-start.js +72 -37
- package/dist/hooks/session-start.js.map +1 -1
- package/dist/hooks/stop.js +140 -32
- package/dist/hooks/stop.js.map +1 -1
- package/dist/hooks/user-prompt-submit.js +7 -29
- package/dist/hooks/user-prompt-submit.js.map +1 -1
- package/harness/assets/architecture-template.md +53 -0
- package/harness/assets/design-template.md +89 -0
- package/harness/assets/domains/architecture.md +43 -0
- package/harness/assets/domains/commercial.md +42 -0
- package/harness/assets/domains/design.md +42 -0
- package/harness/assets/domains/legal.md +43 -0
- package/harness/assets/domains/operations.md +39 -0
- package/harness/assets/domains/product.md +43 -0
- package/harness/assets/domains/quality.md +43 -0
- package/harness/assets/domains/security.md +44 -0
- package/harness/assets/domains/support.md +45 -0
- package/harness/assets/epic-template.md +85 -0
- package/harness/assets/feature-template.md +86 -0
- package/harness/assets/handoff-template.md +26 -0
- package/harness/assets/task-template.md +38 -0
- package/harness/assets/version-template.md +43 -0
- package/harness/harness/__init__.py +7 -0
- package/harness/harness/align.py +582 -0
- package/harness/harness/architecture.py +254 -0
- package/harness/harness/autonomy.py +374 -0
- package/harness/harness/branches.py +408 -0
- package/harness/harness/config.py +1482 -0
- package/harness/harness/coverage.py +199 -0
- package/harness/harness/epic.py +220 -0
- package/harness/harness/events.py +153 -0
- package/harness/harness/extend.py +99 -0
- package/harness/harness/frontmatter.py +218 -0
- package/harness/harness/gate.py +591 -0
- package/harness/harness/generate.py +267 -0
- package/harness/harness/git.py +775 -0
- package/harness/harness/ids.py +140 -0
- package/harness/harness/kickoff.py +231 -0
- package/harness/harness/lint.py +505 -0
- package/harness/harness/model.py +364 -0
- package/harness/harness/peers.py +187 -0
- package/harness/harness/product.py +29 -0
- package/harness/harness/registry.py +382 -0
- package/harness/harness/report.py +227 -0
- package/harness/harness/safety.py +387 -0
- package/harness/harness/scaffold.py +129 -0
- package/harness/harness/shard.py +63 -0
- package/harness/harness/shift.py +348 -0
- package/harness/harness/task.py +507 -0
- package/harness/harness/tree.py +258 -0
- package/harness/harness/version.py +305 -0
- package/harness/harness/wrap.py +217 -0
- package/harness/hooks/guard.py +259 -0
- package/harness/presets/appchy/PRESET.md +717 -0
- package/harness/presets/appchy/references/artifacts.md +539 -0
- package/harness/presets/appchy/references/graph.md +154 -0
- package/harness/presets/appchy/references/operations.md +444 -0
- package/harness/presets/appchy/references/research.md +216 -0
- package/harness/schema/work.config.schema.json +401 -0
- package/harness/test_work.py +5002 -0
- package/harness/work.py +534 -0
- package/package.json +34 -40
- package/bin/config-change.dev.mjs +0 -17
- package/bin/jarvis-dev.mjs +0 -30
- package/bin/pre-tool-use.dev.mjs +0 -23
- package/bin/session-start.dev.mjs +0 -17
- package/bin/stop.dev.mjs +0 -17
- package/bin/user-prompt-submit.dev.mjs +0 -17
- package/dev/bin.js +0 -38718
- package/dev/bin.js.map +0 -1
- package/dev/env.json +0 -1
- package/dev/hooks/config-change.js +0 -110
- package/dev/hooks/config-change.js.map +0 -1
- package/dev/hooks/pre-tool-use.js +0 -120
- package/dev/hooks/pre-tool-use.js.map +0 -1
- package/dev/hooks/session-start.js +0 -115
- package/dev/hooks/session-start.js.map +0 -1
- package/dev/hooks/stop.js +0 -112
- package/dev/hooks/stop.js.map +0 -1
- package/dev/hooks/user-prompt-submit.js +0 -111
- package/dev/hooks/user-prompt-submit.js.map +0 -1
- package/dist/env.json +0 -1
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from .tree import TASK_FM_ORDER
|
|
5
|
+
|
|
6
|
+
#: Keys rendered as `- item` block lists rather than inline `[a, b]`.
|
|
7
|
+
#:
|
|
8
|
+
#: It is not a formatting preference: the inline form splits on commas, and every
|
|
9
|
+
#: key here holds free text a human wrote — a question, what was observed, a
|
|
10
|
+
#: verify command's name and result. `--question "does it ship signed, or later?"`
|
|
11
|
+
#: rendered inline would come back as two list entries, and the answer would be
|
|
12
|
+
#: filed against half a question.
|
|
13
|
+
BLOCK_LISTS = ("sessions", "plans", "asked", "verified", "observed")
|
|
14
|
+
|
|
15
|
+
#: A field this parser reads: a name it models, and whatever follows the colon.
|
|
16
|
+
_KEY = re.compile(r"^([A-Za-z0-9_]+):\s*(.*)$")
|
|
17
|
+
|
|
18
|
+
#: Where one field ends and the next begins — ANY `key:` at column zero, including
|
|
19
|
+
#: the ones `_KEY` declines. It is deliberately wider than what we read: a field we
|
|
20
|
+
#: cannot parse still has to be bounded, or the lines under it get attributed to
|
|
21
|
+
#: whatever came before. A leading `-` or `[` is excluded so a flush-left list item
|
|
22
|
+
#: carrying a colon ("- 2026-01-01 does it ship signed: or later?") stays part of
|
|
23
|
+
#: its list rather than starting a field of its own.
|
|
24
|
+
_FIELD = re.compile(r"^[^\s#\-\[][^:]*:")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def split_frontmatter(text: str):
|
|
28
|
+
"""Return (frontmatter_text, body_text). frontmatter_text is None when the
|
|
29
|
+
file has no `---` fenced block at the top.
|
|
30
|
+
|
|
31
|
+
CRLF is matched as well as LF. A file written on Windows is ordinary YAML that
|
|
32
|
+
every other reader of this format handles; not matching it here meant Python saw
|
|
33
|
+
no frontmatter at all, and a rewrite then returned the file untouched — so a
|
|
34
|
+
`move` on such a task reported success and changed nothing."""
|
|
35
|
+
if not text.startswith("---"):
|
|
36
|
+
return None, text
|
|
37
|
+
m = re.match(r"^---\r?\n(.*?)\r?\n---\r?\n?", text, re.DOTALL)
|
|
38
|
+
if not m:
|
|
39
|
+
return None, text
|
|
40
|
+
return m.group(1), text[m.end():]
|
|
41
|
+
def _eol(text: str) -> str:
|
|
42
|
+
"""The line ending the file already uses.
|
|
43
|
+
|
|
44
|
+
Reading CRLF correctly and writing LF back is still every line rewritten for
|
|
45
|
+
whoever produced it, so what comes out matches what went in."""
|
|
46
|
+
return "\r\n" if text.startswith("---\r\n") else "\n"
|
|
47
|
+
def _inline(raw: str) -> list:
|
|
48
|
+
"""`[a, b]` as a list. Splits on every comma and ignores quoting — narrower than
|
|
49
|
+
YAML, and left that way on purpose: widening it changes how existing values read,
|
|
50
|
+
which is a separate question from whether a write destroys them."""
|
|
51
|
+
return [p.strip() for p in raw[1:-1].strip().split(",") if p.strip()]
|
|
52
|
+
def _bullets(value) -> list:
|
|
53
|
+
"""A block list's item lines. One writer of the ` - ` shape, so the two places
|
|
54
|
+
that emit one cannot drift apart on the indent."""
|
|
55
|
+
return [f" - {item}" for item in as_list(value)]
|
|
56
|
+
#: A field's value when its shape is not one this parser reads. It is a value rather
|
|
57
|
+
#: than a missing key so the renderer still knows the field is THERE — which is what
|
|
58
|
+
#: stops a mutation writing a second copy of a key the file already has.
|
|
59
|
+
KEPT = object()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _name(line: str) -> str:
|
|
63
|
+
"""A field's key as written, whether or not this parser can read its value."""
|
|
64
|
+
return line.split(":", 1)[0].strip()
|
|
65
|
+
def _field(block: list):
|
|
66
|
+
"""One field, as (name, value, source_lines).
|
|
67
|
+
|
|
68
|
+
`value` is `KEPT` when the field's shape is not one this parser reads — a dash in
|
|
69
|
+
the name, a nested map, an unterminated list. Those carry their source lines
|
|
70
|
+
instead, because the alternative to understanding a field is keeping it, never
|
|
71
|
+
dropping it."""
|
|
72
|
+
m = _KEY.match(block[0])
|
|
73
|
+
# Blank and comment lines are nobody's content. Counting them as a field's value
|
|
74
|
+
# made `owner: x` followed by a `# note` unreadable, which the plainer parser
|
|
75
|
+
# before this one got right.
|
|
76
|
+
rest = [line for line in block[1:]
|
|
77
|
+
if line.strip() and not line.lstrip().startswith("#")]
|
|
78
|
+
if not m:
|
|
79
|
+
return _name(block[0]), KEPT, block
|
|
80
|
+
key, raw = m.group(1), m.group(2).strip()
|
|
81
|
+
|
|
82
|
+
# The value as one line, wherever it started. A list prettier wrapped across
|
|
83
|
+
# lines is the same list — and it may open on the key's own line or on the next
|
|
84
|
+
# one, which is why the join happens before anything is decided rather than
|
|
85
|
+
# once per entry shape.
|
|
86
|
+
joined = " ".join(([raw] if raw else []) + [line.strip() for line in rest])
|
|
87
|
+
|
|
88
|
+
if joined.startswith("["):
|
|
89
|
+
return (key, _inline(joined), block) if joined.endswith("]") else (key, KEPT, block)
|
|
90
|
+
if raw:
|
|
91
|
+
# A scalar with lines under it is a shape we do not read — a folded block,
|
|
92
|
+
# most likely — so it is kept whole rather than half-understood.
|
|
93
|
+
return (key, raw, block) if not rest else (key, KEPT, block)
|
|
94
|
+
if not rest:
|
|
95
|
+
return key, "", block
|
|
96
|
+
if all(re.match(r"^\s*-\s+", line) for line in rest):
|
|
97
|
+
# Any indent, zero included. `sessions:` with its items at column zero is
|
|
98
|
+
# valid YAML that a person writes by hand, and demanding leading whitespace
|
|
99
|
+
# read it as empty — which then deleted it.
|
|
100
|
+
return key, [line.strip()[1:].strip() for line in rest], block
|
|
101
|
+
return key, KEPT, block
|
|
102
|
+
def _fields(fm: str):
|
|
103
|
+
"""Walk the frontmatter one field at a time. Everything between a `key:` at
|
|
104
|
+
column zero and the next one belongs to that field."""
|
|
105
|
+
lines = fm.splitlines()
|
|
106
|
+
i = 0
|
|
107
|
+
while i < len(lines):
|
|
108
|
+
if not _FIELD.match(lines[i]):
|
|
109
|
+
i += 1
|
|
110
|
+
continue
|
|
111
|
+
start = i
|
|
112
|
+
i += 1
|
|
113
|
+
while i < len(lines) and not _FIELD.match(lines[i]):
|
|
114
|
+
i += 1
|
|
115
|
+
yield _field(lines[start:i])
|
|
116
|
+
def parse_frontmatter(text: str) -> dict:
|
|
117
|
+
"""Parse the controlled subset of YAML used in frontmatter:
|
|
118
|
+
`key: scalar`, `key: []`, `key: [a, b]` (wrapped or not), and block lists of
|
|
119
|
+
`- item`. A field outside that subset is absent rather than empty — an empty
|
|
120
|
+
value would be indistinguishable from one somebody wrote."""
|
|
121
|
+
fm, _ = split_frontmatter(text)
|
|
122
|
+
if fm is None:
|
|
123
|
+
return {}
|
|
124
|
+
return {k: v for k, v, _ in _fields(fm) if v is not KEPT}
|
|
125
|
+
def render_frontmatter(data: dict, order=TASK_FM_ORDER, kept=None, eol="\n") -> str:
|
|
126
|
+
"""Serialize a frontmatter dict back to a `---` fenced block. Known keys are
|
|
127
|
+
emitted in `order`; the `BLOCK_LISTS` keys are rendered as block lists. `kept`
|
|
128
|
+
maps a field the parser could not read to its source lines, put back as they
|
|
129
|
+
were found."""
|
|
130
|
+
kept = kept or {}
|
|
131
|
+
keys = [k for k in order if k in data]
|
|
132
|
+
keys += [k for k in data if k not in order]
|
|
133
|
+
lines = []
|
|
134
|
+
for k in keys:
|
|
135
|
+
# A field nobody could read is not a field nobody has. Rendering this one
|
|
136
|
+
# from `data` would write a SECOND `verified:` into the file — the mutation
|
|
137
|
+
# appended to a list it was told was empty — and the first would then be
|
|
138
|
+
# invisible to every later read while its bytes sat there looking fine.
|
|
139
|
+
if k in kept:
|
|
140
|
+
continue
|
|
141
|
+
v = data[k]
|
|
142
|
+
if k in BLOCK_LISTS:
|
|
143
|
+
# A scalar here is a person writing `plans: plan.md`, which is the same
|
|
144
|
+
# thing as a one-item list and used to be dropped for not being one.
|
|
145
|
+
if as_list(v):
|
|
146
|
+
lines.append(f"{k}:")
|
|
147
|
+
lines += _bullets(v)
|
|
148
|
+
continue
|
|
149
|
+
if isinstance(v, list):
|
|
150
|
+
lines.append(f"{k}: [{', '.join(v)}]")
|
|
151
|
+
else:
|
|
152
|
+
lines.append(f"{k}: {v}")
|
|
153
|
+
for name, block in kept.items():
|
|
154
|
+
lines += block
|
|
155
|
+
# Whatever the mutation added lands under the lines it could not read, so a
|
|
156
|
+
# session or a verification is still recorded against a field this parser
|
|
157
|
+
# will go on declining to understand.
|
|
158
|
+
if name in BLOCK_LISTS:
|
|
159
|
+
lines += _bullets(data.get(name))
|
|
160
|
+
return "---" + eol + eol.join(lines) + eol + "---" + eol
|
|
161
|
+
def rewrite_frontmatter(text: str, mutate, order=TASK_FM_ORDER) -> str:
|
|
162
|
+
"""Apply `mutate(data)` to the parsed frontmatter and re-render the file."""
|
|
163
|
+
fm, body = split_frontmatter(text)
|
|
164
|
+
if fm is None:
|
|
165
|
+
return text
|
|
166
|
+
fields = list(_fields(fm))
|
|
167
|
+
data = {k: v for k, v, _ in fields if v is not KEPT}
|
|
168
|
+
kept = {k: block for k, v, block in fields if v is KEPT}
|
|
169
|
+
mutate(data)
|
|
170
|
+
return render_frontmatter(data, order, kept, _eol(text)) + body
|
|
171
|
+
def read_item(md: Path) -> str:
|
|
172
|
+
"""A work file's text, with the line endings it actually has on disk.
|
|
173
|
+
|
|
174
|
+
`Path.read_text()` translates CRLF to LF on the way in, and `write_text` does not
|
|
175
|
+
put it back — so a file written on Windows came back LF, was written LF, and
|
|
176
|
+
every line of it changed on a command asked to set one field. Nothing was lost
|
|
177
|
+
that a reader would notice, which is what made it worth closing: a diff touching
|
|
178
|
+
the whole file is one nobody reviews."""
|
|
179
|
+
with open(md, "r", encoding="utf-8", newline="") as f:
|
|
180
|
+
return f.read()
|
|
181
|
+
def write_item(md: Path, text: str) -> None:
|
|
182
|
+
"""The other half of `read_item`: write the endings the text already carries,
|
|
183
|
+
rather than the ones this platform prefers."""
|
|
184
|
+
with open(md, "w", encoding="utf-8", newline="") as f:
|
|
185
|
+
f.write(text)
|
|
186
|
+
def rewrite_file(md: Path, mutate, order=TASK_FM_ORDER) -> None:
|
|
187
|
+
"""`rewrite_frontmatter` over a file in place. The one door onto a board file's
|
|
188
|
+
frontmatter, so what it guarantees is guaranteed once rather than at thirteen
|
|
189
|
+
call sites that each read and wrote for themselves."""
|
|
190
|
+
write_item(md, rewrite_frontmatter(read_item(md), mutate, order))
|
|
191
|
+
def unquote(value: str) -> str:
|
|
192
|
+
"""A scalar with its surrounding quotes taken off.
|
|
193
|
+
|
|
194
|
+
`_inline` splits on commas and leaves quoting alone, deliberately — widening it
|
|
195
|
+
would change how every existing value reads. So a field authored in the quoted
|
|
196
|
+
JSON style (`paths: ["a", "b"]`, `retired: ["x=y"]`) arrives with the quote
|
|
197
|
+
characters part of the string, and every reader of one has to take them off. This
|
|
198
|
+
is that, once, beside the parser that hands them over.
|
|
199
|
+
|
|
200
|
+
Only a MATCHED pair: a lone quote is a character somebody typed, not quoting, and
|
|
201
|
+
stripping it would quietly rewrite the value.
|
|
202
|
+
"""
|
|
203
|
+
text = str(value).strip()
|
|
204
|
+
for quote in ('"', "'"):
|
|
205
|
+
if len(text) >= 2 and text.startswith(quote) and text.endswith(quote):
|
|
206
|
+
return text[1:-1]
|
|
207
|
+
return text
|
|
208
|
+
def as_list(value) -> list:
|
|
209
|
+
if isinstance(value, list):
|
|
210
|
+
return value
|
|
211
|
+
return [value] if value else []
|
|
212
|
+
def title_of(md: Path) -> str:
|
|
213
|
+
"""The title — the first `# ` heading in the body."""
|
|
214
|
+
body = split_frontmatter(md.read_text())[1]
|
|
215
|
+
for line in body.splitlines():
|
|
216
|
+
if line.startswith("# "):
|
|
217
|
+
return line[2:].strip()
|
|
218
|
+
return md.parent.name
|