@appchy/jarvis 0.1.80 → 0.1.81
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/harness/harness/gate.py +6 -1
- package/harness/harness/git.py +38 -0
- package/harness/test_work.py +89 -0
- package/package.json +3 -3
package/dist/bin.js
CHANGED
|
@@ -10110,7 +10110,7 @@ import { createRequire as createRequire2 } from "module";
|
|
|
10110
10110
|
var _require = createRequire2(import.meta.url);
|
|
10111
10111
|
var VERSION2 = _require("../package.json").version ?? "0.0.0";
|
|
10112
10112
|
var IS_DEV = String(_require("../package.json").name ?? "").endsWith("-dev");
|
|
10113
|
-
var SHA = "
|
|
10113
|
+
var SHA = "fa00fb0";
|
|
10114
10114
|
var BUILT = "2026-09-11";
|
|
10115
10115
|
var BUILD = SHA ?? "source";
|
|
10116
10116
|
var BUILD_LABEL = `${SHA ? `${VERSION2} (${SHA}${BUILT ? ` ${BUILT}` : ""})` : `${VERSION2} (source)`}${IS_DEV ? " \u2014 development build" : ""}`;
|
package/harness/harness/gate.py
CHANGED
|
@@ -624,7 +624,12 @@ def gate(root, task, accept: str = "", owner: str = "") -> list:
|
|
|
624
624
|
# The harness names the files and stops there: committing somebody's source
|
|
625
625
|
# is a far larger claim on their repo than committing the board, and a board
|
|
626
626
|
# write deliberately leaves a session's own code alone (founder, 2026-09-10).
|
|
627
|
-
|
|
627
|
+
#
|
|
628
|
+
# Scoped to what this task is answerable for, because a checkout is shared
|
|
629
|
+
# and the unscoped version means nobody completes anything until everybody
|
|
630
|
+
# commits. Unclaimed paths, and a task that declares no regions, stay strict.
|
|
631
|
+
loose = (git.uncommitted_mine(root.parent, root, list(task.code or []))
|
|
632
|
+
if git.enabled() else [])
|
|
628
633
|
if loose:
|
|
629
634
|
shown = ", ".join(loose[:5])
|
|
630
635
|
more = f" and {len(loose) - 5} more" if len(loose) > 5 else ""
|
package/harness/harness/git.py
CHANGED
|
@@ -136,6 +136,44 @@ def uncommitted_code(repo) -> list:
|
|
|
136
136
|
return sorted(found)
|
|
137
137
|
|
|
138
138
|
|
|
139
|
+
def uncommitted_mine(repo, root, regions: list) -> list:
|
|
140
|
+
"""Of the uncommitted code, what THIS task is answerable for.
|
|
141
|
+
|
|
142
|
+
**`moved_only_elsewhere` already decided this question for commits**, and the
|
|
143
|
+
two have to agree. That one says a commit outside your `code:` regions does not
|
|
144
|
+
invalidate your evidence, because a suite failure over there only warns — and
|
|
145
|
+
holding that while letting a merely DIRTY file over there refuse you is the same
|
|
146
|
+
inconsistency, read one step earlier.
|
|
147
|
+
|
|
148
|
+
It is not hypothetical. A session finishing harness work was blocked by a peer's
|
|
149
|
+
19 uncommitted UI files in a shared checkout, none of them in the region its
|
|
150
|
+
task declared. In a checkout two people share, the unscoped rule means nobody
|
|
151
|
+
can complete anything until everybody commits.
|
|
152
|
+
|
|
153
|
+
**Unclaimed is always yours, and that is the whole safety of it.**
|
|
154
|
+
`system_for_path` answers `None` both for a file nothing declares and for a repo
|
|
155
|
+
whose systems declare nothing, and from here the two are indistinguishable.
|
|
156
|
+
Reading either as *not my business* would make the check vacuous in exactly the
|
|
157
|
+
repos that never filled in `paths:`. A task that declares no `code:` of its own
|
|
158
|
+
is answerable for all of it, for the same reason: it has not said what it
|
|
159
|
+
touches, so it cannot say a file is somebody else's.
|
|
160
|
+
"""
|
|
161
|
+
loose = uncommitted_code(repo)
|
|
162
|
+
if not loose or not regions:
|
|
163
|
+
return loose
|
|
164
|
+
from .registry import scan_systems, system_for_path
|
|
165
|
+
systems = scan_systems(root)
|
|
166
|
+
if not systems:
|
|
167
|
+
return loose
|
|
168
|
+
mine = {r for r in regions if r}
|
|
169
|
+
kept = []
|
|
170
|
+
for name in loose:
|
|
171
|
+
owner = system_for_path(root, name, systems)
|
|
172
|
+
if owner is None or (mine & set(owner.code)):
|
|
173
|
+
kept.append(name)
|
|
174
|
+
return kept
|
|
175
|
+
|
|
176
|
+
|
|
139
177
|
def machine() -> str:
|
|
140
178
|
"""Which machine acted. `WORK_MACHINE` where something knows a better name for
|
|
141
179
|
this box than its hostname — a daemon that already has an identity for it —
|
package/harness/test_work.py
CHANGED
|
@@ -3634,6 +3634,95 @@ def test_partly_committed_code_is_still_uncommitted_code():
|
|
|
3634
3634
|
config.apply(config.DEFAULTS)
|
|
3635
3635
|
|
|
3636
3636
|
|
|
3637
|
+
def test_a_peers_uncommitted_work_in_another_system_does_not_block_you():
|
|
3638
|
+
# The mirror of `moved_only_elsewhere`, and it has to agree with it: if a
|
|
3639
|
+
# COMMIT outside your regions cannot invalidate your evidence, a merely DIRTY
|
|
3640
|
+
# file outside them cannot refuse you either. Holding both at once is the
|
|
3641
|
+
# inconsistency, read one step earlier.
|
|
3642
|
+
#
|
|
3643
|
+
# Measured rather than imagined: the session that built the unscoped version
|
|
3644
|
+
# was blocked by a peer's 19 uncommitted UI files, none in the region its task
|
|
3645
|
+
# declared. In a shared checkout that rule means nobody completes anything
|
|
3646
|
+
# until everybody commits.
|
|
3647
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
3648
|
+
try:
|
|
3649
|
+
repo = _git_repo(tmp, push=False)
|
|
3650
|
+
arch = repo / "work" / "architecture"
|
|
3651
|
+
arch.mkdir(parents=True, exist_ok=True)
|
|
3652
|
+
(arch / "board.md").write_text(
|
|
3653
|
+
"---\ntype: system\ncode: [data]\npaths: [packages/board]\n"
|
|
3654
|
+
"depends_on: []\n---\n\n# Board\n")
|
|
3655
|
+
(arch / "screen.md").write_text(
|
|
3656
|
+
"---\ntype: system\ncode: [web]\npaths: [apps/web]\n"
|
|
3657
|
+
"depends_on: []\n---\n\n# Screen\n")
|
|
3658
|
+
for f in ("packages/board/store.ts", "apps/web/page.tsx", "scripts/loose.py"):
|
|
3659
|
+
(repo / f).parent.mkdir(parents=True, exist_ok=True)
|
|
3660
|
+
(repo / f).write_text("first\n")
|
|
3661
|
+
_provable_task(repo) # commits the board AND the code above
|
|
3662
|
+
|
|
3663
|
+
root = repo / "work"
|
|
3664
|
+
# The task says what it touches, which is what earns it the narrowing.
|
|
3665
|
+
brief = root / "versions" / "01-a-cut" / "an-epic" / "in-progress" / "alpha" / "task.md"
|
|
3666
|
+
brief.write_text(brief.read_text().replace(
|
|
3667
|
+
"priority: P0", "priority: P0\ncode: [data]"))
|
|
3668
|
+
_git(repo, "add", "-A")
|
|
3669
|
+
_git(repo, "commit", "-qm", "alpha declares its region")
|
|
3670
|
+
|
|
3671
|
+
with _work_dir(str(root)) as work_root:
|
|
3672
|
+
old, gate.VERIFY = gate.VERIFY, {"tests": "true"}
|
|
3673
|
+
try:
|
|
3674
|
+
assert gate.cmd_verify({"task": "alpha"}) == 0
|
|
3675
|
+
|
|
3676
|
+
# A PEER IS MID-EDIT IN ANOTHER SYSTEM. You can still finish.
|
|
3677
|
+
(repo / "apps/web/page.tsx").write_text("a peer, mid-thought\n")
|
|
3678
|
+
assert gate.gate(work_root, model.locate(work_root, "alpha")) == []
|
|
3679
|
+
|
|
3680
|
+
# YOUR OWN REGION, UNCOMMITTED. That is the thing this catches.
|
|
3681
|
+
(repo / "packages/board/store.ts").write_text("the fix, uncommitted\n")
|
|
3682
|
+
reasons = gate.gate(work_root, model.locate(work_root, "alpha"))
|
|
3683
|
+
assert any("packages/board/store.ts" in r for r in reasons), reasons
|
|
3684
|
+
assert not any("apps/web/page.tsx" in r for r in reasons), reasons
|
|
3685
|
+
|
|
3686
|
+
# A PATH NOTHING CLAIMS IS ALWAYS YOURS. The safety property: a
|
|
3687
|
+
# repo that never filled in `paths:` must not get a gate that
|
|
3688
|
+
# passes everything, which is worse than the noise it replaces.
|
|
3689
|
+
_git(repo, "checkout", "--", "packages/board/store.ts")
|
|
3690
|
+
(repo / "scripts/loose.py").write_text("claimed by no system\n")
|
|
3691
|
+
reasons = gate.gate(work_root, model.locate(work_root, "alpha"))
|
|
3692
|
+
assert any("scripts/loose.py" in r for r in reasons), reasons
|
|
3693
|
+
finally:
|
|
3694
|
+
gate.VERIFY = old
|
|
3695
|
+
finally:
|
|
3696
|
+
config.apply(config.DEFAULTS)
|
|
3697
|
+
|
|
3698
|
+
|
|
3699
|
+
def test_a_task_that_declares_no_region_is_answerable_for_all_of_it():
|
|
3700
|
+
# It has not said what it touches, so it cannot say a file is somebody else's.
|
|
3701
|
+
# The same reading `moved_only_elsewhere` gives an empty `regions`.
|
|
3702
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
3703
|
+
try:
|
|
3704
|
+
repo = _git_repo(tmp, push=False)
|
|
3705
|
+
arch = repo / "work" / "architecture"
|
|
3706
|
+
arch.mkdir(parents=True, exist_ok=True)
|
|
3707
|
+
(arch / "screen.md").write_text(
|
|
3708
|
+
"---\ntype: system\ncode: [web]\npaths: [apps/web]\n"
|
|
3709
|
+
"depends_on: []\n---\n\n# Screen\n")
|
|
3710
|
+
(repo / "apps/web").mkdir(parents=True)
|
|
3711
|
+
(repo / "apps/web/page.tsx").write_text("first\n")
|
|
3712
|
+
_provable_task(repo)
|
|
3713
|
+
with _work_dir(str(repo / "work")) as root:
|
|
3714
|
+
old, gate.VERIFY = gate.VERIFY, {"tests": "true"}
|
|
3715
|
+
try:
|
|
3716
|
+
assert gate.cmd_verify({"task": "alpha"}) == 0
|
|
3717
|
+
(repo / "apps/web/page.tsx").write_text("changed\n")
|
|
3718
|
+
reasons = gate.gate(root, model.locate(root, "alpha"))
|
|
3719
|
+
assert any("apps/web/page.tsx" in r for r in reasons), reasons
|
|
3720
|
+
finally:
|
|
3721
|
+
gate.VERIFY = old
|
|
3722
|
+
finally:
|
|
3723
|
+
config.apply(config.DEFAULTS)
|
|
3724
|
+
|
|
3725
|
+
|
|
3637
3726
|
def test_a_repo_that_never_asked_for_git_completes_exactly_as_before():
|
|
3638
3727
|
# The harness is shared with repos that opted out of all of this. Refusing on
|
|
3639
3728
|
# their working tree would be a new claim on somebody who asked for none.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appchy/jarvis",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.81",
|
|
4
4
|
"description": "Jarvis — local AI coding assistant CLI",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -57,15 +57,15 @@
|
|
|
57
57
|
"typescript": "^5.7.0",
|
|
58
58
|
"vitest": "^2.1.0",
|
|
59
59
|
"@jarvis/agents": "1.0.0",
|
|
60
|
-
"@jarvis/board": "0.1.0",
|
|
61
60
|
"@jarvis/anthropic": "1.0.0",
|
|
61
|
+
"@jarvis/board": "0.1.0",
|
|
62
62
|
"@jarvis/data": "0.1.0",
|
|
63
63
|
"@jarvis/errors": "1.0.0",
|
|
64
64
|
"@jarvis/logger": "1.0.0",
|
|
65
65
|
"@jarvis/rpc": "1.0.0",
|
|
66
66
|
"@jarvis/types": "1.0.0",
|
|
67
|
-
"@jarvis/ui": "0.1.0",
|
|
68
67
|
"@jarvis/typescript-config": "1.0.0",
|
|
68
|
+
"@jarvis/ui": "0.1.0",
|
|
69
69
|
"@jarvis/vitest-config": "1.0.0"
|
|
70
70
|
},
|
|
71
71
|
"scripts": {
|