@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.
- package/bin/config-change.mjs +1 -1
- package/bin/jarvis.mjs +8 -12
- package/bin/pre-tool-use.mjs +2 -2
- package/bin/session-start.mjs +1 -1
- package/bin/stop.mjs +1 -1
- package/bin/user-prompt-submit.mjs +1 -1
- package/dist/bin.js +10104 -37174
- package/dist/bin.js.map +1 -1
- package/dist/data/backends.mjs +469 -0
- package/dist/data/chunk-7REP35VA.mjs +462 -0
- package/dist/data/chunk-AKQQC5IT.mjs +1224 -0
- package/dist/data/chunk-AYOJSS2F.mjs +72 -0
- package/dist/data/chunk-RRJ6KKYL.mjs +42 -0
- package/dist/data/chunk-YWSWQEJN.mjs +138 -0
- package/dist/data/embedders.mjs +80 -0
- package/dist/data/finders.mjs +35 -0
- package/dist/data/index.mjs +137 -0
- package/dist/data/linkers.mjs +83 -0
- package/dist/data/mcp.mjs +2624 -0
- package/dist/data/persistences.mjs +8 -0
- package/dist/data/rerankers.mjs +52 -0
- package/dist/data/stores.mjs +38 -0
- package/dist/hooks/config-change.js +7 -29
- package/dist/hooks/config-change.js.map +1 -1
- package/dist/hooks/pre-tool-use.js +129 -32
- package/dist/hooks/pre-tool-use.js.map +1 -1
- package/dist/hooks/session-start.js +72 -37
- package/dist/hooks/session-start.js.map +1 -1
- package/dist/hooks/stop.js +140 -32
- package/dist/hooks/stop.js.map +1 -1
- package/dist/hooks/user-prompt-submit.js +7 -29
- package/dist/hooks/user-prompt-submit.js.map +1 -1
- package/harness/assets/architecture-template.md +53 -0
- package/harness/assets/design-template.md +89 -0
- package/harness/assets/domains/architecture.md +43 -0
- package/harness/assets/domains/commercial.md +42 -0
- package/harness/assets/domains/design.md +42 -0
- package/harness/assets/domains/legal.md +43 -0
- package/harness/assets/domains/operations.md +39 -0
- package/harness/assets/domains/product.md +43 -0
- package/harness/assets/domains/quality.md +43 -0
- package/harness/assets/domains/security.md +44 -0
- package/harness/assets/domains/support.md +45 -0
- package/harness/assets/epic-template.md +85 -0
- package/harness/assets/feature-template.md +86 -0
- package/harness/assets/handoff-template.md +26 -0
- package/harness/assets/task-template.md +38 -0
- package/harness/assets/version-template.md +43 -0
- package/harness/harness/__init__.py +7 -0
- package/harness/harness/align.py +582 -0
- package/harness/harness/architecture.py +254 -0
- package/harness/harness/autonomy.py +374 -0
- package/harness/harness/branches.py +408 -0
- package/harness/harness/config.py +1482 -0
- package/harness/harness/coverage.py +199 -0
- package/harness/harness/epic.py +220 -0
- package/harness/harness/events.py +153 -0
- package/harness/harness/extend.py +99 -0
- package/harness/harness/frontmatter.py +218 -0
- package/harness/harness/gate.py +591 -0
- package/harness/harness/generate.py +267 -0
- package/harness/harness/git.py +775 -0
- package/harness/harness/ids.py +140 -0
- package/harness/harness/kickoff.py +231 -0
- package/harness/harness/lint.py +505 -0
- package/harness/harness/model.py +364 -0
- package/harness/harness/peers.py +187 -0
- package/harness/harness/product.py +29 -0
- package/harness/harness/registry.py +382 -0
- package/harness/harness/report.py +227 -0
- package/harness/harness/safety.py +387 -0
- package/harness/harness/scaffold.py +129 -0
- package/harness/harness/shard.py +63 -0
- package/harness/harness/shift.py +348 -0
- package/harness/harness/task.py +507 -0
- package/harness/harness/tree.py +258 -0
- package/harness/harness/version.py +305 -0
- package/harness/harness/wrap.py +217 -0
- package/harness/hooks/guard.py +259 -0
- package/harness/presets/appchy/PRESET.md +717 -0
- package/harness/presets/appchy/references/artifacts.md +539 -0
- package/harness/presets/appchy/references/graph.md +154 -0
- package/harness/presets/appchy/references/operations.md +444 -0
- package/harness/presets/appchy/references/research.md +216 -0
- package/harness/schema/work.config.schema.json +401 -0
- package/harness/test_work.py +5002 -0
- package/harness/work.py +534 -0
- package/package.json +34 -40
- package/bin/config-change.dev.mjs +0 -17
- package/bin/jarvis-dev.mjs +0 -30
- package/bin/pre-tool-use.dev.mjs +0 -23
- package/bin/session-start.dev.mjs +0 -17
- package/bin/stop.dev.mjs +0 -17
- package/bin/user-prompt-submit.dev.mjs +0 -17
- package/dev/bin.js +0 -38718
- package/dev/bin.js.map +0 -1
- package/dev/env.json +0 -1
- package/dev/hooks/config-change.js +0 -110
- package/dev/hooks/config-change.js.map +0 -1
- package/dev/hooks/pre-tool-use.js +0 -120
- package/dev/hooks/pre-tool-use.js.map +0 -1
- package/dev/hooks/session-start.js +0 -115
- package/dev/hooks/session-start.js.map +0 -1
- package/dev/hooks/stop.js +0 -112
- package/dev/hooks/stop.js.map +0 -1
- package/dev/hooks/user-prompt-submit.js +0 -111
- package/dev/hooks/user-prompt-submit.js.map +0 -1
- package/dist/env.json +0 -1
package/harness/work.py
ADDED
|
@@ -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.
|
|
4
|
-
"description": "Jarvis
|
|
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-
|
|
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
|
-
"
|
|
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
|
-
"@
|
|
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
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
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
|
-
"@
|
|
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
|
-
"
|
|
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);
|