codex-workspace-bootstrap 0.6.0__tar.gz → 0.7.0__tar.gz
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.
- {codex_workspace_bootstrap-0.6.0/src/codex_workspace_bootstrap.egg-info → codex_workspace_bootstrap-0.7.0}/PKG-INFO +17 -8
- {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/README.md +16 -7
- {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/pyproject.toml +1 -1
- {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/src/codex_workspace_bootstrap/__init__.py +1 -1
- {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/src/codex_workspace_bootstrap/agents.py +32 -7
- codex_workspace_bootstrap-0.7.0/src/codex_workspace_bootstrap/audit.py +366 -0
- {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/src/codex_workspace_bootstrap/cli.py +9 -0
- {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/src/codex_workspace_bootstrap/doctor.py +5 -1
- {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/src/codex_workspace_bootstrap/instructions.py +165 -36
- {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/src/codex_workspace_bootstrap/preflight.py +45 -6
- {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/src/codex_workspace_bootstrap/sarif.py +4 -0
- {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0/src/codex_workspace_bootstrap.egg-info}/PKG-INFO +17 -8
- codex_workspace_bootstrap-0.7.0/tests/test_audit.py +324 -0
- {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/tests/test_cli.py +97 -2
- {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/tests/test_doctor.py +17 -0
- {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/tests/test_instruction_integrity.py +111 -0
- {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/tests/test_preflight.py +142 -0
- {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/tests/test_public_repo_patterns.py +25 -1
- {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/tests/test_release_consistency.py +36 -0
- {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/tests/test_sarif.py +22 -0
- codex_workspace_bootstrap-0.6.0/src/codex_workspace_bootstrap/audit.py +0 -217
- codex_workspace_bootstrap-0.6.0/tests/test_audit.py +0 -78
- {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/LICENSE +0 -0
- {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/setup.cfg +0 -0
- {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/src/codex_workspace_bootstrap/__main__.py +0 -0
- {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/src/codex_workspace_bootstrap/fixes.py +0 -0
- {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/src/codex_workspace_bootstrap.egg-info/SOURCES.txt +0 -0
- {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/src/codex_workspace_bootstrap.egg-info/dependency_links.txt +0 -0
- {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/src/codex_workspace_bootstrap.egg-info/entry_points.txt +0 -0
- {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/src/codex_workspace_bootstrap.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codex-workspace-bootstrap
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Preflight AI coding repositories before Codex, Copilot, Cline, Claude, Gemini, Continue, or Cursor touches them.
|
|
5
5
|
Author: kohli217
|
|
6
6
|
License: MIT
|
|
@@ -123,8 +123,8 @@ The goal is not a vanity score. The result is one of:
|
|
|
123
123
|
- README
|
|
124
124
|
- license
|
|
125
125
|
- `.gitignore`
|
|
126
|
-
- common project manifests
|
|
127
|
-
- local Git / Python / Node.js /
|
|
126
|
+
- common project manifests, including Python, Node.js, Go, Rust, JVM, and .NET roots
|
|
127
|
+
- local Git / Python / Node.js / repository-selected package manager / PowerShell / WSL / Codex signals
|
|
128
128
|
|
|
129
129
|
### AI instruction coverage
|
|
130
130
|
|
|
@@ -153,7 +153,7 @@ The lint is intentionally conservative: different files may contain additional c
|
|
|
153
153
|
|
|
154
154
|
### Risk signals
|
|
155
155
|
|
|
156
|
-
The audit warns about common secret-bearing filenames without printing their contents. When Git is available, it distinguishes **tracked**, **ignored**, and **untracked/unknown** candidates. Tracked risky filenames can become blocking findings in strict mode.
|
|
156
|
+
The audit warns about common secret-bearing filenames without printing their contents, including environment-specific `.env.*` files while excluding common template names such as `.env.example` and `.env.sample`. When Git is available, it distinguishes **tracked**, **ignored**, and **untracked/unknown** candidates. Tracked risky filenames can become blocking findings in strict mode.
|
|
157
157
|
|
|
158
158
|
This is intentionally a lightweight preflight check, not a replacement for deep scanners such as Gitleaks or Trivy.
|
|
159
159
|
|
|
@@ -211,6 +211,14 @@ Fail CI on any instruction-integrity finding:
|
|
|
211
211
|
cwb preflight . --fail-on-integrity
|
|
212
212
|
```
|
|
213
213
|
|
|
214
|
+
Require the full preflight state to be `READY`:
|
|
215
|
+
|
|
216
|
+
```powershell
|
|
217
|
+
cwb preflight . --require-ready
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Use `--require-ready` when CI should reject both `NEEDS ATTENTION` and `BLOCKED`, including missing repository-wide AI instructions or essential repository markers.
|
|
221
|
+
|
|
214
222
|
### Detailed audit
|
|
215
223
|
|
|
216
224
|
```powershell
|
|
@@ -248,7 +256,7 @@ cwb doctor .
|
|
|
248
256
|
cwb init-agents .
|
|
249
257
|
```
|
|
250
258
|
|
|
251
|
-
Generation is evidence-based: the tool
|
|
259
|
+
Generation is evidence-based: the tool identifies common Python, Node.js, Go, Rust, JVM, and .NET project roots, then only emits validation commands that have direct repository evidence. Python/Node-specific validation is cross-checked against package-manager evidence, package scripts, pytest configuration, and README commands. Plausible but unconfirmed commands are separated for maintainer review. Existing `AGENTS.md` files are never overwritten unless `--force` is explicit.
|
|
252
260
|
|
|
253
261
|
## GitHub Action
|
|
254
262
|
|
|
@@ -259,11 +267,12 @@ The reusable Action is published on GitHub Marketplace.
|
|
|
259
267
|
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
|
|
260
268
|
with:
|
|
261
269
|
python-version: "3.13"
|
|
262
|
-
- uses: kohli217/codex-workspace-bootstrap@v0.
|
|
270
|
+
- uses: kohli217/codex-workspace-bootstrap@v0.7.0
|
|
263
271
|
with:
|
|
264
272
|
path: .
|
|
265
273
|
strict: "true"
|
|
266
274
|
fail_on_integrity: "true"
|
|
275
|
+
require_ready: "true"
|
|
267
276
|
```
|
|
268
277
|
|
|
269
278
|
The Action adds the preflight Markdown report to the **GitHub Actions job summary**, so maintainers get a readable readiness snapshot without digging through raw logs. Optional SARIF output contains both repository-audit and instruction-integrity findings and can be uploaded to GitHub Code Scanning.
|
|
@@ -291,7 +300,7 @@ The intent is to complement those tools, not replace them.
|
|
|
291
300
|
- Suspected secret files are not opened or printed by the filename-risk check.
|
|
292
301
|
- Repository contents are not sent to a remote AI service by the core audit.
|
|
293
302
|
- Instruction files are read locally for deterministic linting; extracted commands are never executed by the integrity lint.
|
|
294
|
-
- Repository instruction/configuration inputs are not
|
|
303
|
+
- Repository instruction/configuration and project-marker inputs are not trusted through symbolic links.
|
|
295
304
|
- `init-agents` and automatic fixes refuse symlinked `AGENTS.md` targets rather than writing through them.
|
|
296
305
|
- Existing regular `AGENTS.md` files are protected unless overwrite is explicit.
|
|
297
306
|
- A passing preflight is evidence about the checks performed, **not a security guarantee**.
|
|
@@ -325,7 +334,7 @@ py -m pip install codex-workspace-bootstrap
|
|
|
325
334
|
Pinned GitHub release artifact:
|
|
326
335
|
|
|
327
336
|
```powershell
|
|
328
|
-
py -m pip install "https://github.com/kohli217/codex-workspace-bootstrap/releases/download/v0.
|
|
337
|
+
py -m pip install "https://github.com/kohli217/codex-workspace-bootstrap/releases/download/v0.7.0/codex_workspace_bootstrap-0.7.0-py3-none-any.whl"
|
|
329
338
|
```
|
|
330
339
|
|
|
331
340
|
## Development
|
|
@@ -95,8 +95,8 @@ The goal is not a vanity score. The result is one of:
|
|
|
95
95
|
- README
|
|
96
96
|
- license
|
|
97
97
|
- `.gitignore`
|
|
98
|
-
- common project manifests
|
|
99
|
-
- local Git / Python / Node.js /
|
|
98
|
+
- common project manifests, including Python, Node.js, Go, Rust, JVM, and .NET roots
|
|
99
|
+
- local Git / Python / Node.js / repository-selected package manager / PowerShell / WSL / Codex signals
|
|
100
100
|
|
|
101
101
|
### AI instruction coverage
|
|
102
102
|
|
|
@@ -125,7 +125,7 @@ The lint is intentionally conservative: different files may contain additional c
|
|
|
125
125
|
|
|
126
126
|
### Risk signals
|
|
127
127
|
|
|
128
|
-
The audit warns about common secret-bearing filenames without printing their contents. When Git is available, it distinguishes **tracked**, **ignored**, and **untracked/unknown** candidates. Tracked risky filenames can become blocking findings in strict mode.
|
|
128
|
+
The audit warns about common secret-bearing filenames without printing their contents, including environment-specific `.env.*` files while excluding common template names such as `.env.example` and `.env.sample`. When Git is available, it distinguishes **tracked**, **ignored**, and **untracked/unknown** candidates. Tracked risky filenames can become blocking findings in strict mode.
|
|
129
129
|
|
|
130
130
|
This is intentionally a lightweight preflight check, not a replacement for deep scanners such as Gitleaks or Trivy.
|
|
131
131
|
|
|
@@ -183,6 +183,14 @@ Fail CI on any instruction-integrity finding:
|
|
|
183
183
|
cwb preflight . --fail-on-integrity
|
|
184
184
|
```
|
|
185
185
|
|
|
186
|
+
Require the full preflight state to be `READY`:
|
|
187
|
+
|
|
188
|
+
```powershell
|
|
189
|
+
cwb preflight . --require-ready
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Use `--require-ready` when CI should reject both `NEEDS ATTENTION` and `BLOCKED`, including missing repository-wide AI instructions or essential repository markers.
|
|
193
|
+
|
|
186
194
|
### Detailed audit
|
|
187
195
|
|
|
188
196
|
```powershell
|
|
@@ -220,7 +228,7 @@ cwb doctor .
|
|
|
220
228
|
cwb init-agents .
|
|
221
229
|
```
|
|
222
230
|
|
|
223
|
-
Generation is evidence-based: the tool
|
|
231
|
+
Generation is evidence-based: the tool identifies common Python, Node.js, Go, Rust, JVM, and .NET project roots, then only emits validation commands that have direct repository evidence. Python/Node-specific validation is cross-checked against package-manager evidence, package scripts, pytest configuration, and README commands. Plausible but unconfirmed commands are separated for maintainer review. Existing `AGENTS.md` files are never overwritten unless `--force` is explicit.
|
|
224
232
|
|
|
225
233
|
## GitHub Action
|
|
226
234
|
|
|
@@ -231,11 +239,12 @@ The reusable Action is published on GitHub Marketplace.
|
|
|
231
239
|
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
|
|
232
240
|
with:
|
|
233
241
|
python-version: "3.13"
|
|
234
|
-
- uses: kohli217/codex-workspace-bootstrap@v0.
|
|
242
|
+
- uses: kohli217/codex-workspace-bootstrap@v0.7.0
|
|
235
243
|
with:
|
|
236
244
|
path: .
|
|
237
245
|
strict: "true"
|
|
238
246
|
fail_on_integrity: "true"
|
|
247
|
+
require_ready: "true"
|
|
239
248
|
```
|
|
240
249
|
|
|
241
250
|
The Action adds the preflight Markdown report to the **GitHub Actions job summary**, so maintainers get a readable readiness snapshot without digging through raw logs. Optional SARIF output contains both repository-audit and instruction-integrity findings and can be uploaded to GitHub Code Scanning.
|
|
@@ -263,7 +272,7 @@ The intent is to complement those tools, not replace them.
|
|
|
263
272
|
- Suspected secret files are not opened or printed by the filename-risk check.
|
|
264
273
|
- Repository contents are not sent to a remote AI service by the core audit.
|
|
265
274
|
- Instruction files are read locally for deterministic linting; extracted commands are never executed by the integrity lint.
|
|
266
|
-
- Repository instruction/configuration inputs are not
|
|
275
|
+
- Repository instruction/configuration and project-marker inputs are not trusted through symbolic links.
|
|
267
276
|
- `init-agents` and automatic fixes refuse symlinked `AGENTS.md` targets rather than writing through them.
|
|
268
277
|
- Existing regular `AGENTS.md` files are protected unless overwrite is explicit.
|
|
269
278
|
- A passing preflight is evidence about the checks performed, **not a security guarantee**.
|
|
@@ -297,7 +306,7 @@ py -m pip install codex-workspace-bootstrap
|
|
|
297
306
|
Pinned GitHub release artifact:
|
|
298
307
|
|
|
299
308
|
```powershell
|
|
300
|
-
py -m pip install "https://github.com/kohli217/codex-workspace-bootstrap/releases/download/v0.
|
|
309
|
+
py -m pip install "https://github.com/kohli217/codex-workspace-bootstrap/releases/download/v0.7.0/codex_workspace_bootstrap-0.7.0-py3-none-any.whl"
|
|
301
310
|
```
|
|
302
311
|
|
|
303
312
|
## Development
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "codex-workspace-bootstrap"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.7.0"
|
|
8
8
|
description = "Preflight AI coding repositories before Codex, Copilot, Cline, Claude, Gemini, Continue, or Cursor touches them."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -8,6 +8,16 @@ import re
|
|
|
8
8
|
|
|
9
9
|
PYTHON_MARKERS = ("pyproject.toml", "requirements.txt", "setup.py", "setup.cfg")
|
|
10
10
|
NODE_MARKER = "package.json"
|
|
11
|
+
GO_MARKERS = ("go.mod",)
|
|
12
|
+
RUST_MARKERS = ("Cargo.toml",)
|
|
13
|
+
JVM_MARKERS = (
|
|
14
|
+
"pom.xml",
|
|
15
|
+
"build.gradle",
|
|
16
|
+
"build.gradle.kts",
|
|
17
|
+
"settings.gradle",
|
|
18
|
+
"settings.gradle.kts",
|
|
19
|
+
)
|
|
20
|
+
DOTNET_SUFFIXES = (".sln", ".csproj", ".fsproj", ".vbproj")
|
|
11
21
|
README_NAMES = ("README.md", "README.rst", "README.txt", "README")
|
|
12
22
|
|
|
13
23
|
|
|
@@ -18,12 +28,27 @@ class ValidationCommand:
|
|
|
18
28
|
review_required: bool = False
|
|
19
29
|
|
|
20
30
|
|
|
31
|
+
def _regular_file(path: Path) -> bool:
|
|
32
|
+
return path.is_file() and not path.is_symlink()
|
|
33
|
+
|
|
34
|
+
|
|
21
35
|
def detect_project_signals(root: Path) -> list[str]:
|
|
22
36
|
signals: list[str] = []
|
|
23
|
-
if any((root / marker)
|
|
37
|
+
if any(_regular_file(root / marker) for marker in PYTHON_MARKERS):
|
|
24
38
|
signals.append("Python")
|
|
25
|
-
if (root / NODE_MARKER)
|
|
39
|
+
if _regular_file(root / NODE_MARKER):
|
|
26
40
|
signals.append("Node.js")
|
|
41
|
+
if any(_regular_file(root / marker) for marker in GO_MARKERS):
|
|
42
|
+
signals.append("Go")
|
|
43
|
+
if any(_regular_file(root / marker) for marker in RUST_MARKERS):
|
|
44
|
+
signals.append("Rust")
|
|
45
|
+
if any(_regular_file(root / marker) for marker in JVM_MARKERS):
|
|
46
|
+
signals.append("JVM")
|
|
47
|
+
if any(
|
|
48
|
+
_regular_file(path) and path.suffix.lower() in DOTNET_SUFFIXES
|
|
49
|
+
for path in root.iterdir()
|
|
50
|
+
):
|
|
51
|
+
signals.append(".NET")
|
|
27
52
|
return signals
|
|
28
53
|
|
|
29
54
|
|
|
@@ -44,7 +69,7 @@ def _node_script_names(root: Path) -> set[str]:
|
|
|
44
69
|
return {name for name, value in scripts.items() if isinstance(name, str) and isinstance(value, str)}
|
|
45
70
|
|
|
46
71
|
|
|
47
|
-
def
|
|
72
|
+
def detect_node_package_managers(root: Path) -> set[str]:
|
|
48
73
|
managers: set[str] = set()
|
|
49
74
|
package_json = root / NODE_MARKER
|
|
50
75
|
|
|
@@ -154,8 +179,8 @@ def validation_plan(root: Path) -> list[ValidationCommand]:
|
|
|
154
179
|
if "Python" in signals:
|
|
155
180
|
pytest_evidence = (
|
|
156
181
|
_pyproject_has_pytest(root)
|
|
157
|
-
or (root / "pytest.ini")
|
|
158
|
-
or (root / "conftest.py")
|
|
182
|
+
or _regular_file(root / "pytest.ini")
|
|
183
|
+
or _regular_file(root / "conftest.py")
|
|
159
184
|
)
|
|
160
185
|
if "python -m pytest" in documented or "pytest" in documented:
|
|
161
186
|
plan.append(ValidationCommand("python -m pytest", "documented in README"))
|
|
@@ -174,7 +199,7 @@ def validation_plan(root: Path) -> list[ValidationCommand]:
|
|
|
174
199
|
|
|
175
200
|
if "Node.js" in signals:
|
|
176
201
|
scripts = _node_script_names(root)
|
|
177
|
-
managers =
|
|
202
|
+
managers = detect_node_package_managers(root)
|
|
178
203
|
manager = next(iter(managers)) if len(managers) == 1 else None
|
|
179
204
|
|
|
180
205
|
if manager:
|
|
@@ -244,7 +269,7 @@ def validation_commands(root: Path) -> list[str]:
|
|
|
244
269
|
|
|
245
270
|
def generate_agents(root: Path) -> str:
|
|
246
271
|
signals = detect_project_signals(root)
|
|
247
|
-
signal_text = ", ".join(signals) if signals else "No
|
|
272
|
+
signal_text = ", ".join(signals) if signals else "No supported project manifest detected"
|
|
248
273
|
plan = validation_plan(root)
|
|
249
274
|
|
|
250
275
|
confirmed = [item for item in plan if not item.review_required]
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict, dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from typing import Iterable
|
|
10
|
+
|
|
11
|
+
from .agents import detect_node_package_managers
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class Check:
|
|
16
|
+
name: str
|
|
17
|
+
status: str
|
|
18
|
+
message: str
|
|
19
|
+
blocking: bool = False
|
|
20
|
+
|
|
21
|
+
def to_dict(self) -> dict[str, object]:
|
|
22
|
+
return asdict(self)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
COMMON_TOOLS: tuple[tuple[str, tuple[str, ...]], ...] = (
|
|
26
|
+
("git", ("git", "--version")),
|
|
27
|
+
("python", ("python", "--version")),
|
|
28
|
+
("node", ("node", "--version")),
|
|
29
|
+
("powershell", ("powershell", "-NoProfile", "-Command", "$PSVersionTable.PSVersion.ToString()")),
|
|
30
|
+
("wsl", ("wsl", "--status")),
|
|
31
|
+
("codex", ("codex", "--version")),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
MANIFESTS = (
|
|
35
|
+
"pyproject.toml",
|
|
36
|
+
"requirements.txt",
|
|
37
|
+
"package.json",
|
|
38
|
+
"go.mod",
|
|
39
|
+
"Cargo.toml",
|
|
40
|
+
"pom.xml",
|
|
41
|
+
"build.gradle",
|
|
42
|
+
"build.gradle.kts",
|
|
43
|
+
"settings.gradle",
|
|
44
|
+
"settings.gradle.kts",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
RISK_FILENAMES = {
|
|
48
|
+
".env",
|
|
49
|
+
".env.local",
|
|
50
|
+
"credentials.json",
|
|
51
|
+
"service-account.json",
|
|
52
|
+
"id_rsa",
|
|
53
|
+
"id_ed25519",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
RISK_SUFFIXES = (".pem", ".p12", ".pfx", ".key")
|
|
57
|
+
SAFE_ENV_TEMPLATE_NAMES = {
|
|
58
|
+
".env.example",
|
|
59
|
+
".env.sample",
|
|
60
|
+
".env.template",
|
|
61
|
+
".env.dist",
|
|
62
|
+
".env.defaults",
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
NODE_PACKAGE_MANAGER_COMMANDS: dict[str, tuple[str, ...]] = {
|
|
66
|
+
"npm": ("npm", "--version"),
|
|
67
|
+
"pnpm": ("pnpm", "--version"),
|
|
68
|
+
"yarn": ("yarn", "--version"),
|
|
69
|
+
"bun": ("bun", "--version"),
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _tool_check(label: str, command: tuple[str, ...]) -> Check:
|
|
74
|
+
executable = shutil.which(command[0])
|
|
75
|
+
if not executable:
|
|
76
|
+
return Check(label, "warn", f"{command[0]} command not found")
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
result = subprocess.run(
|
|
80
|
+
command,
|
|
81
|
+
capture_output=True,
|
|
82
|
+
text=True,
|
|
83
|
+
timeout=5,
|
|
84
|
+
check=False,
|
|
85
|
+
)
|
|
86
|
+
output = (result.stdout or result.stderr).strip().splitlines()
|
|
87
|
+
detail = output[0] if output else "available"
|
|
88
|
+
if result.returncode == 0:
|
|
89
|
+
return Check(label, "pass", detail)
|
|
90
|
+
return Check(label, "warn", f"available but returned exit code {result.returncode}")
|
|
91
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
92
|
+
return Check(label, "warn", f"could not execute: {exc}")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _iter_project_files(root: Path) -> Iterable[Path]:
|
|
96
|
+
excluded = {
|
|
97
|
+
".git",
|
|
98
|
+
".venv",
|
|
99
|
+
"venv",
|
|
100
|
+
"node_modules",
|
|
101
|
+
"__pycache__",
|
|
102
|
+
".pytest_cache",
|
|
103
|
+
".mypy_cache",
|
|
104
|
+
".ruff_cache",
|
|
105
|
+
".tox",
|
|
106
|
+
".nox",
|
|
107
|
+
"dist",
|
|
108
|
+
"build",
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for current, dirnames, filenames in os.walk(root, followlinks=False):
|
|
112
|
+
current_path = Path(current)
|
|
113
|
+
dirnames[:] = [
|
|
114
|
+
name
|
|
115
|
+
for name in dirnames
|
|
116
|
+
if name not in excluded and not (current_path / name).is_symlink()
|
|
117
|
+
]
|
|
118
|
+
for filename in filenames:
|
|
119
|
+
yield current_path / filename
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _git_tracked_files(root: Path) -> set[str] | None:
|
|
123
|
+
if shutil.which("git") is None:
|
|
124
|
+
return None
|
|
125
|
+
try:
|
|
126
|
+
result = subprocess.run(
|
|
127
|
+
("git", "-C", str(root), "ls-files", "-z"),
|
|
128
|
+
capture_output=True,
|
|
129
|
+
text=False,
|
|
130
|
+
timeout=10,
|
|
131
|
+
check=False,
|
|
132
|
+
)
|
|
133
|
+
if result.returncode != 0:
|
|
134
|
+
return None
|
|
135
|
+
return {
|
|
136
|
+
item.decode(sys.getfilesystemencoding(), errors="surrogateescape")
|
|
137
|
+
for item in result.stdout.split(b"\0")
|
|
138
|
+
if item
|
|
139
|
+
}
|
|
140
|
+
except (OSError, subprocess.SubprocessError):
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _git_matches(root: Path, args: tuple[str, ...]) -> bool:
|
|
145
|
+
if shutil.which("git") is None:
|
|
146
|
+
return False
|
|
147
|
+
try:
|
|
148
|
+
result = subprocess.run(
|
|
149
|
+
("git", "-C", str(root), *args),
|
|
150
|
+
capture_output=True,
|
|
151
|
+
text=True,
|
|
152
|
+
timeout=5,
|
|
153
|
+
check=False,
|
|
154
|
+
)
|
|
155
|
+
return result.returncode == 0
|
|
156
|
+
except (OSError, subprocess.SubprocessError):
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _classify_risky_paths(
|
|
161
|
+
root: Path,
|
|
162
|
+
risky: list[str],
|
|
163
|
+
tracked_files: set[str] | None = None,
|
|
164
|
+
) -> tuple[list[str], list[str], list[str]]:
|
|
165
|
+
tracked: list[str] = []
|
|
166
|
+
ignored: list[str] = []
|
|
167
|
+
untracked: list[str] = []
|
|
168
|
+
|
|
169
|
+
for relative in risky:
|
|
170
|
+
if tracked_files is not None and relative in tracked_files:
|
|
171
|
+
tracked.append(relative)
|
|
172
|
+
elif tracked_files is None and _git_matches(root, ("ls-files", "--error-unmatch", "--", relative)):
|
|
173
|
+
tracked.append(relative)
|
|
174
|
+
elif _git_matches(root, ("check-ignore", "--quiet", "--", relative)):
|
|
175
|
+
ignored.append(relative)
|
|
176
|
+
else:
|
|
177
|
+
untracked.append(relative)
|
|
178
|
+
|
|
179
|
+
return tracked, ignored, untracked
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _is_risky_filename(name: str) -> bool:
|
|
183
|
+
lowered = name.lower()
|
|
184
|
+
if lowered in RISK_FILENAMES or lowered.endswith(RISK_SUFFIXES):
|
|
185
|
+
return True
|
|
186
|
+
if lowered.startswith(".env.") and lowered not in SAFE_ENV_TEMPLATE_NAMES:
|
|
187
|
+
return True
|
|
188
|
+
return False
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _preview(paths: list[str], limit: int = 8) -> str:
|
|
192
|
+
visible = ", ".join(sorted(paths)[:limit])
|
|
193
|
+
suffix = "" if len(paths) <= limit else f" (+{len(paths) - limit} more)"
|
|
194
|
+
return f"{visible}{suffix}"
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def audit_repository(root: Path) -> list[Check]:
|
|
198
|
+
root = root.resolve()
|
|
199
|
+
checks: list[Check] = []
|
|
200
|
+
|
|
201
|
+
git_dir = root / ".git"
|
|
202
|
+
checks.append(
|
|
203
|
+
Check(
|
|
204
|
+
"git-repository",
|
|
205
|
+
"pass" if git_dir.exists() else "warn",
|
|
206
|
+
"Git repository detected" if git_dir.exists() else ".git directory not found",
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
readme = next(
|
|
211
|
+
(
|
|
212
|
+
p
|
|
213
|
+
for p in root.iterdir()
|
|
214
|
+
if p.is_file() and not p.is_symlink() and p.name.lower().startswith("readme")
|
|
215
|
+
),
|
|
216
|
+
None,
|
|
217
|
+
)
|
|
218
|
+
checks.append(
|
|
219
|
+
Check(
|
|
220
|
+
"readme",
|
|
221
|
+
"pass" if readme else "warn",
|
|
222
|
+
f"README detected: {readme.name}" if readme else "README not found",
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
license_file = next(
|
|
227
|
+
(
|
|
228
|
+
p
|
|
229
|
+
for p in root.iterdir()
|
|
230
|
+
if p.is_file() and not p.is_symlink() and p.name.lower().startswith("license")
|
|
231
|
+
),
|
|
232
|
+
None,
|
|
233
|
+
)
|
|
234
|
+
checks.append(
|
|
235
|
+
Check(
|
|
236
|
+
"license",
|
|
237
|
+
"pass" if license_file else "warn",
|
|
238
|
+
f"License detected: {license_file.name}" if license_file else "License file not found",
|
|
239
|
+
)
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
checks.append(
|
|
243
|
+
Check(
|
|
244
|
+
"gitignore",
|
|
245
|
+
"pass"
|
|
246
|
+
if (root / ".gitignore").is_file() and not (root / ".gitignore").is_symlink()
|
|
247
|
+
else "warn",
|
|
248
|
+
".gitignore detected"
|
|
249
|
+
if (root / ".gitignore").is_file() and not (root / ".gitignore").is_symlink()
|
|
250
|
+
else ".gitignore not found",
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
checks.append(
|
|
255
|
+
Check(
|
|
256
|
+
"agents",
|
|
257
|
+
"pass"
|
|
258
|
+
if (root / "AGENTS.md").is_file() and not (root / "AGENTS.md").is_symlink()
|
|
259
|
+
else "warn",
|
|
260
|
+
"AGENTS.md detected"
|
|
261
|
+
if (root / "AGENTS.md").is_file() and not (root / "AGENTS.md").is_symlink()
|
|
262
|
+
else "AGENTS.md not found; run init-agents",
|
|
263
|
+
)
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
manifests = [
|
|
267
|
+
name
|
|
268
|
+
for name in MANIFESTS
|
|
269
|
+
if (root / name).is_file() and not (root / name).is_symlink()
|
|
270
|
+
]
|
|
271
|
+
manifests.extend(
|
|
272
|
+
sorted(
|
|
273
|
+
path.name
|
|
274
|
+
for path in root.iterdir()
|
|
275
|
+
if path.is_file()
|
|
276
|
+
and not path.is_symlink()
|
|
277
|
+
and path.suffix.lower() in {".sln", ".csproj", ".fsproj", ".vbproj"}
|
|
278
|
+
)
|
|
279
|
+
)
|
|
280
|
+
checks.append(
|
|
281
|
+
Check(
|
|
282
|
+
"project-manifest",
|
|
283
|
+
"pass" if manifests else "warn",
|
|
284
|
+
f"Detected: {', '.join(manifests)}" if manifests else "No common project manifest detected",
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
for label, command in COMMON_TOOLS:
|
|
289
|
+
checks.append(_tool_check(label, command))
|
|
290
|
+
|
|
291
|
+
if (root / "package.json").is_file() and not (root / "package.json").is_symlink():
|
|
292
|
+
managers = detect_node_package_managers(root)
|
|
293
|
+
if managers:
|
|
294
|
+
if len(managers) > 1:
|
|
295
|
+
checks.append(
|
|
296
|
+
Check(
|
|
297
|
+
"package-manager-evidence",
|
|
298
|
+
"warn",
|
|
299
|
+
"Conflicting Node.js package-manager evidence detected: "
|
|
300
|
+
+ ", ".join(sorted(managers)),
|
|
301
|
+
)
|
|
302
|
+
)
|
|
303
|
+
for manager in sorted(managers):
|
|
304
|
+
command = NODE_PACKAGE_MANAGER_COMMANDS[manager]
|
|
305
|
+
checks.append(_tool_check(manager, command))
|
|
306
|
+
else:
|
|
307
|
+
checks.append(
|
|
308
|
+
Check(
|
|
309
|
+
"package-manager-evidence",
|
|
310
|
+
"warn",
|
|
311
|
+
"Node.js project detected, but packageManager/lockfile evidence does not confirm npm, pnpm, yarn, or bun",
|
|
312
|
+
)
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
tracked_files = _git_tracked_files(root)
|
|
316
|
+
risky: set[str] = set()
|
|
317
|
+
|
|
318
|
+
for path in _iter_project_files(root):
|
|
319
|
+
if _is_risky_filename(path.name):
|
|
320
|
+
risky.add(path.relative_to(root).as_posix())
|
|
321
|
+
|
|
322
|
+
# Generated/dependency directories are pruned from the filesystem walk to
|
|
323
|
+
# avoid expensive scans. Tracked files are different: if a risky filename
|
|
324
|
+
# is committed, it must still be surfaced even inside a pruned directory.
|
|
325
|
+
if tracked_files is not None:
|
|
326
|
+
risky.update(
|
|
327
|
+
relative
|
|
328
|
+
for relative in tracked_files
|
|
329
|
+
if _is_risky_filename(Path(relative).name)
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
if risky:
|
|
333
|
+
tracked, ignored, untracked = _classify_risky_paths(
|
|
334
|
+
root,
|
|
335
|
+
sorted(risky),
|
|
336
|
+
tracked_files,
|
|
337
|
+
)
|
|
338
|
+
parts: list[str] = []
|
|
339
|
+
if tracked:
|
|
340
|
+
parts.append(f"tracked: {_preview(tracked)}")
|
|
341
|
+
if ignored:
|
|
342
|
+
parts.append(f"ignored: {_preview(ignored)}")
|
|
343
|
+
if untracked:
|
|
344
|
+
parts.append(f"untracked/unknown: {_preview(untracked)}")
|
|
345
|
+
|
|
346
|
+
checks.append(
|
|
347
|
+
Check(
|
|
348
|
+
"secret-risk-files",
|
|
349
|
+
"warn",
|
|
350
|
+
"Potential secret-bearing filenames detected (" + "; ".join(parts) + "). "
|
|
351
|
+
"The audit does not read file contents.",
|
|
352
|
+
blocking=bool(tracked),
|
|
353
|
+
)
|
|
354
|
+
)
|
|
355
|
+
else:
|
|
356
|
+
checks.append(Check("secret-risk-files", "pass", "No common secret-bearing filenames detected"))
|
|
357
|
+
|
|
358
|
+
return checks
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def summary(checks: list[Check]) -> dict[str, int]:
|
|
362
|
+
return {
|
|
363
|
+
"passed": sum(c.status == "pass" for c in checks),
|
|
364
|
+
"warnings": sum(c.status == "warn" for c in checks),
|
|
365
|
+
"blocking": sum(c.blocking for c in checks),
|
|
366
|
+
}
|
|
@@ -41,6 +41,11 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
41
41
|
action="store_true",
|
|
42
42
|
help="Return a non-zero exit code when AI instruction integrity findings are present",
|
|
43
43
|
)
|
|
44
|
+
preflight.add_argument(
|
|
45
|
+
"--require-ready",
|
|
46
|
+
action="store_true",
|
|
47
|
+
help="Return a non-zero exit code unless the preflight state is READY",
|
|
48
|
+
)
|
|
44
49
|
|
|
45
50
|
fix = sub.add_parser("fix", help="Preview safe repository-readiness fixes")
|
|
46
51
|
fix.add_argument("path", nargs="?", default=".")
|
|
@@ -85,6 +90,7 @@ def _run_preflight(
|
|
|
85
90
|
sarif_path: str | None,
|
|
86
91
|
strict: bool,
|
|
87
92
|
fail_on_integrity: bool,
|
|
93
|
+
require_ready: bool,
|
|
88
94
|
) -> int:
|
|
89
95
|
root = Path(path).expanduser().resolve()
|
|
90
96
|
if not root.exists() or not root.is_dir():
|
|
@@ -161,6 +167,8 @@ def _run_preflight(
|
|
|
161
167
|
return 1
|
|
162
168
|
if fail_on_integrity and report["instruction_summary"]["findings"]:
|
|
163
169
|
return 1
|
|
170
|
+
if require_ready and report["state"] != "READY":
|
|
171
|
+
return 1
|
|
164
172
|
return 0
|
|
165
173
|
|
|
166
174
|
|
|
@@ -292,6 +300,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
292
300
|
args.sarif_path,
|
|
293
301
|
args.strict,
|
|
294
302
|
args.fail_on_integrity,
|
|
303
|
+
args.require_ready,
|
|
295
304
|
)
|
|
296
305
|
if args.command == "fix":
|
|
297
306
|
return _run_fix(args.path, args.apply)
|
|
@@ -23,7 +23,11 @@ GUIDANCE: dict[str, str] = {
|
|
|
23
23
|
"git": "Verify Git is installed and available on PATH.",
|
|
24
24
|
"python": "Verify Python or the Windows Python launcher is installed and available on PATH.",
|
|
25
25
|
"node": "Verify Node.js is installed and available on PATH when the project requires it.",
|
|
26
|
-
"npm": "Verify npm is available
|
|
26
|
+
"npm": "Verify npm is installed and available on PATH for this repository.",
|
|
27
|
+
"pnpm": "Verify pnpm is installed and available on PATH for this repository.",
|
|
28
|
+
"yarn": "Verify Yarn is installed and available on PATH for this repository.",
|
|
29
|
+
"bun": "Verify Bun is installed and available on PATH for this repository.",
|
|
30
|
+
"package-manager-evidence": "Confirm the intended Node.js package manager in package.json packageManager or with the repository lockfile.",
|
|
27
31
|
"powershell": "Verify Windows PowerShell or PowerShell is available on PATH.",
|
|
28
32
|
"wsl": "Check WSL status only when the repository workflow requires Linux tooling.",
|
|
29
33
|
"codex": "Verify the Codex CLI is installed and available on PATH when local Codex workflows are intended.",
|