@pugi/cli 0.1.0-beta.10 → 0.1.0-beta.100
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 +132 -0
- package/LICENSE +1 -1
- package/README.md +53 -11
- 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/retro.js +210 -0
- 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 +3 -3
- 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/db.js +506 -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/codegraph/parser.js +71 -0
- package/dist/core/codegraph/types.js +34 -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 +13 -13
- 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 +72 -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 +333 -7
- package/dist/core/edits/format-detector.js +260 -0
- package/dist/core/edits/format-matrix.js +26 -0
- package/dist/core/edits/fuzzy-ladder.js +650 -0
- package/dist/core/edits/index.js +5 -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 +29 -29
- package/dist/core/engine/anvil-client.js +214 -26
- package/dist/core/engine/auto-compact.js +247 -0
- package/dist/core/engine/budgets.js +220 -0
- package/dist/core/engine/compact-llm-summarizer.js +124 -0
- package/dist/core/engine/context-prefix.js +155 -0
- package/dist/core/engine/index.js +1 -1
- package/dist/core/engine/intensity.js +163 -0
- package/dist/core/engine/intent.js +260 -0
- package/dist/core/engine/native-pugi.js +1559 -227
- package/dist/core/engine/prompts.js +187 -19
- package/dist/core/engine/strip-internal-fields.js +124 -0
- package/dist/core/engine/tool-bridge.js +1887 -59
- package/dist/core/engine/verification-patterns.js +195 -0
- 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 +46 -0
- package/dist/core/hooks/index.js +15 -0
- package/dist/core/hooks/registry.js +216 -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/hooks/worktree-events.js +158 -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 +551 -41
- package/dist/core/lsp/language-detect.js +66 -0
- package/dist/core/lsp/post-edit-diagnostics.js +171 -0
- package/dist/core/lsp/server-detect.js +173 -0
- package/dist/core/lsp/symbol-cache.js +162 -0
- package/dist/core/lsp/symbol-tools.js +664 -0
- package/dist/core/mcp/client.js +97 -28
- package/dist/core/mcp/http-server.js +553 -0
- package/dist/core/mcp/orchestrator-config.js +192 -0
- package/dist/core/mcp/orchestrator-tools.js +806 -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/notes/notes-paths.js +113 -0
- package/dist/core/notes/notes-recorder.js +140 -0
- package/dist/core/notes/notes-writer.js +53 -0
- package/dist/core/notes/renderers.js +0 -0
- package/dist/core/notes/slug.js +105 -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 +107 -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-gitignore.js +52 -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/engine-bridge.js +303 -0
- 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 +2690 -229
- package/dist/core/repl/slash-commands.js +540 -41
- 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/tool-route.js +382 -0
- 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/retro/git-collector.js +251 -0
- package/dist/core/retro/health-card.js +25 -0
- package/dist/core/retro/metrics.js +342 -0
- package/dist/core/retro/narrative.js +249 -0
- package/dist/core/retro/plane-collector.js +274 -0
- package/dist/core/retro/pr-issue-link.js +65 -0
- package/dist/core/retro/types.js +16 -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/sandboxing/adapter.js +29 -0
- package/dist/core/sandboxing/index.js +49 -0
- package/dist/core/sandboxing/none.js +19 -0
- package/dist/core/sandboxing/seatbelt.js +183 -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 +119 -0
- package/dist/core/settings.js +378 -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 +30 -30
- 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 +146 -52
- 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/include-parser.js +249 -0
- package/dist/core/worktree-manager/cleanup.js +123 -0
- package/dist/core/worktree-manager/manager.js +303 -0
- package/dist/index.js +36 -0
- package/dist/runtime/bootstrap.js +190 -0
- package/dist/runtime/cli.js +4345 -561
- package/dist/runtime/commands/agents.js +31 -31
- 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 +74 -40
- package/dist/runtime/commands/cost.js +199 -0
- package/dist/runtime/commands/delegate.js +27 -4
- 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 +187 -0
- package/dist/runtime/commands/index-cmd.js +353 -0
- package/dist/runtime/commands/init.js +254 -0
- package/dist/runtime/commands/lsp.js +200 -38
- package/dist/runtime/commands/mcp.js +935 -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 +12 -12
- 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/servers.js +236 -0
- 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 +8 -8
- package/dist/runtime/commands/worktrees.js +155 -0
- package/dist/runtime/deprecation-warning.js +69 -0
- package/dist/runtime/engine-exit-code.js +50 -0
- package/dist/runtime/headless-repl.js +195 -0
- package/dist/runtime/headless.js +548 -0
- package/dist/runtime/load-hooks-or-exit.js +71 -0
- package/dist/runtime/plan-decompose.js +22 -22
- package/dist/runtime/sigint-guard.js +272 -0
- package/dist/runtime/stream-renderer.js +195 -0
- package/dist/runtime/update-check.js +28 -28
- package/dist/runtime/version.js +65 -0
- package/dist/runtime/worktree-bootstrap.js +579 -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 +89 -28
- package/dist/tools/ask-user-question.js +337 -0
- package/dist/tools/ask-user.js +115 -0
- package/dist/tools/bash.js +624 -46
- package/dist/tools/brief.js +224 -0
- package/dist/tools/cron.js +433 -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/http-request.js +336 -0
- package/dist/tools/lsp-tools.js +377 -1
- 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 +120 -5
- package/dist/tools/server-tools.js +892 -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 +22 -1
- package/dist/tui/ask-modal.js +14 -14
- package/dist/tui/ask-user-question-chips.js +315 -0
- 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/multi-file-diff-approval.js +375 -0
- package/dist/tui/onboarding-wizard.js +240 -0
- package/dist/tui/permissions-picker.js +86 -0
- package/dist/tui/render.js +36 -1
- package/dist/tui/repl-render.js +239 -25
- 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 +125 -45
- 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/package.json +21 -5
- package/test/scenarios/codegen-create-file.scenario.txt +13 -0
- package/test/scenarios/compact-force.scenario.txt +12 -0
- package/test/scenarios/identity.scenario.txt +11 -0
- package/test/scenarios/persona-handoff.scenario.txt +12 -0
- package/test/scenarios/walkback.scenario.txt +12 -0
- package/dist/core/engine/compaction-hook.js +0 -154
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Artifact chain step definitions — Pugi .
|
|
3
|
+
*
|
|
4
|
+
* The chain encodes Pugi's moat: an operator drops one high-level intent
|
|
5
|
+
* (`pugi chain new "<intent>"`) and the CLI walks a deterministic 7-step
|
|
6
|
+
* pipeline that produces verifiable artifacts at each stage. Every step
|
|
7
|
+
* dispatches to a specialist persona via the existing `pugi delegate`
|
|
8
|
+
* surface so the same Tier 1 roster powers both ad-hoc dispatch and the
|
|
9
|
+
* artifact chain.
|
|
10
|
+
*
|
|
11
|
+
* The pipeline:
|
|
12
|
+
*
|
|
13
|
+
* 1. prd — Olivia (PM): structured PRD (goals / users /
|
|
14
|
+
* acceptance criteria / scope)
|
|
15
|
+
* 2. adr — Marcus (CTO): architectural decision record
|
|
16
|
+
* against the team ADR template
|
|
17
|
+
* 3. mindmap — Marcus (CTO): mermaid mindmap of the solution
|
|
18
|
+
* surface (modules + boundaries)
|
|
19
|
+
* 4. er — Hiroshi (Lead Dev):entity-relationship mermaid for
|
|
20
|
+
* the data model the design implies
|
|
21
|
+
* 5. sequence — Hiroshi (Lead Dev):mermaid sequence diagram covering
|
|
22
|
+
* the critical happy + sad paths
|
|
23
|
+
* 6. tests — Vera (QA): spec scaffolding mapped from the
|
|
24
|
+
* PRD acceptance criteria
|
|
25
|
+
* 7. code — Hiroshi (Lead Dev):implementation diff against the
|
|
26
|
+
* scaffolded specs
|
|
27
|
+
*
|
|
28
|
+
* Persona slugs are lowercase ASCII to satisfy the server-side delegate
|
|
29
|
+
* grammar (`^[a-z]+$`, mirrors `PUGI_DELEGATE_REGEX` in
|
|
30
|
+
* `apps/admin-api/src/pugi/sessions.controller.ts`). The chain never
|
|
31
|
+
* invents new personas — it only orchestrates the existing roster.
|
|
32
|
+
*
|
|
33
|
+
* Module contract:
|
|
34
|
+
*
|
|
35
|
+
* - This file is PURE data. No fs, no network, no side effects.
|
|
36
|
+
* Importing it from a hot path (REPL keystroke handler) is safe.
|
|
37
|
+
* - The step ORDER is authoritative; downstream consumers MUST iterate
|
|
38
|
+
* `CHAIN_STEPS` instead of hand-rolling their own arrays so future
|
|
39
|
+
* re-ordering lands in exactly one place.
|
|
40
|
+
* - Personas are looked up by slug at dispatch time so a roster change
|
|
41
|
+
* does not silently break the chain — the dispatcher surfaces an
|
|
42
|
+
* `unknown_persona` outcome the operator can see.
|
|
43
|
+
*/
|
|
44
|
+
/**
|
|
45
|
+
* Ordered, frozen table of the seven steps. The chain state machine
|
|
46
|
+
* advances through this array exactly once; re-ordering OR insertion
|
|
47
|
+
* is a breaking change for any chain currently on disk.
|
|
48
|
+
*/
|
|
49
|
+
export const CHAIN_STEPS = Object.freeze([
|
|
50
|
+
{
|
|
51
|
+
id: 'prd',
|
|
52
|
+
ordinal: 1,
|
|
53
|
+
persona: 'olivia',
|
|
54
|
+
personaLabel: 'Olivia (PM)',
|
|
55
|
+
artifactFilename: 'PRD.md',
|
|
56
|
+
briefTemplate: 'Produce a structured PRD for chain {{chainId}}. ' +
|
|
57
|
+
'Operator intent: "{{intent}}". ' +
|
|
58
|
+
'Cover: goals, target users, success metrics, ' +
|
|
59
|
+
'numbered acceptance criteria (## Acceptance Criteria), scope + non-scope, ' +
|
|
60
|
+
'risks. Output markdown ready for prd-check ingestion.',
|
|
61
|
+
gloss: 'Structured PRD — goals, users, acceptance criteria, scope',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'adr',
|
|
65
|
+
ordinal: 2,
|
|
66
|
+
persona: 'marcus',
|
|
67
|
+
personaLabel: 'Marcus (CTO)',
|
|
68
|
+
artifactFilename: 'ADR.md',
|
|
69
|
+
briefTemplate: 'Produce an architectural decision record for chain {{chainId}}. ' +
|
|
70
|
+
'Read the PRD at .pugi/chains/{{chainId}}/PRD.md verbatim. ' +
|
|
71
|
+
'Follow the team ADR template: Status, Context, Decision, ' +
|
|
72
|
+
'Consequences, Alternatives Considered. ' +
|
|
73
|
+
'Be explicit about boundary changes the decision implies.',
|
|
74
|
+
gloss: 'Architectural decision record per team ADR template',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: 'mindmap',
|
|
78
|
+
ordinal: 3,
|
|
79
|
+
persona: 'marcus',
|
|
80
|
+
personaLabel: 'Marcus (CTO)',
|
|
81
|
+
artifactFilename: 'MINDMAP.mmd',
|
|
82
|
+
briefTemplate: 'Produce a mermaid mindmap of the solution surface for chain {{chainId}}. ' +
|
|
83
|
+
'Read PRD + ADR at .pugi/chains/{{chainId}}/PRD.md and ADR.md. ' +
|
|
84
|
+
'Root node = the feature name; first-level branches = subsystems; ' +
|
|
85
|
+
'leaves = concrete modules / endpoints / tables. ' +
|
|
86
|
+
'Output ONE mermaid `mindmap` fenced block — no prose.',
|
|
87
|
+
gloss: 'Mermaid mindmap — solution surface (subsystems + modules)',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: 'er',
|
|
91
|
+
ordinal: 4,
|
|
92
|
+
persona: 'hiroshi',
|
|
93
|
+
personaLabel: 'Hiroshi (Lead Dev)',
|
|
94
|
+
artifactFilename: 'ER.mmd',
|
|
95
|
+
briefTemplate: 'Produce a mermaid entity-relationship diagram for chain {{chainId}}. ' +
|
|
96
|
+
'Source: PRD + ADR + MINDMAP under .pugi/chains/{{chainId}}/. ' +
|
|
97
|
+
'Use `erDiagram` syntax. Cover every persisted entity the ' +
|
|
98
|
+
'design implies; mark PK / FK relationships explicitly.',
|
|
99
|
+
gloss: 'Mermaid ER diagram — persisted entities + relationships',
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: 'sequence',
|
|
103
|
+
ordinal: 5,
|
|
104
|
+
persona: 'hiroshi',
|
|
105
|
+
personaLabel: 'Hiroshi (Lead Dev)',
|
|
106
|
+
artifactFilename: 'SEQUENCE.mmd',
|
|
107
|
+
briefTemplate: 'Produce mermaid sequence diagrams for chain {{chainId}}. ' +
|
|
108
|
+
'Cover the critical happy path + at least one sad path. ' +
|
|
109
|
+
'Source: PRD acceptance criteria + ER diagram. ' +
|
|
110
|
+
'Use `sequenceDiagram` syntax; one diagram per fenced block.',
|
|
111
|
+
gloss: 'Mermaid sequence diagrams — happy + sad path flows',
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: 'tests',
|
|
115
|
+
ordinal: 6,
|
|
116
|
+
persona: 'vera',
|
|
117
|
+
personaLabel: 'Vera (QA)',
|
|
118
|
+
artifactFilename: 'TESTS.md',
|
|
119
|
+
briefTemplate: 'Produce spec scaffolding for chain {{chainId}}. ' +
|
|
120
|
+
'Map every numbered PRD acceptance criterion (under ## Acceptance Criteria) ' +
|
|
121
|
+
'to one or more concrete test descriptions using node:test + node:assert. ' +
|
|
122
|
+
'Format: criterion-id → test file path → `it(...)` blocks. ' +
|
|
123
|
+
'Output markdown only — no executable code.',
|
|
124
|
+
gloss: 'Spec scaffolding — PRD criteria mapped to test descriptions',
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
id: 'code',
|
|
128
|
+
ordinal: 7,
|
|
129
|
+
persona: 'hiroshi',
|
|
130
|
+
personaLabel: 'Hiroshi (Lead Dev)',
|
|
131
|
+
artifactFilename: 'CODE.md',
|
|
132
|
+
briefTemplate: 'Produce an implementation plan + diff references for chain {{chainId}}. ' +
|
|
133
|
+
'Read PRD, ADR, MINDMAP, ER, SEQUENCE, TESTS under .pugi/chains/{{chainId}}/. ' +
|
|
134
|
+
'Output: file-by-file changes (path, change kind, summary), ' +
|
|
135
|
+
'with each change tied back to a PRD criterion id. ' +
|
|
136
|
+
'NO inline patches — patches land via `pugi patch apply` after operator review.',
|
|
137
|
+
gloss: 'Implementation plan — file changes mapped to PRD criteria',
|
|
138
|
+
},
|
|
139
|
+
]);
|
|
140
|
+
/**
|
|
141
|
+
* Resolve a step descriptor by id. Returns `undefined` for unknown ids
|
|
142
|
+
* so callers can surface a structured error instead of panicking.
|
|
143
|
+
*/
|
|
144
|
+
export function findStep(id) {
|
|
145
|
+
return CHAIN_STEPS.find((step) => step.id === id);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Index of step ids in chain order. Exported so the state machine can
|
|
149
|
+
* answer "what is the next step after X?" without re-scanning the
|
|
150
|
+
* table on every transition.
|
|
151
|
+
*/
|
|
152
|
+
export const CHAIN_STEP_IDS = Object.freeze(CHAIN_STEPS.map((s) => s.id));
|
|
153
|
+
/**
|
|
154
|
+
* Total number of steps in the chain. Hard-coded constant exported so
|
|
155
|
+
* downstream renderers can size their progress bars without iterating.
|
|
156
|
+
*/
|
|
157
|
+
export const CHAIN_STEP_COUNT = CHAIN_STEPS.length;
|
|
158
|
+
/**
|
|
159
|
+
* Interpolate the brief template with chain context. The template
|
|
160
|
+
* grammar is intentionally minimal — only `{{chainId}}` and `{{intent}}`
|
|
161
|
+
* are honoured. Unknown placeholders are left verbatim so a template
|
|
162
|
+
* typo surfaces in the dispatched brief instead of silently dropping.
|
|
163
|
+
*/
|
|
164
|
+
export function renderBrief(template, ctx) {
|
|
165
|
+
return template
|
|
166
|
+
.replace(/\{\{chainId\}\}/g, ctx.chainId)
|
|
167
|
+
.replace(/\{\{intent\}\}/g, ctx.intent);
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=steps.js.map
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const MIN_QUESTIONS = 1;
|
|
2
|
+
const MAX_QUESTIONS = 4;
|
|
3
|
+
const MIN_OPTIONS = 2;
|
|
4
|
+
const MAX_OPTIONS = 4;
|
|
5
|
+
const MAX_HEADER_LENGTH = 12;
|
|
6
|
+
export function validateBatch(batch) {
|
|
7
|
+
const errors = [];
|
|
8
|
+
if (!Array.isArray(batch.questions)) {
|
|
9
|
+
errors.push({
|
|
10
|
+
field: 'questions',
|
|
11
|
+
message: 'questions must be an array',
|
|
12
|
+
});
|
|
13
|
+
return errors;
|
|
14
|
+
}
|
|
15
|
+
if (batch.questions.length < MIN_QUESTIONS || batch.questions.length > MAX_QUESTIONS) {
|
|
16
|
+
errors.push({
|
|
17
|
+
field: 'questions',
|
|
18
|
+
message: 'questions must contain 1 to 4 items',
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
for (const [questionIndex, question] of batch.questions.entries()) {
|
|
22
|
+
const questionPath = `questions[${questionIndex}]`;
|
|
23
|
+
if (typeof question.question !== 'string' || !question.question.endsWith('?')) {
|
|
24
|
+
errors.push({
|
|
25
|
+
field: `${questionPath}.question`,
|
|
26
|
+
message: 'question must end with ?',
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
if (typeof question.header !== 'string' || question.header.length < 1 || question.header.length > MAX_HEADER_LENGTH) {
|
|
30
|
+
errors.push({
|
|
31
|
+
field: `${questionPath}.header`,
|
|
32
|
+
message: 'header must be 1 to 12 characters',
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
if (typeof question.multiSelect !== 'boolean') {
|
|
36
|
+
errors.push({
|
|
37
|
+
field: `${questionPath}.multiSelect`,
|
|
38
|
+
message: 'multiSelect must be a boolean',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
if (!Array.isArray(question.options)) {
|
|
42
|
+
errors.push({
|
|
43
|
+
field: `${questionPath}.options`,
|
|
44
|
+
message: 'options must be an array',
|
|
45
|
+
});
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (question.options.length < MIN_OPTIONS || question.options.length > MAX_OPTIONS) {
|
|
49
|
+
errors.push({
|
|
50
|
+
field: `${questionPath}.options`,
|
|
51
|
+
message: 'options must contain 2 to 4 items',
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
for (const [optionIndex, option] of question.options.entries()) {
|
|
55
|
+
const optionPath = `${questionPath}.options[${optionIndex}]`;
|
|
56
|
+
if (typeof option.label !== 'string' || option.label.length < 1) {
|
|
57
|
+
errors.push({
|
|
58
|
+
field: `${optionPath}.label`,
|
|
59
|
+
message: 'label must be a non-empty string',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
if (typeof option.description !== 'string' || option.description.length < 1) {
|
|
63
|
+
errors.push({
|
|
64
|
+
field: `${optionPath}.description`,
|
|
65
|
+
message: 'description must be a non-empty string',
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
if (option.preview !== undefined && (typeof option.preview !== 'string' || option.preview.length < 1)) {
|
|
69
|
+
errors.push({
|
|
70
|
+
field: `${optionPath}.preview`,
|
|
71
|
+
message: 'preview must be a non-empty string when set',
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
if (option.recommended !== undefined && typeof option.recommended !== 'boolean') {
|
|
75
|
+
errors.push({
|
|
76
|
+
field: `${optionPath}.recommended`,
|
|
77
|
+
message: 'recommended must be a boolean when set',
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return errors;
|
|
83
|
+
}
|
|
84
|
+
export function formatHeader(header) {
|
|
85
|
+
if (header.length <= MAX_HEADER_LENGTH)
|
|
86
|
+
return header;
|
|
87
|
+
return `${header.slice(0, MAX_HEADER_LENGTH - 3)}...`;
|
|
88
|
+
}
|
|
89
|
+
export function findRecommended(options) {
|
|
90
|
+
return options.find(option => option.recommended === true) ?? null;
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=question.js.map
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tenant-wide JSONL audit trail .
|
|
3
|
+
*
|
|
4
|
+
* Pugi already records every tool_call / tool_result in two places:
|
|
5
|
+
*
|
|
6
|
+
* 1. The global per-workspace log at `<workspace>/.pugi/events.jsonl`
|
|
7
|
+
* (audit-replay source of truth; see `core/session.ts`).
|
|
8
|
+
* 2. The per-session mirror at
|
|
9
|
+
* `<workspace>/.pugi/sessions/<sessionId>/events.jsonl`
|
|
10
|
+
* (operator-friendly per-run copy, see `native-pugi.ts`).
|
|
11
|
+
*
|
|
12
|
+
* Both live under the workspace directory and disappear when the
|
|
13
|
+
* operator wipes the workspace or runs many ephemeral sandboxes.
|
|
14
|
+
* What's missing is a TENANT-wide structured audit log: a single
|
|
15
|
+
* append-only NDJSON stream per (tenant, workspace) pair that the
|
|
16
|
+
* operator (or a SOC pipeline) can tail across every session over
|
|
17
|
+
* the lifetime of the host.
|
|
18
|
+
*
|
|
19
|
+
* Spec :
|
|
20
|
+
*
|
|
21
|
+
* - Path: `~/.pugi/audit/<tenant>/<workspace-slug>-<hash>.jsonl`
|
|
22
|
+
* - One JSON line per event with shared shape:
|
|
23
|
+
* `{ ts, tenant, workspace, workspaceHash, event, sessionId, data }`
|
|
24
|
+
* - Events covered: `tool_call`, `tool_result`, `dispatch_start`,
|
|
25
|
+
* `dispatch_end`, `permission_denied`, `auto_compact`,
|
|
26
|
+
* `budget_exhausted`.
|
|
27
|
+
* - Append-only — no rotation logic. Operators wire `logrotate`
|
|
28
|
+
* themselves if they want size caps.
|
|
29
|
+
* - Opt-out: `PUGI_AUDIT_TRAIL_DISABLE=1`.
|
|
30
|
+
* - Failures NEVER throw. Audit MUST NOT break a dispatch.
|
|
31
|
+
* - Tenant fallback: when `PUGI_API_KEY` is unset, tenant is `local`.
|
|
32
|
+
*
|
|
33
|
+
* Why duplicate the per-session log on disk:
|
|
34
|
+
*
|
|
35
|
+
* The per-session mirror clusters by `sessionId` (one dir per run).
|
|
36
|
+
* To answer "what did this tenant DO across every session this week
|
|
37
|
+
* from this workspace" an operator otherwise has to glob hundreds of
|
|
38
|
+
* session dirs and merge by timestamp. The audit trail flattens that
|
|
39
|
+
* into one tail-able stream per (tenant, workspace) — same shape an
|
|
40
|
+
* ops pipeline would expect from a hosted log surface.
|
|
41
|
+
*/
|
|
42
|
+
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
43
|
+
import { createHash } from 'node:crypto';
|
|
44
|
+
import { homedir } from 'node:os';
|
|
45
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
46
|
+
import { collectStrings, scanForInjection, summarizeFindings, } from '../security/injection-scanner.js';
|
|
47
|
+
/**
|
|
48
|
+
* Opt-out env var. Mirrors the convention every other Pugi feature uses
|
|
49
|
+
* (`PUGI_BARE`, `PUGI_AGENTMEMORY_RECALL_ENABLED=false`, etc.).
|
|
50
|
+
* Operators set this when they pipe the CLI through a sandbox that
|
|
51
|
+
* already captures audit upstream and they want to skip the duplicate.
|
|
52
|
+
*/
|
|
53
|
+
export const PUGI_AUDIT_TRAIL_DISABLE_VAR = 'PUGI_AUDIT_TRAIL_DISABLE';
|
|
54
|
+
/**
|
|
55
|
+
* Tenant fallback used when the operator has not exported
|
|
56
|
+
* `PUGI_API_KEY`. The audit trail still flows — it just lives under
|
|
57
|
+
* `~/.pugi/audit/local/...` so a single-user workstation gets a useful
|
|
58
|
+
* forensic log without needing API-key plumbing.
|
|
59
|
+
*/
|
|
60
|
+
export const LOCAL_TENANT_FALLBACK = 'local';
|
|
61
|
+
/**
|
|
62
|
+
* Sanitize the workspace basename to a safe filesystem slug:
|
|
63
|
+
* lowercase a-z + 0-9 + `-`. Anything else collapses to `-`. We avoid
|
|
64
|
+
* the empty case (root workspace) by falling back to `workspace`.
|
|
65
|
+
*
|
|
66
|
+
* Why not a hash here too: the hash is appended separately so two
|
|
67
|
+
* workspaces with the same basename (e.g. two clones of the same repo
|
|
68
|
+
* sitting in different parent dirs) get distinct files. The slug is
|
|
69
|
+
* the human-readable half operators eyeball at `ls ~/.pugi/audit/...`.
|
|
70
|
+
*/
|
|
71
|
+
export function sanitizeWorkspaceSlug(workspaceRoot) {
|
|
72
|
+
const base = basename(resolve(workspaceRoot));
|
|
73
|
+
const sanitized = base
|
|
74
|
+
.toLowerCase()
|
|
75
|
+
.replace(/[^a-z0-9-]+/g, '-')
|
|
76
|
+
.replace(/-+/g, '-')
|
|
77
|
+
.replace(/^-|-$/g, '');
|
|
78
|
+
return sanitized.length > 0 ? sanitized : 'workspace';
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Stable, anonymous workspace handle. We use the FIRST 8 hex of
|
|
82
|
+
* sha256(workspaceRoot). 8 hex = 32 bits = ~4 billion buckets, more
|
|
83
|
+
* than enough to disambiguate `~/code/foo` from `~/other/foo` on the
|
|
84
|
+
* same host without leaking the absolute path through the file name.
|
|
85
|
+
*
|
|
86
|
+
* The hash is over the RESOLVED path so symlink trickery cannot point
|
|
87
|
+
* two different audit streams at the same file by accident.
|
|
88
|
+
*/
|
|
89
|
+
export function computeWorkspaceHash(workspaceRoot) {
|
|
90
|
+
return createHash('sha256')
|
|
91
|
+
.update(resolve(workspaceRoot))
|
|
92
|
+
.digest('hex')
|
|
93
|
+
.slice(0, 8);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Derive the tenant slug from `PUGI_API_KEY`. We hash the key (sha256,
|
|
97
|
+
* 12 hex prefix) rather than emitting the raw key — the audit trail is
|
|
98
|
+
* a plaintext file on the local FS and the tenant slug shows up in
|
|
99
|
+
* every path under `~/.pugi/audit/`. A truncated hash is enough to
|
|
100
|
+
* cluster every (tenant, workspace) over time without leaking the key
|
|
101
|
+
* if the operator accidentally `tar`s their `~/.pugi` for support.
|
|
102
|
+
*
|
|
103
|
+
* The hash is purely a CLI-local clustering key — the runtime backend
|
|
104
|
+
* has its own (different) tenant identifier and never sees this.
|
|
105
|
+
*/
|
|
106
|
+
export function resolveTenant(env = process.env) {
|
|
107
|
+
const key = env.PUGI_API_KEY?.trim();
|
|
108
|
+
if (!key)
|
|
109
|
+
return LOCAL_TENANT_FALLBACK;
|
|
110
|
+
// 12 hex = 48 bits — enough disambiguation for any realistic per-host
|
|
111
|
+
// tenant cardinality; still short enough for operators to eyeball at
|
|
112
|
+
// `ls ~/.pugi/audit/`.
|
|
113
|
+
return createHash('sha256').update(key).digest('hex').slice(0, 12);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Resolve the audit file path for a given (tenant, workspace) pair.
|
|
117
|
+
* Pure path arithmetic — the caller is responsible for `mkdir -p`
|
|
118
|
+
* before append (handled inside `writeAuditEvent`).
|
|
119
|
+
*/
|
|
120
|
+
export function resolveAuditPath(workspaceRoot, tenant, home = homedir()) {
|
|
121
|
+
const slug = sanitizeWorkspaceSlug(workspaceRoot);
|
|
122
|
+
const hash = computeWorkspaceHash(workspaceRoot);
|
|
123
|
+
return join(home, '.pugi', 'audit', tenant, `${slug}-${hash}.jsonl`);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Predicate: is the audit trail disabled via env opt-out?
|
|
127
|
+
*
|
|
128
|
+
* Accept `1`, `true`, `yes` (case-insensitive) as positive; anything
|
|
129
|
+
* else — including `0`, `false`, `''`, and the var being absent — keeps
|
|
130
|
+
* the trail enabled. Mirrors the convention used in `bare-mode/` and
|
|
131
|
+
* elsewhere in the CLI.
|
|
132
|
+
*/
|
|
133
|
+
export function isAuditDisabled(env = process.env) {
|
|
134
|
+
const raw = env[PUGI_AUDIT_TRAIL_DISABLE_VAR]?.trim().toLowerCase();
|
|
135
|
+
if (!raw)
|
|
136
|
+
return false;
|
|
137
|
+
return raw === '1' || raw === 'true' || raw === 'yes';
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Append a single audit event to the per-tenant per-workspace NDJSON
|
|
141
|
+
* trail. Never throws — failures (FS unwritable, opt-out, malformed
|
|
142
|
+
* input) are silently swallowed so a misconfigured audit surface
|
|
143
|
+
* cannot break a dispatch. The engine adapter's existing per-session
|
|
144
|
+
* mirror remains intact as a redundant copy.
|
|
145
|
+
*
|
|
146
|
+
* Append-only: every call writes exactly one line. No rotation, no
|
|
147
|
+
* truncation. Operators wire `logrotate` if they want size caps.
|
|
148
|
+
*
|
|
149
|
+
* macOS hardening: we `mkdir -p` the parent dir on every call (cheap
|
|
150
|
+
* in practice — Node short-circuits when the dir exists) so a manual
|
|
151
|
+
* `rm -rf ~/.pugi/audit/<tenant>/` between runs does not turn the next
|
|
152
|
+
* append into ENOENT. The mode is `0o700` for the tenant dir and
|
|
153
|
+
* `0o600` for the JSONL file so curious users on a shared host cannot
|
|
154
|
+
* read another tenant's trail.
|
|
155
|
+
*/
|
|
156
|
+
export function writeAuditEvent(input) {
|
|
157
|
+
const env = input.env ?? process.env;
|
|
158
|
+
if (isAuditDisabled(env))
|
|
159
|
+
return;
|
|
160
|
+
try {
|
|
161
|
+
const tenant = (input.tenant?.trim() || resolveTenant(env)) || LOCAL_TENANT_FALLBACK;
|
|
162
|
+
const home = input.home ?? homedir();
|
|
163
|
+
const path = resolveAuditPath(input.workspaceRoot, tenant, home);
|
|
164
|
+
const now = input.now ? input.now() : new Date().toISOString();
|
|
165
|
+
const envelope = {
|
|
166
|
+
ts: now,
|
|
167
|
+
tenant,
|
|
168
|
+
workspace: sanitizeWorkspaceSlug(input.workspaceRoot),
|
|
169
|
+
workspaceHash: computeWorkspaceHash(input.workspaceRoot),
|
|
170
|
+
event: input.event,
|
|
171
|
+
sessionId: input.sessionId,
|
|
172
|
+
data: input.data,
|
|
173
|
+
};
|
|
174
|
+
try {
|
|
175
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// mkdir failure is silent — the appendFileSync below will surface
|
|
179
|
+
// the real error and the outer catch swallows it. We still try
|
|
180
|
+
// the write so EEXIST on the dir (the only real path here) does
|
|
181
|
+
// not block the append.
|
|
182
|
+
}
|
|
183
|
+
appendFileSync(path, `${JSON.stringify(envelope)}\n`, {
|
|
184
|
+
encoding: 'utf8',
|
|
185
|
+
mode: 0o600,
|
|
186
|
+
});
|
|
187
|
+
// Injection scan (ported an external utility,
|
|
188
|
+
// Apache-2.0). Wrap the OUTBOUND `data` payload through the
|
|
189
|
+
// scanner. Findings emit a SECOND audit line of type
|
|
190
|
+
// `injection_detected` so an operator (or SOC pipeline) sees a
|
|
191
|
+
// structured, append-only record without losing the original
|
|
192
|
+
// event. Never blocks the write — hard-block requires a separate
|
|
193
|
+
// CEO-signed PR.
|
|
194
|
+
//
|
|
195
|
+
// Recursion guard: the `injection_detected` event itself carries
|
|
196
|
+
// matched substrings (intentional — they are the evidence). We
|
|
197
|
+
// skip scanning it to avoid an infinite loop of self-detections.
|
|
198
|
+
if (input.event !== 'injection_detected') {
|
|
199
|
+
const findings = scanAuditPayload(input.data);
|
|
200
|
+
if (findings.length > 0) {
|
|
201
|
+
emitInjectionDetected({
|
|
202
|
+
findings,
|
|
203
|
+
triggeringEvent: input.event,
|
|
204
|
+
sessionId: input.sessionId,
|
|
205
|
+
workspaceRoot: input.workspaceRoot,
|
|
206
|
+
tenant: input.tenant,
|
|
207
|
+
env: input.env,
|
|
208
|
+
home: input.home,
|
|
209
|
+
now: input.now,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
// Audit failures must NEVER break a dispatch. The session log + the
|
|
216
|
+
// per-session mirror under `<workspace>/.pugi/` remain as redundant
|
|
217
|
+
// surfaces. A future telemetry pass can surface the failure count
|
|
218
|
+
// via the doctor probe; for now silent no-op is the contract.
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Fold the audit `data` payload into a single string and scan it for
|
|
223
|
+
* prompt-injection / invisible-unicode / secret markers. Returns the
|
|
224
|
+
* empty array on clean payloads.
|
|
225
|
+
*
|
|
226
|
+
* Exported for the spec — the scanner module owns the algorithm, this
|
|
227
|
+
* helper owns the payload-walking glue.
|
|
228
|
+
*/
|
|
229
|
+
export function scanAuditPayload(data) {
|
|
230
|
+
// Fold every string anywhere in the payload (keys included) into a
|
|
231
|
+
// single buffer separated by NULs. NUL keeps regex anchors honest
|
|
232
|
+
// (no accidental cross-field match for a `^system:` pattern) without
|
|
233
|
+
// adding bytes that themselves could become a pattern.
|
|
234
|
+
const fragments = collectStrings(data);
|
|
235
|
+
if (fragments.length === 0)
|
|
236
|
+
return [];
|
|
237
|
+
const joined = fragments.join('\0');
|
|
238
|
+
return scanForInjection(joined);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Build the `injection_detected` envelope payload and recurse into
|
|
242
|
+
* `writeAuditEvent` to append it. The recursion is bounded — the
|
|
243
|
+
* recursion guard in `writeAuditEvent` short-circuits on the
|
|
244
|
+
* `injection_detected` event so we never re-scan ourselves.
|
|
245
|
+
*/
|
|
246
|
+
function emitInjectionDetected(input) {
|
|
247
|
+
const summary = summarizeFindings(input.findings);
|
|
248
|
+
// Cap the findings array in the audit line so a payload with
|
|
249
|
+
// hundreds of invisible-unicode hits does not bloat the JSONL row.
|
|
250
|
+
// The summary still carries `total` so operators see the real count.
|
|
251
|
+
const MAX_FINDINGS_PER_EVENT = 32;
|
|
252
|
+
const truncated = input.findings.length > MAX_FINDINGS_PER_EVENT;
|
|
253
|
+
const capped = truncated
|
|
254
|
+
? input.findings.slice(0, MAX_FINDINGS_PER_EVENT)
|
|
255
|
+
: [...input.findings];
|
|
256
|
+
writeAuditEvent({
|
|
257
|
+
event: 'injection_detected',
|
|
258
|
+
sessionId: input.sessionId,
|
|
259
|
+
workspaceRoot: input.workspaceRoot,
|
|
260
|
+
tenant: input.tenant,
|
|
261
|
+
env: input.env,
|
|
262
|
+
home: input.home,
|
|
263
|
+
now: input.now,
|
|
264
|
+
data: {
|
|
265
|
+
triggeringEvent: input.triggeringEvent,
|
|
266
|
+
summary,
|
|
267
|
+
findings: capped,
|
|
268
|
+
truncated,
|
|
269
|
+
// External attribution is recorded inline so a SOC pipeline
|
|
270
|
+
// grepping for the upstream project name lands here.
|
|
271
|
+
detector: 'external-injection-patterns',
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
//# sourceMappingURL=audit-trail.js.map
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UX — `ensureAuthenticated` helper.
|
|
3
|
+
*
|
|
4
|
+
* Auto-login pre-flight for every Pugi command that needs an Anvil
|
|
5
|
+
* credential. Before this helper landed, cold-start without a stored
|
|
6
|
+
* credential surfaced a generic "Login required" message and the
|
|
7
|
+
* operator had к run `pugi login` separately, which broke the muscle-
|
|
8
|
+
* memory of "open terminal, type the command, see the answer".
|
|
9
|
+
*
|
|
10
|
+
* The helper exposes a single contract: `ensureAuthenticated(opts)`.
|
|
11
|
+
* On a happy path (credential resolves) it returns the credential.
|
|
12
|
+
* On a cold-start it either:
|
|
13
|
+
*
|
|
14
|
+
* - launches the device-flow login inline (interactive TTY only,
|
|
15
|
+
* `--no-login` not set), waits for completion, then re-resolves
|
|
16
|
+
* the credential and returns it. The surrounding command continues
|
|
17
|
+
* transparently;
|
|
18
|
+
* - returns `{ status: 'missing' }` with a reason describing why no
|
|
19
|
+
* auto-login was attempted (non-interactive, opted-out, or login
|
|
20
|
+
* aborted by user). The caller bails with a clean message.
|
|
21
|
+
*
|
|
22
|
+
* Cross-command parity: the helper is wired into every command that
|
|
23
|
+
* needs auth (engine commands `code`/`fix`/`build`/`explain`/`plan`,
|
|
24
|
+
* plus `sync`, `chain new`, `smoke`, `review`, `deploy`, ...). The
|
|
25
|
+
* previous patchwork of `resolveActiveCredential() ?? throw` /
|
|
26
|
+
* `if (!config) writeOutput unauthenticated` calls now all funnel
|
|
27
|
+
* through here so future auth changes are one-edit.
|
|
28
|
+
*
|
|
29
|
+
* Session cache: the helper caches the resolved credential per-process.
|
|
30
|
+
* A second command in the same process never re-launches login even if
|
|
31
|
+
* the operator deletes credentials.json mid-run (that is a footgun, not
|
|
32
|
+
* a supported use case — the cached credential is still valid because
|
|
33
|
+
* the auth token in memory has not been revoked).
|
|
34
|
+
*
|
|
35
|
+
* Framework-free: the actual login call is injected via the `login`
|
|
36
|
+
* callback. The CLI passes a closure that calls
|
|
37
|
+
* `performDeviceFlowLogin` (or the interactive picker for token /
|
|
38
|
+
* env). The spec passes a fake that flips an in-memory env var so
|
|
39
|
+
* subsequent `resolveActiveCredential` calls see a credential.
|
|
40
|
+
*/
|
|
41
|
+
/**
|
|
42
|
+
* Process-local cache of resolved credentials. Keyed by `apiUrl` so a
|
|
43
|
+
* future `pugi accounts switch` invocation does not return stale data
|
|
44
|
+
* (different apiUrl → cache miss). Cache is additive-only.
|
|
45
|
+
*/
|
|
46
|
+
const credentialCache = new Map();
|
|
47
|
+
/**
|
|
48
|
+
* Reset the cache. Exported for spec teardown — production callers
|
|
49
|
+
* never need this.
|
|
50
|
+
*/
|
|
51
|
+
export function resetAuthenticatedCache() {
|
|
52
|
+
credentialCache.clear();
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Auth pre-flight. Returns the resolved credential or a structured
|
|
56
|
+
* `missing` envelope. The cached path skips the `resolve()` callback
|
|
57
|
+
* entirely — useful when `resolveActiveCredential` is expensive
|
|
58
|
+
* (filesystem read of ~/.pugi/credentials.json + Zod parse).
|
|
59
|
+
*
|
|
60
|
+
* Headless contract: even on a TTY, when `headless === true` the
|
|
61
|
+
* helper bails with `non_interactive`. Reason: a browser-popup login
|
|
62
|
+
* in the middle of an automated stdin → engine → stdout loop would
|
|
63
|
+
* silently freeze the run.
|
|
64
|
+
*/
|
|
65
|
+
export async function ensureAuthenticated(opts) {
|
|
66
|
+
// Resolve once. Cache by the resolved apiUrl so a subsequent call
|
|
67
|
+
// after `pugi accounts switch` produces a fresh resolution.
|
|
68
|
+
const initial = opts.resolve();
|
|
69
|
+
if (initial) {
|
|
70
|
+
credentialCache.set(initial.apiUrl, initial);
|
|
71
|
+
return { status: 'ready', credential: initial };
|
|
72
|
+
}
|
|
73
|
+
if (opts.skip) {
|
|
74
|
+
return {
|
|
75
|
+
status: 'missing',
|
|
76
|
+
reason: 'disabled',
|
|
77
|
+
detail: 'Authentication skipped (--no-login or PUGI_NO_AUTO_LOGIN). Run `pugi login` to authenticate.',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (opts.headless) {
|
|
81
|
+
return {
|
|
82
|
+
status: 'missing',
|
|
83
|
+
reason: 'non_interactive',
|
|
84
|
+
detail: 'Headless mode cannot launch browser-popup login. Run `pugi login` once with a TTY, then re-run with --headless.',
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (!opts.interactive) {
|
|
88
|
+
return {
|
|
89
|
+
status: 'missing',
|
|
90
|
+
reason: 'non_interactive',
|
|
91
|
+
detail: 'No credential found and stdin is not a TTY. Run `pugi login` with a TTY OR set PUGI_API_KEY before invoking Pugi in CI.',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const write = opts.write ?? ((line) => process.stderr.write(line));
|
|
95
|
+
write('No Pugi credential found. Launching login...\n');
|
|
96
|
+
let succeeded;
|
|
97
|
+
try {
|
|
98
|
+
succeeded = await opts.login();
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
return {
|
|
102
|
+
status: 'missing',
|
|
103
|
+
reason: 'login_failed',
|
|
104
|
+
detail: `Login failed: ${error.message ?? String(error)}`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
if (!succeeded) {
|
|
108
|
+
return {
|
|
109
|
+
status: 'missing',
|
|
110
|
+
reason: 'login_cancelled',
|
|
111
|
+
detail: 'Authentication required to continue. Run `pugi login` when ready.',
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
// Re-resolve. If a successful login was reported but no credential
|
|
115
|
+
// landed on disk, surface that as `login_failed` rather than a
|
|
116
|
+
// silent miss — would otherwise produce a confusing "you said it
|
|
117
|
+
// worked but I still see nothing" loop.
|
|
118
|
+
const resolved = opts.resolve();
|
|
119
|
+
if (!resolved) {
|
|
120
|
+
return {
|
|
121
|
+
status: 'missing',
|
|
122
|
+
reason: 'login_failed',
|
|
123
|
+
detail: 'Login reported success but no credential persisted. Check `pugi whoami`.',
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
credentialCache.set(resolved.apiUrl, resolved);
|
|
127
|
+
return { status: 'ready', credential: resolved };
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=ensure-authenticated.js.map
|