@appchy/jarvis 0.1.80 → 0.1.82

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
@@ -10110,7 +10110,7 @@ import { createRequire as createRequire2 } from "module";
10110
10110
  var _require = createRequire2(import.meta.url);
10111
10111
  var VERSION2 = _require("../package.json").version ?? "0.0.0";
10112
10112
  var IS_DEV = String(_require("../package.json").name ?? "").endsWith("-dev");
10113
- var SHA = "959c0ae";
10113
+ var SHA = "23220f0";
10114
10114
  var BUILT = "2026-09-11";
10115
10115
  var BUILD = SHA ?? "source";
10116
10116
  var BUILD_LABEL = `${SHA ? `${VERSION2} (${SHA}${BUILT ? ` ${BUILT}` : ""})` : `${VERSION2} (source)`}${IS_DEV ? " \u2014 development build" : ""}`;
@@ -624,7 +624,12 @@ def gate(root, task, accept: str = "", owner: str = "") -> list:
624
624
  # The harness names the files and stops there: committing somebody's source
625
625
  # is a far larger claim on their repo than committing the board, and a board
626
626
  # write deliberately leaves a session's own code alone (founder, 2026-09-10).
627
- loose = git.uncommitted_code(root.parent) if git.enabled() else []
627
+ #
628
+ # Scoped to what this task is answerable for, because a checkout is shared
629
+ # and the unscoped version means nobody completes anything until everybody
630
+ # commits. Unclaimed paths, and a task that declares no regions, stay strict.
631
+ loose = (git.uncommitted_mine(root.parent, root, list(task.code or []))
632
+ if git.enabled() else [])
628
633
  if loose:
629
634
  shown = ", ".join(loose[:5])
630
635
  more = f" and {len(loose) - 5} more" if len(loose) > 5 else ""
@@ -136,6 +136,44 @@ def uncommitted_code(repo) -> list:
136
136
  return sorted(found)
137
137
 
138
138
 
139
+ def uncommitted_mine(repo, root, regions: list) -> list:
140
+ """Of the uncommitted code, what THIS task is answerable for.
141
+
142
+ **`moved_only_elsewhere` already decided this question for commits**, and the
143
+ two have to agree. That one says a commit outside your `code:` regions does not
144
+ invalidate your evidence, because a suite failure over there only warns — and
145
+ holding that while letting a merely DIRTY file over there refuse you is the same
146
+ inconsistency, read one step earlier.
147
+
148
+ It is not hypothetical. A session finishing harness work was blocked by a peer's
149
+ 19 uncommitted UI files in a shared checkout, none of them in the region its
150
+ task declared. In a checkout two people share, the unscoped rule means nobody
151
+ can complete anything until everybody commits.
152
+
153
+ **Unclaimed is always yours, and that is the whole safety of it.**
154
+ `system_for_path` answers `None` both for a file nothing declares and for a repo
155
+ whose systems declare nothing, and from here the two are indistinguishable.
156
+ Reading either as *not my business* would make the check vacuous in exactly the
157
+ repos that never filled in `paths:`. A task that declares no `code:` of its own
158
+ is answerable for all of it, for the same reason: it has not said what it
159
+ touches, so it cannot say a file is somebody else's.
160
+ """
161
+ loose = uncommitted_code(repo)
162
+ if not loose or not regions:
163
+ return loose
164
+ from .registry import scan_systems, system_for_path
165
+ systems = scan_systems(root)
166
+ if not systems:
167
+ return loose
168
+ mine = {r for r in regions if r}
169
+ kept = []
170
+ for name in loose:
171
+ owner = system_for_path(root, name, systems)
172
+ if owner is None or (mine & set(owner.code)):
173
+ kept.append(name)
174
+ return kept
175
+
176
+
139
177
  def machine() -> str:
140
178
  """Which machine acted. `WORK_MACHINE` where something knows a better name for
141
179
  this box than its hostname — a daemon that already has an identity for it —
@@ -2738,6 +2738,38 @@ def test_move_to_blocked_is_refused_because_a_blocked_task_needs_a_question():
2738
2738
  pass
2739
2739
 
2740
2740
 
2741
+ def test_a_formatter_cannot_quietly_break_the_scaffolding_templates():
2742
+ """Every `{{TOKEN}}` in every shipped template is intact and substitutable.
2743
+
2744
+ Prettier reads `{{PRIORITY}}` in YAML frontmatter as two nested flow mappings
2745
+ and rewrites it to `{ { PRIORITY } }`, which matches no substitution and
2746
+ scaffolds a broken file. One `pnpm format` did this to 118 files, failed the
2747
+ harness gate, and was reverted by hand.
2748
+
2749
+ `.prettierignore` is the fix; this is the check, and it is deliberately wider
2750
+ than the cause. It fails for a token broken by a formatter, an editor, a merge
2751
+ or a careless edit — and the thing that must hold is the file, not one tool's
2752
+ configuration.
2753
+ """
2754
+ import re
2755
+ from harness.tree import assets_dir
2756
+
2757
+ # A token is `{{NAME}}` with NO inner spaces. The corrupted form still LOOKS
2758
+ # like a token to a reader, which is why this asserts the shape rather than
2759
+ # merely the presence of braces.
2760
+ good = re.compile(r"\{\{[A-Z][A-Z0-9_]*\}\}")
2761
+ broken = re.compile(r"\{\s*\{[^{}]*\}\s*\}")
2762
+ offenders = []
2763
+ for md in sorted(assets_dir().rglob("*.md")):
2764
+ text = md.read_text()
2765
+ for m in broken.finditer(text):
2766
+ if not good.fullmatch(m.group(0)):
2767
+ offenders.append(f"{md.name}: {m.group(0)}")
2768
+ assert not offenders, (
2769
+ "template token(s) no longer substitutable — a formatter has probably "
2770
+ f"reformatted them: {offenders[:5]}")
2771
+
2772
+
2741
2773
  def test_the_completion_gate_refuses_and_says_which_gate_held():
2742
2774
  with tempfile.TemporaryDirectory() as tmp:
2743
2775
  v = _tree(tmp)
@@ -3634,6 +3666,95 @@ def test_partly_committed_code_is_still_uncommitted_code():
3634
3666
  config.apply(config.DEFAULTS)
3635
3667
 
3636
3668
 
3669
+ def test_a_peers_uncommitted_work_in_another_system_does_not_block_you():
3670
+ # The mirror of `moved_only_elsewhere`, and it has to agree with it: if a
3671
+ # COMMIT outside your regions cannot invalidate your evidence, a merely DIRTY
3672
+ # file outside them cannot refuse you either. Holding both at once is the
3673
+ # inconsistency, read one step earlier.
3674
+ #
3675
+ # Measured rather than imagined: the session that built the unscoped version
3676
+ # was blocked by a peer's 19 uncommitted UI files, none in the region its task
3677
+ # declared. In a shared checkout that rule means nobody completes anything
3678
+ # until everybody commits.
3679
+ with tempfile.TemporaryDirectory() as tmp:
3680
+ try:
3681
+ repo = _git_repo(tmp, push=False)
3682
+ arch = repo / "work" / "architecture"
3683
+ arch.mkdir(parents=True, exist_ok=True)
3684
+ (arch / "board.md").write_text(
3685
+ "---\ntype: system\ncode: [data]\npaths: [packages/board]\n"
3686
+ "depends_on: []\n---\n\n# Board\n")
3687
+ (arch / "screen.md").write_text(
3688
+ "---\ntype: system\ncode: [web]\npaths: [apps/web]\n"
3689
+ "depends_on: []\n---\n\n# Screen\n")
3690
+ for f in ("packages/board/store.ts", "apps/web/page.tsx", "scripts/loose.py"):
3691
+ (repo / f).parent.mkdir(parents=True, exist_ok=True)
3692
+ (repo / f).write_text("first\n")
3693
+ _provable_task(repo) # commits the board AND the code above
3694
+
3695
+ root = repo / "work"
3696
+ # The task says what it touches, which is what earns it the narrowing.
3697
+ brief = root / "versions" / "01-a-cut" / "an-epic" / "in-progress" / "alpha" / "task.md"
3698
+ brief.write_text(brief.read_text().replace(
3699
+ "priority: P0", "priority: P0\ncode: [data]"))
3700
+ _git(repo, "add", "-A")
3701
+ _git(repo, "commit", "-qm", "alpha declares its region")
3702
+
3703
+ with _work_dir(str(root)) as work_root:
3704
+ old, gate.VERIFY = gate.VERIFY, {"tests": "true"}
3705
+ try:
3706
+ assert gate.cmd_verify({"task": "alpha"}) == 0
3707
+
3708
+ # A PEER IS MID-EDIT IN ANOTHER SYSTEM. You can still finish.
3709
+ (repo / "apps/web/page.tsx").write_text("a peer, mid-thought\n")
3710
+ assert gate.gate(work_root, model.locate(work_root, "alpha")) == []
3711
+
3712
+ # YOUR OWN REGION, UNCOMMITTED. That is the thing this catches.
3713
+ (repo / "packages/board/store.ts").write_text("the fix, uncommitted\n")
3714
+ reasons = gate.gate(work_root, model.locate(work_root, "alpha"))
3715
+ assert any("packages/board/store.ts" in r for r in reasons), reasons
3716
+ assert not any("apps/web/page.tsx" in r for r in reasons), reasons
3717
+
3718
+ # A PATH NOTHING CLAIMS IS ALWAYS YOURS. The safety property: a
3719
+ # repo that never filled in `paths:` must not get a gate that
3720
+ # passes everything, which is worse than the noise it replaces.
3721
+ _git(repo, "checkout", "--", "packages/board/store.ts")
3722
+ (repo / "scripts/loose.py").write_text("claimed by no system\n")
3723
+ reasons = gate.gate(work_root, model.locate(work_root, "alpha"))
3724
+ assert any("scripts/loose.py" in r for r in reasons), reasons
3725
+ finally:
3726
+ gate.VERIFY = old
3727
+ finally:
3728
+ config.apply(config.DEFAULTS)
3729
+
3730
+
3731
+ def test_a_task_that_declares_no_region_is_answerable_for_all_of_it():
3732
+ # It has not said what it touches, so it cannot say a file is somebody else's.
3733
+ # The same reading `moved_only_elsewhere` gives an empty `regions`.
3734
+ with tempfile.TemporaryDirectory() as tmp:
3735
+ try:
3736
+ repo = _git_repo(tmp, push=False)
3737
+ arch = repo / "work" / "architecture"
3738
+ arch.mkdir(parents=True, exist_ok=True)
3739
+ (arch / "screen.md").write_text(
3740
+ "---\ntype: system\ncode: [web]\npaths: [apps/web]\n"
3741
+ "depends_on: []\n---\n\n# Screen\n")
3742
+ (repo / "apps/web").mkdir(parents=True)
3743
+ (repo / "apps/web/page.tsx").write_text("first\n")
3744
+ _provable_task(repo)
3745
+ with _work_dir(str(repo / "work")) as root:
3746
+ old, gate.VERIFY = gate.VERIFY, {"tests": "true"}
3747
+ try:
3748
+ assert gate.cmd_verify({"task": "alpha"}) == 0
3749
+ (repo / "apps/web/page.tsx").write_text("changed\n")
3750
+ reasons = gate.gate(root, model.locate(root, "alpha"))
3751
+ assert any("apps/web/page.tsx" in r for r in reasons), reasons
3752
+ finally:
3753
+ gate.VERIFY = old
3754
+ finally:
3755
+ config.apply(config.DEFAULTS)
3756
+
3757
+
3637
3758
  def test_a_repo_that_never_asked_for_git_completes_exactly_as_before():
3638
3759
  # The harness is shared with repos that opted out of all of this. Refusing on
3639
3760
  # their working tree would be a new claim on somebody who asked for none.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appchy/jarvis",
3
- "version": "0.1.80",
3
+ "version": "0.1.82",
4
4
  "description": "Jarvis — local AI coding assistant CLI",
5
5
  "private": false,
6
6
  "type": "module",
@@ -56,16 +56,16 @@
56
56
  "tsup": "^8.5.1",
57
57
  "typescript": "^5.7.0",
58
58
  "vitest": "^2.1.0",
59
- "@jarvis/agents": "1.0.0",
60
- "@jarvis/board": "0.1.0",
61
59
  "@jarvis/anthropic": "1.0.0",
62
- "@jarvis/data": "0.1.0",
60
+ "@jarvis/board": "0.1.0",
61
+ "@jarvis/agents": "1.0.0",
63
62
  "@jarvis/errors": "1.0.0",
64
63
  "@jarvis/logger": "1.0.0",
65
64
  "@jarvis/rpc": "1.0.0",
66
65
  "@jarvis/types": "1.0.0",
67
- "@jarvis/ui": "0.1.0",
66
+ "@jarvis/data": "0.1.0",
68
67
  "@jarvis/typescript-config": "1.0.0",
68
+ "@jarvis/ui": "0.1.0",
69
69
  "@jarvis/vitest-config": "1.0.0"
70
70
  },
71
71
  "scripts": {