supipowers 1.5.3 → 2.0.0
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/README.md +14 -8
- package/bin/install.mjs +20 -5
- package/bin/install.ts +95 -0
- package/package.json +8 -4
- package/skills/context-mode/SKILL.md +17 -10
- package/skills/harness/SKILL.md +94 -0
- package/skills/ui-design/SKILL.md +63 -0
- package/skills/ui-design/sub-agent-templates/component-builder.md +29 -0
- package/skills/ui-design/sub-agent-templates/design-critic.md +46 -0
- package/skills/ui-design/sub-agent-templates/pencil/component-builder.md +29 -0
- package/skills/ui-design/sub-agent-templates/pencil/design-critic.md +42 -0
- package/skills/ui-design/sub-agent-templates/pencil/section-assembler.md +27 -0
- package/skills/ui-design/sub-agent-templates/section-assembler.md +27 -0
- package/skills/ultraplan-discover/SKILL.md +96 -0
- package/skills/ultraplan-intake/SKILL.md +89 -0
- package/skills/ultraplan-research/SKILL.md +129 -0
- package/skills/ultraplan-review/SKILL.md +86 -0
- package/skills/ultraplan-review-scope/SKILL.md +111 -0
- package/skills/ultraplan-review-structure/SKILL.md +120 -0
- package/skills/ultraplan-review-tdd/SKILL.md +142 -0
- package/skills/ultraplan-scout/SKILL.md +110 -0
- package/skills/ultraplan-synthesize/SKILL.md +124 -0
- package/src/{quality/ai-session.ts → ai/final-message.ts} +27 -0
- package/src/ai/schema-text.ts +129 -0
- package/src/ai/structured-output.ts +274 -0
- package/src/ai/template.ts +27 -0
- package/src/bootstrap.ts +63 -28
- package/src/commands/agents.ts +131 -42
- package/src/commands/ai-review.ts +251 -30
- package/src/commands/clear.ts +434 -0
- package/src/commands/commit.ts +1 -0
- package/src/commands/config.ts +242 -44
- package/src/commands/context.ts +55 -28
- package/src/commands/doctor.ts +234 -6
- package/src/commands/fix-pr.ts +306 -131
- package/src/commands/generate.ts +111 -21
- package/src/commands/memory.ts +192 -0
- package/src/commands/model-picker.ts +28 -21
- package/src/commands/model.ts +18 -8
- package/src/commands/optimize-context.ts +408 -29
- package/src/commands/plan.ts +2 -0
- package/src/commands/qa.ts +312 -137
- package/src/commands/release.ts +259 -76
- package/src/commands/review.ts +293 -59
- package/src/commands/status.ts +200 -13
- package/src/commands/supi.ts +3 -35
- package/src/commands/ui-design.ts +394 -0
- package/src/commands/ultraplan.ts +1518 -0
- package/src/commands/update.ts +86 -0
- package/src/config/defaults.ts +62 -0
- package/src/config/loader.ts +448 -60
- package/src/config/schema.ts +108 -2
- package/src/context/optimizer.ts +25 -33
- package/src/context/rule-renderer.ts +223 -0
- package/src/context/savings.ts +258 -0
- package/src/context/startup-check.ts +380 -0
- package/src/context/startup-optimizer.ts +355 -0
- package/src/context/tokenignore.ts +146 -0
- package/src/context-mode/cache-handle.ts +49 -0
- package/src/context-mode/cache-preview.ts +71 -0
- package/src/context-mode/cache-store.ts +738 -0
- package/src/context-mode/compressor.ts +131 -26
- package/src/context-mode/dedup.ts +108 -0
- package/src/context-mode/detector.ts +35 -4
- package/src/context-mode/event-extractor.ts +14 -12
- package/src/context-mode/event-store.ts +91 -36
- package/src/context-mode/hooks.ts +798 -56
- package/src/context-mode/knowledge/store.ts +255 -11
- package/src/context-mode/memory-store.ts +325 -0
- package/src/context-mode/metrics-recorder.ts +158 -0
- package/src/context-mode/metrics-store.ts +765 -0
- package/src/context-mode/model.ts +24 -0
- package/src/context-mode/processor-keys.ts +29 -0
- package/src/context-mode/processors/build.ts +66 -0
- package/src/context-mode/processors/docker.ts +57 -0
- package/src/context-mode/processors/git.ts +111 -0
- package/src/context-mode/processors/json.ts +112 -0
- package/src/context-mode/processors/k8s.ts +67 -0
- package/src/context-mode/processors/lint.ts +67 -0
- package/src/context-mode/processors/log.ts +86 -0
- package/src/context-mode/processors/registry.ts +116 -0
- package/src/context-mode/processors/test-runner.ts +102 -0
- package/src/context-mode/processors/types.ts +20 -0
- package/src/context-mode/repomap.ts +400 -0
- package/src/context-mode/routing.ts +97 -24
- package/src/context-mode/sandbox/runners.ts +5 -1
- package/src/context-mode/snapshot-builder.ts +106 -11
- package/src/context-mode/source-hash.ts +173 -0
- package/src/context-mode/tool-name.ts +11 -0
- package/src/context-mode/tools.ts +654 -22
- package/src/context-mode/web/fetcher.ts +31 -12
- package/src/debug/logger.ts +2 -1
- package/src/deps/registry.ts +1 -1
- package/src/discipline/failure-summarizer.ts +170 -0
- package/src/discipline/failure-taxonomy.ts +131 -0
- package/src/discipline/workflow-invariants.ts +125 -0
- package/src/discovery/index.ts +31 -0
- package/src/discovery/lsp.ts +87 -0
- package/src/discovery/rank.ts +144 -0
- package/src/discovery/sources.ts +89 -0
- package/src/discovery/workflow.ts +87 -0
- package/src/docs/contracts.ts +39 -0
- package/src/docs/drift.ts +117 -87
- package/src/fix-pr/assessment.ts +200 -0
- package/src/fix-pr/contracts.ts +47 -0
- package/src/fix-pr/fetch-comments.ts +80 -0
- package/src/fix-pr/prompt-builder.ts +58 -40
- package/src/fix-pr/scripts/exec.ts +34 -0
- package/src/fix-pr/scripts/trigger-review.ts +106 -0
- package/src/fix-pr/scripts/wait-and-check.ts +108 -0
- package/src/fix-pr/types.ts +4 -0
- package/src/git/branch-finish.ts +5 -0
- package/src/git/commit-contract.ts +83 -0
- package/src/git/commit.ts +121 -184
- package/src/git/status.ts +62 -8
- package/src/harness/anti_slop/architecture-parser.ts +210 -0
- package/src/harness/anti_slop/backend-factory.ts +30 -0
- package/src/harness/anti_slop/backend.ts +140 -0
- package/src/harness/anti_slop/desloppify-adapter.ts +319 -0
- package/src/harness/anti_slop/fallow-adapter.ts +305 -0
- package/src/harness/anti_slop/installer.ts +227 -0
- package/src/harness/anti_slop/queue.ts +216 -0
- package/src/harness/anti_slop/recommend.ts +84 -0
- package/src/harness/anti_slop/score.ts +180 -0
- package/src/harness/anti_slop/synthetic-edit-test.ts +128 -0
- package/src/harness/artifacts/agents-md.ts +88 -0
- package/src/harness/artifacts/checks-wiring.ts +57 -0
- package/src/harness/artifacts/docs-tree.ts +79 -0
- package/src/harness/artifacts/lint-configs.ts +136 -0
- package/src/harness/artifacts/review-agents.ts +67 -0
- package/src/harness/bare-entry.ts +108 -0
- package/src/harness/command.ts +1010 -0
- package/src/harness/default-agents/design.md +23 -0
- package/src/harness/default-agents/discover.md +18 -0
- package/src/harness/default-agents/implement.md +24 -0
- package/src/harness/default-agents/plan.md +19 -0
- package/src/harness/default-agents/research.md +21 -0
- package/src/harness/default-agents/validate.md +22 -0
- package/src/harness/gc/reporter.ts +28 -0
- package/src/harness/gc/runner.ts +136 -0
- package/src/harness/hooks/layer-context-inject.ts +155 -0
- package/src/harness/hooks/post-session-sweep.ts +130 -0
- package/src/harness/hooks/pre-edit-dupe-probe.ts +224 -0
- package/src/harness/hooks/register.ts +118 -0
- package/src/harness/model.ts +117 -0
- package/src/harness/pipeline.ts +348 -0
- package/src/harness/project-paths.ts +235 -0
- package/src/harness/stage-runner.ts +107 -0
- package/src/harness/stages/design.ts +386 -0
- package/src/harness/stages/discover.ts +454 -0
- package/src/harness/stages/implement.ts +162 -0
- package/src/harness/stages/plan.ts +335 -0
- package/src/harness/stages/research.ts +263 -0
- package/src/harness/stages/validate.ts +684 -0
- package/src/harness/storage.ts +467 -0
- package/src/harness/tools.ts +426 -0
- package/src/lsp/bridge.ts +56 -95
- package/src/lsp/capabilities.ts +108 -0
- package/src/lsp/contracts.ts +35 -0
- package/src/lsp/detector.ts +8 -12
- package/src/markdown-frontmatter.ts +68 -0
- package/src/mempalace/bridge.ts +129 -0
- package/src/mempalace/config.ts +75 -0
- package/src/mempalace/format.ts +163 -0
- package/src/mempalace/hooks.ts +370 -0
- package/src/mempalace/installer-helper.ts +194 -0
- package/src/mempalace/python/mempalace_bridge.py +440 -0
- package/src/mempalace/runtime.ts +565 -0
- package/src/mempalace/schema.ts +264 -0
- package/src/mempalace/session-summary.ts +198 -0
- package/src/mempalace/tool.ts +186 -0
- package/src/mempalace/uv.ts +256 -0
- package/src/migrate/runner.ts +354 -0
- package/src/planning/approval-flow.ts +206 -9
- package/src/planning/plan-writer-prompt.ts +4 -3
- package/src/planning/planning-ask-tool.ts +39 -0
- package/src/planning/render-markdown.ts +74 -0
- package/src/planning/spec.ts +42 -0
- package/src/planning/system-prompt.ts +11 -8
- package/src/planning/validate.ts +84 -0
- package/src/platform/omp.ts +15 -2
- package/src/platform/system-prompt.ts +37 -0
- package/src/platform/test-utils.ts +3 -0
- package/src/platform/types.ts +6 -1
- package/src/qa/config.ts +12 -6
- package/src/qa/detect-app-type.ts +13 -6
- package/src/qa/matrix.ts +12 -6
- package/src/qa/prompt-builder.ts +28 -30
- package/src/qa/scripts/dev-server-utils.ts +72 -0
- package/src/qa/scripts/run-e2e-tests.ts +226 -0
- package/src/qa/scripts/start-dev-server.ts +138 -0
- package/src/qa/scripts/stop-dev-server.ts +77 -0
- package/src/qa/session.ts +13 -7
- package/src/quality/ai-setup.ts +27 -25
- package/src/quality/contracts.ts +34 -0
- package/src/quality/gates/ai-review.ts +20 -58
- package/src/quality/gates/command.ts +249 -46
- package/src/quality/review-gates.ts +18 -2
- package/src/quality/runner.ts +63 -22
- package/src/quality/schemas.ts +37 -2
- package/src/quality/setup.ts +96 -16
- package/src/release/changelog.ts +1 -1
- package/src/release/channels/custom.ts +13 -3
- package/src/release/channels/types.ts +5 -0
- package/src/release/contracts.ts +90 -0
- package/src/release/executor.ts +122 -45
- package/src/release/prompt.ts +18 -2
- package/src/release/targets.ts +86 -0
- package/src/release/version.ts +96 -71
- package/src/review/agent-loader.ts +221 -109
- package/src/review/fixer.ts +10 -6
- package/src/review/multi-agent-runner.ts +114 -13
- package/src/review/output.ts +12 -139
- package/src/review/runner.ts +12 -6
- package/src/review/scope.ts +144 -24
- package/src/review/types.ts +1 -20
- package/src/review/validator.ts +12 -6
- package/src/storage/fix-pr-sessions.ts +21 -14
- package/src/storage/plans.ts +14 -5
- package/src/storage/qa-sessions.ts +25 -19
- package/src/storage/reliability-metrics.ts +180 -0
- package/src/storage/reports.ts +8 -7
- package/src/storage/review-sessions.ts +55 -20
- package/src/tool-catalog/active-tool-controller.ts +164 -0
- package/src/tool-catalog/active-tool-planner.ts +212 -0
- package/src/tool-catalog/tool-groups.ts +102 -0
- package/src/types.ts +1399 -5
- package/src/ui-design/backend-adapter.ts +78 -0
- package/src/ui-design/backends/local-html.ts +82 -0
- package/src/ui-design/backends/pencil-mcp.ts +111 -0
- package/src/ui-design/components-scanner.ts +124 -0
- package/src/ui-design/config.ts +55 -0
- package/src/ui-design/pen-scanner.ts +95 -0
- package/src/ui-design/pen-selector.ts +72 -0
- package/src/ui-design/prompt-builder.ts +73 -0
- package/src/ui-design/scanner.ts +136 -0
- package/src/ui-design/session.ts +974 -0
- package/src/ui-design/system-prompt.ts +312 -0
- package/src/ui-design/tokens-scanner.ts +181 -0
- package/src/ui-design/types.ts +96 -0
- package/src/ultraplan/agent-catalog.ts +522 -0
- package/src/ultraplan/authoring/agent-catalog.ts +310 -0
- package/src/ultraplan/authoring/authoring-tools.ts +552 -0
- package/src/ultraplan/authoring/command-handlers.ts +339 -0
- package/src/ultraplan/authoring/markdown.ts +510 -0
- package/src/ultraplan/authoring/model.ts +162 -0
- package/src/ultraplan/authoring/pipeline.ts +319 -0
- package/src/ultraplan/authoring/stage-runner.ts +141 -0
- package/src/ultraplan/authoring/stages/approve.ts +249 -0
- package/src/ultraplan/authoring/stages/discover.ts +289 -0
- package/src/ultraplan/authoring/stages/intake.ts +203 -0
- package/src/ultraplan/authoring/stages/research.ts +399 -0
- package/src/ultraplan/authoring/stages/review.ts +333 -0
- package/src/ultraplan/authoring/stages/scout.ts +188 -0
- package/src/ultraplan/authoring/stages/synthesize.ts +348 -0
- package/src/ultraplan/authoring/storage.ts +594 -0
- package/src/ultraplan/authoring/synth-gate.ts +165 -0
- package/src/ultraplan/authoring-draft.ts +653 -0
- package/src/ultraplan/authoring-persist.ts +180 -0
- package/src/ultraplan/authoring-tool.ts +608 -0
- package/src/ultraplan/authoring-wizard.ts +587 -0
- package/src/ultraplan/batch/merge.ts +98 -0
- package/src/ultraplan/batch/planner.ts +150 -0
- package/src/ultraplan/batch/presenter.ts +97 -0
- package/src/ultraplan/batch/storage.ts +420 -0
- package/src/ultraplan/batch/supervisor.ts +317 -0
- package/src/ultraplan/batch/worker.ts +26 -0
- package/src/ultraplan/batch/worktree.ts +110 -0
- package/src/ultraplan/contracts.ts +1593 -0
- package/src/ultraplan/default-agents/authoring/discoverer.md +12 -0
- package/src/ultraplan/default-agents/authoring/intake.md +12 -0
- package/src/ultraplan/default-agents/authoring/planner.md +12 -0
- package/src/ultraplan/default-agents/authoring/researcher.md +12 -0
- package/src/ultraplan/default-agents/authoring/scope-checker.md +12 -0
- package/src/ultraplan/default-agents/authoring/scout.md +12 -0
- package/src/ultraplan/default-agents/authoring/structure-checker.md +12 -0
- package/src/ultraplan/default-agents/authoring/tdd-checker.md +12 -0
- package/src/ultraplan/default-agents/backend-domain-reviewer.md +10 -0
- package/src/ultraplan/default-agents/backend-executor.md +10 -0
- package/src/ultraplan/default-agents/backend-stack-reviewer.md +10 -0
- package/src/ultraplan/default-agents/backend-tester.md +10 -0
- package/src/ultraplan/default-agents/frontend-domain-reviewer.md +10 -0
- package/src/ultraplan/default-agents/frontend-executor.md +10 -0
- package/src/ultraplan/default-agents/frontend-stack-reviewer.md +10 -0
- package/src/ultraplan/default-agents/frontend-tester.md +10 -0
- package/src/ultraplan/default-agents/infrastructure-domain-reviewer.md +10 -0
- package/src/ultraplan/default-agents/infrastructure-executor.md +10 -0
- package/src/ultraplan/default-agents/infrastructure-stack-reviewer.md +10 -0
- package/src/ultraplan/default-agents/infrastructure-tester.md +10 -0
- package/src/ultraplan/execution/contract.ts +71 -0
- package/src/ultraplan/execution/policy.ts +217 -0
- package/src/ultraplan/execution/runtime-tools.ts +107 -0
- package/src/ultraplan/execution/session-runner.ts +281 -0
- package/src/ultraplan/next-router.ts +85 -0
- package/src/ultraplan/presenter.ts +359 -0
- package/src/ultraplan/project-paths.ts +342 -0
- package/src/ultraplan/runtime/active-execution.ts +72 -0
- package/src/ultraplan/runtime/apply-mutation.ts +416 -0
- package/src/ultraplan/runtime/blockers.ts +243 -0
- package/src/ultraplan/runtime/hook-bridge.ts +486 -0
- package/src/ultraplan/runtime/launch-context.ts +207 -0
- package/src/ultraplan/runtime/migration.ts +524 -0
- package/src/ultraplan/runtime/normalize.ts +281 -0
- package/src/ultraplan/runtime/proof.ts +260 -0
- package/src/ultraplan/runtime/reducer.ts +416 -0
- package/src/ultraplan/runtime/repair.ts +251 -0
- package/src/ultraplan/runtime/tracker-storage.ts +368 -0
- package/src/ultraplan/session-selection.ts +291 -0
- package/src/ultraplan/storage.ts +374 -0
- package/src/utils/editor.ts +38 -0
- package/src/utils/executable.ts +80 -0
- package/src/utils/paths.ts +1 -20
- package/src/utils/shell.ts +31 -0
- package/src/visual/companion.ts +2 -1
- package/src/visual/scripts/frame-template.html +60 -0
- package/src/visual/scripts/index.js +59 -13
- package/src/visual/scripts/package.json +3 -0
- package/src/visual/start-server.ts +2 -1
- package/src/workspace/git-scope.ts +64 -0
- package/src/workspace/locks.ts +23 -0
- package/src/workspace/package-manager.ts +117 -0
- package/src/workspace/path-mapping.ts +75 -0
- package/src/workspace/project-slug.ts +92 -0
- package/src/workspace/repo-root.ts +137 -0
- package/src/workspace/selector.ts +115 -0
- package/src/workspace/state-paths.ts +118 -0
- package/src/workspace/targets.ts +313 -0
- package/src/fix-pr/scripts/diff-comments.sh +0 -33
- package/src/fix-pr/scripts/fetch-pr-comments.sh +0 -25
- package/src/fix-pr/scripts/trigger-review.sh +0 -36
- package/src/fix-pr/scripts/wait-and-check.sh +0 -37
- package/src/qa/scripts/detect-app-type.sh +0 -68
- package/src/qa/scripts/discover-routes.sh +0 -143
- package/src/qa/scripts/run-e2e-tests.sh +0 -131
- package/src/qa/scripts/start-dev-server.sh +0 -46
- package/src/qa/scripts/stop-dev-server.sh +0 -36
- package/src/review/prompts/fix-output-schema.md +0 -18
- package/src/review/prompts/review-output-schema.md +0 -38
- package/src/review/template.ts +0 -15
- /package/src/{review → ai}/prompts/invalid-output-retry.md +0 -0
|
@@ -47,10 +47,22 @@ export function isBashSearchCommand(command: unknown): boolean {
|
|
|
47
47
|
return BASH_SEARCH_PATTERNS.some((p) => p.test(command));
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
/** Check if a Read call is a full-file read (no limit/offset/
|
|
50
|
+
/** Check if a Read call is a full-file read (no limit/offset/path selector = likely analysis, not edit prep) */
|
|
51
51
|
export function isFullFileRead(input: Record<string, unknown> | undefined): boolean {
|
|
52
52
|
if (!input) return true;
|
|
53
|
-
return input.limit == null && input.offset == null && input
|
|
53
|
+
return input.limit == null && input.offset == null && !hasEmbeddedReadSelector(input);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const READ_PATH_SELECTOR_RE = /:(?:raw|\d+(?:[-+]\d+)?|L\d+(?:-L?\d+|\+L?\d+)?)$/i;
|
|
57
|
+
|
|
58
|
+
function getReadPath(input: Record<string, unknown>): string | null {
|
|
59
|
+
const path = input.path ?? input.file_path;
|
|
60
|
+
return typeof path === "string" && path.length > 0 ? path : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function hasEmbeddedReadSelector(input: Record<string, unknown>): boolean {
|
|
64
|
+
const path = getReadPath(input);
|
|
65
|
+
return path != null && READ_PATH_SELECTOR_RE.test(path);
|
|
54
66
|
}
|
|
55
67
|
|
|
56
68
|
/** Block result returned by routing functions */
|
|
@@ -63,33 +75,35 @@ export interface BlockResult {
|
|
|
63
75
|
export function routeToolCall(
|
|
64
76
|
toolName: string,
|
|
65
77
|
input: Record<string, unknown> | undefined,
|
|
66
|
-
|
|
78
|
+
status: ContextModeStatus,
|
|
67
79
|
options: { enforceRouting: boolean; blockHttpCommands: boolean },
|
|
68
80
|
): BlockResult | undefined {
|
|
69
|
-
|
|
81
|
+
const searchReplacement = getSearchReplacement(status);
|
|
82
|
+
const shellSearchReplacement = getShellSearchReplacement(status);
|
|
83
|
+
const fetchReplacement = status.tools.ctxFetchAndIndex ? "ctx_fetch_and_index" : null;
|
|
84
|
+
const bashHttpReplacement = getBashHttpReplacement(status);
|
|
70
85
|
|
|
71
|
-
//
|
|
72
|
-
if (options.enforceRouting && toolName === "
|
|
86
|
+
// Search → block only when an active search/shell replacement exists.
|
|
87
|
+
if (options.enforceRouting && toolName === "search" && searchReplacement) {
|
|
73
88
|
return {
|
|
74
89
|
block: true,
|
|
75
|
-
reason:
|
|
76
|
-
'Use ctx_search(queries: ["<pattern>"]) or ctx_batch_execute instead of Grep. ' +
|
|
77
|
-
"Results are indexed and compressed to save context window.",
|
|
90
|
+
reason: formatSearchReplacementReason(searchReplacement),
|
|
78
91
|
};
|
|
79
92
|
}
|
|
80
93
|
|
|
81
|
-
// Find/Glob → block
|
|
82
|
-
if (options.enforceRouting && toolName === "find") {
|
|
94
|
+
// Find/Glob → block only when an active shell/search replacement exists.
|
|
95
|
+
if (options.enforceRouting && toolName === "find" && shellSearchReplacement) {
|
|
83
96
|
return {
|
|
84
97
|
block: true,
|
|
85
98
|
reason:
|
|
86
|
-
|
|
87
|
-
|
|
99
|
+
shellSearchReplacement === "ctx_execute"
|
|
100
|
+
? 'Use ctx_execute(language: "shell", code: "find ...") or ctx_batch_execute instead of Find/Glob. Results are indexed and compressed to save context window.'
|
|
101
|
+
: "Use ctx_batch_execute instead of Find/Glob. Results are indexed and compressed to save context window.",
|
|
88
102
|
};
|
|
89
103
|
}
|
|
90
104
|
|
|
91
|
-
// Fetch/WebFetch → block
|
|
92
|
-
if (toolName === "fetch" || toolName === "web_fetch") {
|
|
105
|
+
// Fetch/WebFetch → block only when ctx_fetch_and_index is active.
|
|
106
|
+
if ((toolName === "fetch" || toolName === "web_fetch") && fetchReplacement) {
|
|
93
107
|
return {
|
|
94
108
|
block: true,
|
|
95
109
|
reason:
|
|
@@ -102,26 +116,85 @@ export function routeToolCall(
|
|
|
102
116
|
if (toolName === "bash") {
|
|
103
117
|
const command = input?.command;
|
|
104
118
|
|
|
105
|
-
// Bash search commands → block
|
|
106
|
-
if (options.enforceRouting && isBashSearchCommand(command)) {
|
|
119
|
+
// Bash search commands → block only when an active shell/search replacement exists.
|
|
120
|
+
if (options.enforceRouting && isBashSearchCommand(command) && shellSearchReplacement) {
|
|
107
121
|
return {
|
|
108
122
|
block: true,
|
|
109
123
|
reason:
|
|
110
|
-
|
|
111
|
-
|
|
124
|
+
shellSearchReplacement === "ctx_execute"
|
|
125
|
+
? 'Use ctx_execute(language: "shell", code: "<command>") instead of Bash for search commands. For multiple commands, use ctx_batch_execute. Results stay in sandbox and are auto-indexed.'
|
|
126
|
+
: "Use ctx_batch_execute instead of Bash for search commands. Results stay in sandbox and are auto-indexed.",
|
|
112
127
|
};
|
|
113
128
|
}
|
|
114
129
|
|
|
115
|
-
// Bash HTTP commands → block
|
|
116
|
-
if (options.blockHttpCommands && isHttpCommand(command)) {
|
|
130
|
+
// Bash HTTP commands → block only when an active HTTP replacement exists.
|
|
131
|
+
if (options.blockHttpCommands && isHttpCommand(command) && bashHttpReplacement) {
|
|
117
132
|
return {
|
|
118
133
|
block: true,
|
|
119
|
-
reason:
|
|
120
|
-
"Use ctx_fetch_and_index instead of curl/wget. " +
|
|
121
|
-
"It fetches the URL, indexes the content, and returns a compressed summary.",
|
|
134
|
+
reason: formatBashHttpReplacementReason(bashHttpReplacement),
|
|
122
135
|
};
|
|
123
136
|
}
|
|
124
137
|
}
|
|
125
138
|
|
|
126
139
|
return undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function getSearchReplacement(status: ContextModeStatus): "ctx_search" | "ctx_batch_execute" | "ctx_execute" | null {
|
|
143
|
+
if (status.tools.ctxSearch) return "ctx_search";
|
|
144
|
+
if (status.tools.ctxBatchExecute) return "ctx_batch_execute";
|
|
145
|
+
if (status.tools.ctxExecute) return "ctx_execute";
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function getShellSearchReplacement(status: ContextModeStatus): "ctx_execute" | "ctx_batch_execute" | null {
|
|
150
|
+
if (status.tools.ctxExecute) return "ctx_execute";
|
|
151
|
+
if (status.tools.ctxBatchExecute) return "ctx_batch_execute";
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function getBashHttpReplacement(status: ContextModeStatus): "ctx_fetch_and_index" | "ctx_execute" | null {
|
|
156
|
+
if (status.tools.ctxFetchAndIndex) return "ctx_fetch_and_index";
|
|
157
|
+
if (status.tools.ctxExecute) return "ctx_execute";
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Native host tools that are fully shadowed by an active ctx_* replacement.
|
|
163
|
+
*
|
|
164
|
+
* When `enforceRouting` is on, these tools should be hidden from the model's
|
|
165
|
+
* active tool catalog (via `setActiveTools`) so the LLM never tries to call
|
|
166
|
+
* them only to receive a routing-block error. The `routeToolCall` runtime
|
|
167
|
+
* block remains as a safety net for hosts that cannot filter the tool list.
|
|
168
|
+
*
|
|
169
|
+
* Bash is intentionally NOT included: it is needed for non-search shell work,
|
|
170
|
+
* and `routeToolCall` already blocks only the search/HTTP subset.
|
|
171
|
+
*/
|
|
172
|
+
export function getShadowedNativeTools(status: ContextModeStatus): string[] {
|
|
173
|
+
const shadowed: string[] = [];
|
|
174
|
+
if (getSearchReplacement(status)) shadowed.push("search");
|
|
175
|
+
if (getShellSearchReplacement(status)) shadowed.push("find");
|
|
176
|
+
if (status.tools.ctxFetchAndIndex) {
|
|
177
|
+
shadowed.push("fetch", "web_fetch");
|
|
178
|
+
}
|
|
179
|
+
return shadowed;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function formatSearchReplacementReason(replacement: "ctx_search" | "ctx_batch_execute" | "ctx_execute"): string {
|
|
183
|
+
if (replacement === "ctx_search") {
|
|
184
|
+
return 'Use active ctx_search(queries: ["<pattern>"]) instead of Search. Results are indexed and compressed to save context window.';
|
|
185
|
+
}
|
|
186
|
+
if (replacement === "ctx_batch_execute") {
|
|
187
|
+
return "Use active ctx_batch_execute instead of Search. Results are indexed and compressed to save context window.";
|
|
188
|
+
}
|
|
189
|
+
return 'Use active ctx_execute(language: "shell", code: "grep ...") instead of Search. Results stay in sandbox to save context window.';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function formatBashHttpReplacementReason(replacement: "ctx_fetch_and_index" | "ctx_execute"): string {
|
|
193
|
+
if (replacement === "ctx_fetch_and_index") {
|
|
194
|
+
return (
|
|
195
|
+
"Use ctx_fetch_and_index instead of curl/wget. " +
|
|
196
|
+
"It fetches the URL, indexes the content, and returns a compressed summary."
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
return 'Use ctx_execute(language: "shell", code: "<http request>") instead of Bash HTTP commands. Only printed summaries enter context.';
|
|
127
200
|
}
|
|
@@ -5,10 +5,14 @@ export interface LanguageRunner {
|
|
|
5
5
|
compileCmd?: (srcPath: string, outPath: string) => string[];
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
function getPythonBinary(): string[] {
|
|
9
|
+
return process.platform === "win32" ? ["python"] : ["python3"];
|
|
10
|
+
}
|
|
11
|
+
|
|
8
12
|
const RUNNERS: Record<string, LanguageRunner> = {
|
|
9
13
|
javascript: { binary: ["bun", "run"], fileExt: ".js" },
|
|
10
14
|
typescript: { binary: ["bun", "run"], fileExt: ".ts" },
|
|
11
|
-
python: { binary:
|
|
15
|
+
python: { binary: getPythonBinary(), fileExt: ".py" },
|
|
12
16
|
shell: { binary: ["bash"], fileExt: ".sh" },
|
|
13
17
|
ruby: { binary: ["ruby"], fileExt: ".rb" },
|
|
14
18
|
go: { binary: ["go", "run"], fileExt: ".go" },
|
|
@@ -19,6 +19,14 @@ function escapeXML(str: string): string {
|
|
|
19
19
|
.replace(/'/g, "'");
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
function latestFirst(events: TrackedEvent[]): TrackedEvent[] {
|
|
23
|
+
return [...events].sort((a, b) => {
|
|
24
|
+
const byTimestamp = b.timestamp - a.timestamp;
|
|
25
|
+
if (byTimestamp !== 0) return byTimestamp;
|
|
26
|
+
return (b.id ?? 0) - (a.id ?? 0);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
22
30
|
interface SnapshotOpts {
|
|
23
31
|
compactCount?: number;
|
|
24
32
|
searchTool?: string;
|
|
@@ -81,22 +89,47 @@ function buildReferenceSnapshot(eventStore: EventStore, sessionId: string, opts:
|
|
|
81
89
|
}
|
|
82
90
|
|
|
83
91
|
// --- files ---
|
|
84
|
-
const fileEvents = eventStore.getEvents(sessionId, { categories: ["file"], limit: 200 });
|
|
92
|
+
const fileEvents = latestFirst(eventStore.getEvents(sessionId, { categories: ["file"], limit: 200 }));
|
|
85
93
|
if (fileEvents.length > 0) {
|
|
86
94
|
const edited = new Set<string>();
|
|
87
95
|
const read = new Set<string>();
|
|
96
|
+
const modSeen = new Set<string>();
|
|
97
|
+
const readSeen = new Set<string>();
|
|
98
|
+
let maskedStale = 0;
|
|
88
99
|
for (const f of fileEvents) {
|
|
89
100
|
const data = safeParse(f.data);
|
|
90
101
|
const p = typeof data?.path === "string" ? data.path : null;
|
|
91
102
|
if (!p) continue;
|
|
92
|
-
|
|
93
|
-
|
|
103
|
+
const op = data?.op;
|
|
104
|
+
const sourceKey = typeof data?.sourceHash === "string" ? data.sourceHash : p.replace(/\\/g, "/");
|
|
105
|
+
if (op === "edit" || op === "write") {
|
|
106
|
+
if (modSeen.has(sourceKey)) {
|
|
107
|
+
maskedStale += 1;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
modSeen.add(sourceKey);
|
|
111
|
+
edited.add(p);
|
|
112
|
+
} else if (op === "read") {
|
|
113
|
+
if (modSeen.has(sourceKey)) {
|
|
114
|
+
maskedStale += 1;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (readSeen.has(sourceKey)) {
|
|
118
|
+
maskedStale += 1;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
readSeen.add(sourceKey);
|
|
122
|
+
read.add(p);
|
|
123
|
+
}
|
|
94
124
|
}
|
|
125
|
+
// Modifications dominate: do not double-list a path that was both edited and read.
|
|
126
|
+
for (const p of edited) read.delete(p);
|
|
95
127
|
if (edited.size > 0 || read.size > 0) {
|
|
96
128
|
sections.push("");
|
|
97
|
-
sections.push(` <files count="${edited.size + read.size}">`);
|
|
129
|
+
sections.push(` <files count="${edited.size + read.size}" stale_masked="${maskedStale}">`);
|
|
98
130
|
if (edited.size > 0) sections.push(` Edited: ${[...edited].map(escapeXML).join(", ")}`);
|
|
99
131
|
if (read.size > 0) sections.push(` Read: ${[...read].map(escapeXML).join(", ")}`);
|
|
132
|
+
if (maskedStale > 0) sections.push(` Masked stale observations: ${maskedStale}`);
|
|
100
133
|
const queryPaths = [...edited, ...read].slice(0, 5);
|
|
101
134
|
sections.push(` For full details:`);
|
|
102
135
|
sections.push(` ctx_search(queries: [${queryPaths.map((p) => `"${escapeXML(p)}"`).join(", ")}], source: "session-events")`);
|
|
@@ -288,18 +321,28 @@ function buildFallbackSnapshot(eventStore: EventStore, sessionId: string): strin
|
|
|
288
321
|
}
|
|
289
322
|
|
|
290
323
|
// Files modified (write/edit only, deduplicated)
|
|
291
|
-
const fileEvents = eventStore.getEvents(sessionId, { categories: ["file"], limit: 200 });
|
|
324
|
+
const fileEvents = latestFirst(eventStore.getEvents(sessionId, { categories: ["file"], limit: 200 }));
|
|
292
325
|
const modifiedPaths = new Set<string>();
|
|
326
|
+
const seenSources = new Set<string>();
|
|
327
|
+
let maskedStaleFiles = 0;
|
|
293
328
|
for (const f of fileEvents) {
|
|
294
329
|
const data = safeParse(f.data);
|
|
295
|
-
|
|
296
|
-
|
|
330
|
+
const p = typeof data?.path === "string" ? data.path : null;
|
|
331
|
+
if (!p) continue;
|
|
332
|
+
if (data?.op !== "edit" && data?.op !== "write") continue;
|
|
333
|
+
const sourceKey = typeof data?.sourceHash === "string" ? data.sourceHash : p.replace(/\\/g, "/");
|
|
334
|
+
if (seenSources.has(sourceKey)) {
|
|
335
|
+
maskedStaleFiles += 1;
|
|
336
|
+
continue;
|
|
297
337
|
}
|
|
338
|
+
seenSources.add(sourceKey);
|
|
339
|
+
modifiedPaths.add(p);
|
|
298
340
|
}
|
|
299
341
|
if (modifiedPaths.size > 0) {
|
|
300
342
|
sections.push(" <files_modified>");
|
|
301
343
|
const paths = [...modifiedPaths].slice(0, CAPS.files);
|
|
302
344
|
for (const p of paths) sections.push(` - ${escapeXML(p)}`);
|
|
345
|
+
if (maskedStaleFiles > 0) sections.push(` - stale observations masked: ${maskedStaleFiles}`);
|
|
303
346
|
sections.push(" </files_modified>");
|
|
304
347
|
}
|
|
305
348
|
|
|
@@ -346,11 +389,63 @@ function safeParse(json: string): Record<string, unknown> | null {
|
|
|
346
389
|
function extractTaskContent(data: Record<string, unknown> | null): string | null {
|
|
347
390
|
if (!data?.input) return null;
|
|
348
391
|
const input = data.input as Record<string, unknown>;
|
|
349
|
-
if (Array.isArray(input.ops))
|
|
350
|
-
|
|
351
|
-
|
|
392
|
+
if (!Array.isArray(input.ops)) return JSON.stringify(input).slice(0, 100);
|
|
393
|
+
|
|
394
|
+
const parts: string[] = [];
|
|
395
|
+
for (const rawOp of input.ops as Array<Record<string, unknown>>) {
|
|
396
|
+
if (!rawOp || typeof rawOp !== "object") continue;
|
|
397
|
+
const verb = typeof rawOp.op === "string" ? rawOp.op : "task";
|
|
398
|
+
|
|
399
|
+
if (verb === "init" && Array.isArray(rawOp.list)) {
|
|
400
|
+
for (const phase of rawOp.list as Array<Record<string, unknown>>) {
|
|
401
|
+
const items = Array.isArray(phase?.items) ? phase.items : [];
|
|
402
|
+
for (const item of items) {
|
|
403
|
+
if (typeof item === "string" && item) parts.push(`init: ${item}`);
|
|
404
|
+
else if (item && typeof item === "object" && typeof (item as { label?: unknown }).label === "string") {
|
|
405
|
+
parts.push(`init: ${(item as { label: string }).label}`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
} else if (verb === "replace" && Array.isArray(rawOp.phases)) {
|
|
410
|
+
// Legacy `todo_write` shape (pre-14.5.11): `op:"replace", phases:[{ name, tasks:[{ content }] }]`.
|
|
411
|
+
// Persisted event rows still carry this shape until 7-day retention expires; we keep
|
|
412
|
+
// read-side compatibility here so resume snapshots remain truthful for those rows.
|
|
413
|
+
for (const phase of rawOp.phases as Array<Record<string, unknown>>) {
|
|
414
|
+
const tasks = Array.isArray(phase?.tasks) ? phase.tasks : [];
|
|
415
|
+
for (const task of tasks) {
|
|
416
|
+
const content = task && typeof task === "object" && typeof (task as { content?: unknown }).content === "string"
|
|
417
|
+
? (task as { content: string }).content
|
|
418
|
+
: "";
|
|
419
|
+
if (content) parts.push(`replace: ${content}`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
} else if (verb === "append" && Array.isArray(rawOp.items)) {
|
|
423
|
+
for (const item of rawOp.items) {
|
|
424
|
+
if (typeof item === "string" && item) parts.push(`append: ${item}`);
|
|
425
|
+
else if (item && typeof item === "object" && typeof (item as { label?: unknown }).label === "string") {
|
|
426
|
+
// Legacy append items shaped as objects with `label`.
|
|
427
|
+
parts.push(`append: ${(item as { label: string }).label}`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
} else if (verb === "note") {
|
|
431
|
+
const text = typeof rawOp.text === "string" ? rawOp.text : "";
|
|
432
|
+
if (text) parts.push(`note: ${text}`);
|
|
433
|
+
} else {
|
|
434
|
+
const legacyContent = typeof rawOp.content === "string" ? rawOp.content : "";
|
|
435
|
+
if (legacyContent) {
|
|
436
|
+
parts.push(`${verb}: ${legacyContent}`);
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const target = (typeof rawOp.task === "string" && rawOp.task)
|
|
441
|
+
|| (typeof rawOp.phase === "string" && rawOp.phase)
|
|
442
|
+
|| "all";
|
|
443
|
+
parts.push(`${verb}: ${target}`);
|
|
444
|
+
}
|
|
352
445
|
}
|
|
353
|
-
|
|
446
|
+
|
|
447
|
+
// Preserve the existing 100-char cap so prompts stay bounded.
|
|
448
|
+
return parts.length > 0 ? parts.join("; ").slice(0, 100) : null;
|
|
354
449
|
}
|
|
355
450
|
|
|
356
451
|
function formatErrorSummary(data: Record<string, unknown> | null): string | null {
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// src/context-mode/source-hash.ts
|
|
2
|
+
//
|
|
3
|
+
// Compute the per-row `unique_source_hash` for L1 metrics. The hash exists
|
|
4
|
+
// to power "unique-source share" without storing raw paths or commands. Two
|
|
5
|
+
// invariants:
|
|
6
|
+
//
|
|
7
|
+
// 1. The same logical source produces the same hash regardless of platform
|
|
8
|
+
// separator or relative-vs-absolute spelling, given the same `cwd`.
|
|
9
|
+
// 2. The same path under two different `projectSlug` values produces
|
|
10
|
+
// different hashes, so a hash is never a cross-project identifier.
|
|
11
|
+
//
|
|
12
|
+
// Tool inputs are never copied verbatim; only canonicalized prefixes flow
|
|
13
|
+
// into the hash input.
|
|
14
|
+
|
|
15
|
+
import { createHash } from "node:crypto";
|
|
16
|
+
|
|
17
|
+
import { canonicalToolName } from "./tool-name.js";
|
|
18
|
+
|
|
19
|
+
export interface UniqueSourceHashOpts {
|
|
20
|
+
tool: string;
|
|
21
|
+
input: Record<string, unknown> | undefined;
|
|
22
|
+
cwd: string;
|
|
23
|
+
projectSlug: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Detect whether a path string is absolute on either POSIX or Windows.
|
|
28
|
+
*
|
|
29
|
+
* Recognized absolute forms:
|
|
30
|
+
* - leading `/` (POSIX)
|
|
31
|
+
* - drive-letter prefix `[A-Za-z]:` followed by `\` or `/` (Windows)
|
|
32
|
+
* - UNC prefix `\\` or `//` (network share / WSL `\\?\` namespace)
|
|
33
|
+
*/
|
|
34
|
+
function isAbsolutePath(p: string): boolean {
|
|
35
|
+
if (p.length === 0) return false;
|
|
36
|
+
if (p[0] === "/" || p[0] === "\\") {
|
|
37
|
+
// Includes both POSIX `/x` and Windows `\\server\share`
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
// Drive-letter Windows path: `C:\…` or `c:/…`
|
|
41
|
+
if (/^[A-Za-z]:[\\/]/.test(p)) return true;
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Normalize separators and collapse `.` / `..` segments using POSIX rules. */
|
|
46
|
+
function toPosix(p: string): string {
|
|
47
|
+
return p.replace(/\\/g, "/");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function stripCurrentDirPrefixBeforeWindowsAbsolute(p: string): string {
|
|
51
|
+
return p.replace(/^\.[\\/]+(?=[A-Za-z]:[\\/])/, "");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
/** Resolve `pathInput` to a canonical absolute POSIX form. */
|
|
56
|
+
export function canonicalizeSourcePath(pathInput: string, cwd: string): string {
|
|
57
|
+
const normalizedInput = stripCurrentDirPrefixBeforeWindowsAbsolute(pathInput);
|
|
58
|
+
const posix = toPosix(normalizedInput);
|
|
59
|
+
if (isAbsolutePath(normalizedInput)) {
|
|
60
|
+
return collapsePosix(posix);
|
|
61
|
+
}
|
|
62
|
+
const cwdPosix = toPosix(cwd);
|
|
63
|
+
return collapsePosix(joinPosix(cwdPosix, posix));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Lightweight POSIX `path.normalize` — segment-by-segment without depending on
|
|
67
|
+
* Node's platform-specific `path.posix.normalize` differences. */
|
|
68
|
+
function collapsePosix(p: string): string {
|
|
69
|
+
const isAbs = p.startsWith("/");
|
|
70
|
+
const parts = p.split("/").filter(Boolean);
|
|
71
|
+
const stack: string[] = [];
|
|
72
|
+
for (const part of parts) {
|
|
73
|
+
if (part === ".") continue;
|
|
74
|
+
if (part === "..") {
|
|
75
|
+
if (stack.length > 0 && stack[stack.length - 1] !== "..") {
|
|
76
|
+
stack.pop();
|
|
77
|
+
} else if (!isAbs) {
|
|
78
|
+
stack.push("..");
|
|
79
|
+
}
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
stack.push(part);
|
|
83
|
+
}
|
|
84
|
+
const joined = stack.join("/");
|
|
85
|
+
return isAbs ? "/" + joined : joined || ".";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function joinPosix(left: string, right: string): string {
|
|
89
|
+
if (left === "") return right;
|
|
90
|
+
if (left.endsWith("/")) return left + right;
|
|
91
|
+
return left + "/" + right;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function sha256Hex(input: string): string {
|
|
95
|
+
return createHash("sha256").update(input).digest("hex");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Truncate a bash command to a stable, leak-free identifier:
|
|
100
|
+
* - split on whitespace, take the first 4 tokens, lowercase them, rejoin.
|
|
101
|
+
*
|
|
102
|
+
* Anything past the 4th token (env exports, secrets, file paths, command
|
|
103
|
+
* arguments) is dropped. The first 4 tokens give us enough resolution to
|
|
104
|
+
* distinguish `bun run typecheck` from `bun run test` while never copying
|
|
105
|
+
* the rest of the command into the hash input.
|
|
106
|
+
*/
|
|
107
|
+
function truncateBashCommand(command: string): string {
|
|
108
|
+
return command
|
|
109
|
+
.trim()
|
|
110
|
+
.split(/\s+/)
|
|
111
|
+
.slice(0, 4)
|
|
112
|
+
.map((token) => token.toLowerCase())
|
|
113
|
+
.join(" ");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Produce the unique-source hash, or `null` for tools whose inputs do not
|
|
118
|
+
* map to a stable source identifier.
|
|
119
|
+
*/
|
|
120
|
+
export function uniqueSourceHash(opts: UniqueSourceHashOpts): string | null {
|
|
121
|
+
const { tool, input, cwd, projectSlug } = opts;
|
|
122
|
+
const canonical = canonicalToolName(tool);
|
|
123
|
+
|
|
124
|
+
switch (canonical) {
|
|
125
|
+
case "read":
|
|
126
|
+
case "open": {
|
|
127
|
+
const p = typeof input?.path === "string" ? input.path : null;
|
|
128
|
+
if (!p) return null;
|
|
129
|
+
const absolute = canonicalizeSourcePath(p, cwd);
|
|
130
|
+
return sha256Hex(`file:${absolute}:${projectSlug}`);
|
|
131
|
+
}
|
|
132
|
+
case "search": {
|
|
133
|
+
const paths = Array.isArray(input?.paths)
|
|
134
|
+
? (input.paths as unknown[]).filter((p): p is string => typeof p === "string")
|
|
135
|
+
: [];
|
|
136
|
+
const pattern = typeof input?.pattern === "string" ? input.pattern : "";
|
|
137
|
+
if (paths.length === 0) {
|
|
138
|
+
// Pattern-only search (no scope): keep a deterministic salt so distinct
|
|
139
|
+
// pattern-only calls dedup correctly per-project.
|
|
140
|
+
return sha256Hex(`search:${pattern}:${projectSlug}`);
|
|
141
|
+
}
|
|
142
|
+
// Order matters: OMP runs each path under root-level resolution, so [a,b] != [b,a].
|
|
143
|
+
// Use SOH (\u0001) as the joiner — it cannot appear in a path on any platform.
|
|
144
|
+
const joined = paths.map((p) => canonicalizeSourcePath(p, cwd)).join("\u0001");
|
|
145
|
+
return sha256Hex(`search:${joined}:${pattern}:${projectSlug}`);
|
|
146
|
+
}
|
|
147
|
+
case "find": {
|
|
148
|
+
const paths = Array.isArray(input?.paths)
|
|
149
|
+
? (input.paths as unknown[]).filter((p): p is string => typeof p === "string")
|
|
150
|
+
: [];
|
|
151
|
+
if (paths.length === 0) {
|
|
152
|
+
// Defensive: 14.7.x requires `paths`, so this should not happen.
|
|
153
|
+
return sha256Hex(`find::${projectSlug}`);
|
|
154
|
+
}
|
|
155
|
+
const joined = paths.map((p) => canonicalizeSourcePath(p, cwd)).join("\u0001");
|
|
156
|
+
return sha256Hex(`find:${joined}:${projectSlug}`);
|
|
157
|
+
}
|
|
158
|
+
case "edit":
|
|
159
|
+
case "write": {
|
|
160
|
+
const p = typeof input?.path === "string" ? input.path : null;
|
|
161
|
+
if (!p) return null;
|
|
162
|
+
const absolute = canonicalizeSourcePath(p, cwd);
|
|
163
|
+
return sha256Hex(`file:${absolute}:${projectSlug}`);
|
|
164
|
+
}
|
|
165
|
+
case "bash": {
|
|
166
|
+
const command = typeof input?.command === "string" ? input.command : "";
|
|
167
|
+
const truncated = truncateBashCommand(command);
|
|
168
|
+
return sha256Hex(`bash:${truncated}:${projectSlug}`);
|
|
169
|
+
}
|
|
170
|
+
default:
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// src/context-mode/tool-name.ts
|
|
2
|
+
//
|
|
3
|
+
// OMP 14.3.0 renamed `read` to `open` at the wire level while keeping `read`
|
|
4
|
+
// as a legacy alias. Internally we keep using the historical `read` key so
|
|
5
|
+
// every dispatch site only has to learn one name. Apply this normalizer to
|
|
6
|
+
// the raw `tool_call`/`tool_result` `toolName` before any switch.
|
|
7
|
+
|
|
8
|
+
/** Map an OMP tool name to its canonical internal key. */
|
|
9
|
+
export function canonicalToolName(name: string): string {
|
|
10
|
+
return name === "open" ? "read" : name;
|
|
11
|
+
}
|