@appchy/jarvis 0.1.77 → 0.1.79
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 +12 -8
- package/dist/bin.js.map +1 -1
- package/harness/harness/gate.py +83 -16
- package/harness/harness/git.py +41 -0
- package/harness/test_work.py +216 -0
- package/package.json +3 -3
package/harness/harness/gate.py
CHANGED
|
@@ -384,12 +384,19 @@ def _results_out(data) -> list:
|
|
|
384
384
|
for r in (data.get("results") or [])]
|
|
385
385
|
|
|
386
386
|
|
|
387
|
-
def
|
|
388
|
-
"""
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
387
|
+
def _verify_report(root, name) -> int:
|
|
388
|
+
"""Say where the run stands. **Never starts one.**
|
|
389
|
+
|
|
390
|
+
ASKING AND STARTING ARE TWO OPERATIONS, and they used to be one call whose
|
|
391
|
+
meaning depended on timing: it attached when a run was in flight and started
|
|
392
|
+
one when none was. So a session checking whether the gates had finished set a
|
|
393
|
+
four-minute build going by asking — which is how a repo with a shared build
|
|
394
|
+
directory raced itself, and the resulting corruption was read as a flaky gate
|
|
395
|
+
for three sessions.
|
|
396
|
+
|
|
397
|
+
The split is on this side rather than the other because THIS is the call that
|
|
398
|
+
gets repeated. Polling is the loop; starting happens once and is said out
|
|
399
|
+
loud. A caller that repeats itself must be unable to cause anything.
|
|
393
400
|
"""
|
|
394
401
|
running = live_run(root)
|
|
395
402
|
if running:
|
|
@@ -424,6 +431,31 @@ def _verify_async(root, name) -> int:
|
|
|
424
431
|
"done": done.get("done") or [], "passed": bool(done.get("passed")),
|
|
425
432
|
"commit": done.get("sha") or "", "results": _results_out(done)})
|
|
426
433
|
|
|
434
|
+
return _answer(
|
|
435
|
+
"idle",
|
|
436
|
+
f"No gates have run for '{name or 'no task'}' at this commit. Start them "
|
|
437
|
+
f"explicitly — asking never starts a run.",
|
|
438
|
+
{"gates": sorted(VERIFY)})
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def _verify_start(root, name) -> int:
|
|
442
|
+
"""Start the gates in the background. **Never reports on somebody else's run.**
|
|
443
|
+
|
|
444
|
+
Refuses when a run is in flight rather than quietly attaching to it. Attaching
|
|
445
|
+
is what let one call report a run started for a DIFFERENT task, whose verdict
|
|
446
|
+
would then have been read against this one's commit — measured 2026-09-09.
|
|
447
|
+
A refusal that names the task holding the checkout is the honest answer, and
|
|
448
|
+
the report door is one call away.
|
|
449
|
+
"""
|
|
450
|
+
running = live_run(root)
|
|
451
|
+
if running:
|
|
452
|
+
return _answer(
|
|
453
|
+
"running", _busy(running),
|
|
454
|
+
{"gate": running.get("current") or "starting",
|
|
455
|
+
"elapsedSeconds": _elapsed(running),
|
|
456
|
+
"gates": running.get("gates") or [], "done": running.get("done") or [],
|
|
457
|
+
"results": _results_out(running)})
|
|
458
|
+
|
|
427
459
|
child = subprocess.Popen(
|
|
428
460
|
[sys.executable, os.path.abspath(sys.argv[0]), "verify", "--task", name,
|
|
429
461
|
"--detached"],
|
|
@@ -432,17 +464,24 @@ def _verify_async(root, name) -> int:
|
|
|
432
464
|
return _answer(
|
|
433
465
|
"started",
|
|
434
466
|
f"Gates started ({len(VERIFY)}): {', '.join(sorted(VERIFY))}. They take "
|
|
435
|
-
f"minutes, not seconds — ask
|
|
467
|
+
f"minutes, not seconds — ask for progress, which never starts another.",
|
|
436
468
|
{"gates": sorted(VERIFY)})
|
|
437
469
|
|
|
438
470
|
|
|
439
471
|
def cmd_verify(args) -> int:
|
|
440
472
|
"""Execute the verify commands and RECORD the result against the task.
|
|
441
473
|
|
|
442
|
-
|
|
443
|
-
is the
|
|
444
|
-
|
|
445
|
-
|
|
474
|
+
Four doors, and **asking is not one of the ones that starts anything.**
|
|
475
|
+
Blocking is the person's: typing the command is the explicit act, so it runs
|
|
476
|
+
the gates and prints the table. `--async` starts a background run for the tool
|
|
477
|
+
surface, because a call that takes 174 seconds cannot be a relayed round trip.
|
|
478
|
+
`--status` only reports, and is what a caller waiting for a result uses.
|
|
479
|
+
`--detached` is the child `--async` spawns and is nobody's to type.
|
|
480
|
+
|
|
481
|
+
The two used to be one call that started or attached depending on timing, and
|
|
482
|
+
the ambiguity cost more than it saved: a session polling for a result started
|
|
483
|
+
the run it was waiting for, and one call attached to a run belonging to
|
|
484
|
+
another task and would have reported that verdict here.
|
|
446
485
|
"""
|
|
447
486
|
root = find_work_root()
|
|
448
487
|
name = (args.get("task") or "").strip()
|
|
@@ -456,16 +495,17 @@ def cmd_verify(args) -> int:
|
|
|
456
495
|
_execute(root, name)
|
|
457
496
|
return 0
|
|
458
497
|
|
|
498
|
+
if args.get("status"):
|
|
499
|
+
return _verify_report(root, name)
|
|
500
|
+
|
|
501
|
+
if args.get("async"):
|
|
502
|
+
return _verify_start(root, name)
|
|
503
|
+
|
|
459
504
|
running = live_run(root)
|
|
460
505
|
if running:
|
|
461
|
-
if args.get("async"):
|
|
462
|
-
return _verify_async(root, name)
|
|
463
506
|
print(f"error: {_busy(running)}", file=sys.stderr)
|
|
464
507
|
return 1
|
|
465
508
|
|
|
466
|
-
if args.get("async"):
|
|
467
|
-
return _verify_async(root, name)
|
|
468
|
-
|
|
469
509
|
results, ok, sha = _execute(root, name)
|
|
470
510
|
_print_report(results, sha)
|
|
471
511
|
if name:
|
|
@@ -568,6 +608,33 @@ def gate(root, task, accept: str = "", owner: str = "") -> list:
|
|
|
568
608
|
elif not head and when != date.today().isoformat():
|
|
569
609
|
reasons.append(f"verify last passed on {when}, not today, and there "
|
|
570
610
|
f"is no git sha to pin it to — re-run it")
|
|
611
|
+
|
|
612
|
+
# …and that the code ARRIVED, which is the other half and was missing.
|
|
613
|
+
# Everything above reads COMMITTED history, so it proves the code moved
|
|
614
|
+
# FORWARD PAST the evidence and never that it got there at all. A task can
|
|
615
|
+
# be green with its whole implementation in one working tree; three were,
|
|
616
|
+
# for as long as ten days, and the only commits around them were
|
|
617
|
+
# `docs(work):` ones, which look exactly like a healthy board.
|
|
618
|
+
#
|
|
619
|
+
# It refuses rather than warns because the gates RAN AGAINST THE WORKING
|
|
620
|
+
# TREE. If that tree is not what is in git, the evidence and the commit it
|
|
621
|
+
# is pinned to describe different code, and calling that shipped is the
|
|
622
|
+
# false claim this whole file exists to prevent.
|
|
623
|
+
#
|
|
624
|
+
# The harness names the files and stops there: committing somebody's source
|
|
625
|
+
# is a far larger claim on their repo than committing the board, and a board
|
|
626
|
+
# write deliberately leaves a session's own code alone (founder, 2026-09-10).
|
|
627
|
+
loose = git.uncommitted_code(root.parent) if git.enabled() else []
|
|
628
|
+
if loose:
|
|
629
|
+
shown = ", ".join(loose[:5])
|
|
630
|
+
more = f" and {len(loose) - 5} more" if len(loose) > 5 else ""
|
|
631
|
+
reasons.append(
|
|
632
|
+
f"{len(loose)} uncommitted change(s) outside the board — the gates "
|
|
633
|
+
f"ran against this working tree, so completing now would record "
|
|
634
|
+
f"shipped for code no commit contains: {shown}{more}. Commit them "
|
|
635
|
+
f"(or stash or gitignore what is not this task's), then re-run "
|
|
636
|
+
f"`jarvis work verify --task {task.name}` — committing moves HEAD, "
|
|
637
|
+
f"so the evidence has to be taken on the tree that shipped.")
|
|
571
638
|
else:
|
|
572
639
|
reasons.append("no `verify` commands configured — an unconfigured repo "
|
|
573
640
|
"cannot prove anything, so nothing in it can complete. Set "
|
package/harness/harness/git.py
CHANGED
|
@@ -95,6 +95,47 @@ def enabled() -> bool:
|
|
|
95
95
|
return bool(GIT.get("commit"))
|
|
96
96
|
|
|
97
97
|
|
|
98
|
+
def uncommitted_code(repo) -> list:
|
|
99
|
+
"""Everything outside the board that is not in git yet, sorted.
|
|
100
|
+
|
|
101
|
+
The mirror of `changed`, and the completion gate's half of it. `changed` asks
|
|
102
|
+
what a board write would CARRY; this asks what a completion would LEAVE BEHIND.
|
|
103
|
+
|
|
104
|
+
**Why this is accuracy rather than caution.** A verify run executes the
|
|
105
|
+
configured commands against the WORKING TREE, so a pass means "these gates pass
|
|
106
|
+
here, now". It is then recorded against HEAD. If anything outside the board
|
|
107
|
+
differs from HEAD, those two are statements about different trees, and the
|
|
108
|
+
board ends up saying shipped about code no commit contains. Measured three
|
|
109
|
+
times, once on the task whose entire subject was the harness losing what it is
|
|
110
|
+
trusted to keep — it completed with 7/7 gates recorded twice and none of its
|
|
111
|
+
fix in HEAD.
|
|
112
|
+
|
|
113
|
+
Untracked files count, and they are the important half: the lost work's own
|
|
114
|
+
shape was NEW files git had never heard of. Anything gitignored never appears
|
|
115
|
+
here, so a scratch directory somebody has already told git to ignore is not
|
|
116
|
+
this check's business.
|
|
117
|
+
|
|
118
|
+
Claims and the run file are excluded for the reason they are never committed —
|
|
119
|
+
they are one machine's coordination, true for minutes.
|
|
120
|
+
"""
|
|
121
|
+
board = [r.rstrip("/") for r in (GIT.get("paths") or [])]
|
|
122
|
+
code, out, _ = _git(repo, "status", "--porcelain", "-z",
|
|
123
|
+
"--untracked-files=all", "--no-renames")
|
|
124
|
+
if code != 0:
|
|
125
|
+
return []
|
|
126
|
+
found = []
|
|
127
|
+
for entry in out.split("\0"):
|
|
128
|
+
if len(entry) <= 3:
|
|
129
|
+
continue
|
|
130
|
+
p = entry[3:]
|
|
131
|
+
if Path(p).name in LOCAL:
|
|
132
|
+
continue
|
|
133
|
+
if any(p == r or p.startswith(f"{r}/") for r in board):
|
|
134
|
+
continue
|
|
135
|
+
found.append(p)
|
|
136
|
+
return sorted(found)
|
|
137
|
+
|
|
138
|
+
|
|
98
139
|
def machine() -> str:
|
|
99
140
|
"""Which machine acted. `WORK_MACHINE` where something knows a better name for
|
|
100
141
|
this box than its hostname — a daemon that already has an identity for it —
|
package/harness/test_work.py
CHANGED
|
@@ -2799,6 +2799,83 @@ def test_a_verify_run_is_visible_to_the_next_caller():
|
|
|
2799
2799
|
gate.VERIFY = old
|
|
2800
2800
|
|
|
2801
2801
|
|
|
2802
|
+
def _said(capsys_free_call) -> dict:
|
|
2803
|
+
"""The JSON one of the async doors printed, as a dict."""
|
|
2804
|
+
import io
|
|
2805
|
+
from contextlib import redirect_stdout
|
|
2806
|
+
|
|
2807
|
+
buf = io.StringIO()
|
|
2808
|
+
with redirect_stdout(buf):
|
|
2809
|
+
capsys_free_call()
|
|
2810
|
+
for line in reversed(buf.getvalue().splitlines()):
|
|
2811
|
+
if line.strip().startswith("{"):
|
|
2812
|
+
return json.loads(line)
|
|
2813
|
+
raise AssertionError(f"no JSON on stdout: {buf.getvalue()!r}")
|
|
2814
|
+
|
|
2815
|
+
|
|
2816
|
+
def test_asking_where_the_gates_stand_never_starts_them():
|
|
2817
|
+
# Asking and starting used to be ONE call whose meaning depended on timing, so a
|
|
2818
|
+
# session polling for a result set off the four-minute run it was waiting for.
|
|
2819
|
+
# Polling is the loop and starting happens once, so the repeatable call is the
|
|
2820
|
+
# one that has to be unable to cause anything.
|
|
2821
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
2822
|
+
v = _tree(tmp)
|
|
2823
|
+
e = _epic(v, "an-epic")
|
|
2824
|
+
_task(e / "in-progress", "alpha", body="# T\n")
|
|
2825
|
+
with _work_dir(tmp) as root:
|
|
2826
|
+
old, gate.VERIFY = gate.VERIFY, {"tests": "true"}
|
|
2827
|
+
try:
|
|
2828
|
+
out = _said(lambda: gate.cmd_verify({"task": "alpha", "status": True}))
|
|
2829
|
+
assert out["state"] == "idle", out
|
|
2830
|
+
# The real assertion: nothing ran, and nothing is running.
|
|
2831
|
+
assert gate.read_run(root) is None, "asking wrote a run file"
|
|
2832
|
+
assert gate.live_run(root) is None
|
|
2833
|
+
finally:
|
|
2834
|
+
gate.VERIFY = old
|
|
2835
|
+
|
|
2836
|
+
|
|
2837
|
+
def test_starting_while_a_run_is_live_names_whose_it_is_rather_than_attaching():
|
|
2838
|
+
# Attaching is what let one call report a run started for a DIFFERENT task,
|
|
2839
|
+
# whose verdict would then have been read against this one's commit.
|
|
2840
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
2841
|
+
v = _tree(tmp)
|
|
2842
|
+
e = _epic(v, "an-epic")
|
|
2843
|
+
_task(e / "in-progress", "alpha", body="# T\n")
|
|
2844
|
+
_task(e / "in-progress", "beta", body="# T\n")
|
|
2845
|
+
with _work_dir(tmp) as root:
|
|
2846
|
+
old, gate.VERIFY = gate.VERIFY, {"tests": "true"}
|
|
2847
|
+
try:
|
|
2848
|
+
gate._write_run(root, {"task": "alpha", "pid": os.getpid(),
|
|
2849
|
+
"machine": "here", "started": gate._now().isoformat(),
|
|
2850
|
+
"gates": ["tests"], "current": "tests", "done": [],
|
|
2851
|
+
"finished": None, "read": False})
|
|
2852
|
+
out = _said(lambda: gate.cmd_verify({"task": "beta", "async": True}))
|
|
2853
|
+
assert out["state"] == "running", out
|
|
2854
|
+
assert "alpha" in out["message"], out["message"]
|
|
2855
|
+
finally:
|
|
2856
|
+
gate.VERIFY = old
|
|
2857
|
+
|
|
2858
|
+
|
|
2859
|
+
def test_a_finished_run_for_another_task_is_not_handed_over_as_this_ones():
|
|
2860
|
+
# The wrong-verdict half. A result proven for one item says nothing about
|
|
2861
|
+
# another, and reporting it would put a green gate on work nobody ran.
|
|
2862
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
2863
|
+
v = _tree(tmp)
|
|
2864
|
+
e = _epic(v, "an-epic")
|
|
2865
|
+
_task(e / "in-progress", "alpha", body="# T\n")
|
|
2866
|
+
_task(e / "in-progress", "beta", body="# T\n")
|
|
2867
|
+
with _work_dir(tmp) as root:
|
|
2868
|
+
old, gate.VERIFY = gate.VERIFY, {"tests": "true"}
|
|
2869
|
+
try:
|
|
2870
|
+
gate.cmd_verify({"task": "alpha"})
|
|
2871
|
+
assert gate.read_run(root)["passed"] is True
|
|
2872
|
+
out = _said(lambda: gate.cmd_verify({"task": "beta", "status": True}))
|
|
2873
|
+
assert out["state"] == "idle", out
|
|
2874
|
+
assert out["passed"] is False, out
|
|
2875
|
+
finally:
|
|
2876
|
+
gate.VERIFY = old
|
|
2877
|
+
|
|
2878
|
+
|
|
2802
2879
|
def test_a_second_verify_is_refused_while_one_is_running():
|
|
2803
2880
|
with tempfile.TemporaryDirectory() as tmp:
|
|
2804
2881
|
v = _tree(tmp)
|
|
@@ -3367,6 +3444,145 @@ def test_a_board_write_reaches_the_ORIGIN_not_just_the_disk():
|
|
|
3367
3444
|
config.apply(config.DEFAULTS)
|
|
3368
3445
|
|
|
3369
3446
|
|
|
3447
|
+
def _provable_task(repo, name="alpha"):
|
|
3448
|
+
"""A task in a git-mode repo with everything but the code committed: criteria
|
|
3449
|
+
ticked and a passing verify recorded, so the ONLY thing a gate can hold on is
|
|
3450
|
+
whether the implementation reached git."""
|
|
3451
|
+
e = repo / "work" / "versions" / "01-a-cut" / "an-epic"
|
|
3452
|
+
(e / "in-progress" / name).mkdir(parents=True)
|
|
3453
|
+
(e / "epic.md").write_text("---\ntype: epic\n---\n\n# E\n")
|
|
3454
|
+
(e.parent / "version.md").write_text(
|
|
3455
|
+
"---\ncreated: 2026-08-01\norder: 1\noutcome: x\n---\n\n# Cut\n")
|
|
3456
|
+
(e / "in-progress" / name / "task.md").write_text(
|
|
3457
|
+
f"---\npriority: P0\n---\n\n# {name}\n\n## Acceptance criteria\n\n- [x] it works\n")
|
|
3458
|
+
_git(repo, "add", "-A")
|
|
3459
|
+
_git(repo, "commit", "-qm", "the board so far")
|
|
3460
|
+
return e
|
|
3461
|
+
|
|
3462
|
+
|
|
3463
|
+
def test_completing_refuses_while_the_code_is_only_in_the_working_tree():
|
|
3464
|
+
# Every other check in the gate reads COMMITTED history, so all of them prove
|
|
3465
|
+
# the code moved FORWARD PAST the evidence and none proves it arrived. Three
|
|
3466
|
+
# tasks completed green with their whole implementation in one working tree —
|
|
3467
|
+
# one of them the task about the harness losing what it is trusted to keep,
|
|
3468
|
+
# with 7/7 recorded twice and none of its fix in HEAD.
|
|
3469
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
3470
|
+
try:
|
|
3471
|
+
repo = _git_repo(tmp, push=False)
|
|
3472
|
+
e = _provable_task(repo)
|
|
3473
|
+
with _work_dir(str(repo / "work")) as root:
|
|
3474
|
+
old, gate.VERIFY = gate.VERIFY, {"tests": "true"}
|
|
3475
|
+
try:
|
|
3476
|
+
assert gate.cmd_verify({"task": "alpha"}) == 0
|
|
3477
|
+
# Clean tree: nothing left to hold, so the gate is satisfied.
|
|
3478
|
+
assert gate.gate(root, model.locate(root, "alpha")) == []
|
|
3479
|
+
|
|
3480
|
+
# Now the failure this exists for — the implementation exists
|
|
3481
|
+
# and is in no commit. An UNTRACKED file, because that is the
|
|
3482
|
+
# lost work's own shape: new files git had never heard of.
|
|
3483
|
+
(repo / "src").mkdir()
|
|
3484
|
+
(repo / "src" / "shipped.ts").write_text("export const it = 1;\n")
|
|
3485
|
+
reasons = gate.gate(root, model.locate(root, "alpha"))
|
|
3486
|
+
assert any("uncommitted" in r for r in reasons), reasons
|
|
3487
|
+
assert any("src/shipped.ts" in r for r in reasons), reasons
|
|
3488
|
+
|
|
3489
|
+
# And the move itself refuses, leaving the task where it was —
|
|
3490
|
+
# warning after moving is what made the board say done anyway.
|
|
3491
|
+
assert task.cmd_move({"name": "alpha", "status": "complete",
|
|
3492
|
+
"delivered": "it ships",
|
|
3493
|
+
"not-included": "nothing"}) == 1
|
|
3494
|
+
assert (e / "in-progress" / "alpha").is_dir()
|
|
3495
|
+
|
|
3496
|
+
# Committing satisfies THIS check and trips the older one, and
|
|
3497
|
+
# that is the flow rather than a snag: committing moves HEAD, so
|
|
3498
|
+
# evidence taken before it describes a different commit. The two
|
|
3499
|
+
# together mean commit, then verify, then complete — and the
|
|
3500
|
+
# refusal says so rather than leaving it to be discovered.
|
|
3501
|
+
_git(repo, "add", "-A")
|
|
3502
|
+
_git(repo, "commit", "-qm", "the code")
|
|
3503
|
+
reasons = gate.gate(root, model.locate(root, "alpha"))
|
|
3504
|
+
assert not any("uncommitted" in r for r in reasons), reasons
|
|
3505
|
+
assert any("code moved since verify" in r for r in reasons), reasons
|
|
3506
|
+
|
|
3507
|
+
assert gate.cmd_verify({"task": "alpha"}) == 0
|
|
3508
|
+
assert gate.gate(root, model.locate(root, "alpha")) == []
|
|
3509
|
+
finally:
|
|
3510
|
+
gate.VERIFY = old
|
|
3511
|
+
finally:
|
|
3512
|
+
config.apply(config.DEFAULTS)
|
|
3513
|
+
|
|
3514
|
+
|
|
3515
|
+
def test_partly_committed_code_is_still_uncommitted_code():
|
|
3516
|
+
# What `the-hub-says-why-a-write-failed` actually was: part of its work in HEAD
|
|
3517
|
+
# and part not. A check that asked "did anything land" would have passed it.
|
|
3518
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
3519
|
+
try:
|
|
3520
|
+
repo = _git_repo(tmp, push=False)
|
|
3521
|
+
_provable_task(repo)
|
|
3522
|
+
(repo / "src").mkdir()
|
|
3523
|
+
(repo / "src" / "landed.ts").write_text("export const a = 1;\n")
|
|
3524
|
+
_git(repo, "add", "-A")
|
|
3525
|
+
_git(repo, "commit", "-qm", "half of it")
|
|
3526
|
+
(repo / "src" / "left-behind.ts").write_text("export const b = 2;\n")
|
|
3527
|
+
with _work_dir(str(repo / "work")) as root:
|
|
3528
|
+
old, gate.VERIFY = gate.VERIFY, {"tests": "true"}
|
|
3529
|
+
try:
|
|
3530
|
+
assert gate.cmd_verify({"task": "alpha"}) == 0
|
|
3531
|
+
reasons = gate.gate(root, model.locate(root, "alpha"))
|
|
3532
|
+
assert any("src/left-behind.ts" in r for r in reasons), reasons
|
|
3533
|
+
finally:
|
|
3534
|
+
gate.VERIFY = old
|
|
3535
|
+
finally:
|
|
3536
|
+
config.apply(config.DEFAULTS)
|
|
3537
|
+
|
|
3538
|
+
|
|
3539
|
+
def test_a_repo_that_never_asked_for_git_completes_exactly_as_before():
|
|
3540
|
+
# The harness is shared with repos that opted out of all of this. Refusing on
|
|
3541
|
+
# their working tree would be a new claim on somebody who asked for none.
|
|
3542
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
3543
|
+
try:
|
|
3544
|
+
repo = _git_repo(tmp, push=False)
|
|
3545
|
+
_provable_task(repo)
|
|
3546
|
+
(repo / "src").mkdir()
|
|
3547
|
+
(repo / "src" / "shipped.ts").write_text("export const it = 1;\n")
|
|
3548
|
+
config.apply({**config.DEFAULTS,
|
|
3549
|
+
"git": {"commit": False, "push": False,
|
|
3550
|
+
"remote": "origin", "paths": ["work"]}})
|
|
3551
|
+
with _work_dir(str(repo / "work")) as root:
|
|
3552
|
+
old, gate.VERIFY = gate.VERIFY, {"tests": "true"}
|
|
3553
|
+
try:
|
|
3554
|
+
assert gate.cmd_verify({"task": "alpha"}) == 0
|
|
3555
|
+
assert gate.gate(root, model.locate(root, "alpha")) == []
|
|
3556
|
+
finally:
|
|
3557
|
+
gate.VERIFY = old
|
|
3558
|
+
finally:
|
|
3559
|
+
config.apply(config.DEFAULTS)
|
|
3560
|
+
|
|
3561
|
+
|
|
3562
|
+
def test_a_gitignored_scratch_file_is_not_this_gates_business():
|
|
3563
|
+
# The cost of refusing is that loose files block completion, and the escape is
|
|
3564
|
+
# the one people already use. A file git has been told to ignore never reaches
|
|
3565
|
+
# the check at all.
|
|
3566
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
3567
|
+
try:
|
|
3568
|
+
repo = _git_repo(tmp, push=False)
|
|
3569
|
+
_provable_task(repo)
|
|
3570
|
+
(repo / ".gitignore").write_text("scratch/\n")
|
|
3571
|
+
_git(repo, "add", "-A")
|
|
3572
|
+
_git(repo, "commit", "-qm", "ignore scratch")
|
|
3573
|
+
(repo / "scratch").mkdir()
|
|
3574
|
+
(repo / "scratch" / "notes.md").write_text("thinking out loud\n")
|
|
3575
|
+
with _work_dir(str(repo / "work")) as root:
|
|
3576
|
+
old, gate.VERIFY = gate.VERIFY, {"tests": "true"}
|
|
3577
|
+
try:
|
|
3578
|
+
assert gate.cmd_verify({"task": "alpha"}) == 0
|
|
3579
|
+
assert gate.gate(root, model.locate(root, "alpha")) == []
|
|
3580
|
+
finally:
|
|
3581
|
+
gate.VERIFY = old
|
|
3582
|
+
finally:
|
|
3583
|
+
config.apply(config.DEFAULTS)
|
|
3584
|
+
|
|
3585
|
+
|
|
3370
3586
|
def test_the_commit_trailer_names_the_item_the_event_and_the_machine():
|
|
3371
3587
|
# Four other pieces of work read this format, so it is asserted rather than
|
|
3372
3588
|
# assumed. Who, when and which branch stay OUT of it — git knows them, and a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appchy/jarvis",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.79",
|
|
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/anthropic": "1.0.0",
|
|
61
|
+
"@jarvis/board": "0.1.0",
|
|
60
62
|
"@jarvis/data": "0.1.0",
|
|
61
63
|
"@jarvis/errors": "1.0.0",
|
|
62
|
-
"@jarvis/board": "0.1.0",
|
|
63
64
|
"@jarvis/logger": "1.0.0",
|
|
64
65
|
"@jarvis/rpc": "1.0.0",
|
|
65
66
|
"@jarvis/types": "1.0.0",
|
|
66
67
|
"@jarvis/typescript-config": "1.0.0",
|
|
67
68
|
"@jarvis/ui": "0.1.0",
|
|
68
|
-
"@jarvis/anthropic": "1.0.0",
|
|
69
69
|
"@jarvis/vitest-config": "1.0.0"
|
|
70
70
|
},
|
|
71
71
|
"scripts": {
|