codex-workspace-bootstrap 0.6.1__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.1/src/codex_workspace_bootstrap.egg-info → codex_workspace_bootstrap-0.7.0}/PKG-INFO +15 -6
- {codex_workspace_bootstrap-0.6.1 → codex_workspace_bootstrap-0.7.0}/README.md +14 -5
- {codex_workspace_bootstrap-0.6.1 → codex_workspace_bootstrap-0.7.0}/pyproject.toml +1 -1
- {codex_workspace_bootstrap-0.6.1 → codex_workspace_bootstrap-0.7.0}/src/codex_workspace_bootstrap/__init__.py +1 -1
- {codex_workspace_bootstrap-0.6.1 → codex_workspace_bootstrap-0.7.0}/src/codex_workspace_bootstrap/agents.py +22 -1
- {codex_workspace_bootstrap-0.6.1 → codex_workspace_bootstrap-0.7.0}/src/codex_workspace_bootstrap/audit.py +79 -7
- {codex_workspace_bootstrap-0.6.1 → codex_workspace_bootstrap-0.7.0}/src/codex_workspace_bootstrap/cli.py +9 -0
- {codex_workspace_bootstrap-0.6.1 → codex_workspace_bootstrap-0.7.0}/src/codex_workspace_bootstrap/instructions.py +100 -7
- {codex_workspace_bootstrap-0.6.1 → codex_workspace_bootstrap-0.7.0}/src/codex_workspace_bootstrap/preflight.py +17 -0
- {codex_workspace_bootstrap-0.6.1 → codex_workspace_bootstrap-0.7.0/src/codex_workspace_bootstrap.egg-info}/PKG-INFO +15 -6
- {codex_workspace_bootstrap-0.6.1 → codex_workspace_bootstrap-0.7.0}/tests/test_audit.py +117 -1
- {codex_workspace_bootstrap-0.6.1 → codex_workspace_bootstrap-0.7.0}/tests/test_cli.py +64 -3
- {codex_workspace_bootstrap-0.6.1 → codex_workspace_bootstrap-0.7.0}/tests/test_instruction_integrity.py +22 -0
- {codex_workspace_bootstrap-0.6.1 → codex_workspace_bootstrap-0.7.0}/tests/test_preflight.py +79 -0
- {codex_workspace_bootstrap-0.6.1 → codex_workspace_bootstrap-0.7.0}/tests/test_public_repo_patterns.py +25 -1
- {codex_workspace_bootstrap-0.6.1 → codex_workspace_bootstrap-0.7.0}/tests/test_release_consistency.py +36 -0
- {codex_workspace_bootstrap-0.6.1 → codex_workspace_bootstrap-0.7.0}/LICENSE +0 -0
- {codex_workspace_bootstrap-0.6.1 → codex_workspace_bootstrap-0.7.0}/setup.cfg +0 -0
- {codex_workspace_bootstrap-0.6.1 → codex_workspace_bootstrap-0.7.0}/src/codex_workspace_bootstrap/__main__.py +0 -0
- {codex_workspace_bootstrap-0.6.1 → codex_workspace_bootstrap-0.7.0}/src/codex_workspace_bootstrap/doctor.py +0 -0
- {codex_workspace_bootstrap-0.6.1 → codex_workspace_bootstrap-0.7.0}/src/codex_workspace_bootstrap/fixes.py +0 -0
- {codex_workspace_bootstrap-0.6.1 → codex_workspace_bootstrap-0.7.0}/src/codex_workspace_bootstrap/sarif.py +0 -0
- {codex_workspace_bootstrap-0.6.1 → codex_workspace_bootstrap-0.7.0}/src/codex_workspace_bootstrap.egg-info/SOURCES.txt +0 -0
- {codex_workspace_bootstrap-0.6.1 → codex_workspace_bootstrap-0.7.0}/src/codex_workspace_bootstrap.egg-info/dependency_links.txt +0 -0
- {codex_workspace_bootstrap-0.6.1 → codex_workspace_bootstrap-0.7.0}/src/codex_workspace_bootstrap.egg-info/entry_points.txt +0 -0
- {codex_workspace_bootstrap-0.6.1 → codex_workspace_bootstrap-0.7.0}/src/codex_workspace_bootstrap.egg-info/top_level.txt +0 -0
- {codex_workspace_bootstrap-0.6.1 → codex_workspace_bootstrap-0.7.0}/tests/test_doctor.py +0 -0
- {codex_workspace_bootstrap-0.6.1 → codex_workspace_bootstrap-0.7.0}/tests/test_sarif.py +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,7 +123,7 @@ 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
|
|
126
|
+
- common project manifests, including Python, Node.js, Go, Rust, JVM, and .NET roots
|
|
127
127
|
- local Git / Python / Node.js / repository-selected package manager / PowerShell / WSL / Codex signals
|
|
128
128
|
|
|
129
129
|
### AI instruction coverage
|
|
@@ -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.
|
|
@@ -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,7 +95,7 @@ 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
|
|
98
|
+
- common project manifests, including Python, Node.js, Go, Rust, JVM, and .NET roots
|
|
99
99
|
- local Git / Python / Node.js / repository-selected package manager / PowerShell / WSL / Codex signals
|
|
100
100
|
|
|
101
101
|
### AI instruction coverage
|
|
@@ -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.
|
|
@@ -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
|
|
|
@@ -28,6 +38,17 @@ def detect_project_signals(root: Path) -> list[str]:
|
|
|
28
38
|
signals.append("Python")
|
|
29
39
|
if _regular_file(root / NODE_MARKER):
|
|
30
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")
|
|
31
52
|
return signals
|
|
32
53
|
|
|
33
54
|
|
|
@@ -248,7 +269,7 @@ def validation_commands(root: Path) -> list[str]:
|
|
|
248
269
|
|
|
249
270
|
def generate_agents(root: Path) -> str:
|
|
250
271
|
signals = detect_project_signals(root)
|
|
251
|
-
signal_text = ", ".join(signals) if signals else "No
|
|
272
|
+
signal_text = ", ".join(signals) if signals else "No supported project manifest detected"
|
|
252
273
|
plan = validation_plan(root)
|
|
253
274
|
|
|
254
275
|
confirmed = [item for item in plan if not item.review_required]
|
|
@@ -5,6 +5,7 @@ from pathlib import Path
|
|
|
5
5
|
import os
|
|
6
6
|
import shutil
|
|
7
7
|
import subprocess
|
|
8
|
+
import sys
|
|
8
9
|
from typing import Iterable
|
|
9
10
|
|
|
10
11
|
from .agents import detect_node_package_managers
|
|
@@ -38,6 +39,9 @@ MANIFESTS = (
|
|
|
38
39
|
"Cargo.toml",
|
|
39
40
|
"pom.xml",
|
|
40
41
|
"build.gradle",
|
|
42
|
+
"build.gradle.kts",
|
|
43
|
+
"settings.gradle",
|
|
44
|
+
"settings.gradle.kts",
|
|
41
45
|
)
|
|
42
46
|
|
|
43
47
|
RISK_FILENAMES = {
|
|
@@ -50,6 +54,13 @@ RISK_FILENAMES = {
|
|
|
50
54
|
}
|
|
51
55
|
|
|
52
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
|
+
}
|
|
53
64
|
|
|
54
65
|
NODE_PACKAGE_MANAGER_COMMANDS: dict[str, tuple[str, ...]] = {
|
|
55
66
|
"npm": ("npm", "--version"),
|
|
@@ -108,6 +119,28 @@ def _iter_project_files(root: Path) -> Iterable[Path]:
|
|
|
108
119
|
yield current_path / filename
|
|
109
120
|
|
|
110
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
|
+
|
|
111
144
|
def _git_matches(root: Path, args: tuple[str, ...]) -> bool:
|
|
112
145
|
if shutil.which("git") is None:
|
|
113
146
|
return False
|
|
@@ -124,13 +157,19 @@ def _git_matches(root: Path, args: tuple[str, ...]) -> bool:
|
|
|
124
157
|
return False
|
|
125
158
|
|
|
126
159
|
|
|
127
|
-
def _classify_risky_paths(
|
|
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]]:
|
|
128
165
|
tracked: list[str] = []
|
|
129
166
|
ignored: list[str] = []
|
|
130
167
|
untracked: list[str] = []
|
|
131
168
|
|
|
132
169
|
for relative in risky:
|
|
133
|
-
if
|
|
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)):
|
|
134
173
|
tracked.append(relative)
|
|
135
174
|
elif _git_matches(root, ("check-ignore", "--quiet", "--", relative)):
|
|
136
175
|
ignored.append(relative)
|
|
@@ -140,6 +179,15 @@ def _classify_risky_paths(root: Path, risky: list[str]) -> tuple[list[str], list
|
|
|
140
179
|
return tracked, ignored, untracked
|
|
141
180
|
|
|
142
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
|
+
|
|
143
191
|
def _preview(paths: list[str], limit: int = 8) -> str:
|
|
144
192
|
visible = ", ".join(sorted(paths)[:limit])
|
|
145
193
|
suffix = "" if len(paths) <= limit else f" (+{len(paths) - limit} more)"
|
|
@@ -220,6 +268,15 @@ def audit_repository(root: Path) -> list[Check]:
|
|
|
220
268
|
for name in MANIFESTS
|
|
221
269
|
if (root / name).is_file() and not (root / name).is_symlink()
|
|
222
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
|
+
)
|
|
223
280
|
checks.append(
|
|
224
281
|
Check(
|
|
225
282
|
"project-manifest",
|
|
@@ -255,14 +312,29 @@ def audit_repository(root: Path) -> list[Check]:
|
|
|
255
312
|
)
|
|
256
313
|
)
|
|
257
314
|
|
|
258
|
-
|
|
315
|
+
tracked_files = _git_tracked_files(root)
|
|
316
|
+
risky: set[str] = set()
|
|
317
|
+
|
|
259
318
|
for path in _iter_project_files(root):
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
+
)
|
|
263
331
|
|
|
264
332
|
if risky:
|
|
265
|
-
tracked, ignored, untracked = _classify_risky_paths(
|
|
333
|
+
tracked, ignored, untracked = _classify_risky_paths(
|
|
334
|
+
root,
|
|
335
|
+
sorted(risky),
|
|
336
|
+
tracked_files,
|
|
337
|
+
)
|
|
266
338
|
parts: list[str] = []
|
|
267
339
|
if tracked:
|
|
268
340
|
parts.append(f"tracked: {_preview(tracked)}")
|
|
@@ -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)
|
|
@@ -148,15 +148,108 @@ def _frontmatter_value(text: str, keys: tuple[str, ...]) -> str | None:
|
|
|
148
148
|
return None
|
|
149
149
|
|
|
150
150
|
|
|
151
|
+
def _split_top_level_commas(value: str) -> list[str]:
|
|
152
|
+
items: list[str] = []
|
|
153
|
+
current: list[str] = []
|
|
154
|
+
quote: str | None = None
|
|
155
|
+
escaped = False
|
|
156
|
+
braces = 0
|
|
157
|
+
brackets = 0
|
|
158
|
+
parens = 0
|
|
159
|
+
|
|
160
|
+
for char in value:
|
|
161
|
+
if escaped:
|
|
162
|
+
current.append(char)
|
|
163
|
+
escaped = False
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
if char == "\\":
|
|
167
|
+
current.append(char)
|
|
168
|
+
escaped = True
|
|
169
|
+
continue
|
|
170
|
+
|
|
171
|
+
if quote:
|
|
172
|
+
current.append(char)
|
|
173
|
+
if char == quote:
|
|
174
|
+
quote = None
|
|
175
|
+
continue
|
|
176
|
+
|
|
177
|
+
if char in {"'", '"'}:
|
|
178
|
+
quote = char
|
|
179
|
+
current.append(char)
|
|
180
|
+
continue
|
|
181
|
+
|
|
182
|
+
if char == "{":
|
|
183
|
+
braces += 1
|
|
184
|
+
elif char == "}":
|
|
185
|
+
braces = max(0, braces - 1)
|
|
186
|
+
elif char == "[":
|
|
187
|
+
brackets += 1
|
|
188
|
+
elif char == "]":
|
|
189
|
+
brackets = max(0, brackets - 1)
|
|
190
|
+
elif char == "(":
|
|
191
|
+
parens += 1
|
|
192
|
+
elif char == ")":
|
|
193
|
+
parens = max(0, parens - 1)
|
|
194
|
+
|
|
195
|
+
if char == "," and braces == 0 and brackets == 0 and parens == 0:
|
|
196
|
+
item = "".join(current).strip()
|
|
197
|
+
if item:
|
|
198
|
+
items.append(item)
|
|
199
|
+
current = []
|
|
200
|
+
continue
|
|
201
|
+
|
|
202
|
+
current.append(char)
|
|
203
|
+
|
|
204
|
+
item = "".join(current).strip()
|
|
205
|
+
if item:
|
|
206
|
+
items.append(item)
|
|
207
|
+
return items
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _strip_matching_quotes(value: str) -> str:
|
|
211
|
+
value = value.strip()
|
|
212
|
+
if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}:
|
|
213
|
+
return value[1:-1].strip()
|
|
214
|
+
return value
|
|
215
|
+
|
|
216
|
+
|
|
151
217
|
def _split_patterns(value: str) -> list[str]:
|
|
152
|
-
value = value.strip()
|
|
218
|
+
value = value.strip()
|
|
219
|
+
|
|
220
|
+
# Inline YAML arrays are common, but a glob character class such as [ab]
|
|
221
|
+
# is also valid. Only treat brackets as an array when the interior looks
|
|
222
|
+
# list-like.
|
|
153
223
|
if value.startswith("[") and value.endswith("]"):
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
]
|
|
224
|
+
inner = value[1:-1].strip()
|
|
225
|
+
if "," in inner or inner.startswith(("'", '"')):
|
|
226
|
+
value = inner
|
|
227
|
+
|
|
228
|
+
raw_items = _split_top_level_commas(value)
|
|
229
|
+
patterns: list[str] = []
|
|
230
|
+
|
|
231
|
+
for raw in raw_items:
|
|
232
|
+
item = _strip_matching_quotes(raw)
|
|
233
|
+
if not item:
|
|
234
|
+
continue
|
|
235
|
+
|
|
236
|
+
# Copilot commonly wraps alternatives in one quoted brace expression:
|
|
237
|
+
# "{src/**/test/**,src/**/*.test.ts}". Expand only a brace that wraps
|
|
238
|
+
# the whole selector; embedded brace expansion stays intact so
|
|
239
|
+
# apps/web/{src,tests}/** still has static prefix apps/web.
|
|
240
|
+
if item.startswith("{") and item.endswith("}"):
|
|
241
|
+
alternatives = [
|
|
242
|
+
_strip_matching_quotes(part)
|
|
243
|
+
for part in _split_top_level_commas(item[1:-1])
|
|
244
|
+
]
|
|
245
|
+
alternatives = [part for part in alternatives if part]
|
|
246
|
+
if len(alternatives) > 1:
|
|
247
|
+
patterns.extend(alternatives)
|
|
248
|
+
continue
|
|
249
|
+
|
|
250
|
+
patterns.append(item)
|
|
251
|
+
|
|
252
|
+
return patterns
|
|
160
253
|
|
|
161
254
|
|
|
162
255
|
def _static_prefix(pattern: str) -> str:
|
|
@@ -171,6 +171,23 @@ def build_preflight(root: Path) -> dict[str, object]:
|
|
|
171
171
|
checks = audit_repository(root)
|
|
172
172
|
instructions = detect_instruction_signals(root)
|
|
173
173
|
instruction_findings = lint_instructions(root, instructions)
|
|
174
|
+
|
|
175
|
+
root_manager_conflict = any(
|
|
176
|
+
check.name == "package-manager-evidence"
|
|
177
|
+
and check.status != "pass"
|
|
178
|
+
and check.message.startswith("Conflicting Node.js package-manager evidence")
|
|
179
|
+
for check in checks
|
|
180
|
+
)
|
|
181
|
+
if root_manager_conflict:
|
|
182
|
+
instruction_findings = [
|
|
183
|
+
finding
|
|
184
|
+
for finding in instruction_findings
|
|
185
|
+
if not (
|
|
186
|
+
finding.kind == "package-manager-evidence-conflict"
|
|
187
|
+
and finding.scope == "."
|
|
188
|
+
)
|
|
189
|
+
]
|
|
190
|
+
|
|
174
191
|
instruction_totals = finding_summary(instruction_findings)
|
|
175
192
|
projects = detect_project_signals(root)
|
|
176
193
|
totals = summary(checks)
|
|
@@ -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,7 +123,7 @@ 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
|
|
126
|
+
- common project manifests, including Python, Node.js, Go, Rust, JVM, and .NET roots
|
|
127
127
|
- local Git / Python / Node.js / repository-selected package manager / PowerShell / WSL / Codex signals
|
|
128
128
|
|
|
129
129
|
### AI instruction coverage
|
|
@@ -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.
|
|
@@ -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
|
|
@@ -4,7 +4,7 @@ import subprocess
|
|
|
4
4
|
|
|
5
5
|
import pytest
|
|
6
6
|
|
|
7
|
-
from codex_workspace_bootstrap.audit import audit_repository, summary
|
|
7
|
+
from codex_workspace_bootstrap.audit import _git_tracked_files, audit_repository, summary
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def _git(root: Path, *args: str) -> None:
|
|
@@ -206,3 +206,119 @@ def test_audit_does_not_accept_symlinked_repository_markers(
|
|
|
206
206
|
assert by_name["gitignore"].status == "warn"
|
|
207
207
|
assert by_name["agents"].status == "warn"
|
|
208
208
|
assert by_name["project-manifest"].status == "warn"
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@pytest.mark.skipif(shutil.which("git") is None, reason="git is required")
|
|
212
|
+
def test_tracked_risky_file_inside_pruned_directory_is_blocking(tmp_path: Path) -> None:
|
|
213
|
+
_git(tmp_path, "init")
|
|
214
|
+
nested = tmp_path / "node_modules" / "pkg"
|
|
215
|
+
nested.mkdir(parents=True)
|
|
216
|
+
(nested / ".env").write_text("EXAMPLE=tracked\n", encoding="utf-8")
|
|
217
|
+
_git(tmp_path, "add", "-f", "node_modules/pkg/.env")
|
|
218
|
+
|
|
219
|
+
checks = audit_repository(tmp_path)
|
|
220
|
+
by_name = {check.name: check for check in checks}
|
|
221
|
+
|
|
222
|
+
risk = by_name["secret-risk-files"]
|
|
223
|
+
assert risk.status == "warn"
|
|
224
|
+
assert risk.blocking is True
|
|
225
|
+
assert "node_modules/pkg/.env" in risk.message
|
|
226
|
+
assert "tracked:" in risk.message
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@pytest.mark.skipif(shutil.which("git") is None, reason="git is required")
|
|
230
|
+
def test_tracked_risky_file_inside_dist_is_blocking(tmp_path: Path) -> None:
|
|
231
|
+
_git(tmp_path, "init")
|
|
232
|
+
dist = tmp_path / "dist"
|
|
233
|
+
dist.mkdir()
|
|
234
|
+
(dist / "release.key").write_text("not-a-real-key\n", encoding="utf-8")
|
|
235
|
+
_git(tmp_path, "add", "-f", "dist/release.key")
|
|
236
|
+
|
|
237
|
+
checks = audit_repository(tmp_path)
|
|
238
|
+
by_name = {check.name: check for check in checks}
|
|
239
|
+
|
|
240
|
+
risk = by_name["secret-risk-files"]
|
|
241
|
+
assert risk.blocking is True
|
|
242
|
+
assert "dist/release.key" in risk.message
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@pytest.mark.parametrize(
|
|
246
|
+
"filename",
|
|
247
|
+
[
|
|
248
|
+
"build.gradle.kts",
|
|
249
|
+
"settings.gradle",
|
|
250
|
+
"settings.gradle.kts",
|
|
251
|
+
"demo.sln",
|
|
252
|
+
"demo.csproj",
|
|
253
|
+
"demo.fsproj",
|
|
254
|
+
"demo.vbproj",
|
|
255
|
+
],
|
|
256
|
+
)
|
|
257
|
+
def test_audit_recognizes_extended_project_manifests(tmp_path: Path, filename: str) -> None:
|
|
258
|
+
(tmp_path / filename).write_text("marker\n", encoding="utf-8")
|
|
259
|
+
|
|
260
|
+
checks = audit_repository(tmp_path)
|
|
261
|
+
by_name = {check.name: check for check in checks}
|
|
262
|
+
|
|
263
|
+
assert by_name["project-manifest"].status == "pass"
|
|
264
|
+
assert filename in by_name["project-manifest"].message
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@pytest.mark.parametrize(
|
|
268
|
+
"filename",
|
|
269
|
+
[
|
|
270
|
+
".env.production",
|
|
271
|
+
".env.development.local",
|
|
272
|
+
".env.test",
|
|
273
|
+
".env.staging",
|
|
274
|
+
],
|
|
275
|
+
)
|
|
276
|
+
def test_audit_warns_on_environment_specific_secret_risk_files(
|
|
277
|
+
tmp_path: Path,
|
|
278
|
+
filename: str,
|
|
279
|
+
) -> None:
|
|
280
|
+
(tmp_path / filename).write_text("TOKEN=not-a-real-secret\n", encoding="utf-8")
|
|
281
|
+
|
|
282
|
+
checks = audit_repository(tmp_path)
|
|
283
|
+
by_name = {check.name: check for check in checks}
|
|
284
|
+
|
|
285
|
+
risk = by_name["secret-risk-files"]
|
|
286
|
+
assert risk.status == "warn"
|
|
287
|
+
assert filename in risk.message
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@pytest.mark.parametrize(
|
|
291
|
+
"filename",
|
|
292
|
+
[
|
|
293
|
+
".env.example",
|
|
294
|
+
".env.sample",
|
|
295
|
+
".env.template",
|
|
296
|
+
".env.dist",
|
|
297
|
+
".env.defaults",
|
|
298
|
+
],
|
|
299
|
+
)
|
|
300
|
+
def test_audit_does_not_flag_common_env_templates(tmp_path: Path, filename: str) -> None:
|
|
301
|
+
(tmp_path / filename).write_text("TOKEN=replace-me\n", encoding="utf-8")
|
|
302
|
+
|
|
303
|
+
checks = audit_repository(tmp_path)
|
|
304
|
+
by_name = {check.name: check for check in checks}
|
|
305
|
+
|
|
306
|
+
assert by_name["secret-risk-files"].status == "pass"
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def test_git_tracked_files_uses_surrogateescape_for_path_bytes(
|
|
310
|
+
tmp_path: Path,
|
|
311
|
+
monkeypatch,
|
|
312
|
+
) -> None:
|
|
313
|
+
class Result:
|
|
314
|
+
returncode = 0
|
|
315
|
+
stdout = b"normal.txt\0invalid-\xff.env\0"
|
|
316
|
+
|
|
317
|
+
monkeypatch.setattr(shutil, "which", lambda name: "/usr/bin/git")
|
|
318
|
+
monkeypatch.setattr(subprocess, "run", lambda *args, **kwargs: Result())
|
|
319
|
+
|
|
320
|
+
tracked = _git_tracked_files(tmp_path)
|
|
321
|
+
|
|
322
|
+
assert tracked is not None
|
|
323
|
+
assert "normal.txt" in tracked
|
|
324
|
+
assert any(name.endswith(".env") for name in tracked)
|
|
@@ -11,7 +11,7 @@ def test_init_agents_creates_file(tmp_path: Path) -> None:
|
|
|
11
11
|
code = main(["init-agents", str(tmp_path)])
|
|
12
12
|
assert code == 0
|
|
13
13
|
content = (tmp_path / "AGENTS.md").read_text(encoding="utf-8")
|
|
14
|
-
assert "No
|
|
14
|
+
assert "No supported project manifest detected" in content
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def test_init_agents_generates_python_validation(tmp_path: Path) -> None:
|
|
@@ -101,7 +101,7 @@ def test_version_flag_reports_package_version(capsys: pytest.CaptureFixture[str]
|
|
|
101
101
|
main(["--version"])
|
|
102
102
|
|
|
103
103
|
assert exc_info.value.code == 0
|
|
104
|
-
assert "codex-workspace-bootstrap 0.
|
|
104
|
+
assert "codex-workspace-bootstrap 0.7.0" in capsys.readouterr().out
|
|
105
105
|
|
|
106
106
|
|
|
107
107
|
def test_audit_writes_sarif(tmp_path: Path) -> None:
|
|
@@ -296,6 +296,67 @@ def test_init_agents_ignores_symlinked_project_markers(
|
|
|
296
296
|
|
|
297
297
|
assert code == 0
|
|
298
298
|
content = (tmp_path / "AGENTS.md").read_text(encoding="utf-8")
|
|
299
|
-
assert "No
|
|
299
|
+
assert "No supported project manifest detected" in content
|
|
300
300
|
assert "python -m pytest" not in content
|
|
301
301
|
assert "pnpm run test" not in content
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
@pytest.mark.parametrize(
|
|
305
|
+
("filename", "expected_signal"),
|
|
306
|
+
[
|
|
307
|
+
("go.mod", "Go"),
|
|
308
|
+
("Cargo.toml", "Rust"),
|
|
309
|
+
("build.gradle.kts", "JVM"),
|
|
310
|
+
("demo.sln", ".NET"),
|
|
311
|
+
],
|
|
312
|
+
)
|
|
313
|
+
def test_init_agents_reports_common_project_signal_without_inventing_commands(
|
|
314
|
+
tmp_path: Path,
|
|
315
|
+
filename: str,
|
|
316
|
+
expected_signal: str,
|
|
317
|
+
) -> None:
|
|
318
|
+
(tmp_path / filename).write_text("marker\n", encoding="utf-8")
|
|
319
|
+
|
|
320
|
+
code = main(["init-agents", str(tmp_path)])
|
|
321
|
+
|
|
322
|
+
assert code == 0
|
|
323
|
+
content = (tmp_path / "AGENTS.md").read_text(encoding="utf-8")
|
|
324
|
+
assert expected_signal in content
|
|
325
|
+
assert "No supported project manifest detected" not in content
|
|
326
|
+
for unsupported_guess in ("go test", "cargo test", "gradle test", "dotnet test"):
|
|
327
|
+
assert unsupported_guess not in content
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
@pytest.mark.skipif(shutil.which("git") is None, reason="git is required")
|
|
331
|
+
def test_preflight_require_ready_fails_for_needs_attention(tmp_path: Path) -> None:
|
|
332
|
+
subprocess.run(
|
|
333
|
+
("git", "-C", str(tmp_path), "init"),
|
|
334
|
+
check=True,
|
|
335
|
+
capture_output=True,
|
|
336
|
+
text=True,
|
|
337
|
+
)
|
|
338
|
+
(tmp_path / "README.md").write_text("# demo\n", encoding="utf-8")
|
|
339
|
+
(tmp_path / ".gitignore").write_text(".venv/\n", encoding="utf-8")
|
|
340
|
+
(tmp_path / "pyproject.toml").write_text("[project]\nname='demo'\n", encoding="utf-8")
|
|
341
|
+
|
|
342
|
+
code = main(["preflight", str(tmp_path), "--require-ready"])
|
|
343
|
+
|
|
344
|
+
assert code == 1
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
@pytest.mark.skipif(shutil.which("git") is None, reason="git is required")
|
|
348
|
+
def test_preflight_require_ready_succeeds_for_ready_repository(tmp_path: Path) -> None:
|
|
349
|
+
subprocess.run(
|
|
350
|
+
("git", "-C", str(tmp_path), "init"),
|
|
351
|
+
check=True,
|
|
352
|
+
capture_output=True,
|
|
353
|
+
text=True,
|
|
354
|
+
)
|
|
355
|
+
(tmp_path / "README.md").write_text("# demo\n", encoding="utf-8")
|
|
356
|
+
(tmp_path / ".gitignore").write_text(".venv/\n", encoding="utf-8")
|
|
357
|
+
(tmp_path / "pyproject.toml").write_text("[project]\nname='demo'\n", encoding="utf-8")
|
|
358
|
+
(tmp_path / "AGENTS.md").write_text("# agents\n", encoding="utf-8")
|
|
359
|
+
|
|
360
|
+
code = main(["preflight", str(tmp_path), "--require-ready"])
|
|
361
|
+
|
|
362
|
+
assert code == 0
|
|
@@ -811,3 +811,25 @@ def test_yarn_workspace_command_validates_real_script_name(tmp_path: Path) -> No
|
|
|
811
811
|
|
|
812
812
|
assert not any(item.kind == "missing-package-script" for item in findings)
|
|
813
813
|
assert not any(item.kind == "package-manager-mismatch" for item in findings)
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
def test_embedded_brace_glob_keeps_parent_static_scope(tmp_path: Path) -> None:
|
|
817
|
+
directory = tmp_path / ".github" / "instructions"
|
|
818
|
+
directory.mkdir(parents=True)
|
|
819
|
+
(directory / "web.instructions.md").write_text(
|
|
820
|
+
"---\n"
|
|
821
|
+
'applyTo: "apps/web/{src,tests}/**/*.ts"\n'
|
|
822
|
+
"---\n"
|
|
823
|
+
"Use web validation.\n",
|
|
824
|
+
encoding="utf-8",
|
|
825
|
+
)
|
|
826
|
+
|
|
827
|
+
signals = detect_instruction_signals(tmp_path)
|
|
828
|
+
signal = next(
|
|
829
|
+
item
|
|
830
|
+
for item in signals
|
|
831
|
+
if item.path == ".github/instructions/web.instructions.md"
|
|
832
|
+
)
|
|
833
|
+
|
|
834
|
+
assert signal.kind == "path-specific"
|
|
835
|
+
assert signal.scope == "apps/web"
|
|
@@ -216,3 +216,82 @@ def test_next_actions_prioritizes_conflicting_package_manager_evidence() -> None
|
|
|
216
216
|
and item.title == "Resolve conflicting repository package-manager evidence"
|
|
217
217
|
for item in actions
|
|
218
218
|
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def test_build_preflight_deduplicates_root_package_manager_conflict(
|
|
222
|
+
tmp_path: Path,
|
|
223
|
+
monkeypatch,
|
|
224
|
+
) -> None:
|
|
225
|
+
(tmp_path / "package.json").write_text(
|
|
226
|
+
'{"packageManager":"pnpm@10","scripts":{"test":"vitest"}}',
|
|
227
|
+
encoding="utf-8",
|
|
228
|
+
)
|
|
229
|
+
(tmp_path / "package-lock.json").write_text("{}", encoding="utf-8")
|
|
230
|
+
(tmp_path / "AGENTS.md").write_text("Run §pnpm test§.\n".replace("§", "`"), encoding="utf-8")
|
|
231
|
+
|
|
232
|
+
def fake_tool_check(label: str, command: tuple[str, ...]):
|
|
233
|
+
return Check(label, "pass", "available")
|
|
234
|
+
|
|
235
|
+
monkeypatch.setattr("codex_workspace_bootstrap.audit._tool_check", fake_tool_check)
|
|
236
|
+
|
|
237
|
+
report = build_preflight(tmp_path)
|
|
238
|
+
|
|
239
|
+
audit_conflicts = [
|
|
240
|
+
item
|
|
241
|
+
for item in report["checks"]
|
|
242
|
+
if item["name"] == "package-manager-evidence"
|
|
243
|
+
and item["message"].startswith("Conflicting Node.js package-manager evidence")
|
|
244
|
+
]
|
|
245
|
+
instruction_conflicts = [
|
|
246
|
+
item
|
|
247
|
+
for item in report["instruction_findings"]
|
|
248
|
+
if item["kind"] == "package-manager-evidence-conflict"
|
|
249
|
+
and item["scope"] == "."
|
|
250
|
+
]
|
|
251
|
+
conflict_actions = [
|
|
252
|
+
item
|
|
253
|
+
for item in report["next_actions"]
|
|
254
|
+
if "package-manager" in item["title"].lower()
|
|
255
|
+
and "conflict" in item["title"].lower()
|
|
256
|
+
]
|
|
257
|
+
|
|
258
|
+
assert len(audit_conflicts) == 1
|
|
259
|
+
assert instruction_conflicts == []
|
|
260
|
+
assert len(conflict_actions) == 1
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def test_build_preflight_keeps_nested_package_manager_conflict(
|
|
264
|
+
tmp_path: Path,
|
|
265
|
+
monkeypatch,
|
|
266
|
+
) -> None:
|
|
267
|
+
(tmp_path / "package.json").write_text(
|
|
268
|
+
'{"packageManager":"npm@11","scripts":{"test":"vitest"}}',
|
|
269
|
+
encoding="utf-8",
|
|
270
|
+
)
|
|
271
|
+
(tmp_path / "package-lock.json").write_text("{}", encoding="utf-8")
|
|
272
|
+
(tmp_path / "AGENTS.md").write_text("Run §npm test§.\n".replace("§", "`"), encoding="utf-8")
|
|
273
|
+
|
|
274
|
+
nested = tmp_path / "apps" / "web"
|
|
275
|
+
nested.mkdir(parents=True)
|
|
276
|
+
(nested / "package.json").write_text(
|
|
277
|
+
'{"packageManager":"pnpm@10","scripts":{"test":"vitest"}}',
|
|
278
|
+
encoding="utf-8",
|
|
279
|
+
)
|
|
280
|
+
(nested / "package-lock.json").write_text("{}", encoding="utf-8")
|
|
281
|
+
(nested / "AGENTS.md").write_text("Run §pnpm test§.\n".replace("§", "`"), encoding="utf-8")
|
|
282
|
+
|
|
283
|
+
def fake_tool_check(label: str, command: tuple[str, ...]):
|
|
284
|
+
return Check(label, "pass", "available")
|
|
285
|
+
|
|
286
|
+
monkeypatch.setattr("codex_workspace_bootstrap.audit._tool_check", fake_tool_check)
|
|
287
|
+
|
|
288
|
+
report = build_preflight(tmp_path)
|
|
289
|
+
|
|
290
|
+
nested_conflicts = [
|
|
291
|
+
item
|
|
292
|
+
for item in report["instruction_findings"]
|
|
293
|
+
if item["kind"] == "package-manager-evidence-conflict"
|
|
294
|
+
and item["scope"] == "apps/web"
|
|
295
|
+
]
|
|
296
|
+
|
|
297
|
+
assert len(nested_conflicts) == 1
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
import json
|
|
3
3
|
|
|
4
|
-
from codex_workspace_bootstrap.instructions import lint_instructions
|
|
4
|
+
from codex_workspace_bootstrap.instructions import detect_instruction_signals, lint_instructions
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
def test_public_pattern_openai_codex_pnpm_repo_without_js_command_drift(tmp_path: Path) -> None:
|
|
@@ -70,3 +70,27 @@ def test_public_pattern_continue_gradle_rule_does_not_conflict_with_root_npm(tmp
|
|
|
70
70
|
)
|
|
71
71
|
|
|
72
72
|
assert lint_instructions(tmp_path) == []
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_public_pattern_vscode_brace_wrapped_apply_to_keeps_static_scope(tmp_path: Path) -> None:
|
|
76
|
+
"""Pattern observed in microsoft/vscode: brace-wrapped Copilot applyTo alternatives."""
|
|
77
|
+
directory = tmp_path / ".github" / "instructions"
|
|
78
|
+
directory.mkdir(parents=True)
|
|
79
|
+
(directory / "writing-tests.instructions.md").write_text(
|
|
80
|
+
"---\n"
|
|
81
|
+
"description: Test guidance\n"
|
|
82
|
+
'applyTo: "{src/vs/**/test/**,src/vs/**/*.test.ts,src/vs/**/*.integrationTest.ts}"\n'
|
|
83
|
+
"---\n"
|
|
84
|
+
"# Writing Tests\n",
|
|
85
|
+
encoding="utf-8",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
signals = detect_instruction_signals(tmp_path)
|
|
89
|
+
signal = next(
|
|
90
|
+
item
|
|
91
|
+
for item in signals
|
|
92
|
+
if item.path == ".github/instructions/writing-tests.instructions.md"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
assert signal.kind == "path-specific"
|
|
96
|
+
assert signal.scope == "src/vs"
|
|
@@ -47,3 +47,39 @@ def test_documented_third_party_actions_are_sha_pinned() -> None:
|
|
|
47
47
|
f"{relative} documents mutable ref {action}@{ref}; "
|
|
48
48
|
"pin third-party actions to a full commit SHA"
|
|
49
49
|
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_pypi_auto_publish_uses_triggering_release_artifact() -> None:
|
|
53
|
+
release = (ROOT / ".github" / "workflows" / "release.yml").read_text(encoding="utf-8")
|
|
54
|
+
publish = (ROOT / ".github" / "workflows" / "pypi-publish.yml").read_text(encoding="utf-8")
|
|
55
|
+
|
|
56
|
+
assert "name: python-package-distributions" in release
|
|
57
|
+
assert "dist/*.whl" in release
|
|
58
|
+
assert "dist/*.tar.gz" in release
|
|
59
|
+
assert "python -m twine check dist/*" in release
|
|
60
|
+
|
|
61
|
+
assert "github.event.workflow_run.id" in publish
|
|
62
|
+
assert "github-token: ${{ github.token }}" in publish
|
|
63
|
+
assert "Download exact distributions from triggering Release run" in publish
|
|
64
|
+
assert "gh release view" not in publish
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_release_documentation_matches_attestation_permissions() -> None:
|
|
68
|
+
docs = (ROOT / "docs" / "RELEASING.md").read_text(encoding="utf-8")
|
|
69
|
+
|
|
70
|
+
for permission in (
|
|
71
|
+
"contents: write",
|
|
72
|
+
"id-token: write",
|
|
73
|
+
"attestations: write",
|
|
74
|
+
"artifact-metadata: write",
|
|
75
|
+
):
|
|
76
|
+
assert f"`{permission}`" in docs
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_powershell_installer_default_matches_package_version() -> None:
|
|
80
|
+
version = _project_version()
|
|
81
|
+
installer = (ROOT / "scripts" / "install.ps1").read_text(encoding="utf-8")
|
|
82
|
+
|
|
83
|
+
assert f'[string]$Version = "{version}"' in installer
|
|
84
|
+
assert "releases/download/v$Version/codex_workspace_bootstrap-$Version-py3-none-any.whl" in installer
|
|
85
|
+
assert 'Write-Host " cwb preflight ."' in installer
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|