@appchy/jarvis 0.1.44 → 0.1.46
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 +55 -33
- package/dist/bin.js.map +1 -1
- package/dist/data/backends.mjs +2 -2
- package/dist/data/{chunk-AKQQC5IT.mjs → chunk-AA75N6NR.mjs} +3 -3
- package/dist/data/{chunk-7REP35VA.mjs → chunk-LYW75USL.mjs} +9 -0
- package/dist/data/embedders.mjs +10 -49
- package/dist/data/index.mjs +2 -2
- package/dist/data/labelers.mjs +107 -0
- package/dist/data/linkers.mjs +1 -1
- package/dist/data/mcp.mjs +2 -2
- package/dist/data/stores.mjs +2 -2
- package/dist/hooks/config-change.js.map +1 -1
- package/dist/hooks/pre-tool-use.js +6 -4
- package/dist/hooks/pre-tool-use.js.map +1 -1
- package/dist/hooks/session-start.js +5 -4
- package/dist/hooks/session-start.js.map +1 -1
- package/dist/hooks/stop.js +5 -6
- package/dist/hooks/stop.js.map +1 -1
- package/dist/hooks/user-prompt-submit.js.map +1 -1
- package/harness/harness/gate.py +85 -16
- package/harness/harness/git.py +28 -37
- package/harness/test_work.py +126 -10
- package/package.json +1 -1
- package/dist/data/rerankers.mjs +0 -52
package/harness/harness/git.py
CHANGED
|
@@ -34,9 +34,10 @@ paths say.
|
|
|
34
34
|
|
|
35
35
|
**What a commit contains, exactly.** Only the configured paths, committed with a
|
|
36
36
|
pathspec, so a session's unrelated staged code is neither swept in nor disturbed.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
**And the retry never touches the working tree.** A rejected push rebases onto the
|
|
38
|
+
new tip with autostash explicitly OFF, so git declines to rebase over uncommitted
|
|
39
|
+
work rather than moving it aside; the board commit stays local and `sync` sends it.
|
|
40
|
+
Nothing here stashes, resets or restores a session's own edits.
|
|
40
41
|
"""
|
|
41
42
|
import contextlib
|
|
42
43
|
import json
|
|
@@ -539,7 +540,9 @@ def _push(repo) -> tuple:
|
|
|
539
540
|
|
|
540
541
|
A rebase that hits a conflict is aborted rather than left half-done: a session
|
|
541
542
|
dropped into a conflicted rebase it did not ask for cannot get on with its work,
|
|
542
|
-
and the commit is already safe.
|
|
543
|
+
and the commit is already safe. For the same reason it rebases with autostash
|
|
544
|
+
OFF, so a tree with uncommitted work in it stops the rebase instead of having
|
|
545
|
+
that work moved aside — the push waits, and nothing of the session's own moves.
|
|
543
546
|
"""
|
|
544
547
|
remote = GIT["remote"]
|
|
545
548
|
why = ""
|
|
@@ -557,16 +560,18 @@ def _push(repo) -> tuple:
|
|
|
557
560
|
break
|
|
558
561
|
if attempt == _ATTEMPTS - 1:
|
|
559
562
|
break
|
|
560
|
-
|
|
561
|
-
|
|
563
|
+
# `autoStash=false` is SET rather than left unsaid, because unsaid means
|
|
564
|
+
# whatever the machine's own git config says — and a machine that turned
|
|
565
|
+
# autostash on globally would go on moving a session's edits aside, which
|
|
566
|
+
# is the behaviour this explicitly refuses. A dirty tree makes git decline
|
|
567
|
+
# the rebase, and declining is the whole point: the board commit is already
|
|
568
|
+
# made and safe, so the only thing left to lose here is the session's own
|
|
569
|
+
# uncommitted work, and nothing may move that without being asked.
|
|
570
|
+
code, _, rebase_err = _git(repo, "-c", "rebase.autoStash=false", "pull",
|
|
562
571
|
"--rebase", remote, timeout=_NET_TIMEOUT)
|
|
563
572
|
if code != 0:
|
|
564
573
|
_git(repo, "rebase", "--abort")
|
|
565
|
-
why =
|
|
566
|
-
break
|
|
567
|
-
stranded = _unstash(repo, pre.strip())
|
|
568
|
-
if stranded:
|
|
569
|
-
why = stranded
|
|
574
|
+
why = _why_no_rebase(rebase_err)
|
|
570
575
|
break
|
|
571
576
|
from .tree import cli
|
|
572
577
|
|
|
@@ -574,34 +579,20 @@ def _push(repo) -> tuple:
|
|
|
574
579
|
f"`{cli()} sync` sends it when you can reach {remote}.")
|
|
575
580
|
|
|
576
581
|
|
|
577
|
-
def
|
|
578
|
-
"""
|
|
579
|
-
|
|
580
|
-
`pull --rebase` exits **0** when the rebase itself lands and only the autostash
|
|
581
|
-
pop conflicts, so without this the caller reads success: the push goes out and
|
|
582
|
-
the session is told the write was pushed, while its working tree is left holding
|
|
583
|
-
conflict markers it did not ask for and its own uncommitted edits sit in a stash
|
|
584
|
-
nobody mentioned. Going back to where the pull started puts those edits back
|
|
585
|
-
where their author left them, which is the same promise the abort above makes.
|
|
582
|
+
def _why_no_rebase(err: str) -> str:
|
|
583
|
+
"""Why the rebase onto the moved branch did not run, in the caller's terms.
|
|
586
584
|
|
|
587
|
-
The
|
|
588
|
-
|
|
585
|
+
The common case is not a conflict at all: the branch moved while the session had
|
|
586
|
+
uncommitted work open, and git declines to rebase over it. That reads as a scary
|
|
587
|
+
failure and is an ordinary one, so it is named separately and says what to do —
|
|
588
|
+
the board commit is already in git, and only the push is waiting.
|
|
589
589
|
"""
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
return ""
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
# same false report this whole function exists to stop.
|
|
597
|
-
code, _, err = _locking(repo, "reset", "--hard", pre)
|
|
598
|
-
if code != 0:
|
|
599
|
-
return (f"{clash}, and the tree could not be put back ({_tail(err)}) — "
|
|
600
|
-
f"your edits are in `git stash`, and the rebase is half-applied")
|
|
601
|
-
code, _, err = _locking(repo, "stash", "pop")
|
|
602
|
-
if code != 0:
|
|
603
|
-
return f"{clash} — nothing was rebased, and your edits are in `git stash` ({_tail(err)})"
|
|
604
|
-
return f"{clash} — nothing was rebased and your working tree is as you left it"
|
|
590
|
+
if re.search(r"unstaged changes|uncommitted changes|cannot pull with rebase|"
|
|
591
|
+
r"cannot rebase.*(dirty|unstaged)", err, re.I):
|
|
592
|
+
return ("the branch moved, and your own uncommitted edits are in the way of "
|
|
593
|
+
"rebasing onto it — nothing was moved or stashed; commit them and the "
|
|
594
|
+
"next board write pushes both")
|
|
595
|
+
return f"the branch moved and the rebase onto it did not apply: {_tail(err)}"
|
|
605
596
|
|
|
606
597
|
|
|
607
598
|
def _message(item: str, rows: list) -> tuple:
|
package/harness/test_work.py
CHANGED
|
@@ -2386,6 +2386,72 @@ def test_a_failing_verify_run_cannot_be_recorded_as_evidence():
|
|
|
2386
2386
|
gate.VERIFY = old
|
|
2387
2387
|
|
|
2388
2388
|
|
|
2389
|
+
def test_a_failing_gate_keeps_what_it_said_and_a_passing_one_keeps_nothing():
|
|
2390
|
+
# Only `said` used to survive — the last line, cut at 160 characters — and every
|
|
2391
|
+
# other byte the gate produced was captured and dropped. That is why `build`
|
|
2392
|
+
# could fail under the gate, pass by hand, and stay undiagnosed across three
|
|
2393
|
+
# sessions: the evidence was destroyed at the moment it was produced.
|
|
2394
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
2395
|
+
v = _tree(tmp)
|
|
2396
|
+
e = _epic(v, "an-epic")
|
|
2397
|
+
_task(e / "in-progress", "alpha", body="# T\n")
|
|
2398
|
+
with _work_dir(tmp) as root:
|
|
2399
|
+
old = gate.VERIFY
|
|
2400
|
+
gate.VERIFY = {
|
|
2401
|
+
# Both streams, because a build diagnoses on stderr and a test suite
|
|
2402
|
+
# on stdout, and reading one of them is how a failure comes back as
|
|
2403
|
+
# `exit 1` with nothing attached.
|
|
2404
|
+
"noisy": "sh -c 'echo the-reason-on-stdout; "
|
|
2405
|
+
"echo the-reason-on-stderr >&2; exit 3'",
|
|
2406
|
+
"quiet": "sh -c 'echo nothing-worth-keeping; exit 0'",
|
|
2407
|
+
}
|
|
2408
|
+
try:
|
|
2409
|
+
assert gate.cmd_verify({"task": "alpha"}) == 1
|
|
2410
|
+
rows = {r["name"]: r for r in gate.read_run(root)["results"]}
|
|
2411
|
+
assert "the-reason-on-stdout" in rows["noisy"]["log"], \
|
|
2412
|
+
"a failing gate keeps what it wrote to stdout"
|
|
2413
|
+
assert "the-reason-on-stderr" in rows["noisy"]["log"], \
|
|
2414
|
+
"and to stderr, which is where a build puts its diagnosis"
|
|
2415
|
+
assert rows["quiet"]["log"] == "", \
|
|
2416
|
+
"a passing gate keeps nothing — it is noise, and it is the common case"
|
|
2417
|
+
finally:
|
|
2418
|
+
gate.VERIFY = old
|
|
2419
|
+
|
|
2420
|
+
|
|
2421
|
+
def test_what_a_failing_gate_said_reaches_the_caller_not_only_the_run_file():
|
|
2422
|
+
# A caller told `build: exit 1` has to leave the surface and run the build itself
|
|
2423
|
+
# to learn anything, which is the loop this ends. The tool door is the one that
|
|
2424
|
+
# matters: it is what an agent sees, and it is the door the completion gate is
|
|
2425
|
+
# reached through.
|
|
2426
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
2427
|
+
v = _tree(tmp)
|
|
2428
|
+
e = _epic(v, "an-epic")
|
|
2429
|
+
_task(e / "in-progress", "alpha", body="# T\n")
|
|
2430
|
+
with _work_dir(tmp) as root:
|
|
2431
|
+
old = gate.VERIFY
|
|
2432
|
+
gate.VERIFY = {"boom": "sh -c 'echo a-line-that-explains-it >&2; exit 1'"}
|
|
2433
|
+
try:
|
|
2434
|
+
gate.cmd_verify({"task": "alpha"})
|
|
2435
|
+
said = gate._describe(gate.read_run(root))
|
|
2436
|
+
assert "a-line-that-explains-it" in said, \
|
|
2437
|
+
"the caller is told WHY, not only that something failed"
|
|
2438
|
+
finally:
|
|
2439
|
+
gate.VERIFY = old
|
|
2440
|
+
|
|
2441
|
+
|
|
2442
|
+
def test_a_gates_kept_output_is_capped_by_lines_and_by_characters():
|
|
2443
|
+
# Both caps are load-bearing and neither is enough alone: a bundle build prints
|
|
2444
|
+
# few enormous lines while a test suite prints thousands of short ones, so a line
|
|
2445
|
+
# budget alone lets the first through and a character budget alone cuts the
|
|
2446
|
+
# second to a fragment of its last line.
|
|
2447
|
+
many = gate._keep("\n".join(str(n) for n in range(500)), "")
|
|
2448
|
+
assert len(many.splitlines()) <= gate._LOG_LINES
|
|
2449
|
+
assert "499" in many, "and it keeps the END, which is where a failure explains itself"
|
|
2450
|
+
huge = gate._keep("x" * 50_000, "")
|
|
2451
|
+
assert len(huge) <= gate._LOG_CHARS + 1, "the ellipsis is the one extra character"
|
|
2452
|
+
assert huge.startswith("…"), "a cut is visible rather than silent"
|
|
2453
|
+
|
|
2454
|
+
|
|
2389
2455
|
def test_a_verify_run_is_visible_to_the_next_caller():
|
|
2390
2456
|
# The whole point of the run file. A blocking run that recorded nothing would be
|
|
2391
2457
|
# invisible to whoever asked next, and the overlap that corrupts a result is
|
|
@@ -4929,11 +4995,13 @@ def test_a_backslash_in_a_title_scaffolds_and_leaves_the_repo_usable():
|
|
|
4929
4995
|
assert "A \\n in a title" in (Path(tmp) / "README.md").read_text()
|
|
4930
4996
|
|
|
4931
4997
|
|
|
4932
|
-
def
|
|
4933
|
-
#
|
|
4934
|
-
#
|
|
4935
|
-
#
|
|
4936
|
-
#
|
|
4998
|
+
def test_a_push_needing_a_rebase_over_a_dirty_tree_waits_rather_than_moving_it():
|
|
4999
|
+
# The retry used to rebase with `rebase.autoStash=true`, which moved the session's
|
|
5000
|
+
# own uncommitted edits aside to make room and put them back afterwards. Removed
|
|
5001
|
+
# deliberately (founder, 2026-09-08): the board commit is already safe by this
|
|
5002
|
+
# point, so the only thing left for the retry to lose is work nobody asked it to
|
|
5003
|
+
# touch. Now git declines the rebase, the push waits for `sync`, and the tree is
|
|
5004
|
+
# exactly as its author left it.
|
|
4937
5005
|
with tempfile.TemporaryDirectory() as tmp:
|
|
4938
5006
|
try:
|
|
4939
5007
|
repo = _git_repo(tmp)
|
|
@@ -4960,22 +5028,59 @@ def test_a_push_whose_autostash_conflicts_is_reported_and_the_tree_restored():
|
|
|
4960
5028
|
repo, "ours", [{"event": "created", "name": "ours"}])
|
|
4961
5029
|
|
|
4962
5030
|
assert committed, "the board commit is made before any of this and is safe"
|
|
4963
|
-
assert not pushed, f"
|
|
4964
|
-
assert "NOT pushed" in note and "
|
|
4965
|
-
f"the caller
|
|
5031
|
+
assert not pushed, f"the branch moved and the rebase could not run: {note}"
|
|
5032
|
+
assert "NOT pushed" in note and "sync" in note, \
|
|
5033
|
+
f"the caller is told the write is safe and how to send it: {note}"
|
|
5034
|
+
assert "uncommitted" in note, \
|
|
5035
|
+
f"and told WHY it could not push, in terms it can act on: {note}"
|
|
4966
5036
|
assert (repo / "src" / "app.ts").read_text() == "mine, not yet committed\n", \
|
|
4967
|
-
"the session's edits are
|
|
5037
|
+
"the session's own edits are untouched — not stashed, not restored"
|
|
4968
5038
|
assert "<<<<<<<" not in (repo / "src" / "app.ts").read_text()
|
|
4969
5039
|
assert not _git(repo, "diff", "--name-only", "--diff-filter=U").stdout.strip(), \
|
|
4970
5040
|
"no half-done merge is left in the index"
|
|
4971
5041
|
assert not _git(repo, "stash", "list").stdout.strip(), \
|
|
4972
|
-
"and nothing
|
|
5042
|
+
"and nothing was ever put in a stash to begin with"
|
|
4973
5043
|
assert "ours.md" in _git(repo, "log", "-1", "--name-only",
|
|
4974
5044
|
"--format=").stdout
|
|
4975
5045
|
finally:
|
|
4976
5046
|
config.apply(config.DEFAULTS)
|
|
4977
5047
|
|
|
4978
5048
|
|
|
5049
|
+
def test_a_push_needing_a_rebase_over_a_clean_tree_still_lands():
|
|
5050
|
+
# Removing the autostash must not cost the ordinary case. With nothing
|
|
5051
|
+
# uncommitted there is nothing in the rebase's way, so a board write racing
|
|
5052
|
+
# another machine still rebases onto the new tip and pushes on the retry.
|
|
5053
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
5054
|
+
try:
|
|
5055
|
+
repo = _git_repo(tmp)
|
|
5056
|
+
(repo / "src").mkdir(exist_ok=True)
|
|
5057
|
+
(repo / "src" / "app.ts").write_text("original\n")
|
|
5058
|
+
_git(repo, "add", "-A")
|
|
5059
|
+
_git(repo, "commit", "-qm", "code")
|
|
5060
|
+
_git(repo, "push", "-q", "origin", "HEAD:main")
|
|
5061
|
+
|
|
5062
|
+
import subprocess
|
|
5063
|
+
other = repo.parent / "other"
|
|
5064
|
+
subprocess.run(["git", "clone", "-q", str(repo.parent / "origin.git"),
|
|
5065
|
+
str(other)], check=True, capture_output=True)
|
|
5066
|
+
for k, val in (("user.email", "o@o"), ("user.name", "O")):
|
|
5067
|
+
_git(other, "config", k, val)
|
|
5068
|
+
(other / "src" / "app.ts").write_text("theirs\n")
|
|
5069
|
+
_git(other, "commit", "-qam", "theirs")
|
|
5070
|
+
_git(other, "push", "-q", "origin", "HEAD:main")
|
|
5071
|
+
|
|
5072
|
+
(repo / "work" / "ours.md").write_text("ours\n")
|
|
5073
|
+
committed, pushed, note = _board_write(
|
|
5074
|
+
repo, "ours", [{"event": "created", "name": "ours"}])
|
|
5075
|
+
|
|
5076
|
+
assert committed and pushed, \
|
|
5077
|
+
f"a clean tree rebases onto the moved branch and lands: {note}"
|
|
5078
|
+
assert "theirs\n" == (repo / "src" / "app.ts").read_text(), \
|
|
5079
|
+
"and the session ends up on top of what the other machine pushed"
|
|
5080
|
+
finally:
|
|
5081
|
+
config.apply(config.DEFAULTS)
|
|
5082
|
+
|
|
5083
|
+
|
|
4979
5084
|
# --- what a session is told before it starts ----------------------------------
|
|
4980
5085
|
|
|
4981
5086
|
|
|
@@ -5067,7 +5172,18 @@ if __name__ == "__main__":
|
|
|
5067
5172
|
raise SystemExit(f"collector missed {len(missed)} test(s) — is this block still "
|
|
5068
5173
|
f"the last thing in the file? {sorted(missed)}")
|
|
5069
5174
|
|
|
5175
|
+
# The id vocabulary is MUTABLE GLOBAL STATE — `config.apply` rebinds `ids.LEDGER` and the
|
|
5176
|
+
# patterns built from it — and any test that runs a command through `work.main` applies the
|
|
5177
|
+
# AMBIENT repo's config as a side effect. Claude Code sets `CLAUDE_PROJECT_DIR` on everything
|
|
5178
|
+
# it spawns, so the suite found a real config there and ran the rest of its cases in that
|
|
5179
|
+
# repo's dialect, while fixtures written from the shipped default stopped matching. The gate
|
|
5180
|
+
# therefore passed from a terminal and failed under `jarvis work verify` — the same tree, two
|
|
5181
|
+
# answers, decided by who called it.
|
|
5182
|
+
#
|
|
5183
|
+
# So each test starts from a STATED vocabulary rather than whatever the one before it left
|
|
5184
|
+
# behind. Cases that exercise a different prefix still set their own; they just no longer leak.
|
|
5070
5185
|
for fn in tests:
|
|
5186
|
+
config.apply(config.DEFAULTS)
|
|
5071
5187
|
fn()
|
|
5072
5188
|
print(f"ok {fn.__name__}")
|
|
5073
5189
|
print(f"\n{len(tests)} passed")
|
package/package.json
CHANGED
package/dist/data/rerankers.mjs
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import "./chunk-AKQQC5IT.mjs";
|
|
2
|
-
import "./chunk-7REP35VA.mjs";
|
|
3
|
-
import "./chunk-RRJ6KKYL.mjs";
|
|
4
|
-
import {
|
|
5
|
-
reranker
|
|
6
|
-
} from "./chunk-AYOJSS2F.mjs";
|
|
7
|
-
import "./chunk-YWSWQEJN.mjs";
|
|
8
|
-
|
|
9
|
-
// ../../packages/data/src/rerankers/local.ts
|
|
10
|
-
function localReranker(options = {}) {
|
|
11
|
-
const model = options.model ?? "Xenova/ms-marco-MiniLM-L-6-v2";
|
|
12
|
-
const dtype = options.dtype ?? "q8";
|
|
13
|
-
const pkg = "@huggingface/transformers";
|
|
14
|
-
let ready;
|
|
15
|
-
const load = async () => {
|
|
16
|
-
if (!ready) {
|
|
17
|
-
const mod = await import(pkg).catch(() => {
|
|
18
|
-
throw new Error(
|
|
19
|
-
"localReranker needs '@huggingface/transformers' \u2014 run `pnpm add @huggingface/transformers` (optional dep)."
|
|
20
|
-
);
|
|
21
|
-
});
|
|
22
|
-
ready = Promise.all([
|
|
23
|
-
mod.AutoTokenizer.from_pretrained(model),
|
|
24
|
-
mod.AutoModelForSequenceClassification.from_pretrained(model, { dtype })
|
|
25
|
-
]).then(([tokenizer, m]) => ({ tokenizer, model: m }));
|
|
26
|
-
}
|
|
27
|
-
return ready;
|
|
28
|
-
};
|
|
29
|
-
return reranker({
|
|
30
|
-
name: `local-rerank:${model}`,
|
|
31
|
-
family: "localReranker",
|
|
32
|
-
...options.limit !== void 0 ? { limit: options.limit } : {},
|
|
33
|
-
rerank: async (query, hits, opts) => {
|
|
34
|
-
if (hits.length === 0) return hits;
|
|
35
|
-
const { tokenizer, model: m } = await load();
|
|
36
|
-
const inputs = tokenizer(new Array(hits.length).fill(query), {
|
|
37
|
-
text_pair: hits.map((h) => h.text),
|
|
38
|
-
padding: true,
|
|
39
|
-
truncation: true
|
|
40
|
-
});
|
|
41
|
-
const { logits } = await m(inputs);
|
|
42
|
-
const scores = logits.sigmoid().tolist();
|
|
43
|
-
const scored = hits.map((hit, i) => ({ ...hit, score: scores[i]?.[0] ?? 0 }));
|
|
44
|
-
scored.sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
|
|
45
|
-
const topN = opts?.topN;
|
|
46
|
-
return typeof topN === "number" ? scored.slice(0, topN) : scored;
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
export {
|
|
51
|
-
localReranker
|
|
52
|
-
};
|