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.
Files changed (30) hide show
  1. {codex_workspace_bootstrap-0.6.0/src/codex_workspace_bootstrap.egg-info → codex_workspace_bootstrap-0.7.0}/PKG-INFO +17 -8
  2. {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/README.md +16 -7
  3. {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/pyproject.toml +1 -1
  4. {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/src/codex_workspace_bootstrap/__init__.py +1 -1
  5. {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/src/codex_workspace_bootstrap/agents.py +32 -7
  6. codex_workspace_bootstrap-0.7.0/src/codex_workspace_bootstrap/audit.py +366 -0
  7. {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/src/codex_workspace_bootstrap/cli.py +9 -0
  8. {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/src/codex_workspace_bootstrap/doctor.py +5 -1
  9. {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/src/codex_workspace_bootstrap/instructions.py +165 -36
  10. {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/src/codex_workspace_bootstrap/preflight.py +45 -6
  11. {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/src/codex_workspace_bootstrap/sarif.py +4 -0
  12. {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0/src/codex_workspace_bootstrap.egg-info}/PKG-INFO +17 -8
  13. codex_workspace_bootstrap-0.7.0/tests/test_audit.py +324 -0
  14. {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/tests/test_cli.py +97 -2
  15. {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/tests/test_doctor.py +17 -0
  16. {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/tests/test_instruction_integrity.py +111 -0
  17. {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/tests/test_preflight.py +142 -0
  18. {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/tests/test_public_repo_patterns.py +25 -1
  19. {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/tests/test_release_consistency.py +36 -0
  20. {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/tests/test_sarif.py +22 -0
  21. codex_workspace_bootstrap-0.6.0/src/codex_workspace_bootstrap/audit.py +0 -217
  22. codex_workspace_bootstrap-0.6.0/tests/test_audit.py +0 -78
  23. {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/LICENSE +0 -0
  24. {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/setup.cfg +0 -0
  25. {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/src/codex_workspace_bootstrap/__main__.py +0 -0
  26. {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/src/codex_workspace_bootstrap/fixes.py +0 -0
  27. {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/src/codex_workspace_bootstrap.egg-info/SOURCES.txt +0 -0
  28. {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/src/codex_workspace_bootstrap.egg-info/dependency_links.txt +0 -0
  29. {codex_workspace_bootstrap-0.6.0 → codex_workspace_bootstrap-0.7.0}/src/codex_workspace_bootstrap.egg-info/entry_points.txt +0 -0
  30. {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.6.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 / npm / PowerShell / WSL / Codex signals
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 cross-checks project manifests, 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.
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.6.0
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 read through symbolic links.
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.6.0/codex_workspace_bootstrap-0.6.0-py3-none-any.whl"
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 / npm / PowerShell / WSL / Codex signals
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 cross-checks project manifests, 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.
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.6.0
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 read through symbolic links.
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.6.0/codex_workspace_bootstrap-0.6.0-py3-none-any.whl"
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.6.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"
@@ -1,3 +1,3 @@
1
1
  """codex-workspace-bootstrap package."""
2
2
 
3
- __version__ = "0.6.0"
3
+ __version__ = "0.7.0"
@@ -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).exists() for marker in PYTHON_MARKERS):
37
+ if any(_regular_file(root / marker) for marker in PYTHON_MARKERS):
24
38
  signals.append("Python")
25
- if (root / NODE_MARKER).exists():
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 _node_package_managers(root: Path) -> set[str]:
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").exists()
158
- or (root / "conftest.py").exists()
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 = _node_package_managers(root)
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 common Python or Node.js manifest detected"
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 with the active Node.js installation.",
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.",