alissa-tools-github-orcloop 0.3.5__tar.gz → 0.3.6__tar.gz
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.
- {alissa_tools_github_orcloop-0.3.5/src/main/alissa_tools_github_orcloop.egg-info → alissa_tools_github_orcloop-0.3.6}/PKG-INFO +1 -1
- {alissa_tools_github_orcloop-0.3.5 → alissa_tools_github_orcloop-0.3.6}/src/main/alissa/tools/github/orcloop/loop.py +234 -31
- alissa_tools_github_orcloop-0.3.6/src/main/alissa/tools/github/orcloop/version +1 -0
- {alissa_tools_github_orcloop-0.3.5 → alissa_tools_github_orcloop-0.3.6/src/main/alissa_tools_github_orcloop.egg-info}/PKG-INFO +1 -1
- alissa_tools_github_orcloop-0.3.5/src/main/alissa/tools/github/orcloop/version +0 -1
- {alissa_tools_github_orcloop-0.3.5 → alissa_tools_github_orcloop-0.3.6}/MANIFEST.in +0 -0
- {alissa_tools_github_orcloop-0.3.5 → alissa_tools_github_orcloop-0.3.6}/README.md +0 -0
- {alissa_tools_github_orcloop-0.3.5 → alissa_tools_github_orcloop-0.3.6}/requirements.txt +0 -0
- {alissa_tools_github_orcloop-0.3.5 → alissa_tools_github_orcloop-0.3.6}/setup.cfg +0 -0
- {alissa_tools_github_orcloop-0.3.5 → alissa_tools_github_orcloop-0.3.6}/setup.py +0 -0
- {alissa_tools_github_orcloop-0.3.5 → alissa_tools_github_orcloop-0.3.6}/src/main/alissa/tools/github/orcloop/__init__.py +0 -0
- {alissa_tools_github_orcloop-0.3.5 → alissa_tools_github_orcloop-0.3.6}/src/main/alissa/tools/github/orcloop/__main__.py +0 -0
- {alissa_tools_github_orcloop-0.3.5 → alissa_tools_github_orcloop-0.3.6}/src/main/alissa/tools/github/orcloop/alissa_client.py +0 -0
- {alissa_tools_github_orcloop-0.3.5 → alissa_tools_github_orcloop-0.3.6}/src/main/alissa/tools/github/orcloop/config.py +0 -0
- {alissa_tools_github_orcloop-0.3.5 → alissa_tools_github_orcloop-0.3.6}/src/main/alissa/tools/github/orcloop/ghclient.py +0 -0
- {alissa_tools_github_orcloop-0.3.5 → alissa_tools_github_orcloop-0.3.6}/src/main/alissa/tools/github/orcloop/markers.py +0 -0
- {alissa_tools_github_orcloop-0.3.5 → alissa_tools_github_orcloop-0.3.6}/src/main/alissa/tools/github/orcloop/proc.py +0 -0
- {alissa_tools_github_orcloop-0.3.5 → alissa_tools_github_orcloop-0.3.6}/src/main/alissa/tools/github/orcloop/state.py +0 -0
- {alissa_tools_github_orcloop-0.3.5 → alissa_tools_github_orcloop-0.3.6}/src/main/alissa/tools/github/orcloop/version.py +0 -0
- {alissa_tools_github_orcloop-0.3.5 → alissa_tools_github_orcloop-0.3.6}/src/main/alissa_tools_github_orcloop.egg-info/SOURCES.txt +0 -0
- {alissa_tools_github_orcloop-0.3.5 → alissa_tools_github_orcloop-0.3.6}/src/main/alissa_tools_github_orcloop.egg-info/dependency_links.txt +0 -0
- {alissa_tools_github_orcloop-0.3.5 → alissa_tools_github_orcloop-0.3.6}/src/main/alissa_tools_github_orcloop.egg-info/entry_points.txt +0 -0
- {alissa_tools_github_orcloop-0.3.5 → alissa_tools_github_orcloop-0.3.6}/src/main/alissa_tools_github_orcloop.egg-info/top_level.txt +0 -0
|
@@ -60,6 +60,17 @@ issues the develop daemon can pick up:
|
|
|
60
60
|
crash BETWEEN create and the ledger write: it leaves an unlabeled (and so
|
|
61
61
|
un-triggered, harmless) orphan issue; this is the accepted trade the O2 state
|
|
62
62
|
design already documents.
|
|
63
|
+
* **The origin task goes `committed -> in_progress`** as soon as its issue
|
|
64
|
+
exists (issue #30) -- on the create path, on the adopt path, and on the
|
|
65
|
+
crash-recovery path that completes an earlier release's label -- through the
|
|
66
|
+
same `AlissaClient.set_status` the close edge opens its pending_validation
|
|
67
|
+
walk with. `committed` means scoped-in TODO and released work is being worked,
|
|
68
|
+
so without this a task would read TODO for the entire implementation window
|
|
69
|
+
and only jump to `pending_validation` at merge. It is a strictly-from-
|
|
70
|
+
`committed` move (anything further along is left alone -- never a downgrade)
|
|
71
|
+
and it is NON-BLOCKING: a failed set is a warning, the release stands, and the
|
|
72
|
+
close edge's walk applies the same transition at merge. See
|
|
73
|
+
`_mark_in_progress`.
|
|
63
74
|
* **Operator park**: if the operator removes the trigger label from a released
|
|
64
75
|
issue, orcloop NEVER re-labels it (that would override the operator) and
|
|
65
76
|
raises the `parked` escalation once. Park is detected on the close edge,
|
|
@@ -92,8 +103,11 @@ titled `autodev: <owner>/<repo>` (`bow_prefix`).
|
|
|
92
103
|
one line at a time).
|
|
93
104
|
* **Discovery is O(total BOW membership)**, never O(all committed tasks): ONE
|
|
94
105
|
`GET /v1/bodies-of-work`, then one membership read per matching BOW, then one
|
|
95
|
-
`get_task` per
|
|
96
|
-
|
|
106
|
+
`get_task` per member whose row reads `committed` or `in_progress`
|
|
107
|
+
(`BOW_RESOLVE_STATES` -- membership rows carry a status but no task number, so
|
|
108
|
+
that gate is applied before spending the detail read). The second status is
|
|
109
|
+
not a release candidate: it is how a released task whose detach did not land
|
|
110
|
+
reads, and the read is what makes the retry reachable at all.
|
|
97
111
|
`list_marked_committed_tasks` is never called on the release path.
|
|
98
112
|
* **All the gates are unchanged**: committed status, the dependsOn gate, lane
|
|
99
113
|
occupancy, priority-then-age ordering, the same mechanical rendering. A
|
|
@@ -119,8 +133,10 @@ titled `autodev: <owner>/<repo>` (`bow_prefix`).
|
|
|
119
133
|
naming the unreadable count, the reason on every held decision, and a skipped
|
|
120
134
|
fallback sweep (which likewise cannot distinguish unattached from unread).
|
|
121
135
|
* **Detach is the TERMINAL act of a successful release** (create-issue ->
|
|
122
|
-
ledger -> label -> detach), never the first: a crash mid-release
|
|
123
|
-
task attached and therefore still discoverable. orcloop NEVER
|
|
136
|
+
ledger -> label -> status -> detach), never the first: a crash mid-release
|
|
137
|
+
leaves the task attached and therefore still discoverable. orcloop NEVER
|
|
138
|
+
attaches. The status step sits before it and is not coupled to it in either
|
|
139
|
+
direction, so a status failure can never leave a task attached.
|
|
124
140
|
* **Operator detach = park, and it is terminal.** A detached task simply stops
|
|
125
141
|
being a candidate; nothing re-attaches it and no later pass resurrects it. If
|
|
126
142
|
the operator RE-attaches a task orcloop already released, the dedupe path
|
|
@@ -138,7 +154,12 @@ CRASH STORY, per step of the bow-mode release (all resumed by the next poll):
|
|
|
138
154
|
3. crash after the label, before the detach -- the row is `labeled=1` and the
|
|
139
155
|
task is still attached; the next pass takes the dedupe path and detaches.
|
|
140
156
|
Detach is idempotent server-side (`removed: false` when already gone), so a
|
|
141
|
-
crash *during* the detach is equally safe.
|
|
157
|
+
crash *during* the detach is equally safe. Note the status set landed first,
|
|
158
|
+
so the task arrives at that pass at `in_progress`: the feed's committed gates
|
|
159
|
+
would judge it as a non-candidate and hold it forever, which is why an
|
|
160
|
+
already-released member is routed to the dedupe path on the LEDGER's answer
|
|
161
|
+
before its status is judged at all (`_bow_feed_pass`, and `BOW_RESOLVE_STATES`
|
|
162
|
+
for the membership-row half of the same gate).
|
|
142
163
|
|
|
143
164
|
THE SCAN-FALLBACK SWEEP (`scan_fallback_interval`, default hourly, 0 = off)
|
|
144
165
|
is the `bow` feed's level-based safety net: every interval the loop ALSO runs
|
|
@@ -252,10 +273,28 @@ log = logging.getLogger(__name__)
|
|
|
252
273
|
# the dependency (soft/hard) does not enter here -- both hold identically.
|
|
253
274
|
DEP_CLEAR_STATES = frozenset({"pending_validation", "validated"})
|
|
254
275
|
|
|
255
|
-
# The
|
|
256
|
-
#
|
|
257
|
-
#
|
|
258
|
-
|
|
276
|
+
# The two origin-task statuses the `committed → in_progress` transition moves
|
|
277
|
+
# between. `committed` = scoped-in TODO; `in_progress` = being worked.
|
|
278
|
+
#
|
|
279
|
+
# BOTH edges make exactly this move, through the same `AlissaClient.set_status`:
|
|
280
|
+
# the close edge as the first step of its pending_validation walk, and (issue
|
|
281
|
+
# #30) the release edge the moment the GitHub issue exists. Released to the
|
|
282
|
+
# pipeline IS being worked -- devloop claims a labeled issue within minutes, so
|
|
283
|
+
# a released task that kept reading `committed` until merge would read as TODO
|
|
284
|
+
# to every observer for the whole implementation window. One definition of the
|
|
285
|
+
# transition, so the two edges can never drift on which one they mean.
|
|
286
|
+
TASK_TODO_STATE = "committed"
|
|
287
|
+
TASK_IN_FLIGHT_STATE = "in_progress"
|
|
288
|
+
|
|
289
|
+
# The close edge only mutates a task the loop itself left in one of these two
|
|
290
|
+
# states. `in_progress` is the NORMAL one since issue #30 -- the release edge
|
|
291
|
+
# sets it the moment the issue exists, so that is how a task in flight reads.
|
|
292
|
+
# `committed` is now the exception rather than the rule: a release-edge status
|
|
293
|
+
# set that failed, a release that crashed before reaching it, or a task released
|
|
294
|
+
# by a dist older than 0.3.6. Either way there is work left to do, and the walk
|
|
295
|
+
# in `_mutate_close` starts from whichever it finds. Any OTHER status means the
|
|
296
|
+
# operator moved the task by hand -- skip, never fight them.
|
|
297
|
+
CLOSE_EXPECTED_STATES = frozenset({TASK_TODO_STATE, TASK_IN_FLIGHT_STATE})
|
|
259
298
|
|
|
260
299
|
# Priority ordering for the release queue. Unknown/blank priority sorts last
|
|
261
300
|
# (rank 0); ties break on age (older first).
|
|
@@ -330,6 +369,24 @@ CLOSE_HANDOFF_STATE = "pending_validation"
|
|
|
330
369
|
# gate still decides whether the work is ready to leave the queue.
|
|
331
370
|
BOW_RELEASABLE_STATE = "committed"
|
|
332
371
|
|
|
372
|
+
# The member statuses that earn a `get_task` before being judged. Wider than
|
|
373
|
+
# BOW_RELEASABLE_STATE by exactly one entry, and only since the release edge
|
|
374
|
+
# started setting `in_progress` (issue #30): an ATTACHED task at that status is
|
|
375
|
+
# most likely one of orcloop's own releases whose terminal detach did not land,
|
|
376
|
+
# and the detach retry lives behind the detail read (`_dedupe_result`). Gating
|
|
377
|
+
# it out on the membership row alone would strand it -- attached forever, held
|
|
378
|
+
# once per poll, invisible to the marker sweep (which lists COMMITTED tasks),
|
|
379
|
+
# and unreachable by the only `detach_task` caller there is. The detail read is
|
|
380
|
+
# the price of that recovery, and the bound is one read per poll per member at
|
|
381
|
+
# this status -- no writes, and nothing that scales with the task graph. Two
|
|
382
|
+
# shapes reach it, and only one is transient: an unfinished detach clears on the
|
|
383
|
+
# pass that resolves it (in steady state a released task leaves the feed on the
|
|
384
|
+
# pass that released it), while a task an operator ATTACHED at `in_progress` is
|
|
385
|
+
# held and stays attached, so it earns that read every poll until they detach
|
|
386
|
+
# it. Still the right trade against a permanent strand. Every other status
|
|
387
|
+
# (draft, blocked, cancelled, validated...) is held off the row, unread.
|
|
388
|
+
BOW_RESOLVE_STATES = frozenset({BOW_RELEASABLE_STATE, TASK_IN_FLIGHT_STATE})
|
|
389
|
+
|
|
333
390
|
# `state.meta` key: unix seconds of the last scan-fallback sweep, so the
|
|
334
391
|
# low-frequency safety net keeps its cadence across daemon restarts (a restart
|
|
335
392
|
# loop must not turn an hourly sweep into a per-boot full scan).
|
|
@@ -698,9 +755,13 @@ class Orchestrator:
|
|
|
698
755
|
sweep needs to measure itself against this pass (see `FeedPass`).
|
|
699
756
|
|
|
700
757
|
Cost is O(total BOW membership): one list call, one membership read per
|
|
701
|
-
feed BOW, and one `get_task` per
|
|
702
|
-
|
|
703
|
-
never spent on work
|
|
758
|
+
feed BOW, and one `get_task` per member the row reports at a status in
|
|
759
|
+
`BOW_RESOLVE_STATES`. Every other status is held off the row alone, so
|
|
760
|
+
the detail read is never spent on work nothing could do anything with.
|
|
761
|
+
The read IS spent on `in_progress` members, which cannot release --
|
|
762
|
+
deliberately: that is how an already-released task whose detach failed
|
|
763
|
+
reads, and the gate below routes it to `_dedupe_result` to finish the
|
|
764
|
+
detach. Recovering it is worth one read.
|
|
704
765
|
|
|
705
766
|
Membership is read for EVERY feed BOW before any member is evaluated,
|
|
706
767
|
because a task attached to two feed BOWs has an ambiguous target repo
|
|
@@ -840,7 +901,7 @@ class Orchestrator:
|
|
|
840
901
|
ambiguous = self._ambiguous_claim(owners)
|
|
841
902
|
reason = ambiguous or broken
|
|
842
903
|
|
|
843
|
-
if member.status
|
|
904
|
+
if member.status not in BOW_RESOLVE_STATES:
|
|
844
905
|
if ambiguous is not None:
|
|
845
906
|
log.warning("%s is %s", self._member_slug(member), ambiguous)
|
|
846
907
|
# Held off the membership row, so no `get_task` and no task
|
|
@@ -871,11 +932,31 @@ class Orchestrator:
|
|
|
871
932
|
# it: a task moved out of `committed` between the two reads
|
|
872
933
|
# must not release off a stale membership row.
|
|
873
934
|
if task.status != BOW_RELEASABLE_STATE:
|
|
935
|
+
if task.task_number in released_github or self.state.is_released(
|
|
936
|
+
task.task_number
|
|
937
|
+
):
|
|
938
|
+
# Not a release candidate at all -- an ALREADY-RELEASED
|
|
939
|
+
# task that is still attached is an unfinished detach,
|
|
940
|
+
# and since the release edge leaves what it releases at
|
|
941
|
+
# `in_progress` (issue #30) this is the shape that
|
|
942
|
+
# arrives here. Judging it on its status would hold the
|
|
943
|
+
# very task whose detach is waiting to be retried, in a
|
|
944
|
+
# state nothing else in the daemon can leave: the ledger
|
|
945
|
+
# answer has to come first. Same call the committed path
|
|
946
|
+
# makes below, so the retry is the one already
|
|
947
|
+
# documented.
|
|
948
|
+
results.append(
|
|
949
|
+
self._dedupe_result(task.task_number, bow=bow, task=task)
|
|
950
|
+
)
|
|
951
|
+
continue
|
|
952
|
+
stale = (
|
|
953
|
+
" — the membership row was stale"
|
|
954
|
+
if member.status == BOW_RELEASABLE_STATE else ""
|
|
955
|
+
)
|
|
874
956
|
results.append(self._logged(task.ref, Decision(
|
|
875
957
|
Action.HELD,
|
|
876
958
|
f"attached to {bow.title!r} but its task detail says "
|
|
877
|
-
f"{task.status!r}, not {BOW_RELEASABLE_STATE!r}
|
|
878
|
-
f"membership row was stale",
|
|
959
|
+
f"{task.status!r}, not {BOW_RELEASABLE_STATE!r}{stale}",
|
|
879
960
|
task.task_number,
|
|
880
961
|
)))
|
|
881
962
|
continue
|
|
@@ -911,7 +992,9 @@ class Orchestrator:
|
|
|
911
992
|
if task.task_number in released_github or self.state.is_released(
|
|
912
993
|
task.task_number
|
|
913
994
|
):
|
|
914
|
-
results.append(
|
|
995
|
+
results.append(
|
|
996
|
+
self._dedupe_result(task.task_number, bow=bow, task=task)
|
|
997
|
+
)
|
|
915
998
|
continue
|
|
916
999
|
assert target is not None # `broken` is None, so it parsed
|
|
917
1000
|
pending.append((task, bow, target))
|
|
@@ -1178,7 +1261,11 @@ class Orchestrator:
|
|
|
1178
1261
|
return results
|
|
1179
1262
|
|
|
1180
1263
|
def _dedupe_result(
|
|
1181
|
-
self,
|
|
1264
|
+
self,
|
|
1265
|
+
task_number: int,
|
|
1266
|
+
*,
|
|
1267
|
+
bow: BodyOfWork | None = None,
|
|
1268
|
+
task: Task | None = None,
|
|
1182
1269
|
) -> tuple[str, Decision]:
|
|
1183
1270
|
"""A task the dedupe set already covers. Usually a plain SKIPPED, but a
|
|
1184
1271
|
ledger row still at `labeled=0` is a create->ledger crash survivor: the
|
|
@@ -1190,7 +1277,18 @@ class Orchestrator:
|
|
|
1190
1277
|
Seeing an already-released task in the feed at all means it is still
|
|
1191
1278
|
attached: either a crash landed between the label and the detach, or the
|
|
1192
1279
|
operator re-attached it. Either way the answer is the same -- never a
|
|
1193
|
-
second issue, just finish the release by detaching.
|
|
1280
|
+
second issue, just finish the release by detaching.
|
|
1281
|
+
|
|
1282
|
+
The crash-recovery branch completes the ORIGIN STATUS too, because it is
|
|
1283
|
+
completing the very release that never reached it: it applies the
|
|
1284
|
+
trigger label, which arms devloop, so leaving the task `committed` would
|
|
1285
|
+
reproduce exactly the symptom issue #30 was filed for. `task` is passed
|
|
1286
|
+
in by the `bow` feed (which has already read the detail); the `scan`
|
|
1287
|
+
feed reaches this method with a task NUMBER only -- deliberately, so a
|
|
1288
|
+
dedupe hit costs no detail read -- so on that path the read is spent
|
|
1289
|
+
HERE, inside the rare branch. The hot branch below returns first and
|
|
1290
|
+
still reads nothing, and a failed read is a warning, never a hold: the
|
|
1291
|
+
label is what matters and it has already landed."""
|
|
1194
1292
|
ref = f"TASK-{task_number}"
|
|
1195
1293
|
row = self.state.release_for(task_number)
|
|
1196
1294
|
if row is None or row["labeled"]:
|
|
@@ -1208,6 +1306,7 @@ class Orchestrator:
|
|
|
1208
1306
|
return self._logged(ref, Decision(
|
|
1209
1307
|
Action.RELEASED,
|
|
1210
1308
|
f"[dry-run] would complete labeling of {slug} (crash recovery)"
|
|
1309
|
+
f"{self._resolved_in_progress(ref, task)}"
|
|
1211
1310
|
f"{self._detach(ref, bow)}",
|
|
1212
1311
|
task_number,
|
|
1213
1312
|
))
|
|
@@ -1222,12 +1321,89 @@ class Orchestrator:
|
|
|
1222
1321
|
return self._logged(ref, Decision(
|
|
1223
1322
|
Action.HELD, f"crash-recovery labeling of {slug} failed: {exc}", task_number,
|
|
1224
1323
|
))
|
|
1324
|
+
in_flight = self._resolved_in_progress(ref, task)
|
|
1225
1325
|
return self._logged(ref, Decision(
|
|
1226
1326
|
Action.RELEASED,
|
|
1227
|
-
f"crash recovery: applied label to {slug}{
|
|
1327
|
+
f"crash recovery: applied label to {slug}{in_flight}"
|
|
1328
|
+
f"{self._detach(ref, bow)}",
|
|
1228
1329
|
task_number,
|
|
1229
1330
|
))
|
|
1230
1331
|
|
|
1332
|
+
def _resolved_in_progress(self, ref: str, task: Task | None) -> str:
|
|
1333
|
+
"""`_mark_in_progress` for a caller that may not hold the task detail.
|
|
1334
|
+
|
|
1335
|
+
The read is spent only where it is called from (the rare crash-recovery
|
|
1336
|
+
branch), and only when the caller did not already have the task. A
|
|
1337
|
+
failed read cannot hold anything -- the issue is filed and labeled by
|
|
1338
|
+
now -- so it degrades exactly like a failed status set: one warning, and
|
|
1339
|
+
the close edge's walk corrects the status at merge."""
|
|
1340
|
+
if task is None:
|
|
1341
|
+
try:
|
|
1342
|
+
task = self.alissa.get_task(ref)
|
|
1343
|
+
except AlissaError as exc:
|
|
1344
|
+
log.warning(
|
|
1345
|
+
"could not read %s to set it %s after completing its "
|
|
1346
|
+
"release: %s — the release stands and the close edge "
|
|
1347
|
+
"applies the same transition at merge",
|
|
1348
|
+
ref, TASK_IN_FLIGHT_STATE, exc,
|
|
1349
|
+
)
|
|
1350
|
+
return f"; STATUS SET PENDING (→ {TASK_IN_FLIGHT_STATE}: {exc})"
|
|
1351
|
+
return self._mark_in_progress(task)
|
|
1352
|
+
|
|
1353
|
+
def _mark_in_progress(self, task: Task) -> str:
|
|
1354
|
+
"""Move a released origin task `committed → in_progress` and return a
|
|
1355
|
+
suffix for the decision's reason.
|
|
1356
|
+
|
|
1357
|
+
Released IS being worked (see `TASK_IN_FLIGHT_STATE`): by the time this
|
|
1358
|
+
runs the issue exists and carries the trigger label, so a developer
|
|
1359
|
+
session is minutes away, while the close edge's status walk only runs at
|
|
1360
|
+
merge -- hours or days later. Without this step the task reads TODO for
|
|
1361
|
+
that entire window.
|
|
1362
|
+
|
|
1363
|
+
Three disciplines, each deliberate:
|
|
1364
|
+
|
|
1365
|
+
* **Transition matrix.** Attempted ONLY from `committed`. A task some
|
|
1366
|
+
other surface has already advanced (a human, a race with the close
|
|
1367
|
+
edge) is left exactly as it is: no downgrade, no illegal
|
|
1368
|
+
`in_progress → in_progress` re-issue, no error -- a silent no-op that
|
|
1369
|
+
returns an empty suffix, so the release reads the same as it always
|
|
1370
|
+
did.
|
|
1371
|
+
* **Never blocking.** A failed set logs a WARNING naming the task and
|
|
1372
|
+
the release proceeds unchanged: the issue exists, the label is on, the
|
|
1373
|
+
detach still runs, and the decision is still `released`. No escalation
|
|
1374
|
+
kind, because the close edge's walk applies this same transition at
|
|
1375
|
+
merge -- the failure is cosmetic and self-healing, and a page that
|
|
1376
|
+
resolves itself is a page that teaches operators to ignore pages.
|
|
1377
|
+
* **Before the detach.** Callers sequence this ahead of `_detach` so the
|
|
1378
|
+
release's terminal act stays terminal. Nothing here couples to the
|
|
1379
|
+
detach in either direction, so a status failure can never leave a task
|
|
1380
|
+
attached to its feed. The converse -- a detach failure meeting a task
|
|
1381
|
+
this method has already advanced -- is why `_bow_feed_pass` consults
|
|
1382
|
+
the ledger before it judges an attached task's status (see `_detach`).
|
|
1383
|
+
|
|
1384
|
+
The mechanism is the close edge's, not a second one: the same
|
|
1385
|
+
`AlissaClient.set_status` call and the same two status constants that
|
|
1386
|
+
`_mutate_close` opens its pending_validation walk with."""
|
|
1387
|
+
if task.status != TASK_TODO_STATE:
|
|
1388
|
+
return ""
|
|
1389
|
+
if self.config.dry_run:
|
|
1390
|
+
return f"; [dry-run] would set {task.ref} {TASK_IN_FLIGHT_STATE}"
|
|
1391
|
+
try:
|
|
1392
|
+
self.alissa.set_status(task.ref, TASK_IN_FLIGHT_STATE)
|
|
1393
|
+
except AlissaError as exc:
|
|
1394
|
+
log.warning(
|
|
1395
|
+
"released %s but could not set it %s: %s — the release stands "
|
|
1396
|
+
"(the issue exists and is labeled) and the close edge applies "
|
|
1397
|
+
"the same transition at merge, so the task is only mis-reported "
|
|
1398
|
+
"as TODO until then",
|
|
1399
|
+
task.ref, TASK_IN_FLIGHT_STATE, exc,
|
|
1400
|
+
)
|
|
1401
|
+
return f"; STATUS SET PENDING (→ {TASK_IN_FLIGHT_STATE}: {exc})"
|
|
1402
|
+
log.info(
|
|
1403
|
+
"%s → %s (released to the pipeline)", task.ref, TASK_IN_FLIGHT_STATE
|
|
1404
|
+
)
|
|
1405
|
+
return f"; task → {TASK_IN_FLIGHT_STATE}"
|
|
1406
|
+
|
|
1231
1407
|
def _detach(self, ref: str, bow: BodyOfWork | None) -> str:
|
|
1232
1408
|
"""Detach a released task from its feed BOW -- the TERMINAL act of a
|
|
1233
1409
|
successful release -- and return a suffix for the decision's reason.
|
|
@@ -1237,7 +1413,14 @@ class Orchestrator:
|
|
|
1237
1413
|
durable in the ledger, and leaving the task attached is the SAFE
|
|
1238
1414
|
failure -- it stays discoverable, and the next pass's dedupe path
|
|
1239
1415
|
retries the detach. The reverse (detaching first) would be a take-then-
|
|
1240
|
-
act that could lose the work entirely.
|
|
1416
|
+
act that could lose the work entirely.
|
|
1417
|
+
|
|
1418
|
+
That retry is only reachable because `_bow_feed_pass` asks the LEDGER
|
|
1419
|
+
before it judges an attached task's status: the release edge runs
|
|
1420
|
+
`_mark_in_progress` before this call, so the task waiting for a retry is
|
|
1421
|
+
`in_progress`, which every committed-shaped gate would otherwise treat
|
|
1422
|
+
as "not a candidate" -- a hold nothing could ever clear, since this is
|
|
1423
|
+
`detach_task`'s only caller and the close edge never detaches."""
|
|
1241
1424
|
if bow is None:
|
|
1242
1425
|
return ""
|
|
1243
1426
|
if self.config.dry_run:
|
|
@@ -1513,6 +1696,11 @@ class Orchestrator:
|
|
|
1513
1696
|
in-flight whatever orcloop thinks, so there is no trigger to withhold,
|
|
1514
1697
|
only a ledger row to record and a lane to account for.
|
|
1515
1698
|
|
|
1699
|
+
Both branches end at `_mark_in_progress`, like the create path: adoption
|
|
1700
|
+
means the work is (back) in flight, and the already-labeled branch is
|
|
1701
|
+
the strongest case of all -- that issue has been armed for devloop since
|
|
1702
|
+
before this pass ran.
|
|
1703
|
+
|
|
1516
1704
|
An adopted body is a human's, so it is not the renderer's: only the
|
|
1517
1705
|
trailer is contractual (it is all the develop daemon reads too). A body
|
|
1518
1706
|
without the `Autonomous-Dev` trailer is adopted anyway -- the target
|
|
@@ -1576,11 +1764,12 @@ class Orchestrator:
|
|
|
1576
1764
|
verb = "would record" if labeled else "would adopt"
|
|
1577
1765
|
unlabeled_note = "" if labeled else f", no {self.config.label!r} label"
|
|
1578
1766
|
occupancy.setdefault(target, []).append(issue_scope)
|
|
1767
|
+
in_flight = self._mark_in_progress(task)
|
|
1579
1768
|
return self._logged(task.ref, Decision(
|
|
1580
1769
|
Action.SKIPPED if labeled else Action.RELEASED,
|
|
1581
1770
|
f"[dry-run] {verb} pre-existing {issue.issue_slug} (carries "
|
|
1582
1771
|
f"this task's `Alissa-Task:` trailer{unlabeled_note})"
|
|
1583
|
-
f"{self._detach(task.ref, bow)}",
|
|
1772
|
+
f"{in_flight}{self._detach(task.ref, bow)}",
|
|
1584
1773
|
task.task_number,
|
|
1585
1774
|
))
|
|
1586
1775
|
|
|
@@ -1598,11 +1787,16 @@ class Orchestrator:
|
|
|
1598
1787
|
# ran before this issue existed, so no later candidate would
|
|
1599
1788
|
# otherwise measure itself against it.
|
|
1600
1789
|
occupancy.setdefault(target, []).append(issue_scope)
|
|
1790
|
+
# `skipped` describes what was FILED (nothing), not what is
|
|
1791
|
+
# happening to the work: an armed issue for this task is live,
|
|
1792
|
+
# so the task is in flight exactly like an adopted one.
|
|
1793
|
+
in_flight = self._mark_in_progress(task)
|
|
1601
1794
|
return self._logged(task.ref, Decision(
|
|
1602
1795
|
Action.SKIPPED,
|
|
1603
1796
|
f"already released by hand as {issue.issue_slug} (trailer "
|
|
1604
1797
|
f"and {self.config.label!r} label present); recorded in the "
|
|
1605
|
-
f"ledger, no second issue{
|
|
1798
|
+
f"ledger, no second issue{in_flight}"
|
|
1799
|
+
f"{self._detach(task.ref, bow)}",
|
|
1606
1800
|
task.task_number,
|
|
1607
1801
|
))
|
|
1608
1802
|
self.github.add_label(issue.owner, issue.repo, issue.number, self.config.label)
|
|
@@ -1619,11 +1813,13 @@ class Orchestrator:
|
|
|
1619
1813
|
))
|
|
1620
1814
|
|
|
1621
1815
|
occupancy.setdefault(target, []).append(issue_scope)
|
|
1816
|
+
in_flight = self._mark_in_progress(task)
|
|
1622
1817
|
return self._logged(task.ref, Decision(
|
|
1623
1818
|
Action.RELEASED,
|
|
1624
1819
|
f"adopted pre-existing {issue.issue_slug} (carries this task's "
|
|
1625
1820
|
f"trailer, was unlabeled): ledger row recorded and "
|
|
1626
|
-
f"{self.config.label!r} applied{
|
|
1821
|
+
f"{self.config.label!r} applied{in_flight}"
|
|
1822
|
+
f"{self._detach(task.ref, bow)}",
|
|
1627
1823
|
task.task_number,
|
|
1628
1824
|
))
|
|
1629
1825
|
|
|
@@ -1805,10 +2001,11 @@ class Orchestrator:
|
|
|
1805
2001
|
bow: BodyOfWork | None = None,
|
|
1806
2002
|
) -> tuple[str, Decision]:
|
|
1807
2003
|
"""Create the issue (unlabeled), record the ledger row, stamp the label,
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
crash at any point leaves the work
|
|
2004
|
+
move the origin task `committed → in_progress`, then -- under the `bow`
|
|
2005
|
+
feed -- detach the task from its Body of Work. In that order, and detach
|
|
2006
|
+
strictly last: the label is the develop daemon's trigger and the detach
|
|
2007
|
+
is the release's terminal act, so a crash at any point leaves the work
|
|
2008
|
+
discoverable rather than lost.
|
|
1812
2009
|
Occupancy is updated in-memory so a later candidate in THIS pass sees
|
|
1813
2010
|
the new occupant and respects the lane.
|
|
1814
2011
|
|
|
@@ -1824,10 +2021,12 @@ class Orchestrator:
|
|
|
1824
2021
|
|
|
1825
2022
|
if self.config.dry_run:
|
|
1826
2023
|
occupancy.setdefault(target, []).append(scope)
|
|
2024
|
+
in_flight = self._mark_in_progress(task)
|
|
1827
2025
|
return self._logged(task.ref, Decision(
|
|
1828
2026
|
Action.RELEASED,
|
|
1829
2027
|
f"[dry-run] would release to {target} "
|
|
1830
|
-
f"(label {self.config.label!r}){
|
|
2028
|
+
f"(label {self.config.label!r}){in_flight}"
|
|
2029
|
+
f"{self._detach(task.ref, bow)}",
|
|
1831
2030
|
task.task_number,
|
|
1832
2031
|
))
|
|
1833
2032
|
|
|
@@ -1848,9 +2047,13 @@ class Orchestrator:
|
|
|
1848
2047
|
))
|
|
1849
2048
|
|
|
1850
2049
|
occupancy.setdefault(target, []).append(scope)
|
|
2050
|
+
# Status BEFORE detach, and outside the try above: the issue is filed
|
|
2051
|
+
# and armed, so nothing from here on may turn this into a non-release.
|
|
2052
|
+
in_flight = self._mark_in_progress(task)
|
|
1851
2053
|
return self._logged(task.ref, Decision(
|
|
1852
2054
|
Action.RELEASED,
|
|
1853
|
-
f"released to {issue.issue_slug}{
|
|
2055
|
+
f"released to {issue.issue_slug}{in_flight}"
|
|
2056
|
+
f"{self._detach(task.ref, bow)}",
|
|
1854
2057
|
task.task_number,
|
|
1855
2058
|
))
|
|
1856
2059
|
|
|
@@ -2025,8 +2228,8 @@ class Orchestrator:
|
|
|
2025
2228
|
|
|
2026
2229
|
Criteria satisfaction is naturally idempotent (already-satisfied ones are
|
|
2027
2230
|
skipped), so it needs no gate."""
|
|
2028
|
-
if task.status ==
|
|
2029
|
-
self.alissa.set_status(ref,
|
|
2231
|
+
if task.status == TASK_TODO_STATE:
|
|
2232
|
+
self.alissa.set_status(ref, TASK_IN_FLIGHT_STATE)
|
|
2030
2233
|
for criterion in task.criteria:
|
|
2031
2234
|
if not criterion.satisfied:
|
|
2032
2235
|
self.alissa.satisfy_criterion(ref, criterion.id)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.3.6
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.3.5
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|