@appchy/jarvis 0.1.73 → 0.1.74

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 CHANGED
@@ -10109,7 +10109,7 @@ import { createRequire as createRequire2 } from "module";
10109
10109
  var _require = createRequire2(import.meta.url);
10110
10110
  var VERSION2 = _require("../package.json").version ?? "0.0.0";
10111
10111
  var IS_DEV = String(_require("../package.json").name ?? "").endsWith("-dev");
10112
- var SHA = "9e5b550";
10112
+ var SHA = "9566241";
10113
10113
  var BUILT = "2026-09-10";
10114
10114
  var BUILD = SHA ?? "source";
10115
10115
  var BUILD_LABEL = `${SHA ? `${VERSION2} (${SHA}${BUILT ? ` ${BUILT}` : ""})` : `${VERSION2} (source)`}${IS_DEV ? " \u2014 development build" : ""}`;
@@ -51,11 +51,26 @@ def _uncommitted(repo: Path, work: Path) -> list:
51
51
  return sorted(dirty)
52
52
 
53
53
 
54
+ def _archived_cuts(root: Path) -> list:
55
+ """Cuts sitting in the archive's former home, `work/archive/versions/<cut>/`.
56
+
57
+ Every repo that ever archived a cut has these, and the layout change made them
58
+ UNREACHABLE: `locate_version` looks in `versions/` and `versions/archive/`, so
59
+ `path` and `where` answer "no version named that" for a record the repo still
60
+ holds. jarvis had archived nothing, which is why the first migration never met
61
+ this and three siblings each have seven to eleven of them.
62
+ """
63
+ old = root / "archive" / "versions"
64
+ if not old.is_dir():
65
+ return []
66
+ return sorted(p for p in old.iterdir() if (p / "version.md").is_file())
67
+
68
+
54
69
  def cmd_migrate(args) -> int:
55
- """`work/backlog/` moves inside `versions/`, and `versions/archive/` is made.
70
+ """`work/backlog/` and the archive both move inside `versions/`.
56
71
 
57
- Both halves of one layout: backlog and archive are states a cut's work is in,
58
- so they belong beside the cuts rather than in a directory of their own.
72
+ One layout: backlog and archive are states a cut's work is in, so they belong
73
+ beside the cuts rather than in directories of their own.
59
74
  """
60
75
  root = find_work_root()
61
76
  repo = root.parent
@@ -64,18 +79,26 @@ def cmd_migrate(args) -> int:
64
79
  old = root / "backlog"
65
80
  new = backlog_dir(root)
66
81
  archive = archive_dir(root)
82
+ cuts = _archived_cuts(root)
67
83
 
68
- # The one hard refusal. Both present means somebody has already started this,
69
- # or a repo has grown a second backlog — and merging two trees is a judgement
70
- # about which copy of a task is real, which is not a call a migration makes.
84
+ # The hard refusals. Two of anything means somebody has already started this,
85
+ # or a repo has grown a second copy — and merging two trees is a judgement
86
+ # about which copy is real, which is not a call a migration makes.
71
87
  if old.is_dir() and new.is_dir():
72
88
  die(f"both {rel(old, root)} and {rel(new, root)} exist — this repo has two "
73
89
  f"backlogs and only a person can say which task is the real one. "
74
90
  f"Merge them by hand, then run this again.")
91
+ clashes = [c.name for c in cuts if (archive / c.name).exists()]
92
+ if clashes:
93
+ die(f"{len(clashes)} cut(s) are archived in both places: "
94
+ f"{', '.join(clashes)}. Which record is the real one is yours to say — "
95
+ f"merge them by hand, then run this again.")
75
96
 
76
97
  todo = []
77
98
  if old.is_dir():
78
99
  todo.append("the backlog moves inside versions/")
100
+ if cuts:
101
+ todo.append(f"{len(cuts)} archived cut(s) move inside versions/")
79
102
  if not archive.is_dir():
80
103
  todo.append("versions/archive/ is created")
81
104
  if not todo:
@@ -88,6 +111,8 @@ def cmd_migrate(args) -> int:
88
111
  print(f" {line}")
89
112
  if old.is_dir():
90
113
  print(f" {sum(1 for _ in old.rglob('task.md'))} task(s) would move")
114
+ for c in cuts:
115
+ print(f" archived: {c.name}")
91
116
  return 0
92
117
 
93
118
  dirty = _uncommitted(repo, root)
@@ -124,6 +149,31 @@ def cmd_migrate(args) -> int:
124
149
  (archive / ".gitkeep").write_text("")
125
150
  print(f"created {rel(archive, root)}")
126
151
 
152
+ if cuts:
153
+ # One block for every cut. Each carries a whole released version's worth of
154
+ # documents, and the links between them are the reason this is worth doing
155
+ # properly rather than with a `mv`.
156
+ with links.repairing(repo) as moved:
157
+ for c in cuts:
158
+ moved[c.resolve()] = (archive / c.name).resolve()
159
+ shutil.move(str(c), str(archive / c.name))
160
+ print(f"moved {len(cuts)} archived cut(s) -> {rel(archive, root)}")
161
+
162
+ # What is left under the old archive is NOT the harness's. One repo keeps
163
+ # an `archive/backlog/` of its own there; moving it would be a guess about
164
+ # somebody else's records, and deleting the directory would take it with.
165
+ stale_home = root / "archive"
166
+ leftover = sorted(p.name for p in stale_home.iterdir()) if stale_home.is_dir() else []
167
+ leftover = [n for n in leftover if n != "versions"]
168
+ versions_dir = stale_home / "versions"
169
+ if versions_dir.is_dir() and not any(versions_dir.iterdir()):
170
+ versions_dir.rmdir()
171
+ if leftover:
172
+ print(f" {rel(stale_home, root)} still holds {', '.join(leftover)} — "
173
+ f"the harness does not own those, so they were left alone")
174
+ elif stale_home.is_dir() and not any(stale_home.iterdir()):
175
+ stale_home.rmdir()
176
+
127
177
  print("done — commit this, then every machine working this repo needs a CLI "
128
178
  "new enough to read it")
129
179
  return 0
@@ -5520,6 +5520,32 @@ def test_migrate_moves_the_backlog_inside_versions_and_repairs_what_pointed_at_i
5520
5520
  f"a link outside work/ was left pointing at {href}"
5521
5521
 
5522
5522
 
5523
+ def test_migrate_brings_already_archived_cuts_to_where_lookups_look():
5524
+ # The layout change moved where `archive` WRITES without moving what it had
5525
+ # already written, and `locate_version` only reads the new place — so every
5526
+ # cut a repo had archived answered "no version named that". jarvis had
5527
+ # archived none, which is why this survived the first migration.
5528
+ with tempfile.TemporaryDirectory() as tmp:
5529
+ _tree(tmp)
5530
+ root = Path(tmp)
5531
+ (root / "versions" / "backlog").rmdir()
5532
+ (root / "backlog").mkdir()
5533
+ old = root / "archive" / "versions" / "09-shipped"
5534
+ old.mkdir(parents=True)
5535
+ (old / "version.md").write_text(
5536
+ "---\ncreated: 2026-01-01\norder: 9\noutcome: x\nreleased: 2026-02-01\n"
5537
+ "---\n\n# Shipped\n")
5538
+ # Something the harness does not own, in the same directory.
5539
+ (root / "archive" / "backlog" / "an-old-idea").mkdir(parents=True)
5540
+
5541
+ assert _migrate(tmp) == 0
5542
+ assert (root / "versions" / "archive" / "09-shipped" / "version.md").is_file()
5543
+ assert model.locate_version(root, "09-shipped") is not None, \
5544
+ "an archived cut is still not lookupable after migrating"
5545
+ assert (root / "archive" / "backlog" / "an-old-idea").is_dir(), \
5546
+ "the migration moved records the harness does not own"
5547
+
5548
+
5523
5549
  def test_migrate_says_nothing_to_do_on_a_repo_already_migrated():
5524
5550
  # Nobody remembers which repos have been through it, so the safe answer to
5525
5551
  # running it twice has to be "nothing", not a second move or an error.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appchy/jarvis",
3
- "version": "0.1.73",
3
+ "version": "0.1.74",
4
4
  "description": "Jarvis — local AI coding assistant CLI",
5
5
  "private": false,
6
6
  "type": "module",
@@ -56,17 +56,17 @@
56
56
  "tsup": "^8.5.1",
57
57
  "typescript": "^5.7.0",
58
58
  "vitest": "^2.1.0",
59
- "@jarvis/agents": "1.0.0",
59
+ "@jarvis/anthropic": "1.0.0",
60
60
  "@jarvis/board": "0.1.0",
61
61
  "@jarvis/data": "0.1.0",
62
62
  "@jarvis/errors": "1.0.0",
63
+ "@jarvis/logger": "1.0.0",
63
64
  "@jarvis/rpc": "1.0.0",
64
65
  "@jarvis/types": "1.0.0",
65
66
  "@jarvis/typescript-config": "1.0.0",
66
67
  "@jarvis/ui": "0.1.0",
67
68
  "@jarvis/vitest-config": "1.0.0",
68
- "@jarvis/anthropic": "1.0.0",
69
- "@jarvis/logger": "1.0.0"
69
+ "@jarvis/agents": "1.0.0"
70
70
  },
71
71
  "scripts": {
72
72
  "dev": "tsx watch src/bin.ts start --foreground",