@appchy/jarvis 0.1.90 → 0.1.91

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
@@ -10145,7 +10145,7 @@ import { createRequire as createRequire2 } from "module";
10145
10145
  var _require = createRequire2(import.meta.url);
10146
10146
  var VERSION2 = _require("../package.json").version ?? "0.0.0";
10147
10147
  var IS_DEV = String(_require("../package.json").name ?? "").endsWith("-dev");
10148
- var SHA = "223098e";
10148
+ var SHA = "eaa6a77";
10149
10149
  var BUILT = "2026-09-11";
10150
10150
  var BUILD = SHA ?? "source";
10151
10151
  var BUILD_LABEL = `${SHA ? `${VERSION2} (${SHA}${BUILT ? ` ${BUILT}` : ""})` : `${VERSION2} (source)`}${IS_DEV ? " \u2014 development build" : ""}`;
@@ -27,6 +27,14 @@ continues: {{CONTINUES}}
27
27
  epic with no `covers:` is a lint: work that moves no stated criterion is
28
28
  work nobody can tell is finished.
29
29
 
30
+ `one_goal:` is the epic's answer to the THIN-epic check, and it is a
31
+ sentence rather than a flag. An epic under the task floor is usually a topic
32
+ wearing an epic costume — but not always, and where the goal genuinely needs
33
+ only these slices, saying so in writing is the exemption. An empty one
34
+ exempts nothing, the same way `owner:` does: a field that can be silenced by
35
+ writing anything in it is not a check. The task tier carries the same field
36
+ for the same reason, about its WIDTH rather than its size.
37
+
30
38
  `owner:` is the ONE exemption from that, and it is not an escape hatch.
31
39
  Some epics genuinely move no product criterion — a trailer, a test harness,
32
40
  a migration — because nothing a user can do changes. Those answer to an
@@ -174,16 +174,24 @@ def _shape_lint(root: Path, s: dict) -> list:
174
174
  # epic for the same reason rule 3 does: a want nobody has planned has no
175
175
  # slices yet, and "fold it into an epic that fits" is advice about work
176
176
  # being cut, not about a note somebody parked.
177
+ #
178
+ # The exemption is DECLARED, the same way a task declares its width is
179
+ # deliberate. This message used to end "unless §Plan names the slices not
180
+ # yet cut" — an escape nothing implemented, so the only way to satisfy it
181
+ # was to stop reading the warning. A tool that promises an exemption it
182
+ # does not honour teaches people to ignore every warning beside it, which
183
+ # costs more than the thin epic ever did.
177
184
  for e in [e for v in s["versions"] for e in v.epics]:
178
185
  if not e.planned or e.done_tier:
179
186
  continue
180
187
  n = len(e.all_tasks())
181
- if n < EPIC_TASK_FLOOR:
188
+ if n < EPIC_TASK_FLOOR and not e.one_goal:
182
189
  warns.append(
183
190
  f"epic {e.name}: {n} task(s) — an epic is the goal you plan "
184
191
  f"TOGETHER, and under {EPIC_TASK_FLOOR} there is no together. "
185
192
  f"Fold {'it' if n == 1 else 'them'} into an existing epic that "
186
- f"fits, unless §Plan names the slices not yet cut")
193
+ f"fits — or, if this goal genuinely needs only these, say why in "
194
+ f"`one_goal:`")
187
195
 
188
196
  # 3b — a task inside an epic carrying its own plan.md. The design is settled
189
197
  # ONCE in `epic.md` §Plan; a second design doc per task is the
@@ -148,6 +148,13 @@ class Epic:
148
148
  # the tree disagrees with — the folder's home is the only statement of it.
149
149
  self.done_tier = folder.parent.name == DONE_TIER
150
150
  self.covers = as_list(self.fm.get("covers"))
151
+ # The same declared exemption a task carries for its WIDTH, at the tier
152
+ # above and for its SIZE. One field name across both rather than a second
153
+ # vocabulary: in each case the claim is that the shape was chosen and the
154
+ # sentence is what makes it reviewable. Empty exempts nothing, exactly as
155
+ # `owner:` and the task's own do — a field that can be silenced by writing
156
+ # anything in it is not a check.
157
+ self.one_goal = str(self.fm.get("one_goal", "")).strip()
151
158
  # Same vocabulary as a task's `owner:` — a bare `<feature>` or a bare
152
159
  # org-domain. It exists on an epic for one reason: an epic owned by a
153
160
  # DOMAIN has no `AC-nn` layer to qualify against, because a domain hosts
@@ -453,12 +453,13 @@ def _task(folder: Path, name: str, body: str = "# T\n", fm: str = "priority: P1\
453
453
 
454
454
 
455
455
  def _epic(folder: Path, name: str, covers: str = "[lesson/AC-01]",
456
- owner: str = ""):
456
+ owner: str = "", one_goal: str = ""):
457
457
  d = folder / name
458
458
  d.mkdir(parents=True)
459
459
  own = f"owner: {owner}\n" if owner else ""
460
+ goal = f"one_goal: {one_goal}\n" if one_goal else ""
460
461
  (d / "epic.md").write_text(
461
- f"---\ntype: epic\ncreated: 2026-08-01\ncovers: {covers}\n{own}---\n\n# {name}\n")
462
+ f"---\ntype: epic\ncreated: 2026-08-01\ncovers: {covers}\n{own}{goal}---\n\n# {name}\n")
462
463
  return d
463
464
 
464
465
 
@@ -857,6 +858,32 @@ def test_an_empty_one_goal_exempts_nothing():
857
858
  assert any("wide: spans 3 code regions" in x for x in _warns(Path(tmp)))
858
859
 
859
860
 
861
+ def test_a_thin_epic_that_says_its_size_is_deliberate_is_not_flagged():
862
+ """The message used to end "unless §Plan names the slices not yet cut" — an
863
+ escape nothing implemented, so the only way to satisfy it was to stop reading
864
+ the warning. The exemption is a SENTENCE now, the same shape a task uses for
865
+ its width, so one vocabulary covers both tiers."""
866
+ with tempfile.TemporaryDirectory() as tmp:
867
+ v = _tree(tmp)
868
+ e = _epic(v, "thin", one_goal="the two slices are the whole of it; there "
869
+ "is no third thing this goal needs")
870
+ _task(e / "queue", "one")
871
+ # Specific to the FLOOR message: the same epic trips other lints in a bare
872
+ # fixture tree, and a substring match on its name would pass or fail on one
873
+ # of those instead of on the thing under test.
874
+ assert not any("epic thin: 1 task(s)" in x for x in _warns(Path(tmp)))
875
+
876
+
877
+ def test_an_empty_one_goal_exempts_no_thin_epic_either():
878
+ """Checked the way the task's own is, or the field becomes a way to silence
879
+ the lint by writing nothing at all in it."""
880
+ with tempfile.TemporaryDirectory() as tmp:
881
+ v = _tree(tmp)
882
+ e = _epic(v, "thin", one_goal=" ")
883
+ _task(e / "queue", "one")
884
+ assert any("epic thin: 1 task(s)" in x for x in _warns(Path(tmp)))
885
+
886
+
860
887
  def test_a_backlog_epic_is_not_asked_what_it_covers():
861
888
  """A captured want has no criteria yet — they are chosen when it is PLANNED,
862
889
  and asking first only produces a guess the planning session argues with."""
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appchy/jarvis",
3
- "version": "0.1.90",
3
+ "version": "0.1.91",
4
4
  "description": "Jarvis — local AI coding assistant CLI",
5
5
  "private": false,
6
6
  "type": "module",
@@ -59,14 +59,14 @@
59
59
  "@jarvis/agents": "1.0.0",
60
60
  "@jarvis/anthropic": "1.0.0",
61
61
  "@jarvis/board": "0.1.0",
62
- "@jarvis/data": "0.1.0",
63
62
  "@jarvis/errors": "1.0.0",
64
63
  "@jarvis/logger": "1.0.0",
65
64
  "@jarvis/rpc": "1.0.0",
65
+ "@jarvis/types": "1.0.0",
66
66
  "@jarvis/typescript-config": "1.0.0",
67
- "@jarvis/vitest-config": "1.0.0",
68
67
  "@jarvis/ui": "0.1.0",
69
- "@jarvis/types": "1.0.0"
68
+ "@jarvis/vitest-config": "1.0.0",
69
+ "@jarvis/data": "0.1.0"
70
70
  },
71
71
  "scripts": {
72
72
  "dev": "tsx watch src/bin.ts start --foreground",