@appchy/jarvis 0.1.70 → 0.1.72
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 +15 -21
- package/dist/bin.js.map +1 -1
- package/harness/harness/links.py +4 -1
- package/harness/harness/model.py +2 -2
- package/harness/harness/task.py +12 -0
- package/harness/harness/version.py +22 -6
- package/harness/test_work.py +40 -1
- package/package.json +6 -6
package/harness/harness/links.py
CHANGED
|
@@ -94,7 +94,10 @@ def repair(before: dict, moves: dict) -> tuple:
|
|
|
94
94
|
continue
|
|
95
95
|
|
|
96
96
|
text = original = now.read_text(errors="ignore")
|
|
97
|
-
|
|
97
|
+
# One entry per distinct link: a href repeated in a file is rewritten
|
|
98
|
+
# everywhere by the first replace, and counting the rest as failures
|
|
99
|
+
# reports a repair that worked as one that did not.
|
|
100
|
+
for href, target, existed in dict.fromkeys(links):
|
|
98
101
|
if not existed:
|
|
99
102
|
continue
|
|
100
103
|
want = _after(target, moves)
|
package/harness/harness/model.py
CHANGED
|
@@ -310,11 +310,11 @@ def locate(root: Path, name: str):
|
|
|
310
310
|
return t
|
|
311
311
|
return None
|
|
312
312
|
def locate_version(root: Path, name: str):
|
|
313
|
-
"""Find a version by name. Looks in `archive
|
|
313
|
+
"""Find a version by name. Looks in `versions/archive/` too, so a finished
|
|
314
314
|
cut stays LOOKUPABLE after it leaves the board — `path` and `where` keep
|
|
315
315
|
answering, while `scan` (and therefore `list`, the README and every lint)
|
|
316
316
|
sees only live work. Archived is off the board, not gone."""
|
|
317
|
-
for base in ("versions", "archive
|
|
317
|
+
for base in ("versions", "versions/archive"):
|
|
318
318
|
folder = root / base / name
|
|
319
319
|
if (folder / "version.md").is_file():
|
|
320
320
|
return Version(folder)
|
package/harness/harness/task.py
CHANGED
|
@@ -334,7 +334,19 @@ def cmd_move(args) -> int:
|
|
|
334
334
|
if dest.exists():
|
|
335
335
|
die(f"{rel(dest, root)} already exists")
|
|
336
336
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
337
|
+
|
|
338
|
+
# The bucket is part of every path that names this task, so a status change
|
|
339
|
+
# moves the folder and breaks whatever pointed at it — and `move` fires on every
|
|
340
|
+
# pickup and every completion, far more often than `place`. One audited board
|
|
341
|
+
# had 16 of its 37 broken links from a bucket change alone.
|
|
342
|
+
before = links.snapshot(root.parent)
|
|
343
|
+
moved_from = task.folder.resolve()
|
|
337
344
|
shutil.move(str(task.folder), str(dest))
|
|
345
|
+
repaired, unresolved = links.repair(before, {moved_from: dest.resolve()})
|
|
346
|
+
if repaired:
|
|
347
|
+
print(f" repaired {repaired} link(s) that pointed at it")
|
|
348
|
+
for line in unresolved:
|
|
349
|
+
print(f" COULD NOT PLACE {line}")
|
|
338
350
|
|
|
339
351
|
def mutate(d):
|
|
340
352
|
d["updated"] = date.today().isoformat()
|
|
@@ -5,13 +5,13 @@ from datetime import date
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
7
|
from . import ids
|
|
8
|
-
from .tree import DONE_TIER, VERSION_FM_ORDER, cli, die, find_work_root, rel
|
|
8
|
+
from .tree import archive_dir, DONE_TIER, VERSION_FM_ORDER, cli, die, find_work_root, rel
|
|
9
9
|
from .frontmatter import rewrite_file
|
|
10
10
|
from .model import _is_epic_dir, locate_version, scan
|
|
11
11
|
from .scaffold import _check_unused, _check_version_name, _scaffold_version
|
|
12
12
|
from .epic import cmd_epic_release
|
|
13
13
|
from .generate import _sync
|
|
14
|
-
from . import events
|
|
14
|
+
from . import events, links
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def cmd_version_new(args) -> int:
|
|
@@ -231,8 +231,17 @@ def cmd_archive(args) -> int:
|
|
|
231
231
|
version = locate_version(root, name)
|
|
232
232
|
if not version:
|
|
233
233
|
die(f"no version named '{name}'")
|
|
234
|
-
|
|
235
|
-
|
|
234
|
+
# A cut whose work all moved elsewhere has nothing left to deliver and no
|
|
235
|
+
# outcome it honestly met, so releasing it would be a lie — and until now
|
|
236
|
+
# archiving it was refused, which left it in the live list looking open. One
|
|
237
|
+
# repo had two such cuts holding 87 completed tasks between them. What is
|
|
238
|
+
# actually being asked is whether anything is still owed, so ask that.
|
|
239
|
+
open_work = [t for t in version.all_tasks() if t.status != "complete"]
|
|
240
|
+
if not version.released and open_work:
|
|
241
|
+
die(f"version '{name}' is not released and still holds "
|
|
242
|
+
f"{len(open_work)} unfinished task(s) — release it, or move that work "
|
|
243
|
+
f"somewhere it can be finished. A cut leaves the board when nothing is "
|
|
244
|
+
f"owed on it, not when somebody stops looking at it.")
|
|
236
245
|
if version.fm.get("archived"):
|
|
237
246
|
die(f"version '{name}' is already archived ({version.fm['archived']})")
|
|
238
247
|
|
|
@@ -267,7 +276,7 @@ def cmd_archive(args) -> int:
|
|
|
267
276
|
removed += 1
|
|
268
277
|
|
|
269
278
|
if dry:
|
|
270
|
-
dest = root /
|
|
279
|
+
dest = archive_dir(root) / name
|
|
271
280
|
print(f" stamp archived: on {rel(version.md, root)}")
|
|
272
281
|
print(f" move {rel(version.folder, root)} -> {rel(dest, root)}")
|
|
273
282
|
print(f"\n {removed} file(s)/dir(s) would be stripped. "
|
|
@@ -291,11 +300,18 @@ def cmd_archive(args) -> int:
|
|
|
291
300
|
# past 60 completed tasks to find the 6 that were actually moving. The
|
|
292
301
|
# record is preserved verbatim in `archive/versions/<v>/`, where `path` and
|
|
293
302
|
# `where` still resolve it — it is just no longer in the way.
|
|
294
|
-
dest = _contained(root /
|
|
303
|
+
dest = _contained(archive_dir(root) / name, root)
|
|
295
304
|
if dest.exists():
|
|
296
305
|
die(f"{rel(dest, root)} already exists")
|
|
297
306
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
307
|
+
before = links.snapshot(root.parent)
|
|
308
|
+
moved_from = version.folder.resolve()
|
|
298
309
|
shutil.move(str(_contained(version.folder, root)), str(dest))
|
|
310
|
+
repaired, unresolved = links.repair(before, {moved_from: dest.resolve()})
|
|
311
|
+
if repaired:
|
|
312
|
+
print(f" repaired {repaired} link(s) that pointed into it")
|
|
313
|
+
for line in unresolved:
|
|
314
|
+
print(f" COULD NOT PLACE {line}")
|
|
299
315
|
|
|
300
316
|
where = f" — full docs in git history @ {sha}" if sha else ""
|
|
301
317
|
events.append(root, "archived", name, stripped=removed)
|
package/harness/test_work.py
CHANGED
|
@@ -953,7 +953,7 @@ def test_archive_moves_the_version_off_the_board_but_keeps_it_lookupable():
|
|
|
953
953
|
os.environ.pop("WORK_DIR")
|
|
954
954
|
|
|
955
955
|
assert not (root / "versions" / "26-cut").exists()
|
|
956
|
-
dest = root / "
|
|
956
|
+
dest = root / "versions" / "archive" / "26-cut"
|
|
957
957
|
assert (dest / "version.md").is_file()
|
|
958
958
|
# stripped to task.md, and the epic grouping survives the move
|
|
959
959
|
assert (dest / "an-epic" / "complete" / "shipped-thing" / "task.md").is_file()
|
|
@@ -1778,6 +1778,45 @@ def test_doctor_passes_a_degraded_but_legal_install_and_fails_a_real_break():
|
|
|
1778
1778
|
assert f"{tree.cli()} init" in out.getvalue()
|
|
1779
1779
|
|
|
1780
1780
|
|
|
1781
|
+
def test_a_finished_cut_leaves_the_board_without_pretending_it_shipped():
|
|
1782
|
+
# A cut whose work all moved elsewhere met no outcome, so releasing it is a lie
|
|
1783
|
+
# — and archiving used to be refused without a release, which left it in the
|
|
1784
|
+
# live list looking open. One repo had two such cuts holding 87 done tasks.
|
|
1785
|
+
import io, contextlib, os
|
|
1786
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
1787
|
+
v = _tree(tmp, name="01-spent")
|
|
1788
|
+
root = Path(tmp)
|
|
1789
|
+
e = _epic(v, "an-epic")
|
|
1790
|
+
(e / "complete").mkdir()
|
|
1791
|
+
_task(e / "complete", "shipped-this")
|
|
1792
|
+
|
|
1793
|
+
os.environ["WORK_DIR"] = tmp
|
|
1794
|
+
try:
|
|
1795
|
+
with contextlib.redirect_stdout(io.StringIO()):
|
|
1796
|
+
assert version.cmd_archive({"name": "01-spent"}) == 0
|
|
1797
|
+
assert (root / "versions" / "archive" / "01-spent").is_dir(), \
|
|
1798
|
+
"archive lives inside versions/, beside the cuts"
|
|
1799
|
+
assert not (root / "versions" / "01-spent").exists()
|
|
1800
|
+
|
|
1801
|
+
# Still owed work is the thing that actually blocks it, not the absence
|
|
1802
|
+
# of a release stamp.
|
|
1803
|
+
v2 = root / "versions" / "02-busy"
|
|
1804
|
+
v2.mkdir()
|
|
1805
|
+
(v2 / "version.md").write_text(
|
|
1806
|
+
"---\ncreated: 2026-08-01\norder: 02\noutcome: b\n---\n\n# 02-busy\n")
|
|
1807
|
+
e2 = _epic(v2, "another-epic")
|
|
1808
|
+
(e2 / "queue").mkdir()
|
|
1809
|
+
_task(e2 / "queue", "still-owed")
|
|
1810
|
+
with contextlib.redirect_stdout(io.StringIO()):
|
|
1811
|
+
try:
|
|
1812
|
+
version.cmd_archive({"name": "02-busy"})
|
|
1813
|
+
raise AssertionError("archived a cut that still owes work")
|
|
1814
|
+
except SystemExit:
|
|
1815
|
+
pass
|
|
1816
|
+
finally:
|
|
1817
|
+
os.environ.pop("WORK_DIR", None)
|
|
1818
|
+
|
|
1819
|
+
|
|
1781
1820
|
def test_links_are_repaired_by_what_they_pointed_at_not_by_what_they_say():
|
|
1782
1821
|
from harness import links
|
|
1783
1822
|
with tempfile.TemporaryDirectory() as tmp:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appchy/jarvis",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.72",
|
|
4
4
|
"description": "Jarvis — local AI coding assistant CLI",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -57,16 +57,16 @@
|
|
|
57
57
|
"typescript": "^5.7.0",
|
|
58
58
|
"vitest": "^2.1.0",
|
|
59
59
|
"@jarvis/agents": "1.0.0",
|
|
60
|
-
"@jarvis/
|
|
60
|
+
"@jarvis/board": "0.1.0",
|
|
61
61
|
"@jarvis/data": "0.1.0",
|
|
62
|
-
"@jarvis/errors": "1.0.0",
|
|
63
62
|
"@jarvis/logger": "1.0.0",
|
|
64
63
|
"@jarvis/rpc": "1.0.0",
|
|
65
|
-
"@jarvis/board": "0.1.0",
|
|
66
64
|
"@jarvis/typescript-config": "1.0.0",
|
|
67
|
-
"@jarvis/types": "1.0.0",
|
|
68
65
|
"@jarvis/ui": "0.1.0",
|
|
69
|
-
"@jarvis/vitest-config": "1.0.0"
|
|
66
|
+
"@jarvis/vitest-config": "1.0.0",
|
|
67
|
+
"@jarvis/anthropic": "1.0.0",
|
|
68
|
+
"@jarvis/types": "1.0.0",
|
|
69
|
+
"@jarvis/errors": "1.0.0"
|
|
70
70
|
},
|
|
71
71
|
"scripts": {
|
|
72
72
|
"dev": "tsx watch src/bin.ts start --foreground",
|