@appchy/jarvis 0.1.89 → 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/data/mcp.mjs CHANGED
@@ -1651,14 +1651,14 @@ function planScope(ctx, req) {
1651
1651
  anticipated,
1652
1652
  editCodeNodes,
1653
1653
  governance,
1654
- obligations: toObligations(governance, anticipated),
1654
+ obligations: toObligations(governance, anticipated, testsCovering),
1655
1655
  references,
1656
1656
  testsCovering,
1657
1657
  blastRadius,
1658
1658
  lowConfidence
1659
1659
  };
1660
1660
  }
1661
- function toObligations(governance, existing = []) {
1661
+ function toObligations(governance, existing = [], keep = []) {
1662
1662
  const rules = governance.map((g) => {
1663
1663
  const full = g.body !== void 0;
1664
1664
  return {
@@ -1668,13 +1668,19 @@ function toObligations(governance, existing = []) {
1668
1668
  statement: full ? `Its full text is here. Say how the plan satisfies it, or why it does not apply.` : `Not read yet \u2014 only its title arrived. Read it, then say how the plan satisfies it.`
1669
1669
  };
1670
1670
  });
1671
+ const preserved = keep.map((c) => ({
1672
+ type: "keep",
1673
+ id: c.line === void 0 ? c.file : `${c.file}:${c.line}`,
1674
+ statement: `Passes over this code today. It must still pass \u2014 and it must not be the thing the plan changes to make it.`
1675
+ }));
1671
1676
  return [
1672
1677
  ...rules,
1673
1678
  ...existing.map((e) => ({
1674
1679
  type: "existing",
1675
1680
  id: e.line === void 0 ? e.file : `${e.file}:${e.line}`,
1676
1681
  statement: `Already in the tree and close to what was described. Reuse it, or say why the plan builds alongside it.`
1677
- }))
1682
+ })),
1683
+ ...preserved
1678
1684
  ];
1679
1685
  }
1680
1686
  function isCodeHit(hit) {
@@ -2304,7 +2310,8 @@ async function scope(ctx, req) {
2304
2310
  const topGov = plan.governance.filter((g) => g.body === void 0).slice(0, 3).map((g) => g.id).join(", ");
2305
2311
  const unread = plan.obligations.filter((o) => o.type === "governance" && !o.full).length;
2306
2312
  const reuse = plan.obligations.filter((o) => o.type === "existing").length;
2307
- const summary = plan.lowConfidence ? `Low-confidence (build-new) scope for "${req.task}": no code strongly matched \u2014 ${plan.anticipated.length} integration point(s) to READ, not edit; ${plan.governance.length} concept-matched governance. Lead with search + read.` : `Scoped "${req.task}": ${plan.editSet.length} edit target(s), ${plan.governance.length} governance, ${plan.testsCovering.length} covering test(s), blast radius ${plan.blastRadius.total} (${confidence})${topFiles ? ` \u2014 ${topFiles}` : ""}.${rules.ids > 0 && topGov ? ` ${rules.full} rule(s) came back in full; NEXT: read ${topGov} for the ${rules.ids} that did not,` : rules.full > 0 ? ` All ${rules.full} governing rule(s) came back in FULL \u2014 read them here, not with map_read;` : " NEXT:"} then check the edit-set with map_scope {edits} before you commit.${plan.obligations.length > 0 ? ` ${plan.obligations.length} obligation(s) outstanding${unread > 0 ? `, ${unread} rule(s) not yet read` : ""}${reuse > 0 ? `, ${reuse} thing(s) that already exist` : ""} \u2014 see \`obligations\`.` : ""}`;
2313
+ const keep = plan.obligations.filter((o) => o.type === "keep").length;
2314
+ const summary = plan.lowConfidence ? `Low-confidence (build-new) scope for "${req.task}": no code strongly matched \u2014 ${plan.anticipated.length} integration point(s) to READ, not edit; ${plan.governance.length} concept-matched governance. Lead with search + read.` : `Scoped "${req.task}": ${plan.editSet.length} edit target(s), ${plan.governance.length} governance, ${plan.testsCovering.length} covering test(s), blast radius ${plan.blastRadius.total} (${confidence})${topFiles ? ` \u2014 ${topFiles}` : ""}.${rules.ids > 0 && topGov ? ` ${rules.full} rule(s) came back in full; NEXT: read ${topGov} for the ${rules.ids} that did not,` : rules.full > 0 ? ` All ${rules.full} governing rule(s) came back in FULL \u2014 read them here, not with map_read;` : " NEXT:"} then check the edit-set with map_scope {edits} before you commit.${plan.obligations.length > 0 ? ` ${plan.obligations.length} obligation(s) outstanding${unread > 0 ? `, ${unread} rule(s) not yet read` : ""}${reuse > 0 ? `, ${reuse} thing(s) that already exist` : ""}${keep > 0 ? `, ${keep} that must keep passing` : ""} \u2014 see \`obligations\`.` : ""}`;
2308
2315
  return {
2309
2316
  summary,
2310
2317
  task: req.task,
@@ -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.89",
3
+ "version": "0.1.91",
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
59
  "@jarvis/agents": "1.0.0",
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
- "@jarvis/types": "1.0.0",
66
64
  "@jarvis/rpc": "1.0.0",
65
+ "@jarvis/types": "1.0.0",
67
66
  "@jarvis/typescript-config": "1.0.0",
67
+ "@jarvis/ui": "0.1.0",
68
68
  "@jarvis/vitest-config": "1.0.0",
69
- "@jarvis/ui": "0.1.0"
69
+ "@jarvis/data": "0.1.0"
70
70
  },
71
71
  "scripts": {
72
72
  "dev": "tsx watch src/bin.ts start --foreground",