@appchy/jarvis 0.1.66 → 0.1.68

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
@@ -10115,7 +10115,7 @@ import { createRequire as createRequire2 } from "module";
10115
10115
  var _require = createRequire2(import.meta.url);
10116
10116
  var VERSION2 = _require("../package.json").version ?? "0.0.0";
10117
10117
  var IS_DEV = String(_require("../package.json").name ?? "").endsWith("-dev");
10118
- var SHA = "e0c2058";
10118
+ var SHA = "ac6ee80";
10119
10119
  var BUILT = "2026-09-10";
10120
10120
  var BUILD = SHA ?? "source";
10121
10121
  var BUILD_LABEL = `${SHA ? `${VERSION2} (${SHA}${BUILT ? ` ${BUILT}` : ""})` : `${VERSION2} (source)`}${IS_DEV ? " \u2014 development build" : ""}`;
@@ -85,6 +85,17 @@ second copy of it here is a second copy to drift.
85
85
  """
86
86
 
87
87
 
88
+ #: Where a repo's `work.config.json` says its schema lives. The package is published,
89
+ #: so a CDN already serves it and there is nothing to host — and it is the same line in
90
+ #: every repo, which a path into anybody's node_modules could never be.
91
+ _WORK_CONFIG = (
92
+ "{\n"
93
+ ' "$schema": "https://cdn.jsdelivr.net/npm/@appchy/jarvis/harness/schema/'
94
+ 'work.config.schema.json"\n'
95
+ "}\n"
96
+ )
97
+
98
+
88
99
  def cmd_init(args) -> int:
89
100
  """Scaffold a work tree: the board, the roadmap, and the nine org-domains.
90
101
 
@@ -128,6 +139,19 @@ def cmd_init(args) -> int:
128
139
  write(root / name / "README.md",
129
140
  _charter(name, name.replace("-", " ").capitalize()))
130
141
 
142
+ # The repo's own config, which `init` did not use to write — so every repo that
143
+ # adopted the harness hand-copied one from a sibling, and copied whatever that
144
+ # sibling's `$schema` happened to point at. Three of them ended up naming a path
145
+ # inside a plugin that no longer exists, and a dangling `$schema` is not an error
146
+ # anywhere, so nothing said so.
147
+ #
148
+ # Only the pointer is written. Everything else in this file is a choice about the
149
+ # repo — its ledger letter, its verify commands, its instruction preset — and a
150
+ # scaffold that guesses those is a scaffold somebody has to undo. The schema tells
151
+ # the editor what the rest may say, which is exactly what a first-time adopter has
152
+ # no way to know.
153
+ write(project / ".claude" / "work.config.json", _WORK_CONFIG)
154
+
131
155
  print(f"initialised {rel(root, root) or 'work/'} at {root}")
132
156
  if made:
133
157
  print(f" created {len(made)}: {', '.join(made)}")
@@ -285,12 +285,46 @@ def cmd_doctor(args) -> int:
285
285
  from .align import _foreign_qualifier
286
286
  foreign = _foreign_qualifier()
287
287
  other = re.compile(r"\b([A-Z][A-Z0-9]{0,3})-\d+\b")
288
+ # A LEDGER declares itself by defining entries: `### J-52 — …`, `## S-1. …`.
289
+ # Prose never does. That distinction is the whole check — without it the
290
+ # pattern above reads `SHA-256`, `JP-8000` and `AUTO-3` as citations, which
291
+ # on one audio repo was 53 of its reported strays and no configuration
292
+ # could have silenced them honestly: there is no repo called SHA, and
293
+ # calling SHA a ledger to go green is the lie the check exists to prevent.
294
+ #
295
+ # Definitions are looked for in the board AND in the standards file the
296
+ # spine names, because a repo's second ledger usually lives there rather
297
+ # than under `work/`. An unconfigured install is still caught, which is what
298
+ # this check is FOR: its rules are headings, so its prefix is declared here
299
+ # and reported as unmatched.
300
+ # The separator is load-bearing, not decoration. Every ledger in every repo
301
+ # here writes `### J-52 — Title` or `## S-1. Title`; a heading that merely
302
+ # TALKS about an id writes `### AUTO-4 ARRIVED AS A DERIVATION`, with a
303
+ # bare space. Without this, one such heading in one brief was enough to
304
+ # promote a synth's model number to a ledger.
305
+ defines = re.compile(r"^#{1,6}\s+([A-Z][A-Z0-9]{0,3})-\d+(?:\.|\s+[\u2014\u2013-])")
288
306
  allowed = {cfg["ids"]["prefix"], *cfg["ids"]["recognised"], "AC", "B"}
307
+
308
+ board = list(root.rglob("*.md"))
309
+ spine = cfg.get("spine", {}).get("standards")
310
+ elsewhere = [repo / spine] if spine and (repo / spine).is_file() else []
311
+
312
+ declared, lines_by_doc = set(), {}
313
+ for md in board + elsewhere:
314
+ lines = md.read_text(errors="ignore").splitlines()
315
+ if md in board:
316
+ lines_by_doc[md] = lines
317
+ for line in lines:
318
+ d = defines.match(line)
319
+ if d:
320
+ declared.add(d.group(1))
321
+
289
322
  stray = {}
290
- for md in root.rglob("*.md"):
291
- for line in md.read_text(errors="ignore").splitlines():
323
+ for lines in lines_by_doc.values():
324
+ for line in lines:
292
325
  for m in other.finditer(line):
293
- if m.group(1) in allowed or foreign.search(line[:m.start()]):
326
+ if (m.group(1) in allowed or m.group(1) not in declared
327
+ or foreign.search(line[:m.start()])):
294
328
  continue
295
329
  stray.setdefault(m.group(1), 0)
296
330
  stray[m.group(1)] += 1
@@ -298,15 +332,16 @@ def cmd_doctor(args) -> int:
298
332
  top = ", ".join(f"{k}-nn x{v}" for k, v in
299
333
  sorted(stray.items(), key=lambda kv: -kv[1])[:4])
300
334
  warn("ids", f"{sum(stray.values())} id(s) match no configured prefix "
301
- f"({top}). Three readings, and they are far apart. If one "
302
- f"of those IS this repo's ledger letter, set ids.prefix "
303
- f" every check is reading the wrong dialect until you "
304
- f"do. If this repo keeps a SECOND ledger of its own "
305
- f" standards beside rules, say add its letter to "
306
- f"ids.recognised: it is read as native and never "
307
- f"generated. If they are a sibling repo's ids discussed "
308
- f"in prose, add that repo to ids.foreign, which excuses "
309
- f"them where the repo is named alongside.")
335
+ f"({top}). Each of those prefixes DEFINES entries "
336
+ f"somewhere in this repo, so they are ledgers rather than "
337
+ f"prose, and three readings remain. If one IS this repo's "
338
+ f"ledger letter, set ids.prefix every check is reading "
339
+ f"the wrong dialect until you do. If this repo keeps a "
340
+ f"SECOND ledger of its own standards beside rules, say "
341
+ f" add its letter to ids.recognised: it is read as native "
342
+ f"and never generated. If a sibling repo's ledger is "
343
+ f"quoted here in full, add that repo to ids.foreign, which "
344
+ f"excuses it where the repo is named alongside.")
310
345
  else:
311
346
  ok("ids", f"every id matches a configured prefix")
312
347
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://github.com/appchy-ai/plugins/work.config.schema.json",
3
+ "$id": "https://cdn.jsdelivr.net/npm/@appchy/jarvis/harness/schema/work.config.schema.json",
4
4
  "title": "work.config.json",
5
5
  "description": "Per-repo configuration for the `work` harness, committed at .claude/work.config.json. This is the ONLY configuration path: Claude Code has deliberately ignored `pluginConfigs` in project settings since v2.1.207, because a cloned repo could otherwise inject values into hook commands and MCP configs. Treat it as a security surface — a value that reaches a shell command is the same injection wearing different clothes.",
6
6
  "type": "object",
@@ -1778,6 +1778,41 @@ def test_doctor_passes_a_degraded_but_legal_install_and_fails_a_real_break():
1778
1778
  assert f"{tree.cli()} init" in out.getvalue()
1779
1779
 
1780
1780
 
1781
+ def test_doctor_reads_a_ledger_as_an_id_and_leaves_prose_alone():
1782
+ import io, contextlib
1783
+ with tempfile.TemporaryDirectory() as tmp:
1784
+ root = _initialised(tmp)
1785
+
1786
+ # Prose that merely LOOKS like a citation. A hash algorithm, a synth's model
1787
+ # number, and a heading that talks about an id rather than defining one — all
1788
+ # measured on a real audio repo, where they were 53 reported strays that no
1789
+ # configuration could have silenced honestly.
1790
+ (root / "product" / "notes.md").write_text(
1791
+ "# Notes\n\n"
1792
+ "An identical SHA-256 over 10,000 bytes, and the JP-8000 supersaw.\n\n"
1793
+ "### AUTO-4 ARRIVED AS A DERIVATION RATHER THAN A TOLERANCE\n\n"
1794
+ "Their `AUTO-1..4` clause families.\n"
1795
+ )
1796
+ with contextlib.redirect_stdout(io.StringIO()) as out:
1797
+ assert safety.cmd_doctor({"project_root": tmp}) == 0
1798
+ said = out.getvalue()
1799
+ for prose in ("SHA-nn", "JP-nn", "AUTO-nn"):
1800
+ assert prose not in said, f"{prose} is prose, not a citation"
1801
+ assert "every id matches a configured prefix" in said
1802
+
1803
+ # An UNCONFIGURED install is the thing this check exists to catch, and it still
1804
+ # is: a ledger declares itself by DEFINING entries, and those are headings.
1805
+ (root / "architecture" / "camera.md").write_text(
1806
+ "---\ntype: system\n---\n\n# Camera\n\n"
1807
+ "### Q-1 — The camera never settles a shot alone\n\n"
1808
+ "### Q-2 — A frame is stamped once\n"
1809
+ )
1810
+ with contextlib.redirect_stdout(io.StringIO()) as out:
1811
+ assert safety.cmd_doctor({"project_root": tmp}) == 0
1812
+ said = out.getvalue()
1813
+ assert "Q-nn" in said, "an unconfigured ledger must still be reported"
1814
+
1815
+
1781
1816
  def test_align_does_not_read_the_harnesss_own_modules_or_suite():
1782
1817
  # AC-07, moved here from `config-seam`: the shipped suite's fixture ids must not
1783
1818
  # resolve against a CONSUMER's ledger. "The payload lives in the plugin cache so
@@ -1893,6 +1928,30 @@ def test_init_scaffolds_the_whole_tree_and_is_idempotent_and_non_destructive():
1893
1928
  assert (root / "design" / "README.md").is_file(), "init did not backfill"
1894
1929
 
1895
1930
 
1931
+ def test_init_writes_a_config_whose_schema_pointer_exists_and_never_replaces_one():
1932
+ with tempfile.TemporaryDirectory() as tmp:
1933
+ _initialised(tmp)
1934
+ cfg = Path(tmp) / ".claude" / "work.config.json"
1935
+ assert cfg.is_file(), "a repo adopting the harness had to hand-copy this file"
1936
+
1937
+ # It parses, and the only thing scaffolded is the pointer — everything else in
1938
+ # this file is a choice about the repo, and a guess is something to undo.
1939
+ body = json.loads(cfg.read_text())
1940
+ assert list(body) == ["$schema"]
1941
+ assert body["$schema"].endswith("/harness/schema/work.config.schema.json")
1942
+
1943
+ # The pointer names the package, not a path inside anybody's plugin or
1944
+ # node_modules — the failure this replaces was three repos naming a path that
1945
+ # had been deleted, which no editor reports.
1946
+ assert "@appchy/jarvis" in body["$schema"]
1947
+ assert "plugins" not in body["$schema"]
1948
+
1949
+ # A repo that has already configured itself keeps what it wrote.
1950
+ cfg.write_text('{"ids": {"prefix": "Z"}}')
1951
+ assert architecture.cmd_init({"project_root": tmp}) == 0
1952
+ assert json.loads(cfg.read_text()) == {"ids": {"prefix": "Z"}}
1953
+
1954
+
1896
1955
  def test_domain_new_pulls_a_shipped_charter_and_still_works_for_an_unknown_name():
1897
1956
  import os
1898
1957
  with tempfile.TemporaryDirectory() as tmp:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appchy/jarvis",
3
- "version": "0.1.66",
3
+ "version": "0.1.68",
4
4
  "description": "Jarvis — local AI coding assistant CLI",
5
5
  "private": false,
6
6
  "type": "module",