@appchy/jarvis 0.1.36 → 0.1.39

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.
Files changed (108) hide show
  1. package/bin/config-change.mjs +1 -1
  2. package/bin/jarvis.mjs +8 -12
  3. package/bin/pre-tool-use.mjs +2 -2
  4. package/bin/session-start.mjs +1 -1
  5. package/bin/stop.mjs +1 -1
  6. package/bin/user-prompt-submit.mjs +1 -1
  7. package/dist/bin.js +10104 -37174
  8. package/dist/bin.js.map +1 -1
  9. package/dist/data/backends.mjs +469 -0
  10. package/dist/data/chunk-7REP35VA.mjs +462 -0
  11. package/dist/data/chunk-AKQQC5IT.mjs +1224 -0
  12. package/dist/data/chunk-AYOJSS2F.mjs +72 -0
  13. package/dist/data/chunk-RRJ6KKYL.mjs +42 -0
  14. package/dist/data/chunk-YWSWQEJN.mjs +138 -0
  15. package/dist/data/embedders.mjs +80 -0
  16. package/dist/data/finders.mjs +35 -0
  17. package/dist/data/index.mjs +137 -0
  18. package/dist/data/linkers.mjs +83 -0
  19. package/dist/data/mcp.mjs +2624 -0
  20. package/dist/data/persistences.mjs +8 -0
  21. package/dist/data/rerankers.mjs +52 -0
  22. package/dist/data/stores.mjs +38 -0
  23. package/dist/hooks/config-change.js +7 -29
  24. package/dist/hooks/config-change.js.map +1 -1
  25. package/dist/hooks/pre-tool-use.js +129 -32
  26. package/dist/hooks/pre-tool-use.js.map +1 -1
  27. package/dist/hooks/session-start.js +72 -37
  28. package/dist/hooks/session-start.js.map +1 -1
  29. package/dist/hooks/stop.js +140 -32
  30. package/dist/hooks/stop.js.map +1 -1
  31. package/dist/hooks/user-prompt-submit.js +7 -29
  32. package/dist/hooks/user-prompt-submit.js.map +1 -1
  33. package/harness/assets/architecture-template.md +53 -0
  34. package/harness/assets/design-template.md +89 -0
  35. package/harness/assets/domains/architecture.md +43 -0
  36. package/harness/assets/domains/commercial.md +42 -0
  37. package/harness/assets/domains/design.md +42 -0
  38. package/harness/assets/domains/legal.md +43 -0
  39. package/harness/assets/domains/operations.md +39 -0
  40. package/harness/assets/domains/product.md +43 -0
  41. package/harness/assets/domains/quality.md +43 -0
  42. package/harness/assets/domains/security.md +44 -0
  43. package/harness/assets/domains/support.md +45 -0
  44. package/harness/assets/epic-template.md +85 -0
  45. package/harness/assets/feature-template.md +86 -0
  46. package/harness/assets/handoff-template.md +26 -0
  47. package/harness/assets/task-template.md +38 -0
  48. package/harness/assets/version-template.md +43 -0
  49. package/harness/harness/__init__.py +7 -0
  50. package/harness/harness/align.py +582 -0
  51. package/harness/harness/architecture.py +254 -0
  52. package/harness/harness/autonomy.py +374 -0
  53. package/harness/harness/branches.py +408 -0
  54. package/harness/harness/config.py +1482 -0
  55. package/harness/harness/coverage.py +199 -0
  56. package/harness/harness/epic.py +220 -0
  57. package/harness/harness/events.py +153 -0
  58. package/harness/harness/extend.py +99 -0
  59. package/harness/harness/frontmatter.py +218 -0
  60. package/harness/harness/gate.py +591 -0
  61. package/harness/harness/generate.py +267 -0
  62. package/harness/harness/git.py +775 -0
  63. package/harness/harness/ids.py +140 -0
  64. package/harness/harness/kickoff.py +231 -0
  65. package/harness/harness/lint.py +505 -0
  66. package/harness/harness/model.py +364 -0
  67. package/harness/harness/peers.py +187 -0
  68. package/harness/harness/product.py +29 -0
  69. package/harness/harness/registry.py +382 -0
  70. package/harness/harness/report.py +227 -0
  71. package/harness/harness/safety.py +387 -0
  72. package/harness/harness/scaffold.py +129 -0
  73. package/harness/harness/shard.py +63 -0
  74. package/harness/harness/shift.py +348 -0
  75. package/harness/harness/task.py +507 -0
  76. package/harness/harness/tree.py +258 -0
  77. package/harness/harness/version.py +305 -0
  78. package/harness/harness/wrap.py +217 -0
  79. package/harness/hooks/guard.py +259 -0
  80. package/harness/presets/appchy/PRESET.md +717 -0
  81. package/harness/presets/appchy/references/artifacts.md +539 -0
  82. package/harness/presets/appchy/references/graph.md +154 -0
  83. package/harness/presets/appchy/references/operations.md +444 -0
  84. package/harness/presets/appchy/references/research.md +216 -0
  85. package/harness/schema/work.config.schema.json +401 -0
  86. package/harness/test_work.py +5002 -0
  87. package/harness/work.py +534 -0
  88. package/package.json +34 -40
  89. package/bin/config-change.dev.mjs +0 -17
  90. package/bin/jarvis-dev.mjs +0 -30
  91. package/bin/pre-tool-use.dev.mjs +0 -23
  92. package/bin/session-start.dev.mjs +0 -17
  93. package/bin/stop.dev.mjs +0 -17
  94. package/bin/user-prompt-submit.dev.mjs +0 -17
  95. package/dev/bin.js +0 -38718
  96. package/dev/bin.js.map +0 -1
  97. package/dev/env.json +0 -1
  98. package/dev/hooks/config-change.js +0 -110
  99. package/dev/hooks/config-change.js.map +0 -1
  100. package/dev/hooks/pre-tool-use.js +0 -120
  101. package/dev/hooks/pre-tool-use.js.map +0 -1
  102. package/dev/hooks/session-start.js +0 -115
  103. package/dev/hooks/session-start.js.map +0 -1
  104. package/dev/hooks/stop.js +0 -112
  105. package/dev/hooks/stop.js.map +0 -1
  106. package/dev/hooks/user-prompt-submit.js +0 -111
  107. package/dev/hooks/user-prompt-submit.js.map +0 -1
  108. package/dist/env.json +0 -1
@@ -0,0 +1,534 @@
1
+ """
2
+ jarvis work — deterministic mechanics for the work coding harness.
3
+
4
+ Work lives at <repo>/work/, in three tiers — version, epic, task — beside the
5
+ nine org-domains that hold every kind of durable call. There is NO central
6
+ ledger: a durable rule lives in the domain or system that owns it, and
7
+ `where <id>` resolves any id to its home.
8
+
9
+ work/
10
+ ├── ROADMAP.md the release sequence
11
+ ├── product/<feature>.md what the app does (B-nn · AC-nn)
12
+ ├── architecture/<system>.md how it is built (hosted rules)
13
+ ├── design/ quality/ security/ operations/ the remaining domains,
14
+ │ support/ commercial/ legal/ each hosting its own rules
15
+ ├── versions/<v>/version.md a RELEASE — permanent record
16
+ │ └── <epic>/epic.md the plan-it-together doc; removed at release
17
+ │ └── {queue,in-progress,complete}/<task>/
18
+ └── backlog/<epic>/<task>/ epics planned but not yet in a cut
19
+
20
+ A VERSION is a release: it states a user-visible `outcome:` and cannot open
21
+ while an earlier one is unreleased. An EPIC is temporary — the coherent goal
22
+ planned together, archived at release. A TASK is ONE goal, end-to-end,
23
+ internally phased, and always belongs to an epic.
24
+
25
+ A task's status is the bucket it sits in — never a frontmatter field; a backlog
26
+ task has none until pulled, which is why the backlog has no buckets. A version's
27
+ status is derived. Names are globally unique across all three tiers, so a name
28
+ alone resolves anywhere — `place` dispatches on what the name IS.
29
+
30
+ WHAT IS DERIVED, and therefore never written by hand: the `code:` region
31
+ vocabulary (the union of what the systems declare) · work/README.md's table ·
32
+ architecture/README.md and its dependency map · the rules constraining a
33
+ task (`task.code ∩ system.code`). If a fact can be computed it is not stored.
34
+
35
+ Subcommands:
36
+ version-new <v> --outcome "…" [--title ...] [--order N]
37
+ [--start|--end|--target DATE] [--design] [--architecture]
38
+ epic-new <name> [--version <v>] [--title ...] [--covers <feature>/AC-01,…]
39
+ [--owner <feature>|<domain>] domain = it moves no product criterion
40
+ [--continues <epic>] carries on an epic from an earlier cut
41
+ task-new <name> --epic <e> [--priority P0|P1|P2] [--tier 0-3] [--title ...]
42
+ [--depends a,b] [--owner <feature>|<domain>] [--code region,region]
43
+ [--covers AC-01,AC-02] [--start|--end DATE]
44
+ feature-new <feature> [--state idea|defined|building|shipped]
45
+ domain-new <name> system-new <name> --code region,region
46
+ place <name> --version <v> [--epic <e>] which CUT it lives in — from
47
+ <name> --backlog --epic <e> anywhere, to anywhere
48
+ an EPIC moves whole; a task needs --epic
49
+ move <name> <queue|in-progress|complete>
50
+ handoff <name> plan <name> [--file <path>]
51
+ session <name> [--id <uuid>] the current session, or one this one STARTED
52
+ kickoff <name> [--next "…"] [--watch "…"] [--prompt-only]
53
+ the opening prompt for the NEXT session, derived —
54
+ and how to start it where `session.mcp` names a server
55
+ release <v> archive <v> [--dry-run]
56
+ init [--project <dir>] scaffold work/ + the nine domains (idempotent)
57
+ doctor [--project <dir>] runtime, config, tree, ids, graph — self-diagnosis
58
+ id-new --host <name> --title "…" claim the next free rule id, atomically
59
+ config [--project <dir>] the merged config, as one JSON object
60
+ config set <key> <value> [--json] write one dotted key, validated first
61
+ config unset <key> drop an override, back to the default
62
+ context [--project <dir>] the SessionStart pointers, all config-derived
63
+ remind --used <tokens> [--session <id>] wrap up yet? one JSON object, or
64
+ nothing. Takes a MEASUREMENT: how full a
65
+ session is, is its client's to answer
66
+ applies --file <path> [--session <id>] what a session must be told now that
67
+ it is about to write this file — the judgements
68
+ no gate catches, and which system it is in
69
+ list · readme · path <name> · code <region>
70
+ where <id> id -> home, code:, enforced_by, citation count
71
+ find <name|branch|#pr> where work is being done — branch and PR, both ways
72
+ list --branch <ref> the board as another branch has it (read-only)
73
+ rules --task <name> what constrains a task, derived
74
+ align [--class <name>] every misalignment class; REPORT-ONLY, exit 0
75
+ wrap [--task <name>] finish a session cleanly: what a machine knows
76
+ about this run, then what to do. Writes NOTHING
77
+ coverage [--feature <name>] what a RUN proved vs what features promise
78
+ migrate-owner [--force] rename `product:` -> `owner:` across tasks
79
+
80
+ The unattended loop — a schedule calls `next`, and the rest exists so a run that
81
+ cannot finish has somewhere to put the reason instead of stalling or guessing:
82
+
83
+ next [--instance <id>] [--peek] take ONE task and claim it; prints the read order
84
+ status what shipped · what waits on you · what is at risk
85
+ method the METHOD in full — how work is done here. The session
86
+ block names it; this is the body, on request.
87
+ verify [--task <name>] [--async] RUN verify.* (shell=False) and record the result;
88
+ --async starts them and returns, then reports
89
+ progress and finally the result. One at a time
90
+ per checkout: two corrupt each other.
91
+ observed <task> --ac AC-01 --saw "…" an eyes-on, for what a test cannot prove
92
+ ask <task> --question "…" [--options "a | b"] [--owner who] [--durable]
93
+ park a question, move on — exits 0, never blocks
94
+ needs every open question, oldest first
95
+ answer <task> --choose "…" resolve it and put the task back to work
96
+ drop <name> [--why "…"] give a claim back
97
+ log [--since DATE] [--task <n>] · digest [--since DATE]
98
+ sync pull the board fresh; send anything unpushed
99
+
100
+ Where `git.commit` is set, a board write PULLS, WRITES, COMMITS and PUSHES, and the
101
+ commit's own trailers are the record — there is no work/.events.jsonl. Off by
102
+ default: the harness is shared, and a repo that never asked must not start
103
+ committing. `sync` is the read-only fresh pull, and the recovery for a push that
104
+ could not land at the time.
105
+
106
+ `move <name> complete` is a GATE, not a warning: it refuses unless the criteria are
107
+ checked, `verify` passed at the current commit, and every `covers:` criterion has
108
+ coverage or a recorded eyes-on. `--accept "<reason>" --owner <name>` overrides it,
109
+ as a recorded event with somebody's name on it — never silently.
110
+
111
+ Every mutating command regenerates work/README.md and architecture/README.md,
112
+ then prints lint warnings — there is no manual step.
113
+
114
+ `align` is deliberately non-blocking. With coverage partial a blocking gate
115
+ would be permanently red, and a lint that is always red is one nobody reads;
116
+ the flip to blocking is its own task, once coverage is real.
117
+
118
+ The session id comes from CLAUDE_CODE_SESSION_ID (a silent no-op outside Claude
119
+ Code). work/ is located by walking up from the cwd; WORK_DIR overrides.
120
+ """
121
+
122
+ import copy
123
+ import sys
124
+ from pathlib import Path
125
+
126
+ # The entry ORCHESTRATES; it does not implement (a file grows into a folder,
127
+ # and the entry keeps the domain name). Every mechanic lives in a `harness/` module
128
+ # named for its noun — `harness/task.py`, `harness/align.py` — so adding a
129
+ # subcommand means adding it there plus one line below, never growing this file.
130
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
131
+
132
+ from harness.tree import die, locate_work_root
133
+ from harness import events, git
134
+ from harness.task import cmd_code, cmd_handoff, cmd_move, cmd_new, cmd_path, cmd_place, cmd_plan, cmd_session
135
+ from harness.epic import cmd_epic_new
136
+ from harness.version import cmd_archive, cmd_release, cmd_version_new
137
+ from harness.product import cmd_feature_new
138
+ from harness import ids
139
+ from harness.architecture import (cmd_domain_new, cmd_init, cmd_rules, cmd_system_new,
140
+ cmd_where)
141
+ from harness.coverage import cmd_coverage
142
+ from harness.report import cmd_align, cmd_list, cmd_migrate_owner, cmd_readme
143
+ from harness.wrap import cmd_wrap
144
+ from harness.config import (DEFAULTS, ConfigError, apply, cmd_applies, cmd_config,
145
+ cmd_context, cmd_method, cmd_remind, resolve)
146
+ from harness.safety import cmd_doctor, cmd_id_new
147
+ from harness.autonomy import cmd_answer, cmd_ask, cmd_digest, cmd_log, cmd_needs
148
+ from harness.shift import cmd_drop, cmd_next, cmd_status
149
+ from harness.gate import cmd_observed, cmd_verify
150
+ from harness.kickoff import cmd_kickoff
151
+ from harness.git import cmd_sync
152
+ from harness.branches import cmd_find
153
+
154
+
155
+ #: Every subcommand the ladder in `main` dispatches, in the order it tries them.
156
+ #: Hand-written prose here drifted from the ladder and told callers that `new`
157
+ #: was expected while nothing dispatched it — so this is the ONE list, and
158
+ #: `test_work.py` fails when it stops matching what `main` actually handles.
159
+ SUBCOMMANDS = (
160
+ "config", "method", "context", "remind", "applies", "init", "doctor", "id-new", "task-new", "new",
161
+ "epic-new", "feature-new", "version-new", "place", "handoff", "release", "archive",
162
+ "find", "list", "readme", "move", "plan", "session", "kickoff", "path", "code",
163
+ "domain-new", "system-new", "where", "rules", "align", "wrap", "coverage",
164
+ "migrate-owner", "next", "status", "drop", "ask", "answer", "needs", "verify",
165
+ "observed", "log", "digest", "sync",
166
+ )
167
+
168
+
169
+ def parse_argv(argv):
170
+ """Tiny flag parser: positionals + `--flag value` pairs."""
171
+ pos, flags = [], {}
172
+ i = 0
173
+ while i < len(argv):
174
+ tok = argv[i]
175
+ if tok.startswith("--"):
176
+ key = tok[2:]
177
+ if i + 1 < len(argv) and not argv[i + 1].startswith("--"):
178
+ flags[key] = argv[i + 1]
179
+ i += 2
180
+ else:
181
+ flags[key] = "true"
182
+ i += 1
183
+ else:
184
+ pos.append(tok)
185
+ i += 1
186
+ return pos, flags
187
+
188
+
189
+ def _project_root(flags) -> Path:
190
+ """The repo the harness is acting on. `--project` wins, then the env, then the
191
+ cwd — the same precedence `find_work_root` uses, so config and tree can never
192
+ disagree about which repo this is."""
193
+ import os
194
+ p = flags.get("project") or os.environ.get("CLAUDE_PROJECT_DIR")
195
+ if p:
196
+ return Path(p).resolve()
197
+ wd = os.environ.get("WORK_DIR")
198
+ if wd:
199
+ return Path(wd).resolve().parent
200
+ return Path.cwd().resolve()
201
+
202
+
203
+ def main() -> int:
204
+ # Where the payload is, for the one caller that has to import from it rather
205
+ # than run it: the Stop hook reads config and runs a repo's own extensions, and
206
+ # doing either from a second implementation would be a second reader of the same
207
+ # file. It asks the harness where it lives instead of deriving it.
208
+ if len(sys.argv) > 1 and sys.argv[1] == "--payload":
209
+ print(Path(__file__).resolve().parent)
210
+ return 0
211
+
212
+ # `--help` prints the same thing no arguments do. The wrapper forwards every
213
+ # argument untouched — including this one, which it would otherwise be tempted to
214
+ # answer itself with a summary of a surface it does not own.
215
+ if len(sys.argv) < 2 or sys.argv[1] in ("--help", "-h", "help"):
216
+ print(__doc__)
217
+ return 0 if len(sys.argv) > 1 else 1
218
+ cmd, rest = sys.argv[1], sys.argv[2:]
219
+ pos, flags = parse_argv(rest)
220
+
221
+ # `config set|unset` runs BEFORE the resolve, and that is not an optimisation.
222
+ # It is the command you reach for when the config is wrong — and the resolve
223
+ # below is fatal on a config that is wrong, so ordering it first would make the
224
+ # one command that can repair an invalid config the one command an invalid
225
+ # config prevents. `doctor` and `init` are exempt for the same reason, and this
226
+ # was found the honest way: a half-written `session` block locked the writer out
227
+ # of all four repos that had it.
228
+ if cmd == "config" and pos and pos[0] in ("set", "unset"):
229
+ verb = pos[0]
230
+ if verb == "set" and len(pos) < 3:
231
+ die("usage: jarvis work config set <key> <value> [--json] "
232
+ "e.g. jarvis work config set ids.prefix ADR")
233
+ if verb == "unset" and len(pos) < 2:
234
+ die("usage: jarvis work config unset <key> "
235
+ "(back to the shipped default)")
236
+ return cmd_config({}, {"_verb": verb, "key": pos[1],
237
+ "value": pos[2] if len(pos) > 2 else None,
238
+ "project_root": _project_root(flags), **flags})
239
+
240
+ # Resolve config BEFORE any command runs. `ids` must be reconfigured before
241
+ # anything compiles a pattern from it, and a command that parsed first would
242
+ # read the whole repo in the shipped dialect rather than the repo's own.
243
+ # A broken config is fatal, never a warning: a half-applied config makes every
244
+ # downstream check report against a model the repo did not choose.
245
+ try:
246
+ cfg = resolve(_project_root(flags))
247
+ except ConfigError as e:
248
+ # The two commands you reach for WHEN the config is broken cannot be the two
249
+ # a broken config prevents. The comment above has said they are exempt since
250
+ # this ordering was written; the code only ever exempted `config set|unset`,
251
+ # so `doctor`'s own "config is invalid" branch was unreachable and the one
252
+ # command whose job is diagnosing a broken install died on the install it
253
+ # was there to diagnose. They run on the shipped defaults and report the
254
+ # real config themselves.
255
+ if cmd not in ("doctor", "init"):
256
+ die(f"{e}")
257
+ cfg = copy.deepcopy(DEFAULTS)
258
+ apply(cfg)
259
+
260
+ # A command a hook fires before every edit must never reach the commit below.
261
+ # It would put a network round trip on the hot path, and — worse — it would land
262
+ # whatever half-written board edits the session happened to have open at that
263
+ # moment, at a moment nobody chose. These read the tree and write nothing into it
264
+ # but gitignored per-session markers, so there is nothing of their own to carry.
265
+ if cmd in _HOOK_PATH or not git.enabled():
266
+ return dispatch(cmd, pos, flags, cfg)
267
+
268
+ # Pull → write → commit → push. The commit is what makes the write unloseable,
269
+ # so it runs in a `finally`: a command that mutated the tree and then exited
270
+ # non-zero has still changed the board, and leaving that uncommitted is exactly
271
+ # the hole this closes.
272
+ root = locate_work_root()[0]
273
+ repo = root.parent if root else _project_root(flags)
274
+ if cmd in git.WRITES:
275
+ _say(git.refresh(repo))
276
+ try:
277
+ return dispatch(cmd, pos, flags, cfg)
278
+ finally:
279
+ # Driven by what CHANGED, never by whether an event was recorded. Not every
280
+ # command that writes the tree files an event — `version-new` scaffolds a
281
+ # version.md and records nothing — and a commit that waited for an event
282
+ # would leave exactly those writes untracked, which is the failure this
283
+ # whole seam exists to close.
284
+ rows = events.pending()
285
+ # The item is what an event names, or what the command was given. A command
286
+ # with neither — `align` sweeping up somebody's hand edit to the tree — names
287
+ # NO item, rather than putting a command name where four other slices expect
288
+ # an item.
289
+ item = rows[0]["name"] if rows else (pos[0] if pos else "")
290
+ committed, pushed, note = git.land(repo, item, rows)
291
+ if committed:
292
+ where = f" and pushed to {git.GIT['remote']}" if pushed else ""
293
+ sys.stdout.flush()
294
+ print(f"git committed{where}")
295
+ _say(note)
296
+
297
+
298
+ def _say(note: str) -> None:
299
+ """A git note the caller has to see. stderr, because it is the difference
300
+ between a write that is safe and one that is only safe on this machine — and
301
+ after a flush, so it does not overtake the command's own buffered output and
302
+ read as though it happened first."""
303
+ if note:
304
+ sys.stdout.flush()
305
+ print(f"git {note}", file=sys.stderr)
306
+ sys.stderr.flush()
307
+
308
+
309
+ #: Commands a HOOK fires rather than a person, and which therefore run at a moment
310
+ #: nobody picked. They are kept clear of the commit that every other command drives:
311
+ #: a hook is not a decision to publish the board.
312
+ #:
313
+ #: NOT folded into `_READS`, though every name here is in it. That set is "commands
314
+ #: that only look", and one of its members is `align`, which is deliberately expected
315
+ #: to sweep up somebody's hand edit and commit it — the commit site says so in as many
316
+ #: words. Reusing it would turn a read-set into a commit-policy and change that
317
+ #: behaviour silently. `remind` has the same shape as this one and is deliberately
318
+ #: left out: it fires once at the end of a turn rather than before every edit, and
319
+ #: whether the Stop hook should be committing the board is a question somebody should
320
+ #: answer on purpose rather than have settled as a side effect of this.
321
+ _HOOK_PATH = {"applies"}
322
+
323
+
324
+ #: Commands that only LOOK. `--branch` is meaningful on these and refused on every
325
+ #: other, which is the safe default: a command added later is covered without anyone
326
+ #: remembering to come back here.
327
+ _READS = {"list", "find", "path", "code", "where", "rules", "status", "digest",
328
+ "log", "needs", "align", "coverage", "readme", "config", "context",
329
+ "doctor", "kickoff", "remind", "applies", "wrap"}
330
+
331
+
332
+ def _reads_only(cmd: str, flags: dict) -> None:
333
+ """`--branch` reads; it never writes.
334
+
335
+ Reading any branch and writing your own is the whole arrangement: a branch's
336
+ board describes that branch's world and merges when the branch does. Accepting
337
+ `--branch` on a write would mean committing to a ref this clone does not have
338
+ checked out — which git can be made to do, and which nobody would then see in
339
+ their own tree. Refused with the reason, because whoever hit this is one step
340
+ from what they actually wanted.
341
+ """
342
+ if flags.get("branch") and cmd not in _READS:
343
+ die(f"`{cmd}` changes the board, and a change lands on the branch you have "
344
+ f"checked out — never on another one. Check out {flags['branch']} and "
345
+ f"run it there. To LOOK at that branch from here: "
346
+ f"`jarvis work list --branch {flags['branch']}`, or `jarvis work find`.")
347
+
348
+
349
+ def dispatch(cmd, pos, flags, cfg) -> int:
350
+ _reads_only(cmd, flags)
351
+ if cmd == "config":
352
+ # Reads only — `config set|unset` is handled in `main`, ahead of the
353
+ # resolve, because it has to work in a repo the resolve would refuse.
354
+ return cmd_config(cfg, flags)
355
+ if cmd == "method":
356
+ # The project root for the same reason `context` takes it: instructions resolve
357
+ # against the repo, and the method has to answer in a repo with a config and no
358
+ # board yet.
359
+ return cmd_method(cfg, flags, _project_root(flags))
360
+ if cmd == "context":
361
+ # The project root, not the work tree: instructions resolve against the repo
362
+ # (`.claude/presets/…`), and `context` is the one command that must answer
363
+ # before a work tree necessarily exists.
364
+ return cmd_context(cfg, flags, _project_root(flags))
365
+ if cmd == "remind":
366
+ # Same reason as `context`: the repo root, and it must answer before a work
367
+ # tree necessarily exists — a session in a repo that has one config file and
368
+ # no board still gets told when it is running out of room.
369
+ return cmd_remind(cfg, flags, _project_root(flags))
370
+ if cmd == "applies":
371
+ # The repo root, like `context` and `remind`: this resolves the method against
372
+ # the repo and the systems against its work tree, and must answer in a repo
373
+ # that has one and not the other.
374
+ return cmd_applies(cfg, flags, _project_root(flags))
375
+ # `init` runs BEFORE a work tree exists, so it takes the project root directly
376
+ # rather than walking up for a `work/` that is the thing it is about to create.
377
+ if cmd == "init":
378
+ return cmd_init({"project_root": _project_root(flags), **flags})
379
+ # `doctor` must run in a BROKEN repo — that is the whole point — so it takes
380
+ # the project root and never asks `find_work_root` for a tree that may be the
381
+ # thing that is wrong.
382
+ if cmd == "doctor":
383
+ return cmd_doctor({"project_root": _project_root(flags), **flags})
384
+ if cmd == "id-new":
385
+ if not flags.get("host"):
386
+ die("usage: jarvis work id-new --host <domain|system> --title \"…\"")
387
+ return cmd_id_new(flags)
388
+
389
+ # `new` and `task-new` are one verb. The error message below has advertised `new`
390
+ # since the ladder was written and nothing dispatched it, so a caller was told the
391
+ # spelling it had just used was the expected one — this repo's own e2e suite used
392
+ # it and every test in that story had been dead on the fixture ever since.
393
+ if cmd in ("task-new", "new"):
394
+ if not pos:
395
+ die("usage: jarvis work task-new <name> --epic <e> "
396
+ "[--priority P0|P1|P2] [--title ...] [--depends a,b] [--tags a,b] "
397
+ "[--owner <feature>|<domain>] [--code region,region] "
398
+ "[--covers AC-01,AC-02] [--start YYYY-MM-DD] [--end YYYY-MM-DD]")
399
+ return cmd_new({"name": pos[0], **flags})
400
+ if cmd == "epic-new":
401
+ if not pos:
402
+ die("usage: jarvis work epic-new <name> [--version <v>] [--title ...] "
403
+ "[--covers <feature>/AC-01,<feature>/AC-02] "
404
+ "[--owner <feature>|<domain>] [--continues <epic>] "
405
+ "(no --version puts it in the backlog, where cuts are planned)")
406
+ return cmd_epic_new({"name": pos[0], **flags})
407
+ if cmd == "feature-new":
408
+ if not pos:
409
+ die("usage: jarvis work feature-new <name> "
410
+ "[--state idea|defined|building|shipped]")
411
+ return cmd_feature_new({"name": pos[0], **flags})
412
+ if cmd == "version-new":
413
+ if not pos:
414
+ die("usage: jarvis work version-new <name> --outcome \"what a user can "
415
+ "newly do\" [--title ...] [--order N] [--start YYYY-MM-DD] "
416
+ "[--end YYYY-MM-DD] [--target YYYY-MM-DD] [--design] "
417
+ "[--architecture]")
418
+ return cmd_version_new({"name": pos[0], **flags})
419
+ if cmd == "place":
420
+ if not pos:
421
+ die("usage: jarvis work place <name> --version <v> [--epic <e>] "
422
+ "(or --backlog --epic <e> to take it back out of a cut; "
423
+ "an epic moves whole, a single task needs --epic)")
424
+ return cmd_place({"name": pos[0], **flags})
425
+ if cmd == "handoff":
426
+ if not pos:
427
+ die("usage: jarvis work handoff <name>")
428
+ return cmd_handoff({"name": pos[0]})
429
+ if cmd == "release":
430
+ if not pos:
431
+ die("usage: jarvis work release <v>")
432
+ return cmd_release({"name": pos[0]})
433
+ if cmd == "archive":
434
+ if not pos:
435
+ die("usage: jarvis work archive <v> [--dry-run]")
436
+ return cmd_archive({"name": pos[0], **flags})
437
+ if cmd == "find":
438
+ if not pos:
439
+ die("usage: jarvis work find <name|branch|#pr> [--refresh]")
440
+ return cmd_find({"target": pos[0], **flags})
441
+ if cmd == "list":
442
+ return cmd_list(flags)
443
+ if cmd == "readme":
444
+ return cmd_readme(flags)
445
+ if cmd == "move":
446
+ if len(pos) < 2:
447
+ die("usage: jarvis work move <name> <queue|in-progress|complete> "
448
+ "(to block, use `ask` — a blocked task needs a question)")
449
+ return cmd_move({"name": pos[0], "status": pos[1], **flags})
450
+ if cmd == "plan":
451
+ if not pos:
452
+ die("usage: jarvis work plan <name> [--file <path>]")
453
+ return cmd_plan({"name": pos[0], **flags})
454
+ if cmd == "session":
455
+ if not pos:
456
+ die("usage: jarvis work session <name> [--id <uuid>] "
457
+ "(no --id records THIS session; --id records one it started)")
458
+ return cmd_session({"name": pos[0], **flags})
459
+ if cmd == "kickoff":
460
+ if not pos:
461
+ die("usage: jarvis work kickoff <name> [--next \"…\"] [--watch \"…\"] "
462
+ "[--prompt-only]")
463
+ return cmd_kickoff({"name": pos[0], **flags})
464
+ if cmd == "path":
465
+ if not pos:
466
+ die("usage: jarvis work path <name> (a task, an epic or a version)")
467
+ return cmd_path({"name": pos[0]})
468
+ if cmd == "code":
469
+ if not pos:
470
+ die("usage: jarvis work code <region>")
471
+ return cmd_code({"region": pos[0]})
472
+ if cmd == "domain-new":
473
+ if not pos:
474
+ die("usage: jarvis work domain-new <name> [--title ...]")
475
+ return cmd_domain_new({"name": pos[0], **flags})
476
+ if cmd == "system-new":
477
+ if not pos:
478
+ die("usage: jarvis work system-new <name> --code region,region [--title ...]")
479
+ return cmd_system_new({"name": pos[0], **flags})
480
+ if cmd == "where":
481
+ if not pos:
482
+ die(f"usage: jarvis work where <{ids.LEDGER}-nn>")
483
+ return cmd_where({"id": pos[0]})
484
+ if cmd == "rules":
485
+ return cmd_rules({**flags, "task": flags.get("task") or (pos[0] if pos else None)})
486
+ if cmd == "align":
487
+ return cmd_align(flags)
488
+ if cmd == "wrap":
489
+ return cmd_wrap(cfg, flags, _project_root(flags))
490
+ if cmd == "coverage":
491
+ return cmd_coverage(flags)
492
+ if cmd == "migrate-owner":
493
+ return cmd_migrate_owner(flags)
494
+
495
+ # --- the unattended loop -------------------------------------------------
496
+ # `next` is the entry point a schedule calls; everything else here exists so
497
+ # that a run which cannot finish has somewhere to put the reason.
498
+ if cmd == "next":
499
+ return cmd_next(flags)
500
+ if cmd == "status":
501
+ return cmd_status(flags)
502
+ if cmd == "drop":
503
+ if not pos:
504
+ die("usage: jarvis work drop <name> [--why \"…\"]")
505
+ return cmd_drop({"name": pos[0], **flags})
506
+ if cmd == "ask":
507
+ if not pos:
508
+ die("usage: jarvis work ask <task> --question \"…\" "
509
+ "[--options \"a | b\"] [--owner founder] [--durable]")
510
+ return cmd_ask({"name": pos[0], **flags})
511
+ if cmd == "answer":
512
+ if not pos:
513
+ die("usage: jarvis work answer <task> --choose \"…\" [--to queue]")
514
+ return cmd_answer({"name": pos[0], **flags})
515
+ if cmd == "needs":
516
+ return cmd_needs(flags)
517
+ if cmd == "verify":
518
+ return cmd_verify(flags)
519
+ if cmd == "observed":
520
+ if not pos:
521
+ die("usage: jarvis work observed <task> --ac AC-01 --saw \"…\"")
522
+ return cmd_observed({"name": pos[0], **flags})
523
+ if cmd == "log":
524
+ return cmd_log(flags)
525
+ if cmd == "digest":
526
+ return cmd_digest(flags)
527
+ if cmd == "sync":
528
+ return cmd_sync(flags)
529
+
530
+ die(f"unknown subcommand '{cmd}' (expected: {', '.join(SUBCOMMANDS)})")
531
+
532
+
533
+ if __name__ == "__main__":
534
+ sys.exit(main())
package/package.json CHANGED
@@ -1,24 +1,18 @@
1
1
  {
2
2
  "name": "@appchy/jarvis",
3
- "version": "0.1.36",
4
- "description": "Jarvis local AI coding assistant CLI",
3
+ "version": "0.1.39",
4
+ "description": "Jarvis \u2014 local AI coding assistant CLI",
5
5
  "private": false,
6
6
  "type": "module",
7
7
  "main": "./dist/bin.js",
8
8
  "types": "./src/index.ts",
9
9
  "bin": {
10
10
  "jarvis": "./bin/jarvis.mjs",
11
- "jarvis-dev": "./bin/jarvis-dev.mjs",
12
11
  "jarvis-pre-tool-use": "./bin/pre-tool-use.mjs",
13
- "jarvis-pre-tool-use-dev": "./bin/pre-tool-use.dev.mjs",
14
12
  "jarvis-session-start": "./bin/session-start.mjs",
15
- "jarvis-session-start-dev": "./bin/session-start.dev.mjs",
16
13
  "jarvis-stop": "./bin/stop.mjs",
17
- "jarvis-stop-dev": "./bin/stop.dev.mjs",
18
14
  "jarvis-user-prompt-submit": "./bin/user-prompt-submit.mjs",
19
- "jarvis-user-prompt-submit-dev": "./bin/user-prompt-submit.dev.mjs",
20
- "jarvis-config-change": "./bin/config-change.mjs",
21
- "jarvis-config-change-dev": "./bin/config-change.dev.mjs"
15
+ "jarvis-config-change": "./bin/config-change.mjs"
22
16
  },
23
17
  "exports": {
24
18
  ".": "./src/index.ts"
@@ -26,8 +20,19 @@
26
20
  "files": [
27
21
  "bin/",
28
22
  "dist/",
29
- "dev/"
23
+ "harness/",
24
+ "!harness/**/__pycache__",
25
+ "!harness/**/*.pyc"
30
26
  ],
27
+ "scripts": {
28
+ "dev": "tsx watch src/bin.ts start --foreground",
29
+ "build": "tsup",
30
+ "build:tsc": "tsc",
31
+ "start": "node dist/bin.js start",
32
+ "type-check": "tsc --noEmit",
33
+ "prepublishOnly": "pnpm build",
34
+ "test:run": "vitest run"
35
+ },
31
36
  "publishConfig": {
32
37
  "access": "public"
33
38
  },
@@ -35,43 +40,32 @@
35
40
  "node": ">=20.0.0"
36
41
  },
37
42
  "dependencies": {
38
- "@anthropic-ai/claude-agent-sdk": "0.2.101",
39
- "@anthropic-ai/sdk": "^0.39.0",
40
- "chalk": "^5.4.0",
43
+ "@modelcontextprotocol/sdk": "^1.29.0",
41
44
  "chokidar": "^4.0.0",
42
- "cli-highlight": "^2.1.0",
43
45
  "commander": "^13.0.0",
44
46
  "dotenv": "^16.4.0",
45
- "ink": "^5.2.0",
46
- "ink-select-input": "^6.0.0",
47
- "ink-spinner": "^5.0.0",
48
- "ink-text-input": "^6.0.0",
49
- "marked": "^15.0.0",
50
- "react": "^18.3.0",
47
+ "fast-glob": "^3.3.2",
48
+ "jiti": "^2.4.2",
49
+ "node-pty": "^1.1.0",
50
+ "picomatch": "^4.0.2",
51
51
  "tsx": "^4.0.0",
52
- "ws": "^8.18.0"
52
+ "ws": "^8.18.0",
53
+ "yaml": "^2.6.0",
54
+ "zod": "^3.25.76"
53
55
  },
54
56
  "devDependencies": {
55
- "@types/react": "^18.3.0",
57
+ "@jarvis/agents": "workspace:*",
58
+ "@jarvis/anthropic": "workspace:*",
59
+ "@jarvis/board": "workspace:*",
60
+ "@jarvis/data": "workspace:*",
61
+ "@jarvis/logger": "workspace:*",
62
+ "@jarvis/rpc": "workspace:*",
63
+ "@jarvis/types": "workspace:*",
64
+ "@jarvis/typescript-config": "workspace:*",
65
+ "@jarvis/vitest-config": "workspace:*",
56
66
  "@types/ws": "^8.5.0",
57
67
  "tsup": "^8.5.1",
58
68
  "typescript": "^5.7.0",
59
- "@jarvis/agent": "0.0.0",
60
- "@jarvis/agents": "1.0.0",
61
- "@jarvis/anthropic": "1.0.0",
62
- "@jarvis/logger": "1.0.0",
63
- "@jarvis/sdk": "1.0.0",
64
- "@jarvis/tasks": "1.0.0",
65
- "@jarvis/types": "1.0.0",
66
- "@jarvis/transport": "1.0.0",
67
- "@jarvis/typescript-config": "1.0.0",
68
- "@jarvis/mcp": "0.1.0"
69
- },
70
- "scripts": {
71
- "dev": "tsx watch src/bin.ts -- start",
72
- "build": "tsup",
73
- "build:tsc": "tsc",
74
- "start": "node dist/bin.js start",
75
- "type-check": "tsc --noEmit"
69
+ "vitest": "^2.1.0"
76
70
  }
77
- }
71
+ }
@@ -1,17 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Jarvis ConfigChange hook — Dev Launcher.
5
- * See bin/pre-tool-use.dev.mjs for the pattern.
6
- */
7
-
8
- import { fileURLToPath } from "url";
9
- import { dirname, join } from "path";
10
- import { existsSync } from "fs";
11
-
12
- const __dirname = dirname(fileURLToPath(import.meta.url));
13
- const bundle = join(__dirname, "..", "dev", "hooks", "config-change.js");
14
-
15
- if (!existsSync(bundle)) process.exit(0);
16
-
17
- await import(bundle);