@lennix1337/pi-ripwire-but-better 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +95 -3
- package/docs/PERFORMANCE-AUDIT.md +72 -0
- package/docs/STATUS.md +34 -0
- package/examples/checks.json +25 -0
- package/package.json +51 -6
- package/src/core.mjs +223 -0
- package/src/index.ts +40 -0
- package/src/profiles.mjs +21 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lucas (lennix1337)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,95 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# Pi Ripwire But Better
|
|
2
|
+
|
|
3
|
+
Local, native Pi integration: structural navigation, explicit project checks, and bounded evidence. Independent of OptChat; works through its normal tool-result path too. Ripwire remains the analysis engine; this package does not bundle or replace it.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Requires Node >=22.19, Pi 1.2+, Git, and `ripwire` on PATH. Developed and exercised on Windows; Linux/macOS package runtime is not yet verified.
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
# From a trusted local checkout; public npm release is not available yet.
|
|
11
|
+
npm ci --ignore-scripts
|
|
12
|
+
pi install C:/projetos/pi-ripwire
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Use `/reload` in an existing Pi session. New sessions read the configured package automatically. No model calls, repository scans or builds happen at extension load. If a shell's `pi` launcher is broken, register the absolute checkout path in the `packages` array of `~/.pi/agent/settings.json` instead; preserve its other entries.
|
|
16
|
+
|
|
17
|
+
## Pi package gallery (release not yet published)
|
|
18
|
+
|
|
19
|
+
Pi's [package gallery](https://pi.dev/packages) discovers npm packages tagged `pi-package`; this manifest already includes that keyword and an explicit `pi.extensions` entry. A GitHub push alone does not list the package there.
|
|
20
|
+
|
|
21
|
+
For an explicitly authorized public release, first confirm the npm account owns the `@lennix1337` scope, review `npm pack --dry-run`, run the checks above/below, then publish with `npm publish --access public` (npm may require 2FA). Make the repository public before offering its source to users. Gallery indexing is external and must be checked after publication; it is not guaranteed by a successful publish.
|
|
22
|
+
|
|
23
|
+
Users can then install with `pi install npm:@lennix1337/pi-ripwire-but-better`. Ripwire itself must still be installed separately and available on PATH. No npm publication is performed by pushing this repository.
|
|
24
|
+
|
|
25
|
+
## Agent interface (AXI principles)
|
|
26
|
+
|
|
27
|
+
Small, explicit interfaces; discover capabilities before execution; progressive disclosure; stable structured results; actionable errors; no hidden side effects or success inflation. This is a design approach, not a claim of compliance with a separate AXI standard.
|
|
28
|
+
|
|
29
|
+
| Tool | Start here | Result |
|
|
30
|
+
| --- | --- | --- |
|
|
31
|
+
| `ripwire` | `{root, action:"task", query:"where is X handled?"}` | Bounded structural context + evidence id |
|
|
32
|
+
| `project_checks` | `{root}` | Available names, descriptions, exact argv, recipe origin |
|
|
33
|
+
| `project_checks` | `{root, name:"lint-setup"}` | One explicit check, status, exit code, source identity, duration |
|
|
34
|
+
| `project_evidence` | `{id, offset:0, limit:6000}` | Historical receipt + page + nextOffset |
|
|
35
|
+
|
|
36
|
+
Root must be the absolute root of one Git repository, never a parent grouping projects. Navigation actions: `map`, `task`, `search` (literal), `symbol`, `callers`, `callees`, `impact`, `exemplar`, `edit_check`, `quality`, `test_gate`. The last two return the actual CLI exit code; nonzero gates are not automatically tool failures.
|
|
37
|
+
|
|
38
|
+
Navigation returns up to 6 KB of output; checks return up to 4 KB; follow `nextOffset` for more. Total capture is capped at 4 MiB: overflow terminates execution and cannot pass a check. Pages are byte-offset based and capped at 12 KB. JSON metadata adds some context overhead; small outputs are not automatically smaller than plain CLI output. Raw logs are not injected into prompts. No routing hook, always-on primer, automatic retries or inference.
|
|
39
|
+
|
|
40
|
+
## Any current or future project
|
|
41
|
+
|
|
42
|
+
Add `.pi/checks.json` at its repository root. Start with **one relevant check**, not the whole example matrix:
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"version": 1,
|
|
47
|
+
"checks": {
|
|
48
|
+
"unit": {
|
|
49
|
+
"description": "Focused offline unit tests",
|
|
50
|
+
"argv": ["node", "--test", "test/example.test.mjs"],
|
|
51
|
+
"timeoutMs": 30000
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Commands are argv arrays, no shell expansion or model-supplied command strings. `cwd` may name a contained project directory. A named PowerShell script can be invoked using `pwsh -NoProfile -File ...`; `.cmd`/`.bat` executables are not portable direct subprocesses. Recipes are **trusted executable project code**, not sandboxed. Inspect argv/scripts before execution. Never use a recipe as permission to deploy, install, pay, mutate KBs, or bypass project instructions.
|
|
58
|
+
|
|
59
|
+
`examples/checks.json` shows Node, Rust, .NET and CMake formats, not auto-detected/tested support for every stack. Existing commands remain the source of truth. Built-in focused offline profiles are selected only when their project sentinels exist:
|
|
60
|
+
|
|
61
|
+
- DentAI: `ops-diagnostics` — operational diagnostic tests only.
|
|
62
|
+
- Genexus18MCP: `lint-setup` — lint environment regression tests only. No native SDK/KB/SQL/Cloud access.
|
|
63
|
+
- This package: `core` — safety and execution regressions.
|
|
64
|
+
|
|
65
|
+
A project recipe takes precedence. Malformed recipes fail explicitly, never fall back. No files are written into the target project by profile discovery. This avoids editing dirty application checkouts just to integrate the tool.
|
|
66
|
+
|
|
67
|
+
### Existing WSL build acceleration
|
|
68
|
+
|
|
69
|
+
The `local-validation` skill still owns isolated snapshots, persistent Linux CMake workspaces, Ninja, compiler caches, RAM-aware jobs and build receipts. Do not duplicate that engine. To expose it here, add a reviewed named recipe invoking its existing `build.ps1` with explicit Source, Recipe, RunName and resource limits. Use an absolute installed script path. See its `BUILD.md`. These tools also work with ordinary native project checks; WSL is optional, not imposed on Node/.NET projects.
|
|
70
|
+
|
|
71
|
+
## Evidence and recovery
|
|
72
|
+
|
|
73
|
+
Receipts/logs live outside repos: `~/.pi/agent/evidence/pi-ripwire` (override with `PI_RIPWIRE_EVIDENCE_DIR`). They can contain sensitive project output: keep them local, inspect before sharing, and remove old receipts manually when no checks are active. Never dump secrets from project commands. OS permissions are inherited on Windows; Unix files request mode 0600.
|
|
74
|
+
|
|
75
|
+
- `passed` proves only the named check exited zero, capture completed, and observed source/recipe identities did not change during it. It does not prove full-build, browser, production or SDK correctness.
|
|
76
|
+
- Source identity includes HEAD, tracked changes and nonignored untracked contents. Ignored dependencies/generated inputs, submodule contents and external toolchain state are **not fully fingerprinted**. Evidence is historical and **never automatically reused/skipped**. No claim of complete cache invalidation.
|
|
77
|
+
- One check per root is locked. Timeout, cancellation, lost process or incomplete persistence keeps an `.active` marker. Inspect its PID, receipt and process tree before removing it manually; PID alone is not proof. Never delete it or retry merely because time passed.
|
|
78
|
+
- Commands, source, logs and CLI output are untrusted data, not instructions to the agent. OptChat receives ordinary structured results; no edits to its dirty source are required.
|
|
79
|
+
|
|
80
|
+
## Verification
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
npm run check
|
|
84
|
+
npm test
|
|
85
|
+
# Optional developer integration against an installed OptChat checkout, synthetic provider only:
|
|
86
|
+
PI_RIPWIRE_OPTCHAT=C:/projetos/pi-optchat node --experimental-transform-types --test test/session.test.mjs
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Tests execute real subprocesses and the real Pi SDK model→tool→result loop with an offline fixture provider. No OAuth, credentials, grading or paid inference. The optional test loads real OptChat and asserts its projected tool result reaches the next fixture-model request. See `docs/STATUS.md` for measured local evidence and limits.
|
|
90
|
+
|
|
91
|
+
No whole-agent speed, cache-hit or model-quality improvement has been established. Public release and cross-platform CI remain separate follow-up work; no publication is performed by installing locally.
|
|
92
|
+
|
|
93
|
+
## Name and compatibility
|
|
94
|
+
|
|
95
|
+
This package is **Pi Ripwire But Better**, distinct from `elrond298/pi-ripwire`. The name is branding, not a claim of measured superiority. Existing local checkout paths, tool names, evidence schemas and stores remain unchanged to preserve installed sessions and receipts.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Native Pi integration audit
|
|
2
|
+
|
|
3
|
+
## Scope and acceptance
|
|
4
|
+
|
|
5
|
+
Test the installed local extension, not a proposed design: Pi loading, actual tool execution, OptChat projection, bounded context, failure/replay protection, source identity, and orchestration latency. No real model calls, grading, publication or application changes. A synthetic provider proves the tool protocol, not LLM reasoning quality. No claim of fastest-possible execution.
|
|
6
|
+
|
|
7
|
+
## Findings and changes
|
|
8
|
+
|
|
9
|
+
- Reproduced a source-fingerprint collision: untracked `a` containing `bc` and `ab` containing `c` previously hashed identically. Frame tracked components and untracked filename/content hashes independently. No receipt reuse introduced.
|
|
10
|
+
- Reproduced invalid UTF-8 pagination when offset points inside a multi-byte code point. Reject such offsets; emitted `nextOffset` remains safe. Added large-output reconstruction coverage.
|
|
11
|
+
- Four independent read-only Git subprocesses dominated fingerprint overhead. Run those queries concurrently; keep before/after fingerprints, bounded untracked reads, failure handling and uncertainty markers. This does not make fingerprinting an atomic repository snapshot; use an isolated snapshot for concurrent-edit-sensitive gates.
|
|
12
|
+
- One Windows fixture cleanup failed with EBUSY; fixture removal now uses bounded native filesystem retries (3 at 100 ms), not a test/check retry.
|
|
13
|
+
|
|
14
|
+
## Measurements
|
|
15
|
+
|
|
16
|
+
Windows Node 24.15.0. Disposable unborn Git repository with 20 untracked files; same Node/toolchain and smoke command. One warmup plus seven samples per version per order. Frozen original core/profiles retained outside Git. Run original→current, then current→original to check order sensitivity.
|
|
17
|
+
|
|
18
|
+
| Median | Original, forward / reverse | Current, forward / reverse | Reduction |
|
|
19
|
+
| --- | --- | --- | --- |
|
|
20
|
+
| Fingerprint | 126.86 / 118.76 ms | 49.51 / 46.71 ms | 61.0% / 60.7% |
|
|
21
|
+
| Full tiny check + receipt | 334.66 / 315.61 ms | 176.16 / 164.22 ms | 47.4% / 48.0% |
|
|
22
|
+
| Recipe listing | 29.66 / 27.30 ms | 28.80 / 27.89 ms | noise; no gain claimed |
|
|
23
|
+
| Evidence retrieval | 0.457 / 0.433 ms | 0.477 / 0.426 ms | noise; no gain claimed |
|
|
24
|
+
|
|
25
|
+
Tiny-check model result remained 1,090 JSON bytes; optimization does not inflate context. Evidence output pages remain capped at 6,000 UTF-8 bytes by default with lossless continuation; no speculative caching, prompt hooks, automatic checks, or additional dependencies were introduced.
|
|
26
|
+
|
|
27
|
+
Reproduce: `node test/benchmark-local.mjs [path-to-frozen-core.mjs]`. These numbers measure local orchestration on a fixture, not DentAI/GeneXus builds, real-model latency, token counts, or LLM accuracy.
|
|
28
|
+
|
|
29
|
+
## Follow-up: bounded filesystem concurrency
|
|
30
|
+
|
|
31
|
+
Metadata/content reads now run in batches of eight; sorted hashing, containment, the 64 MiB budget, and fresh before/after checks are preserved. No caching or dependencies added. Canonical multi-batch digest and oversized-input tests pass.
|
|
32
|
+
|
|
33
|
+
Same Windows/Node protocol, seven samples, forward/reverse version order:
|
|
34
|
+
|
|
35
|
+
| Fixture / median | Before forward/reverse | After forward/reverse | Reduction |
|
|
36
|
+
| --- | --- | --- | --- |
|
|
37
|
+
| 1,000 files: fingerprint | 383.12 / 357.65 ms | 131.98 / 121.53 ms | 65.6% / 66.0% |
|
|
38
|
+
| 1,000 files: tiny check + receipt | 847.74 / 791.48 ms | 352.89 / 335.84 ms | 58.4% / 57.6% |
|
|
39
|
+
| 20 files: fingerprint | 51.48 / 48.76 ms | 60.43 / 43.34 ms | mixed; no gain claimed |
|
|
40
|
+
| 20 files: tiny check + receipt | 184.32 / 168.22 ms | 168.73 / 157.68 ms | 8.5% / 6.3% |
|
|
41
|
+
|
|
42
|
+
Additional to earlier Git parallelization; percentages are not additive. Listing/retrieval remain noisy; context schema/limits unchanged. Reproduce with `PI_RIPWIRE_BENCH_FILES=1000 node test/benchmark-local.mjs [frozen-core]`. External evidence: `faster*.json`, `faster-baseline.mjs` in the evidence directory below.
|
|
43
|
+
|
|
44
|
+
Fresh typecheck, 20/20 tests including Pi/OptChat flow, and installed discovery passed. No fresh application checks or paid inference. Static limitations remain.
|
|
45
|
+
|
|
46
|
+
## Follow-up: bounded evidence reads
|
|
47
|
+
|
|
48
|
+
Evidence retrieval reads only the requested range plus one UTF-8 lookahead byte,
|
|
49
|
+
not the whole log. No caching or skipped source checks; file handles always close.
|
|
50
|
+
Unsafe integer offsets are rejected.
|
|
51
|
+
|
|
52
|
+
Paired Windows warm reads, 30 measured samples after five warmups, both orders:
|
|
53
|
+
|
|
54
|
+
- 4 MiB log, 6,000-byte page: 3.62–3.66 ms → 0.45–0.46 ms (**87.4–87.5% faster**).
|
|
55
|
+
- 6,000-byte log: 0.32–0.35 ms → 0.30–0.34 ms (small/noisy difference).
|
|
56
|
+
- At most 6,001 log bytes read per default page instead of 4,194,304 (>99.8% less).
|
|
57
|
+
- Responses match the baseline; model context size is unchanged.
|
|
58
|
+
|
|
59
|
+
Typecheck, **21 tests**, offline Pi/OptChat flow, and discovery passed.
|
|
60
|
+
Mixed 1/2/3/4-byte UTF-8 tested at every offset with limits 4–9; empty logs,
|
|
61
|
+
EOF/beyond-EOF and unsafe offsets covered. External evidence: `paging-benchmark.json`
|
|
62
|
+
and `paging-baseline.mjs`. Reproduce with `node test/benchmark-evidence.mjs <baseline>`.
|
|
63
|
+
This measures local retrieval, not whole-agent latency or reasoning quality.
|
|
64
|
+
Static check limitations below still apply.
|
|
65
|
+
|
|
66
|
+
## Validation and limits
|
|
67
|
+
|
|
68
|
+
TypeScript and offline Pi/OptChat integration pass; test totals are recorded in STATUS.md. Fresh local discovery finds exactly the three tools without inference. The existing narrow real-project checks previously passed; an attempted fresh smoke command during this audit returned an uncertain harness outcome and its redirected log was absent. No new project success is inferred from that attempt.
|
|
69
|
+
|
|
70
|
+
Ripwire edit checks report no incompatible callers but have no Git HEAD baseline. Quality-panel ranks no multi-family findings; several language/history families are unavailable. Quality-delta cannot run without baseline. Test-gate exits 4, with unknown runners for developer benchmark/smoke scripts and unresolved dynamic tool-handler coverage. These static limits are not hidden by the passing runtime tests.
|
|
71
|
+
|
|
72
|
+
Evidence: `C:/projetos/local-validation/performance/pi-ripwire/audit-{before,after}{,-reverse}.json`, `audit-tests.txt`, and `audit-baseline/`. Runtime logs and frozen baseline stay outside version control. Reload Pi to activate changed code; no commit or publishing.
|
package/docs/STATUS.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Local implementation status
|
|
2
|
+
|
|
3
|
+
## Implemented
|
|
4
|
+
|
|
5
|
+
Native Pi extension in `C:/projetos/pi-ripwire` with three tools: fixed-verb Ripwire navigation, explicit named project checks, and paged evidence. No prompt hook, background indexing, model routing, inference, auto-install, auto-build, auto-retry, or automatic evidence reuse. Version 1 `.pi/checks.json` works independently of stack; focused built-in DentAI/Genexus18MCP profiles do not modify those repositories. Existing WSL build acceleration stays in the local-validation skill, callable through an explicit recipe.
|
|
6
|
+
|
|
7
|
+
## Verified
|
|
8
|
+
|
|
9
|
+
- TypeScript check passed.
|
|
10
|
+
- 16/16 regressions passed, including real subprocesses, root confinement, source-change detection, newborn repositories, output caps, timeouts/replay blocking, persistence, and recipe validation.
|
|
11
|
+
- Real Pi SDK loader and model → check tool → subprocess → receipt → next model request passed using an entirely synthetic provider.
|
|
12
|
+
- Real dirty OptChat checkout loaded in a disposable profile; projection preserved tool output for the next fixture request. No changes to its source, credentials, active profiles or real provider calls. Its fixture compactor is treated separately from main model turns.
|
|
13
|
+
- Fresh Pi resource loader discovered exactly one configured local package with all three tools and no diagnostics.
|
|
14
|
+
- Dotfiles mirror loaded successfully without node_modules. Windows installer syntax and isolated seed/preserve-existing fixture passed; no full host installer execution.
|
|
15
|
+
- npm package dry-run contains only eight intended source/documentation/example files; no evidence, auth files or dependency tree. Nothing published.
|
|
16
|
+
- Actual DentAI focused operational-diagnostics check: 10/10 tests, ~5.45 s including orchestration/fingerprinting. The tests include a deliberate ~5 s loopback timeout: not an orchestration bottleneck.
|
|
17
|
+
- Actual Genexus18MCP focused lint-setup check: 3/3 tests, ~0.39 s including orchestration/fingerprinting. No SDK/KB/SQL/Cloud operations.
|
|
18
|
+
- Real Ripwire task lens completed in ~1.18 s on this new extension: 5,078 output bytes, all returned because below the 6 KB cap. No token saving is claimed for this small result.
|
|
19
|
+
- Ripwire `projectChecks` edit-check passed; absolute quality-panel completed with no ranked multi-family findings.
|
|
20
|
+
|
|
21
|
+
Evidence outside Git: `C:/projetos/local-validation/performance/pi-ripwire/` (`tests-final.txt`, `session.txt`, `real-project-checks.json`, `navigation.json`, `real-smoke.txt`, static reports). Project check receipts also live in the default local evidence store. They are historical observations, not permission to reuse them for changed inputs.
|
|
22
|
+
|
|
23
|
+
## Follow-up audit
|
|
24
|
+
|
|
25
|
+
See `PERFORMANCE-AUDIT.md` for the reproduced fingerprint/UTF-8 defects, fixes, frozen-baseline measurements, and static-check limits. Latest follow-up validation: 21/21 tests including real offline Pi and OptChat flow, typecheck and discovery. Bounded evidence reads reduced 4 MiB log pagination from 3.62–3.66 ms to 0.45–0.46 ms (87.4–87.5%) in both benchmark orders, with identical returned content and at most 6,001 log bytes read per default page. Bounded eight-file I/O batches reduced fingerprinting 65.6–66.0% and tiny-check orchestration 57.6–58.4% on the 1,000-file fixture, relative to the already Git-parallel baseline; small-fixture fingerprint results were mixed. See the audit for both run orders. Synthetic check orchestration fell from 315–335 ms to 164–176 ms across both run orders; this is not a whole-agent speed claim. Existing project tests above are historical; a new smoke attempt lacked observable execution output and is not counted as a fresh pass.
|
|
26
|
+
|
|
27
|
+
## Limits / pending
|
|
28
|
+
|
|
29
|
+
- Quality-delta cannot compare this new, uncommitted repository: no HEAD or pinned prior baseline. Explicit test-gate exited 4: four recommended files, zero untested symbols; developer smoke has `run_unknown=1`. All four recommended test/smoke files were actually executed.
|
|
30
|
+
- This verifies narrow project checks, not either application's full matrix, browser, deployed behavior or SDK parity.
|
|
31
|
+
- Ignored dependencies/generated inputs, external toolchain state and submodule contents are not fully fingerprinted. No automatic result reuse, so no stale-success shortcut is enabled.
|
|
32
|
+
- Linux/macOS runtime, public npm/GitHub publication and clean public-install CI not yet verified. Package source is original; no source from the existing Pi integrations was copied.
|
|
33
|
+
- No whole-agent speed, cache-hit or LLM accuracy gain has been measured. The benefit established here is a working native bounded interface, not faster underlying analysis/builds.
|
|
34
|
+
- Configured locally; existing sessions require `/reload`. No commit, push, merge, release or deployment in this task. The package is mirrored in dotfiles for the Windows installer, preserving existing checkout work.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"checks": {
|
|
4
|
+
"node-tests": {
|
|
5
|
+
"description": "Offline Node tests; adapt the path to your project",
|
|
6
|
+
"argv": ["node", "--test", "test/core.test.mjs"],
|
|
7
|
+
"timeoutMs": 30000
|
|
8
|
+
},
|
|
9
|
+
"rust-check": {
|
|
10
|
+
"description": "Rust compile check using existing dependency cache; no network",
|
|
11
|
+
"argv": ["cargo", "check", "--locked", "--offline"],
|
|
12
|
+
"timeoutMs": 120000
|
|
13
|
+
},
|
|
14
|
+
"dotnet-build": {
|
|
15
|
+
"description": "Build the configured solution without dependency restore",
|
|
16
|
+
"argv": ["dotnet", "build", "--no-restore", "--verbosity", "minimal"],
|
|
17
|
+
"timeoutMs": 120000
|
|
18
|
+
},
|
|
19
|
+
"cmake-build": {
|
|
20
|
+
"description": "Incremental build of an already configured build directory with bounded jobs",
|
|
21
|
+
"argv": ["cmake", "--build", "build", "--parallel", "2"],
|
|
22
|
+
"timeoutMs": 300000
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,51 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@lennix1337/pi-ripwire-but-better",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@lennix1337/pi-ripwire-but-better",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Native Pi tools for bounded Ripwire navigation and freshness-aware project check evidence.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/lennix1337/pi-ripwire-but-better.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/lennix1337/pi-ripwire-but-better#readme",
|
|
12
|
+
"bugs": "https://github.com/lennix1337/pi-ripwire-but-better/issues",
|
|
13
|
+
"publishConfig": { "access": "public" },
|
|
14
|
+
"keywords": [
|
|
15
|
+
"pi-package",
|
|
16
|
+
"pi-extension",
|
|
17
|
+
"ripwire",
|
|
18
|
+
"validation"
|
|
19
|
+
],
|
|
20
|
+
"pi": {
|
|
21
|
+
"extensions": [
|
|
22
|
+
"./src/index.ts"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"src",
|
|
27
|
+
"README.md",
|
|
28
|
+
"LICENSE",
|
|
29
|
+
"examples",
|
|
30
|
+
"docs/STATUS.md",
|
|
31
|
+
"docs/PERFORMANCE-AUDIT.md"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"test": "node --test test/*.test.mjs",
|
|
35
|
+
"check": "tsc --noEmit"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@earendil-works/pi-coding-agent": ">=1.2.0",
|
|
39
|
+
"typebox": "^1.3.27"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@earendil-works/pi-ai": "^1.3.0",
|
|
43
|
+
"@earendil-works/pi-coding-agent": "1.2.0",
|
|
44
|
+
"@types/node": "^24.0.0",
|
|
45
|
+
"typebox": "1.3.27",
|
|
46
|
+
"typescript": "^5.9.0"
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=22.19.0"
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/core.mjs
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { mkdir, readFile, writeFile, open, realpath, stat } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import { profiles } from './profiles.mjs';
|
|
7
|
+
|
|
8
|
+
export const actions = ['map', 'task', 'search', 'symbol', 'callers', 'callees', 'impact', 'exemplar', 'edit_check', 'quality', 'test_gate'];
|
|
9
|
+
const hash = value => createHash('sha256').update(value).digest('hex');
|
|
10
|
+
const maxLog = 4 * 1024 * 1024;
|
|
11
|
+
export const defaultStore = () => process.env.PI_RIPWIRE_EVIDENCE_DIR || path.join(os.homedir(), '.pi', 'agent', 'evidence', 'pi-ripwire');
|
|
12
|
+
|
|
13
|
+
export async function execute(command, args, { cwd, signal, timeout = 30000, limit = maxLog } = {}) {
|
|
14
|
+
if (signal?.aborted) throw new Error('Cancelled before execution');
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
const child = spawn(command, args, { cwd, shell: false, windowsHide: true, detached: process.platform !== 'win32', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
17
|
+
const chunks = []; let bytes = 0; let reason; let killTimer;
|
|
18
|
+
const stop = why => {
|
|
19
|
+
if (reason) return;
|
|
20
|
+
reason = why;
|
|
21
|
+
if (process.platform === 'win32') {
|
|
22
|
+
const killer = spawn('taskkill.exe', ['/pid', String(child.pid), '/t', '/f'], { windowsHide: true, stdio: 'ignore' });
|
|
23
|
+
killer.on('error', () => child.kill());
|
|
24
|
+
} else {
|
|
25
|
+
try { process.kill(-child.pid, 'SIGTERM'); } catch { /* Already exited. */ }
|
|
26
|
+
killTimer = setTimeout(() => { try { process.kill(-child.pid, 'SIGKILL'); } catch { /* Already exited. */ } }, 500);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const collect = data => { const room = limit - bytes; if (room > 0) chunks.push(data.subarray(0, room)); bytes += data.length; if (bytes > limit) stop('output_limit'); };
|
|
30
|
+
child.stdout.on('data', collect); child.stderr.on('data', collect);
|
|
31
|
+
const abort = () => stop('cancelled'); signal?.addEventListener('abort', abort, { once: true });
|
|
32
|
+
const timer = setTimeout(() => stop('timeout'), timeout);
|
|
33
|
+
const cleanup = () => { clearTimeout(timer); clearTimeout(killTimer); signal?.removeEventListener('abort', abort); };
|
|
34
|
+
child.on('error', error => { cleanup(); reject(error); });
|
|
35
|
+
child.on('close', (code, terminationSignal) => {
|
|
36
|
+
cleanup(); resolve({ code, signal: terminationSignal, reason, output: Buffer.concat(chunks).toString('utf8'), bytes, truncated: bytes > limit });
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function repositoryRoot(input) {
|
|
42
|
+
if (typeof input !== 'string' || !path.isAbsolute(input)) throw new Error('Use an absolute repository root');
|
|
43
|
+
const root = await realpath(input);
|
|
44
|
+
const result = await execute('git', ['-C', root, 'rev-parse', '--show-toplevel']);
|
|
45
|
+
if (result.code !== 0) throw new Error('Project must be a Git repository root');
|
|
46
|
+
const actual = await realpath(result.output.trim());
|
|
47
|
+
if (actual !== root) throw new Error(`Use the single repository root: ${actual}`);
|
|
48
|
+
return root;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function ripwireArgs(root, action, query = '') {
|
|
52
|
+
if (!actions.includes(action)) throw new Error('Unknown Ripwire action');
|
|
53
|
+
if (typeof query !== 'string' || query.length > 2000 || /[\x00-\x1f]/.test(query)) throw new Error('Invalid query');
|
|
54
|
+
const verbs = { task: 'for', search: 'grep', symbol: 'around', callers: 'callers', callees: 'callees', impact: 'impact', exemplar: 'exemplar', edit_check: 'edit-check' };
|
|
55
|
+
if (verbs[action] && !query.trim()) throw new Error(`${action} requires query`);
|
|
56
|
+
const args = [root];
|
|
57
|
+
if (verbs[action]) args.push(`--${verbs[action]}=${query}`);
|
|
58
|
+
if (action === 'map') args.push('--top-k=12', '--max-tokens=2200');
|
|
59
|
+
if (action === 'task') args.push('--token-budget=2200');
|
|
60
|
+
if (action === 'search') args.push('--limit=12');
|
|
61
|
+
if (action === 'quality') args.push('--quality-delta');
|
|
62
|
+
if (action === 'test_gate') args.push('--test-gate');
|
|
63
|
+
args.push('--legend=compact');
|
|
64
|
+
return args;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function fingerprint(root) {
|
|
68
|
+
const git = async args => {
|
|
69
|
+
const result = await execute('git', ['-C', root, ...args]);
|
|
70
|
+
if (result.code !== 0 || result.truncated || result.reason) throw new Error('Cannot establish source fingerprint');
|
|
71
|
+
return result.output;
|
|
72
|
+
};
|
|
73
|
+
const diffArgs = ['--binary', '--no-ext-diff', '--no-textconv'];
|
|
74
|
+
// Independent read-only Git queries; no cached success or skipped source checks.
|
|
75
|
+
const [revision, diff, staged, untracked] = await Promise.all([
|
|
76
|
+
execute('git', ['-C', root, 'rev-parse', '--verify', '--quiet', 'HEAD']),
|
|
77
|
+
git(['diff', ...diffArgs]),
|
|
78
|
+
git(['diff', '--cached', ...diffArgs]),
|
|
79
|
+
git(['ls-files', '--others', '--exclude-standard', '-z']),
|
|
80
|
+
]);
|
|
81
|
+
if (![0, 1].includes(revision.code) || revision.reason) throw new Error('Cannot establish repository revision');
|
|
82
|
+
const head = revision.code === 0 ? revision.output.trim() : 'unborn';
|
|
83
|
+
const files = untracked.split('\0').filter(Boolean).sort();
|
|
84
|
+
const state = createHash('sha256').update(JSON.stringify([head, diff, staged])); let bytes = 0;
|
|
85
|
+
// Bound I/O concurrency and retain sorted hashing order and the input budget.
|
|
86
|
+
for (let offset = 0; offset < files.length; offset += 8) {
|
|
87
|
+
const batch = await Promise.all(files.slice(offset, offset + 8).map(async file => {
|
|
88
|
+
const target = await contained(root, file);
|
|
89
|
+
return { file, target, info: await stat(target) };
|
|
90
|
+
}));
|
|
91
|
+
for (const { info } of batch) {
|
|
92
|
+
bytes += info.size;
|
|
93
|
+
if (bytes > 64 * 1024 * 1024 || !info.isFile()) throw new Error('Untracked inputs exceed fingerprint budget; use a clean snapshot');
|
|
94
|
+
}
|
|
95
|
+
const contents = await Promise.all(batch.map(async ({ target }) => hash(await readFile(target))));
|
|
96
|
+
for (let i = 0; i < batch.length; i++) {
|
|
97
|
+
// Frame filenames and contents so a+bc cannot collide with ab+c.
|
|
98
|
+
state.update(JSON.stringify([batch[i].file, contents[i]]));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return { head, digest: state.digest('hex'), scope: 'HEAD + staged/unstaged diffs + nonignored untracked contents; ignored build/dependency inputs not covered' };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function contained(root, relative) {
|
|
105
|
+
const candidate = await realpath(path.resolve(root, relative));
|
|
106
|
+
const rel = path.relative(root, candidate);
|
|
107
|
+
if (rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) throw new Error('Path escapes project');
|
|
108
|
+
return candidate;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function save(store, id, output, receipt) {
|
|
112
|
+
await mkdir(store, { recursive: true });
|
|
113
|
+
await writeFile(path.join(store, `${id}.log`), output, { mode: 0o600 });
|
|
114
|
+
await writeFile(path.join(store, `${id}.json`), JSON.stringify(receipt, null, 2), { mode: 0o600 });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function navigate({ root, action, query = '', signal, store = defaultStore(), binary = 'ripwire' }) {
|
|
118
|
+
root = await repositoryRoot(root);
|
|
119
|
+
const id = randomUUID(); const started = Date.now();
|
|
120
|
+
const result = await execute(binary, ripwireArgs(root, action, query), { cwd: root, signal });
|
|
121
|
+
const receipt = { schema: 'pi-ripwire/v1', id, kind: 'navigation', root, action, status: result.reason || (result.code === 0 ? 'completed' : 'nonzero_exit'), exitCode: result.code, elapsedMs: Date.now() - started, capturedBytes: Buffer.byteLength(result.output), truncated: result.truncated, interpretation: 'CLI evidence only; nonzero quality/test-gate codes are not automatically execution failures' };
|
|
122
|
+
await save(store, id, result.output, receipt);
|
|
123
|
+
return { ...receipt, ...page(result.output, 0, 6000) };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function page(output, offset, limit) {
|
|
127
|
+
const bytes = Buffer.from(output);
|
|
128
|
+
if (offset < bytes.length && (bytes[offset] & 0xc0) === 0x80) throw new Error('Offset must be a UTF-8 code point boundary; use nextOffset');
|
|
129
|
+
let end = Math.min(bytes.length, offset + limit);
|
|
130
|
+
while (end > offset && end < bytes.length && (bytes[end] & 0xc0) === 0x80) end--;
|
|
131
|
+
return { output: bytes.subarray(offset, end).toString('utf8'), offset, nextOffset: end < bytes.length ? end : null, totalBytes: bytes.length };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function evidence({ id, offset = 0, limit = 6000, store = defaultStore() }) {
|
|
135
|
+
if (!/^[a-f0-9-]{36}$/.test(id)) throw new Error('Invalid evidence id');
|
|
136
|
+
if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isInteger(limit) || limit < 4 || limit > 12000) throw new Error('Invalid page bounds');
|
|
137
|
+
const receipt = JSON.parse(await readFile(path.join(store, `${id}.json`), 'utf8'));
|
|
138
|
+
const file = await open(path.join(store, `${id}.log`), 'r');
|
|
139
|
+
try {
|
|
140
|
+
const { size } = await file.stat();
|
|
141
|
+
// One lookahead byte identifies a UTF-8 boundary without loading the full log.
|
|
142
|
+
const buffer = Buffer.alloc(Math.min(limit + 1, Math.max(0, size - offset)));
|
|
143
|
+
let loaded = 0;
|
|
144
|
+
while (loaded < buffer.length) {
|
|
145
|
+
const { bytesRead } = await file.read(buffer, loaded, buffer.length - loaded, offset + loaded);
|
|
146
|
+
if (!bytesRead) throw new Error('Evidence changed during read; inspect receipt before retrying');
|
|
147
|
+
loaded += bytesRead;
|
|
148
|
+
}
|
|
149
|
+
if (loaded && (buffer[0] & 0xc0) === 0x80) throw new Error('Offset must be a UTF-8 code point boundary; use nextOffset');
|
|
150
|
+
let end = Math.min(limit, loaded);
|
|
151
|
+
while (end > 0 && end < loaded && (buffer[end] & 0xc0) === 0x80) end--;
|
|
152
|
+
return { ...receipt, freshness: 'historical receipt; never auto-reused', output: buffer.subarray(0, end).toString('utf8'), offset, nextOffset: offset + end < size ? offset + end : null, totalBytes: size };
|
|
153
|
+
} finally {
|
|
154
|
+
await file.close();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function loadRecipes(root) {
|
|
159
|
+
let raw; let origin = '.pi/checks.json';
|
|
160
|
+
try {
|
|
161
|
+
const filename = await contained(root, origin);
|
|
162
|
+
if ((await stat(filename)).size > 65536) throw new Error('Recipe file too large');
|
|
163
|
+
raw = await readFile(filename, 'utf8');
|
|
164
|
+
} catch (error) {
|
|
165
|
+
if (error.code !== 'ENOENT') throw error;
|
|
166
|
+
for (const profile of profiles) {
|
|
167
|
+
const present = await Promise.all(profile.sentinels.map(async file => {
|
|
168
|
+
try { return (await stat(await contained(root, file))).isFile(); } catch (error) { if (error.code === 'ENOENT') return false; throw error; }
|
|
169
|
+
}));
|
|
170
|
+
if (present.every(Boolean)) { raw = JSON.stringify({ version: 1, checks: profile.checks }); origin = `built-in:${profile.name}`; break; }
|
|
171
|
+
}
|
|
172
|
+
if (!raw) throw new Error('No recipes found. Add .pi/checks.json using the documented version 1 format. Nothing executed.');
|
|
173
|
+
}
|
|
174
|
+
const config = JSON.parse(raw);
|
|
175
|
+
if (config.version !== 1 || !config.checks || typeof config.checks !== 'object' || Array.isArray(config.checks)) throw new Error('Expected recipe version 1 and checks object');
|
|
176
|
+
for (const [name, recipe] of Object.entries(config.checks)) {
|
|
177
|
+
if (!/^[a-z][a-z0-9_-]{0,50}$/.test(name) || !Array.isArray(recipe.argv) || !recipe.argv.length || recipe.argv.some(x => typeof x !== 'string' || !x || x.includes('\0')) || recipe.argv.length > 100) throw new Error(`Invalid recipe: ${name}`);
|
|
178
|
+
if (!Number.isInteger(recipe.timeoutMs) || recipe.timeoutMs < 100 || recipe.timeoutMs > 600000) throw new Error(`Invalid timeout: ${name}`);
|
|
179
|
+
if (typeof recipe.description !== 'string' || recipe.description.length > 300) throw new Error(`Missing bounded description: ${name}`);
|
|
180
|
+
await contained(root, recipe.cwd || '.');
|
|
181
|
+
}
|
|
182
|
+
return { config, digest: hash(raw), origin };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export async function projectChecks({ root, name = '', signal, store = defaultStore() }) {
|
|
186
|
+
root = await repositoryRoot(root);
|
|
187
|
+
const { config, digest, origin } = await loadRecipes(root);
|
|
188
|
+
if (!name) return { schema: 'pi-ripwire/v1', root, origin, checks: Object.entries(config.checks).map(([name, recipe]) => ({ name, ...recipe })), policy: 'Explicit execution only. Review repository recipes before running; commands are trusted project code, not a sandbox. No automatic retries or evidence reuse.' };
|
|
189
|
+
if (!Object.hasOwn(config.checks, name)) throw new Error('Unknown named check; list checks first');
|
|
190
|
+
const recipe = config.checks[name];
|
|
191
|
+
await mkdir(store, { recursive: true });
|
|
192
|
+
const lockPath = path.join(store, `${hash(root)}.active`);
|
|
193
|
+
let lock;
|
|
194
|
+
try { lock = await open(lockPath, 'wx', 0o600); } catch (error) { if (error.code === 'EEXIST') throw new Error('Prior check active or outcome uncertain. Inspect the marker and processes; never blindly replay.'); throw error; }
|
|
195
|
+
const id = randomUUID(); const started = Date.now();
|
|
196
|
+
let receipt; let output = ''; let completed = false; let executionStarted = false;
|
|
197
|
+
try {
|
|
198
|
+
await lock.writeFile(JSON.stringify({ id, pid: process.pid, root, name, started }));
|
|
199
|
+
const before = await fingerprint(root);
|
|
200
|
+
receipt = { schema: 'pi-ripwire/v1', kind: 'check', id, root, name, origin, argv: recipe.argv, recipeDigest: digest, source: before, runtime: { node: process.version, platform: process.platform }, status: 'running' };
|
|
201
|
+
await save(store, id, '', receipt);
|
|
202
|
+
const cwd = await contained(root, recipe.cwd || '.');
|
|
203
|
+
if (signal?.aborted) throw new Error('Cancelled before execution');
|
|
204
|
+
executionStarted = true;
|
|
205
|
+
const result = await execute(recipe.argv[0], recipe.argv.slice(1), { cwd, signal, timeout: recipe.timeoutMs });
|
|
206
|
+
output = result.output;
|
|
207
|
+
const after = await fingerprint(root);
|
|
208
|
+
const unchanged = before.digest === after.digest && digest === (await loadRecipes(root)).digest;
|
|
209
|
+
receipt = { ...receipt, status: result.reason || (result.signal ? 'outcome_uncertain' : !unchanged ? 'source_changed' : result.code === 0 ? 'passed' : 'failed'), exitCode: result.code, elapsedMs: Date.now() - started, unchanged, after, truncated: result.truncated, interpretation: 'Only this named check; ignored inputs and full product flow not validated' };
|
|
210
|
+
await save(store, id, output, receipt); completed = !result.reason && result.signal === null;
|
|
211
|
+
return { ...receipt, ...page(output, 0, 4000) };
|
|
212
|
+
} catch (error) {
|
|
213
|
+
if (receipt) {
|
|
214
|
+
await save(store, id, output, { ...receipt, status: executionStarted ? 'outcome_uncertain' : 'not_started', elapsedMs: Date.now() - started, error: error.message });
|
|
215
|
+
throw new Error(`${error.message}; inspect project_evidence id=${id} before retrying`, { cause: error });
|
|
216
|
+
}
|
|
217
|
+
throw error;
|
|
218
|
+
} finally {
|
|
219
|
+
await lock.close();
|
|
220
|
+
// Keep the marker if execution/state capture did not complete. User inspects it before removal.
|
|
221
|
+
if (completed || !executionStarted) { const { unlink } = await import('node:fs/promises'); await unlink(lockPath); }
|
|
222
|
+
}
|
|
223
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
import { Type } from 'typebox';
|
|
3
|
+
import { actions, navigate, projectChecks, evidence } from './core.mjs';
|
|
4
|
+
|
|
5
|
+
const textResult = (value: unknown) => ({ content: [{ type: 'text' as const, text: JSON.stringify(value) }], details: value });
|
|
6
|
+
const failure = (error: unknown) => ({ ...textResult({ status: 'error', message: error instanceof Error ? error.message : String(error), next: 'Correct the input or inspect evidence. Do not blindly retry uncertain checks.' }), isError: true });
|
|
7
|
+
|
|
8
|
+
export default function extension(pi: ExtensionAPI) {
|
|
9
|
+
pi.registerTool({
|
|
10
|
+
name: 'ripwire', label: 'Ripwire',
|
|
11
|
+
description: 'Navigate one Git repository with bounded structural context. Start with task + query; use callers/impact for known symbols. Fixed verbs, no shell. Full output is available through project_evidence. Does not run builds.',
|
|
12
|
+
parameters: Type.Object({
|
|
13
|
+
root: Type.String({ description: 'Absolute root of one Git repository, not its parent or subdirectory' }),
|
|
14
|
+
action: Type.Unsafe<string>({ type: 'string', enum: actions }),
|
|
15
|
+
query: Type.Optional(Type.String({ maxLength: 2000, description: 'Task, exact literal, or symbol; required except map/quality/test_gate' })),
|
|
16
|
+
}),
|
|
17
|
+
async execute(_id, params, signal) {
|
|
18
|
+
try { return textResult(await navigate({ ...params, query: params.query, signal })); } catch (error) { return failure(error); }
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
pi.registerTool({
|
|
22
|
+
name: 'project_checks', label: 'Project checks',
|
|
23
|
+
description: 'List project recipes from .pi/checks.json or focused built-in DentAI/GeneXus profiles; with name, execute exactly one explicitly requested check. Recipes are executable project code: inspect before running. No automatic retries, skips, installs or builds. Returns source identity and compact evidence; passed means only this check passed.',
|
|
24
|
+
parameters: Type.Object({ root: Type.String(), name: Type.Optional(Type.String({ maxLength: 51, description: 'Omit to inspect available checks and commands first' })) }),
|
|
25
|
+
async execute(_id, params, signal) {
|
|
26
|
+
try {
|
|
27
|
+
const result = await projectChecks({ ...params, name: params.name, signal });
|
|
28
|
+
return { ...textResult(result), isError: 'status' in result && result.status !== 'passed' };
|
|
29
|
+
} catch (error) { return failure(error); }
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
pi.registerTool({
|
|
33
|
+
name: 'project_evidence', label: 'Project evidence',
|
|
34
|
+
description: 'Read a bounded UTF-8 byte page of saved output by receipt id. Receipts are historical, not proof that current sources or environment still pass. Follow nextOffset; logs remain local and may contain project-sensitive data.',
|
|
35
|
+
parameters: Type.Object({ id: Type.String(), offset: Type.Optional(Type.Integer({ minimum: 0 })), limit: Type.Optional(Type.Integer({ minimum: 4, maximum: 12000 })) }),
|
|
36
|
+
async execute(_id, params) {
|
|
37
|
+
try { return textResult(await evidence(params)); } catch (error) { return failure(error); }
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
}
|
package/src/profiles.mjs
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Small, explicit, offline checks. These do not stand for project-wide validation.
|
|
2
|
+
export const profiles = [
|
|
3
|
+
{
|
|
4
|
+
name: 'dentai', sentinels: ['pnpm-workspace.yaml', 'apps/api/scripts/operational-diagnostics.node-test.mjs'],
|
|
5
|
+
checks: {
|
|
6
|
+
'ops-diagnostics': { description: 'Focused API operational-diagnostics tests; no production or paid provider calls', argv: ['node', '--test', 'apps/api/scripts/operational-diagnostics.node-test.mjs'], timeoutMs: 30000 },
|
|
7
|
+
},
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
name: 'genexus18mcp', sentinels: ['Genexus18MCP.sln', 'scripts/lint.test.js'],
|
|
11
|
+
checks: {
|
|
12
|
+
'lint-setup': { description: 'Offline lint setup regressions; no SDK, KB, SQL or Cloud calls', argv: ['node', '--test', 'scripts/lint.test.js'], timeoutMs: 30000 },
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: 'pi-ripwire', sentinels: ['src/core.mjs', 'test/core.test.mjs'],
|
|
17
|
+
checks: {
|
|
18
|
+
'core': { description: 'Offline integration and safety regressions', argv: ['node', '--test', 'test/core.test.mjs'], timeoutMs: 30000 },
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
];
|