@appchy/jarvis 0.1.78 → 0.1.79

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 = "8edf530";
10113
+ var SHA = "8992daf";
10114
10114
  var BUILT = "2026-09-10";
10115
10115
  var BUILD = SHA ?? "source";
10116
10116
  var BUILD_LABEL = `${SHA ? `${VERSION2} (${SHA}${BUILT ? ` ${BUILT}` : ""})` : `${VERSION2} (source)`}${IS_DEV ? " \u2014 development build" : ""}`;
@@ -608,6 +608,33 @@ def gate(root, task, accept: str = "", owner: str = "") -> list:
608
608
  elif not head and when != date.today().isoformat():
609
609
  reasons.append(f"verify last passed on {when}, not today, and there "
610
610
  f"is no git sha to pin it to — re-run it")
611
+
612
+ # …and that the code ARRIVED, which is the other half and was missing.
613
+ # Everything above reads COMMITTED history, so it proves the code moved
614
+ # FORWARD PAST the evidence and never that it got there at all. A task can
615
+ # be green with its whole implementation in one working tree; three were,
616
+ # for as long as ten days, and the only commits around them were
617
+ # `docs(work):` ones, which look exactly like a healthy board.
618
+ #
619
+ # It refuses rather than warns because the gates RAN AGAINST THE WORKING
620
+ # TREE. If that tree is not what is in git, the evidence and the commit it
621
+ # is pinned to describe different code, and calling that shipped is the
622
+ # false claim this whole file exists to prevent.
623
+ #
624
+ # The harness names the files and stops there: committing somebody's source
625
+ # is a far larger claim on their repo than committing the board, and a board
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 []
628
+ if loose:
629
+ shown = ", ".join(loose[:5])
630
+ more = f" and {len(loose) - 5} more" if len(loose) > 5 else ""
631
+ reasons.append(
632
+ f"{len(loose)} uncommitted change(s) outside the board — the gates "
633
+ f"ran against this working tree, so completing now would record "
634
+ f"shipped for code no commit contains: {shown}{more}. Commit them "
635
+ f"(or stash or gitignore what is not this task's), then re-run "
636
+ f"`jarvis work verify --task {task.name}` — committing moves HEAD, "
637
+ f"so the evidence has to be taken on the tree that shipped.")
611
638
  else:
612
639
  reasons.append("no `verify` commands configured — an unconfigured repo "
613
640
  "cannot prove anything, so nothing in it can complete. Set "
@@ -95,6 +95,47 @@ def enabled() -> bool:
95
95
  return bool(GIT.get("commit"))
96
96
 
97
97
 
98
+ def uncommitted_code(repo) -> list:
99
+ """Everything outside the board that is not in git yet, sorted.
100
+
101
+ The mirror of `changed`, and the completion gate's half of it. `changed` asks
102
+ what a board write would CARRY; this asks what a completion would LEAVE BEHIND.
103
+
104
+ **Why this is accuracy rather than caution.** A verify run executes the
105
+ configured commands against the WORKING TREE, so a pass means "these gates pass
106
+ here, now". It is then recorded against HEAD. If anything outside the board
107
+ differs from HEAD, those two are statements about different trees, and the
108
+ board ends up saying shipped about code no commit contains. Measured three
109
+ times, once on the task whose entire subject was the harness losing what it is
110
+ trusted to keep — it completed with 7/7 gates recorded twice and none of its
111
+ fix in HEAD.
112
+
113
+ Untracked files count, and they are the important half: the lost work's own
114
+ shape was NEW files git had never heard of. Anything gitignored never appears
115
+ here, so a scratch directory somebody has already told git to ignore is not
116
+ this check's business.
117
+
118
+ Claims and the run file are excluded for the reason they are never committed —
119
+ they are one machine's coordination, true for minutes.
120
+ """
121
+ board = [r.rstrip("/") for r in (GIT.get("paths") or [])]
122
+ code, out, _ = _git(repo, "status", "--porcelain", "-z",
123
+ "--untracked-files=all", "--no-renames")
124
+ if code != 0:
125
+ return []
126
+ found = []
127
+ for entry in out.split("\0"):
128
+ if len(entry) <= 3:
129
+ continue
130
+ p = entry[3:]
131
+ if Path(p).name in LOCAL:
132
+ continue
133
+ if any(p == r or p.startswith(f"{r}/") for r in board):
134
+ continue
135
+ found.append(p)
136
+ return sorted(found)
137
+
138
+
98
139
  def machine() -> str:
99
140
  """Which machine acted. `WORK_MACHINE` where something knows a better name for
100
141
  this box than its hostname — a daemon that already has an identity for it —
@@ -3444,6 +3444,145 @@ def test_a_board_write_reaches_the_ORIGIN_not_just_the_disk():
3444
3444
  config.apply(config.DEFAULTS)
3445
3445
 
3446
3446
 
3447
+ def _provable_task(repo, name="alpha"):
3448
+ """A task in a git-mode repo with everything but the code committed: criteria
3449
+ ticked and a passing verify recorded, so the ONLY thing a gate can hold on is
3450
+ whether the implementation reached git."""
3451
+ e = repo / "work" / "versions" / "01-a-cut" / "an-epic"
3452
+ (e / "in-progress" / name).mkdir(parents=True)
3453
+ (e / "epic.md").write_text("---\ntype: epic\n---\n\n# E\n")
3454
+ (e.parent / "version.md").write_text(
3455
+ "---\ncreated: 2026-08-01\norder: 1\noutcome: x\n---\n\n# Cut\n")
3456
+ (e / "in-progress" / name / "task.md").write_text(
3457
+ f"---\npriority: P0\n---\n\n# {name}\n\n## Acceptance criteria\n\n- [x] it works\n")
3458
+ _git(repo, "add", "-A")
3459
+ _git(repo, "commit", "-qm", "the board so far")
3460
+ return e
3461
+
3462
+
3463
+ def test_completing_refuses_while_the_code_is_only_in_the_working_tree():
3464
+ # Every other check in the gate reads COMMITTED history, so all of them prove
3465
+ # the code moved FORWARD PAST the evidence and none proves it arrived. Three
3466
+ # tasks completed green with their whole implementation in one working tree —
3467
+ # one of them the task about the harness losing what it is trusted to keep,
3468
+ # with 7/7 recorded twice and none of its fix in HEAD.
3469
+ with tempfile.TemporaryDirectory() as tmp:
3470
+ try:
3471
+ repo = _git_repo(tmp, push=False)
3472
+ e = _provable_task(repo)
3473
+ with _work_dir(str(repo / "work")) as root:
3474
+ old, gate.VERIFY = gate.VERIFY, {"tests": "true"}
3475
+ try:
3476
+ assert gate.cmd_verify({"task": "alpha"}) == 0
3477
+ # Clean tree: nothing left to hold, so the gate is satisfied.
3478
+ assert gate.gate(root, model.locate(root, "alpha")) == []
3479
+
3480
+ # Now the failure this exists for — the implementation exists
3481
+ # and is in no commit. An UNTRACKED file, because that is the
3482
+ # lost work's own shape: new files git had never heard of.
3483
+ (repo / "src").mkdir()
3484
+ (repo / "src" / "shipped.ts").write_text("export const it = 1;\n")
3485
+ reasons = gate.gate(root, model.locate(root, "alpha"))
3486
+ assert any("uncommitted" in r for r in reasons), reasons
3487
+ assert any("src/shipped.ts" in r for r in reasons), reasons
3488
+
3489
+ # And the move itself refuses, leaving the task where it was —
3490
+ # warning after moving is what made the board say done anyway.
3491
+ assert task.cmd_move({"name": "alpha", "status": "complete",
3492
+ "delivered": "it ships",
3493
+ "not-included": "nothing"}) == 1
3494
+ assert (e / "in-progress" / "alpha").is_dir()
3495
+
3496
+ # Committing satisfies THIS check and trips the older one, and
3497
+ # that is the flow rather than a snag: committing moves HEAD, so
3498
+ # evidence taken before it describes a different commit. The two
3499
+ # together mean commit, then verify, then complete — and the
3500
+ # refusal says so rather than leaving it to be discovered.
3501
+ _git(repo, "add", "-A")
3502
+ _git(repo, "commit", "-qm", "the code")
3503
+ reasons = gate.gate(root, model.locate(root, "alpha"))
3504
+ assert not any("uncommitted" in r for r in reasons), reasons
3505
+ assert any("code moved since verify" in r for r in reasons), reasons
3506
+
3507
+ assert gate.cmd_verify({"task": "alpha"}) == 0
3508
+ assert gate.gate(root, model.locate(root, "alpha")) == []
3509
+ finally:
3510
+ gate.VERIFY = old
3511
+ finally:
3512
+ config.apply(config.DEFAULTS)
3513
+
3514
+
3515
+ def test_partly_committed_code_is_still_uncommitted_code():
3516
+ # What `the-hub-says-why-a-write-failed` actually was: part of its work in HEAD
3517
+ # and part not. A check that asked "did anything land" would have passed it.
3518
+ with tempfile.TemporaryDirectory() as tmp:
3519
+ try:
3520
+ repo = _git_repo(tmp, push=False)
3521
+ _provable_task(repo)
3522
+ (repo / "src").mkdir()
3523
+ (repo / "src" / "landed.ts").write_text("export const a = 1;\n")
3524
+ _git(repo, "add", "-A")
3525
+ _git(repo, "commit", "-qm", "half of it")
3526
+ (repo / "src" / "left-behind.ts").write_text("export const b = 2;\n")
3527
+ with _work_dir(str(repo / "work")) as root:
3528
+ old, gate.VERIFY = gate.VERIFY, {"tests": "true"}
3529
+ try:
3530
+ assert gate.cmd_verify({"task": "alpha"}) == 0
3531
+ reasons = gate.gate(root, model.locate(root, "alpha"))
3532
+ assert any("src/left-behind.ts" in r for r in reasons), reasons
3533
+ finally:
3534
+ gate.VERIFY = old
3535
+ finally:
3536
+ config.apply(config.DEFAULTS)
3537
+
3538
+
3539
+ def test_a_repo_that_never_asked_for_git_completes_exactly_as_before():
3540
+ # The harness is shared with repos that opted out of all of this. Refusing on
3541
+ # their working tree would be a new claim on somebody who asked for none.
3542
+ with tempfile.TemporaryDirectory() as tmp:
3543
+ try:
3544
+ repo = _git_repo(tmp, push=False)
3545
+ _provable_task(repo)
3546
+ (repo / "src").mkdir()
3547
+ (repo / "src" / "shipped.ts").write_text("export const it = 1;\n")
3548
+ config.apply({**config.DEFAULTS,
3549
+ "git": {"commit": False, "push": False,
3550
+ "remote": "origin", "paths": ["work"]}})
3551
+ with _work_dir(str(repo / "work")) as root:
3552
+ old, gate.VERIFY = gate.VERIFY, {"tests": "true"}
3553
+ try:
3554
+ assert gate.cmd_verify({"task": "alpha"}) == 0
3555
+ assert gate.gate(root, model.locate(root, "alpha")) == []
3556
+ finally:
3557
+ gate.VERIFY = old
3558
+ finally:
3559
+ config.apply(config.DEFAULTS)
3560
+
3561
+
3562
+ def test_a_gitignored_scratch_file_is_not_this_gates_business():
3563
+ # The cost of refusing is that loose files block completion, and the escape is
3564
+ # the one people already use. A file git has been told to ignore never reaches
3565
+ # the check at all.
3566
+ with tempfile.TemporaryDirectory() as tmp:
3567
+ try:
3568
+ repo = _git_repo(tmp, push=False)
3569
+ _provable_task(repo)
3570
+ (repo / ".gitignore").write_text("scratch/\n")
3571
+ _git(repo, "add", "-A")
3572
+ _git(repo, "commit", "-qm", "ignore scratch")
3573
+ (repo / "scratch").mkdir()
3574
+ (repo / "scratch" / "notes.md").write_text("thinking out loud\n")
3575
+ with _work_dir(str(repo / "work")) as root:
3576
+ old, gate.VERIFY = gate.VERIFY, {"tests": "true"}
3577
+ try:
3578
+ assert gate.cmd_verify({"task": "alpha"}) == 0
3579
+ assert gate.gate(root, model.locate(root, "alpha")) == []
3580
+ finally:
3581
+ gate.VERIFY = old
3582
+ finally:
3583
+ config.apply(config.DEFAULTS)
3584
+
3585
+
3447
3586
  def test_the_commit_trailer_names_the_item_the_event_and_the_machine():
3448
3587
  # Four other pieces of work read this format, so it is asserted rather than
3449
3588
  # assumed. Who, when and which branch stay OUT of it — git knows them, and a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appchy/jarvis",
3
- "version": "0.1.78",
3
+ "version": "0.1.79",
4
4
  "description": "Jarvis — local AI coding assistant CLI",
5
5
  "private": false,
6
6
  "type": "module",
@@ -65,8 +65,8 @@
65
65
  "@jarvis/rpc": "1.0.0",
66
66
  "@jarvis/types": "1.0.0",
67
67
  "@jarvis/typescript-config": "1.0.0",
68
- "@jarvis/vitest-config": "1.0.0",
69
- "@jarvis/ui": "0.1.0"
68
+ "@jarvis/ui": "0.1.0",
69
+ "@jarvis/vitest-config": "1.0.0"
70
70
  },
71
71
  "scripts": {
72
72
  "dev": "tsx watch src/bin.ts start --foreground",