@appchy/jarvis 0.1.45 → 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.
@@ -132,8 +132,29 @@ def _elapsed(data) -> int:
132
132
  return 0
133
133
 
134
134
 
135
+ #: How much of a failing gate's output is kept, per gate. A gate that fails is
136
+ #: usually failing for something in its last few dozen lines, and the whole of a
137
+ #: test run or a bundle build is megabytes — this is the amount that answers "why"
138
+ #: without turning the run file into a log archive or a caller's context into one.
139
+ _LOG_LINES = 40
140
+ _LOG_CHARS = 4000
141
+
142
+
135
143
  def run_verify(root, names=None, progress=None) -> tuple:
136
- """Run the configured verify commands. Returns (results, ok).
144
+ """Run the configured verify commands. Returns (results, ok), where a result is
145
+ (name, status, said, log).
146
+
147
+ **A gate that FAILS keeps its output; one that passes keeps none.** For a long
148
+ time only `said` survived — the last line, cut at 160 characters — and every
149
+ other byte the gate produced was captured and dropped on the floor. That is why
150
+ `build` could fail under `jarvis work verify`, pass when run by hand, and stay
151
+ undiagnosed across three sessions: the evidence was destroyed at the moment it
152
+ was produced, so the only way to learn anything was to run the thing again by
153
+ hand and hope it failed the same way. A gate nobody can diagnose is a gate
154
+ somebody eventually waives.
155
+
156
+ Passing gates keep nothing on purpose. Their output is noise, it is the common
157
+ case, and storing it would put megabytes through a file rewritten on every tick.
137
158
 
138
159
  A SKIP is reported separately and never counts as a pass — "there was no lint
139
160
  configured" and "lint passed" are different facts, and collapsing them is how a
@@ -153,30 +174,50 @@ def run_verify(root, names=None, progress=None) -> tuple:
153
174
  try:
154
175
  argv = shlex.split(cmd)
155
176
  except ValueError as e:
156
- results.append((name, "SKIP", f"unparseable command: {e}"))
177
+ results.append((name, "SKIP", f"unparseable command: {e}", ""))
157
178
  continue
158
179
  if not argv:
159
- results.append((name, "SKIP", "empty command"))
180
+ results.append((name, "SKIP", "empty command", ""))
160
181
  continue
161
182
  try:
162
183
  proc = subprocess.run(argv, cwd=repo, capture_output=True, text=True,
163
184
  timeout=1800)
164
185
  except FileNotFoundError:
165
- results.append((name, "SKIP", f"{argv[0]}: not found"))
186
+ results.append((name, "SKIP", f"{argv[0]}: not found", ""))
166
187
  continue
167
188
  except subprocess.TimeoutExpired:
168
- results.append((name, "FAIL", "timed out after 30m"))
189
+ results.append((name, "FAIL", "timed out after 30m", ""))
169
190
  continue
170
191
  except OSError as e:
171
- results.append((name, "SKIP", str(e)))
192
+ results.append((name, "SKIP", str(e), ""))
172
193
  continue
173
194
  tail = (proc.stderr or proc.stdout or "").strip().splitlines()
174
- results.append((name, "PASS" if proc.returncode == 0 else "FAIL",
175
- tail[-1][:160] if tail else f"exit {proc.returncode}"))
195
+ failed = proc.returncode != 0
196
+ results.append((name, "FAIL" if failed else "PASS",
197
+ tail[-1][:160] if tail else f"exit {proc.returncode}",
198
+ # BOTH streams, in that order: a build writes its diagnosis
199
+ # to stderr and its progress to stdout, and a tool suite does
200
+ # the opposite. Reading one of them is how a failure comes
201
+ # back as "exit 1" with nothing attached.
202
+ _keep(proc.stdout, proc.stderr) if failed else ""))
176
203
  ok = bool(results) and all(r[1] == "PASS" for r in results)
177
204
  return results, ok
178
205
 
179
206
 
207
+ def _keep(stdout: str, stderr: str) -> str:
208
+ """The tail of what a failing gate said, capped by lines and then by characters.
209
+
210
+ Both caps are needed and neither is enough alone: a Next.js build prints few,
211
+ enormous lines while a test suite prints thousands of short ones, so a line
212
+ budget alone lets one through and a character budget alone cuts the other to a
213
+ fragment of its last line.
214
+ """
215
+ text = "\n".join(part.strip() for part in (stdout, stderr) if part.strip())
216
+ lines = text.splitlines()[-_LOG_LINES:]
217
+ kept = "\n".join(lines)
218
+ return kept if len(kept) <= _LOG_CHARS else "…" + kept[-_LOG_CHARS:]
219
+
220
+
180
221
  def _record(root, name, results, ok, sha) -> bool:
181
222
  """Write the outcome onto the task. False when there is no such task."""
182
223
  task = locate(root, name)
@@ -184,7 +225,7 @@ def _record(root, name, results, ok, sha) -> bool:
184
225
  return False
185
226
  entry = (f"{date.today().isoformat()} {'pass' if ok else 'FAIL'} "
186
227
  f"{sha[:12] or 'no-git'} "
187
- + " ".join(f"{n}={s}" for n, s, _ in results))
228
+ + " ".join(f"{n}={s}" for n, s, *_ in results))
188
229
 
189
230
  def mutate(d):
190
231
  d["verified"] = as_list(d.get("verified")) + [entry]
@@ -192,7 +233,7 @@ def _record(root, name, results, ok, sha) -> bool:
192
233
 
193
234
  rewrite_file(task.folder / "task.md", mutate)
194
235
  events.append(root, "verified", name, ok=ok, sha=sha[:12] or None,
195
- results={n: s for n, s, _ in results})
236
+ results={n: s for n, s, *_ in results})
196
237
  return True
197
238
 
198
239
 
@@ -224,14 +265,15 @@ def _execute(root, name) -> tuple:
224
265
 
225
266
  def progress(gate, so_far):
226
267
  data["current"] = gate
227
- data["done"] = [n for n, _, _ in so_far]
268
+ data["done"] = [n for n, *_ in so_far]
228
269
  _write_run(root, data)
229
270
 
230
271
  results, ok = run_verify(root, progress=progress)
231
272
  sha = _head(root.parent)
232
- data.update(current="", done=[n for n, _, _ in results],
273
+ data.update(current="", done=[n for n, *_ in results],
233
274
  finished=_now().isoformat(), passed=ok, sha=sha,
234
- results=[{"name": n, "status": s, "said": d} for n, s, d in results])
275
+ results=[{"name": n, "status": s, "said": d, "log": g}
276
+ for n, s, d, g in results])
235
277
  _write_run(root, data)
236
278
 
237
279
  if name and not _record(root, name, results, ok, sha):
@@ -240,11 +282,19 @@ def _execute(root, name) -> tuple:
240
282
 
241
283
 
242
284
  def _print_report(results, sha) -> None:
243
- for n, status, detail in results:
285
+ for n, status, detail, *_ in results:
244
286
  mark = {"PASS": "✓", "FAIL": "✗"}.get(status, "·")
245
287
  print(f" {mark} {status:5} {n:14} {detail}")
246
288
  print(f"\n {sum(1 for r in results if r[1] == 'PASS')}/{len(results)} passed"
247
289
  + (f" · at {sha[:8]}" if sha else ""))
290
+ # What the failing gates actually said, after the table rather than inside it —
291
+ # the table is a shape somebody scans, and a gate's output is paragraphs. Only
292
+ # failures print: on a green run this adds nothing at all.
293
+ for n, status, _, log in ((r + ("",))[:4] for r in results):
294
+ if status == "FAIL" and log:
295
+ print(f"\n ── {n} ──")
296
+ for line in log.splitlines():
297
+ print(f" {line}")
248
298
 
249
299
 
250
300
  def _busy(running) -> str:
@@ -272,8 +322,27 @@ def _describe(data) -> str:
272
322
  return f"All {len(results)} gates passed{f' at {sha}' if sha else ''}."
273
323
  head = (f"{len(failed)} of {len(results)} gates did not pass"
274
324
  f"{f' at {sha}' if sha else ''} — fix these, then ask again:")
275
- return "\n".join([head] + [f" {r.get('name')}: {r.get('said') or 'no output'}"
276
- for r in failed])
325
+ return "\n".join([head] + [_failure(r) for r in failed])
326
+
327
+
328
+ #: How many of a failing gate's kept lines reach a CALLER, as opposed to the run
329
+ #: file. Deliberately smaller: a caller pays for these in context on every failed
330
+ #: verify, and the point here is to say enough that the next step is obvious without
331
+ #: re-running anything. The rest is in the run file for whoever needs the whole of it.
332
+ _SAID_LINES = 10
333
+
334
+
335
+ def _failure(row) -> str:
336
+ """One failing gate, as much as a caller needs to act without re-running it.
337
+
338
+ A caller told only `build: exit 1` has to leave the surface and run the build
339
+ itself to learn anything — which is the loop this exists to end, and the reason
340
+ three sessions in a row recorded `build` as "not diagnosed here".
341
+ """
342
+ name, said = row.get("name"), row.get("said") or "no output"
343
+ log = (row.get("log") or "").splitlines()[-_SAID_LINES:]
344
+ body = "".join(f"\n {line}" for line in log)
345
+ return f" {name}: {said}{body}"
277
346
 
278
347
 
279
348
  def _still_current(repo, sha: str) -> bool:
@@ -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
- One thing does move: if a push is rejected and the retry rebases, an autostash
38
- restores the session's own edits as working-tree changes rather than staged ones.
39
- The content survives; the staging does not.
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
- _, pre, _ = _git(repo, "rev-parse", "HEAD")
561
- code, _, rebase_err = _git(repo, "-c", "rebase.autoStash=true", "pull",
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 = f"the branch moved and the rebase onto it did not apply: {_tail(rebase_err)}"
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 _unstash(repo, pre: str) -> str:
578
- """Undo a pull whose autostash could not be put back. Returns why-not, or "".
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 board commit is never at risk it was made before any of this, and it is
588
- inside `pre`. It stays committed and unpushed, which is what `sync` is for.
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
- _, unmerged, _ = _git(repo, "diff", "--name-only", "--diff-filter=U")
591
- if not unmerged.strip() or not pre:
592
- return ""
593
- clash = "the branch moved, and your uncommitted edits clash with what was on it"
594
- # Every step is checked before the next one claims it happened. Saying "your
595
- # working tree is as you left it" over a reset that did not run would be the
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:
@@ -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 test_a_push_whose_autostash_conflicts_is_reported_and_the_tree_restored():
4933
- # `pull --rebase` exits 0 when the rebase lands and only the autostash pop
4934
- # conflicts, so the push went out and the caller was told the write was pushed —
4935
- # while the session's working tree was left holding conflict markers and its own
4936
- # edits sat in a stash nobody mentioned.
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"a push that left a conflict behind is not a push: {note}"
4964
- assert "NOT pushed" in note and "clash" in note, \
4965
- f"the caller has to be told what actually happened: {note}"
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 put back where their author left them"
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 is stranded in a stash nobody will look in"
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appchy/jarvis",
3
- "version": "0.1.45",
3
+ "version": "0.1.46",
4
4
  "description": "Jarvis — local AI coding assistant CLI",
5
5
  "private": false,
6
6
  "type": "module",
@@ -49,15 +49,15 @@
49
49
  "tsup": "^8.5.1",
50
50
  "typescript": "^5.7.0",
51
51
  "vitest": "^2.1.0",
52
- "@jarvis/board": "0.1.0",
52
+ "@jarvis/agents": "1.0.0",
53
53
  "@jarvis/anthropic": "1.0.0",
54
- "@jarvis/logger": "1.0.0",
54
+ "@jarvis/board": "0.1.0",
55
55
  "@jarvis/data": "0.1.0",
56
- "@jarvis/types": "1.0.0",
56
+ "@jarvis/logger": "1.0.0",
57
57
  "@jarvis/rpc": "1.0.0",
58
- "@jarvis/vitest-config": "1.0.0",
58
+ "@jarvis/types": "1.0.0",
59
59
  "@jarvis/typescript-config": "1.0.0",
60
- "@jarvis/agents": "1.0.0"
60
+ "@jarvis/vitest-config": "1.0.0"
61
61
  },
62
62
  "scripts": {
63
63
  "dev": "tsx watch src/bin.ts start --foreground",