alissa-tools-github-orcloop 0.3.6__tar.gz → 0.3.8__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.6/src/main/alissa_tools_github_orcloop.egg-info → alissa_tools_github_orcloop-0.3.8}/PKG-INFO +1 -1
- {alissa_tools_github_orcloop-0.3.6 → alissa_tools_github_orcloop-0.3.8}/src/main/alissa/tools/github/orcloop/alissa_client.py +123 -0
- {alissa_tools_github_orcloop-0.3.6 → alissa_tools_github_orcloop-0.3.8}/src/main/alissa/tools/github/orcloop/loop.py +787 -32
- {alissa_tools_github_orcloop-0.3.6 → alissa_tools_github_orcloop-0.3.8}/src/main/alissa/tools/github/orcloop/markers.py +59 -0
- {alissa_tools_github_orcloop-0.3.6 → alissa_tools_github_orcloop-0.3.8}/src/main/alissa/tools/github/orcloop/state.py +10 -6
- alissa_tools_github_orcloop-0.3.8/src/main/alissa/tools/github/orcloop/version +1 -0
- {alissa_tools_github_orcloop-0.3.6 → alissa_tools_github_orcloop-0.3.8/src/main/alissa_tools_github_orcloop.egg-info}/PKG-INFO +1 -1
- alissa_tools_github_orcloop-0.3.6/src/main/alissa/tools/github/orcloop/version +0 -1
- {alissa_tools_github_orcloop-0.3.6 → alissa_tools_github_orcloop-0.3.8}/MANIFEST.in +0 -0
- {alissa_tools_github_orcloop-0.3.6 → alissa_tools_github_orcloop-0.3.8}/README.md +0 -0
- {alissa_tools_github_orcloop-0.3.6 → alissa_tools_github_orcloop-0.3.8}/requirements.txt +0 -0
- {alissa_tools_github_orcloop-0.3.6 → alissa_tools_github_orcloop-0.3.8}/setup.cfg +0 -0
- {alissa_tools_github_orcloop-0.3.6 → alissa_tools_github_orcloop-0.3.8}/setup.py +0 -0
- {alissa_tools_github_orcloop-0.3.6 → alissa_tools_github_orcloop-0.3.8}/src/main/alissa/tools/github/orcloop/__init__.py +0 -0
- {alissa_tools_github_orcloop-0.3.6 → alissa_tools_github_orcloop-0.3.8}/src/main/alissa/tools/github/orcloop/__main__.py +0 -0
- {alissa_tools_github_orcloop-0.3.6 → alissa_tools_github_orcloop-0.3.8}/src/main/alissa/tools/github/orcloop/config.py +0 -0
- {alissa_tools_github_orcloop-0.3.6 → alissa_tools_github_orcloop-0.3.8}/src/main/alissa/tools/github/orcloop/ghclient.py +0 -0
- {alissa_tools_github_orcloop-0.3.6 → alissa_tools_github_orcloop-0.3.8}/src/main/alissa/tools/github/orcloop/proc.py +0 -0
- {alissa_tools_github_orcloop-0.3.6 → alissa_tools_github_orcloop-0.3.8}/src/main/alissa/tools/github/orcloop/version.py +0 -0
- {alissa_tools_github_orcloop-0.3.6 → alissa_tools_github_orcloop-0.3.8}/src/main/alissa_tools_github_orcloop.egg-info/SOURCES.txt +0 -0
- {alissa_tools_github_orcloop-0.3.6 → alissa_tools_github_orcloop-0.3.8}/src/main/alissa_tools_github_orcloop.egg-info/dependency_links.txt +0 -0
- {alissa_tools_github_orcloop-0.3.6 → alissa_tools_github_orcloop-0.3.8}/src/main/alissa_tools_github_orcloop.egg-info/entry_points.txt +0 -0
- {alissa_tools_github_orcloop-0.3.6 → alissa_tools_github_orcloop-0.3.8}/src/main/alissa_tools_github_orcloop.egg-info/top_level.txt +0 -0
|
@@ -127,6 +127,37 @@ would then hold a task whose only dependency is fully validated -- forever,
|
|
|
127
127
|
eventually paging ``release-blocked``. Deduplicating matches what the resolved
|
|
128
128
|
view actually does.
|
|
129
129
|
|
|
130
|
+
TASK CONTEXT ATTACHMENTS (O14)
|
|
131
|
+
==============================
|
|
132
|
+
An operator's decisions about a task do not all fit in the description that was
|
|
133
|
+
written before the work started. Alissa's answer is the task's **context
|
|
134
|
+
attachments** -- ``GET /v1/tasks/<ref>/context`` -> ``{"context": [...]}`` --
|
|
135
|
+
and O14 makes them travel: orcloop renders them into the issue it files, so a
|
|
136
|
+
decision recorded after authoring reaches the implementing worker (repo issue
|
|
137
|
+
#34). Verified live against ``api.alissa.app``:
|
|
138
|
+
|
|
139
|
+
* Each row carries ``_id``, ``title``, ``createdAt`` (ms), ``actorId``/
|
|
140
|
+
``authorName``, and then AT MOST ONE payload of ``markdownContent`` /
|
|
141
|
+
``link`` / a vault reference, plus an optional one-line ``note``. The
|
|
142
|
+
OpenAPI schema pins only ``_id`` and ``title`` (everything else is
|
|
143
|
+
``additionalProperties``), so `get_task_context` treats every payload field
|
|
144
|
+
as optional and an item with none of them still renders as a titled,
|
|
145
|
+
empty subsection rather than being dropped -- a decision the operator
|
|
146
|
+
attached and orcloop silently discarded is the one outcome #34 exists to
|
|
147
|
+
prevent.
|
|
148
|
+
* The task DETAIL payload carries ``contextCount`` (an int), which is what
|
|
149
|
+
lets the release path skip this read entirely for the common zero-context
|
|
150
|
+
task -- see `Task.context_count`.
|
|
151
|
+
|
|
152
|
+
Note for a future reader: the detail payload ALSO embeds the attachments
|
|
153
|
+
themselves under an undocumented ``contextAttachments`` key, so the second
|
|
154
|
+
call could in principle be avoided. orcloop deliberately does not read it. The
|
|
155
|
+
dedicated route is the documented, access-gated one (the OpenAPI ``Task``
|
|
156
|
+
schema pins neither the key nor its item shape), and the release path's
|
|
157
|
+
conservatism depends on being able to tell "there are N attachments" from "I
|
|
158
|
+
read them": a silently-absent embedded array would read as "no context" on
|
|
159
|
+
exactly the payload that declares some.
|
|
160
|
+
|
|
130
161
|
WHO AM I (O8)
|
|
131
162
|
=============
|
|
132
163
|
``GET /v1/ping`` -- ``{"pong", "timestamp", "userId", "actorId",
|
|
@@ -266,6 +297,29 @@ class Dependency:
|
|
|
266
297
|
status: str
|
|
267
298
|
|
|
268
299
|
|
|
300
|
+
@dataclass(frozen=True)
|
|
301
|
+
class ContextItem:
|
|
302
|
+
"""One knowledge/reference attachment on a task (O14).
|
|
303
|
+
|
|
304
|
+
The operator's decision channel: whatever was decided AFTER the task was
|
|
305
|
+
authored lands here rather than being rewritten into the description. Every
|
|
306
|
+
field but `id` is optional in the payload, so every one but `id` defaults
|
|
307
|
+
to empty -- an attachment is identified by its title and rendered from
|
|
308
|
+
whichever payload it carries.
|
|
309
|
+
|
|
310
|
+
`markdown` is the write-up, `link` a bare URL, `note` the one-line "why
|
|
311
|
+
this is relevant"; an attachment may carry any combination, including none
|
|
312
|
+
(a vault reference the REST payload does not inline). `created_at` is ms
|
|
313
|
+
since epoch, 0 when the payload omits it -- the renderer orders on it."""
|
|
314
|
+
|
|
315
|
+
id: str
|
|
316
|
+
title: str = ""
|
|
317
|
+
markdown: str = ""
|
|
318
|
+
link: str = ""
|
|
319
|
+
note: str = ""
|
|
320
|
+
created_at: int = 0
|
|
321
|
+
|
|
322
|
+
|
|
269
323
|
@dataclass(frozen=True)
|
|
270
324
|
class Task:
|
|
271
325
|
"""A task as the DETAIL endpoint reports it -- full description and DoD,
|
|
@@ -293,6 +347,13 @@ class Task:
|
|
|
293
347
|
# len(dependencies) when an upstream is invisible to this actor -- the gate
|
|
294
348
|
# treats that mismatch as "not provably clear".
|
|
295
349
|
depends_on_count: int = 0
|
|
350
|
+
# How many context attachments the DETAIL payload declares (`contextCount`,
|
|
351
|
+
# O14). Purely a "is the second read worth making" hint for the release
|
|
352
|
+
# path: 0/absent skips `get_task_context` entirely, which is the common
|
|
353
|
+
# case. It is NOT a length contract -- attachments can be added or deleted
|
|
354
|
+
# between the two reads -- so nothing keys on it agreeing with what the
|
|
355
|
+
# context endpoint returns. Only "declares some" vs "declares none" matters.
|
|
356
|
+
context_count: int = 0
|
|
296
357
|
|
|
297
358
|
|
|
298
359
|
def _query(params: dict) -> str:
|
|
@@ -625,6 +686,17 @@ class AlissaClient:
|
|
|
625
686
|
created_raw = task.get("createdAt")
|
|
626
687
|
created_at = int(created_raw) if isinstance(created_raw, (int, float)) else 0
|
|
627
688
|
|
|
689
|
+
# A bool IS an int in Python; `contextCount: true` would read as 1 and
|
|
690
|
+
# buy a context read on a payload that declared nothing countable, so
|
|
691
|
+
# it is excluded explicitly. Negatives clamp to 0 -- "fewer than none"
|
|
692
|
+
# is not a reason to make the read.
|
|
693
|
+
count_raw = task.get("contextCount")
|
|
694
|
+
context_count = (
|
|
695
|
+
max(0, int(count_raw))
|
|
696
|
+
if isinstance(count_raw, (int, float)) and not isinstance(count_raw, bool)
|
|
697
|
+
else 0
|
|
698
|
+
)
|
|
699
|
+
|
|
628
700
|
number = task.get("taskNumber")
|
|
629
701
|
return Task(
|
|
630
702
|
ref=f"TASK-{number}" if isinstance(number, int) else str(ref),
|
|
@@ -638,7 +710,58 @@ class AlissaClient:
|
|
|
638
710
|
created_at=created_at,
|
|
639
711
|
dependencies=tuple(dependencies),
|
|
640
712
|
depends_on_count=depends_on_count,
|
|
713
|
+
context_count=context_count,
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
def get_task_context(self, ref: str) -> list[ContextItem]:
|
|
717
|
+
"""The task's context attachments, in the order the endpoint returns
|
|
718
|
+
them (`GET /v1/tasks/<ref>/context`).
|
|
719
|
+
|
|
720
|
+
Ordering is the CALLER's to impose (the renderer sorts on
|
|
721
|
+
`created_at`); this stays a faithful read of the payload. Rows without
|
|
722
|
+
an `_id` are dropped -- an attachment that cannot be addressed cannot be
|
|
723
|
+
cited or edited -- but a row with an id and nothing else is KEPT: the
|
|
724
|
+
payload pins only `_id` and `title`, so treating a shape this adapter
|
|
725
|
+
does not recognise as "no context" would drop the very decisions the
|
|
726
|
+
feature exists to carry.
|
|
727
|
+
|
|
728
|
+
A 2xx whose `context` key is not an array is a contract violation, not
|
|
729
|
+
an empty list, and surfaces as the base `AlissaError` -- the same way
|
|
730
|
+
`get_task` refuses to fabricate a Task. That distinction is load-
|
|
731
|
+
bearing on the release path: it holds the release instead of filing an
|
|
732
|
+
issue that silently lost the operator's decisions.
|
|
733
|
+
"""
|
|
734
|
+
payload = self._request(
|
|
735
|
+
"GET", f"/v1/tasks/{urllib.parse.quote(str(ref), safe='')}/context"
|
|
641
736
|
)
|
|
737
|
+
rows = payload.get("context") if isinstance(payload, dict) else None
|
|
738
|
+
if not isinstance(rows, list):
|
|
739
|
+
raise AlissaError(200, f"no context array in response for {ref!r}")
|
|
740
|
+
|
|
741
|
+
out: list[ContextItem] = []
|
|
742
|
+
for row in rows:
|
|
743
|
+
if not isinstance(row, dict):
|
|
744
|
+
continue
|
|
745
|
+
item_id = row.get("_id")
|
|
746
|
+
if not isinstance(item_id, str) or not item_id:
|
|
747
|
+
continue
|
|
748
|
+
created_raw = row.get("createdAt")
|
|
749
|
+
out.append(
|
|
750
|
+
ContextItem(
|
|
751
|
+
id=item_id,
|
|
752
|
+
title=str(row.get("title") or ""),
|
|
753
|
+
markdown=str(row.get("markdownContent") or ""),
|
|
754
|
+
link=str(row.get("link") or ""),
|
|
755
|
+
note=str(row.get("note") or ""),
|
|
756
|
+
created_at=(
|
|
757
|
+
int(created_raw)
|
|
758
|
+
if isinstance(created_raw, (int, float))
|
|
759
|
+
and not isinstance(created_raw, bool)
|
|
760
|
+
else 0
|
|
761
|
+
),
|
|
762
|
+
)
|
|
763
|
+
)
|
|
764
|
+
return out
|
|
642
765
|
|
|
643
766
|
def add_evidence(self, ref: str, title: str, markdown: str, *, item_type: str = "evidence") -> None:
|
|
644
767
|
"""Attach an evidence/deliverable item (outputs surface, not context).
|