@appchy/jarvis 0.1.74 → 0.1.76
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/dist/bin.js +1 -1
- package/dist/ui/app.js +11 -11
- package/harness/harness/align.py +28 -0
- package/harness/harness/config.py +1 -0
- package/harness/presets/appchy/PRESET.md +8 -0
- package/harness/test_work.py +47 -0
- package/harness/work.py +12 -2
- package/package.json +3 -3
package/harness/harness/align.py
CHANGED
|
@@ -17,6 +17,10 @@ from .lint import _feature_ac_ids, feature_ac_levels
|
|
|
17
17
|
#: exists to remove.
|
|
18
18
|
FOREIGN_REPOS: tuple = ()
|
|
19
19
|
|
|
20
|
+
#: This repo's own standards file, relative to the repo root. Set from config
|
|
21
|
+
#: (`spine.standards`); empty when a repo keeps no second ledger.
|
|
22
|
+
STANDARDS: str = ""
|
|
23
|
+
|
|
20
24
|
#: Directories under `work/` that are NOT durable governance. `product/` and
|
|
21
25
|
#: `architecture/` are handled on their own (they are the two sides the
|
|
22
26
|
#: single-feature check compares); the rest here hold work-in-flight or history,
|
|
@@ -156,6 +160,29 @@ def _foreign_qualifier() -> re.Pattern:
|
|
|
156
160
|
return re.compile(r"(?!)")
|
|
157
161
|
alt = "|".join(re.escape(r) for r in FOREIGN_REPOS)
|
|
158
162
|
return re.compile(rf"(?:{alt})\s*[:=]?\s*[`*\[(\"']{{0,3}}$", re.IGNORECASE)
|
|
163
|
+
def _standards_ids(root: Path) -> set:
|
|
164
|
+
"""Ids this repo declares in its standards file, which does not live under `work/`.
|
|
165
|
+
|
|
166
|
+
Without this the check walked the board alone and reported every one of them as
|
|
167
|
+
resolving to nothing: twenty in this repo, twelve in a sibling, all false, all
|
|
168
|
+
cited by live briefs that were reading the rules correctly. They resolve
|
|
169
|
+
perfectly — they are declared at the repo root, in the file the config already
|
|
170
|
+
names.
|
|
171
|
+
|
|
172
|
+
Twenty false errors on top of three real warnings is worse than a wrong number:
|
|
173
|
+
it is how somebody learns the report is noise and stops reading the part that
|
|
174
|
+
was telling them something.
|
|
175
|
+
|
|
176
|
+
`doctor`'s id check reaches for the same pointer, so the two cannot disagree
|
|
177
|
+
about what counts as declared here.
|
|
178
|
+
"""
|
|
179
|
+
from . import config # deferred — `config.apply` imports this module
|
|
180
|
+
if not STANDARDS:
|
|
181
|
+
return set()
|
|
182
|
+
named = config.standards_index({"spine": {"standards": STANDARDS}}, root.parent)
|
|
183
|
+
return {ids.normalise(i) for i, _ in named}
|
|
184
|
+
|
|
185
|
+
|
|
159
186
|
def _align_citations(root: Path) -> list:
|
|
160
187
|
"""A cited id that resolves to nothing."""
|
|
161
188
|
out = []
|
|
@@ -165,6 +192,7 @@ def _align_citations(root: Path) -> list:
|
|
|
165
192
|
defined = set(definition_sites(root))
|
|
166
193
|
for _, _, rules, _ in hosts(root):
|
|
167
194
|
defined |= set(rules)
|
|
195
|
+
defined |= _standards_ids(root)
|
|
168
196
|
foreign = _foreign_qualifier()
|
|
169
197
|
dangling: dict = {}
|
|
170
198
|
for p in live_surfaces(root):
|
|
@@ -623,6 +623,7 @@ def apply(cfg: dict) -> None:
|
|
|
623
623
|
kickoff.SPINE = [s for s in ([cfg["spine"]["standards"]] if cfg["spine"]["standards"]
|
|
624
624
|
else []) + list(cfg["spine"]["conventions"]) if s]
|
|
625
625
|
align.FOREIGN_REPOS = tuple(cfg["ids"]["foreign"])
|
|
626
|
+
align.STANDARDS = cfg["spine"]["standards"] or ""
|
|
626
627
|
registry.DOMAIN_ORDER = tuple(cfg["domains"]["order"])
|
|
627
628
|
# From the SHIPPED set each time, never from whatever the last `apply` left
|
|
628
629
|
# behind. Unioning into the live value made a second call in one process keep the
|
|
@@ -159,6 +159,14 @@ preference — `jarvis work where` resolves it. Tiny mechanics need no sign-off.
|
|
|
159
159
|
from `jarvis work code <region>` overlap and the feature graph, not a lint. A cheap reshape happens in
|
|
160
160
|
the plan; a real trade-off routes through `AskUserQuestion` and lands as a new rule.
|
|
161
161
|
|
|
162
|
+
**A completed brief records what was BELIEVED then; it is not a statement about now.** Anything under
|
|
163
|
+
`complete/` or in a released cut describes the world its author was working in, and the tools it
|
|
164
|
+
asserts have moved since. Quote one only with its date attached, and check any claim about how a
|
|
165
|
+
command behaves against the command before planning on it. Three such claims were found false in one
|
|
166
|
+
evening across two repos — each specific, plausible, recently dated, and sitting in a file whose whole
|
|
167
|
+
job is to be trusted. Nothing catches this class: the sentence points at no file, so no link check
|
|
168
|
+
goes red, and the tree does not know it is a claim.
|
|
169
|
+
|
|
162
170
|
The completion gate enforces all three: a task that changed behaviour is not complete until every
|
|
163
171
|
`AC-nn` it targeted has passing evidence, `<feature>.md` (+ its `state:`), the durable rules and any
|
|
164
172
|
touched briefs match what was actually built. `map_scope {edits}` over the edit set flags governance the
|
package/harness/test_work.py
CHANGED
|
@@ -284,6 +284,29 @@ def test_a_doc_narrating_a_retirement_is_not_a_dangling_citation():
|
|
|
284
284
|
assert not [f for f in align._align_citations(root) if f[1] == "dangling-citation"]
|
|
285
285
|
|
|
286
286
|
|
|
287
|
+
def test_a_standard_declared_at_the_repo_root_is_not_a_dangling_citation():
|
|
288
|
+
# `align` walked `work/` alone, so every id declared in the standards file at
|
|
289
|
+
# the repo ROOT read as resolving to nothing — twenty here, twelve in a
|
|
290
|
+
# sibling, all false and all cited by briefs that were reading the rules
|
|
291
|
+
# correctly. Twenty false errors on three real warnings is how somebody
|
|
292
|
+
# learns the report is noise.
|
|
293
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
294
|
+
root = _citing_repo(tmp, "Written to `S-1`, and also `S-9`.\n")
|
|
295
|
+
(Path(tmp) / "STANDARDS.md").write_text(
|
|
296
|
+
"# Standards\n\n## S-1. Function signatures\n\ntext\n")
|
|
297
|
+
ids.configure("D", ("S",))
|
|
298
|
+
align.STANDARDS = "STANDARDS.md"
|
|
299
|
+
try:
|
|
300
|
+
found = [m for sev, c, m in align._align_citations(root)
|
|
301
|
+
if c == "dangling-citation"]
|
|
302
|
+
finally:
|
|
303
|
+
align.STANDARDS = ""
|
|
304
|
+
assert not any("S-1" in m for m in found), found
|
|
305
|
+
# And the guard: a standards-shaped id the file does not declare is still
|
|
306
|
+
# reported, or this would be an amnesty rather than a lookup.
|
|
307
|
+
assert any("S-9" in m for m in found), found
|
|
308
|
+
|
|
309
|
+
|
|
287
310
|
def test_a_real_dangling_id_still_reports_in_a_file_that_retires_another():
|
|
288
311
|
# The guard on the check above: file-scoped narration must not become a
|
|
289
312
|
# blanket amnesty for every id the document happens to mention.
|
|
@@ -5482,6 +5505,30 @@ def test_an_epic_move_snapshots_the_repo_once_not_once_per_task():
|
|
|
5482
5505
|
assert len(calls) == 1, f"scanned the repo {len(calls)} times for 3 tasks"
|
|
5483
5506
|
|
|
5484
5507
|
|
|
5508
|
+
def test_the_config_is_found_from_a_subdirectory_of_the_repo():
|
|
5509
|
+
# The tree search walks up for a `work/`; the config search stopped at the cwd.
|
|
5510
|
+
# So standing one directory inside a repo found no config and every check ran
|
|
5511
|
+
# in the shipped dialect against the right tree — `align` from the root
|
|
5512
|
+
# reported twenty citations and from `apps/cli/` reported thirteen entirely
|
|
5513
|
+
# different ones. One tree, two answers, decided by where you stood.
|
|
5514
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
5515
|
+
repo = Path(tmp) / "repo"
|
|
5516
|
+
(repo / "work" / "versions").mkdir(parents=True)
|
|
5517
|
+
deep = repo / "src" / "deep"
|
|
5518
|
+
deep.mkdir(parents=True)
|
|
5519
|
+
|
|
5520
|
+
was = Path.cwd()
|
|
5521
|
+
env = {k: os.environ.pop(k) for k in ("CLAUDE_PROJECT_DIR", "WORK_DIR")
|
|
5522
|
+
if k in os.environ}
|
|
5523
|
+
os.chdir(deep)
|
|
5524
|
+
try:
|
|
5525
|
+
assert entry._project_root({}) == repo.resolve(), \
|
|
5526
|
+
"the config search stopped at the cwd instead of finding the repo"
|
|
5527
|
+
finally:
|
|
5528
|
+
os.chdir(was)
|
|
5529
|
+
os.environ.update(env)
|
|
5530
|
+
|
|
5531
|
+
|
|
5485
5532
|
def _migrate(tmp: str, **flags):
|
|
5486
5533
|
"""`migrate` against a fixture, with WORK_DIR pointing at it."""
|
|
5487
5534
|
from harness import migrate as migrate_mod
|
package/harness/work.py
CHANGED
|
@@ -193,7 +193,16 @@ def parse_argv(argv):
|
|
|
193
193
|
def _project_root(flags) -> Path:
|
|
194
194
|
"""The repo the harness is acting on. `--project` wins, then the env, then the
|
|
195
195
|
cwd — the same precedence `find_work_root` uses, so config and tree can never
|
|
196
|
-
disagree about which repo this is.
|
|
196
|
+
disagree about which repo this is.
|
|
197
|
+
|
|
198
|
+
The last step WALKS UP, and that is the half this claimed and did not do. The
|
|
199
|
+
tree search walks up for a `work/`; this stopped at the cwd, so standing one
|
|
200
|
+
directory into a repo found no config and every check ran in the shipped
|
|
201
|
+
dialect against the right tree. Measured here: `align` from the root reported
|
|
202
|
+
twenty citations, and from `apps/cli/` reported thirteen entirely different
|
|
203
|
+
ones, having stopped recognising this repo's own ids altogether. One tree, two
|
|
204
|
+
answers, decided by where the caller happened to be standing.
|
|
205
|
+
"""
|
|
197
206
|
import os
|
|
198
207
|
p = flags.get("project") or os.environ.get("CLAUDE_PROJECT_DIR")
|
|
199
208
|
if p:
|
|
@@ -201,7 +210,8 @@ def _project_root(flags) -> Path:
|
|
|
201
210
|
wd = os.environ.get("WORK_DIR")
|
|
202
211
|
if wd:
|
|
203
212
|
return Path(wd).resolve().parent
|
|
204
|
-
|
|
213
|
+
root, _, cur = locate_work_root()
|
|
214
|
+
return root.parent if root else cur
|
|
205
215
|
|
|
206
216
|
|
|
207
217
|
def main() -> int:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appchy/jarvis",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.76",
|
|
4
4
|
"description": "Jarvis — local AI coding assistant CLI",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"tsup": "^8.5.1",
|
|
57
57
|
"typescript": "^5.7.0",
|
|
58
58
|
"vitest": "^2.1.0",
|
|
59
|
+
"@jarvis/agents": "1.0.0",
|
|
59
60
|
"@jarvis/anthropic": "1.0.0",
|
|
60
61
|
"@jarvis/board": "0.1.0",
|
|
61
62
|
"@jarvis/data": "0.1.0",
|
|
@@ -65,8 +66,7 @@
|
|
|
65
66
|
"@jarvis/types": "1.0.0",
|
|
66
67
|
"@jarvis/typescript-config": "1.0.0",
|
|
67
68
|
"@jarvis/ui": "0.1.0",
|
|
68
|
-
"@jarvis/vitest-config": "1.0.0"
|
|
69
|
-
"@jarvis/agents": "1.0.0"
|
|
69
|
+
"@jarvis/vitest-config": "1.0.0"
|
|
70
70
|
},
|
|
71
71
|
"scripts": {
|
|
72
72
|
"dev": "tsx watch src/bin.ts start --foreground",
|