@appchy/jarvis 0.1.48 → 0.1.49

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
@@ -9788,7 +9788,7 @@ import { createRequire as createRequire2 } from "module";
9788
9788
  var _require = createRequire2(import.meta.url);
9789
9789
  var VERSION2 = _require("../package.json").version ?? "0.0.0";
9790
9790
  var IS_DEV = String(_require("../package.json").name ?? "").endsWith("-dev");
9791
- var SHA = "5b34716";
9791
+ var SHA = "f529917";
9792
9792
  var BUILT = "2026-09-08";
9793
9793
  var BUILD = SHA ?? "source";
9794
9794
  var BUILD_LABEL = `${SHA ? `${VERSION2} (${SHA}${BUILT ? ` ${BUILT}` : ""})` : `${VERSION2} (source)`}${IS_DEV ? " \u2014 development build" : ""}`;
@@ -107,11 +107,22 @@ def _shape_lint(root: Path, s: dict) -> list:
107
107
 
108
108
  # 2 — a task spanning too many code regions is a version wearing a task
109
109
  # costume: it cannot land as one coherent end-to-end slice.
110
+ # ONE class is exempt and it is DECLARED rather than inferred: a task
111
+ # carrying `one_goal:` has said in writing why its width is right, and
112
+ # the sentence is the exemption. Some architectures have shapes that are
113
+ # genuinely one goal across three regions — measured in `../mixbrix`
114
+ # 2026-09-09, six tasks read *work out a measurement (`dsp`), expose it
115
+ # (`cli`), draw it (`daw`)* and five more *a node behaviour needing the
116
+ # document, the engine and an atom* — and splitting those to satisfy a
117
+ # counter produces three tasks nobody can finish alone. It is checked the
118
+ # way `owner:` is: an EMPTY `one_goal:` exempts nothing, so the field
119
+ # cannot be used to silence this without saying something.
110
120
  for t in active:
111
- if len(t.code) >= TASK_REGION_CAP:
121
+ if len(t.code) >= TASK_REGION_CAP and not t.one_goal:
112
122
  warns.append(f"{t.name}: spans {len(t.code)} code regions "
113
123
  f"({', '.join(t.code)}) — a task is ONE goal; at "
114
- f"{TASK_REGION_CAP}+ it is an epic, not a task")
124
+ f"{TASK_REGION_CAP}+ it is an epic, not a task. If the "
125
+ f"width IS one goal, say why in `one_goal:`")
115
126
 
116
127
  # 3 — an epic with no `covers:` moves no stated acceptance criterion, so
117
128
  # nobody can tell when it is finished. Released epics have no epic.md
@@ -125,7 +136,17 @@ def _shape_lint(root: Path, s: dict) -> list:
125
136
  # an `owner:` that resolves to nothing exempts nothing, so the field
126
137
  # cannot be used to silence this by writing anything in it. An epic
127
138
  # owned by a FEATURE is not exempt: it has criteria available.
128
- for e in [e for v in s["versions"] for e in v.epics] + s["backlog_epics"]:
139
+ #
140
+ # A BACKLOG epic is skipped entirely, and that is a second real class
141
+ # rather than a loophole: it is a captured WANT, written down so it is not
142
+ # lost, and which promises it moves is chosen when somebody PLANS it.
143
+ # Asking first means guessing, and the guess is what the planning session
144
+ # then argues with. Two in `../mixbrix` say so in their own text — *"NOT
145
+ # PLANNED. This is a captured want, not a design."* `planned` cannot
146
+ # express this: it means `epic.md` EXISTS (the release-archive flag), so it
147
+ # is true of every backlog epic and the only way to reach the exemption
148
+ # would be deleting the file holding the want.
149
+ for e in [e for v in s["versions"] for e in v.epics]:
129
150
  if not e.planned:
130
151
  continue
131
152
  if not e.covers:
@@ -149,8 +170,11 @@ def _shape_lint(root: Path, s: dict) -> list:
149
170
  # the design happens ONCE for a whole goal; at one or two tasks there is
150
171
  # no "whole goal", and the board fills with topics wearing epic costumes.
151
172
  # Skip a DONE epic (`<v>/complete/`) — it is history, and an epic that
152
- # shipped two tasks was never the failure this catches.
153
- for e in [e for v in s["versions"] for e in v.epics] + s["backlog_epics"]:
173
+ # shipped two tasks was never the failure this catches. Skip a BACKLOG
174
+ # epic for the same reason rule 3 does: a want nobody has planned has no
175
+ # slices yet, and "fold it into an epic that fits" is advice about work
176
+ # being cut, not about a note somebody parked.
177
+ for e in [e for v in s["versions"] for e in v.epics]:
154
178
  if not e.planned or e.done_tier:
155
179
  continue
156
180
  n = len(e.all_tasks())
@@ -70,6 +70,10 @@ class Task:
70
70
  self.priority = str(self.fm.get("priority", "P2")).strip()
71
71
  self.depends_on = as_list(self.fm.get("depends_on"))
72
72
  self.code = as_list(self.fm.get("code"))
73
+ # A task may DECLARE that its width is deliberate, with the reason.
74
+ # Read as a string rather than a flag: the region cap is answered by
75
+ # an argument, not by a `true` somebody typed to silence it.
76
+ self.one_goal = str(self.fm.get("one_goal", "")).strip()
73
77
  self.covers = as_list(self.fm.get("covers"))
74
78
  # `owner:` is canonical; `product:` is the pre-domain spelling still on
75
79
  # disk while the rename lands. One reader, so no consumer has to know.
@@ -105,6 +105,13 @@ EPIC_FM_ORDER = ("created", "updated", "covers", "continues")
105
105
  # that cheap are exactly what a line budget deletes. What actually goes wrong —
106
106
  # a brief quietly becoming a competing plan — is caught by the artifact lint,
107
107
  # which looks for the FILES, not the size.
108
+ # A task may ANSWER this rather than obey it, since 2026-09-09 (founder call):
109
+ # `one_goal: "<why>"` in its frontmatter says the width is deliberate and states
110
+ # the reason, and the lint then leaves it alone. The cap stayed at 3 rather than
111
+ # moving to 4 on purpose — raising it silences the check everywhere and records
112
+ # nothing, where a declared reason is readable by the next person and still lets
113
+ # the warning bite for a task that has not thought about it. An EMPTY `one_goal:`
114
+ # exempts nothing, the same way a dangling `owner:` exempts no epic.
108
115
  TASK_REGION_CAP = 3
109
116
  # The other end of the same rule, added 2026-08-02 (founder call), and it names a
110
117
  # measured failure too: the backlog had reached 14 epics of which NINE held two
@@ -801,6 +801,64 @@ def test_shape_lint_flags_a_task_spanning_three_code_regions():
801
801
  assert not any("narrow: spans" in x for x in w)
802
802
 
803
803
 
804
+ def test_a_task_that_says_its_width_is_deliberate_is_not_flagged():
805
+ """Some architectures have shapes that genuinely are one goal across three
806
+ regions. The exemption is a SENTENCE, so the reason is readable rather than a
807
+ flag somebody set."""
808
+ with tempfile.TemporaryDirectory() as tmp:
809
+ v = _tree(tmp)
810
+ e = _epic(v, "e")
811
+ _task(e / "queue", "wide", fm="priority: P1\ncode: [lesson, tools, web]\n"
812
+ "one_goal: the measurement, the verb that exposes it and the "
813
+ "drawing are one thing a person asks for\n")
814
+ assert not any("wide: spans" in x for x in _warns(Path(tmp)))
815
+
816
+
817
+ def test_an_empty_one_goal_exempts_nothing():
818
+ """Checked the way `owner:` is. Otherwise the field is a way to silence the
819
+ lint by writing nothing at all in it."""
820
+ with tempfile.TemporaryDirectory() as tmp:
821
+ v = _tree(tmp)
822
+ e = _epic(v, "e")
823
+ _task(e / "queue", "wide",
824
+ fm="priority: P1\ncode: [lesson, tools, web]\none_goal:\n")
825
+ assert any("wide: spans 3 code regions" in x for x in _warns(Path(tmp)))
826
+
827
+
828
+ def test_a_backlog_epic_is_not_asked_what_it_covers():
829
+ """A captured want has no criteria yet — they are chosen when it is PLANNED,
830
+ and asking first only produces a guess the planning session argues with."""
831
+ with tempfile.TemporaryDirectory() as tmp:
832
+ root = Path(tmp)
833
+ _tree(tmp)
834
+ _epic(root / "backlog", "you-reach-it-from-your-phone", covers="[]")
835
+ assert not any("you-reach-it-from-your-phone: no covers:"
836
+ in w for w in _warns(root))
837
+
838
+
839
+ def test_a_backlog_epic_is_not_held_to_the_task_floor():
840
+ """Same reason: a want nobody has planned has no slices, so "fold it into an
841
+ epic that fits" is advice about work being cut rather than a parked note."""
842
+ with tempfile.TemporaryDirectory() as tmp:
843
+ root = Path(tmp)
844
+ _tree(tmp)
845
+ _epic(root / "backlog", "it-sings-what-you-write", covers="[]")
846
+ assert not any("it-sings-what-you-write: 0 task(s)"
847
+ in w for w in _warns(root))
848
+
849
+
850
+ def test_a_version_epic_is_still_held_to_both():
851
+ """The narrowing is to the BACKLOG, not to the rules. An epic pulled into a
852
+ cut answers for its criteria and its slices as it always did."""
853
+ with tempfile.TemporaryDirectory() as tmp:
854
+ root = Path(tmp)
855
+ v = _tree(tmp)
856
+ _epic(v, "in-a-cut", covers="[]")
857
+ w = _warns(root)
858
+ assert any("in-a-cut: no covers:" in x for x in w), w
859
+ assert any("in-a-cut: 0 task(s)" in x for x in w), w
860
+
861
+
804
862
  def test_shape_lint_flags_a_thin_epic_below_the_task_floor():
805
863
  # The other end of the region cap: a one- or two-task epic is a topic wearing
806
864
  # an epic costume, and its epic.md degenerates into a second copy of the brief.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appchy/jarvis",
3
- "version": "0.1.48",
3
+ "version": "0.1.49",
4
4
  "description": "Jarvis — local AI coding assistant CLI",
5
5
  "private": false,
6
6
  "type": "module",
@@ -51,10 +51,10 @@
51
51
  "vitest": "^2.1.0",
52
52
  "@jarvis/agents": "1.0.0",
53
53
  "@jarvis/anthropic": "1.0.0",
54
+ "@jarvis/board": "0.1.0",
54
55
  "@jarvis/data": "0.1.0",
55
56
  "@jarvis/logger": "1.0.0",
56
57
  "@jarvis/rpc": "1.0.0",
57
- "@jarvis/board": "0.1.0",
58
58
  "@jarvis/types": "1.0.0",
59
59
  "@jarvis/typescript-config": "1.0.0",
60
60
  "@jarvis/vitest-config": "1.0.0"