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,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FileTime Tracking for pi-lens
|
|
3
|
+
*
|
|
4
|
+
* Prevents race conditions when auto-formatting or external tools modify files.
|
|
5
|
+
* Tracks file modification times and sizes to detect external changes.
|
|
6
|
+
*
|
|
7
|
+
* Inspired by OpenCode's FileTime system - ensures agents re-read files
|
|
8
|
+
* that have been modified externally (including by formatters).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as fs from "node:fs";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
|
|
14
|
+
// --- Types ---
|
|
15
|
+
|
|
16
|
+
export interface FileStamp {
|
|
17
|
+
readAt: Date;
|
|
18
|
+
mtime: number | undefined;
|
|
19
|
+
ctime: number | undefined;
|
|
20
|
+
size: number | undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface FileTimeState {
|
|
24
|
+
reads: Map<string, Map<string, FileStamp>>; // sessionID -> filePath -> stamp
|
|
25
|
+
locks: Map<string, Promise<void>>; // filePath -> lock promise
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// --- Singleton State ---
|
|
29
|
+
|
|
30
|
+
const globalState: FileTimeState = {
|
|
31
|
+
reads: new Map(),
|
|
32
|
+
locks: new Map(),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// --- Public API ---
|
|
36
|
+
|
|
37
|
+
export class FileTime {
|
|
38
|
+
private sessionID: string;
|
|
39
|
+
|
|
40
|
+
constructor(sessionID: string) {
|
|
41
|
+
this.sessionID = sessionID;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Record a file read with current stats
|
|
46
|
+
* Call this after ANY file modification (including formatting)
|
|
47
|
+
*/
|
|
48
|
+
read(filePath: string): FileStamp {
|
|
49
|
+
const absolutePath = path.resolve(filePath);
|
|
50
|
+
const stamp = createStamp(absolutePath);
|
|
51
|
+
|
|
52
|
+
let sessionReads = globalState.reads.get(this.sessionID);
|
|
53
|
+
if (!sessionReads) {
|
|
54
|
+
sessionReads = new Map();
|
|
55
|
+
globalState.reads.set(this.sessionID, sessionReads);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
sessionReads.set(absolutePath, stamp);
|
|
59
|
+
return stamp;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get last recorded stamp for a file
|
|
64
|
+
*/
|
|
65
|
+
get(filePath: string): FileStamp | undefined {
|
|
66
|
+
const absolutePath = path.resolve(filePath);
|
|
67
|
+
const sessionReads = globalState.reads.get(this.sessionID);
|
|
68
|
+
return sessionReads?.get(absolutePath);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Assert file hasn't changed since last read
|
|
73
|
+
* Throws error if file modified externally - forces agent to re-read
|
|
74
|
+
*/
|
|
75
|
+
assert(filePath: string): void {
|
|
76
|
+
const absolutePath = path.resolve(filePath);
|
|
77
|
+
const sessionReads = globalState.reads.get(this.sessionID);
|
|
78
|
+
const recorded = sessionReads?.get(absolutePath);
|
|
79
|
+
|
|
80
|
+
if (!recorded) {
|
|
81
|
+
throw new FileTimeError(
|
|
82
|
+
`You must read file ${absolutePath} before modifying it. Use the read tool first.`,
|
|
83
|
+
absolutePath,
|
|
84
|
+
"not-read",
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const current = createStamp(absolutePath);
|
|
89
|
+
const changed =
|
|
90
|
+
current.mtime !== recorded.mtime ||
|
|
91
|
+
current.ctime !== recorded.ctime ||
|
|
92
|
+
current.size !== recorded.size;
|
|
93
|
+
|
|
94
|
+
if (changed) {
|
|
95
|
+
throw new FileTimeError(
|
|
96
|
+
`File ${absolutePath} has been modified since it was last read.\n` +
|
|
97
|
+
`Last modification: ${new Date(current.mtime ?? Date.now()).toISOString()}\n` +
|
|
98
|
+
`Last read: ${recorded.readAt.toISOString()}\n\n` +
|
|
99
|
+
`Please read the file again before modifying it.`,
|
|
100
|
+
absolutePath,
|
|
101
|
+
"modified",
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Check if file has changed (non-throwing version of assert)
|
|
108
|
+
*/
|
|
109
|
+
hasChanged(filePath: string): boolean {
|
|
110
|
+
const absolutePath = path.resolve(filePath);
|
|
111
|
+
const sessionReads = globalState.reads.get(this.sessionID);
|
|
112
|
+
const recorded = sessionReads?.get(absolutePath);
|
|
113
|
+
|
|
114
|
+
if (!recorded) return true; // Never read = changed
|
|
115
|
+
|
|
116
|
+
const current = createStamp(absolutePath);
|
|
117
|
+
return (
|
|
118
|
+
current.mtime !== recorded.mtime ||
|
|
119
|
+
current.ctime !== recorded.ctime ||
|
|
120
|
+
current.size !== recorded.size
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Acquire exclusive lock on file
|
|
126
|
+
* Prevents concurrent modifications to same file
|
|
127
|
+
*/
|
|
128
|
+
async withLock<T>(filePath: string, fn: () => Promise<T>): Promise<T> {
|
|
129
|
+
const absolutePath = path.resolve(filePath);
|
|
130
|
+
|
|
131
|
+
// Wait for existing lock
|
|
132
|
+
while (globalState.locks.has(absolutePath)) {
|
|
133
|
+
const existing = globalState.locks.get(absolutePath);
|
|
134
|
+
if (existing) await existing;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Create new lock
|
|
138
|
+
const lockPromise = fn().finally(() => {
|
|
139
|
+
globalState.locks.delete(absolutePath);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
globalState.locks.set(
|
|
143
|
+
absolutePath,
|
|
144
|
+
lockPromise.then(() => {}),
|
|
145
|
+
);
|
|
146
|
+
return lockPromise;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Clear all tracked files for this session
|
|
151
|
+
*/
|
|
152
|
+
clear(): void {
|
|
153
|
+
globalState.reads.delete(this.sessionID);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Clear specific file tracking
|
|
158
|
+
*/
|
|
159
|
+
clearFile(filePath: string): void {
|
|
160
|
+
const absolutePath = path.resolve(filePath);
|
|
161
|
+
const sessionReads = globalState.reads.get(this.sessionID);
|
|
162
|
+
sessionReads?.delete(absolutePath);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// --- Error Type ---
|
|
167
|
+
|
|
168
|
+
export class FileTimeError extends Error {
|
|
169
|
+
readonly filePath: string;
|
|
170
|
+
readonly reason: "not-read" | "modified";
|
|
171
|
+
|
|
172
|
+
constructor(
|
|
173
|
+
message: string,
|
|
174
|
+
filePath: string,
|
|
175
|
+
reason: "not-read" | "modified",
|
|
176
|
+
) {
|
|
177
|
+
super(message);
|
|
178
|
+
this.name = "FileTimeError";
|
|
179
|
+
this.filePath = filePath;
|
|
180
|
+
this.reason = reason;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// --- Utilities ---
|
|
185
|
+
|
|
186
|
+
function createStamp(filePath: string): FileStamp {
|
|
187
|
+
try {
|
|
188
|
+
const stats = fs.statSync(filePath);
|
|
189
|
+
return {
|
|
190
|
+
readAt: new Date(),
|
|
191
|
+
mtime: stats.mtime.getTime(),
|
|
192
|
+
ctime: stats.ctime.getTime(),
|
|
193
|
+
size: stats.size,
|
|
194
|
+
};
|
|
195
|
+
} catch {
|
|
196
|
+
// File doesn't exist - return empty stamp
|
|
197
|
+
return {
|
|
198
|
+
readAt: new Date(),
|
|
199
|
+
mtime: undefined,
|
|
200
|
+
ctime: undefined,
|
|
201
|
+
size: undefined,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// --- Global Helpers ---
|
|
207
|
+
|
|
208
|
+
export function createFileTime(sessionID: string): FileTime {
|
|
209
|
+
return new FileTime(sessionID);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function clearAllSessions(): void {
|
|
213
|
+
globalState.reads.clear();
|
|
214
|
+
globalState.locks.clear();
|
|
215
|
+
}
|
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared file path utilities for pi-lens
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
import { minimatch } from "minimatch";
|
|
8
|
+
import { normalizeFilePath } from "./path-utils.js";
|
|
9
|
+
import { safeSpawnAsync } from "./safe-spawn.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Return the directory where pi-lens stores project-specific data
|
|
13
|
+
* (caches, indexes, worklogs, etc.).
|
|
14
|
+
*
|
|
15
|
+
* Default: store under <project>/.pi/harness/.lens/projects/<project-slug>.
|
|
16
|
+
*
|
|
17
|
+
* Override: set PILENS_DATA_DIR=/some/path — each project gets its own
|
|
18
|
+
* subdirectory named after a sanitized form of its absolute path, e.g.
|
|
19
|
+
* PILENS_DATA_DIR=.pi/harness/.lens/projects
|
|
20
|
+
* → .pi/harness/.lens/projects/home-user-myapp/
|
|
21
|
+
*/
|
|
22
|
+
export function getProjectDataDir(cwd: string): string {
|
|
23
|
+
const configuredBase = process.env.PILENS_DATA_DIR?.trim();
|
|
24
|
+
const base =
|
|
25
|
+
configuredBase ||
|
|
26
|
+
path.join(
|
|
27
|
+
path.resolve(cwd || process.cwd()),
|
|
28
|
+
".pi",
|
|
29
|
+
"harness",
|
|
30
|
+
".lens",
|
|
31
|
+
"projects",
|
|
32
|
+
);
|
|
33
|
+
const normalized = normalizeFilePath(path.resolve(cwd));
|
|
34
|
+
const slug = normalized
|
|
35
|
+
.replace(/^[a-z]:/i, "") // strip Windows drive letter
|
|
36
|
+
.replace(/\/+/g, "-") // separators → dashes
|
|
37
|
+
.replace(/[^A-Za-z0-9-]/g, "") // strip anything else
|
|
38
|
+
.replace(/^-+/, "") // trim leading dashes
|
|
39
|
+
.replace(/-+$/, ""); // trim trailing dashes
|
|
40
|
+
return path.join(base.trim(), slug || "default");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Directories to exclude from all scans (build outputs, dependencies, caches).
|
|
45
|
+
* Used consistently across all scanners to avoid noise from generated files.
|
|
46
|
+
*/
|
|
47
|
+
export const EXCLUDED_DIRS = [
|
|
48
|
+
"node_modules",
|
|
49
|
+
".git",
|
|
50
|
+
"dist",
|
|
51
|
+
"build",
|
|
52
|
+
".turbo",
|
|
53
|
+
".cache",
|
|
54
|
+
"target",
|
|
55
|
+
"out",
|
|
56
|
+
".parcel-cache",
|
|
57
|
+
".svelte-kit",
|
|
58
|
+
".nuxt",
|
|
59
|
+
".yarn",
|
|
60
|
+
".pnpm-store",
|
|
61
|
+
".gradle",
|
|
62
|
+
".next",
|
|
63
|
+
".pi", // pi agent directory, including harness lens runtime data
|
|
64
|
+
".ruff_cache", // Python linter cache
|
|
65
|
+
".worktrees",
|
|
66
|
+
".claude",
|
|
67
|
+
".codex",
|
|
68
|
+
".rescue",
|
|
69
|
+
".agents",
|
|
70
|
+
".gstack",
|
|
71
|
+
".superpowers",
|
|
72
|
+
".guardrails",
|
|
73
|
+
".playwright-cli",
|
|
74
|
+
".playwright-mcp",
|
|
75
|
+
".vscode",
|
|
76
|
+
"venv",
|
|
77
|
+
".venv",
|
|
78
|
+
"coverage",
|
|
79
|
+
"__pycache__",
|
|
80
|
+
".tox",
|
|
81
|
+
".pytest_cache",
|
|
82
|
+
"*.dSYM",
|
|
83
|
+
// Vendored upstream source conventions — universally too large to scan
|
|
84
|
+
"vendor", // Go modules, PHP Composer, Ruby Bundler
|
|
85
|
+
"third_party", // Chromium/Google convention (llama.cpp, sherpa-onnx, gRPC, TF)
|
|
86
|
+
"third-party",
|
|
87
|
+
"vendors",
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
export interface GitignorePattern {
|
|
91
|
+
pattern: string;
|
|
92
|
+
negated: boolean;
|
|
93
|
+
directoryOnly: boolean;
|
|
94
|
+
rooted: boolean;
|
|
95
|
+
hasSlash: boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface ProjectIgnoreMatcher {
|
|
99
|
+
rootDir: string;
|
|
100
|
+
patterns: GitignorePattern[];
|
|
101
|
+
isIgnored(filePath: string, isDirectory?: boolean): boolean;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function resolveGitIgnoreRoot(startDir: string): string {
|
|
105
|
+
const fallback = path.resolve(startDir);
|
|
106
|
+
let current = fallback;
|
|
107
|
+
while (true) {
|
|
108
|
+
if (fs.existsSync(path.join(current, ".git"))) return current;
|
|
109
|
+
const parent = path.dirname(current);
|
|
110
|
+
if (parent === current) return fallback;
|
|
111
|
+
current = parent;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function collapseSlashes(value: string): string {
|
|
116
|
+
let out = "";
|
|
117
|
+
let previousWasSlash = false;
|
|
118
|
+
for (const ch of value) {
|
|
119
|
+
if (ch === "/") {
|
|
120
|
+
if (!previousWasSlash) out += ch;
|
|
121
|
+
previousWasSlash = true;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
out += ch === "\\" ? "/" : ch;
|
|
125
|
+
previousWasSlash = false;
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function stripLeadingDotSlash(value: string): string {
|
|
131
|
+
return value.startsWith("./") ? value.slice(2) : value;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function stripTrailingSlashes(value: string): string {
|
|
135
|
+
let end = value.length;
|
|
136
|
+
while (end > 0 && value[end - 1] === "/") end -= 1;
|
|
137
|
+
return value.slice(0, end);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function stripLeadingSlashes(value: string): string {
|
|
141
|
+
let start = 0;
|
|
142
|
+
while (start < value.length && value[start] === "/") start += 1;
|
|
143
|
+
return value.slice(start);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function normalizeIgnorePath(value: string): string {
|
|
147
|
+
return collapseSlashes(stripLeadingDotSlash(value));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function stripTrailingSpaces(value: string): string {
|
|
151
|
+
// Good-enough gitignore whitespace handling: unescaped trailing spaces are ignored.
|
|
152
|
+
let end = value.length;
|
|
153
|
+
while (end > 0 && value[end - 1] === " " && value[end - 2] !== "\\") end -= 1;
|
|
154
|
+
return value.slice(0, end).replace(/\\ /g, " ");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function parseGitignoreContent(content: string): GitignorePattern[] {
|
|
158
|
+
const patterns: GitignorePattern[] = [];
|
|
159
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
160
|
+
let line = stripTrailingSpaces(rawLine.trimStart());
|
|
161
|
+
if (!line || line.startsWith("#")) continue;
|
|
162
|
+
let negated = false;
|
|
163
|
+
if (line.startsWith("!")) {
|
|
164
|
+
negated = true;
|
|
165
|
+
line = line.slice(1);
|
|
166
|
+
}
|
|
167
|
+
line = normalizeIgnorePath(line);
|
|
168
|
+
if (!line) continue;
|
|
169
|
+
|
|
170
|
+
const directoryOnly = line.endsWith("/");
|
|
171
|
+
if (directoryOnly) line = stripTrailingSlashes(line);
|
|
172
|
+
const rooted = line.startsWith("/");
|
|
173
|
+
if (rooted) line = stripLeadingSlashes(line);
|
|
174
|
+
if (!line) continue;
|
|
175
|
+
|
|
176
|
+
patterns.push({
|
|
177
|
+
pattern: line,
|
|
178
|
+
negated,
|
|
179
|
+
directoryOnly,
|
|
180
|
+
rooted,
|
|
181
|
+
hasSlash: line.includes("/"),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
return patterns;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function expandGitignorePattern(pattern: GitignorePattern): string[] {
|
|
188
|
+
const body = pattern.pattern;
|
|
189
|
+
if (pattern.directoryOnly) {
|
|
190
|
+
if (pattern.rooted || pattern.hasSlash) return [body, `${body}/**`];
|
|
191
|
+
return [body, `${body}/**`, `**/${body}`, `**/${body}/**`];
|
|
192
|
+
}
|
|
193
|
+
if (pattern.rooted || pattern.hasSlash) return [body];
|
|
194
|
+
return [body, `**/${body}`];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function matchesGitignorePattern(
|
|
198
|
+
pattern: GitignorePattern,
|
|
199
|
+
relativePath: string,
|
|
200
|
+
isDirectory: boolean,
|
|
201
|
+
): boolean {
|
|
202
|
+
const candidate = stripLeadingSlashes(normalizeIgnorePath(relativePath));
|
|
203
|
+
if (!candidate) return false;
|
|
204
|
+
const candidates = isDirectory ? [candidate, `${candidate}/`] : [candidate];
|
|
205
|
+
const options = { dot: true, nocase: process.platform === "win32" };
|
|
206
|
+
return expandGitignorePattern(pattern).some((expanded) => {
|
|
207
|
+
if (isDirectory && expanded.endsWith("/**")) {
|
|
208
|
+
const prefix = expanded.slice(0, -3);
|
|
209
|
+
if (candidate === prefix || candidate.startsWith(`${prefix}/`))
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
return candidates.some((value) => minimatch(value, expanded, options));
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function readGitignorePatterns(rootDir: string): GitignorePattern[] {
|
|
217
|
+
const gitignorePath = path.join(rootDir, ".gitignore");
|
|
218
|
+
try {
|
|
219
|
+
return parseGitignoreContent(fs.readFileSync(gitignorePath, "utf-8"));
|
|
220
|
+
} catch {
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function ancestorDirsBetween(rootDir: string, targetDir: string): string[] {
|
|
226
|
+
const relative = path.relative(rootDir, targetDir);
|
|
227
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) return [];
|
|
228
|
+
const dirs = [rootDir];
|
|
229
|
+
if (!relative) return dirs;
|
|
230
|
+
let current = rootDir;
|
|
231
|
+
for (const segment of relative.split(path.sep).filter(Boolean)) {
|
|
232
|
+
current = path.join(current, segment);
|
|
233
|
+
dirs.push(current);
|
|
234
|
+
}
|
|
235
|
+
return dirs;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function buildProjectIgnoreMatcher(
|
|
239
|
+
resolvedRoot: string,
|
|
240
|
+
patterns: GitignorePattern[],
|
|
241
|
+
): ProjectIgnoreMatcher {
|
|
242
|
+
const nestedCache = new Map<
|
|
243
|
+
string,
|
|
244
|
+
{ gitignoreMtimeMs: number; patterns: GitignorePattern[] }
|
|
245
|
+
>();
|
|
246
|
+
const patternsForDir = (dir: string): GitignorePattern[] => {
|
|
247
|
+
if (dir === resolvedRoot) return patterns;
|
|
248
|
+
const gitignoreMtime = gitignoreMtimeMs(dir);
|
|
249
|
+
const cached = nestedCache.get(dir);
|
|
250
|
+
if (cached?.gitignoreMtimeMs === gitignoreMtime) return cached.patterns;
|
|
251
|
+
const nextPatterns = readGitignorePatterns(dir);
|
|
252
|
+
nestedCache.set(dir, {
|
|
253
|
+
gitignoreMtimeMs: gitignoreMtime,
|
|
254
|
+
patterns: nextPatterns,
|
|
255
|
+
});
|
|
256
|
+
return nextPatterns;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
rootDir: resolvedRoot,
|
|
261
|
+
patterns,
|
|
262
|
+
isIgnored(filePath: string, isDirectory = false): boolean {
|
|
263
|
+
const resolved = path.resolve(filePath);
|
|
264
|
+
const rootRelative = path.relative(resolvedRoot, resolved);
|
|
265
|
+
if (
|
|
266
|
+
!rootRelative ||
|
|
267
|
+
rootRelative.startsWith("..") ||
|
|
268
|
+
path.isAbsolute(rootRelative)
|
|
269
|
+
) {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
let ignored = false;
|
|
274
|
+
const patternDirs = ancestorDirsBetween(
|
|
275
|
+
resolvedRoot,
|
|
276
|
+
path.dirname(resolved),
|
|
277
|
+
);
|
|
278
|
+
for (const dir of patternDirs) {
|
|
279
|
+
const dirPatterns = patternsForDir(dir);
|
|
280
|
+
if (dirPatterns.length === 0) continue;
|
|
281
|
+
const relative = path.relative(dir, resolved);
|
|
282
|
+
const normalized = normalizeIgnorePath(relative);
|
|
283
|
+
for (const pattern of dirPatterns) {
|
|
284
|
+
if (!matchesGitignorePattern(pattern, normalized, isDirectory))
|
|
285
|
+
continue;
|
|
286
|
+
ignored = !pattern.negated;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return ignored;
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function createProjectIgnoreMatcher(
|
|
295
|
+
rootDir: string,
|
|
296
|
+
extraPatterns: string[] = [],
|
|
297
|
+
): ProjectIgnoreMatcher {
|
|
298
|
+
const resolvedRoot = resolveGitIgnoreRoot(rootDir);
|
|
299
|
+
const patterns = [
|
|
300
|
+
...readGitignorePatterns(resolvedRoot),
|
|
301
|
+
...parseGitignoreContent(extraPatterns.join("\n")),
|
|
302
|
+
];
|
|
303
|
+
return buildProjectIgnoreMatcher(resolvedRoot, patterns);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const projectIgnoreMatcherCache = new Map<
|
|
307
|
+
string,
|
|
308
|
+
{ gitignoreMtimeMs: number; matcher: ProjectIgnoreMatcher }
|
|
309
|
+
>();
|
|
310
|
+
|
|
311
|
+
function gitignoreMtimeMs(rootDir: string): number {
|
|
312
|
+
try {
|
|
313
|
+
return fs.statSync(path.join(rootDir, ".gitignore")).mtimeMs;
|
|
314
|
+
} catch {
|
|
315
|
+
return -1;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function getProjectIgnoreMatcher(rootDir: string): ProjectIgnoreMatcher {
|
|
320
|
+
const resolvedRoot = resolveGitIgnoreRoot(rootDir);
|
|
321
|
+
const gitignoreMtime = gitignoreMtimeMs(resolvedRoot);
|
|
322
|
+
const cached = projectIgnoreMatcherCache.get(resolvedRoot);
|
|
323
|
+
if (cached?.gitignoreMtimeMs === gitignoreMtime) return cached.matcher;
|
|
324
|
+
|
|
325
|
+
const matcher = createProjectIgnoreMatcher(resolvedRoot);
|
|
326
|
+
projectIgnoreMatcherCache.set(resolvedRoot, {
|
|
327
|
+
gitignoreMtimeMs: gitignoreMtime,
|
|
328
|
+
matcher,
|
|
329
|
+
});
|
|
330
|
+
return matcher;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function isPathIgnoredByProject(
|
|
334
|
+
filePath: string,
|
|
335
|
+
rootDir: string,
|
|
336
|
+
isDirectory = false,
|
|
337
|
+
): boolean {
|
|
338
|
+
return getProjectIgnoreMatcher(rootDir).isIgnored(filePath, isDirectory);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function getProjectIgnoreGlobs(rootDir: string): string[] {
|
|
342
|
+
return readGitignorePatterns(rootDir)
|
|
343
|
+
.filter((pattern) => !pattern.negated)
|
|
344
|
+
.flatMap((pattern) => expandGitignorePattern(pattern));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Read simple directory-name entries from a root .gitignore.
|
|
349
|
+
*
|
|
350
|
+
* Prefer createProjectIgnoreMatcher() for path-aware gitignore matching. This
|
|
351
|
+
* helper is kept for callers/tests that only need simple directory names.
|
|
352
|
+
*/
|
|
353
|
+
export function readGitignoreDirs(rootDir: string): string[] {
|
|
354
|
+
return readGitignorePatterns(rootDir)
|
|
355
|
+
.filter(
|
|
356
|
+
(entry) =>
|
|
357
|
+
!entry.negated &&
|
|
358
|
+
!entry.pattern.includes("*") &&
|
|
359
|
+
!entry.pattern.includes("?") &&
|
|
360
|
+
!entry.pattern.includes("[") &&
|
|
361
|
+
!entry.pattern.includes("/"),
|
|
362
|
+
)
|
|
363
|
+
.map((entry) => entry.pattern);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function globToRegExp(glob: string): RegExp {
|
|
367
|
+
const escaped = glob
|
|
368
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
369
|
+
.replace(/\*/g, ".*")
|
|
370
|
+
.replace(/\?/g, ".");
|
|
371
|
+
return new RegExp(`^${escaped}$`, "i");
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Match directory name against exclusion patterns.
|
|
376
|
+
* Supports exact names and lightweight glob patterns (for example `*.dSYM`).
|
|
377
|
+
*/
|
|
378
|
+
export function isExcludedDirName(
|
|
379
|
+
dirName: string,
|
|
380
|
+
extraPatterns: string[] = [],
|
|
381
|
+
): boolean {
|
|
382
|
+
const candidate = dirName.trim();
|
|
383
|
+
if (!candidate) return false;
|
|
384
|
+
|
|
385
|
+
const patterns = [...EXCLUDED_DIRS, ...extraPatterns]
|
|
386
|
+
.map((p) => p.trim())
|
|
387
|
+
.filter((p) => p.length > 0);
|
|
388
|
+
const candidateLower = candidate.toLowerCase();
|
|
389
|
+
|
|
390
|
+
for (const pattern of patterns) {
|
|
391
|
+
const patLower = pattern.toLowerCase();
|
|
392
|
+
if (!patLower.includes("*") && !patLower.includes("?")) {
|
|
393
|
+
if (candidateLower === patLower) return true;
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (globToRegExp(pattern).test(candidate)) return true;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Convert excluded directory names into glob patterns used by scanners.
|
|
404
|
+
*/
|
|
405
|
+
export function getExcludedDirGlobs(): string[] {
|
|
406
|
+
return EXCLUDED_DIRS.map((dir) => `**/${dir}/**`);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Shared Knip ignore patterns derived from central exclusions.
|
|
411
|
+
*/
|
|
412
|
+
export function getKnipIgnorePatterns(): string[] {
|
|
413
|
+
return [
|
|
414
|
+
...getExcludedDirGlobs(),
|
|
415
|
+
"**/*.test.ts",
|
|
416
|
+
"**/*.test.tsx",
|
|
417
|
+
"**/*.test.js",
|
|
418
|
+
"**/*.test.jsx",
|
|
419
|
+
"**/*.spec.ts",
|
|
420
|
+
"**/*.spec.tsx",
|
|
421
|
+
"**/*.spec.js",
|
|
422
|
+
"**/*.spec.jsx",
|
|
423
|
+
"**/*.poc.test.ts",
|
|
424
|
+
"**/*.poc.test.tsx",
|
|
425
|
+
"**/__tests__/**",
|
|
426
|
+
"**/tests/**",
|
|
427
|
+
];
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Spawn a command and detect whether it modified a file on disk.
|
|
432
|
+
* Returns 1 if the file content changed after the command ran, 0 otherwise.
|
|
433
|
+
* Useful for auto-fix tools (ESLint, Stylelint, RuboCop, etc.).
|
|
434
|
+
*/
|
|
435
|
+
export async function detectFileChangedAfterCommand(
|
|
436
|
+
filePath: string,
|
|
437
|
+
command: string,
|
|
438
|
+
args: string[],
|
|
439
|
+
cwd: string,
|
|
440
|
+
ignoreStatuses: number[] = [],
|
|
441
|
+
): Promise<number> {
|
|
442
|
+
let before = "";
|
|
443
|
+
try {
|
|
444
|
+
before = fs.readFileSync(filePath, "utf-8");
|
|
445
|
+
} catch {
|
|
446
|
+
return 0;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const result = await safeSpawnAsync(command, args, {
|
|
450
|
+
timeout: 30000,
|
|
451
|
+
cwd,
|
|
452
|
+
});
|
|
453
|
+
if (result.error) return 0;
|
|
454
|
+
if (result.status !== 0 && !ignoreStatuses.includes(result.status ?? -1)) {
|
|
455
|
+
return 0;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
try {
|
|
459
|
+
const after = fs.readFileSync(filePath, "utf-8");
|
|
460
|
+
return before !== after ? 1 : 0;
|
|
461
|
+
} catch {
|
|
462
|
+
return 0;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Check if file path is a test/fixture/mock file.
|
|
468
|
+
* Used by secrets scanner, rate command, and dispatch runners
|
|
469
|
+
* to skip these files (false positives on fake credentials, etc).
|
|
470
|
+
*/
|
|
471
|
+
export function isTestFile(filePath: string): boolean {
|
|
472
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
473
|
+
return (
|
|
474
|
+
normalized.includes(".test.") ||
|
|
475
|
+
normalized.includes(".spec.") ||
|
|
476
|
+
normalized.includes("/test/") ||
|
|
477
|
+
normalized.includes("/tests/") ||
|
|
478
|
+
normalized.includes("__tests__/") ||
|
|
479
|
+
normalized.includes("test-utils") ||
|
|
480
|
+
normalized.startsWith("test-") ||
|
|
481
|
+
normalized.includes(".fixture.") ||
|
|
482
|
+
normalized.includes(".mock.")
|
|
483
|
+
);
|
|
484
|
+
}
|