@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,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* brief tool — operator-facing progress brief (tool gap pack).
|
|
3
|
+
*
|
|
4
|
+
* Emits a short, structured status note from the engine to the operator
|
|
5
|
+
* without consuming an assistant turn for narrative. The model calls
|
|
6
|
+
* this tool when it wants to surface "what I am doing right now" — a
|
|
7
|
+
* planning sketch, a blocker reason, a final wrap — in a form the
|
|
8
|
+
* operator can scan at-a-glance.
|
|
9
|
+
*
|
|
10
|
+
* Wire shape:
|
|
11
|
+
* args: { headline: string, status: 'planning'|'working'|'blocked'|'done',
|
|
12
|
+
* detail?: string }
|
|
13
|
+
* side-effect: append one JSON line to `.pugi/briefs/<sessionId>.jsonl`,
|
|
14
|
+
* via the atomic tmp+rename pattern (see writeBriefLine).
|
|
15
|
+
* return: one-line text envelope the engine echoes back to the
|
|
16
|
+
* operator's stdout (the engine adapter renders it as a
|
|
17
|
+
* neutral system line above the next model output).
|
|
18
|
+
*
|
|
19
|
+
* Why JSONL (one record per line) instead of a single rewritten JSON
|
|
20
|
+
* document: briefs accumulate across a session and the operator may
|
|
21
|
+
* want to scroll back through them; a JSONL tail is the natural shape
|
|
22
|
+
* for "give me the last N briefs" — same convention `.pugi/sessions/`
|
|
23
|
+
* already uses for tool-call events.
|
|
24
|
+
*
|
|
25
|
+
* Why atomic tmp+rename for append: a concurrent reader (`pugi briefs
|
|
26
|
+
* --tail`, future TUI surface) must never see a half-written line. The
|
|
27
|
+
* pattern: read existing body -> append the new line -> write the
|
|
28
|
+
* combined body to a sibling tmp file -> rename. Atomic on the same
|
|
29
|
+
* filesystem and POSIX-portable. The overhead (re-write the whole file
|
|
30
|
+
* on each append) is acceptable because briefs are short and capped to
|
|
31
|
+
* a small per-session budget by upstream rate limits.
|
|
32
|
+
*
|
|
33
|
+
* Brand voice: English only, no emoji, no banned words.
|
|
34
|
+
*/
|
|
35
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
36
|
+
import { dirname, join, resolve } from 'node:path';
|
|
37
|
+
/** Maximum header length. Mirrors the agent-progress card cap so the
|
|
38
|
+
* TUI can render briefs and progress cards in the same visual lane. */
|
|
39
|
+
export const BRIEF_HEADLINE_MAX = 120;
|
|
40
|
+
/** Maximum detail length. Keeps a single brief well below the 4 KiB
|
|
41
|
+
* line limit some tooling assumes for JSONL files. */
|
|
42
|
+
export const BRIEF_DETAIL_MAX = 2_000;
|
|
43
|
+
/** Hard cap on total bytes per brief line. Defense-in-depth gate so a
|
|
44
|
+
* pathological detail-with-headline-with-status combo cannot blow the
|
|
45
|
+
* tail consumer's per-line buffer. */
|
|
46
|
+
export const BRIEF_LINE_MAX_BYTES = 4_096;
|
|
47
|
+
/** Canonical brief statuses. The set is intentionally tiny — the goal
|
|
48
|
+
* is "what is the agent doing" at a glance, not a free-form taxonomy. */
|
|
49
|
+
export const BRIEF_STATUSES = ['planning', 'working', 'blocked', 'done'];
|
|
50
|
+
/** Sentinel returned when the input fails schema validation. The
|
|
51
|
+
* dispatcher pattern-matches on the prefix for retry-budget bookkeeping
|
|
52
|
+
* and the model self-corrects from the issue list that follows. */
|
|
53
|
+
export const BRIEF_INVALID_ARGS = 'BRIEF_INVALID_ARGS';
|
|
54
|
+
/** Sentinel returned when the encoded line would exceed the per-line
|
|
55
|
+
* byte cap. Distinct from the args-schema failure so the model knows it
|
|
56
|
+
* needs to shorten the detail / headline, not change the shape. */
|
|
57
|
+
export const BRIEF_LINE_TOO_LARGE = 'BRIEF_LINE_TOO_LARGE';
|
|
58
|
+
/**
|
|
59
|
+
* Validate the raw arguments. Returns the typed value on success or a
|
|
60
|
+
* `BRIEF_INVALID_ARGS: ...` sentinel string the dispatcher surfaces back
|
|
61
|
+
* to the model.
|
|
62
|
+
*/
|
|
63
|
+
export function parseBriefArgs(raw) {
|
|
64
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
65
|
+
return `${BRIEF_INVALID_ARGS}: arguments must be a JSON object`;
|
|
66
|
+
}
|
|
67
|
+
const obj = raw;
|
|
68
|
+
const issues = [];
|
|
69
|
+
const headline = obj['headline'];
|
|
70
|
+
if (typeof headline !== 'string') {
|
|
71
|
+
issues.push('headline: must be a string');
|
|
72
|
+
}
|
|
73
|
+
else if (headline.trim().length === 0) {
|
|
74
|
+
issues.push('headline: must be non-empty');
|
|
75
|
+
}
|
|
76
|
+
else if (headline.length > BRIEF_HEADLINE_MAX) {
|
|
77
|
+
issues.push(`headline: must be <= ${BRIEF_HEADLINE_MAX} chars`);
|
|
78
|
+
}
|
|
79
|
+
const status = obj['status'];
|
|
80
|
+
if (typeof status !== 'string') {
|
|
81
|
+
issues.push('status: must be a string');
|
|
82
|
+
}
|
|
83
|
+
else if (!BRIEF_STATUSES.includes(status)) {
|
|
84
|
+
issues.push(`status: must be one of ${BRIEF_STATUSES.join('|')}`);
|
|
85
|
+
}
|
|
86
|
+
let detail;
|
|
87
|
+
if (obj['detail'] !== undefined && obj['detail'] !== null) {
|
|
88
|
+
if (typeof obj['detail'] !== 'string') {
|
|
89
|
+
issues.push('detail: must be a string when present');
|
|
90
|
+
}
|
|
91
|
+
else if (obj['detail'].length > BRIEF_DETAIL_MAX) {
|
|
92
|
+
issues.push(`detail: must be <= ${BRIEF_DETAIL_MAX} chars`);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
detail = obj['detail'];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (issues.length > 0) {
|
|
99
|
+
return `${BRIEF_INVALID_ARGS}: ${issues.join('; ')}`;
|
|
100
|
+
}
|
|
101
|
+
const result = detail !== undefined
|
|
102
|
+
? { headline: headline, status: status, detail }
|
|
103
|
+
: { headline: headline, status: status };
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Compute the on-disk path for a session's brief log. Public so a
|
|
108
|
+
* future tail consumer (`pugi briefs --tail <session>`) can share the
|
|
109
|
+
* exact resolution rules without duplicating layout knowledge.
|
|
110
|
+
*/
|
|
111
|
+
export function briefLogPath(ctx) {
|
|
112
|
+
const safe = sanitizeSessionId(ctx.sessionId);
|
|
113
|
+
return join(resolve(ctx.workspaceRoot), '.pugi', 'briefs', `${safe}.jsonl`);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Sanitise a session id for use as a filename. Restricts to the same
|
|
117
|
+
* character set agent-progress uses (`[A-Za-z0-9_-]+`) so the basename
|
|
118
|
+
* cannot escape the briefs directory via a `..` segment. Collapses any
|
|
119
|
+
* disallowed character to `_`; an empty result falls back to `session`.
|
|
120
|
+
*
|
|
121
|
+
* NOT exported as a generic helper because the only caller is this
|
|
122
|
+
* module — keep the surface area minimal.
|
|
123
|
+
*/
|
|
124
|
+
function sanitizeSessionId(id) {
|
|
125
|
+
if (typeof id !== 'string' || id.length === 0)
|
|
126
|
+
return 'session';
|
|
127
|
+
const collapsed = id.replace(/[^A-Za-z0-9_-]/g, '_');
|
|
128
|
+
const trimmed = collapsed.slice(0, 64);
|
|
129
|
+
return trimmed.length > 0 ? trimmed : 'session';
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Dispatch entry point. Validates input, persists the brief line, and
|
|
133
|
+
* returns the operator-visible echo envelope. Returns sentinels on
|
|
134
|
+
* recoverable failures so the engine loop can surface them as tool
|
|
135
|
+
* results without tearing down on a throw.
|
|
136
|
+
*/
|
|
137
|
+
export function dispatchBrief(ctx, raw) {
|
|
138
|
+
const parsed = parseBriefArgs(raw);
|
|
139
|
+
if (typeof parsed === 'string') {
|
|
140
|
+
return parsed;
|
|
141
|
+
}
|
|
142
|
+
const ts = (ctx.now ? ctx.now() : new Date()).toISOString();
|
|
143
|
+
const record = parsed.detail !== undefined
|
|
144
|
+
? { ts, status: parsed.status, headline: parsed.headline, detail: parsed.detail }
|
|
145
|
+
: { ts, status: parsed.status, headline: parsed.headline };
|
|
146
|
+
const line = JSON.stringify(record);
|
|
147
|
+
// Defense-in-depth byte cap. The earlier char-length checks bound the
|
|
148
|
+
// best case, but multi-byte UTF-8 input can still push the encoded
|
|
149
|
+
// line over the limit. Surface a distinct sentinel so the model knows
|
|
150
|
+
// to shorten the payload rather than change the shape.
|
|
151
|
+
if (Buffer.byteLength(line, 'utf8') > BRIEF_LINE_MAX_BYTES) {
|
|
152
|
+
return `${BRIEF_LINE_TOO_LARGE}: encoded line exceeds ${BRIEF_LINE_MAX_BYTES} bytes`;
|
|
153
|
+
}
|
|
154
|
+
const path = briefLogPath(ctx);
|
|
155
|
+
appendBriefLine(path, line);
|
|
156
|
+
return formatEcho(record, path);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Atomic append. Read existing body -> append the new line -> write to
|
|
160
|
+
* sibling tmp -> rename. The rename is atomic on the same filesystem
|
|
161
|
+
* so a concurrent tail consumer never reads a half-written line.
|
|
162
|
+
*
|
|
163
|
+
* Append-only via full rewrite is acceptable because brief logs are
|
|
164
|
+
* tiny (one short JSON object per call, sub-kilobyte each, capped by
|
|
165
|
+
* upstream rate limits to a few-hundred per session at most).
|
|
166
|
+
*/
|
|
167
|
+
function appendBriefLine(finalPath, line) {
|
|
168
|
+
const dir = dirname(finalPath);
|
|
169
|
+
if (!existsSync(dir)) {
|
|
170
|
+
mkdirSync(dir, { recursive: true });
|
|
171
|
+
}
|
|
172
|
+
const prior = existsSync(finalPath) ? readFileSync(finalPath, 'utf8') : '';
|
|
173
|
+
const tail = prior.length > 0 && !prior.endsWith('\n') ? '\n' : '';
|
|
174
|
+
const next = `${prior}${tail}${line}\n`;
|
|
175
|
+
const tmpPath = `${finalPath}.tmp-${process.pid}-${briefSequence++}`;
|
|
176
|
+
writeFileSync(tmpPath, next, { encoding: 'utf8', mode: 0o644 });
|
|
177
|
+
renameSync(tmpPath, finalPath);
|
|
178
|
+
}
|
|
179
|
+
let briefSequence = 0;
|
|
180
|
+
/**
|
|
181
|
+
* Render the one-line operator echo. The format is stable so a future
|
|
182
|
+
* REPL renderer can pattern-match the prefix for colouring.
|
|
183
|
+
*/
|
|
184
|
+
function formatEcho(record, path) {
|
|
185
|
+
const detail = record.detail ? ` -- ${truncate(record.detail, 240)}` : '';
|
|
186
|
+
return `[brief ${record.status}] ${record.headline}${detail} (logged to ${path})`;
|
|
187
|
+
}
|
|
188
|
+
function truncate(value, max) {
|
|
189
|
+
if (value.length <= max)
|
|
190
|
+
return value;
|
|
191
|
+
return `${value.slice(0, max - 1)}…`;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* JSON-Schema fragment the schema builder advertises to the model.
|
|
195
|
+
* Hand-written to mirror the `parseBriefArgs` checks 1:1 — same
|
|
196
|
+
* convention `todo_write` and `ask_user_question` use because the
|
|
197
|
+
* runtime engine wires OpenAI-shape JSON Schema and we have not
|
|
198
|
+
* greenlit a zod-to-json-schema dependency.
|
|
199
|
+
*/
|
|
200
|
+
export const briefJsonSchema = {
|
|
201
|
+
type: 'object',
|
|
202
|
+
additionalProperties: false,
|
|
203
|
+
required: ['headline', 'status'],
|
|
204
|
+
properties: {
|
|
205
|
+
headline: {
|
|
206
|
+
type: 'string',
|
|
207
|
+
minLength: 1,
|
|
208
|
+
maxLength: BRIEF_HEADLINE_MAX,
|
|
209
|
+
description: `Short one-line summary, <= ${BRIEF_HEADLINE_MAX} chars.`,
|
|
210
|
+
},
|
|
211
|
+
status: {
|
|
212
|
+
type: 'string',
|
|
213
|
+
enum: [...BRIEF_STATUSES],
|
|
214
|
+
description: 'Lifecycle state: planning (deciding what to do), working (in progress), ' +
|
|
215
|
+
'blocked (waiting on operator), done (final wrap).',
|
|
216
|
+
},
|
|
217
|
+
detail: {
|
|
218
|
+
type: 'string',
|
|
219
|
+
maxLength: BRIEF_DETAIL_MAX,
|
|
220
|
+
description: `Optional context, <= ${BRIEF_DETAIL_MAX} chars.`,
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
//# sourceMappingURL=brief.js.map
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* enter_worktree tool — scratch git worktree primitive (tool gap pack
|
|
3
|
+
*).
|
|
4
|
+
*
|
|
5
|
+
* Spawns `git worktree add <path> [<baseRef>]` at
|
|
6
|
+
* `.pugi/worktrees/<taskId>/` so a long-running agent task can land
|
|
7
|
+
* its edits into an isolated scratch tree. The matching `exit_worktree`
|
|
8
|
+
* tool is the cleanup primitive. The actual fan-out (one worktree per
|
|
9
|
+
* task in a backlog batch) lives behind a separate task — this tool is
|
|
10
|
+
* the primitive both call paths share.
|
|
11
|
+
*
|
|
12
|
+
* Wire shape:
|
|
13
|
+
* args: { taskId: string, baseRef?: string }
|
|
14
|
+
* - taskId: lowercase slug, `[a-z0-9][a-z0-9-]{0,63}`. The
|
|
15
|
+
* pattern bans path-traversal segments (`..`, `/`, `\\`) so
|
|
16
|
+
* the basename cannot escape `.pugi/worktrees/`.
|
|
17
|
+
* - baseRef: optional git ref (branch / tag / SHA). Defaults
|
|
18
|
+
* to `main` so the standard case is "fork from main and go".
|
|
19
|
+
* The CLI validates the ref shape only loosely — git
|
|
20
|
+
* rejects unknown refs upstream and that error message is
|
|
21
|
+
* surfaced verbatim.
|
|
22
|
+
* return: { ok: true, worktreePath, branchName } serialised JSON.
|
|
23
|
+
*
|
|
24
|
+
* Spawn discipline: every git invocation uses `spawnSync` with
|
|
25
|
+
* `shell: false` and a string-array argv so an unsanitised character
|
|
26
|
+
* inside a ref name cannot inject shell metacharacters. The taskId is
|
|
27
|
+
* gated by a strict regex BEFORE the spawn so the path argument is
|
|
28
|
+
* trusted.
|
|
29
|
+
*
|
|
30
|
+
* Path discipline: the worktree path is computed locally
|
|
31
|
+
* (`path.resolve(workspaceRoot, '.pugi/worktrees', taskId)`) and then
|
|
32
|
+
* containment-checked against the workspace root and the
|
|
33
|
+
* `.pugi/worktrees` subtree. Anything else throws — a deformed taskId
|
|
34
|
+
* that survived the regex (it should not) cannot pull the basename
|
|
35
|
+
* outside the expected subtree.
|
|
36
|
+
*
|
|
37
|
+
* Brand voice: English only, no emoji, no banned words.
|
|
38
|
+
*/
|
|
39
|
+
import { spawnSync } from 'node:child_process';
|
|
40
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
41
|
+
import { join, resolve, sep } from 'node:path';
|
|
42
|
+
/** Strict taskId pattern. Lowercase alphanumeric + hyphen, must start
|
|
43
|
+
* with a lowercase letter or digit, length 1..64. The first-character
|
|
44
|
+
* constraint stops a leading hyphen from being interpreted as a flag
|
|
45
|
+
* by `git worktree add` (defense in depth — argv form already protects
|
|
46
|
+
* us, but a leading hyphen is still a smell). */
|
|
47
|
+
export const TASK_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
48
|
+
/** Default base ref when the caller does not supply one. */
|
|
49
|
+
export const DEFAULT_BASE_REF = 'main';
|
|
50
|
+
/** Sentinels. Distinct prefixes so the model / dispatcher can
|
|
51
|
+
* pattern-match without parsing the trailing message. */
|
|
52
|
+
export const ENTER_WORKTREE_INVALID_ARGS = 'ENTER_WORKTREE_INVALID_ARGS';
|
|
53
|
+
export const ENTER_WORKTREE_GIT_FAILED = 'ENTER_WORKTREE_GIT_FAILED';
|
|
54
|
+
export const ENTER_WORKTREE_ALREADY_EXISTS = 'ENTER_WORKTREE_ALREADY_EXISTS';
|
|
55
|
+
export const ENTER_WORKTREE_PATH_ESCAPED = 'ENTER_WORKTREE_PATH_ESCAPED';
|
|
56
|
+
/**
|
|
57
|
+
* Validate the raw arguments. Returns the typed value on success or a
|
|
58
|
+
* `ENTER_WORKTREE_INVALID_ARGS: ...` sentinel string.
|
|
59
|
+
*/
|
|
60
|
+
/**
|
|
61
|
+
* Permissive git ref-name validation. We do not aim to fully implement
|
|
62
|
+
* `git check-ref-format` — instead reject anything outside a safe
|
|
63
|
+
* character class plus a leading-dash check. The leading-dash rule is
|
|
64
|
+
* the real defense: it stops an operator-controlled `baseRef` from
|
|
65
|
+
* being reinterpreted as a CLI flag (e.g. `--detach`, `--force`).
|
|
66
|
+
*/
|
|
67
|
+
function isValidGitRef(s) {
|
|
68
|
+
if (s.startsWith('-'))
|
|
69
|
+
return false;
|
|
70
|
+
return /^[A-Za-z0-9._/-]+$/.test(s);
|
|
71
|
+
}
|
|
72
|
+
export function parseEnterWorktreeArgs(raw) {
|
|
73
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
74
|
+
return `${ENTER_WORKTREE_INVALID_ARGS}: arguments must be a JSON object`;
|
|
75
|
+
}
|
|
76
|
+
const obj = raw;
|
|
77
|
+
const issues = [];
|
|
78
|
+
const taskId = obj['taskId'];
|
|
79
|
+
if (typeof taskId !== 'string') {
|
|
80
|
+
issues.push('taskId: must be a string');
|
|
81
|
+
}
|
|
82
|
+
else if (!TASK_ID_PATTERN.test(taskId)) {
|
|
83
|
+
issues.push(`taskId: must match ${TASK_ID_PATTERN}`);
|
|
84
|
+
}
|
|
85
|
+
let baseRef;
|
|
86
|
+
if (obj['baseRef'] !== undefined && obj['baseRef'] !== null) {
|
|
87
|
+
if (typeof obj['baseRef'] !== 'string') {
|
|
88
|
+
issues.push('baseRef: must be a string when present');
|
|
89
|
+
}
|
|
90
|
+
else if (obj['baseRef'].length === 0) {
|
|
91
|
+
issues.push('baseRef: must be non-empty when present');
|
|
92
|
+
}
|
|
93
|
+
else if (obj['baseRef'].length > 250) {
|
|
94
|
+
// Git imposes its own limits; we cut off well below them to keep
|
|
95
|
+
// error messages and audit lines bounded.
|
|
96
|
+
issues.push('baseRef: must be <= 250 chars when present');
|
|
97
|
+
}
|
|
98
|
+
else if (!isValidGitRef(obj['baseRef'])) {
|
|
99
|
+
// Permissive ref-chars regex that also rejects leading `-` so
|
|
100
|
+
// an operator-controlled `baseRef` cannot be reinterpreted as
|
|
101
|
+
// a git flag (e.g. `--detach`, `--force`). Combined with the
|
|
102
|
+
// `--` separator in argv this is belt-and-suspenders against
|
|
103
|
+
// arg-injection. Triple-review Claude P2,.
|
|
104
|
+
issues.push('baseRef: must match [A-Za-z0-9._/-] and not start with `-`');
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
baseRef = obj['baseRef'];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (issues.length > 0) {
|
|
111
|
+
return `${ENTER_WORKTREE_INVALID_ARGS}: ${issues.join('; ')}`;
|
|
112
|
+
}
|
|
113
|
+
const out = baseRef !== undefined
|
|
114
|
+
? { taskId: taskId, baseRef }
|
|
115
|
+
: { taskId: taskId };
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Compute the absolute on-disk path for a task's worktree. Public so
|
|
120
|
+
* `exit_worktree` and any future TUI surface can share the layout.
|
|
121
|
+
*/
|
|
122
|
+
export function worktreePathFor(workspaceRoot, taskId) {
|
|
123
|
+
return join(resolve(workspaceRoot), '.pugi', 'worktrees', taskId);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Compute the canonical worktree root (`<workspaceRoot>/.pugi/worktrees`).
|
|
127
|
+
* Used by `exit_worktree` for the containment check.
|
|
128
|
+
*/
|
|
129
|
+
export function worktreesRoot(workspaceRoot) {
|
|
130
|
+
return join(resolve(workspaceRoot), '.pugi', 'worktrees');
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Dispatch entry point. Validates input, ensures the parent directory
|
|
134
|
+
* exists, then spawns `git worktree add` with the strict argv form.
|
|
135
|
+
* Returns the structured result envelope as JSON, or a sentinel string
|
|
136
|
+
* on a recoverable failure.
|
|
137
|
+
*/
|
|
138
|
+
export function dispatchEnterWorktree(ctx, raw) {
|
|
139
|
+
const parsed = parseEnterWorktreeArgs(raw);
|
|
140
|
+
if (typeof parsed === 'string') {
|
|
141
|
+
return parsed;
|
|
142
|
+
}
|
|
143
|
+
const baseRef = parsed.baseRef ?? DEFAULT_BASE_REF;
|
|
144
|
+
const root = resolve(ctx.workspaceRoot);
|
|
145
|
+
const targetPath = worktreePathFor(root, parsed.taskId);
|
|
146
|
+
const subtreeRoot = worktreesRoot(root);
|
|
147
|
+
// Defense in depth: re-check that the resolved target stays inside
|
|
148
|
+
// `<workspaceRoot>/.pugi/worktrees/`. The regex on taskId already
|
|
149
|
+
// bans `..` and `/`, so a successful escape here would indicate a
|
|
150
|
+
// catastrophic node:path regression — we still treat it as a fatal
|
|
151
|
+
// refusal rather than trusting the upstream gate alone.
|
|
152
|
+
if (!isContainedIn(targetPath, subtreeRoot)) {
|
|
153
|
+
return `${ENTER_WORKTREE_PATH_ESCAPED}: ${targetPath} is not under ${subtreeRoot}`;
|
|
154
|
+
}
|
|
155
|
+
if (existsSync(targetPath)) {
|
|
156
|
+
return `${ENTER_WORKTREE_ALREADY_EXISTS}: ${targetPath}`;
|
|
157
|
+
}
|
|
158
|
+
// Ensure `.pugi/worktrees/` exists; git worktree add will not create
|
|
159
|
+
// the parent for us.
|
|
160
|
+
if (!existsSync(subtreeRoot)) {
|
|
161
|
+
mkdirSync(subtreeRoot, { recursive: true });
|
|
162
|
+
}
|
|
163
|
+
// Compose the branch name we hand to git. The convention pairs
|
|
164
|
+
// 1:1 with taskId so `exit_worktree` does not need a separate
|
|
165
|
+
// lookup. `-b <branch>` creates the branch as a side effect of the
|
|
166
|
+
// worktree-add; without it git would refuse a worktree that points
|
|
167
|
+
// to an existing branch already checked out elsewhere.
|
|
168
|
+
const branchName = `pugi/worktree/${parsed.taskId}`;
|
|
169
|
+
// `--` separator forces git to treat everything after it as strict
|
|
170
|
+
// positionals. Belt-and-suspenders against arg-injection via
|
|
171
|
+
// `baseRef` (the regex above already rejects leading `-`).
|
|
172
|
+
const argv = [
|
|
173
|
+
'worktree',
|
|
174
|
+
'add',
|
|
175
|
+
'-b',
|
|
176
|
+
branchName,
|
|
177
|
+
'--',
|
|
178
|
+
targetPath,
|
|
179
|
+
baseRef,
|
|
180
|
+
];
|
|
181
|
+
const runner = ctx.runGit ?? defaultGitRunner;
|
|
182
|
+
const result = runner(argv, root);
|
|
183
|
+
if (result.status !== 0) {
|
|
184
|
+
const detail = (result.stderr || result.stdout || `exit=${result.status}`).trim();
|
|
185
|
+
return `${ENTER_WORKTREE_GIT_FAILED}: ${detail}`;
|
|
186
|
+
}
|
|
187
|
+
const out = {
|
|
188
|
+
ok: true,
|
|
189
|
+
worktreePath: targetPath,
|
|
190
|
+
branchName,
|
|
191
|
+
baseRef,
|
|
192
|
+
};
|
|
193
|
+
return JSON.stringify(out);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Default spawnSync wrapper. `shell: false` is non-negotiable — every
|
|
197
|
+
* argv element is forwarded verbatim к the child without shell
|
|
198
|
+
* interpretation. The stdout/stderr captures are forced to utf-8 so the
|
|
199
|
+
* caller works with strings without manual decoding.
|
|
200
|
+
*/
|
|
201
|
+
function defaultGitRunner(argv, cwd) {
|
|
202
|
+
const result = spawnSync('git', [...argv], {
|
|
203
|
+
cwd,
|
|
204
|
+
shell: false,
|
|
205
|
+
encoding: 'utf8',
|
|
206
|
+
timeout: 30_000,
|
|
207
|
+
});
|
|
208
|
+
return {
|
|
209
|
+
status: result.status,
|
|
210
|
+
stdout: typeof result.stdout === 'string' ? result.stdout : '',
|
|
211
|
+
stderr: typeof result.stderr === 'string' ? result.stderr : '',
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Containment check. Resolves both paths and asserts the target lives
|
|
216
|
+
* under the root via a separator-aware prefix match. The separator
|
|
217
|
+
* append on the root protects against the `/foo/bar2` vs `/foo/bar`
|
|
218
|
+
* partial-prefix bug.
|
|
219
|
+
*/
|
|
220
|
+
function isContainedIn(target, root) {
|
|
221
|
+
const rResolved = resolve(root);
|
|
222
|
+
const tResolved = resolve(target);
|
|
223
|
+
if (tResolved === rResolved)
|
|
224
|
+
return false;
|
|
225
|
+
const rootWithSep = rResolved.endsWith(sep) ? rResolved : `${rResolved}${sep}`;
|
|
226
|
+
return tResolved.startsWith(rootWithSep);
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* JSON-Schema fragment the schema builder advertises to the model.
|
|
230
|
+
*/
|
|
231
|
+
export const enterWorktreeJsonSchema = {
|
|
232
|
+
type: 'object',
|
|
233
|
+
additionalProperties: false,
|
|
234
|
+
required: ['taskId'],
|
|
235
|
+
properties: {
|
|
236
|
+
taskId: {
|
|
237
|
+
type: 'string',
|
|
238
|
+
pattern: '^[a-z0-9][a-z0-9-]{0,63}$',
|
|
239
|
+
description: 'Slug used as the worktree basename. ' +
|
|
240
|
+
'Lowercase alphanumeric + hyphen, must start with letter/digit, 1..64 chars.',
|
|
241
|
+
},
|
|
242
|
+
baseRef: {
|
|
243
|
+
type: 'string',
|
|
244
|
+
minLength: 1,
|
|
245
|
+
maxLength: 250,
|
|
246
|
+
description: `Optional git base ref (branch / tag / SHA). Defaults to "${DEFAULT_BASE_REF}".`,
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
//# sourceMappingURL=enter-worktree.js.map
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* exit_worktree tool — opposite of `enter_worktree` (tool gap pack
|
|
3
|
+
*).
|
|
4
|
+
*
|
|
5
|
+
* Tears down a scratch git worktree at the supplied path. The path is
|
|
6
|
+
* STRICTLY validated к live under `<workspaceRoot>/.pugi/worktrees/`
|
|
7
|
+
* — anything outside that subtree refuses with a sentinel and zero
|
|
8
|
+
* filesystem mutation. This is a destructive primitive, so the
|
|
9
|
+
* containment check is the load-bearing safety boundary.
|
|
10
|
+
*
|
|
11
|
+
* Procedure:
|
|
12
|
+
* 1. Validate the argument shape.
|
|
13
|
+
* 2. Resolve the input path and assert it lives under
|
|
14
|
+
* `<workspaceRoot>/.pugi/worktrees/`. Refuse otherwise.
|
|
15
|
+
* 3. Run `git worktree remove --force <path>` so git's bookkeeping
|
|
16
|
+
* drops the worktree. `--force` is required because the model may
|
|
17
|
+
* have left dirty edits inside the scratch tree — that is the
|
|
18
|
+
* normal case for a refactor that the operator is about to merge
|
|
19
|
+
* via a different tool. Best-effort: a non-zero exit from git is
|
|
20
|
+
* logged in the return envelope but does NOT abort the disk
|
|
21
|
+
* cleanup. Git may have already cleaned the entry when the dir
|
|
22
|
+
* itself was removed manually.
|
|
23
|
+
* 4. `rmSync(path, { recursive: true, force: true })` for any
|
|
24
|
+
* leftover files.
|
|
25
|
+
*
|
|
26
|
+
* Wire shape:
|
|
27
|
+
* args: { worktreePath: string }
|
|
28
|
+
* return: { ok: true, removed: boolean, gitStatus: number|null,
|
|
29
|
+
* gitDetail?: string } serialised JSON.
|
|
30
|
+
*
|
|
31
|
+
* Idempotent: calling on a path that no longer exists returns
|
|
32
|
+
* `{ ok: true, removed: false }` with no error. This matches the
|
|
33
|
+
* semantics agent-progress / cleanup operations already follow elsewhere
|
|
34
|
+
* in the CLI.
|
|
35
|
+
*
|
|
36
|
+
* Brand voice: English only, no emoji, no banned words.
|
|
37
|
+
*/
|
|
38
|
+
import { spawnSync } from 'node:child_process';
|
|
39
|
+
import { existsSync, rmSync } from 'node:fs';
|
|
40
|
+
import { resolve, sep } from 'node:path';
|
|
41
|
+
import { worktreesRoot } from './enter-worktree.js';
|
|
42
|
+
/** Sentinels. Distinct prefixes so the model / dispatcher can
|
|
43
|
+
* pattern-match. */
|
|
44
|
+
export const EXIT_WORKTREE_INVALID_ARGS = 'EXIT_WORKTREE_INVALID_ARGS';
|
|
45
|
+
export const EXIT_WORKTREE_PATH_ESCAPED = 'EXIT_WORKTREE_PATH_ESCAPED';
|
|
46
|
+
/**
|
|
47
|
+
* Validate the raw arguments. Returns the typed value on success or a
|
|
48
|
+
* `EXIT_WORKTREE_INVALID_ARGS: ...` sentinel string.
|
|
49
|
+
*/
|
|
50
|
+
export function parseExitWorktreeArgs(raw) {
|
|
51
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
52
|
+
return `${EXIT_WORKTREE_INVALID_ARGS}: arguments must be a JSON object`;
|
|
53
|
+
}
|
|
54
|
+
const obj = raw;
|
|
55
|
+
const worktreePath = obj['worktreePath'];
|
|
56
|
+
if (typeof worktreePath !== 'string') {
|
|
57
|
+
return `${EXIT_WORKTREE_INVALID_ARGS}: worktreePath: must be a string`;
|
|
58
|
+
}
|
|
59
|
+
if (worktreePath.length === 0) {
|
|
60
|
+
return `${EXIT_WORKTREE_INVALID_ARGS}: worktreePath: must be non-empty`;
|
|
61
|
+
}
|
|
62
|
+
return { worktreePath };
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Dispatch entry point. Validates input, asserts the path lives under
|
|
66
|
+
* the allowed root, then runs `git worktree remove --force` followed
|
|
67
|
+
* by a filesystem removal for any leftovers. Returns the structured
|
|
68
|
+
* envelope as JSON.
|
|
69
|
+
*
|
|
70
|
+
* Returns sentinel strings on validation failures or a containment
|
|
71
|
+
* refusal — no throw — so the engine adapter surfaces a recoverable
|
|
72
|
+
* tool result and the model can adapt.
|
|
73
|
+
*/
|
|
74
|
+
export function dispatchExitWorktree(ctx, raw) {
|
|
75
|
+
const parsed = parseExitWorktreeArgs(raw);
|
|
76
|
+
if (typeof parsed === 'string') {
|
|
77
|
+
return parsed;
|
|
78
|
+
}
|
|
79
|
+
const root = resolve(ctx.workspaceRoot);
|
|
80
|
+
const subtreeRoot = worktreesRoot(root);
|
|
81
|
+
const target = resolve(parsed.worktreePath);
|
|
82
|
+
if (!isContainedIn(target, subtreeRoot)) {
|
|
83
|
+
return `${EXIT_WORKTREE_PATH_ESCAPED}: ${target} is not under ${subtreeRoot}`;
|
|
84
|
+
}
|
|
85
|
+
// git worktree remove. Best-effort. Non-zero exit is captured in the
|
|
86
|
+
// return envelope but does not block the rmTree step — a half-cleaned
|
|
87
|
+
// worktree (entry gone but dir survived) is a common operator-side
|
|
88
|
+
// mistake we want to recover from.
|
|
89
|
+
const runner = ctx.runGit ?? defaultGitRunner;
|
|
90
|
+
const gitResult = runner(['worktree', 'remove', '--force', target], root);
|
|
91
|
+
const existedBefore = existsSync(target);
|
|
92
|
+
const rmTree = ctx.rmTree ?? defaultRmTree;
|
|
93
|
+
if (existedBefore) {
|
|
94
|
+
rmTree(target);
|
|
95
|
+
}
|
|
96
|
+
const removed = existedBefore && !existsSync(target);
|
|
97
|
+
const envelope = gitResult.status === 0
|
|
98
|
+
? { ok: true, removed, gitStatus: gitResult.status }
|
|
99
|
+
: {
|
|
100
|
+
ok: true,
|
|
101
|
+
removed,
|
|
102
|
+
gitStatus: gitResult.status,
|
|
103
|
+
gitDetail: (gitResult.stderr || gitResult.stdout || '').trim(),
|
|
104
|
+
};
|
|
105
|
+
return JSON.stringify(envelope);
|
|
106
|
+
}
|
|
107
|
+
function defaultGitRunner(argv, cwd) {
|
|
108
|
+
const result = spawnSync('git', [...argv], {
|
|
109
|
+
cwd,
|
|
110
|
+
shell: false,
|
|
111
|
+
encoding: 'utf8',
|
|
112
|
+
timeout: 30_000,
|
|
113
|
+
});
|
|
114
|
+
return {
|
|
115
|
+
status: result.status,
|
|
116
|
+
stdout: typeof result.stdout === 'string' ? result.stdout : '',
|
|
117
|
+
stderr: typeof result.stderr === 'string' ? result.stderr : '',
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function defaultRmTree(path) {
|
|
121
|
+
rmSync(path, { recursive: true, force: true });
|
|
122
|
+
}
|
|
123
|
+
function isContainedIn(target, root) {
|
|
124
|
+
const rResolved = resolve(root);
|
|
125
|
+
const tResolved = resolve(target);
|
|
126
|
+
if (tResolved === rResolved)
|
|
127
|
+
return false;
|
|
128
|
+
const rootWithSep = rResolved.endsWith(sep) ? rResolved : `${rResolved}${sep}`;
|
|
129
|
+
return tResolved.startsWith(rootWithSep);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* JSON-Schema fragment the schema builder advertises to the model.
|
|
133
|
+
*/
|
|
134
|
+
export const exitWorktreeJsonSchema = {
|
|
135
|
+
type: 'object',
|
|
136
|
+
additionalProperties: false,
|
|
137
|
+
required: ['worktreePath'],
|
|
138
|
+
properties: {
|
|
139
|
+
worktreePath: {
|
|
140
|
+
type: 'string',
|
|
141
|
+
minLength: 1,
|
|
142
|
+
description: 'Absolute path to the worktree to tear down. Must live under ' +
|
|
143
|
+
'<workspaceRoot>/.pugi/worktrees/ — anything else refuses.',
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
//# sourceMappingURL=exit-worktree.js.map
|