@senpi/runtime 3.0.59 → 3.0.62

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.
Files changed (151) hide show
  1. package/README.md +73 -2
  2. package/dist/actions/decision-engine.d.ts +0 -2
  3. package/dist/actions/decision-engine.d.ts.map +1 -1
  4. package/dist/actions/decision-engine.js +1 -79
  5. package/dist/actions/decision-engine.js.map +1 -1
  6. package/dist/cli/deploy-render.d.ts +91 -0
  7. package/dist/cli/deploy-render.d.ts.map +1 -0
  8. package/dist/cli/deploy-render.js +216 -0
  9. package/dist/cli/deploy-render.js.map +1 -0
  10. package/dist/cli/guide-commands.d.ts.map +1 -1
  11. package/dist/cli/guide-commands.js +78 -6
  12. package/dist/cli/guide-commands.js.map +1 -1
  13. package/dist/cli/senpi-commands.d.ts +6 -1
  14. package/dist/cli/senpi-commands.d.ts.map +1 -1
  15. package/dist/cli/senpi-commands.js +164 -5
  16. package/dist/cli/senpi-commands.js.map +1 -1
  17. package/dist/cli/validate-command.d.ts.map +1 -1
  18. package/dist/cli/validate-command.js +32 -1
  19. package/dist/cli/validate-command.js.map +1 -1
  20. package/dist/deploy/deadlines.d.ts +47 -0
  21. package/dist/deploy/deadlines.d.ts.map +1 -0
  22. package/dist/deploy/deadlines.js +56 -0
  23. package/dist/deploy/deadlines.js.map +1 -0
  24. package/dist/deploy/error-text.d.ts +25 -0
  25. package/dist/deploy/error-text.d.ts.map +1 -0
  26. package/dist/deploy/error-text.js +42 -0
  27. package/dist/deploy/error-text.js.map +1 -0
  28. package/dist/deploy/funding.d.ts +87 -0
  29. package/dist/deploy/funding.d.ts.map +1 -0
  30. package/dist/deploy/funding.js +229 -0
  31. package/dist/deploy/funding.js.map +1 -0
  32. package/dist/deploy/install-gate-runner.d.ts +12 -0
  33. package/dist/deploy/install-gate-runner.d.ts.map +1 -0
  34. package/dist/deploy/install-gate-runner.js +17 -0
  35. package/dist/deploy/install-gate-runner.js.map +1 -0
  36. package/dist/deploy/job.d.ts +199 -0
  37. package/dist/deploy/job.d.ts.map +1 -0
  38. package/dist/deploy/job.js +463 -0
  39. package/dist/deploy/job.js.map +1 -0
  40. package/dist/deploy/min-budget.d.ts +68 -0
  41. package/dist/deploy/min-budget.d.ts.map +1 -0
  42. package/dist/deploy/min-budget.js +297 -0
  43. package/dist/deploy/min-budget.js.map +1 -0
  44. package/dist/deploy/orchestrator.d.ts +305 -0
  45. package/dist/deploy/orchestrator.d.ts.map +1 -0
  46. package/dist/deploy/orchestrator.js +2910 -0
  47. package/dist/deploy/orchestrator.js.map +1 -0
  48. package/dist/deploy/package.d.ts +130 -0
  49. package/dist/deploy/package.d.ts.map +1 -0
  50. package/dist/deploy/package.js +351 -0
  51. package/dist/deploy/package.js.map +1 -0
  52. package/dist/deploy/proof-gate.d.ts +82 -0
  53. package/dist/deploy/proof-gate.d.ts.map +1 -0
  54. package/dist/deploy/proof-gate.js +269 -0
  55. package/dist/deploy/proof-gate.js.map +1 -0
  56. package/dist/deploy/python-float.d.ts +34 -0
  57. package/dist/deploy/python-float.d.ts.map +1 -0
  58. package/dist/deploy/python-float.js +46 -0
  59. package/dist/deploy/python-float.js.map +1 -0
  60. package/dist/deploy/recheck-commands.d.ts +50 -0
  61. package/dist/deploy/recheck-commands.d.ts.map +1 -0
  62. package/dist/deploy/recheck-commands.js +59 -0
  63. package/dist/deploy/recheck-commands.js.map +1 -0
  64. package/dist/deploy/register.d.ts +99 -0
  65. package/dist/deploy/register.d.ts.map +1 -0
  66. package/dist/deploy/register.js +689 -0
  67. package/dist/deploy/register.js.map +1 -0
  68. package/dist/deploy/taxonomy.d.ts +24 -0
  69. package/dist/deploy/taxonomy.d.ts.map +1 -0
  70. package/dist/deploy/taxonomy.js +31 -0
  71. package/dist/deploy/taxonomy.js.map +1 -0
  72. package/dist/index.d.ts.map +1 -1
  73. package/dist/index.js +244 -132
  74. package/dist/index.js.map +1 -1
  75. package/dist/runtime/strategy-registry.d.ts +8 -0
  76. package/dist/runtime/strategy-registry.d.ts.map +1 -1
  77. package/dist/runtime/strategy-registry.js +14 -1
  78. package/dist/runtime/strategy-registry.js.map +1 -1
  79. package/dist/runtime-api/intake/intake-mount.d.ts.map +1 -1
  80. package/dist/runtime-api/intake/intake-mount.js +7 -4
  81. package/dist/runtime-api/intake/intake-mount.js.map +1 -1
  82. package/dist/scanners/implementations/market-regime.d.ts +1 -1
  83. package/dist/senpi/client.d.ts +58 -4
  84. package/dist/senpi/client.d.ts.map +1 -1
  85. package/dist/senpi/client.js +192 -15
  86. package/dist/senpi/client.js.map +1 -1
  87. package/dist/senpi/strategy-status.d.ts +32 -0
  88. package/dist/senpi/strategy-status.d.ts.map +1 -0
  89. package/dist/senpi/strategy-status.js +70 -0
  90. package/dist/senpi/strategy-status.js.map +1 -0
  91. package/dist/senpi/types.d.ts +47 -3
  92. package/dist/senpi/types.d.ts.map +1 -1
  93. package/dist/skills-manager/manifest.js +1 -1
  94. package/dist/skills-manager/skills-manifest.json +1 -1
  95. package/dist/telemetry/metrics.d.ts +23 -0
  96. package/dist/telemetry/metrics.d.ts.map +1 -1
  97. package/dist/telemetry/metrics.js +65 -0
  98. package/dist/telemetry/metrics.js.map +1 -1
  99. package/dist/types/strategy.d.ts +9 -1
  100. package/dist/types/strategy.d.ts.map +1 -1
  101. package/dist/universe/package-universe.d.ts +97 -0
  102. package/dist/universe/package-universe.d.ts.map +1 -0
  103. package/dist/universe/package-universe.js +275 -0
  104. package/dist/universe/package-universe.js.map +1 -0
  105. package/dist/validate/codes.d.ts +20 -0
  106. package/dist/validate/codes.d.ts.map +1 -1
  107. package/dist/validate/codes.js +36 -0
  108. package/dist/validate/codes.js.map +1 -1
  109. package/dist/validate/import-stage.js +5 -5
  110. package/dist/validate/import-stage.js.map +1 -1
  111. package/dist/validate/index.d.ts +1 -1
  112. package/dist/validate/index.d.ts.map +1 -1
  113. package/dist/validate/index.js.map +1 -1
  114. package/dist/validate/install-gate.d.ts.map +1 -1
  115. package/dist/validate/install-gate.js +10 -2
  116. package/dist/validate/install-gate.js.map +1 -1
  117. package/dist/validate/live-instruments.d.ts +44 -0
  118. package/dist/validate/live-instruments.d.ts.map +1 -0
  119. package/dist/validate/live-instruments.js +71 -0
  120. package/dist/validate/live-instruments.js.map +1 -0
  121. package/dist/validate/live-stage.js +5 -5
  122. package/dist/validate/live-stage.js.map +1 -1
  123. package/dist/validate/proof.d.ts +11 -1
  124. package/dist/validate/proof.d.ts.map +1 -1
  125. package/dist/validate/proof.js +7 -6
  126. package/dist/validate/proof.js.map +1 -1
  127. package/dist/validate/recipe-checks.d.ts.map +1 -1
  128. package/dist/validate/recipe-checks.js +6 -4
  129. package/dist/validate/recipe-checks.js.map +1 -1
  130. package/dist/validate/run.d.ts +5 -0
  131. package/dist/validate/run.d.ts.map +1 -1
  132. package/dist/validate/run.js +10 -1
  133. package/dist/validate/run.js.map +1 -1
  134. package/dist/validate/target.d.ts +41 -0
  135. package/dist/validate/target.d.ts.map +1 -1
  136. package/dist/validate/target.js +192 -16
  137. package/dist/validate/target.js.map +1 -1
  138. package/dist/validate/universe-stage.d.ts +71 -0
  139. package/dist/validate/universe-stage.d.ts.map +1 -0
  140. package/dist/validate/universe-stage.js +183 -0
  141. package/dist/validate/universe-stage.js.map +1 -0
  142. package/openclaw.plugin.json +1 -1
  143. package/package.json +1 -1
  144. package/dist/runtime/managed-wallet-guard.d.ts +0 -47
  145. package/dist/runtime/managed-wallet-guard.d.ts.map +0 -1
  146. package/dist/runtime/managed-wallet-guard.js +0 -106
  147. package/dist/runtime/managed-wallet-guard.js.map +0 -1
  148. package/dist/runtime/protection-nudge.d.ts +0 -67
  149. package/dist/runtime/protection-nudge.d.ts.map +0 -1
  150. package/dist/runtime/protection-nudge.js +0 -244
  151. package/dist/runtime/protection-nudge.js.map +0 -1
@@ -24,6 +24,7 @@ Quick commands:
24
24
  senpi guide actions Action types and decision modes
25
25
  senpi guide dsl DSL exit: phases, lock modes, tiers, time-based cuts
26
26
  senpi validate <pkg> Prove a strategy runs before funding it (static → import → live)
27
+ senpi deploy -p <pkg> Take a proven package live: create+fund → install → verified tick
27
28
  senpi scanner Per-scanner health: runs, errors, liveness, "(no signals yet)" flag
28
29
  senpi events Local domain-event log — the trade narrative
29
30
  senpi explain <asset> Stitch one asset's position lifecycle from the event log
@@ -478,11 +479,75 @@ senpi config — Manage CLI preferences (~/.openclaw/senpi-cli.json)
478
479
  config unset <key> Remove a key
479
480
  config reset Clear all preferences
480
481
 
482
+ senpi deploy — Take a strategy package live, end to end (the strategy lifecycle)
483
+ openclaw senpi deploy -p <package-dir> --budget <usd> # start (detached): preflight → create+fund → install → verified tick
484
+ openclaw senpi deploy status # watch until terminal; quotes the verified report
485
+ -p, --path <dir> Strategy package directory (contains strategy.yaml)
486
+ --budget <usd> Total budget across the package's wallets (min $10/wallet)
487
+ --decision-model <model> Required if the package uses decision_mode: llm
488
+ --tick-wait <seconds> Wait for one verified scanner tick (default 120, 0 skips)
489
+ --max-wait <seconds> Wait for wallets to reach ACTIVE (default 150)
490
+ --json Raw JSON output
491
+ deploy returns in ~1s; the job runs detached. Poll
492
+ 'deploy status' until it is terminal. Exit codes:
493
+ 0 live · 2 refused · 3 failed · 4 installed-unobserved ·
494
+ 5 interrupted · 6 pending/running · 1 error. An
495
+ unobserved tick reports as "unobserved", never as success.
496
+ 'deploy' itself uses the same classes at START: 0 job
497
+ started · 2 refused (the message leads with its code, e.g.
498
+ [INVALID_REQUEST]) · 1 internal/transport. So 2 always
499
+ means obey the message; 1 always means the deploy's state
500
+ is unknown — never retry a 2.
501
+ A package no 'senpi validate' has ever proven — or one whose
502
+ files have changed since, or whose proof predates this
503
+ runtime — is refused BEFORE any money moves. The refusal
504
+ leads with [E_VALIDATE_NO_PROOF] /
505
+ [E_VALIDATE_CONTENT_CHANGED] /
506
+ [E_VALIDATE_RUNTIME_VERSION_CHANGED] and names the exact
507
+ validate command for the instance that failed. Two instances
508
+ sharing one runtime directory are refused outright
509
+ ([INVALID_REQUEST]): a proof lives beside its recipe, one
510
+ file per directory, so it cannot speak for both.
511
+ Budget is two-tier: BELOW the $10/wallet floor it refuses
512
+ [E_FUNDS_BELOW_FLOOR]; and when the split leaves a wallet
513
+ under its OWN sizing it DEPLOYS and warns
514
+ [W_BUDGET_BELOW_STRATEGY_MIN] (that sleeve runs degraded —
515
+ fewer slots than authored). [W_BUDGET_UNRESOLVED] = the
516
+ minimum could not be computed and may be understated.
517
+ Neither warn stops a deploy; both judge each wallet against
518
+ its OWN allocation, not the budget against the package
519
+ total (those differ once a sleeve is adopted). To run the
520
+ authored design you must close and redeploy — a re-run at
521
+ a bigger --budget only adopts the wallet it already made.
522
+ Follow the warn's own close command verbatim; it is scoped
523
+ to the short sleeves so adopted wallets are left alone.
524
+ Some reports deliberately carry NO close command (a funded
525
+ wallet with no runtime installed; a funding_share: 0
526
+ sleeve; or when [E_ROLLBACK_INCOMPLETE] owns the cleanup)
527
+ — that is the correct answer there, not a gap to fill:
528
+ never substitute a package-wide close.
529
+ Both of those judge the PLAN. After the wallets exist the
530
+ report also checks the OUTCOME: when the backend lands under
531
+ 90% of what a wallet was asked to be funded, it warns
532
+ [W_BUDGET_PARTIAL_FUND] with both numbers ("funded $60.00 of
533
+ requested $500.00 (12%, short $440.00)"). The strategy is
534
+ still live and the exit code does not move — do NOT close it
535
+ over the shortfall. Quote the FUNDED figure, never the
536
+ requested one. Those figures are AT DEPLOY TIME and the note
537
+ re-renders on every later status read, so re-read the
538
+ current funding (status.py) before moving money, then top up
539
+ via the senpi-deposit-withdraw-transfer skill;
540
+ close-and-redeploy is the destructive fallback. The warn is
541
+ silent wherever a D-6 rollback was attempted — if the create
542
+ step carries [E_ROLLBACK_INCOMPLETE], THAT line owns the
543
+ remediation (reclaim), and it is the one to follow.
544
+ When the backend reports no readable funded amount at all,
545
+ you get [W_BUDGET_FUNDED_UNREADABLE] instead: no percentage,
546
+ no shortfall, no top-up. The funded amount is UNKNOWN, never
547
+ $0.00 — ask the USER to verify what actually landed before
548
+ any money action
549
+
481
550
  senpi runtime — Manage runtimes
482
- runtime create Add a runtime from a YAML file
483
- -p, --path <path> Path to strategy YAML file
484
- -c, --content <yaml> Inline YAML content
485
- --runtime-id <id> Override auto-derived ID
486
551
  runtime delete [id] Remove a runtime
487
552
  --id <runtime_id> Delete by runtime ID
488
553
  --address <wallet> Delete by wallet address
@@ -510,6 +575,12 @@ senpi validate — Prove a strategy can execute BEFORE it is funded
510
575
  command, not the code
511
576
  A full unscoped PASS at live depth writes .senpi-proof.json beside the recipe
512
577
  (content-addressed, no expiry); the deploy gate verifies it via verifyProof().
578
+ That gate REFUSES, before any money moves: [E_VALIDATE_NO_PROOF] nothing here
579
+ was ever observed to run · [E_VALIDATE_CONTENT_CHANGED] the files moved since
580
+ the proof, and it names which · [E_VALIDATE_RUNTIME_VERSION_CHANGED] the proof
581
+ predates the running runtime (the package needs no edit — just re-prove it).
582
+ A proof attests that this content RAN and passed, not that every check at that
583
+ depth produced a verdict — deploy re-checks the live instrument universe itself.
513
584
 
514
585
  senpi status — Lightweight runtime health digest
515
586
  status Overall health + scanners (with degraded count),
@@ -623,8 +694,9 @@ senpi.installRuntime
623
694
  Success: { id, name, wallet }
624
695
  Errors: INVALID_REQUEST (missing YAML, duplicate ID/wallet, or the install gate: a recipe that
625
696
  cannot trade — no margin source, no risk guardrails, missing scanner entrypoint, or an
626
- unresolvable scanner path — is refused with what/why/fix per problem),
627
- UNAVAILABLE (bad YAML, start failure)
697
+ unresolvable scanner path — is refused with what/why/fix per problem, the message
698
+ leading with the bracketed code of the first blocking finding, e.g.
699
+ [E_VALIDATE_UNGATED]), UNAVAILABLE (bad YAML, start failure)
628
700
 
629
701
  senpi.listRuntimes
630
702
  Params: (none)
@@ -1 +1 @@
1
- {"version":3,"file":"guide-commands.js","sourceRoot":"","sources":["../../src/cli/guide-commands.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"guide-commands.js","sourceRoot":"","sources":["../../src/cli/guide-commands.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,aAAa,GACjB,0EAA0E,CAAC;AAE7E,+EAA+E;AAE/E,MAAM,aAAa,GAAG,CAAC,OAAe,EAAE,EAAE,CAAC;qBACtB,OAAO;;;;;;;;;;;;;;;;;;;;;;;;CAwB3B,CAAC,IAAI,EAAE,CAAC;AAET,MAAM,aAAa,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoIrB,CAAC,IAAI,EAAE,CAAC;AAET,MAAM,YAAY,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8CpB,CAAC,IAAI,EAAE,CAAC;AAET,MAAM,QAAQ,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0FhB,CAAC,IAAI,EAAE,CAAC;AAET,MAAM,WAAW,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+GnB,CAAC,IAAI,EAAE,CAAC;AAET,MAAM,YAAY,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwCpB,CAAC,IAAI,EAAE,CAAC;AAET,MAAM,CAAC,MAAM,eAAe,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4R9B,CAAC,IAAI,EAAE,CAAC;AAYT;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CACnC,KAAuB,EACvB,OAAe;IAEf,MAAM,KAAK,GAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAsB;SACvD,WAAW,CAAC,0CAA0C,CAAC,CAAC;IAE3D,KAAK,CAAC,MAAM,CAAC,GAAG,EAAE;QAChB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,aAAa,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;IACtD,CAAC,CAAC,CAAC;IAEF,KAAK,CAAC,OAAO,CAAC,UAAU,CAAsB;SAC5C,WAAW,CAAC,iCAAiC,CAAC;SAC9C,MAAM,CAAC,GAAG,EAAE;QACX,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,aAAa,GAAG,IAAI,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;IAEJ,KAAK,CAAC,OAAO,CAAC,SAAS,CAAsB;SAC3C,WAAW,CAAC,iCAAiC,CAAC;SAC9C,MAAM,CAAC,GAAG,EAAE;QACX,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;IAEJ,KAAK,CAAC,OAAO,CAAC,KAAK,CAAsB;SACvC,WAAW,CAAC,6DAA6D,CAAC;SAC1E,MAAM,CAAC,GAAG,EAAE;QACX,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;IACxC,CAAC,CAAC,CAAC;IAEJ,KAAK,CAAC,OAAO,CAAC,UAAU,CAAsB;SAC5C,WAAW,CAAC,uCAAuC,CAAC;SACpD,MAAM,CAAC,GAAG,EAAE;QACX,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;IAEJ,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAsB;SAC1C,WAAW,CAAC,kCAAkC,CAAC;SAC/C,MAAM,CAAC,GAAG,EAAE;QACX,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEJ,KAAK,CAAC,OAAO,CAAC,SAAS,CAAsB;SAC3C,WAAW,CAAC,kCAAkC,CAAC;SAC/C,MAAM,CAAC,GAAG,EAAE;QACX,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,sBAAsB,OAAO,KAAK,aAAa,IAAI,CAAC,CAAC;IAC5E,CAAC,CAAC,CAAC;AACP,CAAC"}
@@ -24,7 +24,12 @@ export declare function renderEventsDigest(entries: StoredEvent[]): string;
24
24
  export declare function renderExplain(asset: string, entries: StoredEvent[]): string;
25
25
  /** Minimal Commander command type for subcommands and options. */
26
26
  interface CommanderCommand {
27
- command(name: string, desc?: string): CommanderCommand;
27
+ /** Commander 14 accepts either a description string or an options object (e.g. `{ hidden: true }`). */
28
+ command(name: string, descOrOpts?: string | {
29
+ hidden?: boolean;
30
+ }, opts?: {
31
+ hidden?: boolean;
32
+ }): CommanderCommand;
28
33
  description(desc: string): CommanderCommand;
29
34
  argument(name: string, description?: string): CommanderCommand;
30
35
  option(flags: string, description?: string): CommanderCommand;
@@ -1 +1 @@
1
- {"version":3,"file":"senpi-commands.d.ts","sourceRoot":"","sources":["../../src/cli/senpi-commands.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAiBH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAI/D;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE;IAAE,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,gBAAgB,GAAG,OAAO,CAAA;CAAE,GAChF,IAAI,CA4vCN;AAgKD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,cAAc,EAAE,GAAG,MAAM,CAWvE;AAmDD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,MAAM,CASjE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,GAAG,MAAM,CAW3E;AA8HD,kEAAkE;AAClE,UAAU,gBAAgB;IACxB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,gBAAgB,CAAC;IACvD,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,CAAC;IAC5C,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,gBAAgB,CAAC;IAC/D,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,gBAAgB,CAAC;IAC9D,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,gBAAgB,CAAC;IACtE,MAAM,CACJ,EAAE,EACE,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,GACzD,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,GACzE,gBAAgB,CAAC;CACrB"}
1
+ {"version":3,"file":"senpi-commands.d.ts","sourceRoot":"","sources":["../../src/cli/senpi-commands.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAmBH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAI/D;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE;IAAE,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,gBAAgB,GAAG,OAAO,CAAA;CAAE,GAChF,IAAI,CAgxCN;AAyVD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,cAAc,EAAE,GAAG,MAAM,CAWvE;AAmDD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,MAAM,CASjE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,GAAG,MAAM,CAW3E;AA8HD,kEAAkE;AAClE,UAAU,gBAAgB;IACxB,uGAAuG;IACvG,OAAO,CACL,IAAI,EAAE,MAAM,EACZ,UAAU,CAAC,EAAE,MAAM,GAAG;QAAE,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,EAC1C,IAAI,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,GAC1B,gBAAgB,CAAC;IACpB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,CAAC;IAC5C,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,gBAAgB,CAAC;IAC/D,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,gBAAgB,CAAC;IAC9D,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,gBAAgB,CAAC;IACtE,MAAM,CACJ,EAAE,EACE,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,GACzD,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,GACzE,gBAAgB,CAAC;CACrB"}
@@ -11,6 +11,8 @@ import { spawnSync } from "../utils/safe-spawn.js";
11
11
  import path from "node:path";
12
12
  import { registerGuideCommands, CHEATSHEET_TEXT } from "./guide-commands.js";
13
13
  import { registerSenpiConfigCommands } from "./senpi-config-commands.js";
14
+ import { exitCodeForDeploy, renderDeploySnapshot } from "./deploy-render.js";
15
+ import { isRefusalMessage } from "../deploy/taxonomy.js";
14
16
  import { registerValidateCommand } from "./validate-command.js";
15
17
  import { VERSION } from "../version.js";
16
18
  /**
@@ -27,14 +29,18 @@ export function registerSenpiCli(program) {
27
29
  });
28
30
  registerSenpiConfigCommands(senpi);
29
31
  registerValidateCommand(senpi);
32
+ registerDeployCommands(senpi);
30
33
  const runtime = senpi.command("runtime");
31
34
  runtime.description("Manage runtimes");
32
35
  runtime
33
- .command("create")
34
- .description("Add a runtime from a YAML file or paste YAML; the gateway validates and runs it")
36
+ // Demoted, not deleted: `senpi deploy` is the supported path (funds preflight + attribution +
37
+ // verified tick). Hidden keeps it out of help/routing while deploy.py keeps working.
38
+ .command("create", { hidden: true })
39
+ .description("Internal — prefer `senpi deploy` (funds preflight + attribution + verified tick). Direct create skips those gates.")
35
40
  .option("-p, --path <path>", "Path to your strategy YAML file")
36
41
  .option("-c, --content <content>", "Paste YAML content directly")
37
42
  .option("--runtime-id <id>", "Name for this runtime (default: from file name or content)")
43
+ .option("--runtime-yaml-dir <dir>", "Directory to resolve relative scanner paths against (content installs only)")
38
44
  .action(async (opts) => {
39
45
  const hasPath = typeof opts.path === "string" && opts.path.length > 0;
40
46
  const hasContent = typeof opts.content === "string";
@@ -75,7 +81,13 @@ export function registerSenpiCli(program) {
75
81
  // scanner's relative `path: ./` and the scaffold can import scan.py. We
76
82
  // send content (not path) so remote gateways work, but that drops the
77
83
  // directory; this hint restores it for FS-sharing (local/box) setups.
78
- ...(hasPath ? { runtimeYamlDir: path.dirname(path.resolve(process.cwd(), opts.path)) } : {}),
84
+ // An explicit --runtime-yaml-dir wins: it is the only way a content install can name
85
+ // the directory its relative scanner paths resolve against (otherwise refused).
86
+ ...(typeof opts.runtimeYamlDir === "string" && opts.runtimeYamlDir.length > 0
87
+ ? { runtimeYamlDir: path.resolve(process.cwd(), opts.runtimeYamlDir) }
88
+ : hasPath
89
+ ? { runtimeYamlDir: path.dirname(path.resolve(process.cwd(), opts.path)) }
90
+ : {}),
79
91
  ...(typeof opts.runtimeId === "string" && opts.runtimeId.length > 0 ? { id: opts.runtimeId } : {}),
80
92
  };
81
93
  // Runtime create is a SLOW synchronous call: the handler starts a long-lived
@@ -600,7 +612,9 @@ export function registerSenpiCli(program) {
600
612
  const params = {};
601
613
  if (typeof opts.runtime === "string" && opts.runtime.length > 0)
602
614
  params.runtimeId = opts.runtime;
603
- const data = callGatewayJson("senpi.getHealthStatus", params);
615
+ // Same class as the deploy verbs' null-data path: a health surface that exits 0 after
616
+ // reading no health is the one thing it exists not to do.
617
+ const data = callGatewayJson("senpi.getHealthStatus", params, (cause) => reportUnreadableGateway("senpi status", cause, " No health was read."));
604
618
  if (!data) {
605
619
  return;
606
620
  }
@@ -1101,11 +1115,151 @@ function fmtMetricNum(n) {
1101
1115
  return String(n);
1102
1116
  return n.toFixed(2);
1103
1117
  }
1104
- function callGatewayJson(method, params) {
1118
+ /**
1119
+ * The `senpi deploy` verb pair — the agent-facing surface for the whole go-live
1120
+ * path. There is no `cancel` (D-4): undeploying a strategy is closing it, and a
1121
+ * wedged job frees its own slot at the deadline. `deploy` starts a DETACHED job and returns in ~1s; monitoring is explicit
1122
+ * polling of `deploy status`. The `senpi.deploy.*` gateway methods behind these
1123
+ * verbs are internal transport and are deliberately not documented anywhere an
1124
+ * agent reads.
1125
+ */
1126
+ function registerDeployCommands(senpi) {
1127
+ const deploy = senpi
1128
+ .command("deploy")
1129
+ .description("Deploy a strategy package end-to-end (detached): funds preflight, wallet create+fund, install, one observed tick. " +
1130
+ "Starts the job and returns — watch with `senpi deploy status`. " +
1131
+ "Start exit codes: 0 job started · 2 refused (the message leads with the code) · 1 internal/transport error")
1132
+ // NOT `requiredOption`: Commander checks mandatory options by walking the ancestor chain from
1133
+ // the leaf command, so a mandatory `-p` on this parent makes `deploy status` fail before
1134
+ // dispatch ("required option '-p, --path <dir>' not specified"). The start action carries its
1135
+ // own explicit guard below, so nothing is lost.
1136
+ .option("-p, --path <dir>", "Strategy package directory (contains strategy.yaml)")
1137
+ .option("--budget <usd>", "Total budget in USD across the package's wallets (min $10/wallet)")
1138
+ .option("--decision-model <model>", "Model for llm-decision actions (required if the package uses decision_mode: llm)")
1139
+ .option("--tick-wait <seconds>", "How long the job waits to observe one verified scanner tick (default 120, 0 skips)")
1140
+ .option("--max-wait <seconds>", "How long the job waits for wallets to reach ACTIVE (default 150)")
1141
+ .option("--json", "Print the raw start response JSON")
1142
+ .action(async (opts) => {
1143
+ // Every numeric flag is validated the same way. A silently-dropped NaN would run the job
1144
+ // with the default window while the agent believes its own value took effect.
1145
+ // These local guards are REFUSALS, not usage errors: nothing was started, and re-running
1146
+ // the same command can only refuse again. They carry the taxonomy code and exit 2 like
1147
+ // every other start-time refusal — reported as 1 the newly-taught start map reads them as
1148
+ // "transport error, state unknown", whose natural handling is to poll `deploy status`.
1149
+ const numeric = (flag, raw, example) => {
1150
+ if (raw === undefined)
1151
+ return undefined;
1152
+ const value = Number(raw);
1153
+ if (!Number.isFinite(value)) {
1154
+ console.error(`[INVALID_REQUEST] ${flag} must be a number, e.g. ${flag} ${example}. Nothing was started.`);
1155
+ return null;
1156
+ }
1157
+ return value;
1158
+ };
1159
+ const budget = numeric("--budget", opts.budget, "500");
1160
+ const tickWaitSec = numeric("--tick-wait", opts.tickWait, "120");
1161
+ const maxWaitSec = numeric("--max-wait", opts.maxWait, "150");
1162
+ if (budget === null || tickWaitSec === null || maxWaitSec === null) {
1163
+ process.exitCode = DEPLOY_REFUSED_EXIT;
1164
+ return;
1165
+ }
1166
+ if (!opts.path) {
1167
+ // This guard fires BEFORE the gateway is called, so the gateway's own coded
1168
+ // missing-package-dir refusal is unreachable from the CLI — this surface has to carry
1169
+ // it verbatim rather than degrade it to a codeless usage error.
1170
+ console.error("[INVALID_REQUEST] A strategy package directory is required. Nothing was started. " +
1171
+ "Run: senpi deploy -p <package-dir> --budget <usd>");
1172
+ process.exitCode = DEPLOY_REFUSED_EXIT;
1173
+ return;
1174
+ }
1175
+ const params = {
1176
+ packageDir: path.resolve(process.cwd(), opts.path),
1177
+ ...(budget !== undefined ? { budget } : {}),
1178
+ ...(opts.decisionModel ? { decisionModel: opts.decisionModel } : {}),
1179
+ ...(tickWaitSec !== undefined ? { tickWaitSec } : {}),
1180
+ ...(maxWaitSec !== undefined ? { maxWaitSec } : {}),
1181
+ };
1182
+ const data = callGatewayJson("senpi.deploy.start", params, (cause) => reportUnreadableGateway("senpi deploy", cause, DEPLOY_STATE_UNKNOWN_HINT));
1183
+ if (!data)
1184
+ return;
1185
+ if (data.ok === false) {
1186
+ const err = data.error;
1187
+ const message = err?.message ?? "Deploy start failed.";
1188
+ console.error(message);
1189
+ // D-12 all the way down: `2 = refused`, `1 = internal/transport`. Every start-time gate
1190
+ // (the [INVALID_REQUEST] family — no DSL exit, missing decision model/env, scanner
1191
+ // `enabled` key, non-positive budget, missing/unreadable package — plus
1192
+ // [E_DEPLOY_IN_PROGRESS]) is a refusal, and the taught surfaces tell agents to branch on
1193
+ // the code. Reported as 1 they read as "internal/transport error", whose natural
1194
+ // handling is retry rather than obey — a retried no-stop-loss refusal is the whole bug.
1195
+ process.exitCode = isRefusalMessage(message) ? DEPLOY_REFUSED_EXIT : 1;
1196
+ return;
1197
+ }
1198
+ const started = (data.payload ?? data);
1199
+ if (opts.json) {
1200
+ console.log(JSON.stringify(started, null, 2));
1201
+ return;
1202
+ }
1203
+ console.log(`deploy ${started.deployId} started — phase: ${started.phase}`);
1204
+ console.log(" watch: openclaw senpi deploy status");
1205
+ });
1206
+ deploy
1207
+ .command("status [deployId]")
1208
+ .description("Show the deploy job: phase while running; the full verified report once terminal. " +
1209
+ "Exit codes: 0 live · 2 refused · 3 failed · 4 installed-unobserved · 5 interrupted · " +
1210
+ "6 pending/still running · 1 error")
1211
+ .option("--json", "Print the raw snapshot JSON")
1212
+ .action(async (deployId, opts) => {
1213
+ const data = callGatewayJson("senpi.deploy.status", deployId ? { deployId } : {}, (cause) => reportUnreadableGateway("senpi deploy status", cause, DEPLOY_STATE_UNKNOWN_HINT));
1214
+ if (!data)
1215
+ return;
1216
+ if (data.ok === false) {
1217
+ console.error(data.error?.message ?? "Status failed.");
1218
+ process.exitCode = 1;
1219
+ return;
1220
+ }
1221
+ const snap = (data.payload ?? data);
1222
+ // D-12: the exit code applies on the --json path too, so a JSON consumer gets the code AND
1223
+ // the payload rather than having to choose.
1224
+ process.exitCode = exitCodeForDeploy(snap);
1225
+ if (opts?.json) {
1226
+ console.log(JSON.stringify(snap, null, 2));
1227
+ return;
1228
+ }
1229
+ renderDeploySnapshot(snap);
1230
+ });
1231
+ }
1232
+ /** D-12: `2 = refused`. Named so the start action's guards and its gateway branch cannot drift. */
1233
+ const DEPLOY_REFUSED_EXIT = 2;
1234
+ /** Appended to a deploy verb's unreadable-gateway message: the one surface that can still answer. */
1235
+ const DEPLOY_STATE_UNKNOWN_HINT = " This says nothing about the deploy itself — if the gateway is up, ask `senpi deploy status` " +
1236
+ "for the job's real state, and check the gateway log if that keeps failing.";
1237
+ /**
1238
+ * The answer for a `gateway call` that produced nothing this CLI could read.
1239
+ *
1240
+ * Always 1 (internal/transport), never 0 — under D-12 a 0 from a deploy verb means `live`, and this
1241
+ * path learned nothing about the job. Deliberately overrides whatever exit code the subprocess
1242
+ * carried, so a `gateway call` that happened to exit 3 cannot masquerade as D-12 `failed`. It never
1243
+ * claims the deploy failed either: a started job may be running right now.
1244
+ */
1245
+ function reportUnreadableGateway(verb, cause, followUp = "") {
1246
+ process.exitCode = 1;
1247
+ console.error((cause === "dispatch"
1248
+ ? `${verb}: the \`gateway call\` subprocess failed, so nothing was read back — exit 1 (transport/internal).`
1249
+ : `${verb}: the gateway answered but its response could not be read — exit 1 (transport/internal).`) +
1250
+ followUp);
1251
+ }
1252
+ /**
1253
+ * `onFailure` fires on either null path, naming which one. Callers that pass it own the exit code
1254
+ * and the message; callers that omit it keep the historical behaviour (the subprocess's own code
1255
+ * plus the gateway-not-running tip, and a silent return on unparseable stdout).
1256
+ */
1257
+ function callGatewayJson(method, params, onFailure) {
1105
1258
  const result = spawnSync(process.argv[0], [process.argv[1], "gateway", "call", method, "--json", "--params", JSON.stringify(params)], { stdio: ["inherit", "pipe", "inherit"], cwd: process.cwd(), encoding: "utf8" });
1106
1259
  if (result.status !== 0) {
1107
1260
  process.exitCode = result.status ?? 1;
1108
1261
  console.error("\nTip: If the gateway is not running, start it with: openclaw gateway run");
1262
+ onFailure?.("dispatch");
1109
1263
  return null;
1110
1264
  }
1111
1265
  try {
@@ -1113,6 +1267,11 @@ function callGatewayJson(method, params) {
1113
1267
  }
1114
1268
  catch {
1115
1269
  process.stdout.write(result.stdout ?? "");
1270
+ // Unparseable stdout means this command did NOT do what its output implies, so it must never
1271
+ // exit 0. Set here rather than per-verb so the swallow cannot re-seed itself on the next
1272
+ // caller; verbs that pass `onFailure` refine the message, not the code.
1273
+ process.exitCode = 1;
1274
+ onFailure?.("unreadable");
1116
1275
  return null;
1117
1276
  }
1118
1277
  }