@pugi/cli 0.1.0-beta.8 → 0.1.0-beta.87
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/CHANGELOG.md +96 -0
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/assets/pugi-prozr2-mascot.ansi +9 -0
- package/bin/run.js +33 -1
- package/dist/commands/deploy.js +40 -40
- package/dist/commands/flatten.js +191 -0
- package/dist/commands/jobs-watch.js +201 -0
- package/dist/commands/jobs.js +42 -27
- package/dist/commands/smoke.js +133 -0
- package/dist/core/agent-progress/cleanup.js +134 -0
- package/dist/core/agent-progress/schema.js +144 -0
- package/dist/core/agent-progress/writer.js +101 -0
- package/dist/core/agents/adaptive-router.js +330 -0
- package/dist/core/agents/query-decomposer.js +297 -0
- package/dist/core/agents/registry.js +2 -2
- package/dist/core/approvals/shortcut-resolver.js +98 -0
- package/dist/core/artifact-chain/dispatcher.js +148 -0
- package/dist/core/artifact-chain/exporter.js +164 -0
- package/dist/core/artifact-chain/state.js +243 -0
- package/dist/core/artifact-chain/steps.js +169 -0
- package/dist/core/ask-user/question.js +92 -0
- package/dist/core/audit/audit-trail.js +275 -0
- package/dist/core/auth/ensure-authenticated.js +129 -0
- package/dist/core/auth/env-provider.js +238 -0
- package/dist/core/auto-open-browser.js +4 -4
- package/dist/core/auto-update/channels.js +122 -0
- package/dist/core/auto-update/checker.js +241 -0
- package/dist/core/auto-update/state.js +235 -0
- package/dist/core/bare-mode/index.js +107 -0
- package/dist/core/bash/redirect.js +281 -0
- package/dist/core/bash-classifier.js +436 -40
- package/dist/core/checkpoint/resumer.js +149 -0
- package/dist/core/checkpoint/rewinder.js +291 -0
- package/dist/core/checkpoints/shadow-git.js +670 -0
- package/dist/core/citations/parser.js +109 -0
- package/dist/core/classifier/yolo-classifier.js +88 -0
- package/dist/core/codegraph/decision-store.js +248 -0
- package/dist/core/codegraph/detect-repo.js +459 -0
- package/dist/core/codegraph/install.js +134 -0
- package/dist/core/codegraph/offer-hook.js +220 -0
- package/dist/core/compact/auto-trigger.js +96 -0
- package/dist/core/compact/buffer-rewriter.js +115 -0
- package/dist/core/compact/summarizer.js +208 -0
- package/dist/core/compact/token-counter.js +108 -0
- package/dist/core/consensus/anvil-fanout.js +25 -25
- package/dist/core/consensus/diff-capture.js +121 -12
- package/dist/core/consensus/rubric.js +21 -21
- package/dist/core/context/builder.js +6 -6
- package/dist/core/context/compaction-events.js +8 -8
- package/dist/core/context/compaction.js +31 -31
- package/dist/core/context/index.js +15 -8
- package/dist/core/context/invariants.js +51 -51
- package/dist/core/context/markdown-loader.js +28 -10
- package/dist/core/context/markdown-traverse.js +255 -0
- package/dist/core/context/pugiignore.js +41 -41
- package/dist/core/context/repo-skeleton.js +37 -37
- package/dist/core/context/tool-eviction.js +55 -0
- package/dist/core/context/watcher.js +32 -32
- package/dist/core/context/working-set.js +23 -23
- package/dist/core/coordinator/agent-tools.js +77 -0
- package/dist/core/coordinator/agent-toolset.js +65 -0
- package/dist/core/coordinator/fsm.js +73 -0
- package/dist/core/coordinator/mode-fsm.js +70 -0
- package/dist/core/cost/rate-card.js +129 -0
- package/dist/core/cost/tracker.js +221 -0
- package/dist/core/credentials.js +12 -12
- package/dist/core/cron/scheduler.js +138 -0
- package/dist/core/denial-tracking/index.js +8 -0
- package/dist/core/denial-tracking/state.js +264 -0
- package/dist/core/diagnostics/probe-runner.js +93 -0
- package/dist/core/diagnostics/probes/api.js +46 -0
- package/dist/core/diagnostics/probes/auth.js +93 -0
- package/dist/core/diagnostics/probes/bare-mode.js +42 -0
- package/dist/core/diagnostics/probes/cli-version.js +127 -0
- package/dist/core/diagnostics/probes/config.js +72 -0
- package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
- package/dist/core/diagnostics/probes/disk.js +81 -0
- package/dist/core/diagnostics/probes/engine-live.js +46 -0
- package/dist/core/diagnostics/probes/git.js +65 -0
- package/dist/core/diagnostics/probes/hooks.js +118 -0
- package/dist/core/diagnostics/probes/mcp.js +75 -0
- package/dist/core/diagnostics/probes/node.js +59 -0
- package/dist/core/diagnostics/probes/pnpm.js +36 -0
- package/dist/core/diagnostics/probes/pugi-md.js +89 -0
- package/dist/core/diagnostics/probes/sandbox.js +40 -0
- package/dist/core/diagnostics/probes/session.js +74 -0
- package/dist/core/diagnostics/probes/status-snapshot.js +488 -0
- package/dist/core/diagnostics/probes/workspace.js +63 -0
- package/dist/core/diagnostics/types.js +70 -0
- package/dist/core/dispatch/cache-cleanup.js +197 -0
- package/dist/core/dispatch/cache-handoff.js +295 -0
- package/dist/core/edits/apply-patch-layer-e.js +189 -0
- package/dist/core/edits/dispatch.js +293 -7
- package/dist/core/edits/format-matrix.js +26 -0
- package/dist/core/edits/fuzzy-ladder.js +650 -0
- package/dist/core/edits/index.js +3 -1
- package/dist/core/edits/journal.js +199 -0
- package/dist/core/edits/layer-a-apply.js +15 -15
- package/dist/core/edits/layer-a-fuzzy-apply.js +198 -0
- package/dist/core/edits/layer-b-apply.js +9 -9
- package/dist/core/edits/layer-c-apply.js +6 -6
- package/dist/core/edits/layer-d-ast.js +557 -14
- package/dist/core/edits/marker-parser.js +12 -12
- package/dist/core/edits/security-gate.js +27 -27
- package/dist/core/edits/verify-hook.js +273 -0
- package/dist/core/edits/worktree.js +322 -0
- package/dist/core/engine/anvil-client.js +140 -26
- package/dist/core/engine/auto-compact.js +179 -0
- package/dist/core/engine/budgets.js +186 -0
- package/dist/core/engine/context-prefix.js +155 -0
- package/dist/core/engine/index.js +1 -1
- package/dist/core/engine/intensity.js +158 -0
- package/dist/core/engine/intent.js +260 -0
- package/dist/core/engine/native-pugi.js +1295 -227
- package/dist/core/engine/prompts.js +134 -16
- package/dist/core/engine/strip-internal-fields.js +124 -0
- package/dist/core/engine/tool-bridge.js +1295 -59
- package/dist/core/evaluation/golden-dataset.js +293 -0
- package/dist/core/feedback/queue.js +177 -0
- package/dist/core/feedback/submitter.js +145 -0
- package/dist/core/file-cache.js +113 -1
- package/dist/core/flatten/flatten-repo.js +439 -0
- package/dist/core/format/osc8-link.js +28 -0
- package/dist/core/hook-chains.js +392 -0
- package/dist/core/hooks/citation-verify-hook.js +138 -0
- package/dist/core/hooks/citation-verify.js +112 -0
- package/dist/core/hooks/events.js +44 -0
- package/dist/core/hooks/index.js +15 -0
- package/dist/core/hooks/registry.js +213 -0
- package/dist/core/hooks/runner.js +236 -0
- package/dist/core/hooks/v2/event-emitter.js +115 -0
- package/dist/core/hooks/v2/executor.js +282 -0
- package/dist/core/hooks/v2/index.js +25 -0
- package/dist/core/hooks/v2/lifecycle.js +104 -0
- package/dist/core/hooks/v2/loader.js +216 -0
- package/dist/core/hooks/v2/matcher.js +125 -0
- package/dist/core/hooks/v2/trust.js +143 -0
- package/dist/core/hooks/v2/types.js +86 -0
- package/dist/core/image/renderer.js +71 -0
- package/dist/core/init/detector.js +582 -0
- package/dist/core/init/template-renderer.js +242 -0
- package/dist/core/jobs/registry.js +18 -18
- package/dist/core/ledger/results-tsv.js +142 -0
- package/dist/core/log-discipline/stdout-redirect.js +51 -0
- package/dist/core/lsp/cache.js +105 -0
- package/dist/core/lsp/client.js +776 -0
- package/dist/core/lsp/language-detect.js +66 -0
- package/dist/core/lsp/post-edit-diagnostics.js +171 -0
- package/dist/core/lsp/symbol-tools.js +372 -0
- package/dist/core/mcp/client.js +97 -28
- package/dist/core/mcp/http-server.js +553 -0
- package/dist/core/mcp/orchestrator-tools.js +662 -0
- package/dist/core/mcp/permission.js +190 -0
- package/dist/core/mcp/registry.js +39 -17
- package/dist/core/mcp/server-tools.js +219 -0
- package/dist/core/mcp/server.js +397 -0
- package/dist/core/mcp/trust.js +10 -10
- package/dist/core/memory/dual-write.js +416 -0
- package/dist/core/memory/passive-extract.js +130 -0
- package/dist/core/memory/phase1-kinds.js +20 -0
- package/dist/core/memory/secret-scanner.js +304 -0
- package/dist/core/memory-sync/queue.js +170 -0
- package/dist/core/metrics/extract.js +113 -0
- package/dist/core/modes/roo-modes.js +68 -0
- package/dist/core/onboarding/ensure-initialized.js +133 -0
- package/dist/core/onboarding/marker.js +111 -0
- package/dist/core/onboarding/telemetry-state.js +108 -0
- package/dist/core/output-style/presets.js +176 -0
- package/dist/core/output-style/state.js +185 -0
- package/dist/core/path-security.js +287 -5
- package/dist/core/permission.js +82 -22
- package/dist/core/permissions/auto-classifier.js +124 -0
- package/dist/core/permissions/bash-parser.js +371 -0
- package/dist/core/permissions/circuit-breaker.js +83 -0
- package/dist/core/permissions/constrained-edit.js +91 -0
- package/dist/core/permissions/gate.js +278 -0
- package/dist/core/permissions/index.js +20 -0
- package/dist/core/permissions/mode.js +174 -0
- package/dist/core/permissions/network-egress.js +137 -0
- package/dist/core/permissions/state.js +241 -0
- package/dist/core/permissions/tool-class.js +93 -0
- package/dist/core/plan-mode/ui-state.js +51 -0
- package/dist/core/plans/plan-artifact.js +721 -0
- package/dist/core/policy-limits/etag-store.js +122 -0
- package/dist/core/prd-check/parser.js +215 -0
- package/dist/core/prd-check/reporter.js +127 -0
- package/dist/core/prd-check/session-review.js +557 -0
- package/dist/core/prd-check/verifiers.js +223 -0
- package/dist/core/prompt-cache/client-cache.js +99 -0
- package/dist/core/prompts/assembly.js +29 -0
- package/dist/core/prompts/registry.js +364 -0
- package/dist/core/pugi-md/cc-compat-rules.js +735 -0
- package/dist/core/pugi-md/context-injector.js +76 -0
- package/dist/core/pugi-md/walk-up.js +207 -0
- package/dist/core/python/uv-installer.js +270 -0
- package/dist/core/python/uv-resolver.js +83 -0
- package/dist/core/rate-limit/narrator.js +146 -0
- package/dist/core/recipes/cli-types.js +20 -0
- package/dist/core/recipes/loader.js +103 -0
- package/dist/core/recipes/runner.js +345 -0
- package/dist/core/recipes/schema.js +587 -0
- package/dist/core/release-notes/parser.js +241 -0
- package/dist/core/release-notes/state.js +116 -0
- package/dist/core/repl/ask.js +37 -37
- package/dist/core/repl/cancellation.js +26 -26
- package/dist/core/repl/cap-warning.js +4 -4
- package/dist/core/repl/clipboard-read.js +11 -11
- package/dist/core/repl/dispatch-fsm.js +12 -12
- package/dist/core/repl/history-search.js +15 -15
- package/dist/core/repl/history.js +28 -18
- package/dist/core/repl/kill-ring.js +5 -5
- package/dist/core/repl/model-pricing.js +135 -0
- package/dist/core/repl/privacy-banner.js +22 -22
- package/dist/core/repl/session.js +2157 -214
- package/dist/core/repl/slash-commands.js +533 -40
- package/dist/core/repl/store/index.js +1 -1
- package/dist/core/repl/store/jsonl-log.js +22 -22
- package/dist/core/repl/store/lockfile.js +10 -10
- package/dist/core/repl/store/session-store.js +136 -107
- package/dist/core/repl/store/types.js +15 -15
- package/dist/core/repl/store/uuid-v7.js +12 -12
- package/dist/core/repl/workspace-context.js +43 -21
- package/dist/core/repo-map/build.js +125 -0
- package/dist/core/repo-map/cache.js +185 -0
- package/dist/core/repo-map/extractor.js +254 -0
- package/dist/core/repo-map/formatter.js +145 -0
- package/dist/core/repo-map/page-rank.js +105 -0
- package/dist/core/repo-map/scanner.js +211 -0
- package/dist/core/retry-budget/budget.js +284 -0
- package/dist/core/retry-budget/index.js +5 -0
- package/dist/core/retry-budget/retry-cap.js +74 -0
- package/dist/core/routing/lead-worker.js +43 -0
- package/dist/core/routing/pre-flight-estimator.js +108 -0
- package/dist/core/runs/run-tree.js +103 -0
- package/dist/core/security/injection-scanner.js +367 -0
- package/dist/core/security/output-filter.js +418 -0
- package/dist/core/session/env-file.js +105 -0
- package/dist/core/session/section-budgets.js +140 -0
- package/dist/core/session.js +92 -0
- package/dist/core/settings.js +286 -5
- package/dist/core/share/formatter.js +271 -0
- package/dist/core/share/redactor.js +221 -0
- package/dist/core/share/uploader.js +267 -0
- package/dist/core/skills/defaults.js +457 -0
- package/dist/core/skills/loader.js +22 -22
- package/dist/core/skills/sources.js +27 -27
- package/dist/core/smoke/headless-driver.js +174 -0
- package/dist/core/smoke/orchestrator.js +194 -0
- package/dist/core/smoke/runner.js +238 -0
- package/dist/core/smoke/scenario-parser.js +316 -0
- package/dist/core/statusline.js +99 -0
- package/dist/core/subagents/dispatcher-real.js +600 -0
- package/dist/core/subagents/dispatcher.js +132 -43
- package/dist/core/subagents/index.js +19 -6
- package/dist/core/subagents/isolation-matrix.js +213 -0
- package/dist/core/subagents/spawn.js +19 -4
- package/dist/core/telemetry/emitter.js +229 -0
- package/dist/core/telemetry/queue.js +251 -0
- package/dist/core/theme/context.js +91 -0
- package/dist/core/theme/presets.js +228 -0
- package/dist/core/theme/state.js +181 -0
- package/dist/core/todos/invariant.js +10 -0
- package/dist/core/todos/state.js +177 -0
- package/dist/core/tool-schema/compressor.js +89 -0
- package/dist/core/transport/version-interceptor.js +166 -0
- package/dist/core/trust.js +2 -2
- package/dist/core/tui/thinking-block.js +64 -0
- package/dist/core/vim/keymap.js +288 -0
- package/dist/core/vim/state.js +92 -0
- package/dist/core/watch-markers/marker-watcher.js +133 -0
- package/dist/core/worktree-manager/cleanup.js +123 -0
- package/dist/core/worktree-manager/manager.js +303 -0
- package/dist/index.js +28 -0
- package/dist/runtime/bootstrap.js +190 -0
- package/dist/runtime/cli.js +4151 -489
- package/dist/runtime/commands/agents.js +30 -30
- package/dist/runtime/commands/budget.js +5 -5
- package/dist/runtime/commands/cancel.js +231 -0
- package/dist/runtime/commands/chain.js +489 -0
- package/dist/runtime/commands/codegraph-status.js +227 -0
- package/dist/runtime/commands/compact.js +297 -0
- package/dist/runtime/commands/config.js +32 -32
- package/dist/runtime/commands/cost.js +199 -0
- package/dist/runtime/commands/delegate.js +244 -13
- package/dist/runtime/commands/dispatch.js +126 -0
- package/dist/runtime/commands/doctor.js +579 -0
- package/dist/runtime/commands/feedback.js +184 -0
- package/dist/runtime/commands/hooks.js +184 -0
- package/dist/runtime/commands/init.js +254 -0
- package/dist/runtime/commands/lsp.js +368 -0
- package/dist/runtime/commands/mcp.js +879 -0
- package/dist/runtime/commands/memory.js +582 -0
- package/dist/runtime/commands/model.js +237 -0
- package/dist/runtime/commands/onboarding.js +275 -0
- package/dist/runtime/commands/patch.js +128 -0
- package/dist/runtime/commands/permissions.js +112 -0
- package/dist/runtime/commands/plan.js +143 -0
- package/dist/runtime/commands/prd-check.js +285 -0
- package/dist/runtime/commands/privacy.js +17 -17
- package/dist/runtime/commands/recipe.js +325 -0
- package/dist/runtime/commands/redo-blob-store.js +92 -0
- package/dist/runtime/commands/redo.js +361 -0
- package/dist/runtime/commands/release-notes.js +229 -0
- package/dist/runtime/commands/repo-map.js +95 -0
- package/dist/runtime/commands/report.js +299 -0
- package/dist/runtime/commands/resume.js +118 -0
- package/dist/runtime/commands/review-consensus.js +68 -53
- package/dist/runtime/commands/rewind.js +333 -0
- package/dist/runtime/commands/roster.js +14 -14
- package/dist/runtime/commands/sessions.js +163 -0
- package/dist/runtime/commands/share.js +316 -0
- package/dist/runtime/commands/skills.js +31 -31
- package/dist/runtime/commands/status.js +186 -0
- package/dist/runtime/commands/stickers.js +82 -0
- package/dist/runtime/commands/style.js +194 -0
- package/dist/runtime/commands/theme.js +196 -0
- package/dist/runtime/commands/undo.js +54 -22
- package/dist/runtime/commands/update.js +289 -0
- package/dist/runtime/commands/vim.js +140 -0
- package/dist/runtime/commands/worktree.js +177 -0
- package/dist/runtime/commands/worktrees.js +155 -0
- package/dist/runtime/headless-repl.js +195 -0
- package/dist/runtime/headless.js +543 -0
- package/dist/runtime/load-hooks-or-exit.js +71 -0
- package/dist/runtime/plan-decompose.js +531 -0
- package/dist/runtime/update-check.js +28 -28
- package/dist/runtime/version.js +65 -0
- package/dist/skills/bundled/batch.js +617 -0
- package/dist/skills/bundled/index.js +45 -0
- package/dist/skills/bundled/loop.js +358 -0
- package/dist/skills/bundled/remember.js +383 -0
- package/dist/skills/bundled/simplify.js +289 -0
- package/dist/skills/bundled/skillify.js +373 -0
- package/dist/skills/bundled/stuck.js +558 -0
- package/dist/skills/bundled/verify.js +439 -0
- package/dist/testing/vcr.js +486 -0
- package/dist/tools/agent-tool.js +229 -0
- package/dist/tools/apply-patch.js +556 -0
- package/dist/tools/ask-user-question.js +222 -0
- package/dist/tools/ask-user.js +115 -0
- package/dist/tools/bash.js +623 -45
- package/dist/tools/brief.js +224 -0
- package/dist/tools/enter-worktree.js +250 -0
- package/dist/tools/exit-worktree.js +147 -0
- package/dist/tools/file-tools.js +161 -44
- package/dist/tools/lsp-tools.js +189 -0
- package/dist/tools/mcp-tool.js +260 -0
- package/dist/tools/multi-edit.js +361 -0
- package/dist/tools/powershell.js +268 -0
- package/dist/tools/registry.js +85 -0
- package/dist/tools/skill-tool.js +96 -0
- package/dist/tools/sleep.js +99 -0
- package/dist/tools/synthetic-output.js +133 -0
- package/dist/tools/tasks.js +208 -0
- package/dist/tools/todo-write.js +184 -0
- package/dist/tools/verify-plan-execution.js +295 -0
- package/dist/tools/web-fetch-injection-scanner.js +207 -0
- package/dist/tools/web-fetch.js +195 -10
- package/dist/tools/web-search.js +458 -0
- package/dist/tui/agent-progress-card.js +111 -0
- package/dist/tui/agent-tree.js +11 -1
- package/dist/tui/ask-modal.js +14 -14
- package/dist/tui/ask-user-question-prompt.js +203 -0
- package/dist/tui/compact-banner.js +81 -0
- package/dist/tui/conversation-pane.js +85 -11
- package/dist/tui/cost-table.js +111 -0
- package/dist/tui/device-flow.js +2 -2
- package/dist/tui/doctor-table.js +46 -0
- package/dist/tui/feedback-prompt.js +156 -0
- package/dist/tui/input-box.js +247 -32
- package/dist/tui/login-picker.js +3 -3
- package/dist/tui/markdown-render.js +6 -6
- package/dist/tui/onboarding-wizard.js +240 -0
- package/dist/tui/permissions-picker.js +86 -0
- package/dist/tui/render.js +35 -0
- package/dist/tui/repl-render.js +332 -54
- package/dist/tui/repl-splash-art.js +16 -16
- package/dist/tui/repl-splash-mascot.js +48 -24
- package/dist/tui/repl-splash.js +22 -22
- package/dist/tui/repl.js +124 -44
- package/dist/tui/slash-palette.js +6 -6
- package/dist/tui/splash.js +2 -2
- package/dist/tui/status-bar.js +109 -31
- package/dist/tui/status-table.js +7 -0
- package/dist/tui/stickers-art.js +136 -0
- package/dist/tui/style-table.js +28 -0
- package/dist/tui/theme-table.js +29 -0
- package/dist/tui/thinking-spinner.js +123 -0
- package/dist/tui/tool-stream-pane.js +53 -4
- package/dist/tui/update-banner.js +27 -2
- package/dist/tui/vim-input.js +267 -0
- package/dist/tui/welcome-banner.js +107 -0
- package/dist/tui/welcome-data.js +293 -0
- package/dist/tui/workspace-context.js +2 -2
- package/docs/examples/codegraph.mcp.json +10 -0
- package/package.json +23 -6
- package/test/scenarios/codegen-create-file.scenario.txt +13 -0
- package/test/scenarios/compact-force.scenario.txt +11 -0
- package/test/scenarios/identity.scenario.txt +11 -0
- package/test/scenarios/persona-handoff.scenario.txt +11 -0
- package/test/scenarios/walkback.scenario.txt +12 -0
- package/dist/core/engine/compaction-hook.js +0 -154
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless mode runner for `pugi --print "<brief>"`.
|
|
3
|
+
*
|
|
4
|
+
* Design intent (CEO directive "нужно тестирование по кругу"):
|
|
5
|
+
*
|
|
6
|
+
* Pugi must be drivable non-interactively by the upstream tool, Codex, CI
|
|
7
|
+
* runners, Docker images, and any agent that can spawn a subprocess.
|
|
8
|
+
* The Ink REPL is the human-facing surface; headless mode is the
|
|
9
|
+
* machine-facing surface. One round-trip = one engine turn = one
|
|
10
|
+
* `--print` invocation.
|
|
11
|
+
*
|
|
12
|
+
* Output contract (mirrors `claude --print --json`):
|
|
13
|
+
*
|
|
14
|
+
* Stdout — ONLY NDJSON event lines (one JSON object per line):
|
|
15
|
+
* {"type":"session.start", sessionId, cwd, timestamp}
|
|
16
|
+
* {"type":"turn.start", turnIndex, timestamp}
|
|
17
|
+
* {"type":"tool.call", callId, tool, args, timestamp}
|
|
18
|
+
* {"type":"tool.result", callId, status, summary, timestamp}
|
|
19
|
+
* {"type":"text.delta", content, timestamp}
|
|
20
|
+
* {"type":"thinking.delta",content, blockId, timestamp}
|
|
21
|
+
* {"type":"turn.end", turnIndex, usage, timestamp}
|
|
22
|
+
* {"type":"session.end", status, totalTokensIn, totalTokensOut,
|
|
23
|
+
* durationMs, filesChanged, timestamp}
|
|
24
|
+
* {"type":"error", kind, message, exit_code}
|
|
25
|
+
*
|
|
26
|
+
* Stderr — human-readable status + error details (never NDJSON).
|
|
27
|
+
*
|
|
28
|
+
* Stdout vs stderr discipline lets a caller pipe cleanly:
|
|
29
|
+
*
|
|
30
|
+
* pugi --print "..." --json 2>/dev/null | jq .
|
|
31
|
+
*
|
|
32
|
+
* Exit codes:
|
|
33
|
+
*
|
|
34
|
+
* 0 session.end emitted with status=done (or "complete")
|
|
35
|
+
* 1 hard error (auth, network, server crash, engine_unavailable)
|
|
36
|
+
* 2 turn-limit / budget exhausted without completion
|
|
37
|
+
*
|
|
38
|
+
* Reuses the existing `NativePugiEngineAdapter` (the same path the
|
|
39
|
+
* REPL drives) — the only difference is the output sink: instead of
|
|
40
|
+
* mounting Ink, we subscribe to the adapter's `streamEmitter` and
|
|
41
|
+
* forward each typed event as NDJSON.
|
|
42
|
+
*/
|
|
43
|
+
import { resolve as resolvePath } from 'node:path';
|
|
44
|
+
import { existsSync, statSync } from 'node:fs';
|
|
45
|
+
import { AnvilEngineLoopClient } from '../core/engine/anvil-client.js';
|
|
46
|
+
import { NativePugiEngineAdapter } from '../core/engine/native-pugi.js';
|
|
47
|
+
import { defaultNonInteractiveMcpPrompt } from '../tools/mcp-tool.js';
|
|
48
|
+
import { loadMcpRegistry } from '../core/mcp/registry.js';
|
|
49
|
+
import { openSession, recordCommandStarted, recordCommandCompleted, } from '../core/session.js';
|
|
50
|
+
import { buildRuntimeConfig, loadRuntimeConfig, } from '@pugi/sdk';
|
|
51
|
+
import { resolveActiveCredential } from '../core/credentials.js';
|
|
52
|
+
/**
|
|
53
|
+
* NDJSON event emitter. One JSON object per line on stdout. Never emits
|
|
54
|
+
* partial lines — every event ends with `\n` so a caller piping into
|
|
55
|
+
* `jq -c` can parse incrementally. Stderr is reserved for human prose
|
|
56
|
+
* (errors, traces) so the operator's `2>/dev/null` filter keeps stdout
|
|
57
|
+
* pure machine-readable.
|
|
58
|
+
*/
|
|
59
|
+
class NdjsonEventSink {
|
|
60
|
+
stdoutWrite;
|
|
61
|
+
constructor(stdoutWrite) {
|
|
62
|
+
this.stdoutWrite = stdoutWrite;
|
|
63
|
+
}
|
|
64
|
+
emit(event) {
|
|
65
|
+
// Stamp every event with `timestamp` if the caller did not supply
|
|
66
|
+
// one. ISO-8601 with millisecond precision is the agreed contract
|
|
67
|
+
// (per spec); a downstream timeline UI can sort lexicographically.
|
|
68
|
+
const stamped = {
|
|
69
|
+
...event,
|
|
70
|
+
timestamp: event.timestamp ?? new Date().toISOString(),
|
|
71
|
+
};
|
|
72
|
+
this.stdoutWrite(`${JSON.stringify(stamped)}\n`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Human-readable text sink. Used when `--print` runs WITHOUT `--json`
|
|
77
|
+
* (a human watching the pipe). Tool calls show as `> tool(args)`,
|
|
78
|
+
* results as `< ok|fail: summary`, text deltas as raw prose chunks.
|
|
79
|
+
* Status frames are demoted to stderr so the conversation flow on
|
|
80
|
+
* stdout stays clean.
|
|
81
|
+
*/
|
|
82
|
+
class TextEventSink {
|
|
83
|
+
stdoutWrite;
|
|
84
|
+
stderrWrite;
|
|
85
|
+
constructor(stdoutWrite, stderrWrite) {
|
|
86
|
+
this.stdoutWrite = stdoutWrite;
|
|
87
|
+
this.stderrWrite = stderrWrite;
|
|
88
|
+
}
|
|
89
|
+
emit(event) {
|
|
90
|
+
switch (event.type) {
|
|
91
|
+
case 'session.start':
|
|
92
|
+
this.stderrWrite(`pugi headless: session ${event.sessionId} cwd=${event.cwd}\n`);
|
|
93
|
+
return;
|
|
94
|
+
case 'turn.start':
|
|
95
|
+
this.stderrWrite(`> turn ${event.turnIndex}\n`);
|
|
96
|
+
return;
|
|
97
|
+
case 'tool.call':
|
|
98
|
+
this.stderrWrite(`> ${event.tool}(${truncate(JSON.stringify(event.args), 80)})\n`);
|
|
99
|
+
return;
|
|
100
|
+
case 'tool.result':
|
|
101
|
+
this.stderrWrite(`< ${event.status === 'success' ? 'ok' : 'fail'}: ${truncate(String(event.summary ?? ''), 120)}\n`);
|
|
102
|
+
return;
|
|
103
|
+
case 'text.delta':
|
|
104
|
+
// Bare prose on stdout — this IS the answer.
|
|
105
|
+
this.stdoutWrite(String(event.content ?? ''));
|
|
106
|
+
return;
|
|
107
|
+
case 'thinking.delta':
|
|
108
|
+
// Thinking is informational; surface on stderr so stdout is the
|
|
109
|
+
// visible answer only. Operators who want thinking pipe stderr.
|
|
110
|
+
this.stderrWrite(`[think] ${truncate(String(event.content ?? ''), 200)}\n`);
|
|
111
|
+
return;
|
|
112
|
+
case 'turn.end':
|
|
113
|
+
this.stderrWrite(`< turn ${event.turnIndex} end\n`);
|
|
114
|
+
return;
|
|
115
|
+
case 'session.end':
|
|
116
|
+
// Newline separator so the final stdout text and stderr summary
|
|
117
|
+
// do not run together when both are tee'd to the same terminal.
|
|
118
|
+
this.stdoutWrite('\n');
|
|
119
|
+
this.stderrWrite(`pugi headless: ${event.status} · ${event.totalTokensIn ?? 0}+${event.totalTokensOut ?? 0} tokens · ${event.durationMs}ms\n`);
|
|
120
|
+
return;
|
|
121
|
+
case 'error':
|
|
122
|
+
this.stderrWrite(`pugi headless: error (${event.kind}) ${event.message}\n`);
|
|
123
|
+
return;
|
|
124
|
+
default:
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function truncate(s, max) {
|
|
130
|
+
return s.length <= max ? s : `${s.slice(0, max - 3)}...`;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Auto-init the workspace if `.pugi/` is missing. Per beta.13 the
|
|
134
|
+
* scaffold is silent — headless callers (CI, agents) drop into fresh
|
|
135
|
+
* cwds all the time and the first run must not require a manual
|
|
136
|
+
* `pugi init` step. We DO surface a stderr breadcrumb so the operator
|
|
137
|
+
* sees what changed on disk.
|
|
138
|
+
*/
|
|
139
|
+
async function ensurePugiInitializedWithLog(cwd, stderrWrite) {
|
|
140
|
+
const pugiDir = resolvePath(cwd, '.pugi');
|
|
141
|
+
if (existsSync(pugiDir) && statSync(pugiDir).isDirectory())
|
|
142
|
+
return;
|
|
143
|
+
// Late-import so the CLI bootstrap cost stays minimal — scaffold is
|
|
144
|
+
// only loaded when the auto-init path actually fires.
|
|
145
|
+
const { scaffoldPugiWorkspace } = await import('./cli.js');
|
|
146
|
+
stderrWrite(`pugi headless: scaffolding .pugi/ in ${cwd}\n`);
|
|
147
|
+
await scaffoldPugiWorkspace({
|
|
148
|
+
cwd,
|
|
149
|
+
noDefaults: true,
|
|
150
|
+
log: (line) => stderrWrite(`[init] ${line}`),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Resolve the runtime credential. Headless mode REQUIRES a token — we
|
|
155
|
+
* cannot launch a device flow without a TTY to drop a browser into. A
|
|
156
|
+
* missing credential is a hard error with exit 1.
|
|
157
|
+
*/
|
|
158
|
+
function resolveHeadlessCredential() {
|
|
159
|
+
const credential = resolveActiveCredential();
|
|
160
|
+
if (credential) {
|
|
161
|
+
return buildRuntimeConfig({
|
|
162
|
+
apiUrl: credential.apiUrl,
|
|
163
|
+
apiKey: credential.apiKey,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
// Fall through to env-only config (CI sets PUGI_API_KEY directly).
|
|
167
|
+
return loadRuntimeConfig();
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Headless entry point. Returns the desired process exit code.
|
|
171
|
+
*
|
|
172
|
+
* 0 — done / completed
|
|
173
|
+
* 1 — hard error (auth, engine_unavailable, transport crash)
|
|
174
|
+
* 2 — turn-limit / budget exhausted
|
|
175
|
+
*
|
|
176
|
+
* Caller (cli.ts) sets `process.exitCode`; we do NOT call `process.exit`
|
|
177
|
+
* directly so a future REPL-embedded `/print` slash can reuse the same
|
|
178
|
+
* function in-process without tearing down the host.
|
|
179
|
+
*/
|
|
180
|
+
export async function runHeadlessPrint(opts) {
|
|
181
|
+
const cwd = resolvePath(opts.cwd);
|
|
182
|
+
// Default writers route to the real process streams; tests inject
|
|
183
|
+
// closures so `node:test`'s binary-IPC hijack of `process.stdout`
|
|
184
|
+
// does not collide with the captured NDJSON output.
|
|
185
|
+
const stdoutWrite = opts.stdoutWrite ?? ((chunk) => process.stdout.write(chunk));
|
|
186
|
+
const stderrWrite = opts.stderrWrite ?? ((chunk) => process.stderr.write(chunk));
|
|
187
|
+
const sink = opts.json
|
|
188
|
+
? new NdjsonEventSink(stdoutWrite)
|
|
189
|
+
: new TextEventSink(stdoutWrite, stderrWrite);
|
|
190
|
+
const startedAt = Date.now();
|
|
191
|
+
// 1. Auto-init the workspace if needed.
|
|
192
|
+
try {
|
|
193
|
+
await ensurePugiInitializedWithLog(cwd, stderrWrite);
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
197
|
+
sink.emit({
|
|
198
|
+
type: 'error',
|
|
199
|
+
kind: 'init_failed',
|
|
200
|
+
message: `failed to scaffold .pugi/: ${message}`,
|
|
201
|
+
exit_code: 1,
|
|
202
|
+
});
|
|
203
|
+
return 1;
|
|
204
|
+
}
|
|
205
|
+
// 2. Resolve the credential. Headless cannot interactively log in.
|
|
206
|
+
// When a test factory is injected we still build a config (the
|
|
207
|
+
// factory ignores it) so the type contract stays narrow.
|
|
208
|
+
let config;
|
|
209
|
+
if (opts.engineClientFactory) {
|
|
210
|
+
config =
|
|
211
|
+
loadRuntimeConfig() ??
|
|
212
|
+
buildRuntimeConfig({
|
|
213
|
+
apiUrl: 'http://127.0.0.1:1/headless-fixture',
|
|
214
|
+
apiKey: 'pugi_headless_fixture',
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
config = resolveHeadlessCredential();
|
|
219
|
+
}
|
|
220
|
+
if (!config) {
|
|
221
|
+
sink.emit({
|
|
222
|
+
type: 'error',
|
|
223
|
+
kind: 'auth_missing',
|
|
224
|
+
message: 'No Pugi credential. Run `pugi login` or set PUGI_API_KEY before invoking `pugi --print` (headless mode cannot launch device flow without a TTY).',
|
|
225
|
+
exit_code: 1,
|
|
226
|
+
});
|
|
227
|
+
return 1;
|
|
228
|
+
}
|
|
229
|
+
// 3. Open a session. The session id flows through every event line
|
|
230
|
+
// so external tooling can correlate the headless run with its
|
|
231
|
+
// server-side trace.
|
|
232
|
+
const session = openSession(cwd);
|
|
233
|
+
if (opts.sessionIdOverride && opts.sessionIdOverride.length > 0) {
|
|
234
|
+
// Override is informational — we cannot reassign `session.id`
|
|
235
|
+
// safely (it threads through `recordToolCall`), but we surface
|
|
236
|
+
// the requested id alongside the resolved one so the caller can
|
|
237
|
+
// join its own logs.
|
|
238
|
+
sink.emit({
|
|
239
|
+
type: 'session.start',
|
|
240
|
+
sessionId: session.id,
|
|
241
|
+
requestedSessionId: opts.sessionIdOverride,
|
|
242
|
+
cwd,
|
|
243
|
+
...(opts.workspace ? { workspace: opts.workspace } : {}),
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
sink.emit({
|
|
248
|
+
type: 'session.start',
|
|
249
|
+
sessionId: session.id,
|
|
250
|
+
cwd,
|
|
251
|
+
...(opts.workspace ? { workspace: opts.workspace } : {}),
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
recordCommandStarted(session, 'print');
|
|
255
|
+
// 4. Construct the engine client + adapter. The factory seam exists
|
|
256
|
+
// purely for tests; production always builds `AnvilEngineLoopClient`.
|
|
257
|
+
const client = opts.engineClientFactory
|
|
258
|
+
? opts.engineClientFactory(config)
|
|
259
|
+
: new AnvilEngineLoopClient(config);
|
|
260
|
+
// MCP registry: best-effort load. Headless mode honors the workspace
|
|
261
|
+
// MCP config the same way the REPL does — a failed registry should
|
|
262
|
+
// not kill the run, but the operator deserves a stderr breadcrumb.
|
|
263
|
+
let mcpRegistry;
|
|
264
|
+
try {
|
|
265
|
+
mcpRegistry = await loadMcpRegistry(cwd);
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
stderrWrite(`pugi headless: MCP registry load failed — ${error.message}. Continuing without MCP.\n`);
|
|
269
|
+
mcpRegistry = undefined;
|
|
270
|
+
}
|
|
271
|
+
const adapter = new NativePugiEngineAdapter({
|
|
272
|
+
client,
|
|
273
|
+
session,
|
|
274
|
+
// --no-tools maps to suppressing fetch + search opt-ins. The
|
|
275
|
+
// tool-bridge schema is determined per-kind; we cannot dial it to
|
|
276
|
+
// zero from the adapter constructor today (that requires a
|
|
277
|
+
// tool-bridge plumbing change). For β1 we honor --no-tools by
|
|
278
|
+
// refusing the engine path entirely when set AND the kind is one
|
|
279
|
+
// that requires tools to be useful — see the gate below.
|
|
280
|
+
allowFetch: !opts.noTools,
|
|
281
|
+
allowSearch: !opts.noTools,
|
|
282
|
+
...(mcpRegistry ? { mcpRegistry } : {}),
|
|
283
|
+
mcpPrompt: defaultNonInteractiveMcpPrompt,
|
|
284
|
+
interactive: false,
|
|
285
|
+
});
|
|
286
|
+
// 5. Subscribe to the adapter's typed stream emitter and forward
|
|
287
|
+
// every event as one NDJSON line. The emitter is the canonical
|
|
288
|
+
// source for tool / text / thinking deltas — the async-iterable
|
|
289
|
+
// `adapter.run()` collapses richer types into lossy `status` for
|
|
290
|
+
// legacy SDK consumers, which we deliberately bypass here.
|
|
291
|
+
let turnIndex = 0;
|
|
292
|
+
let currentTurnTokensIn = 0;
|
|
293
|
+
let currentTurnTokensOut = 0;
|
|
294
|
+
let totalTokens = 0;
|
|
295
|
+
const filesChanged = new Set();
|
|
296
|
+
const sawTurnStart = false; // tracked via emitter
|
|
297
|
+
const handleStreamEvent = (event) => {
|
|
298
|
+
switch (event.type) {
|
|
299
|
+
case 'status': {
|
|
300
|
+
// Status frames carry adapter lifecycle prose. Most are noise
|
|
301
|
+
// for an NDJSON consumer; the structurally meaningful ones we
|
|
302
|
+
// synthesize into typed events below. A few worth surfacing:
|
|
303
|
+
// `turn N: requesting model` → emit turn.start.
|
|
304
|
+
const m = /^turn (\d+): requesting model/.exec(event.message);
|
|
305
|
+
if (m) {
|
|
306
|
+
turnIndex = Number(m[1]);
|
|
307
|
+
sink.emit({ type: 'turn.start', turnIndex, timestamp: event.timestamp });
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const t = /^turn (\d+): model returned final text/.exec(event.message);
|
|
311
|
+
if (t) {
|
|
312
|
+
// Closing turn.end is emitted on result yield below so we
|
|
313
|
+
// have token usage to attach. Skip the synthetic close here.
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
// All other status frames are diagnostic — surface as a
|
|
317
|
+
// structured `status` event so consumers can opt in.
|
|
318
|
+
sink.emit({
|
|
319
|
+
type: 'status',
|
|
320
|
+
message: event.message,
|
|
321
|
+
timestamp: event.timestamp,
|
|
322
|
+
});
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
case 'tool.start':
|
|
326
|
+
sink.emit({
|
|
327
|
+
type: 'tool.call',
|
|
328
|
+
callId: event.callId,
|
|
329
|
+
tool: event.name,
|
|
330
|
+
args: safeJsonParse(event.arguments),
|
|
331
|
+
timestamp: event.timestamp,
|
|
332
|
+
});
|
|
333
|
+
return;
|
|
334
|
+
case 'tool.end':
|
|
335
|
+
sink.emit({
|
|
336
|
+
type: 'tool.result',
|
|
337
|
+
callId: event.callId,
|
|
338
|
+
status: event.ok ? 'success' : 'error',
|
|
339
|
+
summary: event.summary,
|
|
340
|
+
timestamp: event.timestamp,
|
|
341
|
+
});
|
|
342
|
+
return;
|
|
343
|
+
case 'tool.delta':
|
|
344
|
+
// Pass through as a partial chunk so a long-running bash can
|
|
345
|
+
// surface progress.
|
|
346
|
+
sink.emit({
|
|
347
|
+
type: 'tool.delta',
|
|
348
|
+
callId: event.callId,
|
|
349
|
+
chunk: event.chunk,
|
|
350
|
+
timestamp: event.timestamp,
|
|
351
|
+
});
|
|
352
|
+
return;
|
|
353
|
+
case 'text.delta':
|
|
354
|
+
sink.emit({
|
|
355
|
+
type: 'text.delta',
|
|
356
|
+
content: event.chunk,
|
|
357
|
+
timestamp: event.timestamp,
|
|
358
|
+
});
|
|
359
|
+
return;
|
|
360
|
+
case 'thinking.start':
|
|
361
|
+
sink.emit({
|
|
362
|
+
type: 'thinking.start',
|
|
363
|
+
blockId: event.blockId,
|
|
364
|
+
timestamp: event.timestamp,
|
|
365
|
+
});
|
|
366
|
+
return;
|
|
367
|
+
case 'thinking.delta':
|
|
368
|
+
sink.emit({
|
|
369
|
+
type: 'thinking.delta',
|
|
370
|
+
blockId: event.blockId,
|
|
371
|
+
content: event.chunk,
|
|
372
|
+
timestamp: event.timestamp,
|
|
373
|
+
});
|
|
374
|
+
return;
|
|
375
|
+
case 'thinking.end':
|
|
376
|
+
sink.emit({
|
|
377
|
+
type: 'thinking.end',
|
|
378
|
+
blockId: event.blockId,
|
|
379
|
+
timestamp: event.timestamp,
|
|
380
|
+
});
|
|
381
|
+
return;
|
|
382
|
+
default:
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
adapter.streamEmitter.on('event', handleStreamEvent);
|
|
387
|
+
// 6. Optional timeout — wire an AbortController so the engine loop
|
|
388
|
+
// bails cleanly. The signal also feeds the tool-bridge cancellation
|
|
389
|
+
// gate so long-running bash / network tools tear down.
|
|
390
|
+
const abort = new AbortController();
|
|
391
|
+
let timeoutHandle = null;
|
|
392
|
+
if (opts.timeoutSeconds && opts.timeoutSeconds > 0) {
|
|
393
|
+
timeoutHandle = setTimeout(() => {
|
|
394
|
+
abort.abort();
|
|
395
|
+
stderrWrite(`pugi headless: timeout after ${opts.timeoutSeconds}s — aborting engine\n`);
|
|
396
|
+
}, opts.timeoutSeconds * 1000);
|
|
397
|
+
// Don't keep node alive purely for the timeout — the engine path
|
|
398
|
+
// already holds the event loop via the AnvilEngineLoopClient fetch.
|
|
399
|
+
timeoutHandle.unref?.();
|
|
400
|
+
}
|
|
401
|
+
// 7. Drive the adapter to terminal. We iterate the (lossier)
|
|
402
|
+
// EngineEvent stream solely to learn when the result frame lands;
|
|
403
|
+
// every richer event already flew through `streamEmitter` above.
|
|
404
|
+
const kind = opts.kind ?? 'code';
|
|
405
|
+
const taskId = `print-${Date.now()}`;
|
|
406
|
+
let finalStatus = 'failed';
|
|
407
|
+
let finalSummary = '';
|
|
408
|
+
let resultRisks = [];
|
|
409
|
+
let eventRefs = [];
|
|
410
|
+
try {
|
|
411
|
+
const events = adapter.run({
|
|
412
|
+
id: taskId,
|
|
413
|
+
kind,
|
|
414
|
+
prompt: opts.prompt,
|
|
415
|
+
workspaceRoot: cwd,
|
|
416
|
+
allowedPaths: [cwd],
|
|
417
|
+
deniedPaths: [],
|
|
418
|
+
artifacts: [],
|
|
419
|
+
permissionMode: 'auto',
|
|
420
|
+
...(opts.maxTurns ? { budget: { tokens: opts.maxTurns * 16384 } } : {}),
|
|
421
|
+
}, { sessionId: session.id, signal: abort.signal });
|
|
422
|
+
for await (const ev of events) {
|
|
423
|
+
if (ev.type === 'result') {
|
|
424
|
+
finalStatus = ev.result.status;
|
|
425
|
+
finalSummary = ev.result.summary;
|
|
426
|
+
resultRisks = ev.result.risks;
|
|
427
|
+
eventRefs = ev.result.eventRefs;
|
|
428
|
+
for (const f of ev.result.filesChanged)
|
|
429
|
+
filesChanged.add(f);
|
|
430
|
+
}
|
|
431
|
+
// status frames already routed through the emitter — ignore here.
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
catch (error) {
|
|
435
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
436
|
+
sink.emit({
|
|
437
|
+
type: 'error',
|
|
438
|
+
kind: 'engine_crash',
|
|
439
|
+
message,
|
|
440
|
+
exit_code: 1,
|
|
441
|
+
});
|
|
442
|
+
if (timeoutHandle)
|
|
443
|
+
clearTimeout(timeoutHandle);
|
|
444
|
+
adapter.streamEmitter.off('event', handleStreamEvent);
|
|
445
|
+
recordCommandCompleted(session, 'print', 'error');
|
|
446
|
+
return 1;
|
|
447
|
+
}
|
|
448
|
+
if (timeoutHandle)
|
|
449
|
+
clearTimeout(timeoutHandle);
|
|
450
|
+
adapter.streamEmitter.off('event', handleStreamEvent);
|
|
451
|
+
if (mcpRegistry) {
|
|
452
|
+
await mcpRegistry.shutdown().catch(() => {
|
|
453
|
+
/* swallow — best-effort */
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
// 8. Pull headline metrics out of the result's eventRefs (adapter
|
|
457
|
+
// echoes `tokens=N` / `turns=N` / `tool_calls=N` / `outcome=...`).
|
|
458
|
+
const metrics = parseRefs(eventRefs);
|
|
459
|
+
totalTokens = metrics.tokens;
|
|
460
|
+
currentTurnTokensIn;
|
|
461
|
+
currentTurnTokensOut;
|
|
462
|
+
sawTurnStart; // silence TS6133
|
|
463
|
+
// Emit one closing turn.end so consumers always see a paired turn
|
|
464
|
+
// start/end. We use the final turn index reported by the adapter.
|
|
465
|
+
sink.emit({
|
|
466
|
+
type: 'turn.end',
|
|
467
|
+
turnIndex: metrics.turns || turnIndex || 1,
|
|
468
|
+
usage: {
|
|
469
|
+
// Engine outcome carries a single cumulative `tokens` figure; we
|
|
470
|
+
// split it 50/50 as a best-effort estimate until the SDK splits
|
|
471
|
+
// input/output. Documented as approximate so consumers do not
|
|
472
|
+
// treat the breakdown as authoritative.
|
|
473
|
+
tokensIn: Math.floor(totalTokens / 2),
|
|
474
|
+
tokensOut: Math.ceil(totalTokens / 2),
|
|
475
|
+
},
|
|
476
|
+
});
|
|
477
|
+
// 9. Emit session.end. Exit code policy per spec:
|
|
478
|
+
// - done → 0
|
|
479
|
+
// - blocked + outcome=budget_exhausted → 2
|
|
480
|
+
// - blocked + any other reason → 2 (turn limit, tool refused — both
|
|
481
|
+
// count as "did not complete")
|
|
482
|
+
// - failed → 1
|
|
483
|
+
const exitCode = finalStatus === 'done'
|
|
484
|
+
? 0
|
|
485
|
+
: finalStatus === 'blocked'
|
|
486
|
+
? 2
|
|
487
|
+
: 1;
|
|
488
|
+
const durationMs = Date.now() - startedAt;
|
|
489
|
+
sink.emit({
|
|
490
|
+
type: 'session.end',
|
|
491
|
+
sessionId: session.id,
|
|
492
|
+
status: finalStatus,
|
|
493
|
+
outcome: metrics.outcome,
|
|
494
|
+
totalTokensIn: Math.floor(totalTokens / 2),
|
|
495
|
+
totalTokensOut: Math.ceil(totalTokens / 2),
|
|
496
|
+
totalToolCalls: metrics.toolCalls,
|
|
497
|
+
totalTurns: metrics.turns,
|
|
498
|
+
durationMs,
|
|
499
|
+
filesChanged: Array.from(filesChanged).sort(),
|
|
500
|
+
risks: resultRisks,
|
|
501
|
+
finalText: finalSummary,
|
|
502
|
+
});
|
|
503
|
+
recordCommandCompleted(session, 'print', exitCode === 0 ? 'success' : 'error');
|
|
504
|
+
return exitCode;
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Best-effort JSON parse for tool arguments. Engine emits the raw
|
|
508
|
+
* argument string the model produced; if it is malformed we keep the
|
|
509
|
+
* raw string so the consumer can still see what the model sent.
|
|
510
|
+
*/
|
|
511
|
+
function safeJsonParse(s) {
|
|
512
|
+
try {
|
|
513
|
+
return JSON.parse(s);
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
return s;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Parse the `eventRefs` echo the adapter attaches to its result frame.
|
|
521
|
+
* Mirrors `parseEventRefs` in `cli.ts` — kept local so the headless
|
|
522
|
+
* module has no circular import on the runtime entry.
|
|
523
|
+
*/
|
|
524
|
+
function parseRefs(refs) {
|
|
525
|
+
const out = { toolCalls: 0, turns: 0, tokens: 0, outcome: null };
|
|
526
|
+
for (const ref of refs) {
|
|
527
|
+
const idx = ref.indexOf('=');
|
|
528
|
+
if (idx <= 0)
|
|
529
|
+
continue;
|
|
530
|
+
const key = ref.slice(0, idx);
|
|
531
|
+
const value = ref.slice(idx + 1);
|
|
532
|
+
if (key === 'tool_calls')
|
|
533
|
+
out.toolCalls = Number(value) || 0;
|
|
534
|
+
else if (key === 'turns')
|
|
535
|
+
out.turns = Number(value) || 0;
|
|
536
|
+
else if (key === 'tokens')
|
|
537
|
+
out.tokens = Number(value) || 0;
|
|
538
|
+
else if (key === 'outcome')
|
|
539
|
+
out.outcome = value || null;
|
|
540
|
+
}
|
|
541
|
+
return out;
|
|
542
|
+
}
|
|
543
|
+
//# sourceMappingURL=headless.js.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fail-closed hook registry loader.
|
|
3
|
+
*
|
|
4
|
+
* r2 (triple-review P2): the previous fail-open path silently
|
|
5
|
+
* disabled BLOCK rules when `.pugi/hooks.json` parsed badly. This module
|
|
6
|
+
* is the dedicated load path used by the engine command dispatch — it
|
|
7
|
+
* distinguishes three states:
|
|
8
|
+
*
|
|
9
|
+
* (a) No hooks file present → returns `undefined` (acceptable; the
|
|
10
|
+
* operator has no hooks configured).
|
|
11
|
+
* (b) Hooks file present + parses + loads → returns the live registry.
|
|
12
|
+
* (c) Hooks file present + parse/load FAILS → emits a fatal message
|
|
13
|
+
* and signals `refusedExit`. The caller (cli.ts) is expected to
|
|
14
|
+
* honor that by calling `process.exit(1)`. `PUGI_HOOKS_BYPASS=1`
|
|
15
|
+
* is the explicit operator-typed escape hatch.
|
|
16
|
+
*
|
|
17
|
+
* Extracted from `cli.ts` purely so the fail-closed contract is unit-
|
|
18
|
+
* testable without spawning a subprocess. The shape returned is a
|
|
19
|
+
* discriminated union so tests can assert each branch directly.
|
|
20
|
+
*
|
|
21
|
+
* Brand voice: no forbidden words. ASCII only. No emoji.
|
|
22
|
+
*/
|
|
23
|
+
import { existsSync } from 'node:fs';
|
|
24
|
+
import { homedir } from 'node:os';
|
|
25
|
+
import { resolve } from 'node:path';
|
|
26
|
+
import { HookRegistry } from '../core/hooks.js';
|
|
27
|
+
export async function loadHookRegistryOrExit(opts) {
|
|
28
|
+
const stderrWrite = opts.deps?.stderrWrite ??
|
|
29
|
+
((msg) => {
|
|
30
|
+
process.stderr.write(msg);
|
|
31
|
+
});
|
|
32
|
+
const env = opts.deps?.env ?? process.env;
|
|
33
|
+
const projectHooksPath = resolve(opts.workspaceRoot, '.pugi', 'hooks.json');
|
|
34
|
+
const pugiHomeRoot = env.PUGI_HOME ?? resolve(homedir(), '.pugi');
|
|
35
|
+
const userHooksPath = resolve(pugiHomeRoot, 'hooks.json');
|
|
36
|
+
const presentHookFiles = [projectHooksPath, userHooksPath].filter((p) => existsSync(p));
|
|
37
|
+
if (presentHookFiles.length === 0) {
|
|
38
|
+
return { kind: 'no-hooks-file', hooks: undefined };
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const reg = new HookRegistry({
|
|
42
|
+
workspaceRoot: opts.workspaceRoot,
|
|
43
|
+
session: opts.session,
|
|
44
|
+
});
|
|
45
|
+
await reg.load();
|
|
46
|
+
return { kind: 'loaded', hooks: reg };
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
const msg = error.message;
|
|
50
|
+
stderrWrite(`pugi ${opts.label}: hook configuration present but failed to load — refusing to run.\n` +
|
|
51
|
+
` files checked: ${presentHookFiles.join(', ')}\n` +
|
|
52
|
+
` error: ${msg}\n` +
|
|
53
|
+
` fix: validate the JSON or remove the file; or set PUGI_HOOKS_BYPASS=1 to override (NOT RECOMMENDED — disables block rules).\n`);
|
|
54
|
+
if (env.PUGI_HOOKS_BYPASS === '1') {
|
|
55
|
+
stderrWrite(`pugi ${opts.label}: PUGI_HOOKS_BYPASS=1 — continuing WITHOUT hooks despite parse failure.\n`);
|
|
56
|
+
return {
|
|
57
|
+
kind: 'parse-failure-bypassed',
|
|
58
|
+
hooks: undefined,
|
|
59
|
+
filesChecked: presentHookFiles,
|
|
60
|
+
error: msg,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
kind: 'parse-failure-refused',
|
|
65
|
+
filesChecked: presentHookFiles,
|
|
66
|
+
error: msg,
|
|
67
|
+
hooks: undefined,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=load-hooks-or-exit.js.map
|