@monoes/monomindcli 2.16.12 → 2.16.14
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/.claude/agents/core/coder.md +1 -1
- package/.claude/agents/engineering/engineering-devops-automator.md +1 -1
- package/.claude/agents/github/repo-architect.md +1 -1
- package/.claude/agents/testing/production-validator.md +1 -1
- package/.claude/agents/testing/testing-tool-evaluator.md +1 -1
- package/.claude/commands/github/pr-manager.md +1 -1
- package/.claude/helpers/pick-rank.cjs +34 -4
- package/.claude/skills/monolean-audit/SKILL.md +1 -1
- package/README.md +1 -1
- package/dist/src/commands/cleanup-data.d.ts +30 -0
- package/dist/src/commands/cleanup-data.d.ts.map +1 -0
- package/dist/src/commands/cleanup-data.js +138 -0
- package/dist/src/commands/cleanup-data.js.map +1 -0
- package/dist/src/commands/cleanup-scratch.d.ts +43 -0
- package/dist/src/commands/cleanup-scratch.d.ts.map +1 -0
- package/dist/src/commands/cleanup-scratch.js +125 -0
- package/dist/src/commands/cleanup-scratch.js.map +1 -0
- package/dist/src/commands/cleanup.d.ts +2 -39
- package/dist/src/commands/cleanup.d.ts.map +1 -1
- package/dist/src/commands/cleanup.js +5 -247
- package/dist/src/commands/cleanup.js.map +1 -1
- package/dist/src/commands/doctor-gitignore-checks.d.ts +10 -0
- package/dist/src/commands/doctor-gitignore-checks.d.ts.map +1 -0
- package/dist/src/commands/doctor-gitignore-checks.js +141 -0
- package/dist/src/commands/doctor-gitignore-checks.js.map +1 -0
- package/dist/src/commands/doctor-helpers-checks.d.ts +10 -0
- package/dist/src/commands/doctor-helpers-checks.d.ts.map +1 -0
- package/dist/src/commands/doctor-helpers-checks.js +292 -0
- package/dist/src/commands/doctor-helpers-checks.js.map +1 -0
- package/dist/src/commands/doctor-memory-checks.d.ts +47 -0
- package/dist/src/commands/doctor-memory-checks.d.ts.map +1 -0
- package/dist/src/commands/doctor-memory-checks.js +298 -0
- package/dist/src/commands/doctor-memory-checks.js.map +1 -0
- package/dist/src/commands/doctor-monograph-checks.d.ts +9 -0
- package/dist/src/commands/doctor-monograph-checks.d.ts.map +1 -0
- package/dist/src/commands/doctor-monograph-checks.js +331 -0
- package/dist/src/commands/doctor-monograph-checks.js.map +1 -0
- package/dist/src/commands/doctor-project-checks.d.ts +7 -66
- package/dist/src/commands/doctor-project-checks.d.ts.map +1 -1
- package/dist/src/commands/doctor-project-checks.js +10 -1331
- package/dist/src/commands/doctor-project-checks.js.map +1 -1
- package/dist/src/commands/doctor-routing-checks.d.ts +10 -0
- package/dist/src/commands/doctor-routing-checks.d.ts.map +1 -0
- package/dist/src/commands/doctor-routing-checks.js +194 -0
- package/dist/src/commands/doctor-routing-checks.js.map +1 -0
- package/dist/src/commands/doctor-worker-checks.d.ts +17 -0
- package/dist/src/commands/doctor-worker-checks.d.ts.map +1 -0
- package/dist/src/commands/doctor-worker-checks.js +122 -0
- package/dist/src/commands/doctor-worker-checks.js.map +1 -0
- package/dist/src/commands/hooks-routing-commands.js +1 -1
- package/dist/src/commands/hooks-routing-commands.js.map +1 -1
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +6 -15
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/org-control.d.ts +117 -0
- package/dist/src/commands/org-control.d.ts.map +1 -0
- package/dist/src/commands/org-control.js +274 -0
- package/dist/src/commands/org-control.js.map +1 -0
- package/dist/src/commands/org-lifecycle.d.ts +7 -0
- package/dist/src/commands/org-lifecycle.d.ts.map +1 -0
- package/dist/src/commands/org-lifecycle.js +298 -0
- package/dist/src/commands/org-lifecycle.js.map +1 -0
- package/dist/src/commands/org-manage.d.ts +7 -0
- package/dist/src/commands/org-manage.d.ts.map +1 -0
- package/dist/src/commands/org-manage.js +357 -0
- package/dist/src/commands/org-manage.js.map +1 -0
- package/dist/src/commands/org-memory-command.d.ts +3 -0
- package/dist/src/commands/org-memory-command.d.ts.map +1 -0
- package/dist/src/commands/org-memory-command.js +178 -0
- package/dist/src/commands/org-memory-command.js.map +1 -0
- package/dist/src/commands/org-observe-approvals.d.ts +8 -0
- package/dist/src/commands/org-observe-approvals.d.ts.map +1 -0
- package/dist/src/commands/org-observe-approvals.js +211 -0
- package/dist/src/commands/org-observe-approvals.js.map +1 -0
- package/dist/src/commands/org-observe-checkpoint.d.ts +26 -0
- package/dist/src/commands/org-observe-checkpoint.d.ts.map +1 -0
- package/dist/src/commands/org-observe-checkpoint.js +180 -0
- package/dist/src/commands/org-observe-checkpoint.js.map +1 -0
- package/dist/src/commands/org-observe-config.d.ts +8 -0
- package/dist/src/commands/org-observe-config.d.ts.map +1 -0
- package/dist/src/commands/org-observe-config.js +160 -0
- package/dist/src/commands/org-observe-config.js.map +1 -0
- package/dist/src/commands/org-observe-gates.d.ts +4 -0
- package/dist/src/commands/org-observe-gates.d.ts.map +1 -0
- package/dist/src/commands/org-observe-gates.js +182 -0
- package/dist/src/commands/org-observe-gates.js.map +1 -0
- package/dist/src/commands/org-observe-inbox.d.ts +18 -0
- package/dist/src/commands/org-observe-inbox.d.ts.map +1 -0
- package/dist/src/commands/org-observe-inbox.js +154 -0
- package/dist/src/commands/org-observe-inbox.js.map +1 -0
- package/dist/src/commands/org-observe-logs.d.ts +31 -0
- package/dist/src/commands/org-observe-logs.d.ts.map +1 -0
- package/dist/src/commands/org-observe-logs.js +272 -0
- package/dist/src/commands/org-observe-logs.js.map +1 -0
- package/dist/src/commands/org-observe-questions.d.ts +8 -0
- package/dist/src/commands/org-observe-questions.d.ts.map +1 -0
- package/dist/src/commands/org-observe-questions.js +201 -0
- package/dist/src/commands/org-observe-questions.js.map +1 -0
- package/dist/src/commands/org-observe-report.d.ts +8 -0
- package/dist/src/commands/org-observe-report.d.ts.map +1 -0
- package/dist/src/commands/org-observe-report.js +366 -0
- package/dist/src/commands/org-observe-report.js.map +1 -0
- package/dist/src/commands/org-observe-shared.d.ts +20 -0
- package/dist/src/commands/org-observe-shared.d.ts.map +1 -0
- package/dist/src/commands/org-observe-shared.js +55 -0
- package/dist/src/commands/org-observe-shared.js.map +1 -0
- package/dist/src/commands/org-observe.d.ts +9 -101
- package/dist/src/commands/org-observe.d.ts.map +1 -1
- package/dist/src/commands/org-observe.js +12 -1675
- package/dist/src/commands/org-observe.js.map +1 -1
- package/dist/src/commands/org-poll.d.ts +57 -0
- package/dist/src/commands/org-poll.d.ts.map +1 -0
- package/dist/src/commands/org-poll.js +194 -0
- package/dist/src/commands/org-poll.js.map +1 -0
- package/dist/src/commands/org-run.d.ts +3 -0
- package/dist/src/commands/org-run.d.ts.map +1 -0
- package/dist/src/commands/org-run.js +449 -0
- package/dist/src/commands/org-run.js.map +1 -0
- package/dist/src/commands/org-serve.d.ts +19 -0
- package/dist/src/commands/org-serve.d.ts.map +1 -0
- package/dist/src/commands/org-serve.js +395 -0
- package/dist/src/commands/org-serve.js.map +1 -0
- package/dist/src/commands/org-subcommands-config.d.ts +8 -0
- package/dist/src/commands/org-subcommands-config.d.ts.map +1 -0
- package/dist/src/commands/org-subcommands-config.js +93 -0
- package/dist/src/commands/org-subcommands-config.js.map +1 -0
- package/dist/src/commands/org-subcommands-hil.d.ts +11 -0
- package/dist/src/commands/org-subcommands-hil.d.ts.map +1 -0
- package/dist/src/commands/org-subcommands-hil.js +216 -0
- package/dist/src/commands/org-subcommands-hil.js.map +1 -0
- package/dist/src/commands/org-subcommands-observe.d.ts +12 -0
- package/dist/src/commands/org-subcommands-observe.d.ts.map +1 -0
- package/dist/src/commands/org-subcommands-observe.js +249 -0
- package/dist/src/commands/org-subcommands-observe.js.map +1 -0
- package/dist/src/commands/org-subcommands-runtime.d.ts +12 -0
- package/dist/src/commands/org-subcommands-runtime.d.ts.map +1 -0
- package/dist/src/commands/org-subcommands-runtime.js +163 -0
- package/dist/src/commands/org-subcommands-runtime.js.map +1 -0
- package/dist/src/commands/org.d.ts +3 -151
- package/dist/src/commands/org.d.ts.map +1 -1
- package/dist/src/commands/org.js +43 -2735
- package/dist/src/commands/org.js.map +1 -1
- package/dist/src/index.js +2 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/init/never-commit.d.ts +3 -3
- package/dist/src/init/types.d.ts +1 -1
- package/dist/src/init/types.js +1 -1
- package/dist/src/knowledge/document-store.js +1 -1
- package/dist/src/knowledge/document-store.js.map +1 -1
- package/dist/src/mcp-tools/hooks-edit-command.d.ts +10 -0
- package/dist/src/mcp-tools/hooks-edit-command.d.ts.map +1 -0
- package/dist/src/mcp-tools/hooks-edit-command.js +247 -0
- package/dist/src/mcp-tools/hooks-edit-command.js.map +1 -0
- package/dist/src/mcp-tools/hooks-learning.d.ts +9 -0
- package/dist/src/mcp-tools/hooks-learning.d.ts.map +1 -0
- package/dist/src/mcp-tools/hooks-learning.js +408 -0
- package/dist/src/mcp-tools/hooks-learning.js.map +1 -0
- package/dist/src/mcp-tools/hooks-metrics-list.d.ts +8 -0
- package/dist/src/mcp-tools/hooks-metrics-list.d.ts.map +1 -0
- package/dist/src/mcp-tools/hooks-metrics-list.js +205 -0
- package/dist/src/mcp-tools/hooks-metrics-list.js.map +1 -0
- package/dist/src/mcp-tools/hooks-route.d.ts +12 -0
- package/dist/src/mcp-tools/hooks-route.d.ts.map +1 -0
- package/dist/src/mcp-tools/hooks-route.js +251 -0
- package/dist/src/mcp-tools/hooks-route.js.map +1 -0
- package/dist/src/mcp-tools/hooks-routing.d.ts +6 -27
- package/dist/src/mcp-tools/hooks-routing.d.ts.map +1 -1
- package/dist/src/mcp-tools/hooks-routing.js +6 -1727
- package/dist/src/mcp-tools/hooks-routing.js.map +1 -1
- package/dist/src/mcp-tools/hooks-session.d.ts +9 -0
- package/dist/src/mcp-tools/hooks-session.d.ts.map +1 -0
- package/dist/src/mcp-tools/hooks-session.js +243 -0
- package/dist/src/mcp-tools/hooks-session.js.map +1 -0
- package/dist/src/mcp-tools/hooks-task.d.ts +17 -0
- package/dist/src/mcp-tools/hooks-task.d.ts.map +1 -0
- package/dist/src/mcp-tools/hooks-task.js +427 -0
- package/dist/src/mcp-tools/hooks-task.js.map +1 -0
- package/dist/src/mcp-tools/hooks-tools.d.ts +2 -1
- package/dist/src/mcp-tools/hooks-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/hooks-tools.js +2 -1
- package/dist/src/mcp-tools/hooks-tools.js.map +1 -1
- package/dist/src/mcp-tools/memory-tools.js +1 -1
- package/dist/src/mcp-tools/memory-tools.js.map +1 -1
- package/dist/src/memory/bm25-index.d.ts +2 -2
- package/dist/src/memory/bm25-index.js +2 -2
- package/dist/src/memory/embedding-operations.js +2 -2
- package/dist/src/memory/embedding-operations.js.map +1 -1
- package/dist/src/memory/entity-name-key.d.ts +1 -1
- package/dist/src/memory/entity-name-key.js +1 -1
- package/dist/src/memory/hnsw-operations.d.ts +1 -1
- package/dist/src/memory/hnsw-operations.js +1 -1
- package/dist/src/memory/memory-bridge-backend.d.ts +43 -0
- package/dist/src/memory/memory-bridge-backend.d.ts.map +1 -0
- package/dist/src/memory/memory-bridge-backend.js +363 -0
- package/dist/src/memory/memory-bridge-backend.js.map +1 -0
- package/dist/src/memory/memory-bridge-core.d.ts +42 -0
- package/dist/src/memory/memory-bridge-core.d.ts.map +1 -0
- package/dist/src/memory/memory-bridge-core.js +126 -0
- package/dist/src/memory/memory-bridge-core.js.map +1 -0
- package/dist/src/memory/memory-bridge-entries.d.ts +131 -0
- package/dist/src/memory/memory-bridge-entries.d.ts.map +1 -0
- package/dist/src/memory/memory-bridge-entries.js +227 -0
- package/dist/src/memory/memory-bridge-entries.js.map +1 -0
- package/dist/src/memory/memory-bridge-feedback.d.ts +60 -0
- package/dist/src/memory/memory-bridge-feedback.d.ts.map +1 -0
- package/dist/src/memory/memory-bridge-feedback.js +118 -0
- package/dist/src/memory/memory-bridge-feedback.js.map +1 -0
- package/dist/src/memory/memory-bridge-paths.d.ts +123 -0
- package/dist/src/memory/memory-bridge-paths.d.ts.map +1 -0
- package/dist/src/memory/memory-bridge-paths.js +335 -0
- package/dist/src/memory/memory-bridge-paths.js.map +1 -0
- package/dist/src/memory/memory-bridge-search.d.ts +48 -0
- package/dist/src/memory/memory-bridge-search.d.ts.map +1 -0
- package/dist/src/memory/memory-bridge-search.js +383 -0
- package/dist/src/memory/memory-bridge-search.js.map +1 -0
- package/dist/src/memory/memory-bridge-sessions.d.ts +38 -0
- package/dist/src/memory/memory-bridge-sessions.d.ts.map +1 -0
- package/dist/src/memory/memory-bridge-sessions.js +109 -0
- package/dist/src/memory/memory-bridge-sessions.js.map +1 -0
- package/dist/src/memory/memory-bridge-store.d.ts +58 -0
- package/dist/src/memory/memory-bridge-store.d.ts.map +1 -0
- package/dist/src/memory/memory-bridge-store.js +201 -0
- package/dist/src/memory/memory-bridge-store.js.map +1 -0
- package/dist/src/memory/memory-bridge.d.ts +10 -438
- package/dist/src/memory/memory-bridge.d.ts.map +1 -1
- package/dist/src/memory/memory-bridge.js +14 -1787
- package/dist/src/memory/memory-bridge.js.map +1 -1
- package/dist/src/memory/memory-kg-claims.d.ts +52 -0
- package/dist/src/memory/memory-kg-claims.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-claims.js +99 -0
- package/dist/src/memory/memory-kg-claims.js.map +1 -0
- package/dist/src/memory/memory-kg-index.d.ts +86 -0
- package/dist/src/memory/memory-kg-index.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-index.js +256 -0
- package/dist/src/memory/memory-kg-index.js.map +1 -0
- package/dist/src/memory/memory-kg-ingest.d.ts +44 -0
- package/dist/src/memory/memory-kg-ingest.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-ingest.js +361 -0
- package/dist/src/memory/memory-kg-ingest.js.map +1 -0
- package/dist/src/memory/memory-kg-model.d.ts +168 -0
- package/dist/src/memory/memory-kg-model.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-model.js +158 -0
- package/dist/src/memory/memory-kg-model.js.map +1 -0
- package/dist/src/memory/memory-kg-names.d.ts +64 -0
- package/dist/src/memory/memory-kg-names.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-names.js +187 -0
- package/dist/src/memory/memory-kg-names.js.map +1 -0
- package/dist/src/memory/memory-kg-promote.d.ts +79 -0
- package/dist/src/memory/memory-kg-promote.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-promote.js +248 -0
- package/dist/src/memory/memory-kg-promote.js.map +1 -0
- package/dist/src/memory/memory-kg-rebuild.d.ts +77 -0
- package/dist/src/memory/memory-kg-rebuild.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-rebuild.js +299 -0
- package/dist/src/memory/memory-kg-rebuild.js.map +1 -0
- package/dist/src/memory/memory-kg-rollback.d.ts +47 -0
- package/dist/src/memory/memory-kg-rollback.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-rollback.js +193 -0
- package/dist/src/memory/memory-kg-rollback.js.map +1 -0
- package/dist/src/memory/memory-kg-rules.d.ts +64 -0
- package/dist/src/memory/memory-kg-rules.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-rules.js +251 -0
- package/dist/src/memory/memory-kg-rules.js.map +1 -0
- package/dist/src/memory/memory-kg-scan.d.ts +118 -0
- package/dist/src/memory/memory-kg-scan.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-scan.js +209 -0
- package/dist/src/memory/memory-kg-scan.js.map +1 -0
- package/dist/src/memory/memory-kg-search.d.ts +80 -0
- package/dist/src/memory/memory-kg-search.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-search.js +239 -0
- package/dist/src/memory/memory-kg-search.js.map +1 -0
- package/dist/src/memory/memory-kg.d.ts +19 -547
- package/dist/src/memory/memory-kg.d.ts.map +1 -1
- package/dist/src/memory/memory-kg.js +10 -2389
- package/dist/src/memory/memory-kg.js.map +1 -1
- package/dist/src/orgrt/daemon-types.d.ts +196 -0
- package/dist/src/orgrt/daemon-types.d.ts.map +1 -0
- package/dist/src/orgrt/daemon-types.js +38 -0
- package/dist/src/orgrt/daemon-types.js.map +1 -0
- package/dist/src/orgrt/daemon.d.ts +70 -300
- package/dist/src/orgrt/daemon.d.ts.map +1 -1
- package/dist/src/orgrt/daemon.js +41 -2536
- package/dist/src/orgrt/daemon.js.map +1 -1
- package/dist/src/orgrt/idle-watchdog.d.ts +39 -0
- package/dist/src/orgrt/idle-watchdog.d.ts.map +1 -0
- package/dist/src/orgrt/idle-watchdog.js +289 -0
- package/dist/src/orgrt/idle-watchdog.js.map +1 -0
- package/dist/src/orgrt/org-reload.d.ts +7 -0
- package/dist/src/orgrt/org-reload.d.ts.map +1 -0
- package/dist/src/orgrt/org-reload.js +111 -0
- package/dist/src/orgrt/org-reload.js.map +1 -0
- package/dist/src/orgrt/org-start-steps.d.ts +21 -0
- package/dist/src/orgrt/org-start-steps.d.ts.map +1 -0
- package/dist/src/orgrt/org-start-steps.js +305 -0
- package/dist/src/orgrt/org-start-steps.js.map +1 -0
- package/dist/src/orgrt/org-start.d.ts +7 -0
- package/dist/src/orgrt/org-start.d.ts.map +1 -0
- package/dist/src/orgrt/org-start.js +435 -0
- package/dist/src/orgrt/org-start.js.map +1 -0
- package/dist/src/orgrt/org-state-file.d.ts +8 -0
- package/dist/src/orgrt/org-state-file.d.ts.map +1 -0
- package/dist/src/orgrt/org-state-file.js +116 -0
- package/dist/src/orgrt/org-state-file.js.map +1 -0
- package/dist/src/orgrt/org-stop.d.ts +6 -0
- package/dist/src/orgrt/org-stop.d.ts.map +1 -0
- package/dist/src/orgrt/org-stop.js +332 -0
- package/dist/src/orgrt/org-stop.js.map +1 -0
- package/dist/src/orgrt/org-tools.d.ts +11 -0
- package/dist/src/orgrt/org-tools.d.ts.map +1 -0
- package/dist/src/orgrt/org-tools.js +305 -0
- package/dist/src/orgrt/org-tools.js.map +1 -0
- package/dist/src/orgrt/role-incarnation.d.ts +18 -0
- package/dist/src/orgrt/role-incarnation.d.ts.map +1 -0
- package/dist/src/orgrt/role-incarnation.js +374 -0
- package/dist/src/orgrt/role-incarnation.js.map +1 -0
- package/dist/src/orgrt/role-respawn.d.ts +7 -0
- package/dist/src/orgrt/role-respawn.d.ts.map +1 -0
- package/dist/src/orgrt/role-respawn.js +318 -0
- package/dist/src/orgrt/role-respawn.js.map +1 -0
- package/dist/src/orgrt/role-session-opts.d.ts +389 -0
- package/dist/src/orgrt/role-session-opts.d.ts.map +1 -0
- package/dist/src/orgrt/role-session-opts.js +289 -0
- package/dist/src/orgrt/role-session-opts.js.map +1 -0
- package/dist/src/orgrt/runner-resolve.d.ts +43 -0
- package/dist/src/orgrt/runner-resolve.d.ts.map +1 -0
- package/dist/src/orgrt/runner-resolve.js +77 -0
- package/dist/src/orgrt/runner-resolve.js.map +1 -0
- package/dist/src/orgrt/session-gate.d.ts +34 -0
- package/dist/src/orgrt/session-gate.d.ts.map +1 -0
- package/dist/src/orgrt/session-gate.js +74 -0
- package/dist/src/orgrt/session-gate.js.map +1 -0
- package/dist/src/orgrt/session-prompt.d.ts +31 -0
- package/dist/src/orgrt/session-prompt.d.ts.map +1 -0
- package/dist/src/orgrt/session-prompt.js +115 -0
- package/dist/src/orgrt/session-prompt.js.map +1 -0
- package/dist/src/orgrt/session-run.d.ts +18 -0
- package/dist/src/orgrt/session-run.d.ts.map +1 -0
- package/dist/src/orgrt/session-run.js +429 -0
- package/dist/src/orgrt/session-run.js.map +1 -0
- package/dist/src/orgrt/session-stream.d.ts +21 -0
- package/dist/src/orgrt/session-stream.d.ts.map +1 -0
- package/dist/src/orgrt/session-stream.js +173 -0
- package/dist/src/orgrt/session-stream.js.map +1 -0
- package/dist/src/orgrt/session-types.d.ts +219 -0
- package/dist/src/orgrt/session-types.d.ts.map +1 -0
- package/dist/src/orgrt/session-types.js +2 -0
- package/dist/src/orgrt/session-types.js.map +1 -0
- package/dist/src/orgrt/session-usage.d.ts +29 -0
- package/dist/src/orgrt/session-usage.d.ts.map +1 -0
- package/dist/src/orgrt/session-usage.js +64 -0
- package/dist/src/orgrt/session-usage.js.map +1 -0
- package/dist/src/orgrt/session.d.ts +5 -281
- package/dist/src/orgrt/session.d.ts.map +1 -1
- package/dist/src/orgrt/session.js +8 -1117
- package/dist/src/orgrt/session.js.map +1 -1
- package/dist/src/ui/org-hil.mjs +1 -1
- package/dist/src/ui/routes-org-agents.mjs +318 -0
- package/dist/src/ui/routes-org-approvals.mjs +133 -0
- package/dist/src/ui/routes-org-config.mjs +459 -0
- package/dist/src/ui/routes-org-control.mjs +300 -0
- package/dist/src/ui/routes-org-files.mjs +172 -0
- package/dist/src/ui/routes-org-helpers.mjs +84 -0
- package/dist/src/ui/routes-org-knowledge.mjs +262 -0
- package/dist/src/ui/routes-org-lifecycle.mjs +224 -0
- package/dist/src/ui/routes-org-live.mjs +278 -0
- package/dist/src/ui/routes-org-mastermind.mjs +346 -0
- package/dist/src/ui/routes-org-planning.mjs +398 -0
- package/dist/src/ui/routes-org-runs.mjs +430 -0
- package/dist/src/ui/routes-org-status.mjs +342 -0
- package/dist/src/ui/routes-org-workflows.mjs +212 -0
- package/dist/src/ui/routes-org.mjs +26 -3847
- package/dist/src/ui/server.mjs +1 -1
- package/dist/src/utils/native-error.d.ts +1 -1
- package/dist/src/utils/native-error.js +1 -1
- package/dist/src/utils/paths.d.ts +2 -1
- package/dist/src/utils/paths.d.ts.map +1 -1
- package/dist/src/utils/paths.js +2 -1
- package/dist/src/utils/paths.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/org-skills/adversarial-reviewer/SKILL.md +1 -1
- package/org-skills/board-deck-builder/SKILL.md +1 -1
- package/org-skills/code-reviewer/SKILL.md +1 -1
- package/org-skills/debugging-wizard/SKILL.md +1 -1
- package/org-skills/scientific-thinking-literature-review/SKILL.md +1 -1
- package/org-skills/scientific-thinking-scholar-evaluation/SKILL.md +1 -1
- package/package.json +5 -5
|
@@ -1,1677 +1,14 @@
|
|
|
1
1
|
// packages/@monomind/cli/src/commands/org-observe.ts
|
|
2
|
-
// Read-side org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
import { ORG_DIR, OrgDefSchema } from '../orgrt/types.js';
|
|
15
|
-
import { output } from '../output.js';
|
|
16
|
-
import { listOrgConfigFiles, validateOrgName } from './org.js';
|
|
17
|
-
const log = (text) => {
|
|
18
|
-
console.log(text);
|
|
19
|
-
};
|
|
20
|
-
// ─── Agent Exec Protocol JSON output (doc/agent-exec-protocol.md §7) ────────
|
|
21
|
-
// Org observe commands emit machine JSON under the global `--format json`
|
|
22
|
-
// flag: one JSON object on stdout, diagnostics on stderr only. Envelope for
|
|
23
|
-
// lists {v, org, items}; singletons are bare objects carrying v.
|
|
24
|
-
/** True when this invocation asked for protocol JSON output. */
|
|
25
|
-
export const orgJson = (ctx) => ctx.flags.format === 'json';
|
|
26
|
-
/** Print one protocol JSON payload on stdout (compact — one line, NDJSON-safe
|
|
27
|
-
* for line-oriented callers) and return a success result. */
|
|
28
|
-
export const printOrgJson = (payload) => {
|
|
29
|
-
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
30
|
-
return { success: true, data: payload };
|
|
31
|
-
};
|
|
32
|
-
/** M5: `--by <resolver>` — who resolved a decision (default `human`). Returns
|
|
33
|
-
* an error string for an invalid value. */
|
|
34
|
-
const resolverFlag = async (ctx) => {
|
|
35
|
-
const raw = ctx.flags.by;
|
|
36
|
-
if (raw === undefined || raw === false)
|
|
37
|
-
return { ok: true, by: 'human' };
|
|
38
|
-
const { normalizeResolver } = await import('../orgrt/approvals.js');
|
|
39
|
-
const by = normalizeResolver(raw);
|
|
40
|
-
if (!by)
|
|
41
|
-
return { ok: false, message: '--by must be 1-128 printable characters' };
|
|
42
|
-
return { ok: true, by };
|
|
43
|
-
};
|
|
44
|
-
/** Validate org config(s) against OrgDefSchema — the exact parse `org run`/`org serve`
|
|
45
|
-
* perform — plus the structural invariants the runtime assumes but the schema can't
|
|
46
|
-
* express (single root role, resolvable reports_to, unique ids, parseable schedule). */
|
|
47
|
-
export const validateAction = async (ctx) => {
|
|
48
|
-
const orgsDir = join(ctx.cwd || process.cwd(), ORG_DIR);
|
|
49
|
-
let files;
|
|
50
|
-
if (ctx.args[0]) {
|
|
51
|
-
const validated = validateOrgName(ctx.args[0]);
|
|
52
|
-
if (!validated.ok)
|
|
53
|
-
return validated.result;
|
|
54
|
-
files = [`${validated.name}.json`];
|
|
55
|
-
}
|
|
56
|
-
else {
|
|
57
|
-
if (!existsSync(orgsDir))
|
|
58
|
-
return {
|
|
59
|
-
success: false,
|
|
60
|
-
message: 'no orgs directory — create an org first with /mastermind:createorg',
|
|
61
|
-
};
|
|
62
|
-
files = listOrgConfigFiles(orgsDir);
|
|
63
|
-
if (!files.length)
|
|
64
|
-
return { success: false, message: 'no org configs found' };
|
|
65
|
-
}
|
|
66
|
-
let failed = 0;
|
|
67
|
-
for (const f of files) {
|
|
68
|
-
const stem = f.replace(/\.json$/, '');
|
|
69
|
-
const path = join(orgsDir, f);
|
|
70
|
-
const errors = [];
|
|
71
|
-
const warnings = [];
|
|
72
|
-
if (!existsSync(path)) {
|
|
73
|
-
log(output.error(`${stem}: not found (${path})`));
|
|
74
|
-
failed++;
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
try {
|
|
78
|
-
const parsedDef = OrgDefSchema.parse(JSON.parse(readFileSync(path, 'utf8')));
|
|
79
|
-
const bp = resolveOrgDefBlueprints(parsedDef, ctx.cwd || process.cwd());
|
|
80
|
-
const def = bp.def;
|
|
81
|
-
errors.push(...bp.errors);
|
|
82
|
-
for (const n of bp.notes)
|
|
83
|
-
log(output.info(`${stem}: ${n}`));
|
|
84
|
-
errors.push(...checkOrgStructure(def));
|
|
85
|
-
// ADR-O001 D8: a cost tier that can't resolve a model for a role's
|
|
86
|
-
// provider is a config error, not a runtime fallback — surface it here
|
|
87
|
-
// as well as at daemon start, so it's caught before a run is attempted.
|
|
88
|
-
const { validateCostTiers } = await import('../orgrt/cost-tier.js');
|
|
89
|
-
errors.push(...validateCostTiers(def));
|
|
90
|
-
// S3: a typo'd placeholder would reach the prompt verbatim.
|
|
91
|
-
const { unknownPromptVarErrors } = await import('../orgrt/prompt-vars.js');
|
|
92
|
-
errors.push(...unknownPromptVarErrors(def));
|
|
93
|
-
// ADR-O001 D7: catalog size (>15 is an error, <5 a warning) and every
|
|
94
|
-
// loadout's skills/file must resolve — same checks as daemon start.
|
|
95
|
-
const { validateLoadouts } = await import('../orgrt/loadouts.js');
|
|
96
|
-
const loadoutFindings = validateLoadouts(def, ctx.cwd || process.cwd());
|
|
97
|
-
errors.push(...loadoutFindings.errors);
|
|
98
|
-
warnings.push(...loadoutFindings.warnings);
|
|
99
|
-
const { validateRoleSkills } = await import('../orgrt/skill-library.js');
|
|
100
|
-
errors.push(...def.roles.flatMap((r) => validateRoleSkills(r, ctx.cwd || process.cwd())));
|
|
101
|
-
// #258: roles whose policy.git won't have the OS sandbox behind it here
|
|
102
|
-
const gitFindings = gitEnforcementFindings(def);
|
|
103
|
-
errors.push(...gitFindings.errors);
|
|
104
|
-
warnings.push(...gitFindings.warnings);
|
|
105
|
-
if (def.name !== stem)
|
|
106
|
-
warnings.push(`def.name "${def.name}" differs from filename — the runtime addresses this org as "${stem}"`);
|
|
107
|
-
}
|
|
108
|
-
catch (err) {
|
|
109
|
-
errors.push(err instanceof Error ? err.message : String(err));
|
|
110
|
-
}
|
|
111
|
-
for (const w of warnings)
|
|
112
|
-
log(output.warning(`${stem}: ${w}`));
|
|
113
|
-
if (errors.length) {
|
|
114
|
-
failed++;
|
|
115
|
-
for (const e of errors)
|
|
116
|
-
log(output.error(`${stem}: ${e}`));
|
|
117
|
-
}
|
|
118
|
-
else {
|
|
119
|
-
log(output.success(`${stem}: valid${warnings.length ? ` (${warnings.length} warning(s))` : ''}`));
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
return failed
|
|
123
|
-
? { success: false, message: `${failed} of ${files.length} org config(s) failed validation` }
|
|
124
|
-
: { success: true, message: `${files.length} org config(s) valid` };
|
|
125
|
-
};
|
|
126
|
-
/** M2: ids of the org's endpoint roles (automations, not agents) — excluded
|
|
127
|
-
* from the costs/report/flow role tables. Unreadable config → none. */
|
|
128
|
-
const endpointRoleIds = (cwd, name) => {
|
|
129
|
-
try {
|
|
130
|
-
const raw = JSON.parse(readFileSync(join(cwd, ORG_DIR, `${name}.json`), 'utf8'));
|
|
131
|
-
return new Set((Array.isArray(raw.roles) ? raw.roles : [])
|
|
132
|
-
.filter((r) => r?.kind === 'endpoint' && typeof r.id === 'string')
|
|
133
|
-
.map((r) => r.id));
|
|
134
|
-
}
|
|
135
|
-
catch {
|
|
136
|
-
return new Set();
|
|
137
|
-
}
|
|
138
|
-
};
|
|
139
|
-
// Run ids are joined into filesystem paths — enforce the daemon's own id shape
|
|
140
|
-
// so a crafted --run can't traverse out of the org directory (same reason the
|
|
141
|
-
// org-name guard exists).
|
|
142
|
-
const RUN_ID_RE = /^run-[A-Za-z0-9-]+$/;
|
|
143
|
-
const resolveRun = (cwd, name, runFlag) => {
|
|
144
|
-
if (typeof runFlag === 'string' && runFlag)
|
|
145
|
-
return RUN_ID_RE.test(runFlag) ? runFlag : null;
|
|
146
|
-
return listRunDirs(cwd, name)[0] ?? null;
|
|
147
|
-
};
|
|
148
|
-
/** `org logs <name> [--run id] [--role r] [--filter-tool t] [--filter-role r] [--tools-only] [--audit-filter allow|deny] [--follow]` — formatted bus.jsonl tail. */
|
|
149
|
-
export const logsAction = async (ctx, name) => {
|
|
150
|
-
const run = resolveRun(ctx.cwd, name, ctx.flags.run);
|
|
151
|
-
if (!run)
|
|
152
|
-
return {
|
|
153
|
-
success: false,
|
|
154
|
-
message: `no runs found for org ${name} — start one with: monomind org run ${name}`,
|
|
155
|
-
};
|
|
156
|
-
const file = join(ctx.cwd, ORG_DIR, name, run, 'bus.jsonl');
|
|
157
|
-
const roleFilter = typeof ctx.flags.role === 'string' ? ctx.flags.role : null;
|
|
158
|
-
const filterTool = typeof ctx.flags['filter-tool'] === 'string' ? ctx.flags['filter-tool'] : null;
|
|
159
|
-
const filterRole = typeof ctx.flags['filter-role'] === 'string' ? ctx.flags['filter-role'] : null;
|
|
160
|
-
const auditFilter = typeof ctx.flags['audit-filter'] === 'string' ? ctx.flags['audit-filter'] : null;
|
|
161
|
-
const toolsOnly = ctx.flags['tools-only'] === true;
|
|
162
|
-
// Protocol JSON mode: full filtered event array, no live tail (§7.2 — the
|
|
163
|
-
// streaming form is `org events --ndjson --follow`).
|
|
164
|
-
if (orgJson(ctx)) {
|
|
165
|
-
if (ctx.flags.follow === true)
|
|
166
|
-
return { success: false, message: 'json output cannot --follow — use: org events --ndjson' };
|
|
167
|
-
const items = [];
|
|
168
|
-
const accept = (e) => (!toolsOnly || e.type === 'tool') &&
|
|
169
|
-
(!roleFilter || e.from === roleFilter || e.to === roleFilter) &&
|
|
170
|
-
(!filterTool || e.tool === filterTool) &&
|
|
171
|
-
(!filterRole || e.from === filterRole) &&
|
|
172
|
-
(!auditFilter || e.type !== 'tool' || e.decision === auditFilter);
|
|
173
|
-
if (existsSync(file)) {
|
|
174
|
-
for (const line of readFileSync(file, 'utf8').split('\n').filter(Boolean)) {
|
|
175
|
-
try {
|
|
176
|
-
const e = JSON.parse(line);
|
|
177
|
-
if (accept(e))
|
|
178
|
-
items.push(e);
|
|
179
|
-
}
|
|
180
|
-
catch {
|
|
181
|
-
/* skip corrupt interior lines — same policy as the tail drain */
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
return printOrgJson({ v: 1, org: name, run, items });
|
|
186
|
-
}
|
|
187
|
-
const show = (e) => {
|
|
188
|
-
if (toolsOnly && e.type !== 'tool')
|
|
189
|
-
return;
|
|
190
|
-
if (roleFilter && e.from !== roleFilter && e.to !== roleFilter)
|
|
191
|
-
return;
|
|
192
|
-
if (filterTool && e.tool !== filterTool)
|
|
193
|
-
return;
|
|
194
|
-
if (filterRole && e.from !== filterRole)
|
|
195
|
-
return;
|
|
196
|
-
if (auditFilter && e.type === 'tool' && e.decision !== auditFilter)
|
|
197
|
-
return;
|
|
198
|
-
log(formatEvent(e));
|
|
199
|
-
};
|
|
200
|
-
log(output.info(`org ${name} — ${run}${roleFilter ? ` (role: ${roleFilter})` : ''}${filterTool ? ` (tool: ${filterTool})` : ''}${filterRole ? ` (filter-role: ${filterRole})` : ''}${auditFilter ? ` (audit-filter: ${auditFilter})` : ''}`));
|
|
201
|
-
let seenLines = 0;
|
|
202
|
-
const drain = () => {
|
|
203
|
-
if (!existsSync(file))
|
|
204
|
-
return;
|
|
205
|
-
const lines = readFileSync(file, 'utf8').split('\n').filter(Boolean);
|
|
206
|
-
for (let i = seenLines; i < lines.length; i++) {
|
|
207
|
-
try {
|
|
208
|
-
show(JSON.parse(lines[i]));
|
|
209
|
-
seenLines = i + 1;
|
|
210
|
-
}
|
|
211
|
-
catch {
|
|
212
|
-
// Only the FINAL line can be a partial mid-append write worth
|
|
213
|
-
// retrying; a corrupt interior line would otherwise stall the tail
|
|
214
|
-
// forever — skip it and keep going.
|
|
215
|
-
if (i === lines.length - 1)
|
|
216
|
-
break;
|
|
217
|
-
seenLines = i + 1;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
};
|
|
221
|
-
drain();
|
|
222
|
-
if (ctx.flags.follow !== true)
|
|
223
|
-
return { success: true };
|
|
224
|
-
log(output.info('following — Ctrl-C to stop'));
|
|
225
|
-
await new Promise((resolve) => {
|
|
226
|
-
const iv = setInterval(drain, 500);
|
|
227
|
-
process.once('SIGINT', () => {
|
|
228
|
-
clearInterval(iv);
|
|
229
|
-
resolve();
|
|
230
|
-
});
|
|
231
|
-
process.once('SIGTERM', () => {
|
|
232
|
-
clearInterval(iv);
|
|
233
|
-
resolve();
|
|
234
|
-
});
|
|
235
|
-
});
|
|
236
|
-
return { success: true };
|
|
237
|
-
};
|
|
238
|
-
/**
|
|
239
|
-
* `org watch <name> <role> [--verbose] [--stats]` — live-tail one role's
|
|
240
|
-
* assistant chat text.
|
|
241
|
-
*
|
|
242
|
-
* Every runner (Claude included — this isn't specific to the subprocess CLI
|
|
243
|
-
* runners) funnels through session.ts's shared message loop, which emits
|
|
244
|
-
* each assistant-text chunk onto the bus as a `chat` event. This command is
|
|
245
|
-
* just `logsAction` pre-filtered to that event type + role and formatted as
|
|
246
|
-
* a plain transcript instead of the full annotated event line — a friendlier
|
|
247
|
-
* front door onto infrastructure that already exists and already covers
|
|
248
|
-
* every runtime uniformly. For the fuller event stream (tool calls, audit
|
|
249
|
-
* decisions) use `org logs <name> --role <role> --follow` directly.
|
|
250
|
-
*
|
|
251
|
-
* --verbose additionally interleaves that role's `status` events (session
|
|
252
|
-
* start/end, restart/crash/backoff, state changes) into the transcript, so
|
|
253
|
-
* a human watching sees WHY a role went quiet instead of just silence.
|
|
254
|
-
* --stats prints a running token/cost line off that role's `usage` events
|
|
255
|
-
* (emitted per turn by session.ts, same as --verbose: already-existing bus
|
|
256
|
-
* data, no new instrumentation).
|
|
257
|
-
*/
|
|
258
|
-
export const watchAction = async (ctx, name) => {
|
|
259
|
-
const role = ctx.args[1];
|
|
260
|
-
if (!role)
|
|
261
|
-
return { success: false, message: 'usage: monomind org watch <org> <role>' };
|
|
262
|
-
const run = resolveRun(ctx.cwd, name, ctx.flags.run);
|
|
263
|
-
if (!run)
|
|
264
|
-
return {
|
|
265
|
-
success: false,
|
|
266
|
-
message: `no runs found for org ${name} — start one with: monomind org run ${name}`,
|
|
267
|
-
};
|
|
268
|
-
const file = join(ctx.cwd, ORG_DIR, name, run, 'bus.jsonl');
|
|
269
|
-
const verbose = ctx.flags.verbose === true;
|
|
270
|
-
const stats = ctx.flags.stats === true;
|
|
271
|
-
let totalTokens = 0;
|
|
272
|
-
let totalCostUsd = 0;
|
|
273
|
-
const show = (e) => {
|
|
274
|
-
if (e.from !== role)
|
|
275
|
-
return;
|
|
276
|
-
if (e.type === 'chat') {
|
|
277
|
-
log(`${output.info(`${role}:`)} ${e.msg ?? ''}`);
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
if (verbose && e.type === 'status') {
|
|
281
|
-
log(output.warning(`[${e.reason ?? 'status'}] ${e.msg ?? ''}`));
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
if (stats && e.type === 'usage') {
|
|
285
|
-
const tokens = typeof e.data?.tokens === 'number' ? e.data.tokens : 0;
|
|
286
|
-
const costDelta = typeof e.data?.cost_usd === 'number' ? e.data.cost_usd : 0;
|
|
287
|
-
totalTokens += tokens;
|
|
288
|
-
totalCostUsd += costDelta;
|
|
289
|
-
log(output.info(`[stats] +${tokens} tokens (total ${totalTokens}) · +$${costDelta.toFixed(4)} (total $${totalCostUsd.toFixed(4)})`));
|
|
290
|
-
}
|
|
291
|
-
};
|
|
292
|
-
log(output.info(`watching ${name}/${role} — ${run} (Ctrl-C to stop; org logs ${name} --role ${role} --follow for the full event stream)`));
|
|
293
|
-
let seenLines = 0;
|
|
294
|
-
const drain = () => {
|
|
295
|
-
if (!existsSync(file))
|
|
296
|
-
return;
|
|
297
|
-
const lines = readFileSync(file, 'utf8').split('\n').filter(Boolean);
|
|
298
|
-
for (let i = seenLines; i < lines.length; i++) {
|
|
299
|
-
try {
|
|
300
|
-
show(JSON.parse(lines[i]));
|
|
301
|
-
seenLines = i + 1;
|
|
302
|
-
}
|
|
303
|
-
catch {
|
|
304
|
-
if (i === lines.length - 1)
|
|
305
|
-
break; // only the final line may be a mid-append partial write
|
|
306
|
-
seenLines = i + 1;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
};
|
|
310
|
-
drain();
|
|
311
|
-
if (ctx.flags.follow === false)
|
|
312
|
-
return { success: true }; // --follow=false opts out of the default live tail
|
|
313
|
-
await new Promise((resolve) => {
|
|
314
|
-
const iv = setInterval(drain, 500);
|
|
315
|
-
process.once('SIGINT', () => {
|
|
316
|
-
clearInterval(iv);
|
|
317
|
-
resolve();
|
|
318
|
-
});
|
|
319
|
-
process.once('SIGTERM', () => {
|
|
320
|
-
clearInterval(iv);
|
|
321
|
-
resolve();
|
|
322
|
-
});
|
|
323
|
-
});
|
|
324
|
-
return { success: true };
|
|
325
|
-
};
|
|
326
|
-
/** `org report <name> [--run id] [--all] [--by-role] [--format mermaid]` — summarize a run (or list run history). */
|
|
327
|
-
export const reportAction = async (ctx, name) => {
|
|
328
|
-
if (ctx.flags.all === true) {
|
|
329
|
-
const history = readHistory(ctx.cwd, name);
|
|
330
|
-
if (!history.length)
|
|
331
|
-
return { success: false, message: `no run history for org ${name}` };
|
|
332
|
-
if (orgJson(ctx))
|
|
333
|
-
return printOrgJson({ v: 1, org: name, items: history });
|
|
334
|
-
log(output.info(`org ${name} — ${history.length} recorded run(s):`));
|
|
335
|
-
for (const h of history) {
|
|
336
|
-
const dur = h.durationMs != null ? `${Math.round(h.durationMs / 1000)}s` : '?';
|
|
337
|
-
// #302: an honest "why", not a bare "no outcome recorded", when the
|
|
338
|
-
// truth gate actually knows the real cause (idle-stop, failed-start, a
|
|
339
|
-
// boss-restart giving up) and whether work was left outstanding.
|
|
340
|
-
const outcome = h.outcome
|
|
341
|
-
? `${h.outcome.status}: ${h.outcome.summary.slice(0, 60)}`
|
|
342
|
-
: h.closedBy && h.closedBy !== 'org-complete'
|
|
343
|
-
? `no outcome recorded — ended via ${h.closedBy}${h.runnableTasksAtStop > 0 ? `, ${h.runnableTasksAtStop} task(s) left` : ''}`
|
|
344
|
-
: 'no outcome recorded';
|
|
345
|
-
log(output.info(` • ${h.run} ${dur} ${h.totalTokens} tokens ${h.messages} msgs — ${outcome}`));
|
|
346
|
-
}
|
|
347
|
-
return { success: true };
|
|
348
|
-
}
|
|
349
|
-
const run = resolveRun(ctx.cwd, name, ctx.flags.run);
|
|
350
|
-
if (!run)
|
|
351
|
-
return { success: false, message: `no runs found for org ${name}` };
|
|
352
|
-
const events = readRunEvents(ctx.cwd, name, run);
|
|
353
|
-
if (!events.length)
|
|
354
|
-
return { success: false, message: `run ${run} has no recorded events` };
|
|
355
|
-
const s = summarizeRun(events);
|
|
356
|
-
// M2: endpoint roles are automations — they have no row in the role tables.
|
|
357
|
-
const endpointIds = endpointRoleIds(ctx.cwd, name);
|
|
358
|
-
for (const id of endpointIds)
|
|
359
|
-
delete s.roles[id];
|
|
360
|
-
// Protocol JSON mode (§7.2): the run summary as a bare object. Emitted
|
|
361
|
-
// before the human-only flag modes (mermaid/audit/by-role) — those render
|
|
362
|
-
// views of the same summary and have no JSON shape defined by the spec.
|
|
363
|
-
if (orgJson(ctx)) {
|
|
364
|
-
return printOrgJson({
|
|
365
|
-
v: 1,
|
|
366
|
-
org: name,
|
|
367
|
-
run,
|
|
368
|
-
duration_ms: s.durationMs,
|
|
369
|
-
events: s.events,
|
|
370
|
-
messages: s.messages,
|
|
371
|
-
xorg_messages: s.xorgMessages,
|
|
372
|
-
total_tokens: s.totalTokens,
|
|
373
|
-
total_cost_usd: s.totalCostUsd,
|
|
374
|
-
outcome: s.outcome,
|
|
375
|
-
blocker: s.blocker,
|
|
376
|
-
blocker_detail: s.blockerDetail,
|
|
377
|
-
closed_by: s.closedBy,
|
|
378
|
-
runnable_tasks_at_stop: s.runnableTasksAtStop,
|
|
379
|
-
cut_short: s.cutShort,
|
|
380
|
-
crashes: s.crashes,
|
|
381
|
-
roles: s.roles,
|
|
382
|
-
assets: s.assets,
|
|
383
|
-
});
|
|
384
|
-
}
|
|
385
|
-
// Mermaid flowchart (--format mermaid flag)
|
|
386
|
-
if (ctx.flags.format === 'mermaid') {
|
|
387
|
-
// Extract message flow from events
|
|
388
|
-
const messageEvents = events.filter((e) => e.type === 'message' || e.type === 'xorg');
|
|
389
|
-
const roleSet = new Set();
|
|
390
|
-
const edges = new Set();
|
|
391
|
-
for (const e of messageEvents) {
|
|
392
|
-
if (e.from) {
|
|
393
|
-
const fromRole = e.from.includes(':') ? e.from.split(':')[1] : e.from;
|
|
394
|
-
roleSet.add(fromRole);
|
|
395
|
-
if (e.to) {
|
|
396
|
-
const toRole = e.to.includes(':') ? e.to.split(':')[1] : e.to;
|
|
397
|
-
roleSet.add(toRole);
|
|
398
|
-
const edge = `${fromRole} -->|${e.subject || 'msg'}| ${toRole}`;
|
|
399
|
-
edges.add(edge);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
const roles = Array.from(roleSet).sort();
|
|
404
|
-
log(output.info(`flowchart TD`));
|
|
405
|
-
log(output.info(` %% Org flow for ${name} / ${run}`));
|
|
406
|
-
log(output.info(` %% ${messageEvents.length} messages exchanged`));
|
|
407
|
-
log(output.info(` `));
|
|
408
|
-
// Define role nodes with styling
|
|
409
|
-
for (const role of roles) {
|
|
410
|
-
log(output.info(` ${role}[${role}]`));
|
|
411
|
-
}
|
|
412
|
-
log(output.info(` `));
|
|
413
|
-
// Add edges for messages
|
|
414
|
-
for (const edge of edges) {
|
|
415
|
-
log(output.info(` ${edge}`));
|
|
416
|
-
}
|
|
417
|
-
log(output.info(` `));
|
|
418
|
-
log(output.info(`classDef bossNode fill:#f9f,stroke:#333,stroke-width:2px`));
|
|
419
|
-
log(output.info(`classDef workerNode fill:#bbf,stroke:#333,stroke-width:1px`));
|
|
420
|
-
return { success: true, message: `Mermaid flowchart exported for ${name} / ${run}` };
|
|
421
|
-
}
|
|
422
|
-
// Tool audit filter (--audit flag) - show only tool decision events
|
|
423
|
-
if (ctx.flags.audit === true) {
|
|
424
|
-
let toolEvents = events.filter((e) => e.type === 'tool');
|
|
425
|
-
// Optional --tool flag filters to a specific tool name
|
|
426
|
-
if (typeof ctx.flags.tool === 'string' && ctx.flags.tool) {
|
|
427
|
-
const toolName = ctx.flags.tool;
|
|
428
|
-
toolEvents = toolEvents.filter((e) => e.tool === toolName);
|
|
429
|
-
}
|
|
430
|
-
if (!toolEvents.length) {
|
|
431
|
-
const toolFilter = typeof ctx.flags.tool === 'string' ? ` for tool "${ctx.flags.tool}"` : '';
|
|
432
|
-
log(output.info(`No tool events found${toolFilter} in ${run}`));
|
|
433
|
-
return { success: true };
|
|
434
|
-
}
|
|
435
|
-
log(output.info(`Tool audit trail for ${name} / ${run} (${toolEvents.length} events):`));
|
|
436
|
-
log(output.info('┌──────────────────┬─────────────────────────┬──────────┬──────────────────────────────────────┐'));
|
|
437
|
-
log(output.info('│ Role │ Tool │ Decision │ Reason │'));
|
|
438
|
-
log(output.info('├──────────────────┼─────────────────────────┼──────────┼──────────────────────────────────────┤'));
|
|
439
|
-
for (const e of toolEvents) {
|
|
440
|
-
const role = e.from ?? 'system';
|
|
441
|
-
const tool = e.tool ?? 'unknown';
|
|
442
|
-
const decision = e.decision === 'deny' ? 'DENY' : 'ALLOW';
|
|
443
|
-
const reason = e.reason || '-';
|
|
444
|
-
log(output.info(`│ ${role.padEnd(16)} │ ${tool.padEnd(23)} │ ${decision.padEnd(8)} │ ${reason.padEnd(38)} │`));
|
|
445
|
-
}
|
|
446
|
-
log(output.info('└──────────────────┴─────────────────────────┴──────────┴──────────────────────────────────────┘'));
|
|
447
|
-
return { success: true };
|
|
448
|
-
}
|
|
449
|
-
// Per-role cost breakdown (--by-role flag)
|
|
450
|
-
if (ctx.flags['by-role'] === true) {
|
|
451
|
-
const byRole = new Map();
|
|
452
|
-
for (const [roleId, roleStats] of Object.entries(s.roles)) {
|
|
453
|
-
const acc = byRole.get(roleId) ?? { cost: 0, tokens: 0, messages: 0 };
|
|
454
|
-
acc.cost += roleStats.costUsd ?? 0;
|
|
455
|
-
acc.tokens += roleStats.tokens;
|
|
456
|
-
acc.messages += roleStats.messagesSent;
|
|
457
|
-
byRole.set(roleId, acc);
|
|
458
|
-
}
|
|
459
|
-
log(output.info(`Per-role cost breakdown for ${name} / ${run}:`));
|
|
460
|
-
log(output.info('┌──────────────────┬───────────┬────────────┬───────────┐'));
|
|
461
|
-
log(output.info('│ Role │ Cost ($) │ Tokens │ Messages │'));
|
|
462
|
-
log(output.info('├──────────────────┼───────────┼────────────┼───────────┤'));
|
|
463
|
-
for (const [roleId, data] of byRole) {
|
|
464
|
-
log(output.info(`│ ${roleId.padEnd(16)} │ ${(data.cost.toFixed(4)).padStart(9)} │ ${String(data.tokens).padStart(10)} │ ${String(data.messages).padStart(9)} │`));
|
|
465
|
-
}
|
|
466
|
-
log(output.info('└──────────────────┴───────────┴────────────┴───────────┘'));
|
|
467
|
-
return { success: true };
|
|
468
|
-
}
|
|
469
|
-
// Per-role budget ceiling: same split the daemon applies (budget ÷ role count),
|
|
470
|
-
// with any explicit policy.maxTokens override. Missing/unreadable config → no ceilings.
|
|
471
|
-
let perRoleBudget = null;
|
|
472
|
-
let basis = 'uncached';
|
|
473
|
-
const roleCeiling = new Map();
|
|
474
|
-
try {
|
|
475
|
-
const def = OrgDefSchema.parse(JSON.parse(readFileSync(join(ctx.cwd, ORG_DIR, `${name}.json`), 'utf8')));
|
|
476
|
-
const sessionRoles = def.roles.filter((r) => r.kind !== 'endpoint');
|
|
477
|
-
perRoleBudget = Math.floor((def.run_config.budget_tokens ?? 1_000_000) / Math.max(1, sessionRoles.length));
|
|
478
|
-
basis = def.run_config.budget_tokens_basis ?? 'uncached';
|
|
479
|
-
for (const r of sessionRoles) {
|
|
480
|
-
const max = r.policy?.maxTokens;
|
|
481
|
-
roleCeiling.set(r.id, max ?? r.budget_tokens ?? perRoleBudget);
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
catch {
|
|
485
|
-
/* config gone or invalid — report without budget context */
|
|
486
|
-
}
|
|
487
|
-
log(output.info(`ORG REPORT — ${name} / ${run}`));
|
|
488
|
-
log(output.info(` Duration: ${s.durationMs != null ? `${Math.round(s.durationMs / 1000)}s` : '?'} Events: ${s.events} Messages: ${s.messages}${s.xorgMessages ? ` (+${s.xorgMessages} cross-org)` : ''}`));
|
|
489
|
-
log(output.info(` Tokens: ${s.totalTokens}${perRoleBudget ? ` (budget: ${perRoleBudget}/role ${basis === 'billable' ? 'billable' : 'in+out'})` : ''}${s.totalCostUsd ? ` Cost: $${s.totalCostUsd.toFixed(4)}` : ''}`));
|
|
490
|
-
if (s.outcome) {
|
|
491
|
-
// #302: blocker/blockerDetail are top-level on RunSummary (a sibling of
|
|
492
|
-
// outcome, not nested in it — see reporting.ts), rendered here alongside
|
|
493
|
-
// the outcome they were attached to.
|
|
494
|
-
const blockerSuffix = s.blocker
|
|
495
|
-
? ` [blocker: ${s.blocker}${s.blockerDetail ? ` — ${s.blockerDetail}` : ''}]`
|
|
496
|
-
: '';
|
|
497
|
-
log(output.success(` Outcome: ${s.outcome.status} (by ${s.outcome.by}) — ${s.outcome.summary}${blockerSuffix}`));
|
|
498
|
-
}
|
|
499
|
-
else if (s.closedBy && s.closedBy !== 'org-complete') {
|
|
500
|
-
// #302 truth gate: a run that never called org_complete still has a real,
|
|
501
|
-
// recorded cause — never leave an operator reading "not recorded" when
|
|
502
|
-
// we actually know the run was idle-stopped, failed to start, or gave up
|
|
503
|
-
// after a boss-restart, and how much runnable work was left behind.
|
|
504
|
-
const pending = s.runnableTasksAtStop > 0 ? `, ${s.runnableTasksAtStop} task(s) left` : '';
|
|
505
|
-
log(output.warning(` Outcome: not recorded — ended via ${s.closedBy}${pending}`));
|
|
506
|
-
}
|
|
507
|
-
else {
|
|
508
|
-
log(output.warning(' Outcome: not recorded (coordinator never called org_complete)'));
|
|
509
|
-
}
|
|
510
|
-
log(output.info(' Roles:'));
|
|
511
|
-
for (const [id, r] of Object.entries(s.roles)) {
|
|
512
|
-
const wasCutShort = s.cutShort.includes(id);
|
|
513
|
-
const icon = r.crashed ? '✗' : wasCutShort ? '⊘' : '•';
|
|
514
|
-
const suffix = r.crashed ? ' — CRASHED' : wasCutShort ? ' — cut short by stop' : '';
|
|
515
|
-
log(output.info(` ${icon} ${id}: ${r.messagesSent} msgs, ${r.toolsAllowed} tools${r.toolsDenied ? ` (${r.toolsDenied} denied)` : ''}, ${roleTokensNote(r, roleCeiling.get(id), basis)}${suffix}`));
|
|
516
|
-
}
|
|
517
|
-
if (s.assets.length) {
|
|
518
|
-
log(output.info(` Assets (${s.assets.length}):`));
|
|
519
|
-
for (const a of s.assets.slice(0, 20))
|
|
520
|
-
log(output.info(` 📄 ${a}`));
|
|
521
|
-
if (s.assets.length > 20)
|
|
522
|
-
log(output.info(` … and ${s.assets.length - 20} more`));
|
|
523
|
-
}
|
|
524
|
-
return { success: true };
|
|
525
|
-
};
|
|
526
|
-
/** Read questions.json. A MISSING file legitimately means "no questions" → [].
|
|
527
|
-
* Any other failure (unreadable, malformed — e.g. a partial daemon write) THROWS:
|
|
528
|
-
* answerAction rewrites this file from what this returns, so silently coercing a
|
|
529
|
-
* failed read to [] would atomically replace every recorded question with one. */
|
|
530
|
-
const readQuestions = (cwd, name) => {
|
|
531
|
-
const path = join(cwd, ORG_DIR, name, 'questions.json');
|
|
532
|
-
let raw;
|
|
533
|
-
try {
|
|
534
|
-
raw = readFileSync(path, 'utf8');
|
|
535
|
-
}
|
|
536
|
-
catch (err) {
|
|
537
|
-
if (err.code === 'ENOENT')
|
|
538
|
-
return [];
|
|
539
|
-
throw new Error(`cannot read ${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
540
|
-
}
|
|
541
|
-
let parsed;
|
|
542
|
-
try {
|
|
543
|
-
parsed = JSON.parse(raw);
|
|
544
|
-
}
|
|
545
|
-
catch (err) {
|
|
546
|
-
throw new Error(`${path} is not valid JSON (${err instanceof Error ? err.message : String(err)})`);
|
|
547
|
-
}
|
|
548
|
-
if (parsed?.questions === undefined || parsed.questions === null)
|
|
549
|
-
return [];
|
|
550
|
-
if (!Array.isArray(parsed.questions))
|
|
551
|
-
throw new Error(`${path}: "questions" is not an array`);
|
|
552
|
-
return parsed.questions;
|
|
553
|
-
};
|
|
554
|
-
/** `org questions <name> [--all]` — list pending (or all) ask_human questions. */
|
|
555
|
-
export const questionsAction = async (ctx, name) => {
|
|
556
|
-
let all;
|
|
557
|
-
try {
|
|
558
|
-
all = readQuestions(ctx.cwd, name);
|
|
559
|
-
}
|
|
560
|
-
catch (err) {
|
|
561
|
-
log(output.error(`Cannot read questions for org ${name}: ${err instanceof Error ? err.message : String(err)}`));
|
|
562
|
-
return { success: false, message: 'questions.json unreadable' };
|
|
563
|
-
}
|
|
564
|
-
const shown = ctx.flags.all === true ? all : all.filter((q) => q.answer === null);
|
|
565
|
-
if (orgJson(ctx))
|
|
566
|
-
return printOrgJson({ v: 1, org: name, items: shown });
|
|
567
|
-
if (!shown.length) {
|
|
568
|
-
log(output.info(all.length
|
|
569
|
-
? `No pending questions for org ${name} (${all.length} answered — use --all).`
|
|
570
|
-
: `No questions recorded for org ${name}.`));
|
|
571
|
-
return { success: true };
|
|
572
|
-
}
|
|
573
|
-
for (const q of shown) {
|
|
574
|
-
const when = utcDateMinute(q.ts);
|
|
575
|
-
log(output.info(`${q.answer === null ? '❓' : '✓'} [${q.questionId}] ${when} ${q.role}: ${q.question}`));
|
|
576
|
-
if (q.answer !== null)
|
|
577
|
-
log(output.info(` ↳ ${q.answer}`));
|
|
578
|
-
}
|
|
579
|
-
if (shown.some((q) => q.answer === null))
|
|
580
|
-
log(output.info(`\nAnswer with: monomind org answer ${name} <question-id> "your answer"`));
|
|
581
|
-
return { success: true };
|
|
582
|
-
};
|
|
583
|
-
/** Read approvals.json — the tool/action-approval queue checked by
|
|
584
|
-
* checkApproval for Bash/WebFetch/WebSearch/org_complete (session.ts).
|
|
585
|
-
* Distinct from questions.json/gates.json: approving those does NOT touch
|
|
586
|
-
* this file or grant anything here. A MISSING file legitimately means "no
|
|
587
|
-
* pending approvals" → []. Any other failure THROWS, same rationale as
|
|
588
|
-
* readQuestions above — approveAction/denyAction rewrite this file from
|
|
589
|
-
* what this returns. */
|
|
590
|
-
const readApprovals = (cwd, name) => {
|
|
591
|
-
const path = join(cwd, ORG_DIR, name, 'approvals.json');
|
|
592
|
-
let raw;
|
|
593
|
-
try {
|
|
594
|
-
raw = readFileSync(path, 'utf8');
|
|
595
|
-
}
|
|
596
|
-
catch (err) {
|
|
597
|
-
if (err.code === 'ENOENT')
|
|
598
|
-
return [];
|
|
599
|
-
throw new Error(`cannot read ${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
600
|
-
}
|
|
601
|
-
let parsed;
|
|
602
|
-
try {
|
|
603
|
-
parsed = JSON.parse(raw);
|
|
604
|
-
}
|
|
605
|
-
catch (err) {
|
|
606
|
-
throw new Error(`${path} is not valid JSON (${err instanceof Error ? err.message : String(err)})`);
|
|
607
|
-
}
|
|
608
|
-
if (parsed?.approvals === undefined || parsed.approvals === null)
|
|
609
|
-
return [];
|
|
610
|
-
if (!Array.isArray(parsed.approvals))
|
|
611
|
-
throw new Error(`${path}: "approvals" is not an array`);
|
|
612
|
-
return parsed.approvals;
|
|
613
|
-
};
|
|
614
|
-
/** `org approvals <name> [--all]` — list pending (or all) tool/action approval requests. */
|
|
615
|
-
export const approvalsAction = async (ctx, name) => {
|
|
616
|
-
let all;
|
|
617
|
-
try {
|
|
618
|
-
all = readApprovals(ctx.cwd, name);
|
|
619
|
-
}
|
|
620
|
-
catch (err) {
|
|
621
|
-
log(output.error(`Cannot read approvals for org ${name}: ${err instanceof Error ? err.message : String(err)}`));
|
|
622
|
-
return { success: false, message: 'approvals.json unreadable' };
|
|
623
|
-
}
|
|
624
|
-
const shown = ctx.flags.all === true ? all : all.filter((a) => a.approved === null);
|
|
625
|
-
if (orgJson(ctx))
|
|
626
|
-
return printOrgJson({
|
|
627
|
-
v: 1,
|
|
628
|
-
org: name,
|
|
629
|
-
// M5: requestId / resolvedBy / input are always present (null for
|
|
630
|
-
// entries recorded before they existed).
|
|
631
|
-
items: shown.map((a) => ({
|
|
632
|
-
...a,
|
|
633
|
-
requestId: a.requestId ?? null,
|
|
634
|
-
resolvedBy: a.resolvedBy ?? null,
|
|
635
|
-
input: a.input ?? null,
|
|
636
|
-
})),
|
|
637
|
-
});
|
|
638
|
-
if (!shown.length) {
|
|
639
|
-
log(output.info(all.length
|
|
640
|
-
? `No pending approvals for org ${name} (${all.length} resolved — use --all).`
|
|
641
|
-
: `No approval requests recorded for org ${name}.`));
|
|
642
|
-
return { success: true };
|
|
643
|
-
}
|
|
644
|
-
for (const a of shown) {
|
|
645
|
-
const when = utcDateMinute(a.ts);
|
|
646
|
-
const mark = a.approved === null ? '❓' : a.approved ? '✓' : '✗';
|
|
647
|
-
const ref = a.requestId ? ` [${a.requestId}]` : '';
|
|
648
|
-
const by = a.resolvedBy ? ` (by ${a.resolvedBy})` : '';
|
|
649
|
-
log(output.info(`${mark} ${when} ${a.roleId}: ${a.action}${ref}${by}`));
|
|
650
|
-
}
|
|
651
|
-
if (shown.some((a) => a.approved === null))
|
|
652
|
-
log(output.info(`\nApprove with: monomind org approve ${name} <role> <action> [--request <id>] [--by <resolver>]\nDeny with: monomind org deny ${name} <role> <action> [--request <id>] [--by <resolver>]`));
|
|
653
|
-
return { success: true };
|
|
654
|
-
};
|
|
655
|
-
/** `org answer <name> <question-id> <answer...>` — answer a pending ask_human question.
|
|
656
|
-
* Delivers live via the hosting daemon's /api/answer-question when the org is running
|
|
657
|
-
* (broker lookup); otherwise records the answer and queues it for the next run. */
|
|
658
|
-
export const answerAction = async (ctx, name) => {
|
|
659
|
-
const questionId = ctx.args[1];
|
|
660
|
-
const answer = ctx.args.slice(2).join(' ').trim();
|
|
661
|
-
if (!questionId || !answer)
|
|
662
|
-
return {
|
|
663
|
-
success: false,
|
|
664
|
-
message: `usage: monomind org answer ${name} <question-id> "answer text"`,
|
|
665
|
-
};
|
|
666
|
-
let questions;
|
|
667
|
-
try {
|
|
668
|
-
questions = readQuestions(ctx.cwd, name);
|
|
669
|
-
}
|
|
670
|
-
catch (err) {
|
|
671
|
-
log(output.error(`Cannot read questions for org ${name}: ${err instanceof Error ? err.message : String(err)}`));
|
|
672
|
-
return { success: false, message: 'questions.json unreadable — answer not recorded' };
|
|
673
|
-
}
|
|
674
|
-
const q = questions.find((x) => x.questionId === questionId);
|
|
675
|
-
if (!q) {
|
|
676
|
-
log(output.error(`Question "${questionId}" not found for org ${name} — list with: monomind org questions ${name}`));
|
|
677
|
-
return { success: false, message: 'question not found' };
|
|
678
|
-
}
|
|
679
|
-
if (q.answer !== null)
|
|
680
|
-
return { success: false, message: `question "${questionId}" was already answered` };
|
|
681
|
-
const byFlag = await resolverFlag(ctx);
|
|
682
|
-
if (!byFlag.ok)
|
|
683
|
-
return { success: false, message: byFlag.message };
|
|
684
|
-
const resolvedBy = byFlag.by;
|
|
685
|
-
// Live path: the hosting daemon updates questions.json and pushes into the role's mailbox.
|
|
686
|
-
// SEC: answering a role's question is a human decision — authenticate with
|
|
687
|
-
// the operator credential, not the agent-facing one in the broker entry.
|
|
688
|
-
const { lookupOrg, readOperatorCredential } = await import('../orgrt/broker.js');
|
|
689
|
-
const remote = lookupOrg(name);
|
|
690
|
-
if (remote) {
|
|
691
|
-
const cred = readOperatorCredential(name);
|
|
692
|
-
try {
|
|
693
|
-
const res = await fetch(`${remote.url}/api/answer-question`, {
|
|
694
|
-
method: 'POST',
|
|
695
|
-
headers: {
|
|
696
|
-
'Content-Type': 'application/json',
|
|
697
|
-
...(cred ? { 'x-monomind-cred': cred } : {}),
|
|
698
|
-
},
|
|
699
|
-
body: JSON.stringify({ org: name, role: q.role, questionId, answer, resolvedBy }),
|
|
700
|
-
signal: AbortSignal.timeout(10_000),
|
|
701
|
-
});
|
|
702
|
-
const data = (await res.json().catch(() => ({})));
|
|
703
|
-
if (res.ok && data.ok) {
|
|
704
|
-
if (orgJson(ctx))
|
|
705
|
-
return printOrgJson({
|
|
706
|
-
v: 1,
|
|
707
|
-
org: name,
|
|
708
|
-
question_id: questionId,
|
|
709
|
-
role: q.role,
|
|
710
|
-
delivery: 'live',
|
|
711
|
-
answered: true,
|
|
712
|
-
resolvedBy,
|
|
713
|
-
});
|
|
714
|
-
log(output.success(`Answer delivered to ${name}:${q.role} (live).`));
|
|
715
|
-
return { success: true };
|
|
716
|
-
}
|
|
717
|
-
log(output.warning(`Live delivery rejected (${data.error ?? res.status}) — falling back to offline queue.`));
|
|
718
|
-
}
|
|
719
|
-
catch (err) {
|
|
720
|
-
log(output.warning(`Hosting daemon unreachable (${err instanceof Error ? err.message : 'error'}) — falling back to offline queue.`));
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
// Offline path: mirror daemon.answerQuestion's org-not-running branch.
|
|
724
|
-
// RE-READ and merge by questionId just before writing — the pre-fetch
|
|
725
|
-
// snapshot can be up to 10s stale (live-delivery timeout), and rewriting
|
|
726
|
-
// from it would delete questions the daemon appended meanwhile and revert
|
|
727
|
-
// answers it recorded (atomic rename prevents torn writes, not lost updates).
|
|
728
|
-
// A FAILED re-read must abort the write: rewriting from [] would atomically
|
|
729
|
-
// rename a single-question file over every other recorded question.
|
|
730
|
-
let fresh;
|
|
731
|
-
try {
|
|
732
|
-
fresh = readQuestions(ctx.cwd, name);
|
|
733
|
-
}
|
|
734
|
-
catch (err) {
|
|
735
|
-
log(output.error(`Refusing to rewrite questions.json — ${err instanceof Error ? err.message : String(err)}`));
|
|
736
|
-
log(output.warning(`The answer was NOT recorded. Fix or restore ${join(ctx.cwd, ORG_DIR, name, 'questions.json')}, then retry.`));
|
|
737
|
-
return { success: false, message: 'questions.json unreadable — answer not recorded' };
|
|
738
|
-
}
|
|
739
|
-
const freshQ = fresh.find((x) => x.questionId === questionId);
|
|
740
|
-
if (freshQ && freshQ.answer !== null) {
|
|
741
|
-
return {
|
|
742
|
-
success: false,
|
|
743
|
-
message: `question "${questionId}" was answered while this command was running`,
|
|
744
|
-
};
|
|
745
|
-
}
|
|
746
|
-
// Queue BEFORE marking answered (same rule as daemon.answerQuestion): if the
|
|
747
|
-
// append fails, the question must stay pending and answerable. Marking first meant
|
|
748
|
-
// a failed queueMessage recorded the answer as delivered while nothing was queued,
|
|
749
|
-
// and the `already answered` guard then rejected every retry.
|
|
750
|
-
const { queueMessage } = await import('../orgrt/inbox.js');
|
|
751
|
-
try {
|
|
752
|
-
queueMessage(ctx.cwd, name, {
|
|
753
|
-
fromQualified: 'human',
|
|
754
|
-
toRole: q.role,
|
|
755
|
-
subject: `answer:${questionId}`,
|
|
756
|
-
body: `question: ${q.question}\n\nanswer: ${answer}`,
|
|
757
|
-
ts: Date.now(),
|
|
758
|
-
});
|
|
759
|
-
}
|
|
760
|
-
catch (err) {
|
|
761
|
-
log(output.error(`Could not queue the answer for ${name}:${q.role} (${err instanceof Error ? err.message : String(err)}) — answer NOT recorded, retry it.`));
|
|
762
|
-
return { success: false, message: 'queueing failed — answer not recorded' };
|
|
763
|
-
}
|
|
764
|
-
const merged = fresh.some((x) => x.questionId === questionId)
|
|
765
|
-
? fresh.map((x) => x.questionId === questionId ? { ...x, answer, answeredAt: Date.now(), resolvedBy } : x)
|
|
766
|
-
: [...fresh, { ...q, answer, answeredAt: Date.now(), resolvedBy }];
|
|
767
|
-
const dest = join(ctx.cwd, ORG_DIR, name, 'questions.json');
|
|
768
|
-
const tmp = `${dest}.${process.pid}.tmp`;
|
|
769
|
-
writeFileSync(tmp, JSON.stringify({ questions: merged }, null, 2));
|
|
770
|
-
const { renameSync } = await import('node:fs');
|
|
771
|
-
renameSync(tmp, dest);
|
|
772
|
-
if (orgJson(ctx))
|
|
773
|
-
return printOrgJson({
|
|
774
|
-
v: 1,
|
|
775
|
-
org: name,
|
|
776
|
-
question_id: questionId,
|
|
777
|
-
role: q.role,
|
|
778
|
-
delivery: 'queued',
|
|
779
|
-
answered: true,
|
|
780
|
-
resolvedBy,
|
|
781
|
-
});
|
|
782
|
-
log(output.success(`Answer recorded — ${name}:${q.role} receives it when the org next runs.`));
|
|
783
|
-
return { success: true };
|
|
784
|
-
};
|
|
785
|
-
/** `org inbox <name> --json '{"from":"orgA:role","subject":"...","body":"..."}' [--to role]`
|
|
786
|
-
* (or `--from/--subject/--body`) `[--format json]`.
|
|
787
|
-
* Inbound entrypoint for cross-org/remote delivery — orgrt/remote.ts's deliverRemote()
|
|
788
|
-
* shells out to exactly this command over SSH, and mono-agent replies through it.
|
|
789
|
-
*
|
|
790
|
-
* Live path (M3): POST to the hosting daemon's /api/xdeliver with the OPERATOR
|
|
791
|
-
* credential (readOperatorCredential), which lets the daemon trust `from` as
|
|
792
|
-
* given — the sender org need not be registered (`workflow`, an automation
|
|
793
|
-
* role). Without an operator credential it presents the sender org's own
|
|
794
|
-
* broker credential, if that org is registered. Offline path: spool into
|
|
795
|
-
* inbox.jsonl, drained into the role's mailbox on the org's next start.
|
|
796
|
-
*
|
|
797
|
-
* `--format json` prints `{"v":1,"org","to","from","delivery":"live"|"queued",
|
|
798
|
-
* "receipt","messageId"}`. Exit 0 for live and queued; non-zero only for
|
|
799
|
-
* invalid input or an unknown org. */
|
|
800
|
-
export const inboxAction = async (ctx, name) => {
|
|
801
|
-
const json = orgJson(ctx);
|
|
802
|
-
// JSON mode keeps stdout to the single payload — diagnostics go to stderr.
|
|
803
|
-
const note = (text) => {
|
|
804
|
-
if (json)
|
|
805
|
-
process.stderr.write(`${text}\n`);
|
|
806
|
-
else
|
|
807
|
-
log(text);
|
|
808
|
-
};
|
|
809
|
-
const fail = (message) => {
|
|
810
|
-
if (json)
|
|
811
|
-
process.stdout.write(`${JSON.stringify({ v: 1, org: name, error: message })}\n`);
|
|
812
|
-
else
|
|
813
|
-
log(output.error(message));
|
|
814
|
-
return { success: false, message };
|
|
815
|
-
};
|
|
816
|
-
let payload = {};
|
|
817
|
-
const rawJson = ctx.flags.json;
|
|
818
|
-
if (typeof rawJson === 'string') {
|
|
819
|
-
try {
|
|
820
|
-
payload = JSON.parse(rawJson);
|
|
821
|
-
}
|
|
822
|
-
catch {
|
|
823
|
-
return fail('org inbox: --json is not valid JSON');
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
else {
|
|
827
|
-
payload = { from: ctx.flags.from, subject: ctx.flags.subject, body: ctx.flags.body };
|
|
828
|
-
}
|
|
829
|
-
const from = typeof payload.from === 'string' ? payload.from.trim() : '';
|
|
830
|
-
const subject = typeof payload.subject === 'string' ? payload.subject : '';
|
|
831
|
-
const body = typeof payload.body === 'string' ? payload.body : '';
|
|
832
|
-
if (!from || !body)
|
|
833
|
-
return fail('org inbox: payload requires "from" and "body" (via --json or --from/--body)');
|
|
834
|
-
if (!/^[^\s:]{1,128}(:[^\s:]{1,128})?$/.test(from))
|
|
835
|
-
return fail(`org inbox: invalid sender "${from}" — use "<org>:<role>" or "<role>"`);
|
|
836
|
-
const { lookupOrg, normalizeCredential, readOperatorCredential } = await import('../orgrt/broker.js');
|
|
837
|
-
const remote = lookupOrg(name);
|
|
838
|
-
const defPath = join(ctx.cwd, ORG_DIR, `${name}.json`);
|
|
839
|
-
if (!remote && !existsSync(defPath))
|
|
840
|
-
return fail(`Org not found: ${name}`);
|
|
841
|
-
// Target role: explicit --to, else the org's coordinator (reports_to == null),
|
|
842
|
-
// else the first role — matching where a role-less cross-org message should land.
|
|
843
|
-
let toRole = typeof ctx.flags.to === 'string' ? ctx.flags.to : '';
|
|
844
|
-
if (toRole && !/^[a-z0-9][a-z0-9_-]*$/i.test(toRole))
|
|
845
|
-
return fail(`Invalid role id: ${toRole}`);
|
|
846
|
-
if (!toRole) {
|
|
847
|
-
if (!existsSync(defPath))
|
|
848
|
-
return fail(`Org "${name}" config is not in this project — pass --to <role>.`);
|
|
849
|
-
try {
|
|
850
|
-
const def = JSON.parse(readFileSync(defPath, 'utf8'));
|
|
851
|
-
const roles = Array.isArray(def.roles) ? def.roles : [];
|
|
852
|
-
toRole = roles.find((r) => r.reports_to == null)?.id ?? roles[0]?.id ?? '';
|
|
853
|
-
}
|
|
854
|
-
catch (err) {
|
|
855
|
-
return fail(`Could not read org config for ${name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
856
|
-
}
|
|
857
|
-
if (!toRole)
|
|
858
|
-
return fail(`Org "${name}" has no roles to deliver to — pass --to <role>.`);
|
|
859
|
-
}
|
|
860
|
-
const { newMessageId, queueMessage } = await import('../orgrt/inbox.js');
|
|
861
|
-
const messageId = newMessageId();
|
|
862
|
-
const to = `${name}:${toRole}`;
|
|
863
|
-
const done = (delivery, receipt) => {
|
|
864
|
-
if (json)
|
|
865
|
-
return printOrgJson({ v: 1, org: name, to, from, delivery, receipt, messageId });
|
|
866
|
-
log(output.success(receipt));
|
|
867
|
-
return { success: true, message: receipt };
|
|
868
|
-
};
|
|
869
|
-
// Live path: a hosting daemon registered this org with the broker.
|
|
870
|
-
if (remote) {
|
|
871
|
-
const [fromOrg, fromRole] = from.includes(':') ? from.split(':', 2) : ['external', from];
|
|
872
|
-
const operatorCred = readOperatorCredential(name);
|
|
873
|
-
const agentCred = normalizeCredential(remote.credential);
|
|
874
|
-
const cred = operatorCred ?? agentCred;
|
|
875
|
-
const fromCredential = operatorCred ? undefined : lookupOrg(fromOrg)?.credential;
|
|
876
|
-
try {
|
|
877
|
-
const res = await fetch(`${remote.url}/api/xdeliver`, {
|
|
878
|
-
method: 'POST',
|
|
879
|
-
headers: {
|
|
880
|
-
'Content-Type': 'application/json',
|
|
881
|
-
...(cred ? { 'x-monomind-cred': cred } : {}),
|
|
882
|
-
},
|
|
883
|
-
body: JSON.stringify({
|
|
884
|
-
fromOrg,
|
|
885
|
-
fromRole,
|
|
886
|
-
...(fromCredential ? { fromCredential } : {}),
|
|
887
|
-
toOrg: name,
|
|
888
|
-
toRole,
|
|
889
|
-
subject,
|
|
890
|
-
body,
|
|
891
|
-
messageId,
|
|
892
|
-
}),
|
|
893
|
-
signal: AbortSignal.timeout(10_000),
|
|
894
|
-
});
|
|
895
|
-
const data = (await res.json().catch(() => ({})));
|
|
896
|
-
if (res.ok && data.ok) {
|
|
897
|
-
const receipt = data.receipt ?? `delivered to ${to}`;
|
|
898
|
-
// The daemon itself may have queued it (role starting, org waking).
|
|
899
|
-
return done(/^queued/.test(receipt) ? 'queued' : 'live', receipt);
|
|
900
|
-
}
|
|
901
|
-
note(output.warning(`Live delivery rejected (${data.error ?? res.status}) — falling back to offline queue.`));
|
|
902
|
-
}
|
|
903
|
-
catch (err) {
|
|
904
|
-
note(output.warning(`Hosting daemon unreachable (${err instanceof Error ? err.message : 'error'}) — falling back to offline queue.`));
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
// Offline path: spool; drained into the role's mailbox when the org next starts.
|
|
908
|
-
const queued = queueMessage(ctx.cwd, name, {
|
|
909
|
-
fromQualified: from,
|
|
910
|
-
toRole,
|
|
911
|
-
subject,
|
|
912
|
-
body,
|
|
913
|
-
ts: Date.now(),
|
|
914
|
-
messageId,
|
|
915
|
-
});
|
|
916
|
-
if (!queued) {
|
|
917
|
-
const message = `Could not queue the message for ${to} (disk full or permissions).`;
|
|
918
|
-
if (json)
|
|
919
|
-
process.stdout.write(`${JSON.stringify({ v: 1, org: name, error: message })}\n`);
|
|
920
|
-
else
|
|
921
|
-
log(output.error(message));
|
|
922
|
-
return { success: false, message: 'queueing failed' };
|
|
923
|
-
}
|
|
924
|
-
return done('queued', `queued for ${to} (delivered when the org next runs)`);
|
|
925
|
-
};
|
|
926
|
-
/** `org create <name> --template <t> [--goal g] [--schedule s]` — scaffold a config from a template. */
|
|
927
|
-
export const createAction = async (ctx, name) => {
|
|
928
|
-
const templateName = typeof ctx.flags.template === 'string' ? ctx.flags.template : '';
|
|
929
|
-
if (!templateName) {
|
|
930
|
-
log(output.info(`Available templates: ${Object.keys(ORG_TEMPLATES).join(', ')}`));
|
|
931
|
-
return {
|
|
932
|
-
success: false,
|
|
933
|
-
message: 'usage: monomind org create <name> --template <template> [--goal "..."] [--schedule 30m]',
|
|
934
|
-
};
|
|
935
|
-
}
|
|
936
|
-
const def = buildFromTemplate(templateName, name, typeof ctx.flags.goal === 'string' ? ctx.flags.goal : undefined);
|
|
937
|
-
if (!def) {
|
|
938
|
-
log(output.error(`Unknown template "${templateName}" — available: ${Object.keys(ORG_TEMPLATES).join(', ')}`));
|
|
939
|
-
return { success: false, message: 'unknown template' };
|
|
940
|
-
}
|
|
941
|
-
if (typeof ctx.flags.schedule === 'string')
|
|
942
|
-
def.schedule = ctx.flags.schedule;
|
|
943
|
-
const file = join(ctx.cwd, ORG_DIR, `${name}.json`);
|
|
944
|
-
if (existsSync(file) && ctx.flags.force !== true) {
|
|
945
|
-
log(output.error(`Org "${name}" already exists — pass --force to overwrite.`));
|
|
946
|
-
return { success: false, message: 'org exists' };
|
|
947
|
-
}
|
|
948
|
-
OrgDefSchema.parse(def); // templates must always produce a runnable config
|
|
949
|
-
// Per-role model — the single most consequential setting the template picked on
|
|
950
|
-
// the user's behalf. Mirror resolveModel() (same helper `org run`'s cost estimate
|
|
951
|
-
// uses) so a role relying on its runtime/vendor default isn't mislabeled here.
|
|
952
|
-
const modelRows = def.roles.map((r) => {
|
|
953
|
-
const explicit = r.adapter_config?.model;
|
|
954
|
-
return {
|
|
955
|
-
id: r.id,
|
|
956
|
-
model: String(explicit ?? resolveModel(r, r.runtime ?? def.runtime, r.provider?.vendor)),
|
|
957
|
-
explicit: !!explicit,
|
|
958
|
-
};
|
|
959
|
-
});
|
|
960
|
-
const printModels = () => {
|
|
961
|
-
log(output.bold(' Models:'));
|
|
962
|
-
for (const r of modelRows) {
|
|
963
|
-
log(` ${r.id.padEnd(20)} ${r.model}${r.explicit ? '' : ' (default)'}`);
|
|
964
|
-
}
|
|
965
|
-
};
|
|
966
|
-
if (ctx.interactive && ctx.flags.yes !== true) {
|
|
967
|
-
log(output.bold(`\nAbout to create org "${name}" from template "${templateName}" (${def.roles.length} roles):`));
|
|
968
|
-
printModels();
|
|
969
|
-
const { confirm } = await import('../prompt.js');
|
|
970
|
-
const proceed = await confirm({ message: 'Create this org?', default: true });
|
|
971
|
-
if (!proceed) {
|
|
972
|
-
log(output.info('Cancelled — no file written. Adjust --template/--goal, or edit the template, then retry (pass --yes to skip this prompt).'));
|
|
973
|
-
return { success: false, message: 'cancelled by user' };
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
const { mkdirSync } = await import('node:fs');
|
|
977
|
-
mkdirSync(join(ctx.cwd, ORG_DIR), { recursive: true });
|
|
978
|
-
writeFileSync(file, `${JSON.stringify(def, null, 2)}\n`, 'utf8');
|
|
979
|
-
log(output.success(`Org "${name}" created from template "${templateName}" (${def.roles.length} roles).`));
|
|
980
|
-
log(output.info(` Budget: ${def.run_config.budget_tokens} tokens · Turn limit: ${def.run_config.max_turns_per_message} per message (effectively unlimited by default — set run_config.max_turns_per_message, or a role's own max_turns_per_message, to cap it).`));
|
|
981
|
-
if (!ctx.interactive || ctx.flags.yes === true)
|
|
982
|
-
printModels();
|
|
983
|
-
log(output.info(` Edit the goal/roles in ${file}, then: monomind org run ${name}`));
|
|
984
|
-
return { success: true };
|
|
985
|
-
};
|
|
986
|
-
/** `org costs <name> [--run id]` — show per-role cost tracking from runtime.json */
|
|
987
|
-
export const costsAction = async (ctx, name) => {
|
|
988
|
-
const run = resolveRun(ctx.cwd, name, ctx.flags.run);
|
|
989
|
-
if (!run)
|
|
990
|
-
return {
|
|
991
|
-
success: false,
|
|
992
|
-
message: `no runs found for org ${name} — start one with: monomind org run ${name}`,
|
|
993
|
-
};
|
|
994
|
-
// Read runtime.json for the per-role metrics
|
|
995
|
-
const rtPath = join(ctx.cwd, ORG_DIR, name, 'runtime.json');
|
|
996
|
-
if (!existsSync(rtPath)) {
|
|
997
|
-
return { success: false, message: `no runtime state found for org ${name}` };
|
|
998
|
-
}
|
|
999
|
-
let rt;
|
|
1000
|
-
try {
|
|
1001
|
-
rt = JSON.parse(readFileSync(rtPath, 'utf8'));
|
|
1002
|
-
}
|
|
1003
|
-
catch (err) {
|
|
1004
|
-
log(output.error(`Cannot read runtime.json: ${err instanceof Error ? err.message : String(err)}`));
|
|
1005
|
-
return { success: false, message: 'runtime.json unreadable' };
|
|
1006
|
-
}
|
|
1007
|
-
if (rt?.run !== run && !orgJson(ctx)) {
|
|
1008
|
-
log(output.warning(`Note: runtime.json shows run ${rt?.run ?? 'unknown'} — showing metrics for requested run ${run} from history`));
|
|
1009
|
-
}
|
|
1010
|
-
// Also check the run summary for cost data
|
|
1011
|
-
const events = readRunEvents(ctx.cwd, name, run);
|
|
1012
|
-
const summary = events.length ? summarizeRun(events) : null;
|
|
1013
|
-
if (!orgJson(ctx))
|
|
1014
|
-
log(output.info(`Per-role cost breakdown for ${name} / ${run}:`));
|
|
1015
|
-
// Combine data from runtime.json (live metrics) and summary (historical)
|
|
1016
|
-
const roleData = new Map();
|
|
1017
|
-
// M2: endpoint roles are automations — no row in the cost table.
|
|
1018
|
-
const endpointIds = endpointRoleIds(ctx.cwd, name);
|
|
1019
|
-
// Add live metrics from runtime.json
|
|
1020
|
-
if (rt?.roleMetrics) {
|
|
1021
|
-
for (const [roleId, metrics] of Object.entries(rt.roleMetrics)) {
|
|
1022
|
-
if (endpointIds.has(roleId))
|
|
1023
|
-
continue;
|
|
1024
|
-
roleData.set(roleId, {
|
|
1025
|
-
tokens: metrics.tokens,
|
|
1026
|
-
costUsd: metrics.costUsd,
|
|
1027
|
-
messages: 0,
|
|
1028
|
-
});
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
// Add historical data from summary if available
|
|
1032
|
-
if (summary?.roles) {
|
|
1033
|
-
for (const [roleId, roleStats] of Object.entries(summary.roles)) {
|
|
1034
|
-
if (endpointIds.has(roleId))
|
|
1035
|
-
continue;
|
|
1036
|
-
const existing = roleData.get(roleId) ?? { tokens: 0, costUsd: 0, messages: 0 };
|
|
1037
|
-
roleData.set(roleId, {
|
|
1038
|
-
tokens: existing.tokens || roleStats.tokens,
|
|
1039
|
-
costUsd: existing.costUsd || roleStats.costUsd,
|
|
1040
|
-
messages: roleStats.messagesSent,
|
|
1041
|
-
});
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
if (orgJson(ctx)) {
|
|
1045
|
-
const items = [];
|
|
1046
|
-
for (const [roleId, data] of roleData) {
|
|
1047
|
-
items.push({
|
|
1048
|
-
role: roleId,
|
|
1049
|
-
tokens: data.tokens,
|
|
1050
|
-
cost_usd: data.costUsd,
|
|
1051
|
-
messages: data.messages,
|
|
1052
|
-
});
|
|
1053
|
-
}
|
|
1054
|
-
const totals = items.reduce((acc, i) => ({
|
|
1055
|
-
tokens: acc.tokens + i.tokens,
|
|
1056
|
-
cost_usd: acc.cost_usd + i.cost_usd,
|
|
1057
|
-
messages: acc.messages + i.messages,
|
|
1058
|
-
}), { tokens: 0, cost_usd: 0, messages: 0 });
|
|
1059
|
-
return printOrgJson({ v: 1, org: name, run, items, totals });
|
|
1060
|
-
}
|
|
1061
|
-
if (roleData.size === 0) {
|
|
1062
|
-
log(output.info(`No role metrics available yet — metrics populate as agents use tokens.`));
|
|
1063
|
-
return { success: true };
|
|
1064
|
-
}
|
|
1065
|
-
log(output.info('┌──────────────────┬───────────┬────────────┬───────────┐'));
|
|
1066
|
-
log(output.info('│ Role │ Cost ($) │ Tokens │ Messages │'));
|
|
1067
|
-
log(output.info('├──────────────────┼───────────┼────────────┼───────────┤'));
|
|
1068
|
-
let totalCost = 0;
|
|
1069
|
-
let totalTokens = 0;
|
|
1070
|
-
let totalMessages = 0;
|
|
1071
|
-
for (const [roleId, data] of roleData) {
|
|
1072
|
-
log(output.info(`│ ${roleId.padEnd(16)} │ ${(data.costUsd.toFixed(4)).padStart(9)} │ ${String(data.tokens).padStart(10)} │ ${String(data.messages).padStart(9)} │`));
|
|
1073
|
-
totalCost += data.costUsd;
|
|
1074
|
-
totalTokens += data.tokens;
|
|
1075
|
-
totalMessages += data.messages;
|
|
1076
|
-
}
|
|
1077
|
-
log(output.info('├──────────────────┼───────────┼────────────┼───────────┤'));
|
|
1078
|
-
log(output.info(`│ ${('TOTAL').padEnd(16)} │ ${(totalCost.toFixed(4)).padStart(9)} │ ${String(totalTokens).padStart(10)} │ ${String(totalMessages).padStart(9)} │`));
|
|
1079
|
-
log(output.info('└──────────────────┴───────────┴────────────┴───────────┘'));
|
|
1080
|
-
return { success: true };
|
|
1081
|
-
};
|
|
1082
|
-
/** `org flow <name> [--run id]` — export org flow as Mermaid diagram */
|
|
1083
|
-
export const flowAction = async (ctx, name) => {
|
|
1084
|
-
const run = resolveRun(ctx.cwd, name, ctx.flags.run);
|
|
1085
|
-
if (!run)
|
|
1086
|
-
return {
|
|
1087
|
-
success: false,
|
|
1088
|
-
message: `no runs found for org ${name} — start one with: monomind org run ${name}`,
|
|
1089
|
-
};
|
|
1090
|
-
const events = readRunEvents(ctx.cwd, name, run);
|
|
1091
|
-
if (!events.length)
|
|
1092
|
-
return { success: false, message: `run ${run} has no recorded events` };
|
|
1093
|
-
// Extract message flow from events
|
|
1094
|
-
const messageEvents = events.filter((e) => e.type === 'message' || e.type === 'xorg');
|
|
1095
|
-
const roleSet = new Set();
|
|
1096
|
-
const edges = new Set();
|
|
1097
|
-
const edgeObjects = [];
|
|
1098
|
-
for (const e of messageEvents) {
|
|
1099
|
-
if (e.from) {
|
|
1100
|
-
const fromRole = e.from.includes(':') ? e.from.split(':')[1] : e.from;
|
|
1101
|
-
roleSet.add(fromRole);
|
|
1102
|
-
if (e.to) {
|
|
1103
|
-
const toRole = e.to.includes(':') ? e.to.split(':')[1] : e.to;
|
|
1104
|
-
roleSet.add(toRole);
|
|
1105
|
-
const edge = `${fromRole} -->|${e.subject || 'msg'}| ${toRole}`;
|
|
1106
|
-
edges.add(edge);
|
|
1107
|
-
edgeObjects.push({ from: fromRole, to: toRole, subject: e.subject || 'msg' });
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
// M2: endpoint roles are automations — not listed as role nodes.
|
|
1112
|
-
const endpointIds = endpointRoleIds(ctx.cwd, name);
|
|
1113
|
-
const roles = Array.from(roleSet)
|
|
1114
|
-
.filter((r) => !endpointIds.has(r))
|
|
1115
|
-
.sort();
|
|
1116
|
-
// Protocol JSON mode (§7.2): structured roles + edges instead of Mermaid.
|
|
1117
|
-
if (orgJson(ctx))
|
|
1118
|
-
return printOrgJson({ v: 1, org: name, run, roles, edges: edgeObjects });
|
|
1119
|
-
log(output.info(`flowchart TD`));
|
|
1120
|
-
log(output.info(` %% Org flow for ${name} / ${run}`));
|
|
1121
|
-
log(output.info(` %% ${messageEvents.length} messages exchanged`));
|
|
1122
|
-
log(output.info(` `));
|
|
1123
|
-
// Define role nodes with styling
|
|
1124
|
-
for (const role of roles) {
|
|
1125
|
-
log(output.info(` ${role}[${role}]`));
|
|
1126
|
-
}
|
|
1127
|
-
log(output.info(` `));
|
|
1128
|
-
// Add edges for messages
|
|
1129
|
-
for (const edge of edges) {
|
|
1130
|
-
log(output.info(` ${edge}`));
|
|
1131
|
-
}
|
|
1132
|
-
log(output.info(` `));
|
|
1133
|
-
log(output.info(`classDef bossNode fill:#f9f,stroke:#333,stroke-width:2px`));
|
|
1134
|
-
log(output.info(`classDef workerNode fill:#bbf,stroke:#333,stroke-width:1px`));
|
|
1135
|
-
return { success: true, message: `Mermaid flowchart exported for ${name} / ${run}` };
|
|
1136
|
-
};
|
|
1137
|
-
/** `org approve <org> <role> <action>` — approve a pending tool/action approval */
|
|
1138
|
-
/** Shared by approveAction/denyAction: try the live daemon first (updates its
|
|
1139
|
-
* in-memory state and notifies the waiting agent's mailbox immediately), and
|
|
1140
|
-
* fall back to writing approvals.json directly when the org isn't running or
|
|
1141
|
-
* the daemon is unreachable — mirrors answerAction's live-then-offline shape. */
|
|
1142
|
-
async function resolveApproval(ctx, name, role, action, approved) {
|
|
1143
|
-
const verb = approved ? 'approved' : 'denied';
|
|
1144
|
-
// M5: attribution and request scoping.
|
|
1145
|
-
const byFlag = await resolverFlag(ctx);
|
|
1146
|
-
if (!byFlag.ok)
|
|
1147
|
-
return { success: false, message: byFlag.message };
|
|
1148
|
-
const resolvedBy = byFlag.by;
|
|
1149
|
-
const requestId = typeof ctx.flags.request === 'string' && ctx.flags.request ? ctx.flags.request : undefined;
|
|
1150
|
-
if (ctx.flags.request !== undefined && !requestId)
|
|
1151
|
-
return { success: false, message: '--request needs an approval request id (apr-…)' };
|
|
1152
|
-
// SEC: approvals carry human authority — the operator credential, never the
|
|
1153
|
-
// broker entry's agent credential (which any agent subprocess can read).
|
|
1154
|
-
const { lookupOrg, readOperatorCredential } = await import('../orgrt/broker.js');
|
|
1155
|
-
const remote = lookupOrg(name);
|
|
1156
|
-
if (remote) {
|
|
1157
|
-
const cred = readOperatorCredential(name);
|
|
1158
|
-
try {
|
|
1159
|
-
const res = await fetch(`${remote.url}/api/set-approval`, {
|
|
1160
|
-
method: 'POST',
|
|
1161
|
-
headers: {
|
|
1162
|
-
'Content-Type': 'application/json',
|
|
1163
|
-
...(cred ? { 'x-monomind-cred': cred } : {}),
|
|
1164
|
-
},
|
|
1165
|
-
body: JSON.stringify({
|
|
1166
|
-
org: name,
|
|
1167
|
-
role,
|
|
1168
|
-
action,
|
|
1169
|
-
approved,
|
|
1170
|
-
resolvedBy,
|
|
1171
|
-
...(requestId ? { requestId } : {}),
|
|
1172
|
-
}),
|
|
1173
|
-
signal: AbortSignal.timeout(10_000),
|
|
1174
|
-
});
|
|
1175
|
-
const data = (await res.json().catch(() => ({})));
|
|
1176
|
-
if (res.ok && data.ok) {
|
|
1177
|
-
if (orgJson(ctx))
|
|
1178
|
-
return printOrgJson({
|
|
1179
|
-
v: 1,
|
|
1180
|
-
org: name,
|
|
1181
|
-
role,
|
|
1182
|
-
action,
|
|
1183
|
-
approved,
|
|
1184
|
-
delivery: 'live',
|
|
1185
|
-
resolvedBy,
|
|
1186
|
-
requestId: requestId ?? null,
|
|
1187
|
-
});
|
|
1188
|
-
log(approved
|
|
1189
|
-
? output.success(`Approved: ${role} may execute ${action} (live, by ${resolvedBy}).`)
|
|
1190
|
-
: output.info(`Denied: ${role} may NOT execute ${action} (live, by ${resolvedBy}).`));
|
|
1191
|
-
return { success: true, message: `${verb} ${action} for ${role}` };
|
|
1192
|
-
}
|
|
1193
|
-
log(output.warning(`Live delivery rejected (${data.error ?? res.status}) — falling back to offline queue.`));
|
|
1194
|
-
}
|
|
1195
|
-
catch (err) {
|
|
1196
|
-
log(output.warning(`Hosting daemon unreachable (${err instanceof Error ? err.message : 'error'}) — falling back to offline queue.`));
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
// Offline path: org isn't live (or the live call failed) — write approvals.json directly.
|
|
1200
|
-
const approvalsPath = join(ctx.cwd, ORG_DIR, name, 'approvals.json');
|
|
1201
|
-
if (!existsSync(approvalsPath)) {
|
|
1202
|
-
return { success: false, message: `no pending approvals for org ${name}` };
|
|
1203
|
-
}
|
|
1204
|
-
const data = JSON.parse(readFileSync(approvalsPath, 'utf8'));
|
|
1205
|
-
const pending = data.approvals ?? [];
|
|
1206
|
-
// Same selection as the daemon's setApproval: the one request named by
|
|
1207
|
-
// --request, else every still-pending entry for the (role, action) pair.
|
|
1208
|
-
const items = pending.filter((a) => a.roleId === role &&
|
|
1209
|
-
a.action === action &&
|
|
1210
|
-
a.approved === null &&
|
|
1211
|
-
(requestId === undefined || a.requestId === requestId));
|
|
1212
|
-
if (items.length === 0) {
|
|
1213
|
-
return {
|
|
1214
|
-
success: false,
|
|
1215
|
-
message: requestId
|
|
1216
|
-
? `no pending approval ${requestId} found for role ${role} action ${action}`
|
|
1217
|
-
: `no pending approval found for role ${role} action ${action}`,
|
|
1218
|
-
};
|
|
1219
|
-
}
|
|
1220
|
-
const now = Date.now();
|
|
1221
|
-
for (const item of items) {
|
|
1222
|
-
item.approved = approved;
|
|
1223
|
-
item.ts = now;
|
|
1224
|
-
item.resolvedBy = resolvedBy;
|
|
1225
|
-
item.resolvedAt = now;
|
|
1226
|
-
}
|
|
1227
|
-
writeFileSync(approvalsPath, JSON.stringify({ approvals: pending }, null, 2));
|
|
1228
|
-
if (orgJson(ctx))
|
|
1229
|
-
return printOrgJson({
|
|
1230
|
-
v: 1,
|
|
1231
|
-
org: name,
|
|
1232
|
-
role,
|
|
1233
|
-
action,
|
|
1234
|
-
approved,
|
|
1235
|
-
delivery: 'recorded',
|
|
1236
|
-
resolvedBy,
|
|
1237
|
-
requestId: requestId ?? null,
|
|
1238
|
-
});
|
|
1239
|
-
log(approved
|
|
1240
|
-
? output.success(`Approved: ${role} may execute ${action} (by ${resolvedBy})`)
|
|
1241
|
-
: output.info(`Denied: ${role} may NOT execute ${action} (by ${resolvedBy})`));
|
|
1242
|
-
return { success: true, message: `${verb} ${action} for ${role}` };
|
|
1243
|
-
}
|
|
1244
|
-
/** `org approve <org> <role> <action> [--request <id>] [--by <resolver>]` — approve a pending tool/action approval */
|
|
1245
|
-
export const approveAction = async (ctx, name) => {
|
|
1246
|
-
const role = ctx.args[1];
|
|
1247
|
-
const action = ctx.args[2];
|
|
1248
|
-
if (!role || !action) {
|
|
1249
|
-
return { success: false, message: 'usage: org approve <org> <role> <action>' };
|
|
1250
|
-
}
|
|
1251
|
-
return resolveApproval(ctx, name, role, action, true);
|
|
1252
|
-
};
|
|
1253
|
-
/** `org deny <org> <role> <action> [--request <id>] [--by <resolver>]` — deny a pending tool/action approval */
|
|
1254
|
-
export const denyAction = async (ctx, name) => {
|
|
1255
|
-
const role = ctx.args[1];
|
|
1256
|
-
const action = ctx.args[2];
|
|
1257
|
-
if (!role || !action) {
|
|
1258
|
-
return { success: false, message: 'usage: org deny <org> <role> <action>' };
|
|
1259
|
-
}
|
|
1260
|
-
return resolveApproval(ctx, name, role, action, false);
|
|
1261
|
-
};
|
|
1262
|
-
/** `org replay <org> <run-id>` — time-travel debugging: re-emit a past run's bus
|
|
1263
|
-
* events into a fresh replay run for inspection. This is event-log replay only —
|
|
1264
|
-
* it does not restart agent execution or restore live sessions. To actually
|
|
1265
|
-
* resume an org's execution from where it left off, use `org run --resume`. */
|
|
1266
|
-
export const replayAction = async (ctx, name) => {
|
|
1267
|
-
const run = ctx.args[1];
|
|
1268
|
-
if (!run) {
|
|
1269
|
-
return { success: false, message: 'usage: org replay <org> <run-id>' };
|
|
1270
|
-
}
|
|
1271
|
-
const runDir = join(ctx.cwd, ORG_DIR, name, run);
|
|
1272
|
-
if (!existsSync(runDir)) {
|
|
1273
|
-
return { success: false, message: `run ${run} not found for org ${name}` };
|
|
1274
|
-
}
|
|
1275
|
-
const busFile = join(runDir, 'bus.jsonl');
|
|
1276
|
-
if (!existsSync(busFile)) {
|
|
1277
|
-
return { success: false, message: `no bus events found for run ${run}` };
|
|
1278
|
-
}
|
|
1279
|
-
log(output.info(`Replaying org ${name} events from checkpoint ${run}...`));
|
|
1280
|
-
// Create daemon and replay the bus events for debugging/inspection
|
|
1281
|
-
const { OrgDaemon } = await import('../orgrt/daemon.js');
|
|
1282
|
-
const daemon = new OrgDaemon(ctx.cwd, { forward: false });
|
|
1283
|
-
const resumed = await daemon.replayFrom(name, run);
|
|
1284
|
-
if (!resumed) {
|
|
1285
|
-
return {
|
|
1286
|
-
success: false,
|
|
1287
|
-
message: `replay failed - check bus.jsonl and org config for ${name} are valid`,
|
|
1288
|
-
};
|
|
1289
|
-
}
|
|
1290
|
-
log(output.success(`Org ${name} events replayed from ${run} as run ${resumed.run}`));
|
|
1291
|
-
log(output.info(`Use: monomind org logs ${name} --run ${resumed.run} to inspect events.`));
|
|
1292
|
-
log(output.info(`This is debug replay only — it does not restart agent execution. To resume live execution, use: monomind org run ${name} --resume`));
|
|
1293
|
-
return { success: true, message: `replayed events from checkpoint ${run} as ${resumed.run}` };
|
|
1294
|
-
};
|
|
1295
|
-
/** PID of a live `org serve` daemon for this project, or null. Mirrors
|
|
1296
|
-
* runAction's `liveServeDaemonPid` (commands/org.ts, ~line 107) — duplicated
|
|
1297
|
-
* rather than imported because that function isn't exported there and this
|
|
1298
|
-
* file is scoped to not touch org.ts. Liveness is confirmed against the pid
|
|
1299
|
-
* itself, not just heartbeat-file presence, since a SIGKILLed daemon leaves
|
|
1300
|
-
* a stale heartbeat behind; a stamp older than a few beats (daemon beats
|
|
1301
|
-
* every 30s) means it's gone or wedged. */
|
|
1302
|
-
function liveServeDaemonPid(cwd) {
|
|
1303
|
-
try {
|
|
1304
|
-
const hb = JSON.parse(readFileSync(join(cwd, '.monomind', 'serve-heartbeat.json'), 'utf8'));
|
|
1305
|
-
if (typeof hb.pid !== 'number' || hb.pid === process.pid)
|
|
1306
|
-
return null;
|
|
1307
|
-
const age = Date.now() - Date.parse(hb.updatedAt ?? '');
|
|
1308
|
-
if (!Number.isFinite(age) || age > 3 * 60_000)
|
|
1309
|
-
return null;
|
|
1310
|
-
process.kill(hb.pid, 0); // throws if the process is gone
|
|
1311
|
-
return hb.pid;
|
|
1312
|
-
}
|
|
1313
|
-
catch {
|
|
1314
|
-
return null;
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
/** `org resume-from <org>` — resume live execution from the org's persisted
|
|
1318
|
-
* checkpoint (runtime.json): restores mailbox queues, policy/token counters,
|
|
1319
|
-
* and session state, subject to checkpoint TTL and checksum validation. Unlike
|
|
1320
|
-
* `replay`, this restarts real agent execution via `startOrg(..., { resume: true })`. */
|
|
1321
|
-
export const resumeFromAction = async (ctx, name) => {
|
|
1322
|
-
// A live `org serve` daemon (or another `org run`) may already own this
|
|
1323
|
-
// org's execution. Building a fresh in-process OrgDaemon and resuming
|
|
1324
|
-
// unconditionally — as this action used to — puts two processes on one
|
|
1325
|
-
// runtime.json/broker lease and spawns a second set of role sessions
|
|
1326
|
-
// against the same shared git workspace, since the fresh daemon's
|
|
1327
|
-
// duplicate-start guard (`this.orgs.has(name)`) is scoped to its own empty
|
|
1328
|
-
// in-memory map and never sees the other process. `runAction` (org.ts)
|
|
1329
|
-
// already guards this by handing the request to the live daemon via a
|
|
1330
|
-
// runfile instead of racing it — but that runfile mechanism only carries a
|
|
1331
|
-
// plain "start" request (pollRunfiles/org.ts calls `daemon.startOrg(name,
|
|
1332
|
-
// task)` with no resume option), so it can't be reused for a resume
|
|
1333
|
-
// request without also touching org.ts. Refuse instead of racing the live
|
|
1334
|
-
// daemon.
|
|
1335
|
-
const serveOwner = liveServeDaemonPid(ctx.cwd);
|
|
1336
|
-
if (serveOwner != null) {
|
|
1337
|
-
log(output.error(`org ${name}: a live serve daemon (pid ${serveOwner}) already owns this project's orgs — refusing to start a second execution here.`));
|
|
1338
|
-
log(output.info(`Check it against the live daemon instead: monomind org status ${name} / org decisions ${name} / org gates ${name}. ` +
|
|
1339
|
-
`To resume from checkpoint yourself, stop the live daemon first (monomind org stop) or let it pick the org back up on its own schedule.`));
|
|
1340
|
-
return {
|
|
1341
|
-
success: false,
|
|
1342
|
-
message: `org ${name}: already owned by live serve daemon (pid ${serveOwner}) — refused to avoid a duplicate execution`,
|
|
1343
|
-
};
|
|
1344
|
-
}
|
|
1345
|
-
log(output.info(`Resuming org ${name} from checkpoint...`));
|
|
1346
|
-
const { OrgDaemon } = await import('../orgrt/daemon.js');
|
|
1347
|
-
const daemon = new OrgDaemon(ctx.cwd, { forward: false });
|
|
1348
|
-
const resumed = await daemon.resumeOrg(name);
|
|
1349
|
-
if (!resumed) {
|
|
1350
|
-
return {
|
|
1351
|
-
success: false,
|
|
1352
|
-
message: `resume failed for ${name} - check runtime.json checkpoint is present, unexpired, and valid`,
|
|
1353
|
-
};
|
|
1354
|
-
}
|
|
1355
|
-
log(output.success(`Org ${name} resumed - ${resumed.agents.size} role(s) restored`));
|
|
1356
|
-
log(output.info(`Stop with: monomind org stop ${name}`));
|
|
1357
|
-
return { success: true, message: `resumed ${name} - ${resumed.agents.size} role(s) restored` };
|
|
1358
|
-
};
|
|
1359
|
-
/** `org branch <org> <run-id> <label> [--format json]` — snapshot a run's event
|
|
1360
|
-
* log for replay. This is NOT an executable what-if scenario: it copies
|
|
1361
|
-
* bus.jsonl into a new run directory tagged with a `.branch-source` marker so
|
|
1362
|
-
* it can be inspected or replayed later; it does not fork or re-run agent
|
|
1363
|
-
* execution. Delegates to the shared, atomic (tmp+rename) implementation in
|
|
1364
|
-
* checkpoint-ops.ts.
|
|
1365
|
-
*
|
|
1366
|
-
* `<label>` is a note recorded in `.branch-source`, not the new run's name —
|
|
1367
|
-
* the run id is generated. `--format json` reports that generated id as a
|
|
1368
|
-
* field so a scripted caller can feed it to `org replay` without scraping the
|
|
1369
|
-
* human-readable line (#292). */
|
|
1370
|
-
export const branchAction = async (ctx, name) => {
|
|
1371
|
-
const run = ctx.args[1];
|
|
1372
|
-
const label = ctx.args[2];
|
|
1373
|
-
if (!run || !label) {
|
|
1374
|
-
return { success: false, message: 'usage: org branch <org> <run-id> <label>' };
|
|
1375
|
-
}
|
|
1376
|
-
const result = branchCheckpoint(ctx.cwd, name, run, label);
|
|
1377
|
-
if (!result.ok) {
|
|
1378
|
-
return { success: false, message: result.error };
|
|
1379
|
-
}
|
|
1380
|
-
if (orgJson(ctx))
|
|
1381
|
-
return printOrgJson({ v: 1, org: name, run: result.branchRun, from: run, label });
|
|
1382
|
-
log(output.success(`Snapshotted ${run} as run ${result.branchRun} (label: "${label}")`));
|
|
1383
|
-
log(output.info(`Replay it with: monomind org replay ${name} ${result.branchRun}`));
|
|
1384
|
-
return { success: true, message: `snapshotted ${run} as run ${result.branchRun}` };
|
|
1385
|
-
};
|
|
1386
|
-
/** `org decisions <org> [--run id]` — show Rifft-style decision traces */
|
|
1387
|
-
export const decisionsAction = async (ctx, name) => {
|
|
1388
|
-
const run = resolveRun(ctx.cwd, name, ctx.flags.run);
|
|
1389
|
-
if (!run) {
|
|
1390
|
-
return { success: false, message: `no runs found for org ${name}` };
|
|
1391
|
-
}
|
|
1392
|
-
const events = readRunEvents(ctx.cwd, name, run);
|
|
1393
|
-
if (!events.length) {
|
|
1394
|
-
return { success: false, message: `run ${run} has no recorded events` };
|
|
1395
|
-
}
|
|
1396
|
-
// Filter decision trace events
|
|
1397
|
-
const decisionEvents = events.filter((e) => e.type === 'audit' && e.reason === 'decision-trace' && e.data && typeof e.data === 'object');
|
|
1398
|
-
if (orgJson(ctx)) {
|
|
1399
|
-
return printOrgJson({
|
|
1400
|
-
v: 1,
|
|
1401
|
-
org: name,
|
|
1402
|
-
run,
|
|
1403
|
-
items: decisionEvents.map((e) => ({
|
|
1404
|
-
ts: e.ts,
|
|
1405
|
-
role: e.from ?? 'system',
|
|
1406
|
-
...e.data,
|
|
1407
|
-
})),
|
|
1408
|
-
});
|
|
1409
|
-
}
|
|
1410
|
-
if (!decisionEvents.length) {
|
|
1411
|
-
log(output.info(`No decision traces found in ${run}`));
|
|
1412
|
-
return { success: true };
|
|
1413
|
-
}
|
|
1414
|
-
log(output.info(`Decision traces for ${name} / ${run} (${decisionEvents.length} decisions):`));
|
|
1415
|
-
log(output.info('┌──────────────────┬─────────────┬────────────────────────────────────────┐'));
|
|
1416
|
-
log(output.info('│ Role │ Type │ Context │'));
|
|
1417
|
-
log(output.info('├──────────────────┼─────────────┼────────────────────────────────────────┤'));
|
|
1418
|
-
for (const e of decisionEvents) {
|
|
1419
|
-
const role = e.from ?? 'system';
|
|
1420
|
-
const type = e.data.decisionType ?? 'unknown';
|
|
1421
|
-
const context = e.data.context ?? '-';
|
|
1422
|
-
log(output.info(`│ ${role.padEnd(16)} │ ${type.padEnd(11)} │ ${context.padEnd(38)} │`));
|
|
1423
|
-
}
|
|
1424
|
-
log(output.info('└──────────────────┴─────────────┴────────────────────────────────────────┘'));
|
|
1425
|
-
return { success: true, message: `${decisionEvents.length} decision traces` };
|
|
1426
|
-
};
|
|
1427
|
-
// ── Decision gates ──────────────────────────────────────────────────────
|
|
1428
|
-
function readGatesFile(cwd, org) {
|
|
1429
|
-
try {
|
|
1430
|
-
return JSON.parse(readFileSync(join(cwd, ORG_DIR, org, 'gates.json'), 'utf8'));
|
|
1431
|
-
}
|
|
1432
|
-
catch {
|
|
1433
|
-
return { gates: [] };
|
|
1434
|
-
}
|
|
1435
|
-
}
|
|
1436
|
-
/** Read gates.json for a write path. A MISSING file legitimately means "no gates" → [].
|
|
1437
|
-
* Any other failure (unreadable, malformed — e.g. a partial daemon write) THROWS:
|
|
1438
|
-
* gateResolveAction rewrites this file from what this returns, so silently coercing a
|
|
1439
|
-
* failed read to [] would surface a real I/O error as "gate not found" and a
|
|
1440
|
-
* subsequent write would replace every recorded gate with just this one. */
|
|
1441
|
-
function readGatesFileStrict(cwd, org) {
|
|
1442
|
-
const path = join(cwd, ORG_DIR, org, 'gates.json');
|
|
1443
|
-
let raw;
|
|
1444
|
-
try {
|
|
1445
|
-
raw = readFileSync(path, 'utf8');
|
|
1446
|
-
}
|
|
1447
|
-
catch (err) {
|
|
1448
|
-
if (err.code === 'ENOENT')
|
|
1449
|
-
return { gates: [] };
|
|
1450
|
-
throw new Error(`cannot read ${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1451
|
-
}
|
|
1452
|
-
let parsed;
|
|
1453
|
-
try {
|
|
1454
|
-
parsed = JSON.parse(raw);
|
|
1455
|
-
}
|
|
1456
|
-
catch (err) {
|
|
1457
|
-
throw new Error(`${path} is not valid JSON (${err instanceof Error ? err.message : String(err)})`);
|
|
1458
|
-
}
|
|
1459
|
-
if (parsed?.gates === undefined || parsed.gates === null)
|
|
1460
|
-
return { gates: [] };
|
|
1461
|
-
if (!Array.isArray(parsed.gates))
|
|
1462
|
-
throw new Error(`${path}: "gates" is not an array`);
|
|
1463
|
-
return { gates: parsed.gates };
|
|
1464
|
-
}
|
|
1465
|
-
export const gatesAction = async (ctx, name) => {
|
|
1466
|
-
const data = readGatesFile(ctx.cwd, name);
|
|
1467
|
-
const showAll = ctx.flags.all === true;
|
|
1468
|
-
const gates = showAll ? data.gates : data.gates.filter((g) => g.status === 'pending');
|
|
1469
|
-
if (orgJson(ctx))
|
|
1470
|
-
return printOrgJson({ v: 1, org: name, items: gates });
|
|
1471
|
-
if (!gates.length) {
|
|
1472
|
-
log(output.info(showAll
|
|
1473
|
-
? `No gates for org "${name}"`
|
|
1474
|
-
: `No pending gates for org "${name}" (use --all to include resolved)`));
|
|
1475
|
-
return { success: true };
|
|
1476
|
-
}
|
|
1477
|
-
log(output.info(`${showAll ? 'All' : 'Pending'} gates for org "${name}" (${gates.length}):\n`));
|
|
1478
|
-
for (const g of gates) {
|
|
1479
|
-
const status = g.status === 'pending'
|
|
1480
|
-
? '⏳ pending'
|
|
1481
|
-
: g.status === 'approved'
|
|
1482
|
-
? '✅ approved'
|
|
1483
|
-
: '❌ rejected';
|
|
1484
|
-
log(output.info(` ${g.id} ${status} role:${g.roleId}`));
|
|
1485
|
-
log(output.info(` name: ${g.name}`));
|
|
1486
|
-
log(output.info(` desc: ${g.description}`));
|
|
1487
|
-
if (g.resolution)
|
|
1488
|
-
log(output.info(` resolution: ${g.resolution}`));
|
|
1489
|
-
log('');
|
|
1490
|
-
}
|
|
1491
|
-
return { success: true, message: `${gates.length} gate(s)` };
|
|
1492
|
-
};
|
|
1493
|
-
export const gateResolveAction = async (ctx, name, approved) => {
|
|
1494
|
-
const gateId = ctx.args[1];
|
|
1495
|
-
const resolution = ctx.args.slice(2).join(' ') || undefined;
|
|
1496
|
-
if (!gateId)
|
|
1497
|
-
return {
|
|
1498
|
-
success: false,
|
|
1499
|
-
message: `usage: monomind org gate-${approved ? 'approve' : 'reject'} ${name} <gate-id> [resolution]`,
|
|
1500
|
-
};
|
|
1501
|
-
let data;
|
|
1502
|
-
try {
|
|
1503
|
-
data = readGatesFileStrict(ctx.cwd, name);
|
|
1504
|
-
}
|
|
1505
|
-
catch (err) {
|
|
1506
|
-
log(output.error(`Cannot read gates for org ${name}: ${err instanceof Error ? err.message : String(err)}`));
|
|
1507
|
-
return { success: false, message: 'gates.json unreadable — gate not resolved' };
|
|
1508
|
-
}
|
|
1509
|
-
const gate = data.gates.find((g) => g.id === gateId);
|
|
1510
|
-
if (!gate)
|
|
1511
|
-
return { success: false, message: `gate "${gateId}" not found for org "${name}"` };
|
|
1512
|
-
if (gate.status !== 'pending')
|
|
1513
|
-
return { success: false, message: `gate "${gateId}" already resolved (${gate.status})` };
|
|
1514
|
-
const byFlag = await resolverFlag(ctx);
|
|
1515
|
-
if (!byFlag.ok)
|
|
1516
|
-
return { success: false, message: byFlag.message };
|
|
1517
|
-
const resolvedBy = byFlag.by;
|
|
1518
|
-
// SEC: gate resolution is a human decision — operator credential only.
|
|
1519
|
-
const { lookupOrg, readOperatorCredential } = await import('../orgrt/broker.js');
|
|
1520
|
-
const remote = lookupOrg(name);
|
|
1521
|
-
if (remote) {
|
|
1522
|
-
const cred = readOperatorCredential(name);
|
|
1523
|
-
try {
|
|
1524
|
-
const res = await fetch(`${remote.url}/api/resolve-gate`, {
|
|
1525
|
-
method: 'POST',
|
|
1526
|
-
headers: {
|
|
1527
|
-
'Content-Type': 'application/json',
|
|
1528
|
-
...(cred ? { 'x-monomind-cred': cred } : {}),
|
|
1529
|
-
},
|
|
1530
|
-
body: JSON.stringify({ org: name, gateId, approved, resolution, resolvedBy }),
|
|
1531
|
-
signal: AbortSignal.timeout(10_000),
|
|
1532
|
-
});
|
|
1533
|
-
const d = (await res.json().catch(() => ({})));
|
|
1534
|
-
if (res.ok && d.ok) {
|
|
1535
|
-
if (orgJson(ctx))
|
|
1536
|
-
return printOrgJson({
|
|
1537
|
-
v: 1,
|
|
1538
|
-
org: name,
|
|
1539
|
-
gate_id: gateId,
|
|
1540
|
-
status: approved ? 'approved' : 'rejected',
|
|
1541
|
-
delivery: 'live',
|
|
1542
|
-
resolvedBy,
|
|
1543
|
-
});
|
|
1544
|
-
log(output.success(`Gate ${gateId} ${approved ? 'approved' : 'rejected'} (live).`));
|
|
1545
|
-
return { success: true, message: `gate ${approved ? 'approved' : 'rejected'}` };
|
|
1546
|
-
}
|
|
1547
|
-
log(output.warning(`Live resolution rejected (${d.error ?? res.status}) — falling back to offline queue.`));
|
|
1548
|
-
}
|
|
1549
|
-
catch (err) {
|
|
1550
|
-
log(output.warning(`Hosting daemon unreachable (${err instanceof Error ? err.message : 'error'}) — falling back to offline queue.`));
|
|
1551
|
-
}
|
|
1552
|
-
}
|
|
1553
|
-
// Offline path: no live daemon hosts this org (or the live call failed) —
|
|
1554
|
-
// resolve gates.json directly, same as resolveApproval's offline branch.
|
|
1555
|
-
// Re-read fresh (mirrors answerAction/resolveApproval) in case a live
|
|
1556
|
-
// daemon resolved this gate concurrently while the live call above was
|
|
1557
|
-
// in flight or timing out.
|
|
1558
|
-
const { writeGates } = await import('../orgrt/decisions.js');
|
|
1559
|
-
let fresh;
|
|
1560
|
-
try {
|
|
1561
|
-
fresh = readGatesFileStrict(ctx.cwd, name);
|
|
1562
|
-
}
|
|
1563
|
-
catch (err) {
|
|
1564
|
-
log(output.error(`Refusing to rewrite gates.json — ${err instanceof Error ? err.message : String(err)}`));
|
|
1565
|
-
log(output.warning(`The gate was NOT resolved. Fix or restore ${join(ctx.cwd, ORG_DIR, name, 'gates.json')}, then retry.`));
|
|
1566
|
-
return { success: false, message: 'gates.json unreadable — gate not resolved' };
|
|
1567
|
-
}
|
|
1568
|
-
const idx = fresh.gates.findIndex((g) => g.id === gateId);
|
|
1569
|
-
if (idx === -1)
|
|
1570
|
-
return { success: false, message: `gate "${gateId}" not found for org "${name}"` };
|
|
1571
|
-
if (fresh.gates[idx].status !== 'pending') {
|
|
1572
|
-
return {
|
|
1573
|
-
success: false,
|
|
1574
|
-
message: `gate "${gateId}" already resolved (${fresh.gates[idx].status})`,
|
|
1575
|
-
};
|
|
1576
|
-
}
|
|
1577
|
-
fresh.gates[idx] = {
|
|
1578
|
-
...fresh.gates[idx],
|
|
1579
|
-
status: approved ? 'approved' : 'rejected',
|
|
1580
|
-
resolvedAt: Date.now(),
|
|
1581
|
-
resolvedBy,
|
|
1582
|
-
resolution,
|
|
1583
|
-
};
|
|
1584
|
-
writeGates(ctx.cwd, name, fresh);
|
|
1585
|
-
if (orgJson(ctx))
|
|
1586
|
-
return printOrgJson({
|
|
1587
|
-
v: 1,
|
|
1588
|
-
org: name,
|
|
1589
|
-
gate_id: gateId,
|
|
1590
|
-
status: approved ? 'approved' : 'rejected',
|
|
1591
|
-
delivery: 'recorded',
|
|
1592
|
-
resolvedBy,
|
|
1593
|
-
});
|
|
1594
|
-
log(output.success(`Gate ${gateId} ${approved ? 'approved' : 'rejected'} — ${name} picks it up on its next cycle.`));
|
|
1595
|
-
return { success: true, message: `gate ${approved ? 'approved' : 'rejected'} (queued)` };
|
|
1596
|
-
};
|
|
1597
|
-
// ─── org events — the only genuinely new org command (plan D12) ─────────────
|
|
1598
|
-
/** Resolve a `--since` cursor: an event id (skip everything at/before it) or
|
|
1599
|
-
* an ISO-8601 timestamp (skip older events). Returns null when unusable. */
|
|
1600
|
-
function parseSinceCursor(raw) {
|
|
1601
|
-
if (typeof raw !== 'string' || !raw)
|
|
1602
|
-
return null;
|
|
1603
|
-
if (/^run-[A-Za-z0-9-]+/.test(raw))
|
|
1604
|
-
return { id: raw };
|
|
1605
|
-
const ts = Date.parse(raw);
|
|
1606
|
-
return Number.isFinite(ts) ? { iso: raw } : null;
|
|
1607
|
-
}
|
|
1608
|
-
/** `org events <name> [--run id] [--follow] [--since <eventId|iso>] [--ndjson]`
|
|
1609
|
-
* — live tail of a run's bus.jsonl as NDJSON, one BusEvent per line
|
|
1610
|
-
* (protocol §7.3). This is the machine streaming surface; `org logs` stays
|
|
1611
|
-
* the human one. NDJSON is the only output mode (--ndjson accepted for
|
|
1612
|
-
* spec symmetry). */
|
|
1613
|
-
export const eventsAction = async (ctx, name) => {
|
|
1614
|
-
const run = resolveRun(ctx.cwd, name, ctx.flags.run);
|
|
1615
|
-
if (!run)
|
|
1616
|
-
return {
|
|
1617
|
-
success: false,
|
|
1618
|
-
message: `no runs found for org ${name} — start one with: monomind org run ${name}`,
|
|
1619
|
-
};
|
|
1620
|
-
const file = join(ctx.cwd, ORG_DIR, name, run, 'bus.jsonl');
|
|
1621
|
-
const since = parseSinceCursor(ctx.flags.since);
|
|
1622
|
-
let skippedPastId = !since?.id; // true = no id cursor, nothing to skip
|
|
1623
|
-
let seenLines = 0;
|
|
1624
|
-
const emitLine = (line) => {
|
|
1625
|
-
let e;
|
|
1626
|
-
try {
|
|
1627
|
-
e = JSON.parse(line);
|
|
1628
|
-
}
|
|
1629
|
-
catch {
|
|
1630
|
-
return; // corrupt interior line — skip, same policy as logsAction
|
|
1631
|
-
}
|
|
1632
|
-
if (since?.id) {
|
|
1633
|
-
if (!skippedPastId) {
|
|
1634
|
-
if (e.id === since.id)
|
|
1635
|
-
skippedPastId = true;
|
|
1636
|
-
return; // everything strictly before the cursor is replay-suppressed
|
|
1637
|
-
}
|
|
1638
|
-
}
|
|
1639
|
-
if (since?.iso && Date.parse(since.iso) && new Date(e.ts).getTime() < Date.parse(since.iso))
|
|
1640
|
-
return;
|
|
1641
|
-
process.stdout.write(`${JSON.stringify(e)}\n`);
|
|
1642
|
-
};
|
|
1643
|
-
const drain = () => {
|
|
1644
|
-
if (!existsSync(file))
|
|
1645
|
-
return;
|
|
1646
|
-
const lines = readFileSync(file, 'utf8').split('\n').filter(Boolean);
|
|
1647
|
-
for (let i = seenLines; i < lines.length; i++) {
|
|
1648
|
-
// Only the FINAL line can be a partial mid-append write worth retrying.
|
|
1649
|
-
if (i === lines.length - 1) {
|
|
1650
|
-
try {
|
|
1651
|
-
JSON.parse(lines[i]);
|
|
1652
|
-
}
|
|
1653
|
-
catch {
|
|
1654
|
-
break;
|
|
1655
|
-
}
|
|
1656
|
-
}
|
|
1657
|
-
emitLine(lines[i]);
|
|
1658
|
-
seenLines = i + 1;
|
|
1659
|
-
}
|
|
1660
|
-
};
|
|
1661
|
-
drain();
|
|
1662
|
-
if (ctx.flags.follow !== true)
|
|
1663
|
-
return { success: true };
|
|
1664
|
-
await new Promise((resolve) => {
|
|
1665
|
-
const iv = setInterval(drain, 500);
|
|
1666
|
-
process.once('SIGINT', () => {
|
|
1667
|
-
clearInterval(iv);
|
|
1668
|
-
resolve();
|
|
1669
|
-
});
|
|
1670
|
-
process.once('SIGTERM', () => {
|
|
1671
|
-
clearInterval(iv);
|
|
1672
|
-
resolve();
|
|
1673
|
-
});
|
|
1674
|
-
});
|
|
1675
|
-
return { success: true };
|
|
1676
|
-
};
|
|
2
|
+
// Read-side org subcommand actions. Each subcommand family lives in its own
|
|
3
|
+
// org-observe-*.ts module; this module re-exports them so existing import
|
|
4
|
+
// paths (tests, and the org subcommand tables' lazy imports) keep working.
|
|
5
|
+
export { approvalsAction, approveAction, denyAction, } from './org-observe-approvals.js';
|
|
6
|
+
export { branchAction, decisionsAction, replayAction, resumeFromAction, } from './org-observe-checkpoint.js';
|
|
7
|
+
export { createAction, validateAction, } from './org-observe-config.js';
|
|
8
|
+
export { gateResolveAction, gatesAction, } from './org-observe-gates.js';
|
|
9
|
+
export { inboxAction } from './org-observe-inbox.js';
|
|
10
|
+
export { eventsAction, logsAction, watchAction, } from './org-observe-logs.js';
|
|
11
|
+
export { answerAction, questionsAction, } from './org-observe-questions.js';
|
|
12
|
+
export { costsAction, flowAction, reportAction, } from './org-observe-report.js';
|
|
13
|
+
export { orgJson, printOrgJson, } from './org-observe-shared.js';
|
|
1677
14
|
//# sourceMappingURL=org-observe.js.map
|