@pugi/cli 0.1.0-beta.5 → 0.1.0-beta.50
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/THIRD_PARTY_NOTICES.md +40 -0
- package/assets/pugi-mascot.ansi +15 -25
- package/assets/pugi-prozr2-mascot.ansi +9 -0
- package/bin/run.js +33 -1
- package/dist/commands/jobs-watch.js +201 -0
- package/dist/commands/jobs.js +15 -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/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/auth/ensure-authenticated.js +129 -0
- package/dist/core/auth/env-provider.js +238 -0
- 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-classifier.js +400 -4
- package/dist/core/checkpoint/resumer.js +149 -0
- package/dist/core/checkpoint/rewinder.js +291 -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/diff-capture.js +112 -3
- package/dist/core/context/index.js +7 -0
- package/dist/core/context/markdown-traverse.js +255 -0
- package/dist/core/cost/rate-card.js +129 -0
- package/dist/core/cost/tracker.js +221 -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 +86 -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/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/dispatch.js +218 -2
- package/dist/core/edits/journal.js +199 -0
- package/dist/core/edits/layer-d-ast.js +557 -14
- package/dist/core/edits/verify-hook.js +273 -0
- package/dist/core/edits/worktree.js +322 -0
- package/dist/core/engine/anvil-client.js +115 -5
- package/dist/core/engine/budgets.js +98 -0
- package/dist/core/engine/context-prefix.js +155 -0
- package/dist/core/engine/intent.js +260 -0
- package/dist/core/engine/native-pugi.js +860 -211
- package/dist/core/engine/prompts.js +88 -2
- package/dist/core/engine/strip-internal-fields.js +124 -0
- package/dist/core/engine/tool-bridge.js +1045 -36
- 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/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/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/mcp/client.js +75 -6
- 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 +24 -2
- package/dist/core/mcp/server-tools.js +219 -0
- package/dist/core/mcp/server.js +397 -0
- package/dist/core/memory/dual-write.js +416 -0
- package/dist/core/memory/phase1-kinds.js +20 -0
- package/dist/core/memory-sync/queue.js +158 -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 +284 -2
- package/dist/core/permissions/auto-classifier.js +124 -0
- package/dist/core/permissions/circuit-breaker.js +83 -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/state.js +241 -0
- package/dist/core/permissions/tool-class.js +93 -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/pugi-md/context-injector.js +76 -0
- package/dist/core/pugi-md/walk-up.js +207 -0
- package/dist/core/release-notes/parser.js +241 -0
- package/dist/core/release-notes/state.js +116 -0
- package/dist/core/repl/history.js +11 -1
- package/dist/core/repl/model-pricing.js +135 -0
- package/dist/core/repl/session.js +1897 -37
- package/dist/core/repl/slash-commands.js +430 -15
- package/dist/core/repl/store/session-store.js +31 -2
- package/dist/core/repl/workspace-context.js +22 -0
- 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/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/session.js +92 -0
- package/dist/core/settings.js +80 -0
- 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/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/subagents/dispatcher-real.js +600 -0
- package/dist/core/subagents/dispatcher.js +113 -24
- package/dist/core/subagents/index.js +18 -5
- 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/transport/version-interceptor.js +166 -0
- package/dist/core/vim/keymap.js +288 -0
- package/dist/core/vim/state.js +92 -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 +3241 -343
- 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/cost.js +199 -0
- package/dist/runtime/commands/delegate.js +242 -11
- package/dist/runtime/commands/dispatch.js +126 -0
- package/dist/runtime/commands/doctor.js +412 -0
- package/dist/runtime/commands/feedback.js +184 -0
- package/dist/runtime/commands/hooks.js +184 -0
- package/dist/runtime/commands/lsp.js +368 -0
- package/dist/runtime/commands/mcp.js +879 -0
- package/dist/runtime/commands/memory.js +508 -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/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 +17 -2
- package/dist/runtime/commands/rewind.js +333 -0
- package/dist/runtime/commands/sessions.js +163 -0
- package/dist/runtime/commands/share.js +316 -0
- 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 +32 -0
- 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/version.js +65 -0
- package/dist/tools/agent-tool.js +229 -0
- package/dist/tools/apply-patch.js +556 -0
- package/dist/tools/ask-user-question.js +213 -0
- package/dist/tools/ask-user.js +115 -0
- package/dist/tools/bash.js +203 -4
- package/dist/tools/file-tools.js +85 -14
- 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 +51 -0
- package/dist/tools/skill-tool.js +96 -0
- package/dist/tools/tasks.js +208 -0
- package/dist/tools/todo-write.js +184 -0
- package/dist/tools/web-fetch.js +147 -2
- package/dist/tools/web-search.js +458 -0
- package/dist/tui/agent-progress-card.js +111 -0
- package/dist/tui/agent-tree.js +10 -0
- package/dist/tui/ask-modal.js +2 -2
- package/dist/tui/ask-user-question-prompt.js +192 -0
- package/dist/tui/compact-banner.js +81 -0
- package/dist/tui/conversation-pane.js +82 -8
- package/dist/tui/cost-table.js +111 -0
- package/dist/tui/doctor-table.js +46 -0
- package/dist/tui/feedback-prompt.js +156 -0
- package/dist/tui/input-box.js +218 -3
- package/dist/tui/markdown-render.js +4 -4
- 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 +313 -35
- package/dist/tui/repl-splash-art.js +1 -1
- package/dist/tui/repl-splash-mascot.js +32 -8
- package/dist/tui/repl-splash.js +2 -2
- package/dist/tui/repl.js +85 -5
- package/dist/tui/splash.js +1 -1
- package/dist/tui/status-bar.js +94 -16
- 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 +52 -3
- 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/docs/examples/codegraph.mcp.json +10 -0
- package/package.json +12 -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,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AskUserQuestion structured tool — leak L5 (research memo §2.5).
|
|
3
|
+
*
|
|
4
|
+
* Mirrors openclaude's `src/tools/AskUserQuestionTool/prompt.ts`
|
|
5
|
+
* pattern: clarifying questions go through a structured multi-choice
|
|
6
|
+
* tool, NOT free-text prose. The model dispatches the tool with a
|
|
7
|
+
* `question` + a `header` chip + 2-4 `options` (each with `label` +
|
|
8
|
+
* `description`). The UI renders the modal, auto-appends an "Other"
|
|
9
|
+
* fallback for custom text, and surfaces the operator's pick back to
|
|
10
|
+
* the model as a tool_result frame.
|
|
11
|
+
*
|
|
12
|
+
* Why P0 leverage: the structured form forecloses Pugi's recurring
|
|
13
|
+
* "agent rambles instead of dispatching" failure mode at the schema
|
|
14
|
+
* level. When the model is uncertain, the cheapest legal output is
|
|
15
|
+
* `ask_user_question` — not a prose menu, not a fake "Шипану через
|
|
16
|
+
* 8 минут" dispatch promise.
|
|
17
|
+
*
|
|
18
|
+
* Relationship to ask-user.ts (β1 T2):
|
|
19
|
+
* - ask-user.ts is the LEGACY string-array form (`options: string[]`).
|
|
20
|
+
* Kept for back-compat; the existing prompt envelope `<pugi-ask>`
|
|
21
|
+
* and the persona prompts still emit that grammar.
|
|
22
|
+
* - ask-user-question.ts is the STRUCTURED form layered on top. It
|
|
23
|
+
* normalises a {label, description} option into the legacy string
|
|
24
|
+
* before delegating to `askUser`, so the Ink modal does not need
|
|
25
|
+
* two render paths and the abort/timeout race logic is shared.
|
|
26
|
+
*
|
|
27
|
+
* Hard rules (enforced by Zod):
|
|
28
|
+
* - question: 5-500 chars, must end with "?". Plain English.
|
|
29
|
+
* - header: 2-12 chars (short chip label, e.g. "Auth method").
|
|
30
|
+
* - options: 2-4 strict (no more, no less). Mutually exclusive.
|
|
31
|
+
* UI auto-adds "Other" — the model NEVER emits it.
|
|
32
|
+
* - multiSelect: default false.
|
|
33
|
+
*/
|
|
34
|
+
import { z } from 'zod';
|
|
35
|
+
import { askUser } from './ask-user.js';
|
|
36
|
+
/** Cap matches the Ink modal layout: 12 chars fits the header chip. */
|
|
37
|
+
export const ASK_USER_QUESTION_HEADER_MIN = 2;
|
|
38
|
+
export const ASK_USER_QUESTION_HEADER_MAX = 12;
|
|
39
|
+
/** Question must be a real question (ends with ?). 5-500 chars. */
|
|
40
|
+
export const ASK_USER_QUESTION_MIN = 5;
|
|
41
|
+
export const ASK_USER_QUESTION_MAX = 500;
|
|
42
|
+
/** Each option label: 2-40 chars (1-5 words). */
|
|
43
|
+
export const ASK_USER_QUESTION_OPTION_LABEL_MIN = 2;
|
|
44
|
+
export const ASK_USER_QUESTION_OPTION_LABEL_MAX = 40;
|
|
45
|
+
/** Each option description: 10-200 chars (one short sentence). */
|
|
46
|
+
export const ASK_USER_QUESTION_OPTION_DESC_MIN = 10;
|
|
47
|
+
export const ASK_USER_QUESTION_OPTION_DESC_MAX = 200;
|
|
48
|
+
/** Option count: 2-4 strict. UI adds "Other" automatically. */
|
|
49
|
+
export const ASK_USER_QUESTION_OPTIONS_MIN = 2;
|
|
50
|
+
export const ASK_USER_QUESTION_OPTIONS_MAX = 4;
|
|
51
|
+
/**
|
|
52
|
+
* Structured option. `label` is the display text; `description` is the
|
|
53
|
+
* implication line shown dim below it. Both are required — the model
|
|
54
|
+
* cannot ship a label-only option (forces it to think about why each
|
|
55
|
+
* choice exists).
|
|
56
|
+
*/
|
|
57
|
+
export const askUserQuestionOptionSchema = z.strictObject({
|
|
58
|
+
label: z
|
|
59
|
+
.string()
|
|
60
|
+
.min(ASK_USER_QUESTION_OPTION_LABEL_MIN)
|
|
61
|
+
.max(ASK_USER_QUESTION_OPTION_LABEL_MAX)
|
|
62
|
+
.describe('Display text. Concise (1-5 words).'),
|
|
63
|
+
description: z
|
|
64
|
+
.string()
|
|
65
|
+
.min(ASK_USER_QUESTION_OPTION_DESC_MIN)
|
|
66
|
+
.max(ASK_USER_QUESTION_OPTION_DESC_MAX)
|
|
67
|
+
.describe('What this option means / implications.'),
|
|
68
|
+
});
|
|
69
|
+
export const askUserQuestionSchema = z.strictObject({
|
|
70
|
+
question: z
|
|
71
|
+
.string()
|
|
72
|
+
.min(ASK_USER_QUESTION_MIN)
|
|
73
|
+
.max(ASK_USER_QUESTION_MAX)
|
|
74
|
+
.refine((q) => q.trim().endsWith('?'), {
|
|
75
|
+
message: 'question must end with "?"',
|
|
76
|
+
})
|
|
77
|
+
.describe('The complete question. Must end with "?". Plain English, no jargon.'),
|
|
78
|
+
header: z
|
|
79
|
+
.string()
|
|
80
|
+
.min(ASK_USER_QUESTION_HEADER_MIN)
|
|
81
|
+
.max(ASK_USER_QUESTION_HEADER_MAX)
|
|
82
|
+
.describe('Short chip label (max 12 chars). E.g. "Auth method".'),
|
|
83
|
+
options: z
|
|
84
|
+
.array(askUserQuestionOptionSchema)
|
|
85
|
+
.min(ASK_USER_QUESTION_OPTIONS_MIN)
|
|
86
|
+
.max(ASK_USER_QUESTION_OPTIONS_MAX)
|
|
87
|
+
.describe('2-4 mutually-exclusive options. NEVER add "Other" — UI auto-adds.'),
|
|
88
|
+
multiSelect: z
|
|
89
|
+
.boolean()
|
|
90
|
+
.optional()
|
|
91
|
+
.default(false)
|
|
92
|
+
.describe('Allow multiple selections. Default false.'),
|
|
93
|
+
});
|
|
94
|
+
/**
|
|
95
|
+
* Dispatch the structured tool: validate args via Zod, then route
|
|
96
|
+
* through the shared `askUser` primitive so abort/timeout/non-TTY
|
|
97
|
+
* envelope behaviour is identical to the legacy form.
|
|
98
|
+
*
|
|
99
|
+
* The bridge surface is the same `AskUserBridge` signature — the
|
|
100
|
+
* structured form just gives the Ink modal richer metadata to render
|
|
101
|
+
* (header chip + per-option description). The bridge sees the legacy
|
|
102
|
+
* `{question, options: string[]}` shape because all production bridges
|
|
103
|
+
* (Ink modal + non-TTY envelope emitter) already consume that shape.
|
|
104
|
+
* Per-option descriptions and the header chip are surfaced separately
|
|
105
|
+
* via `enrich` — the modal layer reads them off the dispatched payload
|
|
106
|
+
* stash, NOT off the bridge input, so structured callers can layer on
|
|
107
|
+
* top of the legacy interface without touching the modal contract.
|
|
108
|
+
*
|
|
109
|
+
* Return contract:
|
|
110
|
+
* - Interactive + bridge present + operator picks N options →
|
|
111
|
+
* `[ask_user_question:answered] <labels joined by ", ">`.
|
|
112
|
+
* - Interactive + bridge present + operator cancels →
|
|
113
|
+
* `[ask_user_question:cancelled]`.
|
|
114
|
+
* - Interactive + bridge present + timeout →
|
|
115
|
+
* `[ask_user_question:timeout]`.
|
|
116
|
+
* - Non-TTY or no bridge → `[user_input_required]<json>[/...]`
|
|
117
|
+
* envelope identical to the legacy form. Includes `header` +
|
|
118
|
+
* structured options so a scripted caller can parse the full shape.
|
|
119
|
+
*/
|
|
120
|
+
export async function dispatchAskUserQuestion(ctx, rawArgs) {
|
|
121
|
+
const parsed = askUserQuestionSchema.parse(rawArgs);
|
|
122
|
+
// Schema-level guard against the "Other" leak: the prompt rules tell
|
|
123
|
+
// the model NEVER to include "Other" in `options`, but we still reject
|
|
124
|
+
// it defensively in case a future model misreads the spec. The Ink
|
|
125
|
+
// modal auto-appends "Other" itself; a model-supplied duplicate would
|
|
126
|
+
// render two "Other" rows.
|
|
127
|
+
for (const opt of parsed.options) {
|
|
128
|
+
const trimmed = opt.label.trim().toLowerCase();
|
|
129
|
+
if (trimmed === 'other' || trimmed === 'другое') {
|
|
130
|
+
throw new Error('ask_user_question: do NOT include "Other" in options — UI auto-adds it');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const legacyOptions = parsed.options.map((opt) => opt.label);
|
|
134
|
+
const result = await askUser(ctx, {
|
|
135
|
+
question: parsed.question,
|
|
136
|
+
options: legacyOptions,
|
|
137
|
+
multiSelect: parsed.multiSelect ?? false,
|
|
138
|
+
});
|
|
139
|
+
if (result.answers && result.answers.length > 0) {
|
|
140
|
+
return {
|
|
141
|
+
answers: result.answers,
|
|
142
|
+
envelope: `[ask_user_question:answered] ${result.answers.join(', ')}`,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
// Non-TTY / cancelled / timeout. Re-wrap the envelope so callers can
|
|
146
|
+
// grep for the structured tool name even when the underlying primitive
|
|
147
|
+
// surfaced its legacy `[user_input_required]` envelope.
|
|
148
|
+
if (result.envelope.includes('"reason":"timeout"')) {
|
|
149
|
+
return { envelope: '[ask_user_question:timeout]' };
|
|
150
|
+
}
|
|
151
|
+
if (result.envelope.includes('"reason":"cancelled"')) {
|
|
152
|
+
return { envelope: '[ask_user_question:cancelled]' };
|
|
153
|
+
}
|
|
154
|
+
// Default to the legacy envelope verbatim — it is still
|
|
155
|
+
// grep-friendly and includes the structured payload above.
|
|
156
|
+
return { envelope: result.envelope };
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* JSON-Schema fragment surfaced to the model via the tool-bridge
|
|
160
|
+
* `parameters` field. Mirrors the Zod schema 1:1 — kept hand-written
|
|
161
|
+
* because the runtime engine wires OpenAI-compatible JSON Schema and
|
|
162
|
+
* the Zod-to-JSON-Schema converter pulls in a transitive dep we have
|
|
163
|
+
* not greenlit. If the Zod schema above changes, mirror the change here.
|
|
164
|
+
*/
|
|
165
|
+
export const askUserQuestionJsonSchema = {
|
|
166
|
+
type: 'object',
|
|
167
|
+
additionalProperties: false,
|
|
168
|
+
required: ['question', 'header', 'options'],
|
|
169
|
+
properties: {
|
|
170
|
+
question: {
|
|
171
|
+
type: 'string',
|
|
172
|
+
minLength: ASK_USER_QUESTION_MIN,
|
|
173
|
+
maxLength: ASK_USER_QUESTION_MAX,
|
|
174
|
+
description: 'The complete question. Must end with "?". Plain English, no jargon.',
|
|
175
|
+
},
|
|
176
|
+
header: {
|
|
177
|
+
type: 'string',
|
|
178
|
+
minLength: ASK_USER_QUESTION_HEADER_MIN,
|
|
179
|
+
maxLength: ASK_USER_QUESTION_HEADER_MAX,
|
|
180
|
+
description: 'Short chip label (max 12 chars). E.g. "Auth method".',
|
|
181
|
+
},
|
|
182
|
+
options: {
|
|
183
|
+
type: 'array',
|
|
184
|
+
minItems: ASK_USER_QUESTION_OPTIONS_MIN,
|
|
185
|
+
maxItems: ASK_USER_QUESTION_OPTIONS_MAX,
|
|
186
|
+
items: {
|
|
187
|
+
type: 'object',
|
|
188
|
+
additionalProperties: false,
|
|
189
|
+
required: ['label', 'description'],
|
|
190
|
+
properties: {
|
|
191
|
+
label: {
|
|
192
|
+
type: 'string',
|
|
193
|
+
minLength: ASK_USER_QUESTION_OPTION_LABEL_MIN,
|
|
194
|
+
maxLength: ASK_USER_QUESTION_OPTION_LABEL_MAX,
|
|
195
|
+
description: 'Display text. Concise (1-5 words).',
|
|
196
|
+
},
|
|
197
|
+
description: {
|
|
198
|
+
type: 'string',
|
|
199
|
+
minLength: ASK_USER_QUESTION_OPTION_DESC_MIN,
|
|
200
|
+
maxLength: ASK_USER_QUESTION_OPTION_DESC_MAX,
|
|
201
|
+
description: 'What this option means / implications.',
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
description: '2-4 mutually-exclusive options. NEVER add "Other" — UI auto-adds.',
|
|
206
|
+
},
|
|
207
|
+
multiSelect: {
|
|
208
|
+
type: 'boolean',
|
|
209
|
+
description: 'Allow multiple selections. Default false.',
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
//# sourceMappingURL=ask-user-question.js.map
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
export const ASK_USER_DEFAULT_TIMEOUT_MS = 5 * 60 * 1_000;
|
|
2
|
+
/**
|
|
3
|
+
* Schema cap: keep the option count tight so the modal stays scannable.
|
|
4
|
+
* Mirrors `ASK_MAX_OPTIONS` in `core/repl/ask.ts` (4).
|
|
5
|
+
*/
|
|
6
|
+
export const ASK_USER_MAX_OPTIONS = 4;
|
|
7
|
+
export const ASK_USER_MAX_QUESTION_LEN = 1_000;
|
|
8
|
+
export const ASK_USER_MAX_OPTION_LEN = 200;
|
|
9
|
+
export async function askUser(ctx, input) {
|
|
10
|
+
validate(input);
|
|
11
|
+
if (ctx.interactive && ctx.bridge) {
|
|
12
|
+
// β1a r1 (2026-05-26): wrap the bridge in an abort-aware race so a
|
|
13
|
+
// pending modal cannot block the engine loop forever. Two signals
|
|
14
|
+
// can interrupt:
|
|
15
|
+
// 1. `ctx.signal` — the operator cancelled the parent task via
|
|
16
|
+
// Ctrl-C; the engine forwards the loop's AbortSignal here.
|
|
17
|
+
// 2. `ctx.timeoutMs` (default 5 minutes) — operator walked away;
|
|
18
|
+
// the modal stays renderable but the tool surface returns the
|
|
19
|
+
// `cancelled` envelope so the model can make progress.
|
|
20
|
+
// The bridge receives the same `signal` so an Ink-based modal can
|
|
21
|
+
// tear down its render loop and free its keyboard handlers on
|
|
22
|
+
// abort. Bridges that ignore the signal still get pre-empted by
|
|
23
|
+
// the race — they just leak a render until the next operator
|
|
24
|
+
// keystroke.
|
|
25
|
+
const timeoutMs = ctx.timeoutMs ?? ASK_USER_DEFAULT_TIMEOUT_MS;
|
|
26
|
+
// Pre-flight: short-circuit when the caller's signal is already
|
|
27
|
+
// aborted. Avoids constructing a bridge promise that races against
|
|
28
|
+
// an already-resolved abort sentinel — the race ordering is
|
|
29
|
+
// unspecified for promises that have all settled by the time
|
|
30
|
+
// Promise.race is called, which would non-deterministically let
|
|
31
|
+
// the bridge's answer leak through after an explicit cancel.
|
|
32
|
+
if (ctx.signal?.aborted) {
|
|
33
|
+
return { envelope: formatEnvelope(input, 'cancelled') };
|
|
34
|
+
}
|
|
35
|
+
const controller = new AbortController();
|
|
36
|
+
if (ctx.signal) {
|
|
37
|
+
ctx.signal.addEventListener('abort', () => controller.abort(), { once: true });
|
|
38
|
+
}
|
|
39
|
+
let timeoutHandle;
|
|
40
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
41
|
+
timeoutHandle = setTimeout(() => resolve('timeout'), timeoutMs);
|
|
42
|
+
});
|
|
43
|
+
const abortPromise = new Promise((resolve) => {
|
|
44
|
+
controller.signal.addEventListener('abort', () => resolve('aborted'), { once: true });
|
|
45
|
+
});
|
|
46
|
+
let picked;
|
|
47
|
+
try {
|
|
48
|
+
picked = await Promise.race([
|
|
49
|
+
ctx.bridge(input, { signal: controller.signal }),
|
|
50
|
+
timeoutPromise,
|
|
51
|
+
abortPromise,
|
|
52
|
+
]);
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
if (timeoutHandle)
|
|
56
|
+
clearTimeout(timeoutHandle);
|
|
57
|
+
}
|
|
58
|
+
if (picked === 'timeout') {
|
|
59
|
+
controller.abort();
|
|
60
|
+
return { envelope: formatEnvelope(input, 'timeout') };
|
|
61
|
+
}
|
|
62
|
+
if (picked === 'aborted') {
|
|
63
|
+
return { envelope: formatEnvelope(input, 'cancelled') };
|
|
64
|
+
}
|
|
65
|
+
if (!Array.isArray(picked) || picked.length === 0) {
|
|
66
|
+
// Operator declined / closed the modal — surface a structured
|
|
67
|
+
// "no answer" envelope so the model can decide whether to retry.
|
|
68
|
+
const envelope = formatEnvelope(input, 'cancelled');
|
|
69
|
+
return { envelope };
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
answers: picked,
|
|
73
|
+
envelope: formatAnswer(picked),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
// Non-TTY or no bridge — surface the envelope. Caller parses it and
|
|
77
|
+
// either pipes an answer back on a follow-up turn or aborts.
|
|
78
|
+
const envelope = formatEnvelope(input, 'no_tty');
|
|
79
|
+
return { envelope };
|
|
80
|
+
}
|
|
81
|
+
function validate(input) {
|
|
82
|
+
const question = input.question?.trim();
|
|
83
|
+
if (!question)
|
|
84
|
+
throw new Error('ask_user: question is required');
|
|
85
|
+
if (question.length > ASK_USER_MAX_QUESTION_LEN) {
|
|
86
|
+
throw new Error(`ask_user: question exceeds ${ASK_USER_MAX_QUESTION_LEN} char cap`);
|
|
87
|
+
}
|
|
88
|
+
if (!Array.isArray(input.options) || input.options.length < 2) {
|
|
89
|
+
throw new Error('ask_user: at least 2 options required');
|
|
90
|
+
}
|
|
91
|
+
if (input.options.length > ASK_USER_MAX_OPTIONS) {
|
|
92
|
+
throw new Error(`ask_user: at most ${ASK_USER_MAX_OPTIONS} options allowed`);
|
|
93
|
+
}
|
|
94
|
+
for (const opt of input.options) {
|
|
95
|
+
if (typeof opt !== 'string' || !opt.trim()) {
|
|
96
|
+
throw new Error('ask_user: every option must be a non-empty string');
|
|
97
|
+
}
|
|
98
|
+
if (opt.length > ASK_USER_MAX_OPTION_LEN) {
|
|
99
|
+
throw new Error(`ask_user: option exceeds ${ASK_USER_MAX_OPTION_LEN} char cap`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function formatEnvelope(input, reason) {
|
|
104
|
+
const payload = {
|
|
105
|
+
question: input.question,
|
|
106
|
+
options: input.options,
|
|
107
|
+
multiSelect: input.multiSelect === true,
|
|
108
|
+
reason,
|
|
109
|
+
};
|
|
110
|
+
return `[user_input_required]${JSON.stringify(payload)}[/user_input_required]`;
|
|
111
|
+
}
|
|
112
|
+
function formatAnswer(answers) {
|
|
113
|
+
return answers.join(', ');
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=ask-user.js.map
|
package/dist/tools/bash.js
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* drops Windows shell support for M1.
|
|
28
28
|
*/
|
|
29
29
|
import { randomUUID } from 'node:crypto';
|
|
30
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync, } from 'node:fs';
|
|
30
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync, } from 'node:fs';
|
|
31
31
|
import { homedir } from 'node:os';
|
|
32
32
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
33
33
|
import { spawn, spawnSync } from 'node:child_process';
|
|
@@ -60,6 +60,32 @@ export async function bashTool(input, ctx) {
|
|
|
60
60
|
const additionalDirectories = ctx.additionalDirectories ?? [];
|
|
61
61
|
const source = ctx.source ?? 'agent';
|
|
62
62
|
const toolCallId = recordToolCall(ctx.session, 'bash', cmd);
|
|
63
|
+
// Cwd carry-over decision (also re-checked post-run).
|
|
64
|
+
const startCwd = resolveStartCwd(input.cwd ?? ctx.lastBashCwd, ctx.root, additionalDirectories);
|
|
65
|
+
// Workspace-git-boundary guard (CEO P0 #51, 2026-05-29).
|
|
66
|
+
// Runs BEFORE the permission gate so the boundary escape message is
|
|
67
|
+
// the one the operator/engine sees, regardless of permission policy.
|
|
68
|
+
// The leak is structural (git silently writes to an ancestor .git
|
|
69
|
+
// when the workspace lacks one), not a policy violation, so the
|
|
70
|
+
// diagnostic must surface even when the permission gate would
|
|
71
|
+
// otherwise have asked or auto-allowed.
|
|
72
|
+
const boundaryBlock = enforceGitBoundary(cmd, startCwd, ctx.root);
|
|
73
|
+
if (boundaryBlock !== null) {
|
|
74
|
+
emitEvent(ctx.session, 'bash.git_boundary_escape', {
|
|
75
|
+
cmd,
|
|
76
|
+
workspaceRoot: ctx.root,
|
|
77
|
+
resolvedToplevel: boundaryBlock.resolvedToplevel ?? null,
|
|
78
|
+
});
|
|
79
|
+
recordToolResult(ctx.session, toolCallId, 'error', boundaryBlock.reason);
|
|
80
|
+
return {
|
|
81
|
+
stdout: '',
|
|
82
|
+
stderr: boundaryBlock.reason,
|
|
83
|
+
exitCode: 126,
|
|
84
|
+
nextCwd: ctx.lastBashCwd ?? ctx.root,
|
|
85
|
+
truncated: false,
|
|
86
|
+
timedOut: false,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
63
89
|
// Permission gate via the new class-aware engine.
|
|
64
90
|
const decision = evaluateBashPermission(cmd, ctx.settings.permissions.mode, {
|
|
65
91
|
workspaceRoot: ctx.root,
|
|
@@ -78,8 +104,6 @@ export async function bashTool(input, ctx) {
|
|
|
78
104
|
timedOut: false,
|
|
79
105
|
};
|
|
80
106
|
}
|
|
81
|
-
// Cwd carry-over decision (also re-checked post-run).
|
|
82
|
-
const startCwd = resolveStartCwd(input.cwd ?? ctx.lastBashCwd, ctx.root, additionalDirectories);
|
|
83
107
|
// Background job branch.
|
|
84
108
|
if (input.background === true) {
|
|
85
109
|
return runBackground({ cmd, ctx, toolCallId, startCwd, additionalDirectories });
|
|
@@ -565,6 +589,160 @@ function readRegistryEntriesSync() {
|
|
|
565
589
|
return [];
|
|
566
590
|
}
|
|
567
591
|
}
|
|
592
|
+
/**
|
|
593
|
+
* Workspace-git-boundary guard (CEO P0 #51, 2026-05-29).
|
|
594
|
+
*
|
|
595
|
+
* Background: CEO live REPL surfaced a scenario where the customer
|
|
596
|
+
* workspace dir was created INSIDE another git repository (the Pugi
|
|
597
|
+
* monorepo itself). The model emitted `git init && git add . && git
|
|
598
|
+
* commit -m ...` against that workspace. The workspace had no `.git`
|
|
599
|
+
* of its own so git silently walked up to the outer repo's `.git` and
|
|
600
|
+
* committed the customer's files directly to the monorepo's main
|
|
601
|
+
* branch. Had the outer remote been FF-permissive, those files would
|
|
602
|
+
* have pushed to production. This is a customer-of-customer leak.
|
|
603
|
+
*
|
|
604
|
+
* The guard: when the agent emits a mutating git op (add / commit /
|
|
605
|
+
* push / rebase / reset / checkout) and the effective git toplevel
|
|
606
|
+
* (`git -C $cwd rev-parse --show-toplevel`) sits OUTSIDE the workspace
|
|
607
|
+
* root, block the command. The model is steered (via the persona
|
|
608
|
+
* prompt) to run `git init` first; the guard is the defensive net so
|
|
609
|
+
* a careless model emission cannot cross the boundary.
|
|
610
|
+
*
|
|
611
|
+
* Exported so the spec can exercise the predicate in isolation without
|
|
612
|
+
* having to drive the whole bash tool.
|
|
613
|
+
*/
|
|
614
|
+
export const GIT_BOUNDARY_BLOCK_PREFIX = 'git boundary escape:';
|
|
615
|
+
/**
|
|
616
|
+
* Subcommands we treat as definitely mutating for the boundary check.
|
|
617
|
+
* We intentionally OMIT subcommands that have common read-only modes
|
|
618
|
+
* (`branch --list`, `tag --list`, `stash list`, `remote -v`) to keep
|
|
619
|
+
* the guard precise. The CEO P0 #51 leak vector is files written to
|
|
620
|
+
* an ancestor repo's working tree / refs, which the included set
|
|
621
|
+
* fully covers. The omitted subcommands can still create refs in the
|
|
622
|
+
* outer .git, but they do not move customer files into the outer
|
|
623
|
+
* repo's commit graph, so the leak severity is lower and the
|
|
624
|
+
* ergonomic cost of false positives on `--list` flags is higher.
|
|
625
|
+
*/
|
|
626
|
+
const MUTATING_GIT_SUBCOMMANDS = new Set([
|
|
627
|
+
'add',
|
|
628
|
+
'commit',
|
|
629
|
+
'push',
|
|
630
|
+
'rebase',
|
|
631
|
+
'reset',
|
|
632
|
+
'checkout',
|
|
633
|
+
'merge',
|
|
634
|
+
'restore',
|
|
635
|
+
'switch',
|
|
636
|
+
'cherry-pick',
|
|
637
|
+
'am',
|
|
638
|
+
'apply',
|
|
639
|
+
'clean',
|
|
640
|
+
'rm',
|
|
641
|
+
'mv',
|
|
642
|
+
]);
|
|
643
|
+
/**
|
|
644
|
+
* Inspect a shell command for mutating git operations. Returns the
|
|
645
|
+
* first matching subcommand (e.g. 'commit') or null when none of the
|
|
646
|
+
* components are mutating git ops.
|
|
647
|
+
*
|
|
648
|
+
* We split on `&&`, `||`, `;`, `|` so a compound like
|
|
649
|
+
* `mkdir foo && cd foo && git add .` is correctly flagged on the
|
|
650
|
+
* trailing git component.
|
|
651
|
+
*/
|
|
652
|
+
export function detectMutatingGitOp(cmd) {
|
|
653
|
+
const components = cmd.split(/\s*(?:&&|\|\||;|\|)\s*/);
|
|
654
|
+
for (const raw of components) {
|
|
655
|
+
const component = raw.trim();
|
|
656
|
+
if (component === '')
|
|
657
|
+
continue;
|
|
658
|
+
// Strip leading `sudo` wrapper which would otherwise hide the verb.
|
|
659
|
+
const stripped = component.replace(/^sudo\s+/, '');
|
|
660
|
+
// Match `git [<global-flags>] <subcommand> ...`. Global flags we
|
|
661
|
+
// tolerate:
|
|
662
|
+
// - long flag: `--no-pager`, `--git-dir=.git`
|
|
663
|
+
// - short flag with attached value: `-C <path>`, `-c <k=v>`
|
|
664
|
+
// - bare short flag: `-P`
|
|
665
|
+
// Anything weirder falls through and the predicate returns null,
|
|
666
|
+
// which means the guard does not fire on that component — safer
|
|
667
|
+
// to err open here because the destructive classifier and the
|
|
668
|
+
// outer permission gate are independent defences.
|
|
669
|
+
const match = stripped.match(/^git(?:\s+(?:--[A-Za-z][A-Za-z0-9-]*(?:=\S+)?|-[CcP](?:\s+\S+)?|-[A-Za-z]+))*\s+([a-z][a-z0-9-]*)\b/);
|
|
670
|
+
if (!match)
|
|
671
|
+
continue;
|
|
672
|
+
const subcommand = match[1];
|
|
673
|
+
if (subcommand && MUTATING_GIT_SUBCOMMANDS.has(subcommand)) {
|
|
674
|
+
return subcommand;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Resolve the workspace's effective git boundary. Returns:
|
|
681
|
+
* - the absolute path of the .git toplevel that owns `cwd`
|
|
682
|
+
* - null when no .git ancestor exists at all (standalone, no repo)
|
|
683
|
+
*
|
|
684
|
+
* Pure filesystem walk so the guard does not depend on git being on
|
|
685
|
+
* PATH. We look for either a `.git` directory or a `.git` file (the
|
|
686
|
+
* worktree case where `.git` is a pointer file).
|
|
687
|
+
*/
|
|
688
|
+
export function resolveGitToplevel(cwd) {
|
|
689
|
+
let dir = cwd;
|
|
690
|
+
while (true) {
|
|
691
|
+
const dotGit = join(dir, '.git');
|
|
692
|
+
if (existsSync(dotGit))
|
|
693
|
+
return dir;
|
|
694
|
+
const parent = resolve(dir, '..');
|
|
695
|
+
if (parent === dir)
|
|
696
|
+
return null;
|
|
697
|
+
dir = parent;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* The actual guard. Returns null when the command is allowed; returns
|
|
702
|
+
* a block descriptor when it should be denied. The block message uses
|
|
703
|
+
* the literal prefix `git boundary escape:` so callers (and the spec)
|
|
704
|
+
* can pattern-match.
|
|
705
|
+
*/
|
|
706
|
+
export function enforceGitBoundary(cmd, startCwd, workspaceRoot) {
|
|
707
|
+
const subcommand = detectMutatingGitOp(cmd);
|
|
708
|
+
if (subcommand === null)
|
|
709
|
+
return null;
|
|
710
|
+
// Resolve symlinks on both sides so a /var → /private/var macOS
|
|
711
|
+
// realpath divergence does not produce a false escape.
|
|
712
|
+
const root = safeRealpath(workspaceRoot);
|
|
713
|
+
const toplevel = resolveGitToplevel(safeRealpath(startCwd));
|
|
714
|
+
const resolvedToplevel = toplevel === null ? null : safeRealpath(toplevel);
|
|
715
|
+
if (resolvedToplevel === root)
|
|
716
|
+
return null;
|
|
717
|
+
// Either no .git anywhere (standalone) OR the .git that wins is an
|
|
718
|
+
// ancestor — both are escape scenarios. Operator must run `git init`
|
|
719
|
+
// explicitly inside the workspace.
|
|
720
|
+
if (resolvedToplevel === null) {
|
|
721
|
+
return {
|
|
722
|
+
subcommand,
|
|
723
|
+
resolvedToplevel: null,
|
|
724
|
+
reason: `${GIT_BOUNDARY_BLOCK_PREFIX} workspace root '${workspaceRoot}' has no .git ` +
|
|
725
|
+
`and no ancestor repository exists. Run \`git init\` in the workspace first ` +
|
|
726
|
+
`before \`git ${subcommand}\`.`,
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
return {
|
|
730
|
+
subcommand,
|
|
731
|
+
resolvedToplevel,
|
|
732
|
+
reason: `${GIT_BOUNDARY_BLOCK_PREFIX} workspace root '${workspaceRoot}' has no .git; ` +
|
|
733
|
+
`outer toplevel is '${resolvedToplevel}'. Run \`git init\` in the workspace ` +
|
|
734
|
+
`first before \`git ${subcommand}\` (otherwise the operation would write to ` +
|
|
735
|
+
`the ancestor repository, not the workspace).`,
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
function safeRealpath(path) {
|
|
739
|
+
try {
|
|
740
|
+
return realpathSync(path);
|
|
741
|
+
}
|
|
742
|
+
catch {
|
|
743
|
+
return path;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
568
746
|
function removeRegistryEntrySync(jobId) {
|
|
569
747
|
const path = join(homedir(), '.pugi', 'jobs.json');
|
|
570
748
|
const entries = readRegistryEntriesSync().filter((entry) => entry.id !== jobId);
|
|
@@ -592,6 +770,28 @@ export function bashToolSync(input, ctx) {
|
|
|
592
770
|
const additionalDirectories = ctx.additionalDirectories ?? [];
|
|
593
771
|
const source = ctx.source ?? 'agent';
|
|
594
772
|
const toolCallId = recordToolCall(ctx.session, 'bash', cmd);
|
|
773
|
+
const startCwd = resolveStartCwd(input.cwd ?? ctx.lastBashCwd, ctx.root, additionalDirectories);
|
|
774
|
+
// Workspace-git-boundary guard (CEO P0 #51, 2026-05-29). Fires
|
|
775
|
+
// BEFORE the permission gate so the structural boundary diagnostic
|
|
776
|
+
// is the one the operator sees. See the async path for the full
|
|
777
|
+
// rationale.
|
|
778
|
+
const boundaryBlock = enforceGitBoundary(cmd, startCwd, ctx.root);
|
|
779
|
+
if (boundaryBlock !== null) {
|
|
780
|
+
emitEvent(ctx.session, 'bash.git_boundary_escape', {
|
|
781
|
+
cmd,
|
|
782
|
+
workspaceRoot: ctx.root,
|
|
783
|
+
resolvedToplevel: boundaryBlock.resolvedToplevel ?? null,
|
|
784
|
+
});
|
|
785
|
+
recordToolResult(ctx.session, toolCallId, 'error', boundaryBlock.reason);
|
|
786
|
+
return {
|
|
787
|
+
stdout: '',
|
|
788
|
+
stderr: boundaryBlock.reason,
|
|
789
|
+
exitCode: 126,
|
|
790
|
+
nextCwd: ctx.lastBashCwd ?? ctx.root,
|
|
791
|
+
truncated: false,
|
|
792
|
+
timedOut: false,
|
|
793
|
+
};
|
|
794
|
+
}
|
|
595
795
|
const decision = evaluateBashPermission(cmd, ctx.settings.permissions.mode, {
|
|
596
796
|
workspaceRoot: ctx.root,
|
|
597
797
|
additionalDirectories,
|
|
@@ -609,7 +809,6 @@ export function bashToolSync(input, ctx) {
|
|
|
609
809
|
timedOut: false,
|
|
610
810
|
};
|
|
611
811
|
}
|
|
612
|
-
const startCwd = resolveStartCwd(input.cwd ?? ctx.lastBashCwd, ctx.root, additionalDirectories);
|
|
613
812
|
const timeoutMs = sanitizeTimeout(input.timeoutMs);
|
|
614
813
|
const childEnv = buildChildEnv();
|
|
615
814
|
const result = spawnSync('/bin/sh', ['-c', cmd], {
|