@pugi/cli 0.1.0-beta.7 → 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 +4162 -488
- 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,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scenario runner for the Pugi MCP test harness (
|
|
3
|
+
*).
|
|
4
|
+
*
|
|
5
|
+
* Drives a parsed scenario against a stream of `HeadlessEnvelope`
|
|
6
|
+
* objects (the same shape `pugi --headless` emits on stdout) and a
|
|
7
|
+
* filesystem checker for `EXPECT_FILE` directives. The runner is
|
|
8
|
+
* deliberately decoupled from the subprocess spawn so the spec file
|
|
9
|
+
* can inject deterministic envelope arrays without spawning a real
|
|
10
|
+
* `pugi` binary — that strategy keeps the test cycle under 200ms while
|
|
11
|
+
* still exercising the matching semantics every CI run depends on.
|
|
12
|
+
*
|
|
13
|
+
* Matching semantics (the rules the corpus authors care about):
|
|
14
|
+
*
|
|
15
|
+
* - `EXPECT:` after a `>` user-input line scans envelopes that
|
|
16
|
+
* arrived AFTER that user-input. The cursor resets on each new
|
|
17
|
+
* `>`. If no envelope satisfies the pattern, the assertion fails.
|
|
18
|
+
*
|
|
19
|
+
* - `EXPECT_NOT:` runs the inverse — passes if NO envelope in the
|
|
20
|
+
* post-`>` window satisfies the pattern. A negative assertion that
|
|
21
|
+
* fires on every input line gives the operator a clean signal when
|
|
22
|
+
* a forbidden phrase ("Pugi") shows up.
|
|
23
|
+
*
|
|
24
|
+
* - `EXPECT_FILE:` runs once at the END of the scenario, against the
|
|
25
|
+
* final filesystem snapshot. The runner does not race the
|
|
26
|
+
* subprocess — by the time we evaluate file assertions the
|
|
27
|
+
* headless process has exited (or been terminated).
|
|
28
|
+
*
|
|
29
|
+
* Result shape mirrors `node:test` style: top-level pass/fail plus an
|
|
30
|
+
* array of per-assertion records so the CLI can print a grouped
|
|
31
|
+
* summary. Each failure carries the originating line number so the
|
|
32
|
+
* operator can jump straight to the scenario source.
|
|
33
|
+
*/
|
|
34
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
35
|
+
import { resolve } from 'node:path';
|
|
36
|
+
/**
|
|
37
|
+
* Run the assertions in `scenario` against the given envelope stream
|
|
38
|
+
* and filesystem snapshot. Pure function — no I/O outside the filesystem
|
|
39
|
+
* stat that `EXPECT_FILE` performs, and even that is gated by a step
|
|
40
|
+
* actually existing.
|
|
41
|
+
*/
|
|
42
|
+
export function runScenario(inputs) {
|
|
43
|
+
const now = inputs.now ?? Date.now;
|
|
44
|
+
const startedAt = now();
|
|
45
|
+
const failures = [];
|
|
46
|
+
let assertionCount = 0;
|
|
47
|
+
// Group steps into runs anchored by `>` user-input lines. Each run
|
|
48
|
+
// owns the EXPECT/EXPECT_NOT assertions that follow it until the
|
|
49
|
+
// next `>`. EXPECT_FILE is collected globally and evaluated after
|
|
50
|
+
// every user-input run is processed.
|
|
51
|
+
const runs = [];
|
|
52
|
+
const fileChecks = [];
|
|
53
|
+
for (const step of inputs.scenario.steps) {
|
|
54
|
+
if (step.kind === 'user-input') {
|
|
55
|
+
runs.push({ userStep: step, expects: [] });
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (step.kind === 'expect') {
|
|
59
|
+
// Assertions that appear BEFORE any `>` attach to a synthetic
|
|
60
|
+
// pre-run so the matching pass still sees them. Rare in practice,
|
|
61
|
+
// but the parser allows it and the runner should not silently
|
|
62
|
+
// drop them.
|
|
63
|
+
if (runs.length === 0)
|
|
64
|
+
runs.push({ userStep: null, expects: [] });
|
|
65
|
+
const current = runs[runs.length - 1];
|
|
66
|
+
if (current)
|
|
67
|
+
current.expects.push(step);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (step.kind === 'expect-file') {
|
|
71
|
+
fileChecks.push(step);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Build a per-run envelope window. We walk the envelope stream once,
|
|
75
|
+
// assigning each envelope to the most recent user-turn we have seen.
|
|
76
|
+
// The first `user-turn` envelope after a `>` is the marker for that
|
|
77
|
+
// run; assertions match within the slice up to (but not including)
|
|
78
|
+
// the NEXT `user-turn` envelope.
|
|
79
|
+
const userTurnIndices = [];
|
|
80
|
+
for (let i = 0; i < inputs.envelopes.length; i += 1) {
|
|
81
|
+
const env = inputs.envelopes[i];
|
|
82
|
+
if (env && env.kind === 'user-turn')
|
|
83
|
+
userTurnIndices.push(i);
|
|
84
|
+
}
|
|
85
|
+
for (let runIdx = 0; runIdx < runs.length; runIdx += 1) {
|
|
86
|
+
const run = runs[runIdx];
|
|
87
|
+
if (!run)
|
|
88
|
+
continue;
|
|
89
|
+
let start = 0;
|
|
90
|
+
let end = inputs.envelopes.length;
|
|
91
|
+
if (run.userStep && userTurnIndices[runIdx] !== undefined) {
|
|
92
|
+
start = (userTurnIndices[runIdx] ?? 0) + 1;
|
|
93
|
+
const nextUserTurn = userTurnIndices[runIdx + 1];
|
|
94
|
+
end = nextUserTurn ?? inputs.envelopes.length;
|
|
95
|
+
}
|
|
96
|
+
const window = inputs.envelopes.slice(start, end);
|
|
97
|
+
for (const expectation of run.expects) {
|
|
98
|
+
assertionCount += 1;
|
|
99
|
+
const matched = window.some((env) => matchesEnvelope(env, expectation.pattern));
|
|
100
|
+
if (expectation.polarity === 'positive' && !matched) {
|
|
101
|
+
failures.push({
|
|
102
|
+
line: expectation.line,
|
|
103
|
+
message: `EXPECT failed — no envelope matched ${describePattern(expectation.pattern)}`,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
else if (expectation.polarity === 'negative' && matched) {
|
|
107
|
+
failures.push({
|
|
108
|
+
line: expectation.line,
|
|
109
|
+
message: `EXPECT_NOT failed — envelope matched ${describePattern(expectation.pattern)}`,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
for (const check of fileChecks) {
|
|
115
|
+
assertionCount += 1;
|
|
116
|
+
const absolute = resolve(inputs.workspaceRoot, check.file);
|
|
117
|
+
if (!existsSync(absolute)) {
|
|
118
|
+
failures.push({
|
|
119
|
+
line: check.line,
|
|
120
|
+
message: `EXPECT_FILE failed — ${check.file} does not exist`,
|
|
121
|
+
});
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (check.content !== undefined) {
|
|
125
|
+
const body = readFileSync(absolute, 'utf8');
|
|
126
|
+
if (!body.includes(check.content)) {
|
|
127
|
+
failures.push({
|
|
128
|
+
line: check.line,
|
|
129
|
+
message: `EXPECT_FILE failed — ${check.file} does not contain "${check.content}"`,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const durationMs = Math.max(0, now() - startedAt);
|
|
135
|
+
return {
|
|
136
|
+
id: inputs.scenario.id,
|
|
137
|
+
passed: failures.length === 0,
|
|
138
|
+
failures,
|
|
139
|
+
durationMs,
|
|
140
|
+
assertionCount,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Filter parsed scenarios by a simple glob-ish substring matcher.
|
|
145
|
+
* `*` matches any run of characters; otherwise we fall back to plain
|
|
146
|
+
* substring containment so `pugi smoke --filter identity` works as
|
|
147
|
+
* the operator expects. The matcher is intentionally NOT a full RegExp
|
|
148
|
+
* (no anchors, no character classes) because scenarios are addressed
|
|
149
|
+
* by short ids — a `--filter "id*"` form is the maximum complexity
|
|
150
|
+
* the corpus needs.
|
|
151
|
+
*/
|
|
152
|
+
export function filterScenarios(scenarios, pattern) {
|
|
153
|
+
if (!pattern || pattern.length === 0)
|
|
154
|
+
return scenarios;
|
|
155
|
+
const matcher = compileFilterPattern(pattern);
|
|
156
|
+
return scenarios.filter((s) => matcher(s.id));
|
|
157
|
+
}
|
|
158
|
+
function compileFilterPattern(pattern) {
|
|
159
|
+
if (!pattern.includes('*')) {
|
|
160
|
+
return (id) => id.includes(pattern);
|
|
161
|
+
}
|
|
162
|
+
// Escape RegExp metacharacters except `*`, then translate `*` to
|
|
163
|
+
// `.*`. This is the dropbox/glob "fnmatch lite" approach — predictable
|
|
164
|
+
// and small.
|
|
165
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
166
|
+
const re = new RegExp(`^${escaped.replace(/\*/g, '.*')}$`);
|
|
167
|
+
return (id) => re.test(id);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Decide whether a single envelope satisfies a pattern. Exported for
|
|
171
|
+
* tests that want to probe the matching logic without building a full
|
|
172
|
+
* scenario object.
|
|
173
|
+
*/
|
|
174
|
+
export function matchesEnvelope(env, pattern) {
|
|
175
|
+
if (pattern.kind === 'persona-turn-contains') {
|
|
176
|
+
if (env.kind !== 'persona-turn')
|
|
177
|
+
return false;
|
|
178
|
+
return pattern.substrings.some((s) => env.body.includes(s));
|
|
179
|
+
}
|
|
180
|
+
if (pattern.kind === 'tool-call') {
|
|
181
|
+
if (env.kind !== 'tool-call')
|
|
182
|
+
return false;
|
|
183
|
+
// The body is JSON. Tool calls that don't parse as JSON cannot
|
|
184
|
+
// match — surface a clean fail instead of crashing.
|
|
185
|
+
let parsed;
|
|
186
|
+
try {
|
|
187
|
+
parsed = JSON.parse(env.body);
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
if (!isRecord(parsed))
|
|
193
|
+
return false;
|
|
194
|
+
if (pattern.tool !== undefined && parsed['tool'] !== pattern.tool) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
if (pattern.argsSubset !== undefined) {
|
|
198
|
+
const args = parsed['args'];
|
|
199
|
+
if (!isRecord(args))
|
|
200
|
+
return false;
|
|
201
|
+
for (const [k, v] of Object.entries(pattern.argsSubset)) {
|
|
202
|
+
if (String(args[k]) !== v)
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
if (pattern.kind === 'envelope-kind') {
|
|
209
|
+
return env.kind === pattern.envelopeKind;
|
|
210
|
+
}
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
function isRecord(value) {
|
|
214
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
215
|
+
}
|
|
216
|
+
function describePattern(pattern) {
|
|
217
|
+
if (pattern.kind === 'persona-turn-contains') {
|
|
218
|
+
return `persona-turn containing one of [${pattern.substrings
|
|
219
|
+
.map((s) => `"${s}"`)
|
|
220
|
+
.join(', ')}]`;
|
|
221
|
+
}
|
|
222
|
+
if (pattern.kind === 'tool-call') {
|
|
223
|
+
const parts = [];
|
|
224
|
+
if (pattern.tool)
|
|
225
|
+
parts.push(`tool=${pattern.tool}`);
|
|
226
|
+
if (pattern.argsSubset) {
|
|
227
|
+
for (const [k, v] of Object.entries(pattern.argsSubset)) {
|
|
228
|
+
parts.push(`${k}=${v}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return `tool-call ${parts.join(' ')}`.trim();
|
|
232
|
+
}
|
|
233
|
+
if (pattern.kind === 'envelope-kind') {
|
|
234
|
+
return `envelope kind=${pattern.envelopeKind}`;
|
|
235
|
+
}
|
|
236
|
+
return 'unknown pattern';
|
|
237
|
+
}
|
|
238
|
+
//# sourceMappingURL=runner.js.map
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scenario DSL parser for the Pugi MCP test harness (
|
|
3
|
+
* Phase 1).
|
|
4
|
+
*
|
|
5
|
+
* Background — CEO directive `feedback_live_console_test_every_publish`:
|
|
6
|
+
* every CLI publish must be smoke-tested against a fixed corpus of
|
|
7
|
+
* scripted dialogs before it is announced to the operator. The manual
|
|
8
|
+
* routine ("npm i -g + pugi --version + run identity prompt + verify
|
|
9
|
+
* Pugi self-id") is exactly the kind of toil a deterministic harness
|
|
10
|
+
* should automate. Phase 1 lays the foundation; the corpus + headless
|
|
11
|
+
* runner + CI gate.
|
|
12
|
+
*
|
|
13
|
+
* Format — one scenario per file, line-based, intentionally minimal so
|
|
14
|
+
* non-engineers can author cases. The grammar:
|
|
15
|
+
*
|
|
16
|
+
* # comment — ignored
|
|
17
|
+
* # scenario: <id> — optional metadata
|
|
18
|
+
* > "<input>" — single user turn (the engine sees this verbatim)
|
|
19
|
+
* EXPECT: <pred> — positive assertion against the next persona-turn
|
|
20
|
+
* envelope OR a tool-call envelope. Multiple stack
|
|
21
|
+
* against the SAME prior `>` turn until the next
|
|
22
|
+
* `>` resets the cursor.
|
|
23
|
+
* EXPECT_NOT:<pred>— negative assertion (same cursor semantics).
|
|
24
|
+
* EXPECT_FILE:<file> exists [with content "<text>"]
|
|
25
|
+
* — filesystem assertion fired AFTER the scenario
|
|
26
|
+
* finishes. Matches both `exists` literal and a
|
|
27
|
+
* content substring check.
|
|
28
|
+
*
|
|
29
|
+
* The DSL parser is deliberately a pure function over the file contents:
|
|
30
|
+
* the runner module is the thing that spawns `pugi --headless` and
|
|
31
|
+
* matches assertions against the emitted envelope stream. Keeping the
|
|
32
|
+
* parser pure is what lets us spec it with no fixtures.
|
|
33
|
+
*
|
|
34
|
+
* Anti-design — we did NOT use YAML/JSON. The line-based form survives
|
|
35
|
+
* copy-paste from a chat transcript ("> 'ты кто?'" maps 1:1 to the way
|
|
36
|
+
* an operator sees a Pugi dialog), and prevents drift between scenario
|
|
37
|
+
* authoring style and how the operator actually drives the CLI.
|
|
38
|
+
*/
|
|
39
|
+
/**
|
|
40
|
+
* Parse a scenario file body into a `ParsedScenario`. Returns errors
|
|
41
|
+
* alongside the partial scenario so the runner can surface every issue
|
|
42
|
+
* in one pass rather than failing on the first malformed line. The
|
|
43
|
+
* shape mirrors what TypeScript's diagnostics array looks like —
|
|
44
|
+
* familiar pattern for any operator who has read a tsc error.
|
|
45
|
+
*
|
|
46
|
+
* `filePath` is informational; we ALSO consume it to derive the
|
|
47
|
+
* default scenario id when the file body does not declare one.
|
|
48
|
+
*/
|
|
49
|
+
export function parseScenario(filePath, body) {
|
|
50
|
+
const errors = [];
|
|
51
|
+
const steps = [];
|
|
52
|
+
let title;
|
|
53
|
+
let explicitId;
|
|
54
|
+
const lines = body.split(/\r?\n/);
|
|
55
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
56
|
+
const raw = lines[i] ?? '';
|
|
57
|
+
const lineNo = i + 1;
|
|
58
|
+
const trimmed = raw.trim();
|
|
59
|
+
if (trimmed.length === 0)
|
|
60
|
+
continue;
|
|
61
|
+
if (trimmed.startsWith('#')) {
|
|
62
|
+
// Metadata comments. `# scenario: <id>` populates the explicit
|
|
63
|
+
// id; `# title: <text>` populates the title. Plain `# anything`
|
|
64
|
+
// is a free-form comment.
|
|
65
|
+
const meta = /^#\s*(scenario|title)\s*:\s*(.+?)\s*$/i.exec(trimmed);
|
|
66
|
+
if (meta) {
|
|
67
|
+
const key = (meta[1] ?? '').toLowerCase();
|
|
68
|
+
const value = meta[2] ?? '';
|
|
69
|
+
if (key === 'scenario')
|
|
70
|
+
explicitId = value;
|
|
71
|
+
else if (key === 'title')
|
|
72
|
+
title = value;
|
|
73
|
+
}
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (trimmed.startsWith('>')) {
|
|
77
|
+
const body = parseQuotedInput(trimmed.slice(1).trim());
|
|
78
|
+
if (body === null) {
|
|
79
|
+
errors.push({
|
|
80
|
+
line: lineNo,
|
|
81
|
+
message: 'user input line must be quoted (e.g. > "hello")',
|
|
82
|
+
});
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
steps.push({ kind: 'user-input', body, line: lineNo });
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (trimmed.startsWith('EXPECT_FILE:')) {
|
|
89
|
+
const payload = trimmed.slice('EXPECT_FILE:'.length).trim();
|
|
90
|
+
const fileStep = parseExpectFile(payload, lineNo);
|
|
91
|
+
if (fileStep.errors.length > 0) {
|
|
92
|
+
errors.push(...fileStep.errors);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (fileStep.step)
|
|
96
|
+
steps.push(fileStep.step);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (trimmed.startsWith('EXPECT_NOT:')) {
|
|
100
|
+
const payload = trimmed.slice('EXPECT_NOT:'.length).trim();
|
|
101
|
+
const parsed = parseAssertion(payload, lineNo);
|
|
102
|
+
if (parsed.errors.length > 0) {
|
|
103
|
+
errors.push(...parsed.errors);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (parsed.pattern) {
|
|
107
|
+
steps.push({
|
|
108
|
+
kind: 'expect',
|
|
109
|
+
polarity: 'negative',
|
|
110
|
+
pattern: parsed.pattern,
|
|
111
|
+
anchor: 'last-user',
|
|
112
|
+
line: lineNo,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (trimmed.startsWith('EXPECT:')) {
|
|
118
|
+
const payload = trimmed.slice('EXPECT:'.length).trim();
|
|
119
|
+
const parsed = parseAssertion(payload, lineNo);
|
|
120
|
+
if (parsed.errors.length > 0) {
|
|
121
|
+
errors.push(...parsed.errors);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (parsed.pattern) {
|
|
125
|
+
steps.push({
|
|
126
|
+
kind: 'expect',
|
|
127
|
+
polarity: 'positive',
|
|
128
|
+
pattern: parsed.pattern,
|
|
129
|
+
anchor: 'last-user',
|
|
130
|
+
line: lineNo,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
errors.push({
|
|
136
|
+
line: lineNo,
|
|
137
|
+
message: `unrecognized directive (expected one of: > "...", EXPECT:, EXPECT_NOT:, EXPECT_FILE:, # comment): ${trimmed}`,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
const id = explicitId ?? deriveDefaultId(filePath);
|
|
141
|
+
const scenario = {
|
|
142
|
+
id,
|
|
143
|
+
...(title !== undefined ? { title } : {}),
|
|
144
|
+
filePath,
|
|
145
|
+
steps,
|
|
146
|
+
};
|
|
147
|
+
return { scenario, errors };
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Strip the wrapping quotes from a `> "..."` user line. Returns the
|
|
151
|
+
* inner string when the input is properly quoted (single OR double),
|
|
152
|
+
* otherwise null so the parser can surface a diagnostic. Allowing both
|
|
153
|
+
* quote styles is a thoughtful nicety — Russian scenarios frequently
|
|
154
|
+
* embed double quotes inside a Cyrillic phrase, e.g. `> 'ты "сделай"
|
|
155
|
+
* хорошо'`.
|
|
156
|
+
*/
|
|
157
|
+
function parseQuotedInput(raw) {
|
|
158
|
+
if (raw.length < 2)
|
|
159
|
+
return null;
|
|
160
|
+
const first = raw[0];
|
|
161
|
+
const last = raw[raw.length - 1];
|
|
162
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
|
|
163
|
+
return raw.slice(1, -1);
|
|
164
|
+
}
|
|
165
|
+
// Permissive fallback — a bare unquoted line is still parsed as the
|
|
166
|
+
// user input (lets quick prototypes work). The runner does not
|
|
167
|
+
// distinguish between quoted and unquoted forms.
|
|
168
|
+
return raw;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Parse the right-hand side of an `EXPECT:` / `EXPECT_NOT:` directive.
|
|
172
|
+
* Discriminates between the three pattern kinds by looking at the
|
|
173
|
+
* leading keyword. Errors collect rather than throw so the runner can
|
|
174
|
+
* report multiple parse problems in one pass.
|
|
175
|
+
*/
|
|
176
|
+
function parseAssertion(payload, line) {
|
|
177
|
+
const errors = [];
|
|
178
|
+
if (payload.length === 0) {
|
|
179
|
+
return {
|
|
180
|
+
pattern: null,
|
|
181
|
+
errors: [{ line, message: 'EXPECT directive requires a pattern' }],
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
const personaMatch = /^persona-turn\s+contains\s+(.+)$/i.exec(payload);
|
|
185
|
+
if (personaMatch) {
|
|
186
|
+
const substrings = parseOrList(personaMatch[1] ?? '');
|
|
187
|
+
if (substrings.length === 0) {
|
|
188
|
+
errors.push({
|
|
189
|
+
line,
|
|
190
|
+
message: 'persona-turn contains requires one or more quoted substrings',
|
|
191
|
+
});
|
|
192
|
+
return { pattern: null, errors };
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
pattern: { kind: 'persona-turn-contains', substrings },
|
|
196
|
+
errors,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
const toolMatch = /^tool-call(?:\s+(.+))?$/i.exec(payload);
|
|
200
|
+
if (toolMatch) {
|
|
201
|
+
const remainder = (toolMatch[1] ?? '').trim();
|
|
202
|
+
const kv = parseKeyValuePairs(remainder);
|
|
203
|
+
const result = { kind: 'tool-call' };
|
|
204
|
+
if (kv.kind !== undefined)
|
|
205
|
+
result.tool = kv.kind;
|
|
206
|
+
const argsSubset = {};
|
|
207
|
+
for (const [k, v] of Object.entries(kv)) {
|
|
208
|
+
if (k === 'kind')
|
|
209
|
+
continue;
|
|
210
|
+
argsSubset[k] = v;
|
|
211
|
+
}
|
|
212
|
+
if (Object.keys(argsSubset).length > 0)
|
|
213
|
+
result.argsSubset = argsSubset;
|
|
214
|
+
return { pattern: result, errors };
|
|
215
|
+
}
|
|
216
|
+
const envelopeMatch = /^envelope\s+kind=([\w-]+)$/i.exec(payload);
|
|
217
|
+
if (envelopeMatch) {
|
|
218
|
+
return {
|
|
219
|
+
pattern: {
|
|
220
|
+
kind: 'envelope-kind',
|
|
221
|
+
envelopeKind: envelopeMatch[1] ?? '',
|
|
222
|
+
},
|
|
223
|
+
errors,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
errors.push({
|
|
227
|
+
line,
|
|
228
|
+
message: `unrecognized EXPECT pattern: ${payload}`,
|
|
229
|
+
});
|
|
230
|
+
return { pattern: null, errors };
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Parse the EXPECT_FILE payload. Grammar:
|
|
234
|
+
*
|
|
235
|
+
* <relative path> exists
|
|
236
|
+
* <relative path> exists with content "<text>"
|
|
237
|
+
*
|
|
238
|
+
* The path is unquoted (everything up to `exists`); the content phrase
|
|
239
|
+
* is double-quoted so it can include whitespace.
|
|
240
|
+
*/
|
|
241
|
+
function parseExpectFile(payload, line) {
|
|
242
|
+
const errors = [];
|
|
243
|
+
const match = /^(\S+)\s+exists(?:\s+with\s+content\s+"([^"]*)")?\s*$/.exec(payload);
|
|
244
|
+
if (!match) {
|
|
245
|
+
errors.push({
|
|
246
|
+
line,
|
|
247
|
+
message: 'EXPECT_FILE expects: <path> exists [with content "<text>"]',
|
|
248
|
+
});
|
|
249
|
+
return { step: null, errors };
|
|
250
|
+
}
|
|
251
|
+
const file = match[1];
|
|
252
|
+
if (!file) {
|
|
253
|
+
errors.push({ line, message: 'EXPECT_FILE missing file path' });
|
|
254
|
+
return { step: null, errors };
|
|
255
|
+
}
|
|
256
|
+
const content = match[2];
|
|
257
|
+
const step = content !== undefined
|
|
258
|
+
? { kind: 'expect-file', file, content, line }
|
|
259
|
+
: { kind: 'expect-file', file, line };
|
|
260
|
+
return { step, errors };
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Split a `"..." OR "..." OR "..."` list into the inner strings.
|
|
264
|
+
* Whitespace tolerant; the `OR` is case-insensitive so authors who
|
|
265
|
+
* type `or` lowercase are not punished.
|
|
266
|
+
*/
|
|
267
|
+
function parseOrList(raw) {
|
|
268
|
+
const parts = raw.split(/\s+OR\s+/i);
|
|
269
|
+
const result = [];
|
|
270
|
+
for (const part of parts) {
|
|
271
|
+
const trimmed = part.trim();
|
|
272
|
+
if (trimmed.length === 0)
|
|
273
|
+
continue;
|
|
274
|
+
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
275
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
276
|
+
result.push(trimmed.slice(1, -1));
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
// Permissive — accept unquoted token as a single substring. The
|
|
280
|
+
// parser does not enforce quotes here because scenario authors
|
|
281
|
+
// type Cyrillic without delimiters all the time.
|
|
282
|
+
result.push(trimmed);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return result;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Parse a whitespace-separated `key=value` list (e.g.
|
|
289
|
+
* `kind=Write file=hello.txt`) into a flat record. Values may be
|
|
290
|
+
* double-quoted to embed whitespace, though the corpus today uses bare
|
|
291
|
+
* tokens.
|
|
292
|
+
*/
|
|
293
|
+
function parseKeyValuePairs(raw) {
|
|
294
|
+
const out = {};
|
|
295
|
+
if (raw.length === 0)
|
|
296
|
+
return out;
|
|
297
|
+
const re = /(\w+)=(?:"([^"]*)"|(\S+))/g;
|
|
298
|
+
let match;
|
|
299
|
+
while ((match = re.exec(raw)) !== null) {
|
|
300
|
+
const key = match[1];
|
|
301
|
+
if (!key)
|
|
302
|
+
continue;
|
|
303
|
+
const value = match[2] !== undefined ? match[2] : (match[3] ?? '');
|
|
304
|
+
out[key] = value;
|
|
305
|
+
}
|
|
306
|
+
return out;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Derive a default scenario id from the file path: strip the directory,
|
|
310
|
+
* drop the trailing `.scenario.txt` (or `.txt`) suffix.
|
|
311
|
+
*/
|
|
312
|
+
function deriveDefaultId(filePath) {
|
|
313
|
+
const base = filePath.split(/[/\\]/).pop() ?? filePath;
|
|
314
|
+
return base.replace(/\.scenario\.txt$/i, '').replace(/\.txt$/i, '');
|
|
315
|
+
}
|
|
316
|
+
//# sourceMappingURL=scenario-parser.js.map
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { resolve as pathResolve } from 'node:path';
|
|
3
|
+
const DEFAULT_TIMEOUT_MS = 500;
|
|
4
|
+
const MAX_OUTPUT_LINE_CHARS = 240;
|
|
5
|
+
/**
|
|
6
|
+
* Execute the configured status-line command and return the rendered
|
|
7
|
+
* footer text. Never throws — every failure path resolves to `''`
|
|
8
|
+
* and the caller decides what default to show.
|
|
9
|
+
*
|
|
10
|
+
* `cwd` parameter is the workspace root the spawned process should
|
|
11
|
+
* inherit. Caller already validates the workspace bounds; this
|
|
12
|
+
* function trusts it.
|
|
13
|
+
*/
|
|
14
|
+
export async function executeStatusLine(config, input, cwd) {
|
|
15
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
16
|
+
const command = config.command.trim();
|
|
17
|
+
if (command.length === 0)
|
|
18
|
+
return '';
|
|
19
|
+
return new Promise((resolveFn) => {
|
|
20
|
+
let settled = false;
|
|
21
|
+
const finish = (s) => {
|
|
22
|
+
if (settled)
|
|
23
|
+
return;
|
|
24
|
+
settled = true;
|
|
25
|
+
resolveFn(s);
|
|
26
|
+
};
|
|
27
|
+
let child;
|
|
28
|
+
try {
|
|
29
|
+
child = spawn(command, {
|
|
30
|
+
shell: true,
|
|
31
|
+
cwd: pathResolve(cwd),
|
|
32
|
+
env: { ...process.env, PUGI_STATUSLINE: '1' },
|
|
33
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
finish('');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const chunks = [];
|
|
41
|
+
child.stdout?.on('data', (c) => {
|
|
42
|
+
chunks.push(c);
|
|
43
|
+
const joined = Buffer.concat(chunks).toString('utf8');
|
|
44
|
+
if (joined.length > 8192) {
|
|
45
|
+
try {
|
|
46
|
+
child.kill('SIGKILL');
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// swallow — process may have already exited
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
child.stderr?.on('data', () => {
|
|
54
|
+
// Discard stderr to keep the contract stdout-only and the
|
|
55
|
+
// session log readable; future telemetry can capture it.
|
|
56
|
+
});
|
|
57
|
+
const timer = setTimeout(() => {
|
|
58
|
+
try {
|
|
59
|
+
child.kill('SIGKILL');
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// swallow
|
|
63
|
+
}
|
|
64
|
+
finish('');
|
|
65
|
+
}, Math.max(1, Math.min(5000, timeoutMs)));
|
|
66
|
+
child.on('error', () => {
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
finish('');
|
|
69
|
+
});
|
|
70
|
+
child.on('close', () => {
|
|
71
|
+
clearTimeout(timer);
|
|
72
|
+
const out = Buffer.concat(chunks).toString('utf8');
|
|
73
|
+
finish(extractFirstLine(out));
|
|
74
|
+
});
|
|
75
|
+
try {
|
|
76
|
+
child.stdin?.end(JSON.stringify(input));
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
finish('');
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function extractFirstLine(raw) {
|
|
85
|
+
if (raw.length === 0)
|
|
86
|
+
return '';
|
|
87
|
+
const lines = raw.split(/\r?\n/);
|
|
88
|
+
for (const ln of lines) {
|
|
89
|
+
const trimmed = ln.trim();
|
|
90
|
+
if (trimmed.length === 0)
|
|
91
|
+
continue;
|
|
92
|
+
return trimmed.slice(0, MAX_OUTPUT_LINE_CHARS);
|
|
93
|
+
}
|
|
94
|
+
return '';
|
|
95
|
+
}
|
|
96
|
+
// Exposed for tests — extractFirstLine has subtle splitter behaviour
|
|
97
|
+
// (CRLF vs LF, multiple blank leaders) worth locking.
|
|
98
|
+
export const __test = { extractFirstLine };
|
|
99
|
+
//# sourceMappingURL=statusline.js.map
|