@appchy/jarvis 0.1.84 → 0.1.86

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/harness/work.py CHANGED
@@ -79,7 +79,6 @@ Subcommands:
79
79
  wrap [--task <name>] finish a session cleanly: what a machine knows
80
80
  about this run, then what to do. Writes NOTHING
81
81
  coverage [--feature <name>] what a RUN proved vs what features promise
82
- migrate-owner [--force] rename `product:` -> `owner:` across tasks
83
82
 
84
83
  The unattended loop — a schedule calls `next`, and the rest exists so a run that
85
84
  cannot finish has somewhere to put the reason instead of stalling or guessing:
@@ -143,7 +142,7 @@ from harness import ids
143
142
  from harness.architecture import (cmd_domain_new, cmd_init, cmd_rules, cmd_system_new,
144
143
  cmd_where)
145
144
  from harness.coverage import cmd_coverage
146
- from harness.report import cmd_align, cmd_list, cmd_migrate_owner, cmd_readme
145
+ from harness.report import cmd_align, cmd_list, cmd_readme
147
146
  from harness.wrap import cmd_wrap
148
147
  from harness.config import (DEFAULTS, ConfigError, apply, cmd_applies, cmd_config,
149
148
  cmd_context, cmd_method, cmd_remind, resolve)
@@ -166,7 +165,7 @@ SUBCOMMANDS = (
166
165
  "epic-new", "feature-new", "version-new", "place", "handoff", "release", "archive",
167
166
  "find", "list", "readme", "move", "plan", "session", "kickoff", "path", "code",
168
167
  "domain-new", "system-new", "where", "rules", "align", "wrap", "coverage",
169
- "migrate", "migrate-owner", "next", "status", "drop", "ask", "answer", "needs", "verify",
168
+ "migrate", "next", "status", "drop", "ask", "answer", "needs", "verify",
170
169
  "observed", "log", "digest", "sync",
171
170
  )
172
171
 
@@ -192,27 +191,32 @@ def parse_argv(argv):
192
191
 
193
192
 
194
193
  def _project_root(flags) -> Path:
195
- """The repo the harness is acting on. `--project` wins, then the env, then the
196
- cwd the same precedence `find_work_root` uses, so config and tree can never
197
- disagree about which repo this is.
198
-
199
- The last step WALKS UP, and that is the half this claimed and did not do. The
200
- tree search walks up for a `work/`; this stopped at the cwd, so standing one
201
- directory into a repo found no config and every check ran in the shipped
202
- dialect against the right tree. Measured here: `align` from the root reported
203
- twenty citations, and from `apps/cli/` reported thirteen entirely different
204
- ones, having stopped recognising this repo's own ids altogether. One tree, two
205
- answers, decided by where the caller happened to be standing.
194
+ """The repo the harness is acting on: `--project`, else the repo the WORK TREE
195
+ was found in, else the env, else the cwd.
196
+
197
+ **It asks the tree search rather than re-reading the environment**, and that is
198
+ the whole of it. This used to consult the same two variables in the OPPOSITE
199
+ order `CLAUDE_PROJECT_DIR` first, `WORK_DIR` second while claiming in this
200
+ docstring to match the tree search. With both set at once, which they are under
201
+ Claude Code, config resolved from one repo while the tree being mutated was in
202
+ another: every id, every standard and every gate ran in a dialect the tree's
203
+ own repo never chose. One question about which repo this is, asked once.
204
+
205
+ The fallbacks are what keeps `init` and `doctor` working on a repo that has no
206
+ tree yet — there is nothing for the search to find, so the env and then the cwd
207
+ answer instead. And the cwd step WALKS UP: standing one directory into a repo
208
+ used to find no config at all, so `align` from the root reported twenty
209
+ citations and from `apps/cli/` thirteen entirely different ones.
206
210
  """
207
211
  import os
208
- p = flags.get("project") or os.environ.get("CLAUDE_PROJECT_DIR")
212
+ p = flags.get("project")
209
213
  if p:
210
214
  return Path(p).resolve()
211
- wd = os.environ.get("WORK_DIR")
212
- if wd:
213
- return Path(wd).resolve().parent
214
215
  root, _, cur = locate_work_root()
215
- return root.parent if root else cur
216
+ if root:
217
+ return root.parent
218
+ project = os.environ.get("CLAUDE_PROJECT_DIR")
219
+ return Path(project).resolve() if project else cur
216
220
 
217
221
 
218
222
  def main() -> int:
@@ -272,22 +276,23 @@ def main() -> int:
272
276
  cfg = copy.deepcopy(DEFAULTS)
273
277
  apply(cfg)
274
278
 
275
- # A command a hook fires before every edit must never reach the commit below.
276
- # It would put a network round trip on the hot path, and worse it would land
277
- # whatever half-written board edits the session happened to have open at that
278
- # moment, at a moment nobody chose. These read the tree and write nothing into it
279
- # but gitignored per-session markers, so there is nothing of their own to carry.
280
- if cmd in _HOOK_PATH or not git.enabled():
279
+ # THE COMMIT BELONGS TO A COMMAND THAT CHANGES THE BOARD, and to nothing else
280
+ # (founder, 2026-09-10). Every other command`list`, `status`, `where`,
281
+ # `context`, `align`, and the two a Claude Code hook fires — only looks, so it
282
+ # has nothing of its own to land. They used to drive the commit anyway, from a
283
+ # `finally:` that ran for all of them: what got committed was whatever the
284
+ # person happened to have open in `work/` at that moment, filed as
285
+ # "docs(work): board edits" under no item, at a moment nobody chose. The pull
286
+ # was already scoped this way; now both halves ask the same question.
287
+ if not _writes(cmd, flags) or not git.enabled():
281
288
  return dispatch(cmd, pos, flags, cfg)
282
289
 
283
- # Pull → write → commit → push. The commit is what makes the write unloseable,
284
- # so it runs in a `finally`: a command that mutated the tree and then exited
285
- # non-zero has still changed the board, and leaving that uncommitted is exactly
286
- # the hole this closes.
290
+ # Pull → write → commit → push. The commit runs in a `finally`: a command that
291
+ # mutated the tree and then exited non-zero has still changed the board, and
292
+ # leaving that uncommitted is exactly the hole this closes.
287
293
  root = locate_work_root()[0]
288
294
  repo = root.parent if root else _project_root(flags)
289
- if cmd in git.WRITES:
290
- _say(git.refresh(repo))
295
+ _say(git.refresh(repo))
291
296
  try:
292
297
  return dispatch(cmd, pos, flags, cfg)
293
298
  finally:
@@ -298,9 +303,8 @@ def main() -> int:
298
303
  # whole seam exists to close.
299
304
  rows = events.pending()
300
305
  # The item is what an event names, or what the command was given. A command
301
- # with neither `align` sweeping up somebody's hand edit to the tree — names
302
- # NO item, rather than putting a command name where four other slices expect
303
- # an item.
306
+ # with neither names NO item, rather than putting a command name where four
307
+ # other slices expect an item.
304
308
  item = rows[0]["name"] if rows else (pos[0] if pos else "")
305
309
  committed, pushed, note = git.land(repo, item, rows)
306
310
  if committed:
@@ -310,6 +314,25 @@ def main() -> int:
310
314
  _say(note)
311
315
 
312
316
 
317
+ def _writes(cmd: str, flags: dict) -> bool:
318
+ """Does THIS invocation change the board?
319
+
320
+ The command name answers it for all but one. `verify` writes the result of a run
321
+ it EXECUTES, and two of its four doors execute nothing: `--async` starts a
322
+ detached child that commits its own result, and `--status` only reports. Both
323
+ write to `work/.verify`, which is gitignored, so neither has anything of its own
324
+ to land — and both were driving a commit that swept up whatever the person had
325
+ open in `work/`, filed as "docs(work): board edits" under no item. Seen twice on
326
+ this repo's own board in one session, while polling for a gate to finish, which
327
+ is exactly the call a session repeats.
328
+ """
329
+ if cmd not in git.WRITES:
330
+ return False
331
+ if cmd == "verify" and (flags.get("async") or flags.get("status")):
332
+ return False
333
+ return True
334
+
335
+
313
336
  def _say(note: str) -> None:
314
337
  """A git note the caller has to see. stderr, because it is the difference
315
338
  between a write that is safe and one that is only safe on this machine — and
@@ -321,21 +344,6 @@ def _say(note: str) -> None:
321
344
  sys.stderr.flush()
322
345
 
323
346
 
324
- #: Commands a HOOK fires rather than a person, and which therefore run at a moment
325
- #: nobody picked. They are kept clear of the commit that every other command drives:
326
- #: a hook is not a decision to publish the board.
327
- #:
328
- #: NOT folded into `_READS`, though every name here is in it. That set is "commands
329
- #: that only look", and one of its members is `align`, which is deliberately expected
330
- #: to sweep up somebody's hand edit and commit it — the commit site says so in as many
331
- #: words. Reusing it would turn a read-set into a commit-policy and change that
332
- #: behaviour silently. `remind` has the same shape as this one and is deliberately
333
- #: left out: it fires once at the end of a turn rather than before every edit, and
334
- #: whether the Stop hook should be committing the board is a question somebody should
335
- #: answer on purpose rather than have settled as a side effect of this.
336
- _HOOK_PATH = {"applies"}
337
-
338
-
339
347
  #: Commands that only LOOK. `--branch` is meaningful on these and refused on every
340
348
  #: other, which is the safe default: a command added later is covered without anyone
341
349
  #: remembering to come back here.
@@ -506,8 +514,6 @@ def dispatch(cmd, pos, flags, cfg) -> int:
506
514
  return cmd_coverage(flags)
507
515
  if cmd == "migrate":
508
516
  return cmd_migrate(flags)
509
- if cmd == "migrate-owner":
510
- return cmd_migrate_owner(flags)
511
517
 
512
518
  # --- the unattended loop -------------------------------------------------
513
519
  # `next` is the entry point a schedule calls; everything else here exists so
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appchy/jarvis",
3
- "version": "0.1.84",
3
+ "version": "0.1.86",
4
4
  "description": "Jarvis — local AI coding assistant CLI",
5
5
  "private": false,
6
6
  "type": "module",
@@ -58,15 +58,15 @@
58
58
  "vitest": "^2.1.0",
59
59
  "@jarvis/agents": "1.0.0",
60
60
  "@jarvis/data": "0.1.0",
61
- "@jarvis/errors": "1.0.0",
62
- "@jarvis/anthropic": "1.0.0",
63
- "@jarvis/board": "0.1.0",
64
61
  "@jarvis/logger": "1.0.0",
65
62
  "@jarvis/rpc": "1.0.0",
63
+ "@jarvis/anthropic": "1.0.0",
66
64
  "@jarvis/typescript-config": "1.0.0",
67
65
  "@jarvis/types": "1.0.0",
66
+ "@jarvis/ui": "0.1.0",
68
67
  "@jarvis/vitest-config": "1.0.0",
69
- "@jarvis/ui": "0.1.0"
68
+ "@jarvis/errors": "1.0.0",
69
+ "@jarvis/board": "0.1.0"
70
70
  },
71
71
  "scripts": {
72
72
  "dev": "tsx watch src/bin.ts start --foreground",