@appchy/jarvis 0.1.129 → 0.1.131
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 +5 -2
- package/dist/bin.js.map +1 -1
- package/harness/harness/autonomy.py +3 -6
- package/harness/harness/config.py +0 -8
- package/harness/harness/events.py +26 -27
- package/harness/harness/gate.py +2 -2
- package/harness/harness/generate.py +4 -0
- package/harness/harness/git.py +31 -20
- package/harness/harness/holders.cases.json +173 -0
- package/harness/harness/holders.py +72 -0
- package/harness/harness/lint.py +47 -0
- package/harness/harness/migrate.py +31 -0
- package/harness/harness/model.py +26 -0
- package/harness/harness/peers.cases.json +36 -25
- package/harness/harness/peers.py +26 -18
- package/harness/harness/report.py +6 -0
- package/harness/harness/shift.py +94 -132
- package/harness/harness/task.py +5 -11
- package/harness/harness/tree.py +17 -0
- package/harness/harness/version.py +4 -6
- package/harness/presets/appchy/PRESET.md +13 -15
- package/harness/presets/appchy/references/operations.md +1 -1
- package/harness/schema/work.config.schema.json +0 -6
- package/harness/test_work.py +228 -107
- package/harness/work.py +3 -8
- package/package.json +3 -3
package/harness/harness/model.py
CHANGED
|
@@ -6,6 +6,7 @@ from pathlib import Path
|
|
|
6
6
|
from .tree import (archive_dir, backlog_dir, BUCKETS, cli, DONE_TIER,
|
|
7
7
|
PRIORITIES, RESERVED_MD)
|
|
8
8
|
from .frontmatter import as_list, parse_frontmatter, read_item, rewrite_file, title_of
|
|
9
|
+
from . import tree as _tree
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
def current_session_id() -> str:
|
|
@@ -222,6 +223,10 @@ class Version:
|
|
|
222
223
|
self.end = (str(self.fm.get("end", "")).strip() or None)
|
|
223
224
|
self.target = str(self.fm.get("target", "")).strip()
|
|
224
225
|
self.released = str(self.fm.get("released", "")).strip()
|
|
226
|
+
# A cut deliberately sized past its ceiling says so here and stops being
|
|
227
|
+
# flagged — the same written escape an epic and a task already carry. A
|
|
228
|
+
# planned size is legal; an accreted one is what the flag is for.
|
|
229
|
+
self.one_goal = str(self.fm.get("one_goal", "")).strip()
|
|
225
230
|
# Loose tasks — the pre-epic shape, still read so grandfathered versions
|
|
226
231
|
# keep working untouched (v20/v24 hold live work at the reshape).
|
|
227
232
|
self.tasks: dict = {b: [] for b in BUCKETS}
|
|
@@ -276,6 +281,27 @@ class Version:
|
|
|
276
281
|
return bool(tasks) and not self.released and all(
|
|
277
282
|
t.status == "complete" for t in tasks)
|
|
278
283
|
|
|
284
|
+
def open_tasks(self) -> list:
|
|
285
|
+
"""Every task in this cut still owed — queued, in progress or blocked."""
|
|
286
|
+
return [t for t in self.all_tasks() if t.status in _tree.ACTIVE]
|
|
287
|
+
|
|
288
|
+
def over_ceiling(self) -> int:
|
|
289
|
+
"""How many open tasks past its ceiling this cut is, or 0.
|
|
290
|
+
|
|
291
|
+
A FLAG, never a status and never a gate. Whether a cut is full says nothing
|
|
292
|
+
about whether it is planned, current or ready, so it sits beside those rather
|
|
293
|
+
than replacing one, and nothing refuses or re-routes work because of it. It
|
|
294
|
+
is what a person reads to know a reshape is owed — move work to a later cut,
|
|
295
|
+
or reshape an epic — so that the cut which closes is not a half-built one.
|
|
296
|
+
|
|
297
|
+
Zero for a released cut, which owes nothing, and zero for one that has written
|
|
298
|
+
why its size is deliberate: the difference between planning a big cut and
|
|
299
|
+
accreting one is recorded, never guessed.
|
|
300
|
+
"""
|
|
301
|
+
if self.released or self.one_goal:
|
|
302
|
+
return 0
|
|
303
|
+
return max(0, len(self.open_tasks()) - _tree.CUT_TASK_CEILING)
|
|
304
|
+
|
|
279
305
|
def status(self) -> str:
|
|
280
306
|
"""`released` if version.md carries a released date; `ready` if every task
|
|
281
307
|
is complete and nobody has closed it; `current` if any task is in-progress;
|
|
@@ -11,15 +11,18 @@
|
|
|
11
11
|
"not say, and that is where every agent publishing nothing sits.",
|
|
12
12
|
"",
|
|
13
13
|
"`pid` is a marker, never a number: `self` is the running test's own process, which is the",
|
|
14
|
-
"only pid a test can be sure is alive, and `dead` is one nothing can be occupying."
|
|
14
|
+
"only pid a test can be sure is alive, and `dead` is one nothing can be occupying.",
|
|
15
|
+
"",
|
|
16
|
+
"Ids are shaped like the session ids the registry publishes, because only that shape can be",
|
|
17
|
+
"declared ended by its absence — the last case is the one that is not."
|
|
15
18
|
],
|
|
16
19
|
"cases": [
|
|
17
20
|
{
|
|
18
21
|
"name": "a run with one live process is reachable by the name it published",
|
|
19
22
|
"sessions": [
|
|
20
|
-
{ "file": "1.json", "sessionId": "
|
|
23
|
+
{ "file": "1.json", "sessionId": "22222222-2222-4222-8222-222222222222", "name": "repo-a8", "pid": "self", "entrypoint": "cli" }
|
|
21
24
|
],
|
|
22
|
-
"ask": "
|
|
25
|
+
"ask": "22222222-2222-4222-8222-222222222222",
|
|
23
26
|
"expect": {
|
|
24
27
|
"live": "running",
|
|
25
28
|
"processes": 1,
|
|
@@ -29,24 +32,24 @@
|
|
|
29
32
|
{
|
|
30
33
|
"name": "a run whose process is gone is ended, not merely unlisted",
|
|
31
34
|
"sessions": [
|
|
32
|
-
{ "file": "1.json", "sessionId": "
|
|
33
|
-
{ "file": "2.json", "sessionId": "
|
|
35
|
+
{ "file": "1.json", "sessionId": "22222222-2222-4222-8222-222222222222", "name": "repo-a8", "pid": "self", "entrypoint": "cli" },
|
|
36
|
+
{ "file": "2.json", "sessionId": "33333333-3333-4333-8333-333333333333", "name": "repo-b7", "pid": "dead", "entrypoint": "cli" }
|
|
34
37
|
],
|
|
35
|
-
"ask": "
|
|
38
|
+
"ask": "33333333-3333-4333-8333-333333333333",
|
|
36
39
|
"expect": { "live": "ended", "processes": 0, "addresses": [] }
|
|
37
40
|
},
|
|
38
41
|
{
|
|
39
42
|
"name": "a run nothing published is ended, because something published a list without it",
|
|
40
43
|
"sessions": [
|
|
41
|
-
{ "file": "1.json", "sessionId": "
|
|
44
|
+
{ "file": "1.json", "sessionId": "22222222-2222-4222-8222-222222222222", "name": "repo-a8", "pid": "self", "entrypoint": "cli" }
|
|
42
45
|
],
|
|
43
|
-
"ask": "
|
|
46
|
+
"ask": "12121212-1212-4121-8121-121212121212",
|
|
44
47
|
"expect": { "live": "ended", "processes": 0, "addresses": [] }
|
|
45
48
|
},
|
|
46
49
|
{
|
|
47
50
|
"name": "a take that named no run at all concludes nothing, because there is nothing to look up",
|
|
48
51
|
"sessions": [
|
|
49
|
-
{ "file": "1.json", "sessionId": "
|
|
52
|
+
{ "file": "1.json", "sessionId": "22222222-2222-4222-8222-222222222222", "name": "repo-a8", "pid": "self", "entrypoint": "cli" }
|
|
50
53
|
],
|
|
51
54
|
"ask": "",
|
|
52
55
|
"expect": { "live": "unknown", "processes": 0, "addresses": [] }
|
|
@@ -54,22 +57,22 @@
|
|
|
54
57
|
{
|
|
55
58
|
"name": "a client that publishes no list makes every run unknown, never ended",
|
|
56
59
|
"sessions": null,
|
|
57
|
-
"ask": "
|
|
60
|
+
"ask": "22222222-2222-4222-8222-222222222222",
|
|
58
61
|
"expect": { "live": "unknown", "processes": 0, "addresses": [] }
|
|
59
62
|
},
|
|
60
63
|
{
|
|
61
64
|
"name": "an empty list is a claim, so a run not on it has ended",
|
|
62
65
|
"sessions": [],
|
|
63
|
-
"ask": "
|
|
66
|
+
"ask": "22222222-2222-4222-8222-222222222222",
|
|
64
67
|
"expect": { "live": "ended", "processes": 0, "addresses": [] }
|
|
65
68
|
},
|
|
66
69
|
{
|
|
67
70
|
"name": "two live processes on one id are a fork, named both ways and neither picked",
|
|
68
71
|
"sessions": [
|
|
69
|
-
{ "file": "20558.json", "sessionId": "
|
|
70
|
-
{ "file": "15750.json", "sessionId": "
|
|
72
|
+
{ "file": "20558.json", "sessionId": "44444444-4444-4444-8444-444444444444", "name": "jarvis-9a", "pid": "self", "entrypoint": "claude-vscode" },
|
|
73
|
+
{ "file": "15750.json", "sessionId": "44444444-4444-4444-8444-444444444444", "name": "the harness stops", "pid": "self", "entrypoint": "cli" }
|
|
71
74
|
],
|
|
72
|
-
"ask": "
|
|
75
|
+
"ask": "44444444-4444-4444-8444-444444444444",
|
|
73
76
|
"expect": {
|
|
74
77
|
"live": "forked",
|
|
75
78
|
"processes": 2,
|
|
@@ -82,10 +85,10 @@
|
|
|
82
85
|
{
|
|
83
86
|
"name": "a fork is counted by its processes even when one of them published no name",
|
|
84
87
|
"sessions": [
|
|
85
|
-
{ "file": "1.json", "sessionId": "
|
|
86
|
-
{ "file": "2.json", "sessionId": "
|
|
88
|
+
{ "file": "1.json", "sessionId": "55555555-5555-4555-8555-555555555555", "name": "named-one", "pid": "self", "entrypoint": "cli" },
|
|
89
|
+
{ "file": "2.json", "sessionId": "55555555-5555-4555-8555-555555555555", "name": "", "pid": "self", "entrypoint": "claude-vscode" }
|
|
87
90
|
],
|
|
88
|
-
"ask": "
|
|
91
|
+
"ask": "55555555-5555-4555-8555-555555555555",
|
|
89
92
|
"expect": {
|
|
90
93
|
"live": "forked",
|
|
91
94
|
"processes": 2,
|
|
@@ -95,17 +98,17 @@
|
|
|
95
98
|
{
|
|
96
99
|
"name": "a live run that published no name is running, with nothing to address it by",
|
|
97
100
|
"sessions": [
|
|
98
|
-
{ "file": "1.json", "sessionId": "
|
|
101
|
+
{ "file": "1.json", "sessionId": "66666666-6666-4666-8666-666666666666", "name": "", "pid": "self", "entrypoint": "cli" }
|
|
99
102
|
],
|
|
100
|
-
"ask": "
|
|
103
|
+
"ask": "66666666-6666-4666-8666-666666666666",
|
|
101
104
|
"expect": { "live": "running", "processes": 1, "addresses": [] }
|
|
102
105
|
},
|
|
103
106
|
{
|
|
104
107
|
"name": "a door this does not know is passed through under the name its client gave it",
|
|
105
108
|
"sessions": [
|
|
106
|
-
{ "file": "1.json", "sessionId": "
|
|
109
|
+
{ "file": "1.json", "sessionId": "77777777-7777-4777-8777-777777777777", "name": "repo-c1", "pid": "self", "entrypoint": "some-future-client" }
|
|
107
110
|
],
|
|
108
|
-
"ask": "
|
|
111
|
+
"ask": "77777777-7777-4777-8777-777777777777",
|
|
109
112
|
"expect": {
|
|
110
113
|
"live": "running",
|
|
111
114
|
"processes": 1,
|
|
@@ -115,9 +118,9 @@
|
|
|
115
118
|
{
|
|
116
119
|
"name": "a run that names no door at all is still reachable by name",
|
|
117
120
|
"sessions": [
|
|
118
|
-
{ "file": "1.json", "sessionId": "
|
|
121
|
+
{ "file": "1.json", "sessionId": "88888888-8888-4888-8888-888888888888", "name": "repo-d2", "pid": "self" }
|
|
119
122
|
],
|
|
120
|
-
"ask": "
|
|
123
|
+
"ask": "88888888-8888-4888-8888-888888888888",
|
|
121
124
|
"expect": {
|
|
122
125
|
"live": "running",
|
|
123
126
|
"processes": 1,
|
|
@@ -127,15 +130,23 @@
|
|
|
127
130
|
{
|
|
128
131
|
"name": "a half-written file is skipped rather than read as a dead session",
|
|
129
132
|
"sessions": [
|
|
130
|
-
{ "file": "1.json", "sessionId": "
|
|
133
|
+
{ "file": "1.json", "sessionId": "99999999-9999-4999-8999-999999999999", "name": "repo-e3", "pid": "self", "entrypoint": "cli" },
|
|
131
134
|
{ "file": "2.json", "raw": "{\"sessionId\": \"tru" }
|
|
132
135
|
],
|
|
133
|
-
"ask": "
|
|
136
|
+
"ask": "99999999-9999-4999-8999-999999999999",
|
|
134
137
|
"expect": {
|
|
135
138
|
"live": "running",
|
|
136
139
|
"processes": 1,
|
|
137
140
|
"addresses": [{ "name": "repo-e3", "from": "a terminal" }]
|
|
138
141
|
}
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"name": "a shift that names itself was never going to be on the list, so it is unknown and never ended",
|
|
145
|
+
"sessions": [
|
|
146
|
+
{ "file": "1.json", "sessionId": "22222222-2222-4222-8222-222222222222", "name": "repo-a8", "pid": "self", "entrypoint": "cli" }
|
|
147
|
+
],
|
|
148
|
+
"ask": "nightly-shift",
|
|
149
|
+
"expect": { "live": "unknown", "processes": 0, "addresses": [] }
|
|
139
150
|
}
|
|
140
151
|
]
|
|
141
152
|
}
|
package/harness/harness/peers.py
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
"""Somebody else is on this — and here is whether you can reach them.
|
|
2
2
|
|
|
3
3
|
**What was already known, and thrown away.** The harness has always recorded who
|
|
4
|
-
else is working
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
wait for a lease to expire, or take the work and collide.
|
|
4
|
+
else is working: a board commit holds `Work-Session` and `Work-Machine`. What every
|
|
5
|
+
surface then DID with that knowledge was print eight hex characters — `held by
|
|
6
|
+
a7c5d0cb` — which names a session without reaching one. The id was right there and
|
|
7
|
+
led nowhere, so the only move left was to wait, or take the work and collide.
|
|
9
8
|
|
|
10
9
|
So one function turns an id into the thing a session can act on: whether that
|
|
11
10
|
instance is on THIS machine, where `ListAgents` lists it and `SendMessage` reaches
|
|
@@ -260,6 +259,19 @@ def sharing(root) -> str:
|
|
|
260
259
|
return line
|
|
261
260
|
|
|
262
261
|
|
|
262
|
+
def session_shaped(instance: str) -> bool:
|
|
263
|
+
"""Whether an id has the shape of the session ids the registry publishes.
|
|
264
|
+
|
|
265
|
+
Only an id of this shape could ever have been on that list, so only an id of this
|
|
266
|
+
shape can be declared ended by its absence. A shift that names itself, a script,
|
|
267
|
+
a take recorded with no run at all — none of those was ever going to appear there,
|
|
268
|
+
and reading them as ended would free work somebody may still be doing.
|
|
269
|
+
"""
|
|
270
|
+
parts = (instance or "").strip().split("-")
|
|
271
|
+
return ([len(p) for p in parts] == [8, 4, 4, 4, 12]
|
|
272
|
+
and all(c in "0123456789abcdefABCDEF" for p in parts for c in p))
|
|
273
|
+
|
|
274
|
+
|
|
263
275
|
def reach(instance: str, host: str = "") -> dict:
|
|
264
276
|
"""Whether a run is live and what reaches it — `{"live": …, "addresses": […]}`.
|
|
265
277
|
|
|
@@ -270,18 +282,16 @@ def reach(instance: str, host: str = "") -> dict:
|
|
|
270
282
|
than two answers a person has to reconcile.
|
|
271
283
|
|
|
272
284
|
Four states, and the difference between two of them is the whole point. `ended`
|
|
273
|
-
is a CLAIM — something published a list
|
|
274
|
-
|
|
275
|
-
|
|
285
|
+
is a CLAIM — something published a list, this run was not on it, and it is the
|
|
286
|
+
kind of run that list would have held. `unknown` is the absence of a claim: nothing
|
|
287
|
+
here publishes one, the run is on a machine this one cannot see into, or it is not
|
|
288
|
+
a run the list could name. Collapsing them reports every peer of every client that
|
|
276
289
|
publishes nothing as dead, which is worse than the silence it replaces.
|
|
277
290
|
"""
|
|
278
291
|
who = (instance or "").strip()
|
|
279
292
|
away = bool(host.strip()) and bool(here()) and host.strip() != here()
|
|
280
293
|
live = None if away else _live()
|
|
281
|
-
|
|
282
|
-
# look up, so there is nothing to conclude. Reading the miss as `ended` would have
|
|
283
|
-
# every board write made without a session id report as abandoned work.
|
|
284
|
-
if live is None or not who:
|
|
294
|
+
if live is None or not session_shaped(who):
|
|
285
295
|
return {"live": "unknown", "processes": 0, "addresses": []}
|
|
286
296
|
entries = live.get(who)
|
|
287
297
|
if not entries:
|
|
@@ -319,6 +329,10 @@ def describe(instance: str, host: str = "") -> str:
|
|
|
319
329
|
f"here; the board is the only thing you share")
|
|
320
330
|
got = reach(who)
|
|
321
331
|
found = got["addresses"]
|
|
332
|
+
if got["live"] == "unknown" and not session_shaped(who):
|
|
333
|
+
return (f"{short} — this machine, but it names no session this machine "
|
|
334
|
+
f"publishes (a shift that names itself, or a script), so whether it "
|
|
335
|
+
f"is still running cannot be told from here")
|
|
322
336
|
if got["live"] == "unknown":
|
|
323
337
|
return (f"{short} — this machine: `ListAgents` lists it, `SendMessage` reaches "
|
|
324
338
|
f"it if the plan needs to change")
|
|
@@ -341,12 +355,6 @@ def describe(instance: str, host: str = "") -> str:
|
|
|
341
355
|
f"`SendMessage` reaches it if the plan needs to change")
|
|
342
356
|
|
|
343
357
|
|
|
344
|
-
def of_claim(claim: dict) -> str:
|
|
345
|
-
"""The same line, from a `.claim` — which carries the machine since this
|
|
346
|
-
landed, so a hold taken elsewhere reads as elsewhere rather than as local."""
|
|
347
|
-
return describe(str(claim.get("instance", "")), str(claim.get("machine", "")))
|
|
348
|
-
|
|
349
|
-
|
|
350
358
|
def briefing(root, rows: list, branch: str = "") -> str:
|
|
351
359
|
"""Who else is working this checkout right now and what they are on, or `""`.
|
|
352
360
|
|
|
@@ -128,6 +128,12 @@ def cmd_list(args) -> int:
|
|
|
128
128
|
}.get(v.status(), f" · {v.status()}")
|
|
129
129
|
target = f" · target {v.target}" if v.target and not v.released else ""
|
|
130
130
|
print(f"\nVERSION {v.name} — {v.title}{flag}{target}")
|
|
131
|
+
# Being FULL sits beside the status rather than replacing it: a cut can be
|
|
132
|
+
# planned or current and still owe a reshape.
|
|
133
|
+
over = v.over_ceiling()
|
|
134
|
+
if over:
|
|
135
|
+
print(f" over its ceiling by {over} — a reshape is owed "
|
|
136
|
+
f"({len(v.open_tasks())} open)")
|
|
131
137
|
if v.outcome:
|
|
132
138
|
print(f" {v.outcome}")
|
|
133
139
|
for t in _ordered(v.tasks):
|