codex-workspace-bootstrap 0.8.0__tar.gz → 0.9.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 (50) hide show
  1. {codex_workspace_bootstrap-0.8.0/src/codex_workspace_bootstrap.egg-info → codex_workspace_bootstrap-0.9.0}/PKG-INFO +10 -6
  2. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/README.md +9 -5
  3. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/pyproject.toml +1 -1
  4. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap/__init__.py +1 -1
  5. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap/instructions.py +335 -26
  6. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0/src/codex_workspace_bootstrap.egg-info}/PKG-INFO +10 -6
  7. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/tests/test_cli.py +1 -1
  8. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/tests/test_instruction_integrity.py +510 -0
  9. codex_workspace_bootstrap-0.9.0/tests/test_public_repo_patterns.py +344 -0
  10. codex_workspace_bootstrap-0.8.0/tests/test_public_repo_patterns.py +0 -126
  11. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/LICENSE +0 -0
  12. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/setup.cfg +0 -0
  13. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap/__main__.py +0 -0
  14. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap/agents.py +0 -0
  15. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap/audit.py +0 -0
  16. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap/cli.py +0 -0
  17. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap/doctor.py +0 -0
  18. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap/fixes.py +0 -0
  19. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap/integrations/__init__.py +0 -0
  20. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap/integrations/github.py +0 -0
  21. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap/integrations/github_actions_worker.py +0 -0
  22. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap/integrations/github_app.py +0 -0
  23. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap/integrations/github_app_service.py +0 -0
  24. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap/integrations/github_checkout.py +0 -0
  25. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap/integrations/github_cloud_run.py +0 -0
  26. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap/integrations/github_delivery.py +0 -0
  27. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap/integrations/github_manifest.py +0 -0
  28. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap/integrations/github_runtime.py +0 -0
  29. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap/integrations/github_webhook.py +0 -0
  30. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap/preflight.py +0 -0
  31. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap/sarif.py +0 -0
  32. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap.egg-info/SOURCES.txt +0 -0
  33. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap.egg-info/dependency_links.txt +0 -0
  34. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap.egg-info/entry_points.txt +0 -0
  35. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap.egg-info/top_level.txt +0 -0
  36. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/tests/test_audit.py +0 -0
  37. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/tests/test_doctor.py +0 -0
  38. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/tests/test_github_actions_worker.py +0 -0
  39. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/tests/test_github_app_registration.py +0 -0
  40. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/tests/test_github_app_service.py +0 -0
  41. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/tests/test_github_checkout.py +0 -0
  42. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/tests/test_github_cloud_run.py +0 -0
  43. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/tests/test_github_delivery.py +0 -0
  44. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/tests/test_github_integration.py +0 -0
  45. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/tests/test_github_manifest.py +0 -0
  46. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/tests/test_github_runtime.py +0 -0
  47. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/tests/test_github_webhook.py +0 -0
  48. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/tests/test_preflight.py +0 -0
  49. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/tests/test_release_consistency.py +0 -0
  50. {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.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.8.0
3
+ Version: 0.9.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
@@ -288,7 +288,7 @@ The reusable Action is published on GitHub Marketplace.
288
288
  - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
289
289
  with:
290
290
  python-version: "3.13"
291
- - uses: kohli217/codex-workspace-bootstrap@v0.8.0
291
+ - uses: kohli217/codex-workspace-bootstrap@v0.9.0
292
292
  with:
293
293
  path: .
294
294
  strict: "true"
@@ -304,9 +304,11 @@ Read-only evaluations against real public repositories are documented in [docs/P
304
304
 
305
305
  ## GitHub App
306
306
 
307
+ **Public App:** [CWB Preflight](https://github.com/apps/cwb-preflight) · [Install](https://github.com/apps/cwb-preflight/installations/new)
308
+
307
309
  For public OSS repositories, CWB can also run as a self-hosted GitHub App. A verified `push` or `pull_request` event triggers repository-only preflight against the exact event revision and publishes a **CWB Preflight** Check Run on that commit.
308
310
 
309
- The preferred zero-cost development deployment uses:
311
+ The verified zero-cost public-OSS deployment uses:
310
312
 
311
313
  ```text
312
314
  GitHub App
@@ -320,7 +322,9 @@ This path does not require a Google Cloud billing account. It is intentionally *
320
322
 
321
323
  The App keeps the minimum repository permissions documented by the project: Checks read/write, Contents read-only, and Pull requests read-only.
322
324
 
323
- See [docs/GITHUB_APP.md](docs/GITHUB_APP.md) for the integration contract and [deploy/cloudflare/README.md](deploy/cloudflare/README.md) for the completely free development deployment.
325
+ The current public App has been revalidated after promotion: the renamed `CWB Preflight` App produced a successful `READY` Check Run through the full Cloudflare → Queue → GitHub Actions → installation-token → exact-revision checkout path.
326
+
327
+ See [docs/GITHUB_APP.md](docs/GITHUB_APP.md) for the integration contract and [deploy/cloudflare/README.md](deploy/cloudflare/README.md) for the completely free deployment.
324
328
 
325
329
  ## Integration boundary
326
330
 
@@ -347,7 +351,7 @@ The intent is to complement those tools, not replace them.
347
351
  - Suspected secret files are not opened or printed by the filename-risk check.
348
352
  - Repository contents are not sent to a remote AI service by the core audit.
349
353
  - Instruction files are read locally for deterministic linting; extracted commands are never executed by the integrity lint.
350
- - Repository instruction/configuration and project-marker inputs are not trusted through symbolic links.
354
+ - Repository instruction/configuration and project-marker contents are not trusted through symbolic links. CWB may recognize the exact lexical aliases `CLAUDE.md -> AGENTS.md` and `GEMINI.md -> AGENTS.md` (or `./AGENTS.md`) only when the sibling `AGENTS.md` is a regular file; linting reads that regular file directly rather than following either link. Gemini aliases are recognized only when `GEMINI.md` is an effective Gemini context filename.
351
355
  - `init-agents` and automatic fixes refuse symlinked `AGENTS.md` targets rather than writing through them.
352
356
  - Existing regular `AGENTS.md` files are protected unless overwrite is explicit.
353
357
  - A passing preflight is evidence about the checks performed, **not a security guarantee**.
@@ -381,7 +385,7 @@ py -m pip install codex-workspace-bootstrap
381
385
  Pinned GitHub release artifact:
382
386
 
383
387
  ```powershell
384
- py -m pip install "https://github.com/kohli217/codex-workspace-bootstrap/releases/download/v0.8.0/codex_workspace_bootstrap-0.8.0-py3-none-any.whl"
388
+ py -m pip install "https://github.com/kohli217/codex-workspace-bootstrap/releases/download/v0.9.0/codex_workspace_bootstrap-0.9.0-py3-none-any.whl"
385
389
  ```
386
390
 
387
391
  ## Development
@@ -260,7 +260,7 @@ The reusable Action is published on GitHub Marketplace.
260
260
  - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
261
261
  with:
262
262
  python-version: "3.13"
263
- - uses: kohli217/codex-workspace-bootstrap@v0.8.0
263
+ - uses: kohli217/codex-workspace-bootstrap@v0.9.0
264
264
  with:
265
265
  path: .
266
266
  strict: "true"
@@ -276,9 +276,11 @@ Read-only evaluations against real public repositories are documented in [docs/P
276
276
 
277
277
  ## GitHub App
278
278
 
279
+ **Public App:** [CWB Preflight](https://github.com/apps/cwb-preflight) · [Install](https://github.com/apps/cwb-preflight/installations/new)
280
+
279
281
  For public OSS repositories, CWB can also run as a self-hosted GitHub App. A verified `push` or `pull_request` event triggers repository-only preflight against the exact event revision and publishes a **CWB Preflight** Check Run on that commit.
280
282
 
281
- The preferred zero-cost development deployment uses:
283
+ The verified zero-cost public-OSS deployment uses:
282
284
 
283
285
  ```text
284
286
  GitHub App
@@ -292,7 +294,9 @@ This path does not require a Google Cloud billing account. It is intentionally *
292
294
 
293
295
  The App keeps the minimum repository permissions documented by the project: Checks read/write, Contents read-only, and Pull requests read-only.
294
296
 
295
- See [docs/GITHUB_APP.md](docs/GITHUB_APP.md) for the integration contract and [deploy/cloudflare/README.md](deploy/cloudflare/README.md) for the completely free development deployment.
297
+ The current public App has been revalidated after promotion: the renamed `CWB Preflight` App produced a successful `READY` Check Run through the full Cloudflare → Queue → GitHub Actions → installation-token → exact-revision checkout path.
298
+
299
+ See [docs/GITHUB_APP.md](docs/GITHUB_APP.md) for the integration contract and [deploy/cloudflare/README.md](deploy/cloudflare/README.md) for the completely free deployment.
296
300
 
297
301
  ## Integration boundary
298
302
 
@@ -319,7 +323,7 @@ The intent is to complement those tools, not replace them.
319
323
  - Suspected secret files are not opened or printed by the filename-risk check.
320
324
  - Repository contents are not sent to a remote AI service by the core audit.
321
325
  - Instruction files are read locally for deterministic linting; extracted commands are never executed by the integrity lint.
322
- - Repository instruction/configuration and project-marker inputs are not trusted through symbolic links.
326
+ - Repository instruction/configuration and project-marker contents are not trusted through symbolic links. CWB may recognize the exact lexical aliases `CLAUDE.md -> AGENTS.md` and `GEMINI.md -> AGENTS.md` (or `./AGENTS.md`) only when the sibling `AGENTS.md` is a regular file; linting reads that regular file directly rather than following either link. Gemini aliases are recognized only when `GEMINI.md` is an effective Gemini context filename.
323
327
  - `init-agents` and automatic fixes refuse symlinked `AGENTS.md` targets rather than writing through them.
324
328
  - Existing regular `AGENTS.md` files are protected unless overwrite is explicit.
325
329
  - A passing preflight is evidence about the checks performed, **not a security guarantee**.
@@ -353,7 +357,7 @@ py -m pip install codex-workspace-bootstrap
353
357
  Pinned GitHub release artifact:
354
358
 
355
359
  ```powershell
356
- py -m pip install "https://github.com/kohli217/codex-workspace-bootstrap/releases/download/v0.8.0/codex_workspace_bootstrap-0.8.0-py3-none-any.whl"
360
+ py -m pip install "https://github.com/kohli217/codex-workspace-bootstrap/releases/download/v0.9.0/codex_workspace_bootstrap-0.9.0-py3-none-any.whl"
357
361
  ```
358
362
 
359
363
  ## Development
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codex-workspace-bootstrap"
7
- version = "0.8.0"
7
+ version = "0.9.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.8.0"
3
+ __version__ = "0.9.0"
@@ -81,8 +81,8 @@ _COMMAND_PATTERNS: tuple[re.Pattern[str], ...] = (
81
81
  re.compile(r"\bpython\s+-m\s+pytest(?:\s+[^\n`]+)?", re.I),
82
82
  re.compile(r"(?<![\w.-])pytest(?:\s+[^\n`]+)?", re.I),
83
83
  re.compile(r"\bpython\s+-m\s+unittest(?:\s+[^\n`]+)?", re.I),
84
- re.compile(r"\b(?:npm|pnpm|bun)\s+(?:run\s+)?[\w:.-]+(?:\s+[^\n`]+)?", re.I),
85
- re.compile(r"\byarn\s+(?:run\s+)?[\w:.-]+(?:\s+[^\n`]+)?", re.I),
84
+ re.compile(r"\b(?:npm|pnpm|bun)\s+(?:run\s+)?[\w:./=@-]+(?:\s+[^\n`]+)?", re.I),
85
+ re.compile(r"\byarn\s+(?:run\s+)?[\w:./=@-]+(?:\s+[^\n`]+)?", re.I),
86
86
  re.compile(r"\bgo\s+test(?:\s+[^\n`]+)?", re.I),
87
87
  re.compile(r"\bcargo\s+test(?:\s+[^\n`]+)?", re.I),
88
88
  re.compile(r"(?<![\w.-])(?:make|just)\s+[\w:.-]+(?:\s+[^\n`]+)?", re.I),
@@ -308,6 +308,23 @@ def _agent_signals(root: Path) -> list[InstructionSignal]:
308
308
  return found
309
309
 
310
310
 
311
+ def _safe_sibling_instruction_alias(path: Path, target_name: str) -> Path | None:
312
+ if not path.is_symlink():
313
+ return None
314
+ try:
315
+ lexical_target = path.readlink().as_posix()
316
+ except OSError:
317
+ return None
318
+
319
+ if lexical_target not in {target_name, f"./{target_name}"}:
320
+ return None
321
+
322
+ target = path.parent / target_name
323
+ if target.is_symlink() or not target.is_file():
324
+ return None
325
+ return target
326
+
327
+
311
328
  def _hierarchical_named_signals(
312
329
  root: Path,
313
330
  filename: str,
@@ -327,8 +344,38 @@ def _hierarchical_named_signals(
327
344
  return found
328
345
 
329
346
 
347
+ def _safe_alias_signals(
348
+ root: Path,
349
+ alias_name: str,
350
+ target_name: str,
351
+ tool: str,
352
+ ) -> list[InstructionSignal]:
353
+ found: list[InstructionSignal] = []
354
+ for directory, _dirnames, filenames in _walk_repository(root):
355
+ if alias_name not in filenames:
356
+ continue
357
+ alias = directory / alias_name
358
+ if _safe_sibling_instruction_alias(alias, target_name) is None:
359
+ continue
360
+
361
+ rel = alias.relative_to(root).as_posix()
362
+ scope_path = directory.relative_to(root).as_posix()
363
+ scope = "." if scope_path == "." else scope_path
364
+ found.append(InstructionSignal(tool, rel, scope, "alias"))
365
+ return found
366
+
367
+
330
368
  def _claude_signals(root: Path) -> list[InstructionSignal]:
331
- return _hierarchical_named_signals(root, "CLAUDE.md", "Claude Code")
369
+ found = _hierarchical_named_signals(root, "CLAUDE.md", "Claude Code")
370
+ found.extend(
371
+ _safe_alias_signals(
372
+ root,
373
+ "CLAUDE.md",
374
+ "AGENTS.md",
375
+ "Claude Code",
376
+ )
377
+ )
378
+ return found
332
379
 
333
380
 
334
381
  def _gemini_context_filenames(root: Path) -> tuple[str, ...]:
@@ -375,8 +422,19 @@ def _gemini_context_filenames(root: Path) -> tuple[str, ...]:
375
422
 
376
423
  def _gemini_signals(root: Path) -> list[InstructionSignal]:
377
424
  found: list[InstructionSignal] = []
378
- for filename in _gemini_context_filenames(root):
425
+ filenames = _gemini_context_filenames(root)
426
+ for filename in filenames:
379
427
  found.extend(_hierarchical_named_signals(root, filename, "Gemini CLI"))
428
+
429
+ if "GEMINI.md" in filenames:
430
+ found.extend(
431
+ _safe_alias_signals(
432
+ root,
433
+ "GEMINI.md",
434
+ "AGENTS.md",
435
+ "Gemini CLI",
436
+ )
437
+ )
380
438
  return found
381
439
 
382
440
 
@@ -512,7 +570,20 @@ def detect_instruction_signals(root: Path) -> list[InstructionSignal]:
512
570
 
513
571
 
514
572
  def _read_instruction(root: Path, signal: InstructionSignal) -> str:
515
- return _safe_read(root / signal.path)
573
+ path = root / signal.path
574
+ if signal.kind == "alias":
575
+ alias_targets = {
576
+ ("Claude Code", "CLAUDE.md"): "AGENTS.md",
577
+ ("Gemini CLI", "GEMINI.md"): "AGENTS.md",
578
+ }
579
+ target_name = alias_targets.get((signal.tool, path.name))
580
+ if target_name is None:
581
+ return ""
582
+ target = _safe_sibling_instruction_alias(path, target_name)
583
+ if target is None:
584
+ return ""
585
+ return _safe_read(target)
586
+ return _safe_read(path)
516
587
 
517
588
 
518
589
  def _command_regions(text: str) -> list[str]:
@@ -684,12 +755,249 @@ def _repo_package_managers(root: Path, scope: str) -> set[str]:
684
755
  return set()
685
756
 
686
757
 
687
- def _script_names(root: Path, scope: str) -> set[str]:
758
+ def _script_names(root: Path, scope: str) -> set[str] | None:
688
759
  for directory in _directory_chain_to_root(root, scope):
689
- _managers, scripts = _package_evidence_at(directory)
690
- if scripts or (directory / "package.json").is_file():
760
+ package_path = directory / "package.json"
761
+ if package_path.is_file() and not package_path.is_symlink():
762
+ _managers, scripts = _package_evidence_at(directory)
691
763
  return scripts
692
- return set()
764
+ return None
765
+
766
+
767
+ def _workspace_target_for_command(command: str) -> tuple[bool, str | None]:
768
+ tokens = _package_command_tokens(command.strip())
769
+ if not tokens:
770
+ return False, None
771
+
772
+ manager = tokens[0].lower()
773
+ if manager not in {"npm", "pnpm", "yarn", "bun"}:
774
+ return False, None
775
+
776
+ if manager == "yarn" and len(tokens) >= 3 and tokens[1] == "workspace":
777
+ return True, tokens[2]
778
+
779
+ if manager == "npm":
780
+ targets: list[str] = []
781
+ index = 1
782
+ while index < len(tokens):
783
+ token = tokens[index]
784
+ if token == "--":
785
+ break
786
+ if token in {"--workspace", "-w"}:
787
+ if index + 1 < len(tokens):
788
+ targets.append(tokens[index + 1])
789
+ else:
790
+ targets.append("")
791
+ index += 2
792
+ continue
793
+ if token.startswith("--workspace="):
794
+ targets.append(token[len("--workspace="):])
795
+ index += 1
796
+ continue
797
+ if token.startswith("-w="):
798
+ targets.append(token[len("-w="):])
799
+ index += 1
800
+ continue
801
+ index += 1
802
+
803
+ if not targets:
804
+ return False, None
805
+ nonempty = [target for target in targets if target]
806
+ if len(nonempty) != 1 or len(targets) != 1:
807
+ return True, None
808
+ return True, nonempty[0]
809
+
810
+ target_options = {"--filter", "-F", "--workspace", "-w"}
811
+ target_long_options = {"--filter", "--workspace"}
812
+ directory_options = {"--dir", "-C", "--prefix", "--cwd"}
813
+ directory_long_options = {"--dir", "--prefix", "--cwd"}
814
+ targets: list[str] = []
815
+
816
+ index = 1
817
+ while index < len(tokens):
818
+ token = tokens[index]
819
+ if token in target_options:
820
+ if index + 1 < len(tokens):
821
+ targets.append(tokens[index + 1])
822
+ else:
823
+ targets.append("")
824
+ index += 2
825
+ continue
826
+
827
+ matched_inline = False
828
+ for option in target_long_options:
829
+ prefix = f"{option}="
830
+ if token.startswith(prefix):
831
+ targets.append(token[len(prefix):])
832
+ matched_inline = True
833
+ break
834
+ if matched_inline:
835
+ index += 1
836
+ continue
837
+
838
+ if token in directory_options:
839
+ index += 2
840
+ continue
841
+ if any(token.startswith(f"{option}=") for option in directory_long_options):
842
+ index += 1
843
+ continue
844
+
845
+ if not token.startswith("-"):
846
+ break
847
+ index += 1
848
+
849
+ if not targets:
850
+ return False, None
851
+
852
+ nonempty = [target for target in targets if target]
853
+ if len(nonempty) != 1 or len(targets) != 1:
854
+ return True, None
855
+ return True, nonempty[0]
856
+
857
+
858
+ def _directory_target_for_command(command: str) -> tuple[bool, str | None]:
859
+ tokens = _package_command_tokens(command.strip())
860
+ if not tokens:
861
+ return False, None
862
+
863
+ manager = tokens[0].lower()
864
+ if manager not in {"npm", "pnpm", "yarn", "bun"}:
865
+ return False, None
866
+
867
+ supported_options: set[str]
868
+ supported_long_options: set[str]
869
+ if manager == "pnpm":
870
+ supported_options = {"--dir", "-C"}
871
+ supported_long_options = {"--dir"}
872
+ elif manager == "npm":
873
+ supported_options = {"--prefix"}
874
+ supported_long_options = {"--prefix"}
875
+ elif manager in {"yarn", "bun"}:
876
+ supported_options = {"--cwd"}
877
+ supported_long_options = {"--cwd"}
878
+ else:
879
+ return False, None
880
+
881
+ targets: list[str] = []
882
+ index = 1
883
+ while index < len(tokens):
884
+ token = tokens[index]
885
+ if token in supported_options:
886
+ if index + 1 < len(tokens):
887
+ targets.append(tokens[index + 1])
888
+ else:
889
+ targets.append("")
890
+ index += 2
891
+ continue
892
+
893
+ matched_inline = False
894
+ for option in supported_long_options:
895
+ prefix = f"{option}="
896
+ if token.startswith(prefix):
897
+ targets.append(token[len(prefix):])
898
+ matched_inline = True
899
+ break
900
+ if matched_inline:
901
+ index += 1
902
+ continue
903
+
904
+ if token in {"--filter", "-F", "--workspace", "-w"}:
905
+ index += 2
906
+ continue
907
+ if token.startswith(("--filter=", "--workspace=")):
908
+ index += 1
909
+ continue
910
+
911
+ if manager == "yarn" and token == "workspace":
912
+ break
913
+ if not token.startswith("-"):
914
+ break
915
+ index += 1
916
+
917
+ if not targets:
918
+ return False, None
919
+ nonempty = [target for target in targets if target]
920
+ if len(nonempty) != 1 or len(targets) != 1:
921
+ return True, None
922
+ return True, nonempty[0]
923
+
924
+
925
+ def _safe_repository_directory(root: Path, target: str) -> Path | None:
926
+ value = target.strip().replace("\\", "/")
927
+ if not value or value.startswith("/") or re.match(r"^[A-Za-z]:/", value):
928
+ return None
929
+
930
+ parts = [part for part in value.split("/") if part not in {"", "."}]
931
+ if any(part == ".." for part in parts):
932
+ return None
933
+
934
+ candidate = root.joinpath(*parts) if parts else root
935
+ try:
936
+ candidate.resolve().relative_to(root.resolve())
937
+ except (OSError, ValueError):
938
+ return None
939
+ return candidate
940
+
941
+
942
+ def _directory_script_names(root: Path, target: str) -> set[str] | None:
943
+ directory = _safe_repository_directory(root, target)
944
+ if directory is None:
945
+ return None
946
+
947
+ package_path = directory / "package.json"
948
+ if not package_path.is_file() or package_path.is_symlink():
949
+ return None
950
+
951
+ package = _package_json(package_path)
952
+ raw_scripts = package.get("scripts")
953
+ if not isinstance(raw_scripts, dict):
954
+ return set()
955
+ return {
956
+ str(name)
957
+ for name, value in raw_scripts.items()
958
+ if isinstance(name, str) and isinstance(value, str)
959
+ }
960
+
961
+
962
+ def _workspace_script_names(root: Path, target: str) -> set[str] | None:
963
+ matches: list[set[str]] = []
964
+ for directory, _dirnames, filenames in _walk_repository(root):
965
+ if "package.json" not in filenames:
966
+ continue
967
+ package_path = directory / "package.json"
968
+ package = _package_json(package_path)
969
+ if package.get("name") != target:
970
+ continue
971
+ raw_scripts = package.get("scripts")
972
+ if isinstance(raw_scripts, dict):
973
+ scripts = {
974
+ str(name)
975
+ for name, value in raw_scripts.items()
976
+ if isinstance(name, str) and isinstance(value, str)
977
+ }
978
+ else:
979
+ scripts = set()
980
+ matches.append(scripts)
981
+
982
+ if len(matches) != 1:
983
+ return None
984
+ return matches[0]
985
+
986
+
987
+ def _script_names_for_command(root: Path, scope: str, command: str) -> set[str] | None:
988
+ has_workspace_target, workspace_target = _workspace_target_for_command(command)
989
+ if has_workspace_target:
990
+ if workspace_target is None:
991
+ return None
992
+ return _workspace_script_names(root, workspace_target)
993
+
994
+ has_directory_target, directory_target = _directory_target_for_command(command)
995
+ if has_directory_target:
996
+ if directory_target is None:
997
+ return None
998
+ return _directory_script_names(root, directory_target)
999
+
1000
+ return _script_names(root, scope)
693
1001
 
694
1002
 
695
1003
  def _manager_for_command(command: str) -> str | None:
@@ -876,7 +1184,6 @@ def lint_instructions(
876
1184
  instruction_managers[path] = managers
877
1185
 
878
1186
  repo_managers = _repo_package_managers(root, signal.scope)
879
- scripts = _script_names(root, signal.scope)
880
1187
 
881
1188
  if len(repo_managers) == 1:
882
1189
  expected = next(iter(repo_managers))
@@ -893,23 +1200,25 @@ def lint_instructions(
893
1200
  )
894
1201
  )
895
1202
 
896
- if scripts:
897
- for command in commands:
898
- parsed = _script_for_command(command)
899
- if not parsed:
900
- continue
901
- _manager, script, explicit_run = parsed
902
- if script not in scripts and (script == "test" or explicit_run):
903
- findings.append(
904
- InstructionFinding(
905
- "missing-package-script",
906
- "warning",
907
- f"{path} references '{command}', but the nearest package.json for scope '{signal.scope}' has no '{script}' script.",
908
- (path,),
909
- (command,),
910
- signal.scope,
911
- )
1203
+ for command in commands:
1204
+ parsed = _script_for_command(command)
1205
+ if not parsed:
1206
+ continue
1207
+ scripts = _script_names_for_command(root, signal.scope, command)
1208
+ if scripts is None:
1209
+ continue
1210
+ _manager, script, explicit_run = parsed
1211
+ if script not in scripts and (script == "test" or explicit_run):
1212
+ findings.append(
1213
+ InstructionFinding(
1214
+ "missing-package-script",
1215
+ "warning",
1216
+ f"{path} references '{command}', but the targeted or nearest package.json for scope '{signal.scope}' has no '{script}' script.",
1217
+ (path,),
1218
+ (command,),
1219
+ signal.scope,
912
1220
  )
1221
+ )
913
1222
 
914
1223
  for scope, per_file in _same_scope_groups(signals, instruction_managers).items():
915
1224
  manager_sets = {manager for managers in per_file.values() for manager in managers}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-workspace-bootstrap
3
- Version: 0.8.0
3
+ Version: 0.9.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
@@ -288,7 +288,7 @@ The reusable Action is published on GitHub Marketplace.
288
288
  - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
289
289
  with:
290
290
  python-version: "3.13"
291
- - uses: kohli217/codex-workspace-bootstrap@v0.8.0
291
+ - uses: kohli217/codex-workspace-bootstrap@v0.9.0
292
292
  with:
293
293
  path: .
294
294
  strict: "true"
@@ -304,9 +304,11 @@ Read-only evaluations against real public repositories are documented in [docs/P
304
304
 
305
305
  ## GitHub App
306
306
 
307
+ **Public App:** [CWB Preflight](https://github.com/apps/cwb-preflight) · [Install](https://github.com/apps/cwb-preflight/installations/new)
308
+
307
309
  For public OSS repositories, CWB can also run as a self-hosted GitHub App. A verified `push` or `pull_request` event triggers repository-only preflight against the exact event revision and publishes a **CWB Preflight** Check Run on that commit.
308
310
 
309
- The preferred zero-cost development deployment uses:
311
+ The verified zero-cost public-OSS deployment uses:
310
312
 
311
313
  ```text
312
314
  GitHub App
@@ -320,7 +322,9 @@ This path does not require a Google Cloud billing account. It is intentionally *
320
322
 
321
323
  The App keeps the minimum repository permissions documented by the project: Checks read/write, Contents read-only, and Pull requests read-only.
322
324
 
323
- See [docs/GITHUB_APP.md](docs/GITHUB_APP.md) for the integration contract and [deploy/cloudflare/README.md](deploy/cloudflare/README.md) for the completely free development deployment.
325
+ The current public App has been revalidated after promotion: the renamed `CWB Preflight` App produced a successful `READY` Check Run through the full Cloudflare → Queue → GitHub Actions → installation-token → exact-revision checkout path.
326
+
327
+ See [docs/GITHUB_APP.md](docs/GITHUB_APP.md) for the integration contract and [deploy/cloudflare/README.md](deploy/cloudflare/README.md) for the completely free deployment.
324
328
 
325
329
  ## Integration boundary
326
330
 
@@ -347,7 +351,7 @@ The intent is to complement those tools, not replace them.
347
351
  - Suspected secret files are not opened or printed by the filename-risk check.
348
352
  - Repository contents are not sent to a remote AI service by the core audit.
349
353
  - Instruction files are read locally for deterministic linting; extracted commands are never executed by the integrity lint.
350
- - Repository instruction/configuration and project-marker inputs are not trusted through symbolic links.
354
+ - Repository instruction/configuration and project-marker contents are not trusted through symbolic links. CWB may recognize the exact lexical aliases `CLAUDE.md -> AGENTS.md` and `GEMINI.md -> AGENTS.md` (or `./AGENTS.md`) only when the sibling `AGENTS.md` is a regular file; linting reads that regular file directly rather than following either link. Gemini aliases are recognized only when `GEMINI.md` is an effective Gemini context filename.
351
355
  - `init-agents` and automatic fixes refuse symlinked `AGENTS.md` targets rather than writing through them.
352
356
  - Existing regular `AGENTS.md` files are protected unless overwrite is explicit.
353
357
  - A passing preflight is evidence about the checks performed, **not a security guarantee**.
@@ -381,7 +385,7 @@ py -m pip install codex-workspace-bootstrap
381
385
  Pinned GitHub release artifact:
382
386
 
383
387
  ```powershell
384
- py -m pip install "https://github.com/kohli217/codex-workspace-bootstrap/releases/download/v0.8.0/codex_workspace_bootstrap-0.8.0-py3-none-any.whl"
388
+ py -m pip install "https://github.com/kohli217/codex-workspace-bootstrap/releases/download/v0.9.0/codex_workspace_bootstrap-0.9.0-py3-none-any.whl"
385
389
  ```
386
390
 
387
391
  ## Development
@@ -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.8.0" in capsys.readouterr().out
104
+ assert "codex-workspace-bootstrap 0.9.0" in capsys.readouterr().out
105
105
 
106
106
 
107
107
  def test_audit_writes_sarif(tmp_path: Path) -> None: