ultimate-pi 0.18.1 → 0.19.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/.agents/skills/harness-debate-plan/SKILL.md +1 -1
- package/.agents/skills/harness-decisions/SKILL.md +1 -2
- package/.agents/skills/harness-governor/SKILL.md +6 -5
- package/.pi/PACKAGING.md +4 -4
- package/.pi/SYSTEM.md +54 -120
- package/.pi/agents/harness/incident-recorder.md +0 -1
- package/.pi/agents/harness/planning/decompose.md +0 -2
- package/.pi/agents/harness/planning/execution-plan-author.md +0 -2
- package/.pi/agents/harness/planning/hypothesis-validator.md +0 -2
- package/.pi/agents/harness/planning/hypothesis.md +0 -2
- package/.pi/agents/harness/planning/implementation-researcher.md +0 -2
- package/.pi/agents/harness/planning/plan-adversary.md +0 -2
- package/.pi/agents/harness/planning/plan-evaluator.md +1 -3
- package/.pi/agents/harness/planning/planning-context.md +0 -2
- package/.pi/agents/harness/planning/review-integrator.md +0 -2
- package/.pi/agents/harness/planning/sprint-contract-auditor.md +0 -2
- package/.pi/agents/harness/planning/stack-researcher.md +0 -2
- package/.pi/agents/harness/reviewing/adversary.md +0 -2
- package/.pi/agents/harness/reviewing/evaluator.md +0 -2
- package/.pi/agents/harness/reviewing/tie-breaker.md +0 -2
- package/.pi/agents/harness/running/executor.md +0 -2
- package/.pi/agents/harness/sentrux-bootstrap.md +0 -1
- package/.pi/agents/harness/sentrux-steward.md +0 -2
- package/.pi/agents/harness/trace-librarian.md +0 -1
- package/.pi/extensions/00-posthog-network-bootstrap.ts +1 -1
- package/.pi/extensions/agt-kill-switch.ts +57 -0
- package/.pi/extensions/agt-prompt-guard.ts +32 -0
- package/.pi/extensions/custom-footer.ts +46 -145
- package/.pi/extensions/custom-header.ts +1 -1
- package/.pi/extensions/custom-system-prompt.ts +1 -1
- package/.pi/extensions/debate-orchestrator.ts +6 -6
- package/.pi/extensions/harness-ask-user.ts +7 -7
- package/.pi/extensions/harness-debate-tools.ts +26 -42
- package/.pi/extensions/harness-lens.ts +94 -0
- package/.pi/extensions/harness-plan-approval.ts +11 -11
- package/.pi/extensions/harness-run-context.ts +1070 -876
- package/.pi/extensions/harness-subagent-governance.ts +8 -0
- package/.pi/extensions/harness-subagent-submit.ts +34 -163
- package/.pi/extensions/harness-subagents.ts +3 -3
- package/.pi/extensions/harness-telemetry.ts +2 -2
- package/.pi/extensions/harness-web-tools.ts +2 -2
- package/.pi/extensions/policy-gate.ts +25 -5
- package/.pi/extensions/sentrux-rules-sync.ts +1 -1
- package/.pi/extensions/subagent-governance.ts +92 -0
- package/.pi/extensions/trace-recorder.ts +1 -1
- package/.pi/extensions/{ultimate-pi-vcc.ts → vcc-compaction.ts} +1 -1
- package/.pi/harness/README.md +6 -2
- package/.pi/harness/agents.manifest.json +22 -25
- package/.pi/harness/agents.policy.yaml +275 -0
- package/.pi/harness/docs/adrs/0030-inhouse-vcc-compaction.md +1 -1
- package/.pi/harness/docs/adrs/0035-plan-phase-review-gate.md +1 -1
- package/.pi/harness/docs/adrs/0045-harness-lens-minimal-contract.md +49 -0
- package/.pi/harness/docs/adrs/0046-agt-policy-engine.md +51 -0
- package/.pi/harness/docs/adrs/0047-agt-layered-security.md +39 -0
- package/.pi/harness/docs/adrs/0048-tool-call-hook-order.md +25 -0
- package/.pi/harness/docs/adrs/0049-agents-policy-manifest.md +36 -0
- package/.pi/harness/docs/adrs/README.md +5 -0
- package/.pi/harness/evolution/README.md +1 -2
- package/.pi/harness/examples/agents.policy.project.yaml +19 -0
- package/.pi/harness/examples/policies/custom-deny-bash.yaml +9 -0
- package/.pi/harness/policies/bash-denylists.yaml +5 -0
- package/.pi/harness/policies/defaults.yaml +51 -0
- package/.pi/harness/policies/orchestrator.yaml +18 -0
- package/.pi/harness/policies/phases.yaml +10 -0
- package/.pi/harness/policies/roles.yaml +5 -0
- package/.pi/harness/policies/web-guard.yaml +5 -0
- package/.pi/harness/policies/workflow-sequences.yaml +9 -0
- package/.pi/harness/sentrux/architecture.manifest.json +26 -4
- package/.pi/harness/specs/observation.schema.json +2 -1
- package/.pi/lib/agents-policy.d.mts +70 -0
- package/.pi/lib/agents-policy.mjs +325 -0
- package/.pi/lib/agents-policy.ts +19 -0
- package/.pi/lib/agt/audit-run-sink.ts +52 -0
- package/.pi/lib/agt/build-evaluation-context.ts +285 -0
- package/.pi/lib/agt/config.ts +28 -0
- package/.pi/lib/agt/delegation.ts +69 -0
- package/.pi/lib/agt/evaluate-policy.ts +56 -0
- package/.pi/lib/agt/identity-registry.ts +41 -0
- package/.pi/lib/agt/index.ts +55 -0
- package/.pi/lib/agt/kill-switch-state.ts +11 -0
- package/.pi/lib/agt/legacy-evaluate.ts +101 -0
- package/.pi/lib/agt/policy-engine.ts +154 -0
- package/.pi/lib/agt/rings.ts +21 -0
- package/.pi/lib/agt/sre-hooks.ts +45 -0
- package/.pi/lib/agt/trust-run-store.ts +26 -0
- package/.pi/lib/agt/workflow-history.ts +29 -0
- package/.pi/lib/agt-governance-active.ts +14 -0
- package/.pi/lib/agt-tool-guard.ts +78 -0
- package/.pi/lib/ask-user/dialog.ts +314 -0
- package/.pi/{extensions/lib → lib}/debate-bus-core.ts +10 -10
- package/.pi/{extensions/lib → lib}/debate-bus-state.ts +1 -1
- package/.pi/{extensions/lib → lib}/extension-load-guard.ts +13 -2
- package/.pi/lib/harness-agt-tool-guard.ts +5 -0
- package/.pi/{extensions/lib → lib}/harness-artifact-gate.ts +1 -1
- package/.pi/lib/harness-debate-core-deps.ts +14 -0
- package/.pi/lib/harness-debate-workflow-deps.ts +43 -0
- package/.pi/lib/harness-lens/.gitattributes +1 -0
- package/.pi/lib/harness-lens/clients/edit-autopatch.ts +88 -0
- package/.pi/lib/harness-lens/clients/file-kinds.ts +380 -0
- package/.pi/lib/harness-lens/clients/file-time.ts +215 -0
- package/.pi/lib/harness-lens/clients/file-utils.ts +484 -0
- package/.pi/lib/harness-lens/clients/format-service.ts +276 -0
- package/.pi/lib/harness-lens/clients/formatters.ts +1000 -0
- package/.pi/lib/harness-lens/clients/git-guard.ts +31 -0
- package/.pi/lib/harness-lens/clients/indent-retarget.ts +90 -0
- package/.pi/lib/harness-lens/clients/installer/index.ts +2368 -0
- package/.pi/lib/harness-lens/clients/latency-logger.ts +80 -0
- package/.pi/lib/harness-lens/clients/lens-config.ts +43 -0
- package/.pi/lib/harness-lens/clients/lens-events.ts +164 -0
- package/.pi/lib/harness-lens/clients/lsp/aggregation.ts +91 -0
- package/.pi/lib/harness-lens/clients/lsp/client.ts +1466 -0
- package/.pi/lib/harness-lens/clients/lsp/config.ts +216 -0
- package/.pi/lib/harness-lens/clients/lsp/edits.ts +297 -0
- package/.pi/lib/harness-lens/clients/lsp/index.ts +1355 -0
- package/.pi/lib/harness-lens/clients/lsp/interactive-install.ts +424 -0
- package/.pi/lib/harness-lens/clients/lsp/language.ts +223 -0
- package/.pi/lib/harness-lens/clients/lsp/launch.ts +939 -0
- package/.pi/lib/harness-lens/clients/lsp/lsp-index.ts +11 -0
- package/.pi/lib/harness-lens/clients/lsp/path-utils.ts +12 -0
- package/.pi/lib/harness-lens/clients/lsp/server-strategies.ts +81 -0
- package/.pi/lib/harness-lens/clients/lsp/server.ts +1971 -0
- package/.pi/lib/harness-lens/clients/path-utils.ts +182 -0
- package/.pi/lib/harness-lens/clients/pipeline.ts +360 -0
- package/.pi/lib/harness-lens/clients/project-profile.ts +117 -0
- package/.pi/lib/harness-lens/clients/runtime-agent-end.ts +112 -0
- package/.pi/lib/harness-lens/clients/runtime-config.ts +33 -0
- package/.pi/lib/harness-lens/clients/runtime-coordinator.ts +186 -0
- package/.pi/lib/harness-lens/clients/runtime-tool-result.ts +171 -0
- package/.pi/lib/harness-lens/clients/safe-spawn.ts +339 -0
- package/.pi/lib/harness-lens/clients/secrets-scanner.ts +214 -0
- package/.pi/lib/harness-lens/clients/tool-policy.ts +2072 -0
- package/.pi/lib/harness-lens/clients/types.ts +59 -0
- package/.pi/lib/harness-lens/clients/widget-state.ts +283 -0
- package/.pi/lib/harness-lens/index.ts +532 -0
- package/.pi/lib/harness-lens/tools/lsp-diagnostics.ts +706 -0
- package/.pi/lib/harness-lens/tools/lsp-navigation.ts +1246 -0
- package/.pi/{extensions/lib → lib}/harness-posthog.ts +3 -0
- package/.pi/lib/harness-run-context-responses.ts +9 -0
- package/.pi/lib/harness-run-context.ts +0 -2
- package/.pi/{extensions/lib/spawn-policy.ts → lib/harness-spawn-policy.ts} +1 -0
- package/.pi/{extensions/lib → lib}/harness-spawn-topology.ts +1 -1
- package/.pi/lib/harness-subagent-auth.ts +51 -0
- package/.pi/{extensions/lib → lib}/harness-subagent-precheck.ts +10 -7
- package/.pi/{extensions/lib → lib}/harness-subagent-submit-pipeline.ts +3 -3
- package/.pi/lib/harness-subagent-submit-register.ts +163 -0
- package/.pi/{extensions/lib → lib}/harness-subagent-submit-registry.ts +1 -37
- package/.pi/{extensions/lib → lib}/harness-subagents-bridge.ts +53 -14
- package/.pi/{extensions/lib → lib}/harness-subprocess-bootstrap.ts +1 -1
- package/.pi/{extensions/lib → lib}/plan-approval/create-plan.ts +2 -2
- package/.pi/{extensions/lib → lib}/plan-approval/format-plan.ts +2 -2
- package/.pi/{extensions/lib → lib}/plan-approval/plan-review.ts +162 -201
- package/.pi/{extensions/lib → lib}/plan-approval/render.ts +1 -1
- package/.pi/{extensions/lib → lib}/plan-approval/resolve-disk.ts +2 -2
- package/.pi/{extensions/lib → lib}/plan-approval/types.ts +1 -1
- package/.pi/{extensions/lib → lib}/plan-approval/validate.ts +3 -3
- package/.pi/{extensions/lib → lib}/plan-debate-envelope.ts +1 -1
- package/.pi/{extensions/lib → lib}/plan-debate-gate.ts +1 -1
- package/.pi/{extensions/lib → lib}/plan-debate-lane.ts +1 -4
- package/.pi/{extensions/lib → lib}/plan-messenger.ts +1 -1
- package/.pi/prompts/harness-plan.md +1 -1
- package/.pi/prompts/harness-setup.md +37 -64
- package/.pi/scripts/README.md +2 -5
- package/.pi/scripts/generate-agents-policy-yaml.mjs +148 -0
- package/.pi/scripts/harness-agents-manifest.mjs +60 -3
- package/.pi/scripts/harness-agt-doctor.ts +36 -0
- package/.pi/scripts/harness-cli-verify.sh +9 -2
- package/.pi/scripts/harness-verify.mjs +113 -39
- package/.pi/scripts/harness-web-policy-guard.mjs +2 -2
- package/.pi/scripts/validate-plan-dag.mjs +65 -74
- package/.pi/scripts/vendor-pi-vcc-settings.stub.ts +2 -2
- package/.pi/scripts/vendor-sync-pi-vcc.sh +1 -1
- package/.pi/skills/architecture/broker-domain/SKILL.md +65 -0
- package/.pi/skills/architecture/cqrs/SKILL.md +63 -0
- package/.pi/skills/architecture/event-driven/SKILL.md +60 -0
- package/.pi/skills/architecture/hexagonal-ports-adapters/SKILL.md +66 -0
- package/.pi/skills/architecture/layered/SKILL.md +68 -0
- package/.pi/skills/architecture/microkernel/SKILL.md +62 -0
- package/.pi/skills/architecture/microservices/SKILL.md +64 -0
- package/.pi/skills/architecture/modular-monolith/SKILL.md +65 -0
- package/.pi/skills/architecture/orchestration-driven-soa/SKILL.md +61 -0
- package/.pi/skills/architecture/pipeline/SKILL.md +63 -0
- package/.pi/skills/architecture/service-based/SKILL.md +64 -0
- package/.pi/skills/architecture/service-mesh/SKILL.md +60 -0
- package/.pi/skills/architecture/space-based/SKILL.md +60 -0
- package/.pi/skills/ast-grep/SKILL.md +40 -321
- package/.pi/skills/delivery/debugging-discipline/SKILL.md +36 -0
- package/.pi/skills/delivery/documentation-update/SKILL.md +33 -0
- package/.pi/skills/delivery/requirements-to-implementation/SKILL.md +34 -0
- package/.pi/skills/delivery/risk-based-verification/SKILL.md +43 -0
- package/.pi/skills/delivery/tradeoff-analysis/SKILL.md +34 -0
- package/.pi/skills/engineering/api-contract-design/SKILL.md +38 -0
- package/.pi/skills/engineering/cohesion-coupling/SKILL.md +43 -0
- package/.pi/skills/engineering/complexity-control/SKILL.md +31 -0
- package/.pi/skills/engineering/defensive-programming/SKILL.md +38 -0
- package/.pi/skills/engineering/dependency-management/SKILL.md +29 -0
- package/.pi/skills/engineering/domain-modeling/SKILL.md +32 -0
- package/.pi/skills/engineering/error-handling/SKILL.md +37 -0
- package/.pi/skills/engineering/legacy-code-seams/SKILL.md +35 -0
- package/.pi/skills/engineering/naming-and-intent/SKILL.md +29 -0
- package/.pi/skills/engineering/refactoring-safe-evolution/SKILL.md +35 -0
- package/.pi/skills/engineering/routine-function-design/SKILL.md +34 -0
- package/.pi/skills/engineering/small-change-discipline/SKILL.md +35 -0
- package/.pi/skills/lsp-navigation/SKILL.md +89 -0
- package/.pi/skills/quality/code-review-self-check/SKILL.md +35 -0
- package/.pi/skills/quality/privacy-data-handling/SKILL.md +26 -0
- package/.pi/skills/quality/security-review/SKILL.md +34 -0
- package/.pi/skills/quality/test-strategy/SKILL.md +33 -0
- package/.pi/skills/quality/testability-design/SKILL.md +33 -0
- package/.pi/skills/systems/concurrency-safety/SKILL.md +32 -0
- package/.pi/skills/systems/data-modeling-migrations/SKILL.md +31 -0
- package/.pi/skills/systems/observability-instrumentation/SKILL.md +32 -0
- package/.pi/skills/systems/performance-measurement/SKILL.md +35 -0
- package/.pi/skills/systems/reliability-design/SKILL.md +32 -0
- package/.sentrux/rules.toml +20 -4
- package/AGENTS.md +5 -0
- package/CHANGELOG.md +14 -0
- package/README.md +3 -12
- package/THIRD_PARTY_NOTICES.md +12 -21
- package/package.json +15 -7
- package/vendor/pi-subagents/src/agents.ts +45 -1
- package/vendor/pi-subagents/src/subagents.ts +866 -811
- package/vendor/pi-vcc/src/core/brief.ts +68 -99
- package/vendor/pi-vcc/src/core/settings.ts +2 -2
- package/.agents/skills/caveman/SKILL.md +0 -67
- package/.pi/agents/harness/meta-optimizer.md +0 -36
- package/.pi/extensions/lib/ask-user/dialog.ts +0 -260
- package/.pi/extensions/lib/harness-subagent-auth.ts +0 -207
- package/.pi/extensions/lib/harness-subagent-policy.ts +0 -236
- package/.pi/extensions/pi-model-router-harness.ts +0 -42
- package/.pi/harness/evolution/meta-optimizer.mjs +0 -99
- package/.pi/harness/specs/router-tuning-proposal.schema.json +0 -114
- package/.pi/model-router.example.json +0 -36
- package/.pi/prompts/harness-critic.md +0 -10
- package/.pi/prompts/harness-eval.md +0 -10
- package/.pi/prompts/harness-router-tune.md +0 -52
- package/.pi/scripts/harness-generate-model-router.mjs +0 -327
- package/.pi/scripts/harness-model-router-routing.test.mjs +0 -97
- package/.pi/scripts/harness-sync-model-router.mjs +0 -97
- package/.pi/scripts/vendor-sync-pi-model-router.sh +0 -47
- package/vendor/pi-model-router/.prettierignore +0 -4
- package/vendor/pi-model-router/.prettierrc +0 -5
- package/vendor/pi-model-router/AGENTS.md +0 -39
- package/vendor/pi-model-router/LICENSE +0 -21
- package/vendor/pi-model-router/README.md +0 -99
- package/vendor/pi-model-router/UPSTREAM_PIN.md +0 -10
- package/vendor/pi-model-router/docs/ARCHITECTURE.md +0 -54
- package/vendor/pi-model-router/extensions/commands.ts +0 -720
- package/vendor/pi-model-router/extensions/config.ts +0 -348
- package/vendor/pi-model-router/extensions/constants.ts +0 -1
- package/vendor/pi-model-router/extensions/index.ts +0 -478
- package/vendor/pi-model-router/extensions/provider.ts +0 -580
- package/vendor/pi-model-router/extensions/routing.ts +0 -564
- package/vendor/pi-model-router/extensions/state.ts +0 -52
- package/vendor/pi-model-router/extensions/types.ts +0 -95
- package/vendor/pi-model-router/extensions/ui.ts +0 -144
- package/vendor/pi-model-router/model-router.example.json +0 -48
- package/vendor/pi-model-router/package.json +0 -48
- package/vendor/pi-model-router/tsconfig.json +0 -16
- /package/.pi/{prompts → harness/docs}/planning-rubrics.md +0 -0
- /package/.pi/{extensions/lib → lib}/ask-user/fallback.ts +0 -0
- /package/.pi/{extensions/lib → lib}/ask-user/render.ts +0 -0
- /package/.pi/{extensions/lib → lib}/ask-user/schema.ts +0 -0
- /package/.pi/{extensions/lib → lib}/ask-user/types.ts +0 -0
- /package/.pi/{extensions/lib → lib}/ask-user/validate-core.mjs +0 -0
- /package/.pi/{extensions/lib → lib}/ask-user/validate.ts +0 -0
- /package/.pi/{extensions/lib → lib}/harness-cocoindex-refresh.ts +0 -0
- /package/.pi/{extensions/lib → lib}/harness-paths.ts +0 -0
- /package/.pi/{extensions/lib → lib}/harness-spawn-budget.ts +0 -0
- /package/.pi/{extensions/lib → lib}/harness-vcc-settings.ts +0 -0
- /package/.pi/{extensions/lib → lib}/harness-web/run-cli.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-approval/dialog.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-approval/schema.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-approval-readiness.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-debate-eligibility.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-debate-focus.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-debate-id.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-debate-lanes.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-debate-round-status.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-debate-write-guard.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-review-gate.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-review-integrator-rules.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-scope-guard.ts +0 -0
- /package/.pi/{extensions/lib → lib}/posthog-client.ts +0 -0
- /package/.pi/{extensions/lib → lib}/posthog-node.d.ts +0 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path utilities for pi-lens
|
|
3
|
+
*
|
|
4
|
+
* Handles cross-platform path normalization, particularly
|
|
5
|
+
* Windows case-insensitivity issues when using paths as Map keys.
|
|
6
|
+
*
|
|
7
|
+
* Approach (inspired by OpenCode's Filesystem.normalizePath):
|
|
8
|
+
* - On Windows: try realpathSync.native() for canonical casing
|
|
9
|
+
* - Falls back to lowercase for files that don't exist yet
|
|
10
|
+
* - On non-Windows: return path as-is (case-sensitive filesystem)
|
|
11
|
+
* - Always convert backslashes to forward slashes for Map key consistency
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
15
|
+
import { dirname, win32 } from "node:path";
|
|
16
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Detect if a path is a Windows path (has drive letter or UNC prefix).
|
|
20
|
+
*/
|
|
21
|
+
function isWindowsPath(filePath: string): boolean {
|
|
22
|
+
return /^[A-Za-z]:/.test(filePath) || filePath.startsWith("\\\\");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Normalize a file path for consistent Map key usage.
|
|
27
|
+
*
|
|
28
|
+
* On Windows:
|
|
29
|
+
* - If the file exists: uses realpathSync.native() to get the canonical
|
|
30
|
+
* filesystem path (actual casing, resolved symlinks)
|
|
31
|
+
* - If the file doesn't exist: resolves the path and lowercases
|
|
32
|
+
* (needed for new files where we haven't written yet)
|
|
33
|
+
*
|
|
34
|
+
* On non-Windows: returns path as-is (case-sensitive filesystem).
|
|
35
|
+
*
|
|
36
|
+
* Always converts backslashes to forward slashes for consistent Map keys.
|
|
37
|
+
*/
|
|
38
|
+
export function normalizeFilePath(filePath: string): string {
|
|
39
|
+
// Convert backslashes to forward slashes first
|
|
40
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
41
|
+
|
|
42
|
+
if (process.platform !== "win32" && !isWindowsPath(normalized)) {
|
|
43
|
+
return normalized;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Windows: try realpathSync.native() for canonical casing
|
|
47
|
+
// This resolves symlinks and returns the actual filesystem casing
|
|
48
|
+
try {
|
|
49
|
+
const canonical = realpathSync.native(filePath);
|
|
50
|
+
return canonical.replace(/\\/g, "/");
|
|
51
|
+
} catch {
|
|
52
|
+
// File doesn't exist yet (new file) — resolve path and lowercase
|
|
53
|
+
// We need to walk up the directory tree to find the nearest existing
|
|
54
|
+
// parent, resolve its casing, then append the non-existent parts
|
|
55
|
+
try {
|
|
56
|
+
return resolveNonExisting(filePath);
|
|
57
|
+
} catch {
|
|
58
|
+
// Last resort: just lowercase the resolved path
|
|
59
|
+
const resolved = win32.normalize(win32.resolve(filePath));
|
|
60
|
+
return resolved.replace(/\\/g, "/").toLowerCase();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Resolve a non-existing path by finding the nearest existing parent,
|
|
67
|
+
* getting its canonical casing, then appending the non-existent parts lowercased.
|
|
68
|
+
*
|
|
69
|
+
* Example: C:\Users\Foo\newdir\file.ts
|
|
70
|
+
* - C:\Users\Foo exists → realpathSync gives C:\Users\Foo
|
|
71
|
+
* - newdir\file.ts doesn't exist → lowercased
|
|
72
|
+
* - Result: C:/Users/Foo/newdir/file.ts
|
|
73
|
+
*/
|
|
74
|
+
function resolveNonExisting(filePath: string): string {
|
|
75
|
+
const resolved = win32.resolve(filePath);
|
|
76
|
+
let current = resolved;
|
|
77
|
+
const nonExistentParts: string[] = [];
|
|
78
|
+
|
|
79
|
+
// Walk up until we find an existing directory
|
|
80
|
+
while (true) {
|
|
81
|
+
if (existsSync(current)) {
|
|
82
|
+
// Found existing ancestor — get its canonical casing
|
|
83
|
+
const canonical = realpathSync.native(current);
|
|
84
|
+
if (nonExistentParts.length === 0) {
|
|
85
|
+
return canonical.replace(/\\/g, "/");
|
|
86
|
+
}
|
|
87
|
+
// Append non-existent parts (lowercased for consistency)
|
|
88
|
+
const tail = nonExistentParts.reverse().join("/").toLowerCase();
|
|
89
|
+
const base = canonical.replace(/\\/g, "/");
|
|
90
|
+
return base.endsWith("/") ? base + tail : `${base}/${tail}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const parent = dirname(current);
|
|
94
|
+
if (parent === current) {
|
|
95
|
+
// Reached filesystem root without finding existing dir
|
|
96
|
+
// Fall back to full lowercase
|
|
97
|
+
throw new Error("No existing parent found");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
nonExistentParts.push(win32.basename(current));
|
|
101
|
+
current = parent;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Convert a file:// URI to a normalized path.
|
|
107
|
+
* Handles URL decoding and Windows drive letter normalization.
|
|
108
|
+
*/
|
|
109
|
+
export function uriToPath(uri: string): string {
|
|
110
|
+
try {
|
|
111
|
+
const filePath = fileURLToPath(uri);
|
|
112
|
+
return normalizeFilePath(filePath);
|
|
113
|
+
} catch {
|
|
114
|
+
// Not a valid file:// URI, treat as plain path
|
|
115
|
+
return normalizeFilePath(uri);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Convert a path to a file:// URI.
|
|
121
|
+
* Does NOT normalize the path - URIs preserve original casing.
|
|
122
|
+
*/
|
|
123
|
+
export function pathToUri(filePath: string): string {
|
|
124
|
+
return pathToFileURL(filePath).href;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Normalize a Map key lookup for file paths.
|
|
129
|
+
* Use this when getting/setting values in Maps that use file paths as keys.
|
|
130
|
+
*/
|
|
131
|
+
export function normalizeMapKey(filePath: string): string {
|
|
132
|
+
return normalizeFilePath(filePath);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Compare two file paths for equality, handling Windows case-insensitivity
|
|
137
|
+
* and mixed separators (backslash vs forward slash).
|
|
138
|
+
*/
|
|
139
|
+
export function pathsEqual(a: string, b: string): boolean {
|
|
140
|
+
return normalizeFilePath(a) === normalizeFilePath(b);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Check if `child` is under `parent` directory.
|
|
145
|
+
* Separator-agnostic and case-insensitive on Windows.
|
|
146
|
+
*/
|
|
147
|
+
export function isUnderDir(child: string, parent: string): boolean {
|
|
148
|
+
const normChild = normalizeFilePath(child);
|
|
149
|
+
const normParent = normalizeFilePath(parent);
|
|
150
|
+
// Ensure parent ends with / for prefix matching
|
|
151
|
+
const parentPrefix = normParent.endsWith("/") ? normParent : `${normParent}/`;
|
|
152
|
+
return normChild === normParent || normChild.startsWith(parentPrefix);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const VENDOR_DIR_NAMES = new Set([
|
|
156
|
+
"node_modules",
|
|
157
|
+
"vendor",
|
|
158
|
+
"vendors",
|
|
159
|
+
"third_party",
|
|
160
|
+
"third-party",
|
|
161
|
+
]);
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Returns true when a file should be treated as external/vendor and excluded
|
|
165
|
+
* from pipelines (LSP, diagnostics, complexity, etc.).
|
|
166
|
+
*
|
|
167
|
+
* Cases:
|
|
168
|
+
* 1. Outside the project root entirely (e.g. global npm packages, system files)
|
|
169
|
+
* 2. Inside the project but under a vendor directory (node_modules, vendor, third_party, etc.)
|
|
170
|
+
*/
|
|
171
|
+
export function isExternalOrVendorFile(
|
|
172
|
+
filePath: string,
|
|
173
|
+
projectRoot: string,
|
|
174
|
+
): boolean {
|
|
175
|
+
if (!isUnderDir(filePath, projectRoot)) return true;
|
|
176
|
+
const normalized = normalizeFilePath(filePath);
|
|
177
|
+
const rootNorm = normalizeFilePath(projectRoot);
|
|
178
|
+
const rel = normalized.startsWith(rootNorm + "/")
|
|
179
|
+
? normalized.slice(rootNorm.length + 1)
|
|
180
|
+
: normalized;
|
|
181
|
+
return rel.split("/").some((seg) => VENDOR_DIR_NAMES.has(seg));
|
|
182
|
+
}
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-write pipeline for harness lens.
|
|
3
|
+
*
|
|
4
|
+
* The ultimate-pi harness keeps lens focused on edit safety, formatting, LSP sync,
|
|
5
|
+
* and secret blocking.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as nodeFs from "node:fs";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import { detectFileKind, getFileKindLabel } from "./file-kinds.js";
|
|
11
|
+
import type { FormatService } from "./format-service.js";
|
|
12
|
+
import { logLatency } from "./latency-logger.js";
|
|
13
|
+
import { emitLensAnalysisComplete } from "./lens-events.js";
|
|
14
|
+
import { getLSPService } from "./lsp/index.js";
|
|
15
|
+
import { RUNTIME_CONFIG } from "./runtime-config.js";
|
|
16
|
+
import { formatSecrets, scanForSecrets } from "./secrets-scanner.js";
|
|
17
|
+
|
|
18
|
+
const LSP_MAX_FILE_BYTES = RUNTIME_CONFIG.pipeline.lspMaxFileBytes;
|
|
19
|
+
const LSP_MAX_FILE_LINES = RUNTIME_CONFIG.pipeline.lspMaxFileLines;
|
|
20
|
+
const LSP_SPAWN_BUDGET_MS = RUNTIME_CONFIG.pipeline.lspSpawnBudgetMs;
|
|
21
|
+
|
|
22
|
+
export interface PipelineContext {
|
|
23
|
+
filePath: string;
|
|
24
|
+
cwd: string;
|
|
25
|
+
toolName: string;
|
|
26
|
+
modifiedRanges?: { start: number; end: number }[];
|
|
27
|
+
telemetry?: {
|
|
28
|
+
model: string;
|
|
29
|
+
sessionId: string;
|
|
30
|
+
turnIndex: number;
|
|
31
|
+
writeIndex: number;
|
|
32
|
+
};
|
|
33
|
+
getFlag: (name: string) => boolean | string | undefined;
|
|
34
|
+
dbg: (msg: string) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface PipelineDeps {
|
|
38
|
+
getFormatService: () => FormatService;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface PipelineResult {
|
|
42
|
+
output: string;
|
|
43
|
+
hasBlockers: boolean;
|
|
44
|
+
isError: boolean;
|
|
45
|
+
fileModified: boolean;
|
|
46
|
+
changedFiles?: string[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface PhaseTracker {
|
|
50
|
+
start(name: string): void;
|
|
51
|
+
end(name: string, metadata?: Record<string, unknown>): void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type SecretDiagnostic = {
|
|
55
|
+
id: string;
|
|
56
|
+
message: string;
|
|
57
|
+
filePath: string;
|
|
58
|
+
line: number;
|
|
59
|
+
column: number;
|
|
60
|
+
severity: "error";
|
|
61
|
+
semantic: "blocking";
|
|
62
|
+
tool: "secrets-scanner";
|
|
63
|
+
rule: "secrets";
|
|
64
|
+
defectClass: "secrets";
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
function createPhaseTracker(toolName: string, filePath: string): PhaseTracker {
|
|
68
|
+
const phases: Array<{ name: string; startTime: number; ended: boolean }> = [];
|
|
69
|
+
return {
|
|
70
|
+
start(name: string) {
|
|
71
|
+
phases.push({ name, startTime: Date.now(), ended: false });
|
|
72
|
+
},
|
|
73
|
+
end(name: string, metadata?: Record<string, unknown>) {
|
|
74
|
+
const phase = phases.find((item) => item.name === name && !item.ended);
|
|
75
|
+
if (!phase) return;
|
|
76
|
+
phase.ended = true;
|
|
77
|
+
logLatency({
|
|
78
|
+
type: "phase",
|
|
79
|
+
toolName,
|
|
80
|
+
filePath,
|
|
81
|
+
phase: name,
|
|
82
|
+
durationMs: Date.now() - phase.startTime,
|
|
83
|
+
metadata,
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function exceedsLspSyncLimits(content: string): {
|
|
90
|
+
tooLarge: boolean;
|
|
91
|
+
reason: string;
|
|
92
|
+
} {
|
|
93
|
+
const sizeBytes = Buffer.byteLength(content, "utf-8");
|
|
94
|
+
if (sizeBytes > LSP_MAX_FILE_BYTES) {
|
|
95
|
+
return {
|
|
96
|
+
tooLarge: true,
|
|
97
|
+
reason: `${Math.round(sizeBytes / 1024)}KB exceeds ${Math.round(LSP_MAX_FILE_BYTES / 1024)}KB`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const lineCount = content.split("\n").length;
|
|
102
|
+
if (lineCount > LSP_MAX_FILE_LINES) {
|
|
103
|
+
return {
|
|
104
|
+
tooLarge: true,
|
|
105
|
+
reason: `${lineCount} lines exceeds ${LSP_MAX_FILE_LINES}`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { tooLarge: false, reason: "" };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function displayPath(cwd: string, filePath: string): string {
|
|
113
|
+
const relative = path.relative(cwd, filePath);
|
|
114
|
+
return relative && !relative.startsWith("..") && !path.isAbsolute(relative)
|
|
115
|
+
? relative.replace(/\\/g, "/")
|
|
116
|
+
: filePath.replace(/\\/g, "/");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function buildAllClearOutput(elapsed: number, filePath: string): string {
|
|
120
|
+
const kind = detectFileKind(filePath);
|
|
121
|
+
const langLabel = kind ? getFileKindLabel(kind) : path.extname(filePath);
|
|
122
|
+
const parts = kind
|
|
123
|
+
? [`${langLabel} clean`, `${elapsed}ms`]
|
|
124
|
+
: [`${elapsed}ms`];
|
|
125
|
+
return `✓ ${parts.join(" · ")}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface FormatPhaseResult {
|
|
129
|
+
formatChanged: boolean;
|
|
130
|
+
formattersUsed: string[];
|
|
131
|
+
formatFailures: string[];
|
|
132
|
+
fileContent: string | undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function runFormatPhase(
|
|
136
|
+
filePath: string,
|
|
137
|
+
getFormatService: () => FormatService,
|
|
138
|
+
dbg: PipelineContext["dbg"],
|
|
139
|
+
): Promise<FormatPhaseResult> {
|
|
140
|
+
let formatChanged = false;
|
|
141
|
+
let formattersUsed: string[] = [];
|
|
142
|
+
const formatFailures: string[] = [];
|
|
143
|
+
let fileContent: string | undefined;
|
|
144
|
+
|
|
145
|
+
const formatService = getFormatService();
|
|
146
|
+
try {
|
|
147
|
+
formatService.recordRead(filePath);
|
|
148
|
+
const result = await formatService.formatFile(filePath);
|
|
149
|
+
formattersUsed = result.formatters.map((formatter) => formatter.name);
|
|
150
|
+
if (result.anyChanged) {
|
|
151
|
+
formatChanged = true;
|
|
152
|
+
dbg(
|
|
153
|
+
"autoformat: " +
|
|
154
|
+
result.formatters
|
|
155
|
+
.map(
|
|
156
|
+
(formatter) =>
|
|
157
|
+
`${formatter.name}(${formatter.changed ? "changed" : "unchanged"})`,
|
|
158
|
+
)
|
|
159
|
+
.join(", "),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
if (!result.allSucceeded) {
|
|
163
|
+
const failures = result.formatters.filter(
|
|
164
|
+
(formatter) => !formatter.success,
|
|
165
|
+
);
|
|
166
|
+
formatFailures.push(
|
|
167
|
+
...failures.map(
|
|
168
|
+
(formatter) =>
|
|
169
|
+
`${formatter.name}: ${formatter.error ?? "unknown error"}`,
|
|
170
|
+
),
|
|
171
|
+
);
|
|
172
|
+
dbg(
|
|
173
|
+
"autoformat: " +
|
|
174
|
+
failures
|
|
175
|
+
.map(
|
|
176
|
+
(formatter) =>
|
|
177
|
+
`${formatter.name} failed: ${formatter.error ?? "unknown error"}`,
|
|
178
|
+
)
|
|
179
|
+
.join("; "),
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
} catch (err) {
|
|
183
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
184
|
+
formatFailures.push(message);
|
|
185
|
+
dbg(`autoformat error: ${err}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
fileContent = nodeFs.readFileSync(filePath, "utf-8");
|
|
190
|
+
} catch {
|
|
191
|
+
fileContent = undefined;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return { formatChanged, formattersUsed, formatFailures, fileContent };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export async function resyncLspFile(
|
|
198
|
+
filePath: string,
|
|
199
|
+
fileContent: string,
|
|
200
|
+
needsContentRefresh: boolean,
|
|
201
|
+
lspSyncCompleted: boolean,
|
|
202
|
+
getFlag: PipelineContext["getFlag"],
|
|
203
|
+
dbg: PipelineContext["dbg"],
|
|
204
|
+
formatChanged = false,
|
|
205
|
+
): Promise<void> {
|
|
206
|
+
if (getFlag("no-lsp")) return;
|
|
207
|
+
if (!needsContentRefresh && lspSyncCompleted) return;
|
|
208
|
+
if (exceedsLspSyncLimits(fileContent).tooLarge) return;
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
const lspService = getLSPService();
|
|
212
|
+
if (!lspService.supportsLSP(filePath)) return;
|
|
213
|
+
await lspService.openFile(filePath, fileContent, {
|
|
214
|
+
preserveDiagnostics: formatChanged,
|
|
215
|
+
spawnBudgetMs: LSP_SPAWN_BUDGET_MS,
|
|
216
|
+
});
|
|
217
|
+
} catch (err) {
|
|
218
|
+
dbg(`LSP resync error: ${err}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export async function runPipeline(
|
|
223
|
+
ctx: PipelineContext,
|
|
224
|
+
deps: PipelineDeps,
|
|
225
|
+
): Promise<PipelineResult> {
|
|
226
|
+
const { filePath, cwd, toolName, getFlag, dbg } = ctx;
|
|
227
|
+
const { getFormatService } = deps;
|
|
228
|
+
const phase = createPhaseTracker(toolName, filePath);
|
|
229
|
+
const pipelineStart = Date.now();
|
|
230
|
+
phase.start("total");
|
|
231
|
+
|
|
232
|
+
phase.start("read_file");
|
|
233
|
+
let fileContent: string | undefined;
|
|
234
|
+
try {
|
|
235
|
+
fileContent = nodeFs.readFileSync(filePath, "utf-8");
|
|
236
|
+
} catch {
|
|
237
|
+
fileContent = undefined;
|
|
238
|
+
}
|
|
239
|
+
phase.end("read_file");
|
|
240
|
+
|
|
241
|
+
if (fileContent) {
|
|
242
|
+
const secretFindings = scanForSecrets(fileContent, filePath);
|
|
243
|
+
if (secretFindings.length > 0) {
|
|
244
|
+
const durationMs = Date.now() - pipelineStart;
|
|
245
|
+
logLatency({
|
|
246
|
+
type: "tool_result",
|
|
247
|
+
toolName,
|
|
248
|
+
filePath,
|
|
249
|
+
durationMs,
|
|
250
|
+
result: "blocked_secrets",
|
|
251
|
+
metadata: { secretsFound: secretFindings.length },
|
|
252
|
+
});
|
|
253
|
+
const secretDiagnostics: SecretDiagnostic[] = secretFindings.map(
|
|
254
|
+
(finding) => ({
|
|
255
|
+
id: `secrets:${finding.line}`,
|
|
256
|
+
message: finding.message,
|
|
257
|
+
filePath,
|
|
258
|
+
line: finding.line,
|
|
259
|
+
column: 1,
|
|
260
|
+
severity: "error",
|
|
261
|
+
semantic: "blocking",
|
|
262
|
+
tool: "secrets-scanner",
|
|
263
|
+
rule: "secrets",
|
|
264
|
+
defectClass: "secrets",
|
|
265
|
+
}),
|
|
266
|
+
);
|
|
267
|
+
emitLensAnalysisComplete({
|
|
268
|
+
cwd,
|
|
269
|
+
filePath,
|
|
270
|
+
toolName,
|
|
271
|
+
model: ctx.telemetry?.model ?? "unknown",
|
|
272
|
+
sessionId: ctx.telemetry?.sessionId ?? "unknown",
|
|
273
|
+
turnIndex: ctx.telemetry?.turnIndex ?? 0,
|
|
274
|
+
writeIndex: ctx.telemetry?.writeIndex ?? 0,
|
|
275
|
+
diagnostics: secretDiagnostics,
|
|
276
|
+
blockers: secretDiagnostics,
|
|
277
|
+
warnings: [],
|
|
278
|
+
fixed: [],
|
|
279
|
+
resolvedCount: 0,
|
|
280
|
+
hasBlockers: true,
|
|
281
|
+
fileModified: false,
|
|
282
|
+
changedFiles: [],
|
|
283
|
+
durationMs,
|
|
284
|
+
});
|
|
285
|
+
return {
|
|
286
|
+
output: `\n\n${formatSecrets(secretFindings, filePath)}`,
|
|
287
|
+
hasBlockers: true,
|
|
288
|
+
isError: true,
|
|
289
|
+
fileModified: false,
|
|
290
|
+
changedFiles: [],
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
phase.start("format");
|
|
296
|
+
let formatChanged = false;
|
|
297
|
+
let formatFailures: string[] = [];
|
|
298
|
+
const changedFiles = new Set<string>();
|
|
299
|
+
const autoformatDisabled = !!getFlag("no-autoformat");
|
|
300
|
+
const immediateFormat = !!getFlag("immediate-format");
|
|
301
|
+
const formatDeferred =
|
|
302
|
+
!autoformatDisabled && !immediateFormat && !!fileContent;
|
|
303
|
+
if (!autoformatDisabled && immediateFormat && fileContent) {
|
|
304
|
+
const formatResult = await runFormatPhase(filePath, getFormatService, dbg);
|
|
305
|
+
formatChanged = formatResult.formatChanged;
|
|
306
|
+
formatFailures = formatResult.formatFailures;
|
|
307
|
+
fileContent = formatResult.fileContent;
|
|
308
|
+
if (formatChanged) changedFiles.add(path.resolve(filePath));
|
|
309
|
+
} else if (formatDeferred) {
|
|
310
|
+
dbg(`autoformat: deferred until agent_end for ${filePath}`);
|
|
311
|
+
}
|
|
312
|
+
phase.end("format", { formatChanged, deferred: formatDeferred });
|
|
313
|
+
|
|
314
|
+
phase.start("lsp_sync");
|
|
315
|
+
let lspSyncCompleted = false;
|
|
316
|
+
if (fileContent) {
|
|
317
|
+
await resyncLspFile(
|
|
318
|
+
filePath,
|
|
319
|
+
fileContent,
|
|
320
|
+
true,
|
|
321
|
+
false,
|
|
322
|
+
getFlag,
|
|
323
|
+
dbg,
|
|
324
|
+
formatChanged,
|
|
325
|
+
);
|
|
326
|
+
lspSyncCompleted = true;
|
|
327
|
+
}
|
|
328
|
+
phase.end("lsp_sync", { completed: lspSyncCompleted, finalContent: true });
|
|
329
|
+
|
|
330
|
+
let output = "";
|
|
331
|
+
if (formatFailures.length > 0) {
|
|
332
|
+
const details = formatFailures.slice(0, 3).join("; ");
|
|
333
|
+
const suffix =
|
|
334
|
+
formatFailures.length > 3
|
|
335
|
+
? `; ... and ${formatFailures.length - 3} more`
|
|
336
|
+
: "";
|
|
337
|
+
output += `\n\n⚠️ Auto-format failed: ${details}${suffix}`;
|
|
338
|
+
}
|
|
339
|
+
if (formatChanged) {
|
|
340
|
+
const changedList = [...changedFiles].map((changedFile) =>
|
|
341
|
+
displayPath(cwd, changedFile),
|
|
342
|
+
);
|
|
343
|
+
const fileList = changedList.length
|
|
344
|
+
? `\nModified files:\n${changedList.map((file) => ` - ${file}`).join("\n")}`
|
|
345
|
+
: "";
|
|
346
|
+
output += `\n\n⚠️ **File was modified by auto-format. You MUST re-read modified file(s) before making any further edits — the content on disk has changed.**${fileList}`;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const elapsed = Date.now() - pipelineStart;
|
|
350
|
+
if (!output) output = buildAllClearOutput(elapsed, filePath);
|
|
351
|
+
phase.end("total", { hasOutput: !!output });
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
output,
|
|
355
|
+
hasBlockers: false,
|
|
356
|
+
isError: false,
|
|
357
|
+
fileModified: formatChanged,
|
|
358
|
+
changedFiles: [...changedFiles],
|
|
359
|
+
};
|
|
360
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { detectFileKind, type FileKind } from "./file-kinds.js";
|
|
4
|
+
import { isPathIgnoredByProject } from "./file-utils.js";
|
|
5
|
+
|
|
6
|
+
export interface ProjectProfile {
|
|
7
|
+
present: Partial<Record<FileKind, boolean>>;
|
|
8
|
+
counts: Partial<Record<FileKind, number>>;
|
|
9
|
+
detectedKinds: FileKind[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const MARKERS: Partial<Record<FileKind, string[]>> = {
|
|
13
|
+
go: ["go.mod"],
|
|
14
|
+
python: ["pyproject.toml", "requirements.txt", "setup.py"],
|
|
15
|
+
rust: ["Cargo.toml"],
|
|
16
|
+
jsts: ["package.json", "tsconfig.json"],
|
|
17
|
+
java: ["pom.xml", "build.gradle", "build.gradle.kts"],
|
|
18
|
+
ruby: ["Gemfile"],
|
|
19
|
+
php: ["composer.json"],
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const LSP_DEFAULTS: Partial<Record<FileKind, string>> = {
|
|
23
|
+
jsts: "typescript-language-server",
|
|
24
|
+
python: "pyright",
|
|
25
|
+
rust: "rust-analyzer",
|
|
26
|
+
java: "jdtls",
|
|
27
|
+
ruby: "solargraph",
|
|
28
|
+
php: "intelephense",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const MAX_WALK_FILES = 4000;
|
|
32
|
+
const MAX_LSP_PREINSTALL = 3;
|
|
33
|
+
|
|
34
|
+
function countKind(
|
|
35
|
+
kind: FileKind,
|
|
36
|
+
counts: Partial<Record<FileKind, number>>,
|
|
37
|
+
): void {
|
|
38
|
+
counts[kind] = (counts[kind] ?? 0) + 1;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function walkProject(root: string): Partial<Record<FileKind, number>> {
|
|
42
|
+
const counts: Partial<Record<FileKind, number>> = {};
|
|
43
|
+
let scanned = 0;
|
|
44
|
+
const queue = [root];
|
|
45
|
+
|
|
46
|
+
while (queue.length > 0 && scanned < MAX_WALK_FILES) {
|
|
47
|
+
const dir = queue.pop();
|
|
48
|
+
if (!dir) break;
|
|
49
|
+
let entries: fs.Dirent[];
|
|
50
|
+
try {
|
|
51
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
52
|
+
} catch {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
if (scanned >= MAX_WALK_FILES) break;
|
|
57
|
+
const full = path.join(dir, entry.name);
|
|
58
|
+
if (entry.isDirectory()) {
|
|
59
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
60
|
+
if (isPathIgnoredByProject(full, root, true)) continue;
|
|
61
|
+
queue.push(full);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (!entry.isFile()) continue;
|
|
65
|
+
if (isPathIgnoredByProject(full, root, false)) continue;
|
|
66
|
+
scanned += 1;
|
|
67
|
+
const kind = detectFileKind(full);
|
|
68
|
+
if (kind) countKind(kind, counts);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return counts;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function markerPresent(root: string, kind: FileKind): boolean {
|
|
76
|
+
for (const marker of MARKERS[kind] ?? []) {
|
|
77
|
+
if (fs.existsSync(path.join(root, marker))) return true;
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function detectProjectProfile(root: string): ProjectProfile {
|
|
83
|
+
const resolved = path.resolve(root);
|
|
84
|
+
const counts = walkProject(resolved);
|
|
85
|
+
const present: Partial<Record<FileKind, boolean>> = {};
|
|
86
|
+
|
|
87
|
+
for (const [kind, count] of Object.entries(counts)) {
|
|
88
|
+
if ((count ?? 0) > 0) present[kind as FileKind] = true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const kind of Object.keys(MARKERS) as FileKind[]) {
|
|
92
|
+
if (markerPresent(resolved, kind)) present[kind] = true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const detectedKinds = (Object.keys(present) as FileKind[])
|
|
96
|
+
.filter((kind) => present[kind])
|
|
97
|
+
.sort((a, b) => (counts[b] ?? 0) - (counts[a] ?? 0));
|
|
98
|
+
|
|
99
|
+
return { present, counts, detectedKinds };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function lspPreinstallTools(profile: ProjectProfile): string[] {
|
|
103
|
+
const tools: string[] = [];
|
|
104
|
+
for (const kind of profile.detectedKinds) {
|
|
105
|
+
const tool = LSP_DEFAULTS[kind];
|
|
106
|
+
if (tool && !tools.includes(tool)) tools.push(tool);
|
|
107
|
+
if (tools.length >= MAX_LSP_PREINSTALL) break;
|
|
108
|
+
}
|
|
109
|
+
return tools;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function resolveProjectRootForFile(
|
|
113
|
+
filePath: string,
|
|
114
|
+
fallbackRoot: string,
|
|
115
|
+
): string {
|
|
116
|
+
return path.resolve(fallbackRoot);
|
|
117
|
+
}
|