@appchy/jarvis 0.1.82 → 0.1.84
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 +6 -2
- package/dist/bin.js.map +1 -1
- package/dist/ui/app.js +31 -31
- package/harness/harness/architecture.py +2 -2
- package/harness/harness/autonomy.py +3 -3
- package/harness/harness/gate.py +3 -3
- package/harness/harness/kickoff.py +2 -2
- package/harness/harness/model.py +84 -5
- package/harness/harness/shift.py +2 -2
- package/harness/harness/task.py +19 -13
- package/harness/test_work.py +85 -0
- package/package.json +7 -7
|
@@ -2,7 +2,7 @@ from pathlib import Path
|
|
|
2
2
|
|
|
3
3
|
from . import ids
|
|
4
4
|
from .tree import BACKLOG_END, BACKLOG_START, assets_dir, die, find_work_root, rel
|
|
5
|
-
from .model import locate
|
|
5
|
+
from .model import locate, missing
|
|
6
6
|
from .registry import (DOMAIN_ORDER, _citation_counts, definition_sites,
|
|
7
7
|
domains_for_owner, hosts, locate_domain, locate_system,
|
|
8
8
|
scan_systems, systems_for_code)
|
|
@@ -248,7 +248,7 @@ def cmd_rules(args) -> int:
|
|
|
248
248
|
die("usage: jarvis work rules --task <name>")
|
|
249
249
|
t = locate(root, name)
|
|
250
250
|
if not t:
|
|
251
|
-
die(
|
|
251
|
+
die(missing(root, name))
|
|
252
252
|
systems = systems_for_code(root, t.code) if t.code else []
|
|
253
253
|
domains = domains_for_owner(root, t.owner)
|
|
254
254
|
if not systems and not domains:
|
|
@@ -28,7 +28,7 @@ from datetime import date
|
|
|
28
28
|
from .tree import (BLOCKED, BUCKETS, DEFAULT_AUTONOMY_CEILING, TIER3_OWNERS,
|
|
29
29
|
TIER_MEANING, TIERS, die, find_work_root, rel)
|
|
30
30
|
from .frontmatter import as_list, parse_frontmatter, rewrite_file
|
|
31
|
-
from .model import locate, record_session, scan
|
|
31
|
+
from .model import locate, missing, record_session, scan
|
|
32
32
|
from .generate import _sync
|
|
33
33
|
from . import events, links
|
|
34
34
|
|
|
@@ -129,7 +129,7 @@ def cmd_ask(args) -> int:
|
|
|
129
129
|
"[--owner founder] [--durable]")
|
|
130
130
|
task = locate(root, name)
|
|
131
131
|
if not task:
|
|
132
|
-
die(
|
|
132
|
+
die(missing(root, name))
|
|
133
133
|
if task.in_backlog:
|
|
134
134
|
die(f"'{name}' is in backlog — a backlog task has no status to park. "
|
|
135
135
|
f"Pull it into a version first.")
|
|
@@ -195,7 +195,7 @@ def cmd_answer(args) -> int:
|
|
|
195
195
|
die("usage: jarvis work answer <task> --choose \"…\"")
|
|
196
196
|
task = locate(root, name)
|
|
197
197
|
if not task:
|
|
198
|
-
die(
|
|
198
|
+
die(missing(root, name))
|
|
199
199
|
open_qs = _open_questions(task)
|
|
200
200
|
if not open_qs:
|
|
201
201
|
die(f"'{name}' has no open question — nothing to answer")
|
package/harness/harness/gate.py
CHANGED
|
@@ -33,7 +33,7 @@ from pathlib import Path
|
|
|
33
33
|
|
|
34
34
|
from .tree import die, find_work_root
|
|
35
35
|
from .frontmatter import as_list, parse_frontmatter, rewrite_file, split_frontmatter
|
|
36
|
-
from .model import locate
|
|
36
|
+
from .model import locate, missing
|
|
37
37
|
from .shard import _load_run
|
|
38
38
|
from . import events, git, peers
|
|
39
39
|
from .autonomy import tier_of
|
|
@@ -277,7 +277,7 @@ def _execute(root, name) -> tuple:
|
|
|
277
277
|
_write_run(root, data)
|
|
278
278
|
|
|
279
279
|
if name and not _record(root, name, results, ok, sha):
|
|
280
|
-
die(
|
|
280
|
+
die(missing(root, name))
|
|
281
281
|
return results, ok, sha
|
|
282
282
|
|
|
283
283
|
|
|
@@ -530,7 +530,7 @@ def cmd_observed(args) -> int:
|
|
|
530
530
|
die(f"--ac must look like 'AC-01' (got '{ac}')")
|
|
531
531
|
task = locate(root, name)
|
|
532
532
|
if not task:
|
|
533
|
-
die(
|
|
533
|
+
die(missing(root, name))
|
|
534
534
|
entry = f"{date.today().isoformat()} {ac} {saw}"
|
|
535
535
|
md = task.folder / "task.md"
|
|
536
536
|
|
|
@@ -23,7 +23,7 @@ assuming a fresh session infers it.
|
|
|
23
23
|
"""
|
|
24
24
|
from pathlib import Path
|
|
25
25
|
|
|
26
|
-
from .model import locate
|
|
26
|
+
from .model import locate, missing
|
|
27
27
|
from .tree import cli, die, find_work_root, rel
|
|
28
28
|
|
|
29
29
|
#: `graph.mcp`, `session.mcp`, `session.tool`, `spine.*`, `instructions.skill` —
|
|
@@ -182,7 +182,7 @@ def cmd_kickoff(args) -> int:
|
|
|
182
182
|
root = find_work_root()
|
|
183
183
|
task = locate(root, args["name"])
|
|
184
184
|
if not task:
|
|
185
|
-
die(
|
|
185
|
+
die(missing(root, args["name"]))
|
|
186
186
|
prompt = build_prompt(task, root, args.get("next"), args.get("watch"))
|
|
187
187
|
|
|
188
188
|
# `--prompt-only` is the pipe. Everything else this command prints is
|
package/harness/harness/model.py
CHANGED
|
@@ -3,7 +3,7 @@ import re
|
|
|
3
3
|
from datetime import date
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
|
-
from .tree import backlog_dir, BUCKETS, DONE_TIER, PRIORITIES, RESERVED_MD
|
|
6
|
+
from .tree import backlog_dir, BUCKETS, cli, DONE_TIER, PRIORITIES, RESERVED_MD
|
|
7
7
|
from .frontmatter import as_list, parse_frontmatter, read_item, rewrite_file, title_of
|
|
8
8
|
|
|
9
9
|
|
|
@@ -298,8 +298,22 @@ def scan(root: Path) -> dict:
|
|
|
298
298
|
|
|
299
299
|
return {"versions": versions, "backlog": backlog,
|
|
300
300
|
"backlog_epics": backlog_epics}
|
|
301
|
-
def locate(root: Path, name: str):
|
|
302
|
-
"""Find a task by name
|
|
301
|
+
def locate(root: Path, name: str, filed: bool = False):
|
|
302
|
+
"""Find a task by name. LIVE work by default; `filed=True` also searches the
|
|
303
|
+
cuts that have left the board.
|
|
304
|
+
|
|
305
|
+
`locate_version` has always searched all three homes, and says why: off the
|
|
306
|
+
board is not gone, and `path` keeps answering. That was never true of the work
|
|
307
|
+
INSIDE a cut — filing `00-somebody-new-can-start` made six tasks unresolvable
|
|
308
|
+
in one command, and between them they are named in 55 commit trailers, which
|
|
309
|
+
is the record a board write leaves.
|
|
310
|
+
|
|
311
|
+
**It is a parameter rather than the new default, and that is the whole care
|
|
312
|
+
here.** `locate` is also how every WRITE resolves its target, and what a
|
|
313
|
+
release delivered is a matter of record — a task inside a shipped cut must not
|
|
314
|
+
become movable. So reads opt in, and writes stay on live work and refuse by
|
|
315
|
+
saying where the work went rather than claiming it never existed.
|
|
316
|
+
"""
|
|
303
317
|
s = scan(root)
|
|
304
318
|
for v in s["versions"]:
|
|
305
319
|
for t in v.all_tasks():
|
|
@@ -308,7 +322,63 @@ def locate(root: Path, name: str):
|
|
|
308
322
|
for t in s["backlog"]:
|
|
309
323
|
if t.name == name:
|
|
310
324
|
return t
|
|
325
|
+
if filed:
|
|
326
|
+
for v in scan_filed(root):
|
|
327
|
+
for t in v.all_tasks():
|
|
328
|
+
if t.name == name:
|
|
329
|
+
return t
|
|
311
330
|
return None
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def scan_filed(root: Path) -> list:
|
|
334
|
+
"""Every cut that has LEFT the board — both `versions/complete/` and
|
|
335
|
+
`versions/archive/`.
|
|
336
|
+
|
|
337
|
+
Deliberately not `scan_shipped`, which reads `complete/` alone because it
|
|
338
|
+
answers "what has this repo delivered". A cut in `archive/` delivered nothing:
|
|
339
|
+
it left because its work moved elsewhere and there was no outcome it could
|
|
340
|
+
honestly claim. Two different questions, so two functions rather than one with
|
|
341
|
+
a flag that makes every caller state which meaning it wanted.
|
|
342
|
+
"""
|
|
343
|
+
out = []
|
|
344
|
+
for home in ("complete", "archive"):
|
|
345
|
+
base = root / "versions" / home
|
|
346
|
+
if not base.is_dir():
|
|
347
|
+
continue
|
|
348
|
+
out += [Version(v) for v in sorted(base.iterdir())
|
|
349
|
+
if v.is_dir() and (v / "version.md").is_file()]
|
|
350
|
+
return out
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def filed_home(root: Path, name: str) -> str:
|
|
354
|
+
"""Where a name went, as a sentence to append to a refusal — or `""`.
|
|
355
|
+
|
|
356
|
+
"no task named 'x' found" is the least useful true thing to say about work that
|
|
357
|
+
shipped last Thursday, and it is what every write said the moment a cut was
|
|
358
|
+
filed. The lookup that fails looks once more, in the cuts that left the board,
|
|
359
|
+
so the refusal can name the release instead of denying the work ever existed.
|
|
360
|
+
"""
|
|
361
|
+
for v in scan_filed(root):
|
|
362
|
+
for t in v.all_tasks():
|
|
363
|
+
if t.name == name:
|
|
364
|
+
return (f" — it shipped in '{v.name}'"
|
|
365
|
+
+ (f" on {v.released}" if v.released else "")
|
|
366
|
+
+ ", and what a release delivered is a matter of record. "
|
|
367
|
+
+ f"Read it with `{cli()} path {name}`.")
|
|
368
|
+
for e in v.epics:
|
|
369
|
+
if e.name == name:
|
|
370
|
+
return (f" — that epic shipped in '{v.name}'"
|
|
371
|
+
+ (f" on {v.released}" if v.released else "") + ".")
|
|
372
|
+
return ""
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def missing(root: Path, name: str, noun: str = "task") -> str:
|
|
376
|
+
"""The refusal for a name that does not resolve, saying where it went if it did.
|
|
377
|
+
|
|
378
|
+
One wording in one place: thirteen sites said "no task named 'x' found" and
|
|
379
|
+
every one of them was wrong in the same new way once cuts started being filed.
|
|
380
|
+
"""
|
|
381
|
+
return f"no {noun} named '{name}' found{filed_home(root, name)}"
|
|
312
382
|
def locate_version(root: Path, name: str):
|
|
313
383
|
"""Find a version by name. Looks in BOTH homes a cut leaves the board for —
|
|
314
384
|
`versions/complete/` and `versions/archive/` — so a finished cut stays
|
|
@@ -347,9 +417,13 @@ def scan_shipped(root: Path) -> list:
|
|
|
347
417
|
cuts.sort(key=lambda v: (v.order, v.name))
|
|
348
418
|
cuts.sort(key=lambda v: v.released or "", reverse=True)
|
|
349
419
|
return cuts
|
|
350
|
-
def locate_epic(root: Path, name: str):
|
|
420
|
+
def locate_epic(root: Path, name: str, filed: bool = False):
|
|
351
421
|
"""Find an epic by name anywhere — in a version or in the backlog. Names are
|
|
352
|
-
globally unique across tasks, epics and versions, so a name alone resolves.
|
|
422
|
+
globally unique across tasks, epics and versions, so a name alone resolves.
|
|
423
|
+
|
|
424
|
+
`filed=True` reaches the cuts that have left the board, for the reason
|
|
425
|
+
`locate` takes the same parameter: a grouping that shipped is still a name
|
|
426
|
+
people cite, and `release` removes only the plan doc, never the folder."""
|
|
353
427
|
s = scan(root)
|
|
354
428
|
for v in s["versions"]:
|
|
355
429
|
for e in v.epics:
|
|
@@ -358,6 +432,11 @@ def locate_epic(root: Path, name: str):
|
|
|
358
432
|
for e in s["backlog_epics"]:
|
|
359
433
|
if e.name == name:
|
|
360
434
|
return e
|
|
435
|
+
if filed:
|
|
436
|
+
for v in scan_filed(root):
|
|
437
|
+
for e in v.epics:
|
|
438
|
+
if e.name == name:
|
|
439
|
+
return e
|
|
361
440
|
return None
|
|
362
441
|
def epic_home(root: Path, epic: "Epic") -> str:
|
|
363
442
|
"""A human-readable home for an epic — `version '<v>'` or `backlog`."""
|
package/harness/harness/shift.py
CHANGED
|
@@ -25,7 +25,7 @@ from pathlib import Path
|
|
|
25
25
|
|
|
26
26
|
from .tree import BLOCKED, BUCKETS, die, find_work_root, rel
|
|
27
27
|
from .frontmatter import rewrite_file
|
|
28
|
-
from .model import locate, record_session, scan
|
|
28
|
+
from .model import locate, missing, record_session, scan
|
|
29
29
|
from .generate import _sync
|
|
30
30
|
from . import autonomy, events, links, peers
|
|
31
31
|
# The ceiling is read through the MODULE, never bound in with `from … import`.
|
|
@@ -225,7 +225,7 @@ def cmd_drop(args) -> int:
|
|
|
225
225
|
root = find_work_root()
|
|
226
226
|
task = locate(root, args["name"])
|
|
227
227
|
if not task:
|
|
228
|
-
die(
|
|
228
|
+
die(missing(root, args["name"]))
|
|
229
229
|
p = task.folder / CLAIM
|
|
230
230
|
if p.is_file():
|
|
231
231
|
p.unlink()
|
package/harness/harness/task.py
CHANGED
|
@@ -10,7 +10,7 @@ from . import peers, tree
|
|
|
10
10
|
from . import links
|
|
11
11
|
from .tree import BLOCKED, BUCKETS, die, find_work_root, rel
|
|
12
12
|
from .frontmatter import as_list, parse_frontmatter, read_item, rewrite_file, split_frontmatter
|
|
13
|
-
from .model import current_session_id, locate, locate_epic, locate_version, record_session, scan
|
|
13
|
+
from .model import current_session_id, locate, locate_epic, locate_version, missing, record_session, scan
|
|
14
14
|
from .registry import code_vocabulary
|
|
15
15
|
from .scaffold import _check_kebab, _check_owner_ref, _check_priority, _check_unused, _scaffold_handoff, _scaffold_task
|
|
16
16
|
from .epic import _bucketed, cmd_epic_move, epic_for_task
|
|
@@ -160,7 +160,7 @@ def cmd_place(args) -> int:
|
|
|
160
160
|
|
|
161
161
|
task = locate(root, name)
|
|
162
162
|
if not task:
|
|
163
|
-
die(
|
|
163
|
+
die(missing(root, name, "task or epic"))
|
|
164
164
|
if task.version == version.name:
|
|
165
165
|
die(f"'{name}' is already in version '{version_name}'")
|
|
166
166
|
if task.status == "complete":
|
|
@@ -236,7 +236,7 @@ def _to_backlog(root, name: str, args) -> int:
|
|
|
236
236
|
|
|
237
237
|
task = locate(root, name)
|
|
238
238
|
if not task:
|
|
239
|
-
die(
|
|
239
|
+
die(missing(root, name, "task or epic"))
|
|
240
240
|
if task.in_backlog:
|
|
241
241
|
die(f"'{name}' is already in the backlog")
|
|
242
242
|
if task.status == "complete":
|
|
@@ -283,7 +283,7 @@ def cmd_move(args) -> int:
|
|
|
283
283
|
|
|
284
284
|
task = locate(root, name)
|
|
285
285
|
if not task:
|
|
286
|
-
die(
|
|
286
|
+
die(missing(root, name))
|
|
287
287
|
if task.in_backlog:
|
|
288
288
|
die(f"'{name}' is in backlog — pull it into a version first "
|
|
289
289
|
f"(jarvis work place {name} --version <v>)")
|
|
@@ -308,13 +308,13 @@ def cmd_move(args) -> int:
|
|
|
308
308
|
delivered = (args.get("delivered") or "").strip()
|
|
309
309
|
not_included = (args.get("not-included") or args.get("not_included") or "").strip()
|
|
310
310
|
if to == "complete":
|
|
311
|
-
|
|
312
|
-
if
|
|
311
|
+
unmet = delivery_gate(delivered, not_included)
|
|
312
|
+
if unmet:
|
|
313
313
|
print(f"error: '{name}' cannot complete — the delivery line is "
|
|
314
314
|
f"incomplete:", file=sys.stderr)
|
|
315
|
-
for m in
|
|
315
|
+
for m in unmet:
|
|
316
316
|
print(f" · {m}", file=sys.stderr)
|
|
317
|
-
events.append(root, "gate-refused", name, why="; ".join(
|
|
317
|
+
events.append(root, "gate-refused", name, why="; ".join(unmet)[:300])
|
|
318
318
|
return 1
|
|
319
319
|
if not report_gate(root, task, (args.get("accept") or "").strip(),
|
|
320
320
|
(args.get("owner") or "").strip()):
|
|
@@ -376,7 +376,7 @@ def cmd_handoff(args) -> int:
|
|
|
376
376
|
name = args["name"]
|
|
377
377
|
task = locate(root, name)
|
|
378
378
|
if not task:
|
|
379
|
-
die(
|
|
379
|
+
die(missing(root, name))
|
|
380
380
|
if task.in_backlog or task.status == "queue":
|
|
381
381
|
die(f"'{name}' is queued/backlog — handoff is for in-progress work "
|
|
382
382
|
f"(pick it up first: jarvis work move {name} in-progress)")
|
|
@@ -392,7 +392,7 @@ def cmd_session(args) -> int:
|
|
|
392
392
|
root = find_work_root()
|
|
393
393
|
task = locate(root, args["name"])
|
|
394
394
|
if not task:
|
|
395
|
-
die(
|
|
395
|
+
die(missing(root, args["name"]))
|
|
396
396
|
given = (args.get("id") or "").strip()
|
|
397
397
|
label = None
|
|
398
398
|
if given:
|
|
@@ -433,7 +433,7 @@ def cmd_plan(args) -> int:
|
|
|
433
433
|
name = args["name"]
|
|
434
434
|
task = locate(root, name)
|
|
435
435
|
if not task:
|
|
436
|
-
die(
|
|
436
|
+
die(missing(root, name))
|
|
437
437
|
if not args.get("file") and not PLANS_DIR:
|
|
438
438
|
die("no `plans.dir` configured — pass `--file <path>`, or set "
|
|
439
439
|
"`plans.dir` in .claude/work.config.json if this repo keeps a "
|
|
@@ -457,13 +457,19 @@ def cmd_plan(args) -> int:
|
|
|
457
457
|
print(f"recorded plan '{path}' on '{task.name}'")
|
|
458
458
|
return 0
|
|
459
459
|
def cmd_path(args) -> int:
|
|
460
|
+
"""Resolve any name to its folder — including work in a cut that has shipped.
|
|
461
|
+
|
|
462
|
+
This is the READ door, so it opts into the filed cuts at every tier.
|
|
463
|
+
`locate_version` already did; the two below did not, which is how filing one
|
|
464
|
+
cut made six tasks unresolvable while the cut holding them still answered.
|
|
465
|
+
"""
|
|
460
466
|
root = find_work_root()
|
|
461
467
|
name = args["name"]
|
|
462
|
-
task = locate(root, name)
|
|
468
|
+
task = locate(root, name, filed=True)
|
|
463
469
|
if task:
|
|
464
470
|
print(task.folder)
|
|
465
471
|
return 0
|
|
466
|
-
epic = locate_epic(root, name)
|
|
472
|
+
epic = locate_epic(root, name, filed=True)
|
|
467
473
|
if epic:
|
|
468
474
|
print(epic.folder)
|
|
469
475
|
return 0
|
package/harness/test_work.py
CHANGED
|
@@ -2770,6 +2770,91 @@ def test_a_formatter_cannot_quietly_break_the_scaffolding_templates():
|
|
|
2770
2770
|
f"reformatted them: {offenders[:5]}")
|
|
2771
2771
|
|
|
2772
2772
|
|
|
2773
|
+
def _filed_cut(tmp: str, home: str = "complete", released: str = "2026-09-11"):
|
|
2774
|
+
"""A cut that has LEFT the board, with one epic and one completed task in it."""
|
|
2775
|
+
root = Path(tmp)
|
|
2776
|
+
v = root / "versions" / home / "00-shipped"
|
|
2777
|
+
task = v / "an-epic" / "complete" / "the-work"
|
|
2778
|
+
task.mkdir(parents=True)
|
|
2779
|
+
(root / "versions").mkdir(parents=True, exist_ok=True)
|
|
2780
|
+
(root / "versions" / "backlog").mkdir(exist_ok=True)
|
|
2781
|
+
(root / "README.md").write_text(f"# Work\n\n{tree.BACKLOG_START}\n{tree.BACKLOG_END}\n")
|
|
2782
|
+
rel = f"released: {released}\n" if released else ""
|
|
2783
|
+
(v / "version.md").write_text(
|
|
2784
|
+
f"---\ncreated: 2026-08-01\norder: 0\noutcome: x\n{rel}archived: 2026-09-11\n---\n\n# Shipped\n")
|
|
2785
|
+
(task / "task.md").write_text("---\npriority: P0\n---\n\n# The work\n")
|
|
2786
|
+
return v
|
|
2787
|
+
|
|
2788
|
+
|
|
2789
|
+
def test_work_inside_a_filed_cut_still_answers_to_its_name():
|
|
2790
|
+
"""`locate_version` always searched all three homes — off the board is not gone,
|
|
2791
|
+
and `path` keeps answering. The work INSIDE a cut never did.
|
|
2792
|
+
|
|
2793
|
+
Filing one cut made six tasks unresolvable in a single command, and between
|
|
2794
|
+
them they were named in 55 commit trailers, which is the record a board write
|
|
2795
|
+
leaves. Both homes, because a cut leaves the board for two different reasons.
|
|
2796
|
+
"""
|
|
2797
|
+
for home in ("complete", "archive"):
|
|
2798
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
2799
|
+
_filed_cut(tmp, home)
|
|
2800
|
+
with _work_dir(tmp) as root:
|
|
2801
|
+
# Live resolution still sees only live work — `list`, the README
|
|
2802
|
+
# and every lint depend on that and must not start seeing shipped
|
|
2803
|
+
# work because reads were widened.
|
|
2804
|
+
assert model.locate(root, "the-work") is None
|
|
2805
|
+
assert model.locate_epic(root, "an-epic") is None
|
|
2806
|
+
assert model.scan(root)["versions"] == []
|
|
2807
|
+
|
|
2808
|
+
found = model.locate(root, "the-work", filed=True)
|
|
2809
|
+
assert found and found.name == "the-work", home
|
|
2810
|
+
epic = model.locate_epic(root, "an-epic", filed=True)
|
|
2811
|
+
assert epic and epic.name == "an-epic", home
|
|
2812
|
+
assert model.locate_version(root, "00-shipped")
|
|
2813
|
+
|
|
2814
|
+
|
|
2815
|
+
def test_a_write_naming_shipped_work_says_where_it_went():
|
|
2816
|
+
"""The refusal is the point. Every write said "no task named 'x' found" the
|
|
2817
|
+
moment a cut was filed — the least useful true thing to say about work that
|
|
2818
|
+
shipped last Thursday, and indistinguishable from a typo."""
|
|
2819
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
2820
|
+
_filed_cut(tmp)
|
|
2821
|
+
with _work_dir(tmp) as root:
|
|
2822
|
+
said = model.missing(root, "the-work")
|
|
2823
|
+
assert "shipped in '00-shipped'" in said, said
|
|
2824
|
+
assert "2026-09-11" in said, said
|
|
2825
|
+
# A name that genuinely does not exist keeps the plain answer: a
|
|
2826
|
+
# suggestion appended to a typo is noise.
|
|
2827
|
+
assert model.missing(root, "no-such-thing") == \
|
|
2828
|
+
"no task named 'no-such-thing' found"
|
|
2829
|
+
|
|
2830
|
+
|
|
2831
|
+
def test_shipped_work_cannot_be_moved_however_it_is_named():
|
|
2832
|
+
"""Reads were widened; writes were not, and that is the whole care taken here.
|
|
2833
|
+
What a release delivered is a matter of record."""
|
|
2834
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
2835
|
+
v = _filed_cut(tmp)
|
|
2836
|
+
with _work_dir(tmp):
|
|
2837
|
+
try:
|
|
2838
|
+
task.cmd_move({"name": "the-work", "status": "in-progress"})
|
|
2839
|
+
assert False, "shipped work must not be movable"
|
|
2840
|
+
except SystemExit:
|
|
2841
|
+
pass
|
|
2842
|
+
assert (v / "an-epic" / "complete" / "the-work").is_dir()
|
|
2843
|
+
assert not (v / "an-epic" / "in-progress").exists()
|
|
2844
|
+
|
|
2845
|
+
|
|
2846
|
+
def test_a_cut_in_the_archive_is_not_counted_as_delivered():
|
|
2847
|
+
"""`scan_filed` and `scan_shipped` answer different questions. A cut in
|
|
2848
|
+
`archive/` left the board because its work moved and there was no outcome it
|
|
2849
|
+
could honestly claim — counting it as delivered would be the lie archiving
|
|
2850
|
+
exists to avoid."""
|
|
2851
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
2852
|
+
_filed_cut(tmp, "archive", released="")
|
|
2853
|
+
with _work_dir(tmp) as root:
|
|
2854
|
+
assert [v.name for v in model.scan_filed(root)] == ["00-shipped"]
|
|
2855
|
+
assert model.scan_shipped(root) == []
|
|
2856
|
+
|
|
2857
|
+
|
|
2773
2858
|
def test_the_completion_gate_refuses_and_says_which_gate_held():
|
|
2774
2859
|
with tempfile.TemporaryDirectory() as tmp:
|
|
2775
2860
|
v = _tree(tmp)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appchy/jarvis",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.84",
|
|
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/anthropic": "1.0.0",
|
|
60
|
-
"@jarvis/board": "0.1.0",
|
|
61
59
|
"@jarvis/agents": "1.0.0",
|
|
60
|
+
"@jarvis/data": "0.1.0",
|
|
62
61
|
"@jarvis/errors": "1.0.0",
|
|
62
|
+
"@jarvis/anthropic": "1.0.0",
|
|
63
|
+
"@jarvis/board": "0.1.0",
|
|
63
64
|
"@jarvis/logger": "1.0.0",
|
|
64
65
|
"@jarvis/rpc": "1.0.0",
|
|
65
|
-
"@jarvis/types": "1.0.0",
|
|
66
|
-
"@jarvis/data": "0.1.0",
|
|
67
66
|
"@jarvis/typescript-config": "1.0.0",
|
|
68
|
-
"@jarvis/
|
|
69
|
-
"@jarvis/vitest-config": "1.0.0"
|
|
67
|
+
"@jarvis/types": "1.0.0",
|
|
68
|
+
"@jarvis/vitest-config": "1.0.0",
|
|
69
|
+
"@jarvis/ui": "0.1.0"
|
|
70
70
|
},
|
|
71
71
|
"scripts": {
|
|
72
72
|
"dev": "tsx watch src/bin.ts start --foreground",
|