@senpi/runtime 3.0.9-dev.feat-strategy-compos.20260722213012 → 3.0.9-dev.feat-strategy-compos.20260723050017
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/cli/composer-commands.d.ts.map +1 -1
- package/dist/cli/composer-commands.js +37 -2
- package/dist/cli/composer-commands.js.map +1 -1
- package/dist/composer/cli.py +115 -19
- package/dist/composer/sim/timeline.py +47 -3
- package/dist/composer/validator/errors.py +2 -0
- package/dist/composer/verbs/__init__.py +3 -0
- package/dist/composer/verbs/_target.py +138 -0
- package/dist/composer/verbs/close.py +3 -1
- package/dist/composer/verbs/fund.py +78 -16
- package/dist/composer/verbs/install.py +4 -2
- package/dist/composer/verbs/ledger.py +453 -0
- package/dist/composer/verbs/review.py +280 -1
- package/dist/composer/verbs/status.py +26 -5
- package/dist/composer/verbs/update.py +14 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -1
- package/dist/runtime/no-reply-scoping-overlay.d.ts +75 -0
- package/dist/runtime/no-reply-scoping-overlay.d.ts.map +1 -0
- package/dist/runtime/no-reply-scoping-overlay.js +93 -0
- package/dist/runtime/no-reply-scoping-overlay.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""_target — the ONE composer ``<target>`` resolver shared by the graph-consuming verbs (K4).
|
|
2
|
+
|
|
3
|
+
Before K4 the verbs disagreed about what a ``<target>`` is: ``status`` accepted a strategy NAME,
|
|
4
|
+
``check`` / ``deploy`` demanded a full ``graph.yaml`` PATH. A model that typed
|
|
5
|
+
``composer check my-strat`` (a name) or ``composer deploy ./my-strat/`` (a dir) got a bare
|
|
6
|
+
``[Errno 2] No such file or directory`` / ``[Errno 21] Is a directory`` with NO fix_hint
|
|
7
|
+
(dev-test #7). This module is the single acceptance surface. A ``<target>`` is ANY of:
|
|
8
|
+
|
|
9
|
+
* an explicit graph ``.yaml`` FILE path (the pre-K4 contract — resolves to itself, unchanged);
|
|
10
|
+
* a strategy DIR — a canonical ``<state>/composer/<name>/`` dir, or a ``composer deploy``
|
|
11
|
+
``staged/`` tree — holding a ``graph.yaml`` (or ``main/<name>.graph.yaml`` inside a staged tree);
|
|
12
|
+
* a canonical strategy NAME resolved through the state-root layout
|
|
13
|
+
(:func:`composer.verbs._layout.canonical_graph_path`).
|
|
14
|
+
|
|
15
|
+
It resolves to the graph ``.yaml`` :class:`~pathlib.Path` the verb should read, and TEACHES —
|
|
16
|
+
via **CMP256**, the existing "graph file could not be read" class, ENUMERATING the strategies that
|
|
17
|
+
DO exist under the state root — when nothing resolves. It reuses CMP256 rather than minting a
|
|
18
|
+
parallel code: the failure the caller experiences ("I named a graph and it isn't there") is exactly
|
|
19
|
+
CMP256's class; K4 only enriches the fix_hint with what IS available.
|
|
20
|
+
|
|
21
|
+
Back-compat contract: any target that is already an existing FILE resolves to itself, so every
|
|
22
|
+
currently-working ``check``/``deploy`` invocation is byte-identical. The NAME/DIR branches are new
|
|
23
|
+
capability layered on top; a target that looks like a (missing) path or an empty directory falls
|
|
24
|
+
THROUGH to the verb's own ``read_text`` so its pre-K4 CMP256 (missing file / IsADirectory) is
|
|
25
|
+
preserved verbatim — the enumerating CMP256 fires only for a bare NAME that resolves to nothing.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import os
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from typing import Optional
|
|
33
|
+
|
|
34
|
+
from composer.validator.errors import Location, TeachingError
|
|
35
|
+
|
|
36
|
+
from ._layout import COMPOSER_DIR, GRAPH_FILE, canonical_graph_path
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TargetError(Exception):
|
|
40
|
+
"""A composer ``<target>`` could not be resolved to a strategy graph. Carries a
|
|
41
|
+
:class:`TeachingError` (CMP256) the CLI renders exactly like a verb's teaching errors."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, error: TeachingError) -> None:
|
|
44
|
+
super().__init__(error.message)
|
|
45
|
+
self.error = error
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def available_strategies(base_state_dir: Optional[str]) -> list[str]:
|
|
49
|
+
"""The canonical strategy NAMES that carry a ``graph.yaml`` under ``<state>/composer/`` (sorted).
|
|
50
|
+
Empty when the state root is unknown or has no composer dir — the teaching hint degrades to a
|
|
51
|
+
generic instruction rather than fabricating a list."""
|
|
52
|
+
if not base_state_dir:
|
|
53
|
+
return []
|
|
54
|
+
root = Path(base_state_dir) / COMPOSER_DIR
|
|
55
|
+
if not root.is_dir():
|
|
56
|
+
return []
|
|
57
|
+
out: list[str] = []
|
|
58
|
+
for child in sorted(root.iterdir()):
|
|
59
|
+
if child.is_dir() and (child / GRAPH_FILE).is_file():
|
|
60
|
+
out.append(child.name)
|
|
61
|
+
return out
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def available_strategies_hint(base_state_dir: Optional[str]) -> str:
|
|
65
|
+
"""A fix_hint fragment naming the strategies that DO exist (or how to find/create one). Shared by
|
|
66
|
+
the graph verbs (check/deploy) and the lifecycle verbs (status/update) so a "not found" always
|
|
67
|
+
points at the real, on-box options rather than repeating the rejected target."""
|
|
68
|
+
names = available_strategies(base_state_dir)
|
|
69
|
+
if names:
|
|
70
|
+
shown = ", ".join(names[:20]) + (", …" if len(names) > 20 else "")
|
|
71
|
+
return f"Known strategies under the state root: {shown}."
|
|
72
|
+
return ("No strategy graphs were found under the state root's `composer/` dir — run "
|
|
73
|
+
"`composer new <id> --archetype …` first, or pass the path to an existing graph .yaml.")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def looks_like_path(target: str) -> bool:
|
|
77
|
+
"""True when ``target`` is a PATH attempt (an existing path, a value carrying a path separator,
|
|
78
|
+
or a ``.yaml``/``.yml`` filename) rather than a bare strategy NAME. A path-like miss is left to
|
|
79
|
+
the verb's own ``read_text`` so its pre-K4 CMP256 (missing file / IsADirectory) is preserved."""
|
|
80
|
+
if Path(target).exists():
|
|
81
|
+
return True
|
|
82
|
+
if os.sep in target or (os.altsep and os.altsep in target):
|
|
83
|
+
return True
|
|
84
|
+
return target.endswith((".yaml", ".yml"))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def graph_in_dir(d: Path) -> Optional[Path]:
|
|
88
|
+
"""The graph record inside a strategy / staged DIR: the top-level ``graph.yaml`` snapshot
|
|
89
|
+
(canonical dir and deploy staged tree both carry it), else the in-unit ``main/<name>.graph.yaml``
|
|
90
|
+
a staged tree also holds. None when the dir carries no graph record."""
|
|
91
|
+
direct = d / GRAPH_FILE
|
|
92
|
+
if direct.is_file():
|
|
93
|
+
return direct
|
|
94
|
+
main = d / "main"
|
|
95
|
+
if main.is_dir():
|
|
96
|
+
for child in sorted(main.glob("*.graph.yaml")):
|
|
97
|
+
return child
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def resolve_read_path(target: str, base_state_dir: Optional[str]) -> Path:
|
|
102
|
+
"""Resolve a ``check`` / ``deploy`` ``<target>`` to the graph ``.yaml`` :class:`~pathlib.Path`
|
|
103
|
+
to read. Accepts a graph file (→ itself), a strategy/staged DIR (→ the graph inside), or a bare
|
|
104
|
+
strategy NAME (→ its canonical ``composer/<name>/graph.yaml``).
|
|
105
|
+
|
|
106
|
+
Returns the path the caller should ``read_text``. For an existing FILE this is the file itself
|
|
107
|
+
(byte-identical to pre-K4). For a path-like MISS (a missing ``.yaml`` path, or a directory with
|
|
108
|
+
NO graph inside) it returns the ORIGINAL path so the caller's own ``read_text`` raises the OSError
|
|
109
|
+
the caller already wraps as CMP256 (preserving the missing-file / IsADirectory teaching). It
|
|
110
|
+
RAISES :class:`TargetError` (CMP256, enumerating the available strategies) only for a bare NAME
|
|
111
|
+
that resolves to nothing — the case that previously produced a bare ``[Errno 2]``."""
|
|
112
|
+
p = Path(target)
|
|
113
|
+
if p.is_file():
|
|
114
|
+
return p
|
|
115
|
+
if p.is_dir():
|
|
116
|
+
found = graph_in_dir(p)
|
|
117
|
+
# a dir WITH a graph → that graph; an empty/graph-less dir falls through unchanged so the
|
|
118
|
+
# caller's read_text raises IsADirectoryError → its own CMP256 (the pre-K4 behaviour).
|
|
119
|
+
return found if found is not None else p
|
|
120
|
+
if looks_like_path(target):
|
|
121
|
+
# a missing FILE path (…/foo.graph.yaml): let the caller's read_text raise → its CMP256.
|
|
122
|
+
return p
|
|
123
|
+
# a bare STRATEGY NAME resolved through the canonical layout (needs the state root).
|
|
124
|
+
if base_state_dir:
|
|
125
|
+
cg = canonical_graph_path(base_state_dir, target)
|
|
126
|
+
if cg.is_file():
|
|
127
|
+
return cg
|
|
128
|
+
raise TargetError(TeachingError(
|
|
129
|
+
"CMP256",
|
|
130
|
+
f"the composer target {target!r} could not be resolved to a strategy graph — it is not an "
|
|
131
|
+
f"existing graph .yaml file, not a strategy directory, and no canonical "
|
|
132
|
+
f"`composer/{target}/graph.yaml` record exists under the state root.",
|
|
133
|
+
"Pass the path to an existing graph .yaml, a strategy directory, or a known strategy name. "
|
|
134
|
+
+ available_strategies_hint(base_state_dir)
|
|
135
|
+
+ (" (Pass `--state-dir <dir>` or set `SENPI_STATE_DIR` so a bare name can be resolved.)"
|
|
136
|
+
if not base_state_dir else ""),
|
|
137
|
+
Location(),
|
|
138
|
+
))
|
|
@@ -387,7 +387,9 @@ def _resolve_targets(target: str, base_state_dir: str, strategies: list,
|
|
|
387
387
|
rec = json.loads(wj_path.read_text(encoding="utf-8"))
|
|
388
388
|
except (OSError, json.JSONDecodeError):
|
|
389
389
|
rec = {}
|
|
390
|
-
|
|
390
|
+
# K3 follow-up: tolerant address read — a hand-authored marker may use the MCP key spellings.
|
|
391
|
+
from .fund import _rec_wallet
|
|
392
|
+
wallet = _rec_wallet(rec)
|
|
391
393
|
rec_sid = rec.get("strategyId")
|
|
392
394
|
s = next((s for s in strategies
|
|
393
395
|
if (rec_sid and _strat_id(s) == rec_sid) or wallet_match(_strat_wallet(s), wallet)),
|
|
@@ -242,6 +242,33 @@ def _match_strategy(strategies: list, strategy_id: Optional[str], wallet: Option
|
|
|
242
242
|
return None
|
|
243
243
|
|
|
244
244
|
|
|
245
|
+
# Hand-authored / alternate marker key spellings for the wallet ADDRESS (K3). The canonical writer
|
|
246
|
+
# (_write_wallet_json) always emits ``"wallet"``, but a human (or model) authoring a wallet.json
|
|
247
|
+
# marker by hand often copies the field name MCP ``strategy_list`` uses instead —
|
|
248
|
+
# ``strategyWalletAddress`` / ``walletAddress`` / ``address`` (the exact spellings
|
|
249
|
+
# ``close._strat_wallet`` reads). We accept those unambiguous alternates when resolving a marker so
|
|
250
|
+
# the documented adopt/reuse flow survives a hand-authored marker; a backfill then NORMALIZES the
|
|
251
|
+
# file back to the canonical ``"wallet"`` key.
|
|
252
|
+
_WALLET_KEYS = ("wallet", "walletAddress", "strategyWalletAddress", "address")
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _rec_wallet(rec: dict) -> str:
|
|
256
|
+
"""The wallet address a wallet.json marker records, tolerating unambiguous hand-authored key
|
|
257
|
+
spellings (K3). Prefers the canonical ``wallet`` key, then the MCP-shaped alternates. The KEY
|
|
258
|
+
match is case-insensitive (a hand-author may write ``Address``); the VALUE is returned as read.
|
|
259
|
+
Empty string when the marker carries no recognizable address key."""
|
|
260
|
+
if not isinstance(rec, dict):
|
|
261
|
+
return ""
|
|
262
|
+
lower = {str(k).lower(): v for k, v in rec.items()}
|
|
263
|
+
for key in _WALLET_KEYS:
|
|
264
|
+
v = rec.get(key)
|
|
265
|
+
if not v:
|
|
266
|
+
v = lower.get(key.lower())
|
|
267
|
+
if v:
|
|
268
|
+
return str(v)
|
|
269
|
+
return ""
|
|
270
|
+
|
|
271
|
+
|
|
245
272
|
# --------------------------------------------------------------------------- result
|
|
246
273
|
|
|
247
274
|
@dataclass
|
|
@@ -563,12 +590,25 @@ def _verify_existing(target: str, strategy: str, budget: float, rec: dict, wj_pa
|
|
|
563
590
|
mcp_fn: McpFn, created_at: Optional[str],
|
|
564
591
|
kind: Optional[str] = None) -> FundResult:
|
|
565
592
|
"""Step (a): a wallet.json already exists — verify it via strategy_list, never re-fund.
|
|
566
|
-
|
|
567
|
-
|
|
593
|
+
|
|
594
|
+
The marker is resolved by recorded strategyId AND by wallet ADDRESS against the LIVE strategies
|
|
595
|
+
(K3) — the documented adopt/reuse flow, kept alive for a HAND-AUTHORED marker: an alternate
|
|
596
|
+
address key spelling (``walletAddress`` / ``strategyWalletAddress`` / ``address``), an
|
|
597
|
+
address-only marker, or a wrong/stale strategyId sitting next to a live address all still adopt.
|
|
598
|
+
When the match lands by address, the live strategy's own id is written back (a stale recorded id
|
|
599
|
+
is corrected, never propagated), and the file is NORMALIZED in place to the canonical ``wallet``
|
|
600
|
+
key. ``kind`` (the recorded marker), ``created_at`` and ``budget`` are PRESERVED across any
|
|
601
|
+
in-place backfill so a manual wallet never loses its marker on a re-run. CMP231 remains for the
|
|
602
|
+
genuinely-stale case: no live strategy holds this wallet."""
|
|
568
603
|
rec_kind = kind if kind is not None else wallet_kind(rec)
|
|
569
604
|
result_kind = rec_kind or KIND_MANAGED
|
|
570
|
-
|
|
605
|
+
# Resolve the recorded address tolerating hand-authored key spellings (K3); keep the raw
|
|
606
|
+
# canonical-key value separately so we know whether the FILE needs normalizing back to "wallet".
|
|
607
|
+
rec_wallet = _rec_wallet(rec)
|
|
608
|
+
rec_wallet_canonical = str(rec.get("wallet") or "")
|
|
571
609
|
rec_sid = rec.get("strategyId")
|
|
610
|
+
rec_budget = rec.get("budget", budget) # PRESERVE the funded budget across backfills
|
|
611
|
+
rec_created = rec.get("created_at") or created_at
|
|
572
612
|
try:
|
|
573
613
|
strategies = _find_list(mcp_fn("strategy_list", status=LIVE_STATUSES), "strategies")
|
|
574
614
|
mcp_ok = True
|
|
@@ -578,39 +618,61 @@ def _verify_existing(target: str, strategy: str, budget: float, rec: dict, wj_pa
|
|
|
578
618
|
except McpError:
|
|
579
619
|
strategies, mcp_ok = [], False
|
|
580
620
|
|
|
621
|
+
# Resolve by recorded strategyId FIRST, then fall back to the wallet ADDRESS (this is the adopt/
|
|
622
|
+
# reuse path K3 must keep alive for a hand-authored marker). When the match lands via the
|
|
623
|
+
# address fallback, the LIVE strategy's own id is authoritative — a stale-but-truthy recorded
|
|
624
|
+
# strategyId must be CORRECTED to it, never propagated into the result or the backfilled file.
|
|
581
625
|
s = _match_strategy(strategies, rec_sid, rec_wallet)
|
|
582
626
|
status = str(_strat_status(s) or "").upper() if s else ""
|
|
627
|
+
live_sid = _strat_id(s) if s else None
|
|
628
|
+
real_sid = live_sid or rec_sid
|
|
583
629
|
addr = str(_strat_wallet(s) or "") if s else rec_wallet
|
|
630
|
+
write_addr = addr or rec_wallet
|
|
631
|
+
# The file drifts from live truth when the canonical "wallet" key is empty/stale (a
|
|
632
|
+
# created-but-not-yet-ACTIVE mint, or a hand-authored alternate-key/address-only marker) or when
|
|
633
|
+
# the recorded strategyId is stale. Any drift → backfill in place, normalizing to the "wallet"
|
|
634
|
+
# key and the correct id while PRESERVING kind/created_at/budget (requirement 3: a legacy-read
|
|
635
|
+
# marker is rewritten to the SAME path it was read from — never a new canonical file).
|
|
636
|
+
needs_backfill = bool(write_addr) and (
|
|
637
|
+
write_addr != rec_wallet_canonical or real_sid != rec_sid
|
|
638
|
+
)
|
|
584
639
|
|
|
585
640
|
if status == "ACTIVE":
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
created_at=rec.get("created_at") or created_at, kind=rec_kind)
|
|
641
|
+
if needs_backfill:
|
|
642
|
+
_write_wallet_json(wj_path, strategy=strategy, wallet=write_addr,
|
|
643
|
+
strategy_id=real_sid, budget=rec_budget,
|
|
644
|
+
created_at=rec_created, kind=rec_kind)
|
|
591
645
|
return FundResult(
|
|
592
|
-
target=target, verdict=FUNDED, strategy=strategy, wallet=
|
|
593
|
-
strategy_id=
|
|
646
|
+
target=target, verdict=FUNDED, strategy=strategy, wallet=write_addr,
|
|
647
|
+
strategy_id=real_sid, budget=rec_budget,
|
|
594
648
|
kind=result_kind, message="wallet already funded and ACTIVE (no-op).",
|
|
595
649
|
)
|
|
596
650
|
if s is not None and status and status not in CLOSED_STATUSES:
|
|
651
|
+
if needs_backfill:
|
|
652
|
+
_write_wallet_json(wj_path, strategy=strategy, wallet=write_addr,
|
|
653
|
+
strategy_id=real_sid, budget=rec_budget,
|
|
654
|
+
created_at=rec_created, kind=rec_kind)
|
|
597
655
|
return FundResult(
|
|
598
|
-
target=target, verdict=PENDING, strategy=strategy, wallet=
|
|
599
|
-
strategy_id=
|
|
656
|
+
target=target, verdict=PENDING, strategy=strategy, wallet=write_addr,
|
|
657
|
+
strategy_id=real_sid, budget=rec_budget,
|
|
600
658
|
kind=result_kind, message=f"wallet still initializing (status {status}).",
|
|
601
659
|
)
|
|
602
660
|
if not mcp_ok:
|
|
603
661
|
# Could not reach MCP to confirm — do NOT declare stale; re-run once reachable.
|
|
604
662
|
return FundResult(
|
|
605
663
|
target=target, verdict=PENDING, strategy=strategy, wallet=rec_wallet,
|
|
606
|
-
strategy_id=rec_sid, budget=
|
|
664
|
+
strategy_id=rec_sid, budget=rec_budget, kind=result_kind,
|
|
607
665
|
message="could not reach MCP to verify the recorded wallet — re-run `composer fund`.",
|
|
608
666
|
)
|
|
609
|
-
#
|
|
667
|
+
# No LIVE strategy carries this strategyId OR this wallet address → genuinely stale. (fund
|
|
668
|
+
# resolves by wallet address against your active strategies, so "stale" here means no active
|
|
669
|
+
# strategy holds this wallet — not merely that the recorded strategyId moved on.)
|
|
610
670
|
return _refused(target, strategy, TeachingError(
|
|
611
671
|
"CMP231", f"the recorded wallet ({rec_wallet or 'strategyId ' + str(rec_sid)}) is "
|
|
612
|
-
f"{status or '
|
|
613
|
-
f"
|
|
672
|
+
f"{status or 'not held by any of your active strategies'} — wallet.json is stale.",
|
|
673
|
+
f"fund resolves a marker by its recorded strategyId AND by its wallet address against your "
|
|
674
|
+
f"active strategies (`strategy_list`), so 'stale' means NO active strategy holds this wallet "
|
|
675
|
+
f"— its strategy was closed or is gone, and it cannot be installed. To fund a FRESH wallet "
|
|
614
676
|
f"for {strategy!r}, delete the stale marker `{wj_path}` and re-run `composer fund`. If you "
|
|
615
677
|
f"believe it is still open, check MCP `strategy_list` (it may be filtered out as terminal).",
|
|
616
678
|
Location(),
|
|
@@ -295,7 +295,7 @@ def _auto_bind_wallet(target: str, out_dir: Optional[str], base_state_dir: str,
|
|
|
295
295
|
# Imported lazily to avoid an import cycle (fund/close import install at module load).
|
|
296
296
|
from ._box import LIVE_STATUSES, McpCredsMissing, McpError
|
|
297
297
|
from .close import _find_list, _strat_id, _strat_status, _strat_wallet
|
|
298
|
-
from .fund import _match_strategy, _read_wallet_json
|
|
298
|
+
from .fund import _match_strategy, _read_wallet_json, _rec_wallet
|
|
299
299
|
|
|
300
300
|
def _cmp206(msg: str, fix: str) -> tuple[str, TeachingError]:
|
|
301
301
|
return "", TeachingError("CMP206", msg, fix, Location())
|
|
@@ -325,7 +325,9 @@ def _auto_bind_wallet(target: str, out_dir: Optional[str], base_state_dir: str,
|
|
|
325
325
|
f"writes {canonical_wj}), then re-run `composer install` to auto-bind it — or pass "
|
|
326
326
|
f"`--wallet 0x…` explicitly.",
|
|
327
327
|
)
|
|
328
|
-
|
|
328
|
+
# K3 follow-up: read the address with the SAME tolerant reader fund/update use — a
|
|
329
|
+
# hand-authored marker records it under address/walletAddress/strategyWalletAddress.
|
|
330
|
+
rec_wallet = _rec_wallet(rec)
|
|
329
331
|
rec_sid = rec.get("strategyId")
|
|
330
332
|
try:
|
|
331
333
|
strategies = _find_list(mcp_fn("strategy_list", status=LIVE_STATUSES), "strategies")
|