@appchy/jarvis 0.1.113 → 0.1.115

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.
@@ -506,21 +506,43 @@ def test_a_grandfathered_flat_version_still_reads():
506
506
  assert [t.epic for t in ver.all_tasks()] == [None]
507
507
 
508
508
 
509
- def test_release_removes_epic_md_without_orphaning_its_tasks():
510
- # The bug this guards: `release` deletes the plan doc, so "an epic is a dir
511
- # with epic.md" would stop matching and the shipped tasks would disappear.
509
+ def test_release_stamps_the_plan_as_history_and_keeps_it():
510
+ # Release deleted the plan until 2026-09-12. It cost 104 dangling links across
511
+ # the repo and put the text where only somebody who knew the commit could read
512
+ # it, so the plan is now stamped and kept. What this guards is that the stamp
513
+ # actually lands in the file a reader opens — a folder-level marker would be
514
+ # invisible to anyone who arrived by following a link.
512
515
  with tempfile.TemporaryDirectory() as tmp:
513
516
  v = _tree(tmp)
514
517
  e = _epic(v, "publish-under-your-name")
515
518
  _task(e / "complete", "author-a-lesson")
516
519
  version.cmd_epic_release(Path(tmp), model.Version(v))
517
- assert not (e / "epic.md").exists()
520
+ assert (e / "epic.md").is_file()
521
+ body = (e / "epic.md").read_text()
522
+ assert epic._RELEASED_MARKER in body
523
+ # Before the first heading, so it cannot be scrolled past.
524
+ assert body.index(epic._RELEASED_MARKER) < body.index("# ")
518
525
  after = model.Version(v)
519
526
  assert [x.name for x in after.epics] == ["publish-under-your-name"]
520
- assert after.epics[0].planned is False
527
+ assert after.epics[0].planned is True
521
528
  assert [t.name for t in after.all_tasks()] == ["author-a-lesson"]
522
529
 
523
530
 
531
+ def test_releasing_twice_does_not_stack_a_second_stamp():
532
+ # A cut can be reopened and released again, and `align` reads the marker to tell
533
+ # a stamped plan from a live one. Two banners would still read as stamped, so
534
+ # this is about the document staying readable rather than about the check.
535
+ with tempfile.TemporaryDirectory() as tmp:
536
+ v = _tree(tmp)
537
+ e = _epic(v, "publish-under-your-name")
538
+ _task(e / "complete", "author-a-lesson")
539
+ version.cmd_epic_release(Path(tmp), model.Version(v))
540
+ first = (e / "epic.md").read_text()
541
+ version.cmd_epic_release(Path(tmp), model.Version(v))
542
+ assert (e / "epic.md").read_text() == first
543
+ assert first.count(epic._RELEASED_MARKER) == 1
544
+
545
+
524
546
  def test_a_finished_epic_drops_into_the_done_tier_and_comes_back_out():
525
547
  # The tier is DERIVED, so the only thing that may move an epic is what its
526
548
  # tasks say — and it has to work in BOTH directions, or reopening a task would
@@ -951,13 +973,13 @@ def test_a_flat_version_is_grandfathered_but_a_mixed_one_is_not():
951
973
  assert any("straggler: loose in version" in w for w in _warns(Path(tmp)))
952
974
 
953
975
 
954
- def test_release_removes_epic_md_for_an_epic_parked_in_the_done_tier():
976
+ def test_release_stamps_the_plan_of_an_epic_parked_in_the_done_tier():
955
977
  # THE bug on release's only normal path. A version is releasable only when every
956
978
  # task is complete — which is exactly the condition that parks each epic in
957
979
  # `<v>/complete/`. `release` flattens that tier first, then removed `epic.md`
958
980
  # from the PRE-MOVE model: FileNotFoundError, raised *after* `released:` was
959
- # already stamped. A traceback, a half-applied release, and `epic.md` the one
960
- # file release exists to remove — left behind for `align` to report.
981
+ # already stamped. A traceback, a half-applied release, and an unstamped plan
982
+ # left behind for `align` to report.
961
983
  import os
962
984
  with tempfile.TemporaryDirectory() as tmp:
963
985
  root = Path(tmp)
@@ -970,10 +992,13 @@ def test_release_removes_epic_md_for_an_epic_parked_in_the_done_tier():
970
992
  finally:
971
993
  os.environ.pop("WORK_DIR")
972
994
 
973
- # Flattened out of the done tier, epic.md gone, folder kept as the grouping.
995
+ # Flattened out of the done tier, the plan kept and stamped, folder kept as
996
+ # the grouping.
974
997
  assert (v / "a-shipped-goal").is_dir()
975
998
  assert not (v / tree.DONE_TIER).exists()
976
- assert not list(v.rglob("epic.md")), "release must remove every epic.md"
999
+ plans = list(v.rglob("epic.md"))
1000
+ assert len(plans) == 1, "release must keep the plan, not remove it"
1001
+ assert epic._RELEASED_MARKER in plans[0].read_text()
977
1002
  assert (v / "a-shipped-goal" / "complete" / "shipped-thing" / "task.md").is_file()
978
1003
  assert model.Version(v).released
979
1004
 
@@ -2051,6 +2076,103 @@ def test_a_finished_cut_leaves_the_board_without_pretending_it_shipped():
2051
2076
  os.environ.pop("WORK_DIR", None)
2052
2077
 
2053
2078
 
2079
+ def test_a_standing_broken_link_is_settled_by_depth_or_by_board_name():
2080
+ # The set `repair()` refuses on purpose: broken BEFORE any move, so there is no
2081
+ # before-state and identity cannot answer. Two resolvers instead — the tail is
2082
+ # right and the `../` count is wrong, or the item moved bucket and the folder it
2083
+ # lives in is named in the link.
2084
+ from harness import links
2085
+ with tempfile.TemporaryDirectory() as tmp:
2086
+ base = Path(tmp) / "work"
2087
+ (base / "product").mkdir(parents=True)
2088
+ (base / "product" / "board.md").write_text("# board\n")
2089
+ moved = base / "versions" / "a-cut" / "an-epic" / "complete" / "shipped-thing"
2090
+ moved.mkdir(parents=True)
2091
+ (moved / "task.md").write_text("# shipped thing\n")
2092
+
2093
+ doc = base / "versions" / "a-cut" / "an-epic" / "queue" / "live-one" / "task.md"
2094
+ doc.parent.mkdir(parents=True)
2095
+ doc.write_text(
2096
+ # two hops short of product/
2097
+ "governed by [board](../../../product/board.md), "
2098
+ # names a bucket the item has left
2099
+ "after [shipped](../shipped-thing/task.md).\n")
2100
+
2101
+ fixed, stuck = links.repair_dangling(base, apply=True)
2102
+ assert (fixed, stuck) == (2, []), (fixed, stuck)
2103
+ body = doc.read_text()
2104
+ assert "](../../../../../product/board.md)" in body, body
2105
+ assert "](../../complete/shipped-thing/task.md)" in body, body
2106
+ assert links.dangling(base) == []
2107
+
2108
+
2109
+ def test_a_broken_link_two_resolvers_disagree_about_is_left_for_a_person():
2110
+ # A link repaired to the wrong real file is worse than one that is visibly
2111
+ # broken: nothing afterwards can tell it was ever wrong.
2112
+ from harness import links
2113
+ with tempfile.TemporaryDirectory() as tmp:
2114
+ base = Path(tmp) / "work"
2115
+ # The SAME tail exists at two heights on one walk up, so counting `../`
2116
+ # cannot choose between them.
2117
+ (base / "notes").mkdir(parents=True)
2118
+ (base / "notes" / "shared.md").write_text("# the outer one\n")
2119
+ (base / "one" / "notes").mkdir(parents=True)
2120
+ (base / "one" / "notes" / "shared.md").write_text("# the inner one\n")
2121
+ doc = base / "one" / "notes" / "deep" / "doc.md"
2122
+ doc.parent.mkdir(parents=True)
2123
+ doc.write_text("see [it](../../../../notes/shared.md)\n")
2124
+
2125
+ fixed, stuck = links.repair_dangling(base, apply=True)
2126
+ assert fixed == 0, "a link two candidates answer must not be rewritten"
2127
+ assert [h for _, h in stuck] == ["../../../../notes/shared.md"]
2128
+ assert "](../../../../notes/shared.md)" in doc.read_text(), "it was rewritten"
2129
+
2130
+
2131
+ def test_a_link_inside_a_code_span_is_prose_and_never_a_finding():
2132
+ # A doc teaching the syntax writes `](../…md)` in backticks. The live false
2133
+ # positive this check ever produced was a quoted anchor whose span opened on one
2134
+ # line and closed on the next, so the span has to cross newlines.
2135
+ from harness import links
2136
+ with tempfile.TemporaryDirectory() as tmp:
2137
+ base = Path(tmp) / "work"
2138
+ base.mkdir(parents=True)
2139
+ (base / "doc.md").write_text(
2140
+ "Resolving every `](../…md)` in the tree.\n\n"
2141
+ "The anchor reads `[sessions](sessions.md)'s AC-03 and the test\n"
2142
+ "lands later`, which is a quotation.\n\n"
2143
+ "But [this one](../really-gone.md) is a link.\n")
2144
+
2145
+ broken = links.dangling(base)
2146
+ assert [h for _, h, _ in broken] == ["../really-gone.md"], broken
2147
+
2148
+
2149
+ def test_the_links_gate_blocks_on_a_live_brief_and_never_on_a_shipped_cut():
2150
+ # Blocking is only safe because the steady state is zero — every folder move
2151
+ # repairs its own links — and because a shipped cut is allowed to name things
2152
+ # that have since been deleted.
2153
+ import contextlib, io
2154
+ from harness import report
2155
+ with tempfile.TemporaryDirectory() as tmp:
2156
+ root = Path(tmp) / "work"
2157
+ shipped = root / "versions" / "complete" / "01-done" / "an-epic"
2158
+ shipped.mkdir(parents=True)
2159
+ (shipped / "epic.md").write_text("gone: [src](../../../../src/deleted.ts)\n")
2160
+ os.environ["WORK_DIR"] = str(root)
2161
+ try:
2162
+ with contextlib.redirect_stdout(io.StringIO()) as out:
2163
+ assert report.cmd_links({}) == 0, "a shipped cut must not block"
2164
+ assert "never blocking" in out.getvalue()
2165
+
2166
+ live = root / "versions" / "02-open" / "an-epic" / "queue" / "a-task"
2167
+ live.mkdir(parents=True)
2168
+ (live / "task.md").write_text("see [rule](../../../../nowhere.md)\n")
2169
+ with contextlib.redirect_stdout(io.StringIO()) as out:
2170
+ assert report.cmd_links({}) == 1, "a live broken link must block"
2171
+ assert "nowhere.md" in out.getvalue()
2172
+ finally:
2173
+ os.environ.pop("WORK_DIR", None)
2174
+
2175
+
2054
2176
  def test_links_are_repaired_by_what_they_pointed_at_not_by_what_they_say():
2055
2177
  from harness import links
2056
2178
  with tempfile.TemporaryDirectory() as tmp:
@@ -6394,7 +6516,8 @@ def test_an_epic_whose_plan_doc_is_gone_still_moves_into_a_cut():
6394
6516
  def test_releasing_a_cut_holding_an_epic_with_no_plan_doc_still_releases_it():
6395
6517
  # The unlink ran on every epic, so one without a plan doc raised — and it raised
6396
6518
  # AFTER `released:` was stamped, leaving the cut reading as released while every
6397
- # other epic kept the file release exists to remove.
6519
+ # other epic went untouched. The stamp has the same shape of hazard: an epic with
6520
+ # nothing to stamp must be skipped rather than read.
6398
6521
  with tempfile.TemporaryDirectory() as tmp:
6399
6522
  root = Path(tmp)
6400
6523
  v = _tree(tmp)
@@ -6412,9 +6535,10 @@ def test_releasing_a_cut_holding_an_epic_with_no_plan_doc_still_releases_it():
6412
6535
 
6413
6536
  after = model.locate_version(root, "26-cut")
6414
6537
  assert after.released, "the cut never got its released stamp"
6415
- assert not (after.folder / "publish-under-your-name" / "epic.md").exists() \
6416
- and not (after.folder / "complete" / "publish-under-your-name" / "epic.md").exists(), \
6417
- "a plan doc survived the release"
6538
+ plans = list(after.folder.rglob("epic.md"))
6539
+ assert len(plans) == 1, "the epic that had a plan should still have exactly one"
6540
+ assert plans[0].parent.name == "publish-under-your-name"
6541
+ assert epic._RELEASED_MARKER in plans[0].read_text(), "the kept plan is unstamped"
6418
6542
 
6419
6543
 
6420
6544
  def test_a_shipped_criterion_delivered_by_a_filed_cut_is_still_covered():
@@ -7403,13 +7527,13 @@ def test_the_stop_hook_being_switched_off_silences_the_standing_too():
7403
7527
 
7404
7528
 
7405
7529
 
7406
- def test_a_release_says_what_it_is_deleting_before_it_deletes_it():
7407
- # `release` has always printed "promote still-load-bearing decisions to the
7408
- # domain that owns them" AFTER unlinking the files holding those decisions.
7409
- # Measured on 01-one-board: twelve plans, 450,405 bytes, about a hundred durable
7410
- # statements with no owner file, and the recovery commit reconstructed afterwards
7411
- # by hand with `git log --diff-filter=D`. The cost of a release is the plans that
7412
- # go with it, so it is named while they still exist.
7530
+ def test_a_release_says_what_it_is_stamping_shut():
7531
+ # `release` has always printed "promote still-load-bearing decisions to the domain
7532
+ # that owns them", and until 2026-09-12 it printed that AFTER unlinking the files
7533
+ # holding those decisions. Measured on 01-one-board: twelve plans, 450,405 bytes,
7534
+ # about a hundred durable statements with no owner file. The plans are now kept
7535
+ # and stamped rather than deleted, so what the notice owes a reader changed with
7536
+ # it: not a last call before the bytes go, but the argument for the sweep.
7413
7537
  with tempfile.TemporaryDirectory() as tmp:
7414
7538
  try:
7415
7539
  repo = _git_repo(tmp, push=False)
@@ -7432,22 +7556,22 @@ def test_a_release_says_what_it_is_deleting_before_it_deletes_it():
7432
7556
  with _work_dir(str(repo / "work")):
7433
7557
  said = _capture_stdout(lambda: version.cmd_release({"name": "09-a-cut"}))
7434
7558
 
7435
- # The cost, in front of the person, in units they can weigh.
7436
- assert "DELETING 1 epic plan(s)" in said, said
7559
+ # What is going quiet, in front of the person, in units they can weigh.
7560
+ assert "STAMPING 1 epic plan(s)" in said, said
7437
7561
  assert "bytes" in said
7438
7562
  # WHICH sections can outlive the plan — and not the ones that cannot.
7439
7563
  assert "§Governance this implies" in said, said
7440
7564
  assert "§Plan" not in said, "slice ordering is supposed to die with the epic"
7441
- # Where it went, named at the moment it goes rather than reconstructed later.
7442
- assert head and head in said, f"expected the recovery commit {head}: {said}"
7443
7565
 
7444
- # And the claim is true: the plan is out of the tree and in that commit.
7445
- assert not (v / "an-epic" / "epic.md").exists()
7446
- kept = _git(repo, "show", f"{head}:work/versions/09-a-cut/an-epic/epic.md")
7447
- assert "a rule is owed" in kept.stdout, kept.stderr
7566
+ # And the claim is true: the plan is still there, and stamped.
7567
+ plan = v / "an-epic" / "epic.md"
7568
+ assert plan.is_file(), "a released plan is kept, not deleted"
7569
+ assert "a rule is owed" in plan.read_text()
7570
+ assert epic._RELEASED_MARKER in plan.read_text()
7448
7571
 
7449
- # The instruction that follows no longer sends a reader to a deleted file.
7450
- assert "no longer in the tree" in said, said
7572
+ # Nothing sends a reader to git for the text any more, because it is here.
7573
+ assert "no longer in the tree" not in said, said
7574
+ assert head not in said or "recoverable" not in said, said
7451
7575
  finally:
7452
7576
  events._PENDING.clear()
7453
7577
  config.apply(config.DEFAULTS)
package/harness/work.py CHANGED
@@ -77,6 +77,9 @@ Subcommands:
77
77
  list --branch <ref> the board as another branch has it (read-only)
78
78
  rules --task <name> what constrains a task, derived
79
79
  align [--class <name>] every misalignment class; REPORT-ONLY, exit 0
80
+ links [--fix] board links that point at nothing. Exits 1 when a
81
+ LIVE brief or governance file holds one, so a gate
82
+ can block; history is reported and never blocks
80
83
  wrap [--task <name>] finish a session cleanly: what a machine knows
81
84
  about this run, then what to do. Writes NOTHING
82
85
  coverage [--feature <name>] what a RUN proved vs what features promise
@@ -143,7 +146,7 @@ from harness import ids
143
146
  from harness.architecture import (cmd_domain_new, cmd_init, cmd_rules, cmd_system_new,
144
147
  cmd_where)
145
148
  from harness.coverage import cmd_coverage
146
- from harness.report import cmd_align, cmd_list, cmd_readme
149
+ from harness.report import cmd_align, cmd_links, cmd_list, cmd_readme
147
150
  from harness.wrap import cmd_wrap
148
151
  from harness.config import (DEFAULTS, ConfigError, apply, cmd_applies, cmd_config,
149
152
  cmd_context, cmd_method, cmd_remind, resolve)
@@ -167,7 +170,7 @@ SUBCOMMANDS = (
167
170
  "find", "list", "readme", "move", "plan", "session", "kickoff", "path", "code",
168
171
  "domain-new", "system-new", "where", "rules", "align", "wrap", "coverage",
169
172
  "migrate", "next", "status", "drop", "ask", "answer", "needs", "verify",
170
- "observed", "log", "digest", "sync", "check",
173
+ "observed", "log", "digest", "sync", "check", "links",
171
174
  )
172
175
 
173
176
 
@@ -350,7 +353,7 @@ def _say(note: str) -> None:
350
353
  #: remembering to come back here.
351
354
  _READS = {"list", "find", "path", "code", "where", "rules", "status", "digest",
352
355
  "log", "needs", "align", "coverage", "readme", "config", "context",
353
- "doctor", "kickoff", "remind", "applies", "wrap"}
356
+ "doctor", "kickoff", "remind", "applies", "wrap", "links"}
354
357
 
355
358
 
356
359
  def _reads_only(cmd: str, flags: dict) -> None:
@@ -514,6 +517,8 @@ def dispatch(cmd, pos, flags, cfg) -> int:
514
517
  return cmd_rules({**flags, "task": flags.get("task") or (pos[0] if pos else None)})
515
518
  if cmd == "align":
516
519
  return cmd_align(flags)
520
+ if cmd == "links":
521
+ return cmd_links(flags)
517
522
  if cmd == "wrap":
518
523
  return cmd_wrap(cfg, flags, _project_root(flags))
519
524
  if cmd == "coverage":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appchy/jarvis",
3
- "version": "0.1.113",
3
+ "version": "0.1.115",
4
4
  "description": "Jarvis — local AI coding assistant CLI",
5
5
  "private": false,
6
6
  "type": "module",
@@ -57,14 +57,14 @@
57
57
  "typescript": "^5.7.0",
58
58
  "vitest": "^2.1.0",
59
59
  "@jarvis/agents": "1.0.0",
60
- "@jarvis/errors": "1.0.0",
60
+ "@jarvis/anthropic": "1.0.0",
61
61
  "@jarvis/board": "0.1.0",
62
+ "@jarvis/data": "0.1.0",
63
+ "@jarvis/errors": "1.0.0",
62
64
  "@jarvis/logger": "1.0.0",
65
+ "@jarvis/types": "1.0.0",
63
66
  "@jarvis/rpc": "1.0.0",
64
- "@jarvis/anthropic": "1.0.0",
65
67
  "@jarvis/typescript-config": "1.0.0",
66
- "@jarvis/data": "0.1.0",
67
- "@jarvis/types": "1.0.0",
68
68
  "@jarvis/ui": "0.1.0",
69
69
  "@jarvis/vitest-config": "1.0.0"
70
70
  },