@appchy/jarvis 0.1.123 → 0.1.124

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.
@@ -5067,6 +5067,139 @@ def test_a_refusal_about_a_shared_tree_names_who_else_is_in_it():
5067
5067
  peers._RUNNING.clear()
5068
5068
 
5069
5069
 
5070
+ def test_both_readers_of_the_session_registry_agree_about_every_case():
5071
+ # One directory, two readers, two languages — the harness here and the board's
5072
+ # TypeScript, which reads it directly rather than paying a subprocess on every
5073
+ # board list. That is exactly where one session comes to be described two ways,
5074
+ # so the cases live in a file both suites load and both assert the same answer
5075
+ # from. The TypeScript half of this is in packages/board/src/board.test.ts; a
5076
+ # drift between them goes red in both rather than being reconciled by hand.
5077
+ cases = json.loads((Path(__file__).parent / "harness" / "peers.cases.json")
5078
+ .read_text(encoding="utf-8"))["cases"]
5079
+ assert cases, "the shared cases file has to actually hold cases"
5080
+ for case in cases:
5081
+ with tempfile.TemporaryDirectory() as tmp:
5082
+ try:
5083
+ reg = Path(tmp) / "sessions"
5084
+ if case["sessions"] is not None:
5085
+ reg.mkdir()
5086
+ for entry in case["sessions"]:
5087
+ if "raw" in entry:
5088
+ (reg / entry["file"]).write_text(entry["raw"])
5089
+ continue
5090
+ body = {k: v for k, v in entry.items() if k != "file"}
5091
+ # A pid is a marker rather than a number: only the running
5092
+ # process is a pid a test can be sure is alive.
5093
+ body["pid"] = os.getpid() if body["pid"] == "self" else 2 ** 22
5094
+ (reg / entry["file"]).write_text(json.dumps(body))
5095
+ os.environ["WORK_SESSIONS_DIR"] = str(reg)
5096
+ peers._RUNNING.clear()
5097
+ assert peers.reach(case["ask"]) == case["expect"], case["name"]
5098
+ finally:
5099
+ os.environ.pop("WORK_SESSIONS_DIR", None)
5100
+ peers._RUNNING.clear()
5101
+
5102
+
5103
+ def test_a_session_running_twice_is_never_resolved_to_one_of_its_names():
5104
+ # Opening a jarvis-started session in a second local client starts a SECOND
5105
+ # process on the same id rather than attaching to the first, so both are alive
5106
+ # and both append to one transcript. This used to key one entry per id while
5107
+ # walking sorted filenames, so the later name silently won and which one that
5108
+ # was depended on how two pids happened to sort. Measured on a real machine,
5109
+ # five of seventeen live ids were doubled and the winner flipped pair to pair.
5110
+ with tempfile.TemporaryDirectory() as tmp:
5111
+ try:
5112
+ here = Path(tmp) / "repo"
5113
+ here.mkdir()
5114
+ reg = Path(tmp) / "sessions"
5115
+ reg.mkdir()
5116
+ os.environ["WORK_INSTANCE"] = "mine-0000"
5117
+ os.environ["WORK_MACHINE"] = "this-box"
5118
+ os.environ["WORK_SESSIONS_DIR"] = str(reg)
5119
+ peers._RUNNING.clear()
5120
+
5121
+ # The real pair, with the filenames from the sighting: the low pid sorts
5122
+ # LAST lexically, so whichever half a reader picked was arbitrary.
5123
+ for pid, name, door in ((20558, "jarvis-9a", "claude-vscode"),
5124
+ (15750, "the harness stops", "cli")):
5125
+ (reg / f"{pid}.json").write_text(json.dumps(
5126
+ {"sessionId": "forked-4444", "name": name, "cwd": str(here),
5127
+ "entrypoint": door, "pid": os.getpid()}))
5128
+
5129
+ said = peers.describe("forked-4444", "this-box")
5130
+ assert "jarvis-9a" in said and "the harness stops" in said, \
5131
+ "a reader that cannot pick has to be handed both, not one of them"
5132
+ assert "2 processes on ONE session" in said
5133
+ assert "an editor" in said and "a terminal" in said, \
5134
+ "which half is which is published on disk — it costs nothing to say"
5135
+
5136
+ # The checkout line counts SESSIONS, and says why it has more names.
5137
+ line = peers.sharing(here)
5138
+ assert "1 other session is" in line, "two processes are still one session"
5139
+ assert "jarvis-9a" in line and "the harness stops" in line
5140
+ assert "two processes sharing a transcript" in line
5141
+
5142
+ # And one process on an id is untouched — the ordinary case must read
5143
+ # exactly as it did before any of this.
5144
+ (reg / "20558.json").unlink()
5145
+ peers._RUNNING.clear()
5146
+ alone = peers.describe("forked-4444", "this-box")
5147
+ assert "still running" in alone and "the harness stops" in alone
5148
+ assert "processes" not in alone
5149
+ finally:
5150
+ for key in ("WORK_INSTANCE", "WORK_MACHINE", "WORK_SESSIONS_DIR"):
5151
+ os.environ.pop(key, None)
5152
+ peers._RUNNING.clear()
5153
+
5154
+
5155
+ def test_a_session_starts_knowing_who_else_is_in_the_checkout():
5156
+ # A session started blind: the one block it cannot skip said nothing about
5157
+ # another session, so two runs picked up overlapping work and found out when one
5158
+ # overwrote the other. It says who, what they are on, and how to reach them —
5159
+ # and says NOTHING when you are alone, because every session in every repo
5160
+ # running this harness reads it forever.
5161
+ with tempfile.TemporaryDirectory() as tmp:
5162
+ try:
5163
+ here = Path(tmp) / "repo"
5164
+ here.mkdir()
5165
+ reg = Path(tmp) / "sessions"
5166
+ reg.mkdir()
5167
+ os.environ["WORK_INSTANCE"] = "mine-0000"
5168
+ os.environ["WORK_MACHINE"] = "this-box"
5169
+ os.environ["WORK_SESSIONS_DIR"] = str(reg)
5170
+ peers._RUNNING.clear()
5171
+
5172
+ def publish(n, run, name, cwd, door="cli"):
5173
+ (reg / f"{n}.json").write_text(json.dumps(
5174
+ {"sessionId": run, "name": name, "cwd": str(cwd),
5175
+ "entrypoint": door, "pid": os.getpid()}))
5176
+
5177
+ publish(1, "mine-0000", "repo-me", here)
5178
+ assert peers.briefing(here, [], "main") == "", \
5179
+ "alone in the checkout is silence, not a line saying so"
5180
+
5181
+ publish(2, "peer-1111", "repo-a8", here)
5182
+ peers._RUNNING.clear()
5183
+ rows = [{"by": "peer-1111", "name": "make-the-thing", "event": "moved"}]
5184
+ said = peers.briefing(here, rows, "main")
5185
+ assert "repo-a8" in said, "who"
5186
+ assert "make-the-thing" in said, "on what"
5187
+ assert "branch main" in said, "on which branch"
5188
+ assert "SendMessage" in said, "and how to reach them"
5189
+ assert "repo-me" not in said, "never about yourself"
5190
+ assert "jarvis work peers" in said, "and a way to ask again"
5191
+
5192
+ # A peer holding nothing is still a peer worth knowing about — it is the
5193
+ # session about to take the work you were going to.
5194
+ peers._RUNNING.clear()
5195
+ nothing = peers.briefing(here, [], "main")
5196
+ assert "repo-a8" in nothing and "holding nothing" in nothing
5197
+ finally:
5198
+ for key in ("WORK_INSTANCE", "WORK_MACHINE", "WORK_SESSIONS_DIR"):
5199
+ os.environ.pop(key, None)
5200
+ peers._RUNNING.clear()
5201
+
5202
+
5070
5203
  def test_how_long_ago_is_said_in_the_unit_that_changes_the_decision():
5071
5204
  # The question this answers is *is anybody on this*, and the reader is deciding
5072
5205
  # whether to take the work. "6h ago" settles that; a timestamp makes them do
package/harness/work.py CHANGED
@@ -89,6 +89,9 @@ cannot finish has somewhere to put the reason instead of stalling or guessing:
89
89
 
90
90
  next [--instance <id>] [--peek] take ONE task and claim it; prints the read order
91
91
  status what shipped · what waits on you · what is at risk
92
+ peers who else is working this checkout, and on what.
93
+ The session block says it at startup; this asks
94
+ again, because the answer is stale as it prints
92
95
  method the METHOD in full — how work is done here. The session
93
96
  block names it; this is the body, on request.
94
97
  verify <name> [--start | --status] RUN verify.* (shell=False) and record the result.
@@ -155,6 +158,7 @@ from harness.config import (DEFAULTS, ConfigError, apply, cmd_applies, cmd_confi
155
158
  cmd_context, cmd_method, cmd_remind, resolve)
156
159
  from harness.safety import cmd_doctor, cmd_id_new
157
160
  from harness.autonomy import cmd_answer, cmd_ask, cmd_digest, cmd_log, cmd_needs
161
+ from harness.peers import cmd_peers
158
162
  from harness.shift import cmd_drop, cmd_next, cmd_status
159
163
  from harness.gate import cmd_observed, cmd_verify
160
164
  from harness.kickoff import cmd_kickoff
@@ -173,7 +177,7 @@ SUBCOMMANDS = (
173
177
  "find", "list", "readme", "move", "plan", "session", "kickoff", "path", "code",
174
178
  "domain-new", "system-new", "where", "rules", "align", "wrap", "coverage",
175
179
  "migrate", "next", "status", "drop", "ask", "answer", "needs", "verify",
176
- "observed", "log", "digest", "sync", "check", "links",
180
+ "observed", "log", "digest", "sync", "check", "links", "peers",
177
181
  )
178
182
 
179
183
 
@@ -366,7 +370,7 @@ def _say(note: str) -> None:
366
370
  #: `--branch` could never have meant anything for it.
367
371
  _READS = {"list", "find", "path", "code", "where", "rules", "status", "digest",
368
372
  "log", "needs", "align", "coverage", "readme", "config", "context",
369
- "doctor", "kickoff", "remind", "applies", "wrap"}
373
+ "doctor", "kickoff", "remind", "applies", "wrap", "peers"}
370
374
 
371
375
 
372
376
  def _reads_only(cmd: str, flags: dict) -> None:
@@ -546,6 +550,8 @@ def dispatch(cmd, pos, flags, cfg) -> int:
546
550
  return cmd_next(flags)
547
551
  if cmd == "status":
548
552
  return cmd_status(flags)
553
+ if cmd == "peers":
554
+ return cmd_peers(flags)
549
555
  if cmd == "drop":
550
556
  if not pos:
551
557
  die("usage: jarvis work drop <name> [--why \"…\"]")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appchy/jarvis",
3
- "version": "0.1.123",
3
+ "version": "0.1.124",
4
4
  "description": "Jarvis — local AI coding assistant CLI",
5
5
  "private": false,
6
6
  "type": "module",
@@ -63,10 +63,10 @@
63
63
  "@jarvis/errors": "1.0.0",
64
64
  "@jarvis/logger": "1.0.0",
65
65
  "@jarvis/rpc": "1.0.0",
66
- "@jarvis/vitest-config": "1.0.0",
66
+ "@jarvis/types": "1.0.0",
67
67
  "@jarvis/typescript-config": "1.0.0",
68
68
  "@jarvis/ui": "0.1.0",
69
- "@jarvis/types": "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",