@appchy/jarvis 0.1.129 → 0.1.130

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.
@@ -2807,60 +2807,162 @@ def test_a_tier_floor_is_derived_from_the_owner_and_cannot_be_lowered():
2807
2807
  pass
2808
2808
 
2809
2809
 
2810
+ def _next_as(who, **args):
2811
+ """Run `next` as one instance, and leave nothing of that identity behind."""
2812
+ os.environ["WORK_INSTANCE"] = who
2813
+ try:
2814
+ shift.cmd_next(args)
2815
+ finally:
2816
+ os.environ.pop("WORK_INSTANCE", None)
2817
+ peers._RUNNING.clear()
2818
+
2819
+
2820
+ def _taken_by(root) -> dict:
2821
+ """Who took what, read the way every reader of the board now reads it."""
2822
+ from harness import holders
2823
+ return {h["session"]: name
2824
+ for name, found in holders.to_holders_by_item(events.read(root)).items()
2825
+ for h in found}
2826
+
2827
+
2810
2828
  def test_next_takes_one_task_and_a_second_instance_never_gets_the_same_one():
2811
- with tempfile.TemporaryDirectory() as tmp:
2829
+ with tempfile.TemporaryDirectory() as tmp, tempfile.TemporaryDirectory() as reg:
2812
2830
  v = _tree(tmp)
2813
2831
  e = _epic(v, "an-epic")
2814
2832
  _task(e / "queue", "alpha", fm="priority: P0\ntier: 1\ncode: [web]\n")
2815
2833
  _task(e / "queue", "beta", fm="priority: P0\ntier: 1\ncode: [tools]\n")
2816
- with _work_dir(tmp) as root:
2817
- os.environ["WORK_INSTANCE"] = "one"
2818
- shift.cmd_next({})
2819
- os.environ["WORK_INSTANCE"] = "two"
2820
- shift.cmd_next({})
2821
- os.environ.pop("WORK_INSTANCE")
2834
+ try:
2835
+ os.environ["WORK_SESSIONS_DIR"] = reg
2836
+ with _work_dir(tmp) as root:
2837
+ _next_as("one")
2838
+ _next_as("two")
2839
+ taken = _taken_by(root)
2840
+ assert set(taken) == {"one", "two"}, taken
2841
+ assert taken["one"] != taken["two"], "two instances took the same task"
2842
+ finally:
2843
+ os.environ.pop("WORK_SESSIONS_DIR", None)
2822
2844
 
2823
- held = {}
2824
- for t in model.scan(root)["versions"][0].all_tasks():
2825
- c = shift.read_claim(t.folder)
2826
- if c:
2827
- held[c["instance"]] = t.name
2828
- assert set(held) == {"one", "two"}, held
2829
- assert held["one"] != held["two"], "two instances took the same task"
2830
2845
 
2846
+ def test_the_loop_holds_back_from_an_area_somebody_may_still_be_working():
2847
+ # Two runs on one seam is how parallel work produces two answers to one design
2848
+ # question. There is no lease any more: who holds an area is read from the commits
2849
+ # that took its items, and an area is free once everyone holding it has ENDED —
2850
+ # which is a thing only a session the list could have named can be said to be.
2851
+ first = "11111111-1111-4111-8111-111111111111"
2852
+ second = "22222222-2222-4222-8222-222222222222"
2853
+ with tempfile.TemporaryDirectory() as tmp, tempfile.TemporaryDirectory() as reg:
2854
+ v = _tree(tmp)
2855
+ e = _epic(v, "an-epic")
2856
+ _task(e / "queue", "alpha", fm="priority: P0\ntier: 1\ncode: [web]\n")
2857
+ _task(e / "queue", "beta", fm="priority: P1\ntier: 1\ncode: [web]\n")
2858
+ registry = Path(reg) / "1.json"
2859
+ try:
2860
+ os.environ["WORK_SESSIONS_DIR"] = reg
2861
+ with _work_dir(tmp) as root:
2862
+ registry.write_text(json.dumps(
2863
+ {"sessionId": first, "name": "repo-a1", "pid": os.getpid()}))
2864
+ _next_as(first)
2865
+ _next_as(second)
2866
+ assert _taken_by(root) == {first: "alpha"}, \
2867
+ "the second run took an area a running session holds"
2868
+
2869
+ # The first session ends. Nothing had to expire and nobody had to drop
2870
+ # anything: its area is free the moment it is no longer running.
2871
+ registry.write_text(json.dumps(
2872
+ {"sessionId": first, "name": "repo-a1", "pid": 2 ** 22}))
2873
+ _next_as(second)
2874
+ assert _taken_by(root) == {first: "alpha", second: "beta"}, \
2875
+ "an area stayed held after everyone holding it had ended"
2876
+ finally:
2877
+ os.environ.pop("WORK_SESSIONS_DIR", None)
2878
+ peers._RUNNING.clear()
2831
2879
 
2832
- def test_a_claim_on_a_shared_code_region_blocks_the_second_instance():
2833
- # Two instances editing one seam is how parallel work produces two different
2834
- # answers to the same design question. The overlap `work.py code` already
2835
- # computed is what refuses it.
2836
- with tempfile.TemporaryDirectory() as tmp:
2880
+
2881
+ def test_a_run_this_machine_cannot_see_holds_its_area_rather_than_freeing_it():
2882
+ # A shift that names itself is never on the session list, so its absence from it
2883
+ # proves nothing. Reading that as ended would hand its area to a second run while
2884
+ # it may still be working — and all this decides is the loop holding back, which
2885
+ # is never the expensive mistake.
2886
+ second = "22222222-2222-4222-8222-222222222222"
2887
+ with tempfile.TemporaryDirectory() as tmp, tempfile.TemporaryDirectory() as reg:
2837
2888
  v = _tree(tmp)
2838
2889
  e = _epic(v, "an-epic")
2839
2890
  _task(e / "queue", "alpha", fm="priority: P0\ntier: 1\ncode: [web]\n")
2840
2891
  _task(e / "queue", "beta", fm="priority: P1\ntier: 1\ncode: [web]\n")
2841
- with _work_dir(tmp) as root:
2842
- os.environ["WORK_INSTANCE"] = "one"
2843
- shift.cmd_next({})
2844
- os.environ["WORK_INSTANCE"] = "two"
2845
- shift.cmd_next({})
2846
- os.environ.pop("WORK_INSTANCE")
2847
- claims = [c["instance"] for c, _ in shift._live_claims(model.scan(root))]
2848
- assert claims == ["one"], f"the second instance took a conflicting seam: {claims}"
2892
+ try:
2893
+ os.environ["WORK_SESSIONS_DIR"] = reg
2894
+ with _work_dir(tmp) as root:
2895
+ _next_as("nightly-shift")
2896
+ _next_as(second)
2897
+ assert _taken_by(root) == {"nightly-shift": "alpha"}, \
2898
+ "a run nothing here can see had its area taken from under it"
2899
+ finally:
2900
+ os.environ.pop("WORK_SESSIONS_DIR", None)
2901
+ peers._RUNNING.clear()
2849
2902
 
2850
2903
 
2851
- def test_an_expired_lease_is_reaped_so_a_crashed_instance_frees_its_work():
2852
- with tempfile.TemporaryDirectory() as tmp:
2904
+ def test_an_instance_resumes_its_own_take_rather_than_taking_more():
2905
+ # One instance, one task. What it resumes is the take recorded under the name it is
2906
+ # known by — so a name given as a flag has to be the name the take is recorded
2907
+ # under, or the run would read its own take as somebody else's and move on.
2908
+ with tempfile.TemporaryDirectory() as tmp, tempfile.TemporaryDirectory() as reg:
2853
2909
  v = _tree(tmp)
2854
2910
  e = _epic(v, "an-epic")
2855
- d = _task(e / "in-progress", "alpha")
2856
- (d / ".claim").write_text(json.dumps(
2857
- {"instance": "dead", "taken": "2020-01-01T00:00:00+00:00",
2858
- "expires": "2020-01-01T04:00:00+00:00"}))
2859
- assert shift.read_claim(d) is None
2860
- # An unparseable lease is also absent — a lock nobody can read must not be
2861
- # able to strand work forever.
2862
- (d / ".claim").write_text("{not json")
2863
- assert shift.read_claim(d) is None
2911
+ _task(e / "queue", "alpha", fm="priority: P0\ntier: 1\ncode: [web]\n")
2912
+ _task(e / "queue", "beta", fm="priority: P1\ntier: 1\ncode: [tools]\n")
2913
+ try:
2914
+ os.environ["WORK_SESSIONS_DIR"] = reg
2915
+ with _work_dir(tmp) as root:
2916
+ for _ in range(2):
2917
+ try:
2918
+ shift.cmd_next({"instance": "nightly-shift"})
2919
+ finally:
2920
+ os.environ.pop("WORK_INSTANCE", None)
2921
+ assert _taken_by(root) == {"nightly-shift": "alpha"}, \
2922
+ "it took a second task instead of resuming its first"
2923
+ finally:
2924
+ os.environ.pop("WORK_SESSIONS_DIR", None)
2925
+ peers._RUNNING.clear()
2926
+
2927
+
2928
+ def test_status_names_who_took_each_item_from_the_commits_not_a_lease():
2929
+ # One answer to who is on an item. `status` read a lease first and the history only
2930
+ # when there was none, so the one screen a shift is read from and the board could
2931
+ # name different people. It reads the takes now, the way every other door does.
2932
+ import contextlib
2933
+ import io
2934
+ running = "11111111-1111-4111-8111-111111111111"
2935
+ gone = "33333333-3333-4333-8333-333333333333"
2936
+ with tempfile.TemporaryDirectory() as tmp, tempfile.TemporaryDirectory() as reg:
2937
+ v = _tree(tmp)
2938
+ e = _epic(v, "an-epic")
2939
+ for name in ("alpha", "beta", "gamma"):
2940
+ _task(e / "in-progress", name, fm="priority: P1\ntier: 1\n")
2941
+ (Path(reg) / "1.json").write_text(json.dumps(
2942
+ {"sessionId": running, "name": "repo-a1", "pid": os.getpid()}))
2943
+ try:
2944
+ os.environ["WORK_SESSIONS_DIR"] = reg
2945
+ with _work_dir(tmp) as root:
2946
+ for who, name in ((running, "alpha"), (gone, "beta")):
2947
+ os.environ["WORK_INSTANCE"] = who
2948
+ events.append(root, "moved", name, **{"from": "queue", "to": "in-progress"})
2949
+ os.environ["WORK_INSTANCE"] = "44444444-4444-4444-8444-444444444444"
2950
+ out = io.StringIO()
2951
+ with contextlib.redirect_stdout(out):
2952
+ shift.cmd_status({})
2953
+ said = out.getvalue()
2954
+ # The same derivation answers the note a person gets before moving it.
2955
+ assert "repo-a1" in shift.somebody_else(root, "alpha")
2956
+ assert shift.somebody_else(root, "beta") == "", \
2957
+ "a take whose run has ended is not somebody still on it"
2958
+ assert "repo-a1" in said and "still running" in said, said
2959
+ assert "ENDED" in said, "a take whose run has ended has to say so"
2960
+ assert "never taken" in said, "and an item nobody took has to say that"
2961
+ assert "lease" not in said
2962
+ finally:
2963
+ for key in ("WORK_SESSIONS_DIR", "WORK_INSTANCE"):
2964
+ os.environ.pop(key, None)
2965
+ peers._RUNNING.clear()
2864
2966
 
2865
2967
 
2866
2968
  def test_a_task_above_the_ceiling_is_never_taken_unattended():
@@ -3086,9 +3188,6 @@ def test_a_passing_verify_run_is_recorded_and_clears_that_gate():
3086
3188
  # moment its last task lands, so the folder is already elsewhere.
3087
3189
  done = model.locate(root, "alpha")
3088
3190
  assert done.status == "complete"
3089
- # The lease does not survive the work: `status` would otherwise
3090
- # report a live instance on a finished task.
3091
- assert not (done.folder / ".claim").exists()
3092
3191
  finally:
3093
3192
  gate.VERIFY = old
3094
3193
 
@@ -3712,8 +3811,7 @@ def test_a_failing_run_is_described_by_what_the_gate_said():
3712
3811
  assert "abcdef12" in said
3713
3812
 
3714
3813
 
3715
- def test_coordination_files_never_enter_a_commit():
3716
- assert git.CLAIM in git.LOCAL
3814
+ def test_a_verify_run_never_enters_a_commit():
3717
3815
  assert gate.RUN in git.LOCAL
3718
3816
 
3719
3817
 
@@ -3909,7 +4007,7 @@ def test_the_autonomy_ceiling_is_validated_and_never_silently_wrong():
3909
4007
  for bad in (4, -1, "2", True, None):
3910
4008
  try:
3911
4009
  config._validate({**config.DEFAULTS,
3912
- "autonomy": {"ceiling": bad, "lease_minutes": 240}})
4010
+ "autonomy": {"ceiling": bad}})
3913
4011
  assert False, f"expected a refusal for {bad!r}"
3914
4012
  except config.ConfigError:
3915
4013
  pass
@@ -4614,28 +4712,37 @@ def test_the_record_round_trips_out_of_the_commits():
4614
4712
  config.apply(config.DEFAULTS)
4615
4713
 
4616
4714
 
4617
- def test_a_hold_never_enters_git():
4618
- # A claim belongs to a machine and a moment. A replicated one is two endpoints
4619
- # holding the same item with neither knowing which is why holds are filtered
4620
- # out of the commit whatever `git.paths` says, and dropped from the record.
4715
+ def test_migrate_sweeps_the_lease_files_the_deleted_lease_left_behind():
4716
+ # Nothing reads a `.claim` any more and git no longer filters one out of a commit,
4717
+ # so a leftover would ride into the next board write in its folder. The migration a
4718
+ # repo already runs for layout changes is where it goes — and the file it removes,
4719
+ # being untracked, must not be what stops it running.
4720
+ from harness import migrate
4621
4721
  with tempfile.TemporaryDirectory() as tmp:
4622
- try:
4623
- repo = _git_repo(tmp)
4624
- folder = repo / "work" / "versions" / "v" / "e" / "in-progress" / "held"
4625
- folder.mkdir(parents=True)
4626
- (folder / "task.md").write_text("---\npriority: P0\n---\n\n# H\n")
4627
- (folder / ".claim").write_text('{"instance": "someone"}\n')
4628
- _board_write(repo, "held", [{"event": "moved", "name": "held"}])
4629
- listed = _git(repo, "ls-files").stdout
4630
- assert "held/task.md" in listed
4631
- assert ".claim" not in listed, "a claim must never be committed"
4632
-
4633
- events._PENDING.clear()
4634
- events.append(repo / "work", "claimed", "held", expires="2030-01-01")
4635
- assert events.pending() == [], "a hold is not board history"
4636
- events._PENDING.clear()
4637
- finally:
4638
- config.apply(config.DEFAULTS)
4722
+ v = _tree(tmp)
4723
+ e = _epic(v, "an-epic")
4724
+ held = _task(e / "in-progress", "alpha")
4725
+ (held / ".claim").write_text('{"instance": "someone"}\n')
4726
+ with _work_dir(tmp):
4727
+ migrate.cmd_migrate({"dry-run": True})
4728
+ assert (held / ".claim").exists(), "a dry run changes nothing"
4729
+ migrate.cmd_migrate({})
4730
+ assert not (held / ".claim").exists(), "the leftover was left to be committed"
4731
+ assert (held / "task.md").exists(), "and nothing but the leftover was removed"
4732
+
4733
+ # A repo still on its old backlog gets both at once — and the backlog moves first,
4734
+ # carrying any leftover inside it to a path the sweep never listed.
4735
+ import shutil
4736
+ with tempfile.TemporaryDirectory() as tmp:
4737
+ root = Path(tmp) / "work"
4738
+ _tree(str(root))
4739
+ shutil.rmtree(root / "versions" / "backlog")
4740
+ later = _task(root / "backlog" / "an-epic", "later")
4741
+ (later / ".claim").write_text('{"instance": "someone"}\n')
4742
+ with _work_dir(str(root)):
4743
+ migrate.cmd_migrate({})
4744
+ assert not (root / "backlog").exists(), "the old backlog did not move"
4745
+ assert not list(root.rglob(".claim")), "a leftover the backlog carried was not swept"
4639
4746
 
4640
4747
 
4641
4748
  def test_a_commit_leaves_a_sessions_own_code_alone():
@@ -4965,23 +5072,28 @@ def test_a_held_task_names_a_session_you_can_actually_reach():
4965
5072
  os.environ["WORK_SESSIONS_DIR"] = tmp
4966
5073
  # A registry that exists and holds exactly one running session. The pid
4967
5074
  # is this test's own, because the check is whether the process is there.
5075
+ # Shaped like the ids the registry publishes, because only an id of that
5076
+ # shape can be declared ended by its absence from it.
5077
+ theirs = "11111111-1111-4111-8111-111111111111"
5078
+ alive = "22222222-2222-4222-8222-222222222222"
5079
+ ghost = "33333333-3333-4333-8333-333333333333"
4968
5080
  (Path(tmp) / "1.json").write_text(json.dumps(
4969
- {"sessionId": "alive-2222", "name": "repo-a8", "pid": os.getpid()}))
5081
+ {"sessionId": alive, "name": "repo-a8", "pid": os.getpid()}))
4970
5082
  # …and one entry a hard kill left behind. It must not read as running.
4971
5083
  (Path(tmp) / "2.json").write_text(json.dumps(
4972
- {"sessionId": "ghost-3333", "name": "repo-b7", "pid": 2 ** 22}))
5084
+ {"sessionId": ghost, "name": "repo-b7", "pid": 2 ** 22}))
4973
5085
 
4974
5086
  assert "this session" in peers.describe("mine-0000")
4975
5087
 
4976
- away = peers.describe("theirs-1111", "other-box")
5088
+ away = peers.describe(theirs, "other-box")
4977
5089
  assert "another machine" in away and "SendMessage" not in away, \
4978
5090
  "a name you cannot deliver to is worse than saying you cannot"
4979
5091
 
4980
- live = peers.describe("alive-2222", "this-box")
5092
+ live = peers.describe(alive, "this-box")
4981
5093
  assert "repo-a8" in live and "SendMessage" in live, \
4982
5094
  "a running peer is reachable — hand over the address, not a prefix"
4983
5095
 
4984
- for gone in ("theirs-1111", "ghost-3333"):
5096
+ for gone in (theirs, ghost):
4985
5097
  ended = peers.describe(gone, "this-box")
4986
5098
  assert "ENDED" in ended and "SendMessage" not in ended, \
4987
5099
  "a session that is not running cannot be messaged — say so"
@@ -4990,7 +5102,7 @@ def test_a_held_task_names_a_session_you_can_actually_reach():
4990
5102
  # that everyone is dead. It must fall back to what it always said.
4991
5103
  os.environ["WORK_SESSIONS_DIR"] = str(Path(tmp) / "nothing-here")
4992
5104
  assert peers.running() is None
4993
- unknown = peers.describe("theirs-1111", "this-box")
5105
+ unknown = peers.describe(theirs, "this-box")
4994
5106
  assert "SendMessage" in unknown and "ENDED" not in unknown, \
4995
5107
  "a client that publishes nothing must not make every peer look dead"
4996
5108
  finally:
@@ -5120,13 +5232,14 @@ def test_a_session_running_twice_is_never_resolved_to_one_of_its_names():
5120
5232
 
5121
5233
  # The real pair, with the filenames from the sighting: the low pid sorts
5122
5234
  # LAST lexically, so whichever half a reader picked was arbitrary.
5235
+ forked = "44444444-4444-4444-8444-444444444444"
5123
5236
  for pid, name, door in ((20558, "jarvis-9a", "claude-vscode"),
5124
5237
  (15750, "the harness stops", "cli")):
5125
5238
  (reg / f"{pid}.json").write_text(json.dumps(
5126
- {"sessionId": "forked-4444", "name": name, "cwd": str(here),
5239
+ {"sessionId": forked, "name": name, "cwd": str(here),
5127
5240
  "entrypoint": door, "pid": os.getpid()}))
5128
5241
 
5129
- said = peers.describe("forked-4444", "this-box")
5242
+ said = peers.describe(forked, "this-box")
5130
5243
  assert "jarvis-9a" in said and "the harness stops" in said, \
5131
5244
  "a reader that cannot pick has to be handed both, not one of them"
5132
5245
  assert "2 processes on ONE session" in said
@@ -5143,15 +5256,46 @@ def test_a_session_running_twice_is_never_resolved_to_one_of_its_names():
5143
5256
  # exactly as it did before any of this.
5144
5257
  (reg / "20558.json").unlink()
5145
5258
  peers._RUNNING.clear()
5146
- alone = peers.describe("forked-4444", "this-box")
5259
+ alone = peers.describe(forked, "this-box")
5147
5260
  assert "still running" in alone and "the harness stops" in alone
5148
5261
  assert "processes" not in alone
5262
+
5263
+ # A shift that names itself was never going to be on that list, so its
5264
+ # absence proves nothing — and the line must not promise an address a
5265
+ # named shift or a script cannot be messaged at.
5266
+ shift_line = peers.describe("nightly-shift", "this-box")
5267
+ assert "ENDED" not in shift_line and "SendMessage" not in shift_line
5268
+ assert "cannot be told" in shift_line
5149
5269
  finally:
5150
5270
  for key in ("WORK_INSTANCE", "WORK_MACHINE", "WORK_SESSIONS_DIR"):
5151
5271
  os.environ.pop(key, None)
5152
5272
  peers._RUNNING.clear()
5153
5273
 
5154
5274
 
5275
+ def test_both_readers_of_the_board_history_agree_about_who_holds_each_item():
5276
+ # Who is on an item is derived twice — here from what `git._events` parses, and by
5277
+ # the board from what `board.git.ts` reads — and the two had already come to name
5278
+ # different holders: a take on an item already in progress records a run and no
5279
+ # event, which the board counted and this reader threw away. The TypeScript half
5280
+ # asserting these same commits is in packages/board/src/board.test.ts.
5281
+ from harness import holders
5282
+ cases = json.loads((Path(__file__).parent / "harness" / "holders.cases.json")
5283
+ .read_text(encoding="utf-8"))["cases"]
5284
+ assert cases, "the shared cases file has to actually hold cases"
5285
+ for case in cases:
5286
+ rows = []
5287
+ for commit in case["commits"]:
5288
+ record = "\x1f".join((commit["sha"], commit["when"], commit["author"],
5289
+ commit["message"]))
5290
+ rows.extend(git._events(record, eventless=True))
5291
+ # An item nobody took reads as having no holders, however a reader represents
5292
+ # that — so an empty list and an absent key compare the same.
5293
+ got = {name: [{"session": h["session"], "machine": h["machine"],
5294
+ "tookOverFrom": h.get("took_over_from")} for h in found]
5295
+ for name, found in holders.to_holders_by_item(rows).items() if found}
5296
+ assert got == case["expect"], case["name"]
5297
+
5298
+
5155
5299
  def test_a_session_starts_knowing_who_else_is_in_the_checkout():
5156
5300
  # A session started blind: the one block it cannot skip said nothing about
5157
5301
  # another session, so two runs picked up overlapping work and found out when one
@@ -5223,21 +5367,6 @@ def test_how_long_ago_is_said_in_the_unit_that_changes_the_decision():
5223
5367
  assert shift._ago(broken) == "at an unrecorded time"
5224
5368
 
5225
5369
 
5226
- def test_a_claim_records_which_machine_took_it():
5227
- # A claim never enters git, so the pairing of session-to-machine exists nowhere
5228
- # else. Without it every hold reads as local and half of them are lies.
5229
- with tempfile.TemporaryDirectory() as tmp:
5230
- os.environ["WORK_MACHINE"] = "the-box-that-took-it"
5231
- try:
5232
- folder = Path(tmp)
5233
- claim = shift._write_claim(folder, "some-session")
5234
- assert claim["machine"] == "the-box-that-took-it"
5235
- assert json.loads((folder / ".claim").read_text())["machine"] \
5236
- == "the-box-that-took-it"
5237
- finally:
5238
- os.environ.pop("WORK_MACHINE", None)
5239
-
5240
-
5241
5370
  def test_the_board_moving_under_you_says_what_moved_and_who_moved_it():
5242
5371
  # The question at this moment is not "did a fetch happen" — a session can see
5243
5372
  # that. It is whether the plan it is holding is still the plan, and who to ask.
@@ -5246,11 +5375,14 @@ def test_the_board_moving_under_you_says_what_moved_and_who_moved_it():
5246
5375
  os.environ["WORK_INSTANCE"] = "mine-0000"
5247
5376
  os.environ["WORK_MACHINE"] = "this-box"
5248
5377
  os.environ["WORK_SESSIONS_DIR"] = tmp
5378
+ # Shaped like the ids the registry publishes, because only an id of that
5379
+ # shape can be declared ended by its absence from it.
5380
+ theirs = "11111111-1111-4111-8111-111111111111"
5249
5381
  (Path(tmp) / "1.json").write_text(json.dumps(
5250
- {"sessionId": "theirs-1111", "name": "repo-c4", "pid": os.getpid()}))
5382
+ {"sessionId": theirs, "name": "repo-c4", "pid": os.getpid()}))
5251
5383
  note = peers.arrivals([
5252
- {"name": "the-plan-changed", "by": "theirs-1111", "machine": "this-box"},
5253
- {"name": "and-this-one", "by": "theirs-1111", "machine": "this-box"},
5384
+ {"name": "the-plan-changed", "by": theirs, "machine": "this-box"},
5385
+ {"name": "and-this-one", "by": theirs, "machine": "this-box"},
5254
5386
  ])
5255
5387
  assert "the-plan-changed" in note and "and-this-one" in note
5256
5388
  assert "SendMessage" in note, "naming who moved it is the point of saying it"
@@ -5258,9 +5390,11 @@ def test_the_board_moving_under_you_says_what_moved_and_who_moved_it():
5258
5390
  # The same board move, made by somebody who has since finished. Still
5259
5391
  # worth reporting — the plan did change under you — but there is nobody
5260
5392
  # left to ask about it, and saying otherwise sends the reader nowhere.
5393
+ left = "99999999-9999-4999-8999-999999999999"
5261
5394
  gone = peers.arrivals([
5262
- {"name": "the-plan-changed", "by": "left-9999", "machine": "this-box"}])
5395
+ {"name": "the-plan-changed", "by": left, "machine": "this-box"}])
5263
5396
  assert "the-plan-changed" in gone and "SendMessage" not in gone
5397
+ assert "ENDED" in gone, "finished is a claim the list can make about a session"
5264
5398
  # Your OWN commits coming back round are not news, and a line that fires
5265
5399
  # on every pull is one people stop reading.
5266
5400
  assert peers.arrivals([{"name": "x", "by": "mine-0000",
@@ -6506,7 +6640,7 @@ def test_every_shipped_default_is_mechanics_or_nothing():
6506
6640
  # fact about somebody's setup and ships as null.
6507
6641
  allowed = {
6508
6642
  "ids.prefix", "domains.order", "product.tiers",
6509
- "autonomy.ceiling", "autonomy.lease_minutes", "git.remote", "git.paths",
6643
+ "autonomy.ceiling", "git.remote", "git.paths",
6510
6644
  "coverage.shard", "hooks.session_start.enabled", "hooks.stop.enabled",
6511
6645
  # On by default like its two siblings, and for their reason: a hook the
6512
6646
  # harness ships is its own mechanics, not a fact about somebody's setup.
@@ -7566,19 +7700,6 @@ def test_every_event_the_harness_records_is_one_the_record_knows():
7566
7700
  f"recorded but not in KINDS, so it never reaches a digest: {sorted(called - set(events.KINDS))}"
7567
7701
 
7568
7702
 
7569
- def test_a_claim_nobody_can_read_reads_as_absent_rather_than_raising():
7570
- # `read_claim` promises an unreadable claim reads as absent, and the comparison
7571
- # threw rather than the parse: a `.claim` written without a timezone parsed
7572
- # fine and then could not be compared against an aware now. One malformed file
7573
- # took down every command that asks who is holding an item.
7574
- with tempfile.TemporaryDirectory() as tmp:
7575
- folder = Path(tmp) / "a-task"
7576
- folder.mkdir()
7577
- (folder / shift.CLAIM).write_text(json.dumps(
7578
- {"instance": "someone", "expires": "2099-01-01T00:00:00"}))
7579
- assert shift.read_claim(folder) is None
7580
-
7581
-
7582
7703
  def test_the_lowest_tier_the_vocabulary_defines_can_actually_be_set():
7583
7704
  # `TIERS` and `TIER_MEANING` both define tier 0 and the method's own table
7584
7705
  # documents it — reversible and local, a typo, a comment, a test name — and it
package/harness/work.py CHANGED
@@ -100,7 +100,7 @@ Subcommands:
100
100
  The unattended loop — a schedule calls `next`, and the rest exists so a run that
101
101
  cannot finish has somewhere to put the reason instead of stalling or guessing:
102
102
 
103
- next [--instance <id>] [--peek] take ONE task and claim it; prints the read order
103
+ next [--instance <id>] [--peek] take ONE task; prints the read order
104
104
  status what shipped · what waits on you · what is at risk
105
105
  peers who else is working this checkout, and on what.
106
106
  The session block says it at startup; this asks
@@ -119,7 +119,6 @@ cannot finish has somewhere to put the reason instead of stalling or guessing:
119
119
  park a question, move on — exits 0, never blocks
120
120
  needs every open question, oldest first
121
121
  answer <task> --choose "…" resolve it and put the task back to work
122
- drop <name> [--why "…"] give a claim back
123
122
  log [--since DATE] [--task <n>] · digest [--since DATE]
124
123
  sync pull the board fresh; send anything unpushed
125
124
 
@@ -172,7 +171,7 @@ from harness.config import (DEFAULTS, ConfigError, apply, cmd_applies, cmd_confi
172
171
  from harness.safety import cmd_doctor, cmd_id_new
173
172
  from harness.autonomy import cmd_answer, cmd_ask, cmd_digest, cmd_log, cmd_needs
174
173
  from harness.peers import cmd_peers
175
- from harness.shift import cmd_drop, cmd_next, cmd_status
174
+ from harness.shift import cmd_next, cmd_status
176
175
  from harness.gate import cmd_observed, cmd_verify
177
176
  from harness.kickoff import cmd_kickoff
178
177
  from harness.git import cmd_sync
@@ -189,7 +188,7 @@ SUBCOMMANDS = (
189
188
  "epic-new", "feature-new", "version-new", "place", "handoff", "release", "archive",
190
189
  "find", "list", "readme", "move", "plan", "session", "kickoff", "path", "code",
191
190
  "domain-new", "system-new", "where", "rules", "align", "wrap", "coverage",
192
- "migrate", "next", "status", "drop", "ask", "answer", "needs", "verify",
191
+ "migrate", "next", "status", "ask", "answer", "needs", "verify",
193
192
  "observed", "log", "digest", "sync", "check", "links", "peers",
194
193
  )
195
194
 
@@ -568,10 +567,6 @@ def dispatch(cmd, pos, flags, cfg) -> int:
568
567
  return cmd_status(flags)
569
568
  if cmd == "peers":
570
569
  return cmd_peers(flags)
571
- if cmd == "drop":
572
- if not pos:
573
- die("usage: jarvis work drop <name> [--why \"…\"]")
574
- return cmd_drop({"name": pos[0], **flags})
575
570
  if cmd == "ask":
576
571
  if not pos:
577
572
  die("usage: jarvis work ask <task> --question \"…\" "
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appchy/jarvis",
3
- "version": "0.1.129",
3
+ "version": "0.1.130",
4
4
  "description": "Jarvis — local AI coding assistant CLI",
5
5
  "private": false,
6
6
  "type": "module",
@@ -65,8 +65,8 @@
65
65
  "@jarvis/rpc": "1.0.0",
66
66
  "@jarvis/types": "1.0.0",
67
67
  "@jarvis/typescript-config": "1.0.0",
68
- "@jarvis/ui": "0.1.0",
69
- "@jarvis/vitest-config": "1.0.0"
68
+ "@jarvis/vitest-config": "1.0.0",
69
+ "@jarvis/ui": "0.1.0"
70
70
  },
71
71
  "scripts": {
72
72
  "dev": "tsx watch src/bin.ts start --foreground",