@senpi/runtime 3.0.59 → 3.0.63

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 +244 -8
  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":"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"}
@@ -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,CAmxCN;AAuaD;;;;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,14 +612,19 @@ 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
  }
607
621
  if (data.ok === false) {
608
622
  const err = data.error;
609
- console.error(err?.message ?? "Request failed.");
610
- process.exitCode = 1;
623
+ const message = err?.message ?? "Request failed.";
624
+ console.error(message);
625
+ // Same reading as the deploy verbs, and the same reason: a coded refusal is something to
626
+ // obey, not to retry. Kept here so this branch and the subprocess-level one below agree.
627
+ process.exitCode = isRefusalMessage(message) ? DEPLOY_REFUSED_EXIT : 1;
611
628
  return;
612
629
  }
613
630
  const payload = (data.payload ?? data);
@@ -1101,11 +1118,225 @@ function fmtMetricNum(n) {
1101
1118
  return String(n);
1102
1119
  return n.toFixed(2);
1103
1120
  }
1104
- function callGatewayJson(method, params) {
1105
- 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" });
1121
+ /**
1122
+ * The `senpi deploy` verb pair — the agent-facing surface for the whole go-live
1123
+ * path. There is no `cancel` (D-4): undeploying a strategy is closing it, and a
1124
+ * wedged job frees its own slot at the deadline. `deploy` starts a DETACHED job and returns in ~1s; monitoring is explicit
1125
+ * polling of `deploy status`. The `senpi.deploy.*` gateway methods behind these
1126
+ * verbs are internal transport and are deliberately not documented anywhere an
1127
+ * agent reads.
1128
+ */
1129
+ function registerDeployCommands(senpi) {
1130
+ const deploy = senpi
1131
+ .command("deploy")
1132
+ .description("Deploy a strategy package end-to-end (detached): funds preflight, wallet create+fund, install, one observed tick. " +
1133
+ "Starts the job and returns — watch with `senpi deploy status`. " +
1134
+ "Start exit codes: 0 job started · 2 refused (the message leads with the code) · 1 internal/transport error")
1135
+ // NOT `requiredOption`: Commander checks mandatory options by walking the ancestor chain from
1136
+ // the leaf command, so a mandatory `-p` on this parent makes `deploy status` fail before
1137
+ // dispatch ("required option '-p, --path <dir>' not specified"). The start action carries its
1138
+ // own explicit guard below, so nothing is lost.
1139
+ .option("-p, --path <dir>", "Strategy package directory (contains strategy.yaml)")
1140
+ .option("--budget <usd>", "Total budget in USD across the package's wallets (min $10/wallet)")
1141
+ .option("--decision-model <model>", "Model for llm-decision actions (required if the package uses decision_mode: llm)")
1142
+ .option("--tick-wait <seconds>", "How long the job waits to observe one verified scanner tick (default 120, 0 skips)")
1143
+ .option("--max-wait <seconds>", "How long the job waits for wallets to reach ACTIVE (default 150)")
1144
+ .option("--json", "Print the raw start response JSON")
1145
+ .action(async (opts) => {
1146
+ // Every numeric flag is validated the same way. A silently-dropped NaN would run the job
1147
+ // with the default window while the agent believes its own value took effect.
1148
+ // These local guards are REFUSALS, not usage errors: nothing was started, and re-running
1149
+ // the same command can only refuse again. They carry the taxonomy code and exit 2 like
1150
+ // every other start-time refusal — reported as 1 the newly-taught start map reads them as
1151
+ // "transport error, state unknown", whose natural handling is to poll `deploy status`.
1152
+ const numeric = (flag, raw, example) => {
1153
+ if (raw === undefined)
1154
+ return undefined;
1155
+ const value = Number(raw);
1156
+ if (!Number.isFinite(value)) {
1157
+ console.error(`[INVALID_REQUEST] ${flag} must be a number, e.g. ${flag} ${example}. Nothing was started.`);
1158
+ return null;
1159
+ }
1160
+ return value;
1161
+ };
1162
+ const budget = numeric("--budget", opts.budget, "500");
1163
+ const tickWaitSec = numeric("--tick-wait", opts.tickWait, "120");
1164
+ const maxWaitSec = numeric("--max-wait", opts.maxWait, "150");
1165
+ if (budget === null || tickWaitSec === null || maxWaitSec === null) {
1166
+ process.exitCode = DEPLOY_REFUSED_EXIT;
1167
+ return;
1168
+ }
1169
+ if (!opts.path) {
1170
+ // This guard fires BEFORE the gateway is called, so the gateway's own coded
1171
+ // missing-package-dir refusal is unreachable from the CLI — this surface has to carry
1172
+ // it verbatim rather than degrade it to a codeless usage error.
1173
+ console.error("[INVALID_REQUEST] A strategy package directory is required. Nothing was started. " +
1174
+ "Run: senpi deploy -p <package-dir> --budget <usd>");
1175
+ process.exitCode = DEPLOY_REFUSED_EXIT;
1176
+ return;
1177
+ }
1178
+ const params = {
1179
+ packageDir: path.resolve(process.cwd(), opts.path),
1180
+ ...(budget !== undefined ? { budget } : {}),
1181
+ ...(opts.decisionModel ? { decisionModel: opts.decisionModel } : {}),
1182
+ ...(tickWaitSec !== undefined ? { tickWaitSec } : {}),
1183
+ ...(maxWaitSec !== undefined ? { maxWaitSec } : {}),
1184
+ };
1185
+ const data = callGatewayJson("senpi.deploy.start", params, (cause) => reportUnreadableGateway("senpi deploy", cause, DEPLOY_STATE_UNKNOWN_HINT));
1186
+ if (!data)
1187
+ return;
1188
+ if (data.ok === false) {
1189
+ const err = data.error;
1190
+ const message = err?.message ?? "Deploy start failed.";
1191
+ console.error(message);
1192
+ // D-12 all the way down: `2 = refused`, `1 = internal/transport`. Every start-time gate
1193
+ // (the [INVALID_REQUEST] family — no DSL exit, missing decision model/env, scanner
1194
+ // `enabled` key, non-positive budget, missing/unreadable package — plus
1195
+ // [E_DEPLOY_IN_PROGRESS]) is a refusal, and the taught surfaces tell agents to branch on
1196
+ // the code. Reported as 1 they read as "internal/transport error", whose natural
1197
+ // handling is retry rather than obey — a retried no-stop-loss refusal is the whole bug.
1198
+ process.exitCode = isRefusalMessage(message) ? DEPLOY_REFUSED_EXIT : 1;
1199
+ return;
1200
+ }
1201
+ const started = (data.payload ?? data);
1202
+ if (opts.json) {
1203
+ console.log(JSON.stringify(started, null, 2));
1204
+ return;
1205
+ }
1206
+ console.log(`deploy ${started.deployId} started — phase: ${started.phase}`);
1207
+ console.log(" watch: openclaw senpi deploy status");
1208
+ });
1209
+ deploy
1210
+ .command("status [deployId]")
1211
+ .description("Show the deploy job: phase while running; the full verified report once terminal. " +
1212
+ "Exit codes: 0 live · 2 refused · 3 failed · 4 installed-unobserved · 5 interrupted · " +
1213
+ "6 pending/still running · 1 error")
1214
+ .option("--json", "Print the raw snapshot JSON")
1215
+ .action(async (deployId, opts) => {
1216
+ const data = callGatewayJson("senpi.deploy.status", deployId ? { deployId } : {}, (cause) => reportUnreadableGateway("senpi deploy status", cause, DEPLOY_STATUS_STATE_UNKNOWN_HINT));
1217
+ if (!data)
1218
+ return;
1219
+ if (data.ok === false) {
1220
+ const message = data.error?.message ?? "Status failed.";
1221
+ console.error(message);
1222
+ // Read the same way the start verb reads its own gateway error, and for the same reason:
1223
+ // `[NOT_FOUND] No deploy has been started on this agent` is a fact, not a fault. Exit 1
1224
+ // would tell an agent to retry a read whose answer cannot change until a deploy is started.
1225
+ process.exitCode = isRefusalMessage(message) ? DEPLOY_REFUSED_EXIT : 1;
1226
+ return;
1227
+ }
1228
+ const snap = (data.payload ?? data);
1229
+ // D-12: the exit code applies on the --json path too, so a JSON consumer gets the code AND
1230
+ // the payload rather than having to choose.
1231
+ process.exitCode = exitCodeForDeploy(snap);
1232
+ if (opts?.json) {
1233
+ console.log(JSON.stringify(snap, null, 2));
1234
+ return;
1235
+ }
1236
+ renderDeploySnapshot(snap);
1237
+ });
1238
+ }
1239
+ /**
1240
+ * D-12: `2 = refused`. Named so the start action's guards, its gateway branch, and the one place
1241
+ * every verb's gateway call is classified (`callGatewayJson`) cannot drift. `2` means the same thing
1242
+ * wherever it comes from — a deliberate no, to obey rather than retry — so `senpi status` reports a
1243
+ * coded refusal with it too, even though only the deploy verbs publish the full map.
1244
+ */
1245
+ const DEPLOY_REFUSED_EXIT = 2;
1246
+ /** Appended to a deploy verb's unreadable-gateway message: the one surface that can still answer. */
1247
+ const DEPLOY_STATE_UNKNOWN_HINT = " This says nothing about the deploy itself — if the gateway is up, ask `senpi deploy status` " +
1248
+ "for the job's real state, and check the gateway log if that keeps failing.";
1249
+ /**
1250
+ * The `deploy status` variant of the same teaching.
1251
+ *
1252
+ * `DEPLOY_STATE_UNKNOWN_HINT` names `senpi deploy status` as the surface that can still answer,
1253
+ * which is a loop when the verb the reader just ran IS status. Same content — the deploy is
1254
+ * untouched by this failure, and the job may be running right now — routed to the thing that
1255
+ * actually has to change first.
1256
+ */
1257
+ const DEPLOY_STATUS_STATE_UNKNOWN_HINT = " This says nothing about the deploy itself — a started job may be running right now. Start the " +
1258
+ "gateway if it is down (`openclaw gateway run`), check the gateway log, then re-read.";
1259
+ /**
1260
+ * Did the gateway ANSWER this failed `gateway call`, with a refusal?
1261
+ *
1262
+ * `openclaw gateway call` collapses two different worlds into one process status: it catches
1263
+ * everything its RPC throws, prints `Gateway call failed: <error>` to stderr, and exits **1** —
1264
+ * whether the gateway was never reachable or the gateway answered `respond(false, undefined, error)`
1265
+ * and the client rethrew that answer as a `GatewayClientRequestError`. There is no second signal to
1266
+ * read: stdout carries the payload only on success, and the exit code is a constant.
1267
+ *
1268
+ * What DOES separate them is this plugin's own message taxonomy. Every refusal these gateway methods
1269
+ * produce leads with its bracketed code — `[INVALID_REQUEST]`, `[E_UNIVERSE_NOT_LIVE]`,
1270
+ * `[E_NO_DSL_EXIT]`, `[NOT_FOUND]` — and no transport failure does: those read `gateway closed
1271
+ * (1006 …)`, `gateway timeout after 10000ms`, `connect ECONNREFUSED …`, `gateway … requires
1272
+ * credentials`. So the classifier is the taxonomy, not openclaw's error class names (which are its
1273
+ * internals and free to change), and the reader is the ONE reader — `isRefusalMessage`, which is
1274
+ * built on `extractTaxonomyCode` and already knows the advisory `W_` family is not a refusal.
1275
+ *
1276
+ * Every bracket is tried, not just the first: the code sits behind a `Gateway call failed: ` label
1277
+ * and an error name, and a colourised or otherwise decorated stderr can put other brackets ahead
1278
+ * of it.
1279
+ */
1280
+ function gatewayAnsweredRefusal(stderr) {
1281
+ for (let i = stderr.indexOf("["); i >= 0; i = stderr.indexOf("[", i + 1)) {
1282
+ if (isRefusalMessage(stderr.slice(i)))
1283
+ return true;
1284
+ }
1285
+ return false;
1286
+ }
1287
+ /**
1288
+ * The answer for a `gateway call` that produced nothing this CLI could read.
1289
+ *
1290
+ * Always 1 (internal/transport), never 0 — under D-12 a 0 from a deploy verb means `live`, and this
1291
+ * path learned nothing about the job. Deliberately overrides whatever exit code the subprocess
1292
+ * carried, so a `gateway call` that happened to exit 3 cannot masquerade as D-12 `failed`. It never
1293
+ * claims the deploy failed either: a started job may be running right now.
1294
+ */
1295
+ function reportUnreadableGateway(verb, cause, followUp = "") {
1296
+ process.exitCode = 1;
1297
+ console.error((cause === "dispatch"
1298
+ ? `${verb}: the \`gateway call\` subprocess failed, so nothing was read back — exit 1 (transport/internal).`
1299
+ : `${verb}: the gateway answered but its response could not be read — exit 1 (transport/internal).`) +
1300
+ followUp);
1301
+ }
1302
+ /**
1303
+ * `onFailure` fires on either null path, naming which one. Callers that pass it own the exit code
1304
+ * and the message; callers that omit it keep the historical behaviour (the subprocess's own code
1305
+ * plus the gateway-not-running tip, and a silent return on unparseable stdout).
1306
+ *
1307
+ * A gateway-answered REFUSAL takes neither path: it is classified and exited here, so no caller can
1308
+ * print an unreadable-gateway message over an answer the user has already read.
1309
+ */
1310
+ function callGatewayJson(method, params, onFailure) {
1311
+ const result = spawnSync(process.argv[0], [process.argv[1], "gateway", "call", method, "--json", "--params", JSON.stringify(params)],
1312
+ // stderr is PIPED, where it used to be inherited, for one reason: a gateway REFUSAL arrives
1313
+ // only there, and a stderr this process never sees is a stderr it can contradict. The child
1314
+ // still owns every character of it — see the verbatim re-emit directly below.
1315
+ { stdio: ["inherit", "pipe", "pipe"], cwd: process.cwd(), encoding: "utf8" });
1316
+ const stderr = typeof result.stderr === "string" ? result.stderr : "";
1317
+ // FIRST, before anything this CLI prints, which is exactly where an inherited stderr put it —
1318
+ // the child has already exited, so its whole output is ordered ahead of ours. Unmodified: no
1319
+ // reformatting, no truncation, no prefix. The only edit is a terminating newline when the child
1320
+ // did not write one, so the next line cannot be glued onto the last word of a refusal.
1321
+ if (stderr.length > 0) {
1322
+ process.stderr.write(stderr.endsWith("\n") ? stderr : `${stderr}\n`);
1323
+ }
1106
1324
  if (result.status !== 0) {
1325
+ if (gatewayAnsweredRefusal(stderr)) {
1326
+ // The gateway answered, and it refused. D-12: `2 = refused` — the code agents branch on to
1327
+ // OBEY rather than retry. Reported as 1 (internal/transport) these read as "try again", and
1328
+ // a delisted instrument, a package with no DSL exit, or a wallet another package's stamp
1329
+ // claims will refuse identically forever.
1330
+ //
1331
+ // Nothing further is printed. The refusal above is the answer; the unreadable-gateway text
1332
+ // ("nothing was read back", "if the gateway is not running") would contradict it one line
1333
+ // later, which is the defect this branch exists to end.
1334
+ process.exitCode = DEPLOY_REFUSED_EXIT;
1335
+ return null;
1336
+ }
1107
1337
  process.exitCode = result.status ?? 1;
1108
1338
  console.error("\nTip: If the gateway is not running, start it with: openclaw gateway run");
1339
+ onFailure?.("dispatch");
1109
1340
  return null;
1110
1341
  }
1111
1342
  try {
@@ -1113,6 +1344,11 @@ function callGatewayJson(method, params) {
1113
1344
  }
1114
1345
  catch {
1115
1346
  process.stdout.write(result.stdout ?? "");
1347
+ // Unparseable stdout means this command did NOT do what its output implies, so it must never
1348
+ // exit 0. Set here rather than per-verb so the swallow cannot re-seed itself on the next
1349
+ // caller; verbs that pass `onFailure` refine the message, not the code.
1350
+ process.exitCode = 1;
1351
+ onFailure?.("unreadable");
1116
1352
  return null;
1117
1353
  }
1118
1354
  }