@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
|
@@ -119,6 +119,63 @@ const DESTRUCTIVE_PATTERNS = [
|
|
|
119
119
|
// History destruction
|
|
120
120
|
{ pattern: 'history -c' },
|
|
121
121
|
{ pattern: ' >/dev/null 2>&1; rm' },
|
|
122
|
+
// ---------------------------------------------------------------
|
|
123
|
+
// Patterns ported from KeiSeiKit `destructive-guard.sh` (Apache-2.0)
|
|
124
|
+
// and `safety-guard.sh` BLOCKED_PATTERNS array. Upstream source:
|
|
125
|
+
// /tmp/KeiSeiKit/hooks/destructive-guard.sh (lines 7-13)
|
|
126
|
+
// /tmp/KeiSeiKit/hooks/safety-guard.sh (lines 14-50)
|
|
127
|
+
// Attribution: licenses/keiseikit-LICENSE-NOTICE.md.
|
|
128
|
+
//
|
|
129
|
+
// The patterns below need word-boundary matching because their
|
|
130
|
+
// tokens (kill, halt, reboot, ...) appear as substrings of common
|
|
131
|
+
// unrelated words (skills, default, chrooted-rebooter, etc.).
|
|
132
|
+
// Substring `.includes` cannot express that — `regex` is required.
|
|
133
|
+
// ---------------------------------------------------------------
|
|
134
|
+
// Process termination — `kill`, `pkill`, `killall` at command head
|
|
135
|
+
// or after `sudo`. Matches `kill 1234`, `kill -9 $$`, `sudo killall
|
|
136
|
+
// node`, but NOT `skill issue` (no leading boundary) or
|
|
137
|
+
// `git commit -m "skill kill story"` (the kill is inside a quoted
|
|
138
|
+
// string — quote-aware split handled upstream; here we still need
|
|
139
|
+
// the boundary). Anchored to start-of-component or `sudo ` prefix.
|
|
140
|
+
{
|
|
141
|
+
pattern: 'kill',
|
|
142
|
+
regex: /^(?:sudo\s+)?(?:pkill|killall|kill)\b/,
|
|
143
|
+
},
|
|
144
|
+
// System power state — reboot / shutdown / halt / poweroff / init 0
|
|
145
|
+
// / init 6. KeiSei matches these anywhere in the command; we
|
|
146
|
+
// tighten to start-of-component or `sudo ` prefix to avoid FPs on
|
|
147
|
+
// file paths or variable names containing the substring.
|
|
148
|
+
{
|
|
149
|
+
pattern: 'reboot',
|
|
150
|
+
regex: /^(?:sudo\s+)?reboot\b/,
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
pattern: 'shutdown',
|
|
154
|
+
regex: /^(?:sudo\s+)?shutdown\b/,
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
pattern: 'halt',
|
|
158
|
+
regex: /^(?:sudo\s+)?halt\b/,
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
pattern: 'poweroff',
|
|
162
|
+
regex: /^(?:sudo\s+)?poweroff\b/,
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
pattern: 'init 0',
|
|
166
|
+
regex: /^(?:sudo\s+)?init\s+0\b/,
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
pattern: 'init 6',
|
|
170
|
+
regex: /^(?:sudo\s+)?init\s+6\b/,
|
|
171
|
+
},
|
|
172
|
+
// `git clean -f` (without -dx) — KeiSei lists this as destructive
|
|
173
|
+
// because it still deletes untracked files. Pugi previously only
|
|
174
|
+
// gated `git clean -fdx`; broaden to any `-f` variant.
|
|
175
|
+
{
|
|
176
|
+
pattern: 'git clean -f',
|
|
177
|
+
regex: /\bgit\s+clean\s+-[A-Za-z]*f/,
|
|
178
|
+
},
|
|
122
179
|
];
|
|
123
180
|
/**
|
|
124
181
|
* Compound separators. We split on `&&`, `||`, `;`, `|` to classify
|
|
@@ -289,6 +346,93 @@ const BUILD_TEST_PREFIXES = [
|
|
|
289
346
|
'tsc -p',
|
|
290
347
|
'eslint',
|
|
291
348
|
'prettier --check',
|
|
349
|
+
// P0 fix 2026-05-29 (#37 CRITICAL): customer-blocking gap surfaced
|
|
350
|
+
// during dogfood. Engine emitted `chmod +x build.sh`, `node script.js`,
|
|
351
|
+
// `python3 -m pytest`, `git status`, `pnpm build`, `docker ps`, etc.
|
|
352
|
+
// and the classifier returned `unknown` → permission matrix denied
|
|
353
|
+
// в bypassPermissions mode (which the customer expected to auto-allow
|
|
354
|
+
// basic dev tools). Customers could not run ANY real build/test/git
|
|
355
|
+
// workflow through Pugi.
|
|
356
|
+
//
|
|
357
|
+
// The prefixes below cover three classes of developer tooling that
|
|
358
|
+
// are always allowed in `auto`/`dontAsk`/`bypassPermissions` modes,
|
|
359
|
+
// `ask` in interactive modes, and `deny` in `plan` (read-only) mode:
|
|
360
|
+
// - Language runtimes: `node`, `python`, `python3`, `ruby`, etc.
|
|
361
|
+
// - Native build chains: `gcc`, `clang`, `cmake`, `rustc`, etc.
|
|
362
|
+
// - Container/k8s read-class: `docker ps/inspect/logs`, `kubectl get`.
|
|
363
|
+
//
|
|
364
|
+
// Destructive variants are already gated upstream by DESTRUCTIVE_PATTERNS
|
|
365
|
+
// (e.g. `docker system prune`, `kubectl delete --all`). The first-token
|
|
366
|
+
// gate in classifyComponent runs THIS list before the unknown fallback.
|
|
367
|
+
//
|
|
368
|
+
// Language runtime invocations (first-token match, with or without args).
|
|
369
|
+
'node',
|
|
370
|
+
'python',
|
|
371
|
+
'python3',
|
|
372
|
+
'ruby',
|
|
373
|
+
'perl',
|
|
374
|
+
'php',
|
|
375
|
+
'deno',
|
|
376
|
+
'bun',
|
|
377
|
+
'tsx',
|
|
378
|
+
'ts-node',
|
|
379
|
+
// Native build chains.
|
|
380
|
+
'gcc',
|
|
381
|
+
'g++',
|
|
382
|
+
'clang',
|
|
383
|
+
'clang++',
|
|
384
|
+
'cmake',
|
|
385
|
+
'rustc',
|
|
386
|
+
'javac',
|
|
387
|
+
'java',
|
|
388
|
+
// Container/k8s read-class (the destructive subcommands are pre-empted
|
|
389
|
+
// by DESTRUCTIVE_PATTERNS: `docker system prune`, `kubectl delete --all`,
|
|
390
|
+
// `kubectl delete namespace`).
|
|
391
|
+
'docker ps',
|
|
392
|
+
'docker images',
|
|
393
|
+
'docker inspect',
|
|
394
|
+
'docker logs',
|
|
395
|
+
'docker version',
|
|
396
|
+
'docker info',
|
|
397
|
+
'docker exec',
|
|
398
|
+
'docker run',
|
|
399
|
+
'docker stop',
|
|
400
|
+
'docker start',
|
|
401
|
+
'docker restart',
|
|
402
|
+
'docker rm',
|
|
403
|
+
'docker rmi',
|
|
404
|
+
'docker build',
|
|
405
|
+
'docker tag',
|
|
406
|
+
'docker compose',
|
|
407
|
+
'docker-compose',
|
|
408
|
+
'kubectl get',
|
|
409
|
+
'kubectl describe',
|
|
410
|
+
'kubectl logs',
|
|
411
|
+
'kubectl exec',
|
|
412
|
+
'kubectl apply',
|
|
413
|
+
'kubectl create',
|
|
414
|
+
'kubectl rollout',
|
|
415
|
+
'kubectl port-forward',
|
|
416
|
+
'kubectl config',
|
|
417
|
+
// Git read+write surface (network ops already handled by NETWORK_PREFIXES;
|
|
418
|
+
// destructive ops `reset --hard`/`clean -fdx`/`push --force` blocked above).
|
|
419
|
+
// Note: WRITE_WORKSPACE_PREFIXES already covers `git commit/add/checkout/...`.
|
|
420
|
+
// These entries handle plain `git rev-list`, `git cherry-pick`, `git worktree`,
|
|
421
|
+
// `git submodule`, etc that customer scripts commonly invoke.
|
|
422
|
+
'git rev-list',
|
|
423
|
+
'git cherry-pick',
|
|
424
|
+
'git worktree',
|
|
425
|
+
'git submodule',
|
|
426
|
+
'git blame',
|
|
427
|
+
'git describe',
|
|
428
|
+
'git tag --list',
|
|
429
|
+
'git tag -l',
|
|
430
|
+
'git for-each-ref',
|
|
431
|
+
'git ls-remote',
|
|
432
|
+
// gh CLI (GitHub). `gh repo delete` / `gh release delete` reach into
|
|
433
|
+
// network operations but are non-destructive for the local workspace.
|
|
434
|
+
// Permission matrix asks before allowing in auto.
|
|
435
|
+
'gh',
|
|
292
436
|
];
|
|
293
437
|
/** Single-token read-only commands. Argument-free entries match exact. */
|
|
294
438
|
const READ_TOKENS = new Set([
|
|
@@ -327,6 +471,16 @@ const READ_TOKENS = new Set([
|
|
|
327
471
|
'cut',
|
|
328
472
|
'sort',
|
|
329
473
|
'uniq',
|
|
474
|
+
// P0 fix 2026-05-29 (#37 CRITICAL): structured-data inspection tools
|
|
475
|
+
// are pure stdin/stdout transformers (no FS write, no network) when
|
|
476
|
+
// не paired с `>` redirection (the redirection branch above promotes
|
|
477
|
+
// к write_workspace independently). Common в dev scripts for parsing
|
|
478
|
+
// package.json, tsconfig.json, Helm values.yaml, etc.
|
|
479
|
+
// `tee` is INTENTIONALLY excluded — it writes by definition, even
|
|
480
|
+
// в protected paths (`tee /etc/...` is already in DESTRUCTIVE_PATTERNS).
|
|
481
|
+
'jq',
|
|
482
|
+
'yq',
|
|
483
|
+
'column',
|
|
330
484
|
]);
|
|
331
485
|
const READ_PREFIXES = [
|
|
332
486
|
'git status',
|
|
@@ -361,13 +515,23 @@ const WRITE_WORKSPACE_PREFIXES = [
|
|
|
361
515
|
'git tag',
|
|
362
516
|
'git rebase',
|
|
363
517
|
'git merge',
|
|
518
|
+
// P0 fix 2026-05-29 (#37 CRITICAL): file-permission ops are common
|
|
519
|
+
// в build scripts (`chmod +x build.sh`, `chown $USER file`). The
|
|
520
|
+
// destructive variants (`chmod 777 /`, `chmod -R 777 /`, `chmod -R
|
|
521
|
+
// 777 ~`, `chown -R root /`, `chown -R / ...`) are pre-empted by
|
|
522
|
+
// DESTRUCTIVE_PATTERNS which runs BEFORE this list — safe to add
|
|
523
|
+
// here for the non-destructive path. detectProtectedWrite's `\bchmod\b`
|
|
524
|
+
// / `\bchown\b` regex also catches writes into protected paths
|
|
525
|
+
// regardless of this list.
|
|
526
|
+
'chmod ',
|
|
527
|
+
'chown ',
|
|
364
528
|
];
|
|
365
529
|
/**
|
|
366
530
|
* Protected-write triggers. If a command writes to any of these paths
|
|
367
531
|
* the class is `write_protected` regardless of the operation type.
|
|
368
532
|
*
|
|
369
533
|
* Wildcards are handled as substring matches (e.g. `/.ssh/` matches
|
|
370
|
-
* `~/.ssh/foo` and
|
|
534
|
+
* `~/.ssh/foo` and `[HOME]/USER/.ssh/bar`).
|
|
371
535
|
*/
|
|
372
536
|
const PROTECTED_PATH_SUBSTRINGS = [
|
|
373
537
|
'/.ssh/',
|
|
@@ -388,6 +552,40 @@ const PROTECTED_PATH_SUBSTRINGS = [
|
|
|
388
552
|
'/usr/',
|
|
389
553
|
'/var/',
|
|
390
554
|
];
|
|
555
|
+
/**
|
|
556
|
+
* Protected basename triggers — files whose CONTENT must never leak
|
|
557
|
+
* through the bash surface, even when the literal path is workspace-
|
|
558
|
+
* local. Mirrors `permission.ts::protectedBasenames` and `.env.*`
|
|
559
|
+
* pattern so the read-tool gate (which fires on `read .env`) and the
|
|
560
|
+
* bash gate (which fires on `cat .env`) stay symmetric.
|
|
561
|
+
*
|
|
562
|
+
* P0 fix 2026-05-28 (Codex audit): before this list existed, the
|
|
563
|
+
* engine model could circumvent the `read` tool's `protectedTargetReason`
|
|
564
|
+
* check by emitting `bash cat .env` — the classifier saw `cat` (read
|
|
565
|
+
* token) + `.env` (not in PROTECTED_PATH_SUBSTRINGS) and returned class
|
|
566
|
+
* `read`, which the permission matrix allows under every mode. The
|
|
567
|
+
* `local-first-invariants` spec proved the leak: `pugi explain .env`
|
|
568
|
+
* surfaced `SECRET=should_never_leak` in the engine summary.
|
|
569
|
+
*
|
|
570
|
+
* Match shape: the substring must touch a `.` boundary (`/.env`,
|
|
571
|
+
* ` .env`, `.env\b`) or appear as the full token so a path like
|
|
572
|
+
* `apps/codeforge/file.env-template` (no real secret) does not
|
|
573
|
+
* over-trigger.
|
|
574
|
+
*/
|
|
575
|
+
const PROTECTED_BASENAME_PATTERNS = [
|
|
576
|
+
// `.env`, `.env.production`, `.env.local` — anywhere in the command.
|
|
577
|
+
// Boundary on the left is start/whitespace/quote/`/`, on the right
|
|
578
|
+
// start/whitespace/end/quote/`>`/`|`/`;`.
|
|
579
|
+
/(^|[\s'"\/=])\.env(\.[A-Za-z0-9_-]+)?($|[\s'"<>|;&])/,
|
|
580
|
+
// SSH key basenames (covers both `id_rsa` and `id_ed25519` even
|
|
581
|
+
// outside `~/.ssh/`). The `/.ssh/` substring above gates the
|
|
582
|
+
// directory case; this catches a key file copied to the workspace.
|
|
583
|
+
/(^|[\s'"\/])id_(rsa|ed25519|ecdsa|dsa)(\.pub)?($|[\s'"<>|;&])/,
|
|
584
|
+
// Other credential basenames mirrored from permission.ts.
|
|
585
|
+
/(^|[\s'"\/])\.npmrc($|[\s'"<>|;&])/,
|
|
586
|
+
/(^|[\s'"\/])\.pypirc($|[\s'"<>|;&])/,
|
|
587
|
+
/(^|[\s'"\/])\.gitconfig($|[\s'"<>|;&])/,
|
|
588
|
+
];
|
|
391
589
|
/**
|
|
392
590
|
* Obfuscation triggers — any of these forces the `unknown` class so
|
|
393
591
|
* the permission engine can fail closed.
|
|
@@ -469,6 +667,26 @@ function classifyComponent(cmd, ctx) {
|
|
|
469
667
|
matched: protectedRead.matched,
|
|
470
668
|
};
|
|
471
669
|
}
|
|
670
|
+
// 4a-bis. Parent-traversal in read arguments. The file-tools layer
|
|
671
|
+
// refuses `..` segments via `resolveWorkspacePath`, but the bash
|
|
672
|
+
// surface had no equivalent gate — the engine could emit
|
|
673
|
+
// `cat ../README.md` or `ls ..` to enumerate / read outside the
|
|
674
|
+
// workspace, sidestepping the path-security check that the `read`
|
|
675
|
+
// and `glob` tools enforce.
|
|
676
|
+
//
|
|
677
|
+
// P0 fix 2026-05-28 (Codex audit): treat `..` as a path segment
|
|
678
|
+
// (`../`, ` ..`, `..\n`) in any read-class command as a workspace
|
|
679
|
+
// escape. We classify it as `write_protected` so the auto/dontAsk
|
|
680
|
+
// modes refuse, mirroring the `Path escapes workspace` semantics
|
|
681
|
+
// the file-tools layer already provides.
|
|
682
|
+
const traversal = detectParentTraversalRead(trimmed);
|
|
683
|
+
if (traversal) {
|
|
684
|
+
return {
|
|
685
|
+
class: 'write_protected',
|
|
686
|
+
reason: traversal.reason,
|
|
687
|
+
matched: traversal.matched,
|
|
688
|
+
};
|
|
689
|
+
}
|
|
472
690
|
// 4b. .env writes are always protected, even inside the workspace
|
|
473
691
|
// (CEO directive feedback_never_delete_untracked_env.md).
|
|
474
692
|
const envWrite = detectEnvWrite(trimmed);
|
|
@@ -525,6 +743,25 @@ function classifyComponent(cmd, ctx) {
|
|
|
525
743
|
if (trimmed === 'make' || trimmed.startsWith('make ')) {
|
|
526
744
|
return { class: 'build_test', reason: 'make runner', matched: 'make' };
|
|
527
745
|
}
|
|
746
|
+
// 7c. Operator-override safe tokens (P0 fix 2026-05-29 #37).
|
|
747
|
+
// `PUGI_CLASSIFIER_EXTRA_SAFE=tool1,tool2,...` extends the BUILD_TEST
|
|
748
|
+
// first-token list at runtime. This is a security-sensitive escape
|
|
749
|
+
// hatch — operators can add their custom build tools without a
|
|
750
|
+
// recompile. Destructive patterns ALREADY ran above (step 1) so this
|
|
751
|
+
// cannot whitelist `rm`, `mkfs`, `git push --force`, etc. The match
|
|
752
|
+
// is strict first-token equality — not substring — and the env var
|
|
753
|
+
// is read fresh on every classify call so tests can mutate it.
|
|
754
|
+
const extraSafe = readExtraSafeTokens();
|
|
755
|
+
if (extraSafe.size > 0) {
|
|
756
|
+
const firstTokenForExtraSafe = trimmed.split(/\s+/)[0] ?? '';
|
|
757
|
+
if (extraSafe.has(firstTokenForExtraSafe)) {
|
|
758
|
+
return {
|
|
759
|
+
class: 'build_test',
|
|
760
|
+
reason: `PUGI_CLASSIFIER_EXTRA_SAFE override: ${firstTokenForExtraSafe}`,
|
|
761
|
+
matched: firstTokenForExtraSafe,
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
}
|
|
528
765
|
// 7c. Bare `cd <path>` (inside workspace — the cwd-escape detector
|
|
529
766
|
// upgrades the class to write_protected when the target is
|
|
530
767
|
// outside). Standalone `cd` (HOME) is escape, also handled by the
|
|
@@ -568,7 +805,15 @@ function classifyComponent(cmd, ctx) {
|
|
|
568
805
|
}
|
|
569
806
|
function findDestructiveMatch(cmd) {
|
|
570
807
|
const upper = cmd.toUpperCase();
|
|
571
|
-
for (const { pattern, caseInsensitive } of DESTRUCTIVE_PATTERNS) {
|
|
808
|
+
for (const { pattern, caseInsensitive, regex } of DESTRUCTIVE_PATTERNS) {
|
|
809
|
+
if (regex) {
|
|
810
|
+
// Word-boundary regex form (KeiSei-derived patterns). Match
|
|
811
|
+
// against the trimmed component so `^` anchors to command head,
|
|
812
|
+
// not surrounding whitespace from the compound split.
|
|
813
|
+
if (regex.test(cmd.trim()))
|
|
814
|
+
return pattern;
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
572
817
|
if (caseInsensitive) {
|
|
573
818
|
if (upper.includes(pattern))
|
|
574
819
|
return pattern;
|
|
@@ -637,14 +882,57 @@ function nestingDepth(cmd, open, close) {
|
|
|
637
882
|
function escapeRegex(s) {
|
|
638
883
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
639
884
|
}
|
|
885
|
+
/**
|
|
886
|
+
* Operator-override safe tokens. Read from `PUGI_CLASSIFIER_EXTRA_SAFE`
|
|
887
|
+
* (comma-separated). Allows operators to extend the BUILD_TEST first-
|
|
888
|
+
* token list at runtime for site-specific tooling без recompile.
|
|
889
|
+
*
|
|
890
|
+
* Security note: destructive substring patterns run BEFORE this gate
|
|
891
|
+
* (step 1 in classifyComponent), so this cannot whitelist `rm`, `mkfs`,
|
|
892
|
+
* `git push --force`, etc. The env var only adds tools to the benign
|
|
893
|
+
* build_test class. Invalid entries (empty strings, тokens containing
|
|
894
|
+
* shell metas) are silently dropped to avoid surprising classifications.
|
|
895
|
+
*
|
|
896
|
+
* Read fresh on every call so per-test mutations work и so operators
|
|
897
|
+
* can update without restarting the agent loop. The cost (one env var
|
|
898
|
+
* read + Set construction per call) is negligible for the classifier's
|
|
899
|
+
* call frequency.
|
|
900
|
+
*/
|
|
901
|
+
function readExtraSafeTokens() {
|
|
902
|
+
const raw = process.env.PUGI_CLASSIFIER_EXTRA_SAFE;
|
|
903
|
+
if (!raw || raw.trim() === '')
|
|
904
|
+
return new Set();
|
|
905
|
+
const tokens = new Set();
|
|
906
|
+
for (const candidate of raw.split(',')) {
|
|
907
|
+
const trimmed = candidate.trim();
|
|
908
|
+
if (trimmed === '')
|
|
909
|
+
continue;
|
|
910
|
+
// Reject anything containing shell metas or whitespace — only bare
|
|
911
|
+
// tool names allowed. Defends against accidental
|
|
912
|
+
// `PUGI_CLASSIFIER_EXTRA_SAFE='rm -rf /'` smuggling.
|
|
913
|
+
if (/[\s;|&<>$`(){}\[\]'"\\]/.test(trimmed))
|
|
914
|
+
continue;
|
|
915
|
+
tokens.add(trimmed);
|
|
916
|
+
}
|
|
917
|
+
return tokens;
|
|
918
|
+
}
|
|
640
919
|
function detectProtectedWrite(cmd, ctx) {
|
|
641
920
|
// Surface every write target this command produces so we can both
|
|
642
921
|
// protected-path-check and outside-workspace-check them uniformly.
|
|
643
922
|
// Captures `sort -o`, `uniq <in> <out>`, `sed -i` files, `awk '... > "file"'`,
|
|
644
923
|
// and `>` / `>>` redirections without surrounding whitespace.
|
|
645
924
|
const writeTargets = extractWriteTargets(cmd);
|
|
925
|
+
// Strip heredoc bodies before substring scan. Heredoc payloads are
|
|
926
|
+
// DATA (file contents the script writes), not commands the shell
|
|
927
|
+
// executes — a `package.json` body containing `/usr/local/bin/...`
|
|
928
|
+
// would FP as "Write into protected path: /usr/" under the broad
|
|
929
|
+
// includes() scan below. The per-target check at the bottom of this
|
|
930
|
+
// function still catches real `cat > /usr/file << EOF` attempts
|
|
931
|
+
// because extractWriteTargets reads the redirection target, not the
|
|
932
|
+
// heredoc body. CEO dogfood 2026-05-28 (#28 follow-up).
|
|
933
|
+
const cmdForScan = stripHeredocBodies(cmd);
|
|
646
934
|
for (const needle of PROTECTED_PATH_SUBSTRINGS) {
|
|
647
|
-
if (!
|
|
935
|
+
if (!cmdForScan.includes(needle))
|
|
648
936
|
continue;
|
|
649
937
|
// Reading from a protected path is allowed at the classifier
|
|
650
938
|
// layer (the permission engine still gates `read`); writing is
|
|
@@ -706,6 +994,56 @@ function detectProtectedWrite(cmd, ctx) {
|
|
|
706
994
|
* Conservative — we do not try to resolve shell vars or globs; the
|
|
707
995
|
* caller still gates absolute paths via `looksAbsoluteOutsideWorkspace`.
|
|
708
996
|
*/
|
|
997
|
+
/**
|
|
998
|
+
* Strip heredoc bodies so substring scans (e.g. `cmd.includes('/usr/')`)
|
|
999
|
+
* do not false-positive on file content the script is *writing*. A
|
|
1000
|
+
* heredoc starts с `<< 'WORD'` (or `<< WORD` / `<<-WORD`) and ends на a
|
|
1001
|
+
* line containing only WORD. The body between is DATA, not commands.
|
|
1002
|
+
*
|
|
1003
|
+
* Best-effort: handles single-heredoc-per-command (the common case)
|
|
1004
|
+
* AND multiple sequential heredocs. Nested heredocs (heredoc-inside-
|
|
1005
|
+
* heredoc) are rare and out of scope — the substring scan still gates
|
|
1006
|
+
* the outer command, just без stripping the nested body. Per-target
|
|
1007
|
+
* detection at detectProtectedWrite's tail loop catches real
|
|
1008
|
+
* `cat > /usr/file << EOF` attacks regardless of body content.
|
|
1009
|
+
*
|
|
1010
|
+
* CEO dogfood 2026-05-28 (#28): `cat > package.json << 'EOF'\n{"bin":
|
|
1011
|
+
* "/usr/local/bin/foo"}\nEOF` was rejected as "Write into protected
|
|
1012
|
+
* path: /usr/" because the broad substring scan saw `/usr/` in the
|
|
1013
|
+
* JSON body. With heredoc-body stripping, the scan now sees only
|
|
1014
|
+
* `cat > package.json << 'EOF' EOF` which contains no protected path.
|
|
1015
|
+
*/
|
|
1016
|
+
function stripHeredocBodies(cmd) {
|
|
1017
|
+
// Match `<< [-]'WORD'` or `<< [-]"WORD"` or `<< [-]WORD` (quoted form
|
|
1018
|
+
// disables variable expansion in real bash; we treat all three the
|
|
1019
|
+
// same for stripping). Capture the WORD so we can find the close
|
|
1020
|
+
// marker.
|
|
1021
|
+
const heredocStart = /<<-?\s*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\1/g;
|
|
1022
|
+
let out = cmd;
|
|
1023
|
+
let safetyLoops = 0;
|
|
1024
|
+
let match;
|
|
1025
|
+
while ((match = heredocStart.exec(out)) !== null) {
|
|
1026
|
+
if (++safetyLoops > 16)
|
|
1027
|
+
break;
|
|
1028
|
+
const word = match[2];
|
|
1029
|
+
if (!word)
|
|
1030
|
+
continue;
|
|
1031
|
+
const headEnd = match.index + match[0].length;
|
|
1032
|
+
// Find the close-marker line: `\n<optional indent>WORD<\n|$>`.
|
|
1033
|
+
const closeRegex = new RegExp(`\\n\\s*${word}(?:\\n|$)`);
|
|
1034
|
+
closeRegex.lastIndex = headEnd;
|
|
1035
|
+
const closeMatch = closeRegex.exec(out.slice(headEnd));
|
|
1036
|
+
if (!closeMatch)
|
|
1037
|
+
break;
|
|
1038
|
+
const closeStart = headEnd + closeMatch.index;
|
|
1039
|
+
const closeEnd = closeStart + closeMatch[0].length;
|
|
1040
|
+
// Replace heredoc body + close marker с single space so the regex
|
|
1041
|
+
// iterator's lastIndex stays meaningful.
|
|
1042
|
+
out = out.slice(0, headEnd) + ' ' + out.slice(closeEnd);
|
|
1043
|
+
heredocStart.lastIndex = headEnd + 1;
|
|
1044
|
+
}
|
|
1045
|
+
return out;
|
|
1046
|
+
}
|
|
709
1047
|
function extractWriteTargets(cmd) {
|
|
710
1048
|
const targets = [];
|
|
711
1049
|
// Shell redirection (`>`, `>>`) with optional whitespace. Skip
|
|
@@ -777,14 +1115,72 @@ function detectProtectedRead(cmd) {
|
|
|
777
1115
|
firstToken === 'find';
|
|
778
1116
|
if (!isReadTool)
|
|
779
1117
|
return null;
|
|
1118
|
+
// Strip heredoc bodies so `cat > config << 'EOF'\n... /etc/... \nEOF`
|
|
1119
|
+
// does not FP as "Read from protected path" when first-token=`cat` +
|
|
1120
|
+
// redirection writes к workspace-local file. Heredoc payload is data.
|
|
1121
|
+
// CEO dogfood 2026-05-28 (#28).
|
|
1122
|
+
const cmdForScan = stripHeredocBodies(cmd);
|
|
780
1123
|
for (const needle of PROTECTED_PATH_SUBSTRINGS) {
|
|
781
|
-
if (
|
|
1124
|
+
if (cmdForScan.includes(needle)) {
|
|
782
1125
|
return {
|
|
783
1126
|
reason: `Read from protected path: ${needle}`,
|
|
784
1127
|
matched: needle,
|
|
785
1128
|
};
|
|
786
1129
|
}
|
|
787
1130
|
}
|
|
1131
|
+
// P0 fix 2026-05-28: extend protected-read detection to credential
|
|
1132
|
+
// basenames (`cat .env`, `head id_rsa`, `grep TOKEN .env.production`).
|
|
1133
|
+
// Without this branch, the engine model can bypass the `read` tool's
|
|
1134
|
+
// `protectedTargetReason` gate by emitting a bash `cat` — the read
|
|
1135
|
+
// tool refuses, the model falls back to bash, and the classifier
|
|
1136
|
+
// (which only knew about full-path substrings) classified `cat .env`
|
|
1137
|
+
// as benign `read`. The `local-first-invariants` spec proved the leak.
|
|
1138
|
+
for (const pattern of PROTECTED_BASENAME_PATTERNS) {
|
|
1139
|
+
const match = cmd.match(pattern);
|
|
1140
|
+
if (match) {
|
|
1141
|
+
return {
|
|
1142
|
+
reason: `Read from protected basename: ${match[0].trim()}`,
|
|
1143
|
+
matched: match[0].trim(),
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
return null;
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* Detect parent-traversal segments (`..`) inside read-class commands.
|
|
1151
|
+
* The file-tools layer (`resolveWorkspacePath`) refuses these for the
|
|
1152
|
+
* `read`/`glob`/`grep` tools, but bash had no equivalent gate. We
|
|
1153
|
+
* trigger on the SAME shape `path-security.ts` rejects: a `..` segment
|
|
1154
|
+
* separated by `/` or whitespace. Quoted/escaped variants get the same
|
|
1155
|
+
* treatment.
|
|
1156
|
+
*
|
|
1157
|
+
* Returns null on the safe path (no `..` segment) so the caller falls
|
|
1158
|
+
* through to the regular read classification.
|
|
1159
|
+
*/
|
|
1160
|
+
function detectParentTraversalRead(cmd) {
|
|
1161
|
+
const firstToken = cmd.split(/\s+/)[0] ?? '';
|
|
1162
|
+
const isReadTool = READ_TOKENS.has(firstToken) ||
|
|
1163
|
+
READ_PREFIX_TOKENS.has(firstToken) ||
|
|
1164
|
+
firstToken === 'sed' ||
|
|
1165
|
+
firstToken === 'awk' ||
|
|
1166
|
+
firstToken === 'find';
|
|
1167
|
+
if (!isReadTool)
|
|
1168
|
+
return null;
|
|
1169
|
+
// Match `..` as a path segment: preceded by start/whitespace/quote/`/`
|
|
1170
|
+
// and followed by `/`, end-of-string, whitespace, or shell metas.
|
|
1171
|
+
// Avoids over-matching `v1..v2` (range syntax inside a single token)
|
|
1172
|
+
// and `1..5` (numeric ranges) because those lack the path boundary.
|
|
1173
|
+
const traversalPattern = /(^|[\s'"\/])\.\.(\/|$|[\s'"<>|;&])/;
|
|
1174
|
+
const m = cmd.match(traversalPattern);
|
|
1175
|
+
if (m) {
|
|
1176
|
+
return {
|
|
1177
|
+
reason: 'Read command escapes workspace via parent traversal',
|
|
1178
|
+
matched: '..',
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
// Absolute path read of /etc, /usr, /var, etc is already covered by
|
|
1182
|
+
// PROTECTED_PATH_SUBSTRINGS in detectProtectedRead — no extra branch
|
|
1183
|
+
// needed here.
|
|
788
1184
|
return null;
|
|
789
1185
|
}
|
|
790
1186
|
function detectEnvWrite(cmd) {
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resumer — read SessionStore events for a session, apply the L8
|
|
3
|
+
* compact mask + the L9 rewind mask, and return the visible transcript
|
|
4
|
+
* the REPL bootstrap (or a programmatic consumer) should render.
|
|
5
|
+
*
|
|
6
|
+
* Separation of concerns:
|
|
7
|
+
*
|
|
8
|
+
* - This module owns the READ path: list sessions, load events,
|
|
9
|
+
* reconstruct a clean transcript stream. No writes.
|
|
10
|
+
* - The WRITE path (append a rewind-marker, undo-rewind) lives in
|
|
11
|
+
* `./rewinder.ts`.
|
|
12
|
+
* - The REPL session lifecycle (lockfile, Ink mount, dispatch FSM)
|
|
13
|
+
* stays in `core/repl/session.ts`. We do NOT spin up the REPL here.
|
|
14
|
+
*
|
|
15
|
+
* Why route resume through this module at all (vs. operators using
|
|
16
|
+
* `core/repl/store/*` directly):
|
|
17
|
+
*
|
|
18
|
+
* The store returns RAW events. Most consumers want masked events —
|
|
19
|
+
* i.e. the chronological list after compact-boundary masking AND
|
|
20
|
+
* rewind-marker masking. Doing both passes inline at every call site
|
|
21
|
+
* would scatter the mask logic; centralising it here means a future
|
|
22
|
+
* third mask (named checkpoints? selective edit?) lands in one place.
|
|
23
|
+
*/
|
|
24
|
+
import { homedir } from 'node:os';
|
|
25
|
+
import { applyCompactMask } from '../compact/buffer-rewriter.js';
|
|
26
|
+
import { SqliteSessionStore, resolveProjectStoreDir, } from '../repl/store/index.js';
|
|
27
|
+
import { applyRewindMask, findLatestActiveRewind } from './rewinder.js';
|
|
28
|
+
/**
|
|
29
|
+
* Composed mask: compact-mask first (collapses summarised slices into
|
|
30
|
+
* boundary markers + kept tail), then rewind-mask (drops everything
|
|
31
|
+
* inside an active rewind range, including any compaction markers that
|
|
32
|
+
* fell inside it).
|
|
33
|
+
*
|
|
34
|
+
* Order matters: compact-mask reads `coversUntilOffset` against the
|
|
35
|
+
* RAW event indices. Running rewind-mask first would shift indices and
|
|
36
|
+
* break the compact replay anchor. The result is the chronological
|
|
37
|
+
* stream the operator should SEE, with infra rows (rewind markers)
|
|
38
|
+
* stripped.
|
|
39
|
+
*/
|
|
40
|
+
export function applyAllMasks(events) {
|
|
41
|
+
return applyRewindMask(applyCompactMask(events));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* List sessions a `pugi resume` invocation could open. Uses the
|
|
45
|
+
* READ-ONLY store view so the call never takes the lockfile — safe to
|
|
46
|
+
* run alongside a live REPL. Each row carries derived metadata
|
|
47
|
+
* (`visibleEventCount`, `hasActiveRewind`) so the renderer does not
|
|
48
|
+
* need to re-walk events.
|
|
49
|
+
*
|
|
50
|
+
* Returns an empty array when the project store does not exist (no
|
|
51
|
+
* sessions ever started for this project slug). Callers surface a
|
|
52
|
+
* "nothing to resume" message in that branch.
|
|
53
|
+
*/
|
|
54
|
+
export async function listResumableSessions(input) {
|
|
55
|
+
const dir = input.storeDir ?? resolveProjectStoreDir(input.projectSlug, input.home ?? homedir());
|
|
56
|
+
const limit = clampLimit(input.limit ?? 10, 1, 50);
|
|
57
|
+
let view;
|
|
58
|
+
try {
|
|
59
|
+
view = await SqliteSessionStore.openReadOnly(dir);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const rows = await view.list({ project: input.projectSlug, limit });
|
|
66
|
+
const out = [];
|
|
67
|
+
for (const row of rows) {
|
|
68
|
+
const events = await view.events(row.id);
|
|
69
|
+
const visible = applyAllMasks(events);
|
|
70
|
+
const latest = findLatestActiveRewind(events);
|
|
71
|
+
out.push({
|
|
72
|
+
row,
|
|
73
|
+
visibleEventCount: visible.length,
|
|
74
|
+
hasActiveRewind: latest !== null,
|
|
75
|
+
updatedAt: row.updatedAt,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
await view.close();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Load one session for replay. The caller (REPL bootstrap, tests,
|
|
89
|
+
* future programmatic exporters) gets BOTH the raw event stream and
|
|
90
|
+
* the masked view so it can choose its rendering strategy. Returns
|
|
91
|
+
* null when the session does not exist; throws when the store cannot
|
|
92
|
+
* be opened (the caller surfaces a one-line error).
|
|
93
|
+
*
|
|
94
|
+
* The PID lockfile contention is NOT relevant here — we use the
|
|
95
|
+
* read-only view. Concurrent writers from a live REPL are safe.
|
|
96
|
+
*/
|
|
97
|
+
export async function loadSessionForReplay(input) {
|
|
98
|
+
const dir = input.storeDir ?? resolveProjectStoreDir(input.projectSlug, input.home ?? homedir());
|
|
99
|
+
const view = await SqliteSessionStore.openReadOnly(dir);
|
|
100
|
+
try {
|
|
101
|
+
const row = await view.get(input.sessionId);
|
|
102
|
+
if (!row)
|
|
103
|
+
return null;
|
|
104
|
+
const rawEvents = await view.events(row.id);
|
|
105
|
+
const visibleEvents = applyAllMasks(rawEvents);
|
|
106
|
+
const latest = findLatestActiveRewind(rawEvents);
|
|
107
|
+
return {
|
|
108
|
+
row,
|
|
109
|
+
rawEvents,
|
|
110
|
+
visibleEvents,
|
|
111
|
+
hasActiveRewind: latest !== null,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
await view.close();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Load raw + masked events through an already-open SessionStore.
|
|
120
|
+
*
|
|
121
|
+
* Used by the in-REPL `/rewind` slash handler — the live REPL already
|
|
122
|
+
* holds the writer lock, so we cannot open the read-only view in the
|
|
123
|
+
* same process. The store reference IS the active write handle; we
|
|
124
|
+
* just call `loadEvents` and run the masks.
|
|
125
|
+
*
|
|
126
|
+
* Same shape as `loadSessionForReplay` minus the read-only-view setup.
|
|
127
|
+
*/
|
|
128
|
+
export async function loadFromStore(store, sessionId) {
|
|
129
|
+
const row = await store.getSession(sessionId);
|
|
130
|
+
if (!row)
|
|
131
|
+
return null;
|
|
132
|
+
const rawEvents = await store.loadEvents(sessionId);
|
|
133
|
+
const visibleEvents = applyAllMasks(rawEvents);
|
|
134
|
+
const latest = findLatestActiveRewind(rawEvents);
|
|
135
|
+
return {
|
|
136
|
+
row,
|
|
137
|
+
rawEvents,
|
|
138
|
+
visibleEvents,
|
|
139
|
+
hasActiveRewind: latest !== null,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function clampLimit(raw, min, max) {
|
|
143
|
+
if (!Number.isFinite(raw) || raw < min)
|
|
144
|
+
return min;
|
|
145
|
+
if (raw > max)
|
|
146
|
+
return max;
|
|
147
|
+
return Math.floor(raw);
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=resumer.js.map
|